mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-03 00:52:32 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
7.3 KiB
7.3 KiB
Common Anti-Patterns — Before/After
1. Missing help_text
# Before
class ActionSerializer(serializers.ModelSerializer):
name = serializers.CharField()
steps = serializers.ListField(child=ActionStepSerializer())
deleted = serializers.BooleanField()
# After
class ActionSerializer(serializers.ModelSerializer):
name = serializers.CharField(
help_text="Human-readable name for the action. Shown in the UI and used for filtering."
)
steps = serializers.ListField(
child=ActionStepSerializer(),
help_text="Ordered list of match conditions. An event matches if any step matches.",
)
deleted = serializers.BooleanField(
help_text="Whether the action has been soft-deleted. Deleted actions are excluded from queries.",
)
2. Bare JSONField
# Before — generates generic object, agents can't construct valid input
class InsightsFunctionSerializer(serializers.ModelSerializer):
inputs = serializers.JSONField(required=False)
# After — typed via Pydantic model
from pydantic import BaseModel
class InsightsFunctionInputs(BaseModel):
name: str
value: str | int | bool | None = None
secret: bool = False
@extend_schema_field(InsightsFunctionInputs) # type: ignore[arg-type]
class InsightsFunctionInputsField(serializers.JSONField):
pass
class InsightsFunctionSerializer(serializers.ModelSerializer):
inputs = InsightsFunctionInputsField(
required=False,
help_text="Input configuration for the Script function. Each input has a name, value, and secret flag.",
)
3. Bare ListField without child
# Before — generates z.array(z.unknown())
class BatchSerializer(serializers.Serializer):
ids = serializers.ListField(required=True)
# After — generates z.array(z.string())
class BatchSerializer(serializers.Serializer):
ids = serializers.ListField(
child=serializers.CharField(),
min_length=1,
max_length=100,
help_text="List of resource IDs to process.",
)
4. Plain ViewSet with manual validation and no schema
# Before — drf-spectacular discovers nothing
class LLMProxyViewSet(viewsets.ViewSet):
def completion(self, request, *args, **kwargs):
serializer = LLMProxyCompletionSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=400)
data = serializer.validated_data
# ...
# After — @validated_request handles validation + OpenAPI
from drf_spectacular.utils import OpenApiResponse
from insights.api.mixins import validated_request
class LLMProxyViewSet(viewsets.ViewSet):
@validated_request(
request_serializer=LLMProxyCompletionSerializer,
responses={200: OpenApiResponse(response=LLMProxyResponseSerializer, description="LLM completion response")},
summary="LLM completion",
description="Proxy a completion request to the configured LLM provider.",
)
def completion(self, request, *args, **kwargs):
data = request.validated_data
# ...
5. @extend_schema on class instead of method
# Before — decorator on class does nothing for APIView
@extend_schema(request=MySerializer)
class MyView(APIView):
def post(self, request):
...
# After — on the actual handler
class MyView(APIView):
@extend_schema(
request=MySerializer,
responses={201: MyResponseSerializer},
summary="Create a thing",
)
def post(self, request):
...
6. Generic error response types
# Before — z.object({}) for errors
@extend_schema(
responses={
200: DashboardSerializer,
400: OpenApiTypes.OBJECT,
500: OpenApiTypes.OBJECT,
},
)
# After — descriptive responses
@extend_schema(
responses={
200: DashboardSerializer,
400: OpenApiResponse(description="Validation error — field-level error details"),
404: OpenApiResponse(description="Dashboard not found"),
},
)
7. Wrong 204 No Content response annotation
# Before — OpenApiTypes.NONE produces {"schema": null}, which is invalid OpenAPI
# and breaks Orval validation
@extend_schema(responses={204: OpenApiTypes.NONE})
def delete(self, request, **kwargs):
...
return Response(status=status.HTTP_204_NO_CONTENT)
# After — None means "no response body", which is correct for 204
@extend_schema(responses={204: None})
def delete(self, request, **kwargs):
...
return Response(status=status.HTTP_204_NO_CONTENT)
8. Unannotated SerializerMethodField
# Before — return type is unknown in OpenAPI
class TeamSerializer(serializers.ModelSerializer):
member_count = serializers.SerializerMethodField()
def get_member_count(self, obj):
return obj.members.count()
# After — return type declared
from drf_spectacular.utils import extend_schema_field
class TeamSerializer(serializers.ModelSerializer):
member_count = serializers.SerializerMethodField(
help_text="Number of members in this team"
)
@extend_schema_field(serializers.IntegerField())
def get_member_count(self, obj):
return obj.members.count()
9. Custom @action without schema annotation
# Before — MCP tool gets zero parameters
@action(detail=False, methods=["post"], url_path="evaluate")
def evaluate(self, request, **kwargs):
serializer = EvaluateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
...
# After — schema declared
@extend_schema(
request=EvaluateRequestSerializer,
responses={200: EvaluateResponseSerializer},
summary="Run evaluation",
description="Execute an evaluation run against the specified dataset.",
)
@action(detail=False, methods=["post"], url_path="evaluate")
def evaluate(self, request, **kwargs):
serializer = EvaluateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
...
10. CharField where ChoiceField is appropriate
# Before — any string accepted, agents guess at values
status = serializers.CharField(help_text="The status to filter by")
# After — valid values enumerated
status = serializers.ChoiceField(
choices=["active", "archived", "deleted"],
help_text="Filter by resource status.",
)
11. Same serializer for read and write with computed fields
# Before — computed fields cause validation errors on write
class ExperimentSerializer(serializers.ModelSerializer):
results = serializers.JSONField(read_only=True) # Computed
created_by = UserBasicSerializer(read_only=True) # Computed
class Meta:
model = Experiment
fields = "__all__"
# After — separate serializers
class ExperimentWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Experiment
fields = ["name", "description", "feature_flag_key", "filters"]
class ExperimentReadSerializer(serializers.ModelSerializer):
results = serializers.JSONField(read_only=True)
created_by = UserBasicSerializer(read_only=True)
class Meta:
model = Experiment
fields = "__all__"
# In the viewset:
def get_serializer_class(self):
if self.action in ("create", "update", "partial_update"):
return ExperimentWriteSerializer
return ExperimentReadSerializer