-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin refactor + classifier support + idempotency #10
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
eddb688
initial plugin refactor and class scores schema change
joefutrelle 2c2bbd4
adding taxonomy endpoints and fixes, and support for small-batch POSTs
joefutrelle 6c329a4
idempotency for prov and idx tables
joefutrelle 51a7580
optional vastdb support
joefutrelle 31751ce
fixing type coercion regression
joefutrelle d3828ac
fixed optional dependencies so tests work with .[dev]
joefutrelle 552c57d
clarify REST client scope
joefutrelle 27c0b0c
return 422 on schema validation error
joefutrelle b8399dd
guard for missing fields
joefutrelle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| """Classifier taxonomy + classification-decode endpoints. | ||
|
|
||
| Thin routing over existing ImageService methods. No decode/taxonomy business | ||
| logic lives here — the service owns it (see ImageService.decode_classification | ||
| and register/get_classifier_taxonomy). Decoding always uses a record's own | ||
| model_version, never "latest" (which is display / new-work only). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException, status | ||
|
|
||
| from improv.api.deps import get_service | ||
| from improv.api.schemas import ( | ||
| DecodedClassificationResponse, | ||
| DecodeRequest, | ||
| TaxonomyCreate, | ||
| TaxonomyResponse, | ||
| ) | ||
| from improv.service import ImageService | ||
|
|
||
| router = APIRouter(tags=["classification"]) | ||
|
|
||
|
|
||
| def _taxonomy_response(tax) -> TaxonomyResponse: | ||
| return TaxonomyResponse( | ||
| classifier=tax.classifier, | ||
| model_version=tax.model_version, | ||
| class_names=tax.class_names, | ||
| created_at=tax.created_at, | ||
| ) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Taxonomy registration + lookup | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| @router.post( | ||
| "/classifiers/{classifier}/taxonomies", | ||
| response_model=TaxonomyResponse, | ||
| status_code=status.HTTP_201_CREATED, | ||
| ) | ||
| def register_taxonomy( | ||
| classifier: str, | ||
| body: TaxonomyCreate, | ||
| service: ImageService = Depends(get_service), | ||
| ) -> TaxonomyResponse: | ||
| taxonomy, created = service.register_classifier_taxonomy( | ||
| classifier, body.model_version, body.class_names | ||
| ) | ||
| if not created: | ||
| raise HTTPException( | ||
| status_code=409, | ||
| detail=f"Taxonomy for {classifier!r}/{body.model_version!r} already exists.", | ||
| ) | ||
| return _taxonomy_response(taxonomy) | ||
|
|
||
|
|
||
| # Literal /latest registered before /{model_version} so it isn't captured as a | ||
| # version (same pattern the provenance router documents). | ||
| @router.get( | ||
| "/classifiers/{classifier}/taxonomies/latest", | ||
| response_model=TaxonomyResponse, | ||
| ) | ||
| def get_latest_taxonomy( | ||
| classifier: str, | ||
| service: ImageService = Depends(get_service), | ||
| ) -> TaxonomyResponse: | ||
| taxonomy = service.get_latest_classifier_taxonomy(classifier) | ||
| if taxonomy is None: | ||
| raise HTTPException( | ||
| status_code=404, detail=f"No taxonomy registered for classifier {classifier!r}." | ||
| ) | ||
| return _taxonomy_response(taxonomy) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/classifiers/{classifier}/taxonomies/{model_version}", | ||
| response_model=TaxonomyResponse, | ||
| ) | ||
| def get_taxonomy( | ||
| classifier: str, | ||
| model_version: str, | ||
| service: ImageService = Depends(get_service), | ||
| ) -> TaxonomyResponse: | ||
| taxonomy = service.get_classifier_taxonomy(classifier, model_version) | ||
| if taxonomy is None: | ||
| raise HTTPException( | ||
| status_code=404, | ||
| detail=f"No taxonomy for {classifier!r}/{model_version!r}.", | ||
| ) | ||
| return _taxonomy_response(taxonomy) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Decoding — (A) stateless, (B) decoded classification read | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def _decode( | ||
| service: ImageService, | ||
| classifier: str, | ||
| model_version: str, | ||
| scores: list[float], | ||
| winner_index: int, | ||
| ) -> DecodedClassificationResponse: | ||
| """Decode one vector, mapping the service's ValueErrors to HTTP status. | ||
|
|
||
| Pre-checks taxonomy existence (→404) so a missing taxonomy is distinguished | ||
| from a length/range mismatch (→422); decode_classification raises an | ||
| undifferentiated ValueError for all three. | ||
| """ | ||
| if service.get_classifier_taxonomy(classifier, model_version) is None: | ||
| raise HTTPException( | ||
| status_code=404, | ||
| detail=f"No taxonomy for {classifier!r}/{model_version!r}.", | ||
| ) | ||
| try: | ||
| decoded = service.decode_classification( | ||
| classifier, model_version, scores, winner_index | ||
| ) | ||
| except ValueError as exc: | ||
| raise HTTPException(status_code=422, detail=str(exc)) from exc | ||
| return DecodedClassificationResponse(**decoded) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/classifiers/{classifier}/decode", | ||
| response_model=DecodedClassificationResponse, | ||
| ) | ||
| def decode( | ||
| classifier: str, | ||
| body: DecodeRequest, | ||
| service: ImageService = Depends(get_service), | ||
| ) -> DecodedClassificationResponse: | ||
| return _decode( | ||
| service, classifier, body.model_version, body.scores, body.winner_index | ||
| ) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/images/{image_id}/classification", | ||
| response_model=list[DecodedClassificationResponse], | ||
| ) | ||
| def get_decoded_classification( | ||
| image_id: str, | ||
| kind: str, | ||
| instrument: str | None = None, | ||
| service: ImageService = Depends(get_service), | ||
| ) -> list[DecodedClassificationResponse]: | ||
| """Read an image's classification-kind provenance, decoded. | ||
|
|
||
| `classifier` is the plugin `kind`. Each record decodes against its OWN | ||
| model_version (from the stored payload), never "latest". | ||
| """ | ||
| records = service.get_provenance(image_id, kind=kind, instrument=instrument) | ||
| return [ | ||
| _decode( | ||
| service, | ||
| kind, | ||
| r.data["model_version"], | ||
| r.data["scores"], | ||
| r.data["winner_index"], | ||
| ) | ||
| for r in records | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I wonder if there's a way we could further describe the difference between low-volume and high-volume producers. Perhaps some examples of what kind of data would be best for the REST API, and what kind of data would be best for the columnar store?
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.
Yes, this could be elucidated further. The primary use case would be for data coming in small batches in near-real time.