-
Notifications
You must be signed in to change notification settings - Fork 14
Endpoint for stats about verified occurrences #1307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
165ae9b
7b1660c
3110418
ba9c901
5b1bde7
e050a1f
f49c9ca
da2a232
7ba8689
6ad1885
6f51da5
7c144b0
34aace5
36cc677
b74b3cd
2c65cce
336c1fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,7 +31,7 @@ | |||||||||||||||||||||||||||||
| from ami.base.views import ProjectMixin | ||||||||||||||||||||||||||||||
| from ami.main.api.schemas import limit_doc_param, project_id_doc_param | ||||||||||||||||||||||||||||||
| from ami.main.api.serializers import TagSerializer | ||||||||||||||||||||||||||||||
| from ami.main.models_future.occurrence import top_identifiers_for_project | ||||||||||||||||||||||||||||||
| from ami.main.models_future.occurrence import model_agreement_for_project, top_identifiers_for_project | ||||||||||||||||||||||||||||||
| from ami.utils.requests import get_default_classification_threshold | ||||||||||||||||||||||||||||||
| from ami.utils.storages import ConnectionTestResult | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -55,6 +55,7 @@ | |||||||||||||||||||||||||||||
| Tag, | ||||||||||||||||||||||||||||||
| TaxaList, | ||||||||||||||||||||||||||||||
| Taxon, | ||||||||||||||||||||||||||||||
| TaxonRank, | ||||||||||||||||||||||||||||||
| User, | ||||||||||||||||||||||||||||||
| update_detection_counts, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
@@ -71,6 +72,7 @@ | |||||||||||||||||||||||||||||
| EventSerializer, | ||||||||||||||||||||||||||||||
| EventTimelineSerializer, | ||||||||||||||||||||||||||||||
| IdentificationSerializer, | ||||||||||||||||||||||||||||||
| ModelAgreementSerializer, | ||||||||||||||||||||||||||||||
| OccurrenceListSerializer, | ||||||||||||||||||||||||||||||
| OccurrenceSerializer, | ||||||||||||||||||||||||||||||
| PageListSerializer, | ||||||||||||||||||||||||||||||
|
|
@@ -1202,6 +1204,24 @@ def filter_queryset(self, request, queryset, view): | |||||||||||||||||||||||||||||
| return queryset | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| OCCURRENCE_FILTER_BACKENDS = ( | ||||||||||||||||||||||||||||||
| CustomOccurrenceDeterminationFilter, | ||||||||||||||||||||||||||||||
| OccurrenceCollectionFilter, | ||||||||||||||||||||||||||||||
| OccurrenceAlgorithmFilter, | ||||||||||||||||||||||||||||||
| OccurrenceDateFilter, | ||||||||||||||||||||||||||||||
| OccurrenceVerified, | ||||||||||||||||||||||||||||||
| OccurrenceVerifiedByMeFilter, | ||||||||||||||||||||||||||||||
| OccurrenceTaxaListFilter, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| OCCURRENCE_FILTERSET_FIELDS = ( | ||||||||||||||||||||||||||||||
| "event", | ||||||||||||||||||||||||||||||
| "deployment", | ||||||||||||||||||||||||||||||
| "determination__rank", | ||||||||||||||||||||||||||||||
| "detections__source_image", | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class OccurrenceViewSet(DefaultViewSet, ProjectMixin): | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| API endpoint that allows occurrences to be viewed or edited. | ||||||||||||||||||||||||||||||
|
|
@@ -1211,22 +1231,8 @@ class OccurrenceViewSet(DefaultViewSet, ProjectMixin): | |||||||||||||||||||||||||||||
| queryset = Occurrence.objects.all() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| serializer_class = OccurrenceSerializer | ||||||||||||||||||||||||||||||
| # filter_backends = [CustomDeterminationFilter, DjangoFilterBackend, NullsLastOrderingFilter, SearchFilter] | ||||||||||||||||||||||||||||||
| filter_backends = DefaultViewSetMixin.filter_backends + [ | ||||||||||||||||||||||||||||||
| CustomOccurrenceDeterminationFilter, | ||||||||||||||||||||||||||||||
| OccurrenceCollectionFilter, | ||||||||||||||||||||||||||||||
| OccurrenceAlgorithmFilter, | ||||||||||||||||||||||||||||||
| OccurrenceDateFilter, | ||||||||||||||||||||||||||||||
| OccurrenceVerified, | ||||||||||||||||||||||||||||||
| OccurrenceVerifiedByMeFilter, | ||||||||||||||||||||||||||||||
| OccurrenceTaxaListFilter, | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| filterset_fields = [ | ||||||||||||||||||||||||||||||
| "event", | ||||||||||||||||||||||||||||||
| "deployment", | ||||||||||||||||||||||||||||||
| "determination__rank", | ||||||||||||||||||||||||||||||
| "detections__source_image", | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| filter_backends = DefaultViewSetMixin.filter_backends + list(OCCURRENCE_FILTER_BACKENDS) | ||||||||||||||||||||||||||||||
| filterset_fields = list(OCCURRENCE_FILTERSET_FIELDS) | ||||||||||||||||||||||||||||||
| ordering_fields = [ | ||||||||||||||||||||||||||||||
| "created_at", | ||||||||||||||||||||||||||||||
| "updated_at", | ||||||||||||||||||||||||||||||
|
|
@@ -1324,6 +1330,11 @@ class OccurrenceStatsViewSet(viewsets.GenericViewSet, ProjectMixin): | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| permission_classes = [IsActiveStaffOrReadOnly] | ||||||||||||||||||||||||||||||
| require_project = True | ||||||||||||||||||||||||||||||
| # Filter machinery for actions that opt into `self.filter_queryset(...)`. | ||||||||||||||||||||||||||||||
| # `top_identifiers` doesn't call it, so its behavior is unchanged. | ||||||||||||||||||||||||||||||
| queryset = Occurrence.objects.none() | ||||||||||||||||||||||||||||||
| filter_backends = [DjangoFilterBackend, *OCCURRENCE_FILTER_BACKENDS] | ||||||||||||||||||||||||||||||
| filterset_fields = list(OCCURRENCE_FILTERSET_FIELDS) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
mihow marked this conversation as resolved.
|
||||||||||||||||||||||||||||||
| @extend_schema( | ||||||||||||||||||||||||||||||
| parameters=[project_id_doc_param, limit_doc_param], | ||||||||||||||||||||||||||||||
|
|
@@ -1354,6 +1365,48 @@ def top_identifiers(self, request): | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| return Response(serializer.data) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @extend_schema( | ||||||||||||||||||||||||||||||
| parameters=[project_id_doc_param], | ||||||||||||||||||||||||||||||
| responses=ModelAgreementSerializer, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| @action(detail=False, methods=["get"], url_path="model-agreement") | ||||||||||||||||||||||||||||||
| def model_agreement(self, request): | ||||||||||||||||||||||||||||||
| """Verified / human↔model agreement rates over the filtered occurrence set. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Accepts every query param the `/occurrences/` list endpoint accepts. | ||||||||||||||||||||||||||||||
| Reuses `apply_default_filters` so `apply_defaults=false` bypasses | ||||||||||||||||||||||||||||||
| project default taxa lists + score thresholds. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Optional ?agreement_coarsest_rank=<RANK> adds `agreed_coarser_rank_*` | ||||||||||||||||||||||||||||||
| counts — LCAs at the given rank or deeper. Valid values: any | ||||||||||||||||||||||||||||||
| TaxonRank name (FAMILY, GENUS, etc.); invalid → 400. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| project = self.get_active_project() | ||||||||||||||||||||||||||||||
| assert project is not None # require_project=True guarantees this | ||||||||||||||||||||||||||||||
| if not Project.objects.visible_for_user(request.user).filter(pk=project.pk).exists(): | ||||||||||||||||||||||||||||||
| raise NotFound("Project not found.") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| coarsest_rank_param = request.query_params.get("agreement_coarsest_rank") | ||||||||||||||||||||||||||||||
| coarsest_rank = None | ||||||||||||||||||||||||||||||
| if coarsest_rank_param: | ||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| coarsest_rank = TaxonRank[coarsest_rank_param.upper()] | ||||||||||||||||||||||||||||||
| except KeyError: | ||||||||||||||||||||||||||||||
| valid = ", ".join(r.name for r in TaxonRank if r.name != "UNKNOWN") | ||||||||||||||||||||||||||||||
| raise api_exceptions.ValidationError( | ||||||||||||||||||||||||||||||
| {"agreement_coarsest_rank": f"Invalid rank '{coarsest_rank_param}'. Must be one of: {valid}."} | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| if coarsest_rank == TaxonRank.UNKNOWN: | ||||||||||||||||||||||||||||||
| raise api_exceptions.ValidationError( | ||||||||||||||||||||||||||||||
| {"agreement_coarsest_rank": "UNKNOWN is not a valid threshold rank."} | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
Comment on lines
+1389
to
+1402
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject blank Line 1406 only validates truthy input, so 🛠️ Proposed fix- coarsest_rank_param = request.query_params.get("agreement_coarsest_rank")
coarsest_rank = None
- if coarsest_rank_param:
+ if "agreement_coarsest_rank" in request.query_params:
+ coarsest_rank_param = request.query_params.get("agreement_coarsest_rank")
+ if not coarsest_rank_param:
+ raise api_exceptions.ValidationError(
+ {"agreement_coarsest_rank": "This parameter cannot be blank."}
+ )
try:
coarsest_rank = TaxonRank[coarsest_rank_param.upper()]
except KeyError:
valid = ", ".join(r.name for r in TaxonRank if r.name != "UNKNOWN")
raise api_exceptions.ValidationError(🧰 Tools🪛 Ruff (0.15.14)[warning] 1411-1413: Within an (B904) 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we also use proper validation here? add Enum support to SingleParamSerializer if need be, or pydantic? Or a DRF serializer? antenna/docs/claude/reference/api-stats-pattern.md Lines 132 to 145 in f585ddc
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look at SingleParamSerializer implementation
ast-grep --pattern 'class SingleParamSerializer$$$'
echo "---"
rg -n "SingleParamSerializer" ami/base/serializers.py -A 30 | head -80
echo "=== api-stats-pattern.md ==="
cat docs/claude/reference/api-stats-pattern.md | head -200
echo "=== TaxonRank enum ==="
rg -n "class TaxonRank" -A 20Length of output: 14976 🏁 Script executed: # Confirm current imports in ami/base/serializers.py to ensure EnumField won't conflict
rg -n "^from|^import" ami/base/serializers.py | head -30
# Confirm EnumField is not already defined
rg -n "EnumField" ami/base/serializers.pyLength of output: 804 Good, no conflicts. Here's the full plan: Task: Proper enum validation via
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| base_qs = Occurrence.objects.filter(project=project).valid().apply_default_filters(project, request) | ||||||||||||||||||||||||||||||
| filtered_qs = self.filter_queryset(base_qs) | ||||||||||||||||||||||||||||||
| payload = model_agreement_for_project(filtered_qs, coarsest_rank=coarsest_rank) | ||||||||||||||||||||||||||||||
| payload["project_id"] = project.pk | ||||||||||||||||||||||||||||||
| return Response(ModelAgreementSerializer(payload, context={"request": request}).data) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class TaxonTaxaListFilter(filters.BaseFilterBackend): | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we send the help text to the UI to use in tooltips? in the serializer? later in our share types maybe, but right now...?