From 80f2333b5e4a276d8fef159aeebfcf1598a813b5 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 13:46:51 +0200 Subject: [PATCH 01/16] docs(claude): add SDLC Configuration block to CLAUDE.md Configures the project for the sdlc-* skills: JIRA project key (PYSDK), Atlassian Cloud ID, Ketryx project (Python SDK), per-item-type locations with Git preferred for SWRs and SPECs, and the approval structure pulled from Ketryx project settings. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d0f31ad9..bec4072b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1848,3 +1848,25 @@ ls $test_file --- *This documentation provides comprehensive guidance for working with the Aignostics Python SDK. Each module has detailed CLAUDE.md files with implementation specifics, usage examples, and best practices.* + +## SDLC Configuration + +- **JIRA Project Key:** PYSDK +- **Atlassian Cloud ID:** fff788d2-8a2a-4c36-a884-dde2bb4a2b49 +- **Ketryx Project:** Python SDK (KXPRJ2Q4PA8AADY975SFMKF276TYV75) + +**Item type locations:** +- Change Requests: Ketryx Change Request (JIRA) +- Stakeholder Requirements: requirements/SHR-*.md (Git) +- Software Requirements: Requirement (JIRA) | requirements/SWR-*.md (Git, preferred) +- Risks: Risk (JIRA) +- Software Item Specs: Software Item Spec (JIRA) | specifications/SPEC-*.md (Git, preferred) +- Test Cases: tests/**/TC-*.feature (Git) + +**Approval structure:** +- Ketryx Change Request: Product Manager → Engineering Tech Lead → Quality Managers → Risk Managers +- Software Requirement: Area Lead (BD) → Security Engineer → Regulatory Affairs Specialist → Engineer → Engineering Tech Lead → Product Manager +- Software Item Spec: Engineer → Engineering Tech Lead +- Risk: Quality Managers → Security Engineer → Product Managers → Tech Lead → Engineer → Engineering Tech Lead → Product Manager → Engineer +- Test Case: Engineer → Engineering Tech Lead +- Anomaly: Engineer → Engineering Tech Lead From 67a8ffbe1a843643afd147b09ac6ca663a606c08 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 14:31:35 +0200 Subject: [PATCH 02/16] chore: TODO - add commit content --- requirements/SWR-APPLICATION-1-3.md | 10 ++ specifications/SPEC-APPLICATION-SERVICE.md | 7 +- specifications/SPEC_PLATFORM_SERVICE.md | 94 ++++++++++++++++++- .../application/TC-APPLICATION-CLI-05.feature | 44 +++++++++ 4 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 requirements/SWR-APPLICATION-1-3.md create mode 100644 tests/aignostics/application/TC-APPLICATION-CLI-05.feature diff --git a/requirements/SWR-APPLICATION-1-3.md b/requirements/SWR-APPLICATION-1-3.md new file mode 100644 index 00000000..2b16d270 --- /dev/null +++ b/requirements/SWR-APPLICATION-1-3.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-APPLICATION-1-3 +itemTitle: Retrieve Application Version Release Documents +itemHasParent: SHR-APPLICATION-1 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall list, describe, and download release documents attached to a given application version, exposing only documents with public visibility and uploaded status. diff --git a/specifications/SPEC-APPLICATION-SERVICE.md b/specifications/SPEC-APPLICATION-SERVICE.md index 918c7c82..0cc6a88a 100644 --- a/specifications/SPEC-APPLICATION-SERVICE.md +++ b/specifications/SPEC-APPLICATION-SERVICE.md @@ -2,7 +2,7 @@ itemId: SPEC-APPLICATION-SERVICE itemTitle: Application Module Specification itemType: Software Item Spec -itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-2-3, SWR-APPLICATION-2-4, SHR-APPLICATION-3, SWR-APPLICATION-2-12, SWR-APPLICATION-2-11, SWR-APPLICATION-2-13, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-2-5, SWR-APPLICATION-2-7, SWR-APPLICATION-2-8, SWR-APPLICATION-2-9, SWR-APPLICATION-3-3 +itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-1-3, SWR-APPLICATION-2-3, SWR-APPLICATION-2-4, SHR-APPLICATION-3, SWR-APPLICATION-2-12, SWR-APPLICATION-2-11, SWR-APPLICATION-2-13, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-2-5, SWR-APPLICATION-2-7, SWR-APPLICATION-2-8, SWR-APPLICATION-2-9, SWR-APPLICATION-3-3 Module: Application Layer: Domain Service Version: 0.2.106 @@ -21,7 +21,7 @@ The module implements a domain service layer that orchestrates interactions betw The Application Module shall: -- **FR-01** **Application Discovery**: List and browse available applications with filtering capabilities and detailed information retrieval +- **FR-01** **Application Discovery**: List and browse available applications with filtering capabilities and detailed information retrieval, including listing, describing, and downloading public release documents (e.g. output schemas, model manuals) attached to application versions - **FR-02** **Data Preparation**: Automatically scan directories for whole slide images (WSI), extract comprehensive metadata, and validate file formats - **FR-03** **File Upload Management**: Provide secure, chunked file upload to cloud storage with progress tracking and integrity verification - **FR-04** **Run Lifecycle Management**: Submit, monitor, cancel, and delete application runs with real-time status updates @@ -410,6 +410,9 @@ uvx aignostics application [subcommand] [options] - `list`: List all available applications with filtering - `describe`: Get detailed information about a specific application - `dump-schemata`: Export application schemata +- `version document list APPLICATION_VERSION_ID`: List public release documents attached to an application version +- `version document describe APPLICATION_VERSION_ID DOCUMENT_NAME`: Show metadata for a single public release document +- `version document download APPLICATION_VERSION_ID DOCUMENT_NAME [--output PATH]`: Download a public release document to a local path - `run execute`: Combined prepare, upload, and submit workflow - `run prepare`: Generate metadata from source directory - `run upload`: Upload files to cloud storage diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index 457b273f..c2f4a188 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -2,7 +2,7 @@ itemId: SPEC-PLATFORM-SERVICE itemTitle: Platform Module Specification itemType: Software Item Spec -itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-2-1, SWR-APPLICATION-2-5, SWR-APPLICATION-2-6, SWR-APPLICATION-2-7, SWR-APPLICATION-2-9, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-3-1, SWR-APPLICATION-3-2, SWR-APPLICATION-3-3 +itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-1-3, SWR-APPLICATION-2-1, SWR-APPLICATION-2-5, SWR-APPLICATION-2-6, SWR-APPLICATION-2-7, SWR-APPLICATION-2-9, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-3-1, SWR-APPLICATION-3-2, SWR-APPLICATION-3-3 Module: Platform Layer: Platform Service Version: 1.0.0 @@ -27,7 +27,7 @@ The Platform Module shall: - **[FR-06]** Handle authentication errors with retry mechanisms and fallback flows - **[FR-07]** Support proxy configurations and SSL certificate handling for enterprise environments - **[FR-08]** Provide health monitoring for both public and authenticated API endpoints -- **[FR-09]** Manage application and application version resources with listing and filtering capabilities +- **[FR-09]** Manage application, application version, and application version release document resources with listing, filtering, metadata retrieval, and file download capabilities (release documents follow the platform `307` redirect to short-lived GCS signed URLs and are filtered to public visibility and uploaded status by the public API) - **[FR-10]** Create and manage application runs with status monitoring and result retrieval - **[FR-11]** Download and verify file integrity using CRC32C checksums for run artifacts - **[FR-12]** Generate signed URLs for secure Google Cloud Storage access @@ -81,7 +81,8 @@ platform/ | `Applications` | Class | Application resource management | `list()`, `versions` accessor | | `ApplicationRun` | Class | Run lifecycle and result management | `details()`, `cancel()`, `results()`, `download_to_folder()`, `artifact()`, `get_artifact_download_url()`, `ensure_artifacts_downloaded()` | | `Artifact` | Class | Per-artifact handle for resolving fresh presigned download URLs via the `/api/v1/runs/{run_id}/artifacts/{artifact_id}/file` endpoint | `get_download_url()` | -| `Versions` | Class | Application version management | `list()`, `list_sorted()`, `latest()`, `details()` | +| `Versions` | Class | Application version management | `list()`, `list_sorted()`, `latest()`, `details()`, `documents()` | +| `Documents` | Class | Application version release document management | `list()`, `details()`, `download_to_path()`, `get_content_url()` | | `Runs` | Class | Application run management and creation | `create()`, `list()` / `list_data()`, `__call__()` | | `utils` | Module | Resource utility functions and pagination helpers | `paginate()` | @@ -168,6 +169,35 @@ UserInfo: required: [user, organization, role, token] ``` +**Application Version Document Schema:** + +```yaml +ApplicationVersionDocument: + type: object + properties: + id: + type: string + format: uuid + description: Stable identifier for the document + name: + type: string + description: Document filename, unique per application version (e.g. "output_description.pdf") + mime_type: + type: string + description: IANA media type stored alongside the object (e.g. "application/pdf") + visibility: + type: string + enum: [public] + description: Documents exposed via the public API are always public; internal documents are not surfaced + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + required: [id, name, mime_type, visibility, created_at, updated_at] +``` + ### 3.4 Data Flow ```mermaid @@ -275,6 +305,64 @@ class Versions: def details(self, application_version: ApplicationVersion | str) -> ApplicationVersion: """Retrieves details for a specific application version.""" + + def documents(self, application_version: ApplicationVersion | str) -> "Documents": + """Returns a Documents resource bound to the given application version.""" +``` + +```python +class Documents: + """Resource class for retrieving release documents attached to an application version. + + Backed by ``GET /api/v1/applications/{application_id}/versions/{version_id}/documents`` + and the per-document ``/{name}``, ``/{name}/file``, and ``/{name}/content`` endpoints. + The public API exposes only documents with ``visibility=public`` and ``status=uploaded``. + """ + + def __init__(self, api: PublicApi, application_version_id: str) -> None: + """Initializes the Documents resource bound to an application version.""" + + def list(self, nocache: bool = False) -> Iterator[ApplicationVersionDocument]: + """List metadata for all public, uploaded release documents for the bound version. + + Args: + nocache: If True, bypass cache and force a fresh API call. + + Raises: + NotFoundException: When the application version does not exist or is not accessible. + """ + + def details(self, document_name: str, nocache: bool = False) -> ApplicationVersionDocument: + """Retrieves metadata for a single release document by name. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + """ + + def download_to_path(self, document_name: str, destination: Path | str) -> Path: + """Downloads the document file to a local path. + + Follows the platform ``307`` redirect from the ``/file`` endpoint to a short-lived + GCS signed URL with ``Content-Disposition: attachment; filename="{name}"`` and + writes the response body to disk. Returns the absolute path to the written file. + The presigned URL is short-lived; this method resolves and consumes it in a single call. + + Note: Document downloads do not carry a CRC32C checksum (unlike run artifacts); + integrity is bounded by HTTPS transport and the signed-URL lifetime. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + """ + + def get_content_url(self, document_name: str) -> str: + """Resolves a fresh, short-lived presigned URL for the inline-content endpoint. + + Calls ``GET /api/v1/applications/{application_id}/versions/{version_id}/documents/{name}/content`` + with ``allow_redirects=False`` and returns the presigned URL from the redirect + ``Location`` header. Unlike ``download_to_path``, the response from the resolved + URL is served with the stored ``Content-Type`` and no ``Content-Disposition``, + intended for programmatic clients that consume content inline. + """ ``` ```python diff --git a/tests/aignostics/application/TC-APPLICATION-CLI-05.feature b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature new file mode 100644 index 00000000..3e04a98f --- /dev/null +++ b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature @@ -0,0 +1,44 @@ +Feature: Application Version Release Documents + + The system exposes release documents (output schemas, model manuals, etc.) + attached to an application version, allowing users to list document metadata, + describe a single document, and download document files. Only documents with + public visibility and uploaded status are exposed. + + @tests:SPEC-APPLICATION-SERVICE + @tests:SPEC-PLATFORM-SERVICE + @tests:SWR-APPLICATION-1-3 + @id:TC-APPLICATION-CLI-05-01 + Scenario: System lists release documents for an application version + Given the user has access to an application version with release documents attached + When the user requests the list of release documents for the application version + Then the system shall return metadata for documents with public visibility and uploaded status + And the system shall exclude documents with internal visibility or pending status + + @tests:SPEC-APPLICATION-SERVICE + @tests:SPEC-PLATFORM-SERVICE + @tests:SWR-APPLICATION-1-3 + @id:TC-APPLICATION-CLI-05-02 + Scenario: System describes a single release document + Given the user has access to an application version with a public release document + When the user requests metadata for that document by name + Then the system shall return the document metadata including id, mime type, and timestamps + + @tests:SPEC-APPLICATION-SERVICE + @tests:SPEC-PLATFORM-SERVICE + @tests:SWR-APPLICATION-1-3 + @id:TC-APPLICATION-CLI-05-03 + Scenario: System rejects requests for non-existent or non-public release documents + Given the user has access to an application version + When the user requests metadata for a document that does not exist or is not public + Then the system shall raise a not-found error indicating the document is unavailable + + @tests:SPEC-APPLICATION-SERVICE + @tests:SPEC-PLATFORM-SERVICE + @tests:SWR-APPLICATION-1-3 + @id:TC-APPLICATION-CLI-05-04 + Scenario: System downloads a release document file to a local path + Given the user has access to an application version with a public release document + When the user requests download of that document to a local destination + Then the system shall follow the platform redirect to the signed storage URL + And the system shall write the document file using the server-provided filename From 03970a169fd818fdc2eee94aa45757412a61c2ac Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 14:33:49 +0200 Subject: [PATCH 03/16] chore(codegen): regenerate aignx.codegen client from openapi v1.5.0 [PYSDK-122] Regenerate the auto-generated Python client from the public OpenAPI spec v1.5.0, which adds the four release-documents endpoints under /api/v1/applications/{application-id}/versions/{version-id}/documents (list, get metadata, /file, /content) plus the supporting VersionDocumentResponse model and VersionDocumentVisibility enum. This commit is intentionally isolated to keep the generated diff reviewable; the platform-resource wrapper, CLI, and tests follow in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/in/archive/openapi_1.5.0.json | 3336 +++++++++++++++++ codegen/in/openapi.json | 368 +- codegen/out/.openapi-generator/FILES | 3 + codegen/out/aignx/codegen/api/public_api.py | 1908 ++++++++-- codegen/out/aignx/codegen/api_client.py | 2 +- codegen/out/aignx/codegen/configuration.py | 4 +- codegen/out/aignx/codegen/exceptions.py | 2 +- codegen/out/aignx/codegen/models/__init__.py | 3 + .../models/application_read_response.py | 2 +- .../models/application_read_short_response.py | 2 +- .../codegen/models/application_version.py | 2 +- .../aignx/codegen/models/artifact_output.py | 2 +- .../aignx/codegen/models/artifact_state.py | 2 +- .../models/artifact_termination_reason.py | 2 +- .../models/custom_metadata_update_request.py | 2 +- .../models/custom_metadata_update_response.py | 2 +- .../codegen/models/http_validation_error.py | 2 +- .../aignx/codegen/models/input_artifact.py | 2 +- .../models/input_artifact_creation_request.py | 2 +- .../input_artifact_result_read_response.py | 104 + .../codegen/models/item_creation_request.py | 2 +- .../out/aignx/codegen/models/item_output.py | 2 +- .../models/item_result_read_response.py | 14 +- .../out/aignx/codegen/models/item_state.py | 2 +- .../codegen/models/item_termination_reason.py | 2 +- .../aignx/codegen/models/me_read_response.py | 2 +- .../models/organization_read_response.py | 2 +- .../aignx/codegen/models/output_artifact.py | 2 +- .../output_artifact_result_read_response.py | 2 +- .../codegen/models/output_artifact_scope.py | 2 +- .../models/output_artifact_visibility.py | 2 +- .../codegen/models/run_creation_request.py | 2 +- .../codegen/models/run_creation_response.py | 2 +- .../codegen/models/run_item_statistics.py | 2 +- .../out/aignx/codegen/models/run_output.py | 2 +- .../aignx/codegen/models/run_read_response.py | 2 +- codegen/out/aignx/codegen/models/run_state.py | 2 +- .../codegen/models/run_termination_reason.py | 2 +- .../codegen/models/scheduling_request.py | 2 +- .../codegen/models/scheduling_response.py | 2 +- .../codegen/models/user_read_response.py | 2 +- .../aignx/codegen/models/validation_error.py | 2 +- .../models/validation_error_loc_inner.py | 2 +- .../models/version_document_response.py | 99 + .../models/version_document_visibility.py | 37 + .../codegen/models/version_read_response.py | 2 +- codegen/out/aignx/codegen/rest.py | 2 +- codegen/out/docs/PublicApi.md | 320 ++ 48 files changed, 5861 insertions(+), 409 deletions(-) create mode 100644 codegen/in/archive/openapi_1.5.0.json create mode 100644 codegen/out/aignx/codegen/models/input_artifact_result_read_response.py create mode 100644 codegen/out/aignx/codegen/models/version_document_response.py create mode 100644 codegen/out/aignx/codegen/models/version_document_visibility.py diff --git a/codegen/in/archive/openapi_1.5.0.json b/codegen/in/archive/openapi_1.5.0.json new file mode 100644 index 00000000..1e0c6e0e --- /dev/null +++ b/codegen/in/archive/openapi_1.5.0.json @@ -0,0 +1,3336 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Aignostics Platform API", + "description": "\nThe Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. \n\nTo begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. \n\nMore information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com).\n\n**How to authorize and test API endpoints:**\n\n1. Click the \"Authorize\" button in the right corner below\n3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials\n4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint\n\n**Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized.\n\n", + "version": "1.5.0" + }, + "servers": [ + { + "url": "/api" + } + ], + "paths": { + "/v1/applications": { + "get": { + "tags": [ + "Public" + ], + "summary": "List available applications", + "description": "Returns the list of the applications, available to the caller.\n\nThe application is available if any of the versions of the application is assigned to the caller's organization.\nThe response is paginated and sorted according to the provided parameters.", + "operationId": "list_applications_v1_applications_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page-size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 5, + "default": 50, + "title": "Page-Size" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Sort the results by one or more fields. Use `+` for ascending and `-` for descending order.\n\n**Available fields:**\n- `application_id`\n- `name`\n- `description`\n- `regulatory_classes`\n\n**Examples:**\n- `?sort=application_id` - Sort by application_id ascending\n- `?sort=-name` - Sort by name descending\n- `?sort=+description&sort=name` - Sort by description ascending, then name descending", + "title": "Sort" + }, + "description": "Sort the results by one or more fields. Use `+` for ascending and `-` for descending order.\n\n**Available fields:**\n- `application_id`\n- `name`\n- `description`\n- `regulatory_classes`\n\n**Examples:**\n- `?sort=application_id` - Sort by application_id ascending\n- `?sort=-name` - Sort by name descending\n- `?sort=+description&sort=name` - Sort by description ascending, then name descending" + } + ], + "responses": { + "200": { + "description": "A list of applications available to the caller", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationReadShortResponse" + }, + "title": "Response List Applications V1 Applications Get" + }, + "example": [ + { + "application_id": "he-tme", + "name": "Atlas H&E-TME", + "regulatory_classes": [ + "RUO" + ], + "description": "The Atlas H&E TME is an AI application designed to examine FFPE (formalin-fixed, paraffin-embedded) tissues stained with H&E (hematoxylin and eosin), delivering comprehensive insights into the tumor microenvironment.", + "latest_version": { + "number": "1.0.0", + "released_at": "2025-09-01T19:01:05.401Z" + } + }, + { + "application_id": "test-app", + "name": "Test Application", + "regulatory_classes": [ + "RUO" + ], + "description": "This is the test application with two algorithms: TissueQc and Tissue Segmentation", + "latest_version": { + "number": "2.0.0", + "released_at": "2025-09-02T19:01:05.401Z" + } + } + ] + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Read Application By Id", + "description": "Retrieve details of a specific application by its ID.", + "operationId": "read_application_by_id_v1_applications__application_id__get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationReadResponse" + } + } + } + }, + "403": { + "description": "Forbidden - You don't have permission to see this application" + }, + "404": { + "description": "Not Found - Application with the given ID does not exist" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Application Version Details", + "description": "Get the application version details.\n\nAllows caller to retrieve information about application version based on provided application version ID.", + "operationId": "application_version_details_v1_applications__application_id__versions__version__get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionReadResponse" + }, + "example": { + "version_number": "0.4.4", + "changelog": "New deployment", + "input_artifacts": [ + { + "name": "whole_slide_image", + "mime_type": "image/tiff", + "metadata_schema": { + "type": "object", + "$defs": { + "LungCancerMetadata": { + "type": "object", + "title": "LungCancerMetadata", + "required": [ + "type", + "tissue" + ], + "properties": { + "type": { + "enum": [ + "lung" + ], + "type": "string", + "const": "lung", + "title": "Type" + }, + "tissue": { + "enum": [ + "lung", + "lymph node", + "liver", + "adrenal gland", + "bone", + "brain" + ], + "type": "string", + "title": "Tissue" + } + }, + "additionalProperties": false + } + }, + "title": "ExternalImageMetadata", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "checksum_crc32c", + "base_mpp", + "width", + "height", + "cancer" + ], + "properties": { + "stain": { + "enum": [ + "H&E" + ], + "type": "string", + "const": "H&E", + "title": "Stain", + "default": "H&E" + }, + "width": { + "type": "integer", + "title": "Width", + "maximum": 150000, + "minimum": 1 + }, + "cancer": { + "anyOf": [ + { + "$ref": "#/$defs/LungCancerMetadata" + } + ], + "title": "Cancer" + }, + "height": { + "type": "integer", + "title": "Height", + "maximum": 150000, + "minimum": 1 + }, + "base_mpp": { + "type": "number", + "title": "Base Mpp", + "maximum": 0.5, + "minimum": 0.125 + }, + "mime_type": { + "enum": [ + "application/dicom", + "image/tiff" + ], + "type": "string", + "title": "Mime Type", + "default": "image/tiff" + }, + "checksum_crc32c": { + "type": "string", + "title": "Checksum Crc32C" + } + }, + "description": "Metadata corresponding to an external image.", + "additionalProperties": false + } + } + ], + "output_artifacts": [ + { + "name": "tissue_qc:tiff_heatmap", + "mime_type": "image/tiff", + "metadata_schema": { + "type": "object", + "title": "HeatmapMetadata", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "checksum_crc32c", + "width", + "height", + "class_colors" + ], + "properties": { + "width": { + "type": "integer", + "title": "Width" + }, + "height": { + "type": "integer", + "title": "Height" + }, + "base_mpp": { + "type": "number", + "title": "Base Mpp", + "maximum": 0.5, + "minimum": 0.125 + }, + "mime_type": { + "enum": [ + "image/tiff" + ], + "type": "string", + "const": "image/tiff", + "title": "Mime Type", + "default": "image/tiff" + }, + "class_colors": { + "type": "object", + "title": "Class Colors", + "additionalProperties": { + "type": "array", + "maxItems": 3, + "minItems": 3, + "prefixItems": [ + { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + ] + } + }, + "checksum_crc32c": { + "type": "string", + "title": "Checksum Crc32C" + } + }, + "description": "Metadata corresponding to a segmentation heatmap file.", + "additionalProperties": false + }, + "scope": "ITEM", + "visibility": "EXTERNAL" + } + ], + "released_at": "2025-04-16T08:45:20.655972Z" + } + } + } + }, + "403": { + "description": "Forbidden - You don't have permission to see this version" + }, + "404": { + "description": "Not Found - Application version with given ID is not available to you or does not exist" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs": { + "get": { + "tags": [ + "Public" + ], + "summary": "List Runs", + "description": "List runs with filtering, sorting, and pagination capabilities.\n\nReturns paginated runs that were submitted by the user.", + "operationId": "list_runs_v1_runs_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional application ID filter", + "examples": [ + "tissue-segmentation", + "heta" + ], + "title": "Application Id" + }, + "description": "Optional application ID filter" + }, + { + "name": "application_version", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional Version Name", + "examples": [ + "1.0.2", + "1.0.1-beta2" + ], + "title": "Application Version" + }, + "description": "Optional Version Name" + }, + { + "name": "external_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optionally filter runs by items with this external ID", + "examples": [ + "slide_001", + "patient_12345_sample_A" + ], + "title": "External Id" + }, + "description": "Optionally filter runs by items with this external ID" + }, + { + "name": "custom_metadata", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ], + "description": "Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata.\n#### URL Encoding Required\n**Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding.\n\n#### Examples (Clear Format):\n- **Field existence**: `$.study` - Runs that have a study field defined\n- **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value\n- **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75\n- **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\"\n- **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements\n\n#### Examples (URL-Encoded Format):\n- **Field existence**: `%24.study`\n- **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)`\n- **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)`\n- **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)`\n- **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)`\n\n#### Notes\n- JSONPath expressions are evaluated using PostgreSQL's `@?` operator\n- The `$.` prefix is automatically added to root-level field references if missing\n- String values in conditions must be enclosed in double quotes\n- Use `&&` for AND operations and `||` for OR operations\n- Regular expressions use `like_regex` with standard regex syntax\n- **Please remember to URL-encode the entire JSONPath expression when making HTTP requests**\n\n ", + "title": "Custom Metadata" + }, + "description": "Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata.\n#### URL Encoding Required\n**Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding.\n\n#### Examples (Clear Format):\n- **Field existence**: `$.study` - Runs that have a study field defined\n- **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value\n- **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75\n- **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\"\n- **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements\n\n#### Examples (URL-Encoded Format):\n- **Field existence**: `%24.study`\n- **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)`\n- **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)`\n- **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)`\n- **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)`\n\n#### Notes\n- JSONPath expressions are evaluated using PostgreSQL's `@?` operator\n- The `$.` prefix is automatically added to root-level field references if missing\n- String values in conditions must be enclosed in double quotes\n- Use `&&` for AND operations and `||` for OR operations\n- Regular expressions use `like_regex` with standard regex syntax\n- **Please remember to URL-encode the entire JSONPath expression when making HTTP requests**\n\n ", + "examples": { + "no_filter": { + "summary": "No filter (returns all)", + "description": "Returns all items without filtering by custom metadata", + "value": "$" + }, + "field_exists": { + "summary": "Check if field exists", + "description": "Find applications that have a project field defined", + "value": "$.study" + }, + "field_has_value": { + "summary": "Check if field has a certain value", + "description": "Compare a field value against a certain value", + "value": "$.study ? (@ == \"abc-1\")" + }, + "numeric_comparisons": { + "summary": "Compare to a numeric value of a field", + "description": "Compare a field value against a numeric value of a field", + "value": "$.confidence_score ? (@ > 0.75)" + }, + "array_operations": { + "summary": "Check if an array contains a certain value", + "description": "Check if an array contains a certain value", + "value": "$.tags[*] ? (@ == \"draft\")" + }, + "complex_filters": { + "summary": "Combine multiple checks", + "description": "Combine multiple checks", + "value": "$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)" + } + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 5, + "default": 50, + "title": "Page Size" + } + }, + { + "name": "for_organization", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.", + "title": "For Organization" + }, + "description": "Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs." + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Sort the results by one or more fields. Use `+` for ascending and `-` for descending order.\n\n**Available fields:**\n- `run_id`\n- `application_id`\n- `version_number`\n- `custom_metadata`\n- `submitted_at`\n- `submitted_by`\n- `terminated_at`\n- `termination_reason`\n\n**Examples:**\n- `?sort=submitted_at` - Sort by creation time (ascending)\n- `?sort=-submitted_at` - Sort by creation time (descending)\n- `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending)\n", + "title": "Sort" + }, + "description": "Sort the results by one or more fields. Use `+` for ascending and `-` for descending order.\n\n**Available fields:**\n- `run_id`\n- `application_id`\n- `version_number`\n- `custom_metadata`\n- `submitted_at`\n- `submitted_by`\n- `terminated_at`\n- `termination_reason`\n\n**Examples:**\n- `?sort=submitted_at` - Sort by creation time (ascending)\n- `?sort=-submitted_at` - Sort by creation time (descending)\n- `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending)\n" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunReadResponse" + }, + "title": "Response List Runs V1 Runs Get" + } + } + } + }, + "404": { + "description": "Run not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "Public" + ], + "summary": "Initiate Run", + "description": "This endpoint initiates a processing run for a selected application and version, and returns a `run_id` for tracking purposes.\n\nSlide processing occurs asynchronously, allowing you to retrieve results for individual slides as soon as they\ncomplete processing. The system typically processes slides in batches.\nBelow is an example of the required payload for initiating an Atlas H&E TME processing run.\n\n\n### Payload\n\nThe payload includes `application_id`, optional `version_number`, and `items` base fields.\n\n`application_id` is the unique identifier for the application.\n`version_number` is the semantic version to use. If not provided, the latest available version will be used.\n\n`items` includes the list of the items to process (slides, in case of HETA application).\nEvery item has a set of standard fields defined by the API, plus the custom_metadata, specific to the\nchosen application.\n\nExample payload structure with the comments:\n```\n{\n application_id: \"he-tme\",\n version_number: \"1.0.0-beta\",\n items: [{\n \"external_id\": \"slide_1\",\n \"custom_metadata\": {\"project\": \"sample-study\"},\n \"input_artifacts\": [{\n \"name\": \"user_slide\",\n \"download_url\": \"https://...\",\n \"metadata\": {\n \"specimen\": {\n \"disease\": \"LUNG_CANCER\",\n \"tissue\": \"LUNG\"\n },\n \"staining_method\": \"H&E\",\n \"width_px\": 136223,\n \"height_px\": 87761,\n \"resolution_mpp\": 0.2628238,\n \"media-type\":\"image/tiff\",\n \"checksum_base64_crc32c\": \"64RKKA==\"\n }\n }]\n }]\n}\n```\n\n| Parameter | Description |\n| :---- | :---- |\n| `application_id` required | Unique ID for the application |\n| `version_number` optional | Semantic version of the application. If not provided, the latest available version will be used |\n| `items` required | List of submitted items i.e. whole slide images (WSIs) with parameters described below. |\n| `external_id` required | Unique WSI name or ID for easy reference to items, provided by the caller. The `external_id` should be unique across all items of the run. |\n| `input_artifacts` required | List of provided artifacts for a WSI; at the moment Atlas H&E-TME receives only 1 artifact per slide (the slide itself), but for some other applications this can be a slide and a segmentation map |\n| `name` required | Type of artifact; Atlas H&E-TME supports only `\"input_slide\"` |\n| `download_url` required | Signed URL to the input file in the S3 or GCS; Should be valid for at least 6 days |\n| `specimen: disease` required | Supported cancer types for Atlas H&E-TME (see full list in Atlas H&E-TME manual) |\n| `specimen: tissue` required | Supported tissue types for Atlas H&E-TME (see full list in Atlas H&E-TME manual) |\n| `staining_method` required | WSI stain bio-marker; Atlas H&E-TME supports only `\"H&E\"` |\n| `width_px` required | Integer value. Number of pixels of the WSI in the X dimension. |\n| `height_px` required | Integer value. Number of pixels of the WSI in the Y dimension. |\n| `resolution_mpp` required | Resolution of WSI in micrometers per pixel; check allowed range in Atlas H&E-TME manual |\n| `media-type` required | Supported media formats; available values are: image/tiff (for .tiff or .tif WSI), application/dicom (for DICOM ), application/zip (for zipped DICOM), and application/octet-stream (for .svs WSI) |\n| `checksum_base64_crc32c` required | Base64-encoded big-endian CRC32C checksum of the WSI image |\n\n\n\n### Response\n\nThe endpoint returns the run UUID. After that, the job is scheduled for the execution in the background.\n\nTo check the status of the run, call `GET v1/runs/{run_id}` endpoint with the returned run UUID.\n\n### Rejection\n\nApart from the authentication, authorization, and malformed input error, the request can be\nrejected when specific quota limit is exceeded. More details on quotas is described in the\ndocumentation", + "operationId": "create_run_v1_runs_post", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunCreationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunCreationResponse" + } + } + } + }, + "404": { + "description": "Application version not found" + }, + "403": { + "description": "Forbidden - You don't have permission to create this run" + }, + "400": { + "description": "Bad Request - Input validation failed" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get run details", + "description": "This endpoint allows the caller to retrieve the current status of a run along with other relevant run details.\n A run becomes available immediately after it is created through the `POST /v1/runs/` endpoint.\n\n To download the output results, use `GET /v1/runs/{run_id}/` items to get outputs for all slides.\nAccess to a run is restricted to the user who created it.", + "operationId": "get_run_v1_runs__run_id__get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /v1/runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /v1/runs/` endpoint" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunReadResponse" + } + } + } + }, + "404": { + "description": "Run not found because it was deleted." + }, + "403": { + "description": "Forbidden - You don't have permission to see this run" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/cancel": { + "post": { + "tags": [ + "Public" + ], + "summary": "Cancel Run", + "description": "The run can be canceled by the user who created the run.\n\nThe execution can be canceled any time while the run is not in the terminated state. The\npending items of a canceled run will not be processed and will not add to the cost.\n\nWhen the run is canceled, the already completed items remain available for download.", + "operationId": "cancel_run_v1_runs__run_id__cancel_post", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /runs/` endpoint" + } + ], + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Run not found" + }, + "403": { + "description": "Forbidden - You don't have permission to cancel this run" + }, + "409": { + "description": "Conflict - The Run is already cancelled" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/items": { + "get": { + "tags": [ + "Public" + ], + "summary": "List Run Items", + "description": "List items in a run with filtering, sorting, and pagination capabilities.\n\nReturns paginated items within a specific run. Results can be filtered\nby `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions.\n\n## JSONPath Metadata Filtering\nUse PostgreSQL JSONPath expressions to filter items using their custom_metadata.\n\n### Examples:\n- **Field existence**: `$.case_id` - Results that have a case_id field defined\n- **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority\n- **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence\n- **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed\n- **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds\n\n## Notes\n- JSONPath expressions are evaluated using PostgreSQL's `@?` operator\n- The `$.` prefix is automatically added to root-level field references if missing\n- String values in conditions must be enclosed in double quotes\n- Use `&&` for AND operations and `||` for OR operations", + "operationId": "list_run_items_v1_runs__run_id__items_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /v1/runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /v1/runs/` endpoint" + }, + { + "name": "item_id__in", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + { + "type": "null" + } + ], + "description": "Filter for item ids", + "title": "Item Id In" + }, + "description": "Filter for item ids" + }, + { + "name": "external_id__in", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter for items by their external_id from the input payload", + "title": "External Id In" + }, + "description": "Filter for items by their external_id from the input payload" + }, + { + "name": "state", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ItemState" + }, + { + "type": "null" + } + ], + "description": "Filter items by their state", + "title": "State" + }, + "description": "Filter items by their state" + }, + { + "name": "termination_reason", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ItemTerminationReason" + }, + { + "type": "null" + } + ], + "description": "Filter items by their termination reason. Only applies to TERMINATED items.", + "title": "Termination Reason" + }, + "description": "Filter items by their termination reason. Only applies to TERMINATED items." + }, + { + "name": "custom_metadata", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ], + "description": "JSONPath expression to filter items by their custom_metadata", + "title": "Custom Metadata" + }, + "description": "JSONPath expression to filter items by their custom_metadata", + "examples": { + "no_filter": { + "summary": "No filter (returns all)", + "description": "Returns all items without filtering by custom metadata", + "value": "$" + }, + "field_exists": { + "summary": "Check if field exists", + "description": "Find items that have a project field defined", + "value": "$.project" + }, + "field_has_value": { + "summary": "Check if field has a certain value", + "description": "Compare a field value against a certain value", + "value": "$.project ? (@ == \"cancer-research\")" + }, + "numeric_comparisons": { + "summary": "Compare to a numeric value of a field", + "description": "Compare a field value against a numeric value of a field", + "value": "$.duration_hours ? (@ < 2)" + }, + "array_operations": { + "summary": "Check if an array contains a certain value", + "description": "Check if an array contains a certain value", + "value": "$.tags[*] ? (@ == \"production\")" + }, + "complex_filters": { + "summary": "Combine multiple checks", + "description": "Combine multiple checks", + "value": "$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)" + } + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 5, + "default": 50, + "title": "Page Size" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Sort the items by one or more fields. Use `+` for ascending and `-` for descending order.\n **Available fields:**\n- `item_id`\n- `external_id`\n- `custom_metadata`\n- `terminated_at`\n- `termination_reason`\n\n**Examples:**\n- `?sort=item_id` - Sort by id of the item (ascending)\n- `?sort=-external_id` - Sort by external ID (descending)\n- `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)", + "title": "Sort" + }, + "description": "Sort the items by one or more fields. Use `+` for ascending and `-` for descending order.\n **Available fields:**\n- `item_id`\n- `external_id`\n- `custom_metadata`\n- `terminated_at`\n- `termination_reason`\n\n**Examples:**\n- `?sort=item_id` - Sort by id of the item (ascending)\n- `?sort=-external_id` - Sort by external ID (descending)\n- `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemResultReadResponse" + }, + "title": "Response List Run Items V1 Runs Run Id Items Get" + } + } + } + }, + "404": { + "description": "Run not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/items/{external_id}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get Item By Run", + "description": "Retrieve details of a specific item (slide) by its external ID and the run ID.", + "operationId": "get_item_by_run_v1_runs__run_id__items__external_id__get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "The run id, returned by `POST /runs/` endpoint" + }, + { + "name": "external_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The `external_id` that was defined for the item by the customer that triggered the run.", + "title": "External Id" + }, + "description": "The `external_id` that was defined for the item by the customer that triggered the run." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemResultReadResponse" + } + } + } + }, + "404": { + "description": "Not Found - Item with given ID does not exist" + }, + "403": { + "description": "Forbidden - You don't have permission to see this item" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/artifacts/{artifact_id}/file": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get Artifact Url", + "description": "Download the artifact file with the specified artifact_id, belonging to the specified run.\nThe artifact_is is returned by the `GET /v1/runs/{run_id}/items` endpoint as part of the item results, and can also\nbe retrieved via `GET /v1/runs/{run_id}/items/{external_id}`.\n\nThe endpoint may return a redirect response with a presigned URL to download the artifact file from the storage\nbucket. The presigned URL is valid for a limited time, so it should be used immediately after receiving the response.", + "operationId": "get_artifact_url_v1_runs__run_id__artifacts__artifact_id__file_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /runs/` endpoint" + }, + { + "name": "artifact_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The artifact id to download", + "title": "Artifact Id" + }, + "description": "The artifact id to download" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Not Found - Artifact not found for the specified run" + }, + "307": { + "description": "Temporary Redirect - Redirect to the artifact file URL" + }, + "403": { + "description": "Forbidden - You don't have permission to download this artifact" + }, + "410": { + "description": "Gone - Artifact has been deleted" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/artifacts": { + "delete": { + "tags": [ + "Public" + ], + "summary": "Delete Run Items", + "description": "This endpoint allows the caller to explicitly delete artifacts generated by a run.\nIt can only be invoked when the run has reached a final state, i.e.\n`PROCESSED`, `CANCELED_SYSTEM`, or `CANCELED_USER`.\nNote that by default, all artifacts are automatically deleted 30 days after the run finishes,\nregardless of whether the caller explicitly requests such deletion.", + "operationId": "delete_run_items_v1_runs__run_id__artifacts_delete", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /runs/` endpoint" + } + ], + "responses": { + "200": { + "description": "Run artifacts deleted", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Run not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/custom-metadata": { + "put": { + "tags": [ + "Public" + ], + "summary": "Put Run Custom Metadata", + "description": "Update the custom metadata of a run with the specified `run_id`.\n\nOptionally, a checksum may be provided along the custom metadata JSON.\nIt can be used to verify if the custom metadata was updated since the last time it was accessed.\nIf the checksum is provided, it must match the existing custom metadata in the system, ensuring that the current\ncustom metadata value to be overwritten is acknowledged by the user.\nIf no checksum is provided, submitted metadata directly overwrites the existing metadata, without any checks.\n\nThe latest custom metadata and checksum can be retrieved for the run via the `GET /v1/runs/{run_id}` endpoint.\n\n**Note on deadlines:** Run deadlines must be set during run creation and cannot be modified afterward.\nAny deadline changes in custom metadata will be ignored by the system.", + "operationId": "put_run_custom_metadata_v1_runs__run_id__custom_metadata_put", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "Run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "Run id, returned by `POST /runs/` endpoint" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomMetadataUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Custom metadata successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomMetadataUpdateResponse" + } + } + } + }, + "404": { + "description": "Run not found" + }, + "403": { + "description": "Forbidden - You don't have permission to update this run" + }, + "412": { + "description": "Precondition Failed - Checksum mismatch, resource has been modified" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/items/{external_id}/custom-metadata": { + "put": { + "tags": [ + "Public" + ], + "summary": "Put Item Custom Metadata By Run", + "description": "Update the custom metadata of the item with the specified `external_id`, belonging to the specified run.\n\nOptionally, a checksum may be provided along the custom metadata JSON.\nIt can be used to verify if the custom metadata was updated since the last time it was accessed.\nIf the checksum is provided, it must match the existing custom metadata in the system, ensuring that the current\ncustom metadata value to be overwritten is acknowledged by the user.\nIf no checksum is provided, submitted metadata directly overwrites the existing metadata, without any checks.\n\nThe latest custom metadata and checksum can be retrieved\n for individual items via `GET /v1/runs/{run_id}/items/{external_id}`,\n and for all items of a run via `GET /v1/runs/{run_id}/items`.", + "operationId": "put_item_custom_metadata_by_run_v1_runs__run_id__items__external_id__custom_metadata_put", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "description": "The run id, returned by `POST /runs/` endpoint", + "title": "Run Id" + }, + "description": "The run id, returned by `POST /runs/` endpoint" + }, + { + "name": "external_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The `external_id` that was defined for the item by the customer that triggered the run.", + "title": "External Id" + }, + "description": "The `external_id` that was defined for the item by the customer that triggered the run." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomMetadataUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Custom metadata successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomMetadataUpdateResponse" + } + } + } + }, + "403": { + "description": "Forbidden - You don't have permission to update this item" + }, + "404": { + "description": "Item not found" + }, + "412": { + "description": "Precondition Failed - Checksum mismatch" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/me": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get current user", + "description": "Retrieves your identity details, including name, email, and organization.\nThis is useful for verifying that the request is being made under the correct user profile\nand organization context, as well as confirming that the expected environment variables are correctly set\n(in case you are using Python SDK)", + "operationId": "get_me_v1_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeReadResponse" + } + } + } + } + }, + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ] + } + }, + "/v1/applications/{application_id}/versions/{version}/documents": { + "get": { + "tags": [ + "Public" + ], + "summary": "List version documents", + "description": "List public documents attached to an application version.\n\nReturns only documents with ``visibility=public`` and ``status=uploaded``.", + "operationId": "list_version_documents", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VersionDocumentResponse" + }, + "title": "Response List Version Documents" + } + } + } + }, + "404": { + "description": "Application version not found or not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get version document metadata", + "description": "Return metadata for a single public document attached to an application version.", + "operationId": "get_version_document", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionDocumentResponse" + } + } + } + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}/file": { + "get": { + "tags": [ + "Public" + ], + "summary": "Download version document (browser)", + "description": "307 redirect to a short-lived GCS signed URL for downloading a document.\n\nThe signed URL includes ``response-content-disposition=attachment; filename=\"\"``\nso browsers prompt a save-as dialog rather than rendering inline.\nResponse carries ``Cache-Control: no-store``.", + "operationId": "get_version_document_file", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "307": { + "description": "Temporary redirect to signed GCS URL with Content-Disposition: attachment" + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}/content": { + "get": { + "tags": [ + "Public" + ], + "summary": "Stream version document content (programmatic)", + "description": "307 redirect to a short-lived GCS signed URL for streaming document content.\n\nUnlike ``/file``, no ``Content-Disposition`` override is set — GCS serves\nthe object body with its stored ``Content-Type``. Intended for programmatic\nclients that follow redirects and consume the content directly.\nResponse carries ``Cache-Control: no-store``.", + "operationId": "get_version_document_content", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "307": { + "description": "Temporary redirect to signed GCS URL; GCS serves the object with its stored Content-Type" + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApplicationReadResponse": { + "properties": { + "application_id": { + "type": "string", + "title": "Application Id", + "description": "Application ID", + "examples": [ + "he-tme" + ] + }, + "name": { + "type": "string", + "title": "Name", + "description": "Application display name", + "examples": [ + "Atlas H&E-TME" + ] + }, + "regulatory_classes": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regulatory Classes", + "description": "Regulatory classes, to which the applications comply with. Possible values include: RUO, IVDR, FDA.", + "examples": [ + [ + "RUO" + ] + ] + }, + "description": { + "type": "string", + "title": "Description", + "description": "Describing what the application can do ", + "examples": [ + "The Atlas H&E TME is an AI application designed to examine FFPE (formalin-fixed, paraffin-embedded) tissues stained with H&E (hematoxylin and eosin), delivering comprehensive insights into the tumor microenvironment." + ] + }, + "versions": { + "items": { + "$ref": "#/components/schemas/ApplicationVersion" + }, + "type": "array", + "title": "Versions", + "description": "All version numbers available to the user" + } + }, + "type": "object", + "required": [ + "application_id", + "name", + "regulatory_classes", + "description", + "versions" + ], + "title": "ApplicationReadResponse", + "description": "Response schema for `List available applications` and `Read Application by Id` endpoints" + }, + "ApplicationReadShortResponse": { + "properties": { + "application_id": { + "type": "string", + "title": "Application Id", + "description": "Application ID", + "examples": [ + "he-tme" + ] + }, + "name": { + "type": "string", + "title": "Name", + "description": "Application display name", + "examples": [ + "Atlas H&E-TME" + ] + }, + "regulatory_classes": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regulatory Classes", + "description": "Regulatory classes, to which the applications comply with. Possible values include: RUO, IVDR, FDA.", + "examples": [ + [ + "RUO" + ] + ] + }, + "description": { + "type": "string", + "title": "Description", + "description": "Describing what the application can do ", + "examples": [ + "The Atlas H&E TME is an AI application designed to examine FFPE (formalin-fixed, paraffin-embedded) tissues stained with H&E (hematoxylin and eosin), delivering comprehensive insights into the tumor microenvironment." + ] + }, + "latest_version": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationVersion" + }, + { + "type": "null" + } + ], + "description": "The version with highest version number available to the user" + } + }, + "type": "object", + "required": [ + "application_id", + "name", + "regulatory_classes", + "description" + ], + "title": "ApplicationReadShortResponse", + "description": "Response schema for `List available applications` and `Read Application by Id` endpoints" + }, + "ApplicationVersion": { + "properties": { + "number": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Number", + "description": "The number of the latest version", + "examples": [ + "1.0.0" + ] + }, + "released_at": { + "type": "string", + "format": "date-time", + "title": "Released At", + "description": "The timestamp for when the application version was made available in the Platform", + "examples": [ + "2025-09-15T10:30:45.123Z" + ] + } + }, + "type": "object", + "required": [ + "number", + "released_at" + ], + "title": "ApplicationVersion" + }, + "ArtifactOutput": { + "type": "string", + "enum": [ + "NONE", + "AVAILABLE", + "DELETED_BY_USER", + "DELETED_BY_SYSTEM" + ], + "title": "ArtifactOutput" + }, + "ArtifactState": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "TERMINATED" + ], + "title": "ArtifactState" + }, + "ArtifactTerminationReason": { + "type": "string", + "enum": [ + "SUCCEEDED", + "USER_ERROR", + "SYSTEM_ERROR", + "SKIPPED" + ], + "title": "ArtifactTerminationReason" + }, + "CustomMetadataUpdateRequest": { + "properties": { + "custom_metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata", + "description": "JSON metadata that should be set for the run", + "examples": [ + { + "department": "D1", + "study": "abc-1" + } + ] + }, + "custom_metadata_checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata Checksum", + "description": "Optional field to verify that the latest custom metadata was known. If set to the checksum retrieved via the /runs endpoint, it must match the checksum of the current value in the database.", + "examples": [ + "f54fe109" + ] + } + }, + "type": "object", + "title": "CustomMetadataUpdateRequest" + }, + "CustomMetadataUpdateResponse": { + "properties": { + "custom_metadata_checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata Checksum", + "description": "The checksum of the updated custom metadata. If the `custom_metadata` is None,\nthe checksum also None.", + "readOnly": true + } + }, + "type": "object", + "required": [ + "custom_metadata_checksum" + ], + "title": "CustomMetadataUpdateResponse" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "InputArtifact": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "mime_type": { + "type": "string", + "pattern": "^\\w+\\/\\w+[-+.|\\w+]+\\w+$", + "title": "Mime Type", + "examples": [ + "image/tiff" + ] + }, + "metadata_schema": { + "additionalProperties": true, + "type": "object", + "title": "Metadata Schema" + } + }, + "type": "object", + "required": [ + "name", + "mime_type", + "metadata_schema" + ], + "title": "InputArtifact" + }, + "InputArtifactCreationRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Type of artifact. For Atlas H&E-TME, use \"input_slide\"", + "examples": [ + "input_slide" + ] + }, + "download_url": { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri", + "title": "Download Url", + "description": "[Signed URL](https://cloud.google.com/cdn/docs/using-signed-urls) to the input artifact file. The URL should be valid for at least 6 days from the payload submission time.", + "examples": [ + "https://example.com/case-no-1-slide.tiff" + ] + }, + "metadata": { + "additionalProperties": true, + "type": "object", + "title": "Metadata", + "description": "The metadata of the artifact, required by the application version. The JSON schema of the metadata can be requested by `/v1/versions/{application_version_id}`. The schema is located in `input_artifacts.[].metadata_schema`", + "examples": [ + { + "checksum_base64_crc32c": "752f9554", + "height": 2000, + "height_mpp": 0.5, + "width": 10000, + "width_mpp": 0.5 + } + ] + } + }, + "type": "object", + "required": [ + "name", + "download_url", + "metadata" + ], + "title": "InputArtifactCreationRequest", + "description": "Input artifact containing the slide image and associated metadata." + }, + "InputArtifactResultReadResponse": { + "properties": { + "input_artifact_id": { + "type": "string", + "format": "uuid", + "title": "Input Artifact Id", + "description": "The Id of the artifact. Used internally" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the input from the schema from the `/v1/versions/{version_id}` endpoint.", + "examples": [ + "whole_slide_image" + ] + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the input artifact, provided by the user." + }, + "download_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Download Url", + "description": "The download URL to for the input artifact provided by the user." + } + }, + "type": "object", + "required": [ + "input_artifact_id", + "name" + ], + "title": "InputArtifactResultReadResponse" + }, + "ItemCreationRequest": { + "properties": { + "external_id": { + "type": "string", + "maxLength": 255, + "title": "External Id", + "description": "Unique identifier for this item within the run. Used for referencing items. Must be unique across all items in the same run", + "examples": [ + "slide_1", + "patient_001_slide_A", + "sample_12345" + ] + }, + "custom_metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata", + "description": "Optional JSON custom_metadata to store additional information alongside an item.", + "examples": [ + { + "case": "abc" + } + ] + }, + "input_artifacts": { + "items": { + "$ref": "#/components/schemas/InputArtifactCreationRequest" + }, + "type": "array", + "title": "Input Artifacts", + "description": "List of input artifacts for this item. For Atlas H&E-TME, typically contains one artifact (the slide image)", + "examples": [ + [ + { + "download_url": "https://example-bucket.s3.amazonaws.com/slide1.tiff", + "metadata": { + "checksum_base64_crc32c": "64RKKA==", + "height_px": 87761, + "media-type": "image/tiff", + "resolution_mpp": 0.2628238, + "specimen": { + "disease": "LUNG_CANCER", + "tissue": "LUNG" + }, + "staining_method": "H&E", + "width_px": 136223 + }, + "name": "input_slide" + } + ] + ] + } + }, + "type": "object", + "required": [ + "external_id", + "input_artifacts" + ], + "title": "ItemCreationRequest", + "description": "Individual item (slide) to be processed in a run." + }, + "ItemOutput": { + "type": "string", + "enum": [ + "NONE", + "FULL" + ], + "title": "ItemOutput" + }, + "ItemResultReadResponse": { + "properties": { + "item_id": { + "type": "string", + "format": "uuid", + "title": "Item Id", + "description": "Item UUID generated by the Platform" + }, + "external_id": { + "type": "string", + "title": "External Id", + "description": "The external_id of the item from the user payload", + "examples": [ + "slide_1" + ] + }, + "custom_metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata", + "description": "The custom_metadata of the item that has been provided by the user on run creation." + }, + "custom_metadata_checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata Checksum", + "description": "The checksum of the `custom_metadata` field.\nCan be used in the `PUT /runs/{run-id}/items/{external_id}/custom_metadata`\nrequest to avoid unwanted override of the values in concurrent requests.", + "examples": [ + "f54fe109" + ] + }, + "queue_position_org": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Queue Position Org", + "description": "The position of the item in the organization's queue." + }, + "queue_position_platform": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Queue Position Platform", + "description": "The position of the item in the platform's queue." + }, + "state": { + "$ref": "#/components/schemas/ItemState", + "description": "\nThe item moves from `PENDING` to `PROCESSING` to `TERMINATED` state.\nWhen terminated, consult the `termination_reason` property to see whether it was successful.\n " + }, + "output": { + "$ref": "#/components/schemas/ItemOutput", + "description": "The output status of the item (NONE, FULL)" + }, + "termination_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/ItemTerminationReason" + }, + { + "type": "null" + } + ], + "description": "\nWhen the `state` is `TERMINATED` this will explain why\n`SUCCEEDED` -> Successful processing.\n`USER_ERROR` -> Failed because the provided input was invalid.\n`SYSTEM_ERROR` -> There was an error in the model or platform.\n`SKIPPED` -> Was cancelled\n" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Code" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "\n The error message in case the `termination_reason` is in `USER_ERROR` or `SYSTEM_ERROR`\n ", + "examples": [ + "This item was not processed because the threshold of 3 items finishing in error state (user or system error) was reached before the item was processed.", + "The item was not processed because the run was cancelled by the user before the item was processed.", + "User error raised by Application because the input data provided by the user cannot be processed:\nThe image width is 123000 px, but the maximum width is 100000 px", + "A system error occurred during the item execution:\n System went out of memory in cell classification", + "An unknown system error occurred during the item execution" + ] + }, + "terminated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Terminated At", + "description": "Timestamp showing when the item reached a terminal state.", + "examples": [ + "2024-01-15T10:30:45.123Z" + ] + }, + "input_artifacts": { + "items": { + "$ref": "#/components/schemas/InputArtifactResultReadResponse" + }, + "type": "array", + "title": "Input Artifacts", + "description": "\nThe input artifact(s) provided by the user. For most applications, this will be one artifact that\ndefines the whole slide image to be processed.\n " + }, + "output_artifacts": { + "items": { + "$ref": "#/components/schemas/OutputArtifactResultReadResponse" + }, + "type": "array", + "title": "Output Artifacts", + "description": "\nThe list of the results generated by the application algorithm. The number of files and their\ntypes depend on the particular application version, call `/v1/versions/{version_id}` to get\nthe details.\n " + } + }, + "type": "object", + "required": [ + "item_id", + "external_id", + "custom_metadata", + "state", + "output", + "input_artifacts", + "output_artifacts" + ], + "title": "ItemResultReadResponse", + "description": "Response schema for items in `List Run Items` endpoint" + }, + "ItemState": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "TERMINATED" + ], + "title": "ItemState" + }, + "ItemTerminationReason": { + "type": "string", + "enum": [ + "SUCCEEDED", + "USER_ERROR", + "SYSTEM_ERROR", + "SKIPPED" + ], + "title": "ItemTerminationReason" + }, + "MeReadResponse": { + "properties": { + "user": { + "$ref": "#/components/schemas/UserReadResponse" + }, + "organization": { + "$ref": "#/components/schemas/OrganizationReadResponse" + } + }, + "type": "object", + "required": [ + "user", + "organization" + ], + "title": "MeReadResponse", + "description": "Response schema for `Get current user` endpoint" + }, + "OrganizationReadResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Unique organization identifier", + "examples": [ + "org_123456" + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "Organization name (E.g. “aignx”)", + "examples": [ + "aignx" + ] + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Public organization name (E.g. “Aignostics GmbH”)", + "examples": [ + "Aignostics GmbH" + ] + }, + "aignostics_bucket_hmac_access_key_id": { + "type": "string", + "title": "Aignostics Bucket Hmac Access Key Id", + "description": "HMAC access key ID for the Aignostics-provided storage bucket. Used to authenticate requests for uploading files and generating signed URLs", + "examples": [ + "YOUR_HMAC_ACCESS_KEY_ID" + ] + }, + "aignostics_bucket_hmac_secret_access_key": { + "type": "string", + "title": "Aignostics Bucket Hmac Secret Access Key", + "description": "HMAC secret access key paired with the access key ID. Keep this credential secure.", + "examples": [ + "YOUR/HMAC/SECRET_ACCESS_KEY" + ] + }, + "aignostics_bucket_name": { + "type": "string", + "title": "Aignostics Bucket Name", + "description": "Name of the bucket provided by Aignostics for storing input artifacts (slide images)", + "examples": [ + "aignostics-platform-bucket" + ] + }, + "aignostics_bucket_protocol": { + "type": "string", + "title": "Aignostics Bucket Protocol", + "description": "Protocol to use for bucket access. Defines the URL scheme for connecting to the storage service", + "examples": [ + "gs" + ] + }, + "aignostics_logfire_token": { + "type": "string", + "title": "Aignostics Logfire Token", + "description": "Authentication token for Logfire observability service. Enables sending application logs and performance metrics to Aignostics for monitoring and support", + "examples": [ + "your-logfire-token" + ] + }, + "aignostics_sentry_dsn": { + "type": "string", + "title": "Aignostics Sentry Dsn", + "description": "Data Source Name (DSN) for Sentry error tracking service. Allows automatic reporting of errors and exceptions to Aignostics support team", + "examples": [ + "https://2354s3#ewsha@o44.ingest.us.sentry.io/34345123432" + ] + } + }, + "type": "object", + "required": [ + "id", + "aignostics_bucket_hmac_access_key_id", + "aignostics_bucket_hmac_secret_access_key", + "aignostics_bucket_name", + "aignostics_bucket_protocol", + "aignostics_logfire_token", + "aignostics_sentry_dsn" + ], + "title": "OrganizationReadResponse", + "description": "Part of response schema for Organization object in `Get current user` endpoint.\nThis model corresponds to the response schema returned from\nAuth0 GET /v2/organizations/{id} endpoint, flattens out the metadata out\nand doesn't return branding or token_quota objects.\nFor details, see:\nhttps://auth0.com/docs/api/management/v2/organizations/get-organizations-by-id\n\n#### Configuration for integrating with Aignostics Platform services.\n\nThe Aignostics Platform API requires signed URLs for input artifacts (slide images). To simplify this process,\nAignostics provides a dedicated storage bucket. The HMAC credentials below grant read and write\naccess to this bucket, allowing you to upload files and generate the signed URLs needed for API calls.\n\nAdditionally, logging and error reporting tokens enable Aignostics to provide better support and monitor\nsystem performance for your integration." + }, + "OutputArtifact": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "mime_type": { + "type": "string", + "pattern": "^\\w+\\/\\w+[-+.|\\w+]+\\w+$", + "title": "Mime Type", + "examples": [ + "application/vnd.apache.parquet" + ] + }, + "metadata_schema": { + "additionalProperties": true, + "type": "object", + "title": "Metadata Schema" + }, + "scope": { + "$ref": "#/components/schemas/OutputArtifactScope" + }, + "visibility": { + "$ref": "#/components/schemas/OutputArtifactVisibility" + } + }, + "type": "object", + "required": [ + "name", + "mime_type", + "metadata_schema", + "scope", + "visibility" + ], + "title": "OutputArtifact" + }, + "OutputArtifactResultReadResponse": { + "properties": { + "output_artifact_id": { + "type": "string", + "format": "uuid", + "title": "Output Artifact Id", + "description": "The Id of the artifact. Used internally" + }, + "name": { + "type": "string", + "title": "Name", + "description": "\nName of the output from the output schema from the `/v1/versions/{version_id}` endpoint.\n ", + "examples": [ + "tissue_qc:tiff_heatmap" + ] + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the output artifact, provided by the application. Can only be None if the artifact itself was deleted." + }, + "state": { + "$ref": "#/components/schemas/ArtifactState", + "description": "The current state of the artifact (PENDING, PROCESSING, TERMINATED)" + }, + "termination_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/ArtifactTerminationReason" + }, + { + "type": "null" + } + ], + "description": "The reason for termination when state is TERMINATED" + }, + "output": { + "$ref": "#/components/schemas/ArtifactOutput", + "description": "The output status of the artifact (NONE, FULL)" + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Code" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message" + }, + "download_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Download Url", + "description": "\nThe download URL to the output file. The URL is valid for 1 hour after the endpoint is called.\nA new URL is generated every time the endpoint is called.\n ", + "deprecated": true + } + }, + "type": "object", + "required": [ + "output_artifact_id", + "name", + "state", + "output" + ], + "title": "OutputArtifactResultReadResponse" + }, + "OutputArtifactScope": { + "type": "string", + "enum": [ + "ITEM", + "GLOBAL" + ], + "title": "OutputArtifactScope" + }, + "OutputArtifactVisibility": { + "type": "string", + "enum": [ + "INTERNAL", + "EXTERNAL" + ], + "title": "OutputArtifactVisibility" + }, + "RunCreationRequest": { + "properties": { + "application_id": { + "type": "string", + "title": "Application Id", + "description": "Unique ID for the application to use for processing", + "examples": [ + "he-tme" + ] + }, + "version_number": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version Number", + "description": "Semantic version of the application to use for processing. If not provided, the latest available version will be used", + "examples": [ + "1.0.0-beta1" + ] + }, + "custom_metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata", + "description": "Optional JSON metadata to store additional information alongside the run", + "examples": [ + { + "department": "D1", + "study": "abc-1" + } + ] + }, + "scheduling": { + "anyOf": [ + { + "$ref": "#/components/schemas/SchedulingRequest" + }, + { + "type": "null" + } + ], + "description": "Optional scheduling constraints for this run.", + "examples": [ + { + "deadline": "2026-03-05T23:59:59Z", + "due_date": "2026-03-04T23:59:59Z" + } + ] + }, + "items": { + "items": { + "$ref": "#/components/schemas/ItemCreationRequest" + }, + "type": "array", + "minItems": 1, + "title": "Items", + "description": "List of items (slides) to process. Each item represents a whole slide image (WSI) with its associated metadata and artifacts", + "examples": [ + [ + { + "external_id": "slide_1", + "input_artifacts": [ + { + "download_url": "https://example-bucket.s3.amazonaws.com/slide1.tiff?signature=...", + "metadata": { + "checksum_base64_crc32c": "64RKKA==", + "height_px": 87761, + "media-type": "image/tiff", + "resolution_mpp": 0.2628238, + "specimen": { + "disease": "LUNG_CANCER", + "tissue": "LUNG" + }, + "staining_method": "H&E", + "width_px": 136223 + }, + "name": "input_slide" + } + ] + } + ] + ] + } + }, + "type": "object", + "required": [ + "application_id", + "items" + ], + "title": "RunCreationRequest", + "description": "Request schema for `Initiate Run` endpoint.\nIt describes which application version is chosen, and which user data should be processed." + }, + "RunCreationResponse": { + "properties": { + "run_id": { + "type": "string", + "format": "uuid", + "title": "Run Id", + "examples": [ + "3fa85f64-5717-4562-b3fc-2c963f66afa6" + ] + } + }, + "type": "object", + "required": [ + "run_id" + ], + "title": "RunCreationResponse" + }, + "RunItemStatistics": { + "properties": { + "item_count": { + "type": "integer", + "title": "Item Count", + "description": "Total number of the items in the run" + }, + "item_pending_count": { + "type": "integer", + "title": "Item Pending Count", + "description": "The number of items in `PENDING` state" + }, + "item_processing_count": { + "type": "integer", + "title": "Item Processing Count", + "description": "The number of items in `PROCESSING` state" + }, + "item_user_error_count": { + "type": "integer", + "title": "Item User Error Count", + "description": "The number of items in `TERMINATED` state, and the item termination reason is `USER_ERROR`" + }, + "item_system_error_count": { + "type": "integer", + "title": "Item System Error Count", + "description": "The number of items in `TERMINATED` state, and the item termination reason is `SYSTEM_ERROR`" + }, + "item_skipped_count": { + "type": "integer", + "title": "Item Skipped Count", + "description": "The number of items in `TERMINATED` state, and the item termination reason is `SKIPPED`" + }, + "item_succeeded_count": { + "type": "integer", + "title": "Item Succeeded Count", + "description": "The number of items in `TERMINATED` state, and the item termination reason is `SUCCEEDED`" + } + }, + "type": "object", + "required": [ + "item_count", + "item_pending_count", + "item_processing_count", + "item_user_error_count", + "item_system_error_count", + "item_skipped_count", + "item_succeeded_count" + ], + "title": "RunItemStatistics" + }, + "RunOutput": { + "type": "string", + "enum": [ + "NONE", + "PARTIAL", + "FULL" + ], + "title": "RunOutput" + }, + "RunReadResponse": { + "properties": { + "run_id": { + "type": "string", + "format": "uuid", + "title": "Run Id", + "description": "UUID of the application" + }, + "application_id": { + "type": "string", + "title": "Application Id", + "description": "Application id", + "examples": [ + "he-tme" + ] + }, + "version_number": { + "type": "string", + "title": "Version Number", + "description": "Application version number", + "examples": [ + "0.4.4" + ] + }, + "state": { + "$ref": "#/components/schemas/RunState", + "description": "When the run request is received by the Platform, the `state` of it is set to\n`PENDING`. The state changes to `PROCESSING` when at least one item is being processed. After `PROCESSING`, the\nstate of the run can switch back to `PENDING` if there are no processing items, or to `TERMINATED` when the run\nfinished processing." + }, + "output": { + "$ref": "#/components/schemas/RunOutput", + "description": "The status of the output of the run. When 0 items are successfully processed the output is\n`NONE`, after one item is successfully processed, the value is set to `PARTIAL`. When all items of the run are\nsuccessfully processed, the output is set to `FULL`." + }, + "termination_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/RunTerminationReason" + }, + { + "type": "null" + } + ], + "description": "The termination reason of the run. When the run is not in `TERMINATED` state, the\n termination_reason is `null`. If all items of of the run are processed (successfully or with an error), then\n termination_reason is set to `ALL_ITEMS_PROCESSED`. If the run is cancelled by the user, the value is set to\n `CANCELED_BY_USER`. If the run reaches the threshold of number of failed items, the Platform cancels the run\n and sets the termination_reason to `CANCELED_BY_SYSTEM`.\n " + }, + "error_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Code", + "description": "When the termination_reason is set to CANCELED_BY_SYSTEM, the error_code is set to define the\n structured description of the error.", + "examples": [ + "SCHEDULER.ITEMS_WITH_ERROR_THRESHOLD_REACHED" + ] + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "When the termination_reason is set to CANCELED_BY_SYSTEM, the error_message is set to provide\n more insights to the error cause.", + "examples": [ + "Run canceled given errors on more than 10 items." + ] + }, + "statistics": { + "$ref": "#/components/schemas/RunItemStatistics", + "description": "Aggregated statistics of the run execution" + }, + "custom_metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata", + "description": "Optional JSON metadata that was stored in alongside the run by the user", + "examples": [ + { + "department": "D1", + "study": "abc-1" + } + ] + }, + "custom_metadata_checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Custom Metadata Checksum", + "description": "The checksum of the `custom_metadata` field. Can be used in the `PUT /runs/{run-id}/custom_metadata`\nrequest to avoid unwanted override of the values in concurrent requests.", + "examples": [ + "f54fe109" + ] + }, + "submitted_at": { + "type": "string", + "format": "date-time", + "title": "Submitted At", + "description": "Timestamp showing when the run was triggered" + }, + "submitted_by": { + "type": "string", + "title": "Submitted By", + "description": "Id of the user who triggered the run", + "examples": [ + "auth0|123456" + ] + }, + "terminated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Terminated At", + "description": "Timestamp showing when the run reached a terminal state.", + "examples": [ + "2024-01-15T10:30:45.123Z" + ] + }, + "num_preceding_items_org": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Num Preceding Items Org", + "description": "How many Items from other Runs in the same Organization are due to begin processing before this Run's next Item does." + }, + "num_preceding_items_platform": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Num Preceding Items Platform", + "description": "How many Items from other Runs are due to begin processing before this Run's next Item does." + }, + "scheduling": { + "anyOf": [ + { + "$ref": "#/components/schemas/SchedulingResponse" + }, + { + "type": "null" + } + ], + "description": "Scheduling constraints set for this run." + } + }, + "type": "object", + "required": [ + "run_id", + "application_id", + "version_number", + "state", + "output", + "termination_reason", + "error_code", + "error_message", + "statistics", + "submitted_at", + "submitted_by" + ], + "title": "RunReadResponse", + "description": "Response schema for `Get run details` endpoint" + }, + "RunState": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "TERMINATED" + ], + "title": "RunState" + }, + "RunTerminationReason": { + "type": "string", + "enum": [ + "ALL_ITEMS_PROCESSED", + "CANCELED_BY_SYSTEM", + "CANCELED_BY_USER" + ], + "title": "RunTerminationReason" + }, + "SchedulingRequest": { + "properties": { + "due_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Due Date", + "description": "Requested completion time. Items are prioritized to meet this target.", + "examples": [ + "2026-03-04T23:59:59Z" + ] + }, + "deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Deadline", + "description": "Hard deadline. The run will be cancelled if not completed by this time.", + "examples": [ + "2026-03-05T23:59:59Z" + ] + } + }, + "type": "object", + "title": "SchedulingRequest", + "description": "Scheduling constraints for a run." + }, + "SchedulingResponse": { + "properties": { + "due_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Due Date" + }, + "deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Deadline" + } + }, + "type": "object", + "title": "SchedulingResponse", + "description": "Scheduling fields returned in run responses." + }, + "UserReadResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Unique user identifier", + "examples": [ + "auth0|123456" + ] + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email", + "description": "User email", + "examples": [ + "user@domain.com" + ] + }, + "email_verified": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Email Verified", + "examples": [ + true + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "First and last name of the user", + "examples": [ + "Jane Doe" + ] + }, + "given_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Given Name", + "examples": [ + "Jane" + ] + }, + "family_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Family Name", + "examples": [ + "Doe" + ] + }, + "nickname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Nickname", + "examples": [ + "jdoe" + ] + }, + "picture": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Picture", + "examples": [ + "https://example.com/jdoe.jpg" + ] + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "examples": [ + "2023-10-05T14:48:00.000Z" + ] + } + }, + "type": "object", + "required": [ + "id" + ], + "title": "UserReadResponse", + "description": "Part of response schema for User object in `Get current user` endpoint.\nThis model corresponds to the response schema returned from\nAuth0 GET /v2/users/{id} endpoint.\nFor details, see:\nhttps://auth0.com/docs/api/management/v2/users/get-users-by-id" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "VersionDocumentResponse": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "mime_type": { + "type": "string", + "title": "Mime Type" + }, + "visibility": { + "$ref": "#/components/schemas/VersionDocumentVisibility" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "mime_type", + "visibility", + "created_at", + "updated_at" + ], + "title": "VersionDocumentResponse" + }, + "VersionDocumentVisibility": { + "type": "string", + "enum": [ + "public", + "internal" + ], + "title": "VersionDocumentVisibility" + }, + "VersionReadResponse": { + "properties": { + "version_number": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Version Number", + "description": "Semantic version of the application" + }, + "changelog": { + "type": "string", + "title": "Changelog", + "description": "Description of the changes relative to the previous version" + }, + "input_artifacts": { + "items": { + "$ref": "#/components/schemas/InputArtifact" + }, + "type": "array", + "title": "Input Artifacts", + "description": "List of the input fields, provided by the User" + }, + "output_artifacts": { + "items": { + "$ref": "#/components/schemas/OutputArtifact" + }, + "type": "array", + "title": "Output Artifacts", + "description": "List of the output fields, generated by the application" + }, + "released_at": { + "type": "string", + "format": "date-time", + "title": "Released At", + "description": "The timestamp when the application version was registered" + } + }, + "type": "object", + "required": [ + "version_number", + "changelog", + "input_artifacts", + "output_artifacts", + "released_at" + ], + "title": "VersionReadResponse", + "description": "Base Response schema for the `Application Version Details` endpoint" + } + }, + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "scopes": {}, + "authorizationUrl": "https://aignostics-platform-staging.eu.auth0.com/authorize", + "tokenUrl": "https://aignostics-platform-staging.eu.auth0.com/oauth/token" + } + } + } + } + } +} diff --git a/codegen/in/openapi.json b/codegen/in/openapi.json index cae9e020..1e0c6e0e 100644 --- a/codegen/in/openapi.json +++ b/codegen/in/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Aignostics Platform API", "description": "\nThe Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. \n\nTo begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. \n\nMore information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com).\n\n**How to authorize and test API endpoints:**\n\n1. Click the \"Authorize\" button in the right corner below\n3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials\n4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint\n\n**Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized.\n\n", - "version": "1.4.0" + "version": "1.5.0" }, "servers": [ { @@ -1425,6 +1425,263 @@ } ] } + }, + "/v1/applications/{application_id}/versions/{version}/documents": { + "get": { + "tags": [ + "Public" + ], + "summary": "List version documents", + "description": "List public documents attached to an application version.\n\nReturns only documents with ``visibility=public`` and ``status=uploaded``.", + "operationId": "list_version_documents", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VersionDocumentResponse" + }, + "title": "Response List Version Documents" + } + } + } + }, + "404": { + "description": "Application version not found or not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}": { + "get": { + "tags": [ + "Public" + ], + "summary": "Get version document metadata", + "description": "Return metadata for a single public document attached to an application version.", + "operationId": "get_version_document", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionDocumentResponse" + } + } + } + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}/file": { + "get": { + "tags": [ + "Public" + ], + "summary": "Download version document (browser)", + "description": "307 redirect to a short-lived GCS signed URL for downloading a document.\n\nThe signed URL includes ``response-content-disposition=attachment; filename=\"\"``\nso browsers prompt a save-as dialog rather than rendering inline.\nResponse carries ``Cache-Control: no-store``.", + "operationId": "get_version_document_file", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "307": { + "description": "Temporary redirect to signed GCS URL with Content-Disposition: attachment" + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/applications/{application_id}/versions/{version}/documents/{name}/content": { + "get": { + "tags": [ + "Public" + ], + "summary": "Stream version document content (programmatic)", + "description": "307 redirect to a short-lived GCS signed URL for streaming document content.\n\nUnlike ``/file``, no ``Content-Disposition`` override is set — GCS serves\nthe object body with its stored ``Content-Type``. Intended for programmatic\nclients that follow redirects and consume the content directly.\nResponse carries ``Cache-Control: no-store``.", + "operationId": "get_version_document_content", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + } + ], + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Application Id" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Version" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Name" + } + } + ], + "responses": { + "307": { + "description": "Temporary redirect to signed GCS URL; GCS serves the object with its stored Content-Type" + }, + "404": { + "description": "Document not found, not public, or version not accessible" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -1755,6 +2012,58 @@ "title": "InputArtifactCreationRequest", "description": "Input artifact containing the slide image and associated metadata." }, + "InputArtifactResultReadResponse": { + "properties": { + "input_artifact_id": { + "type": "string", + "format": "uuid", + "title": "Input Artifact Id", + "description": "The Id of the artifact. Used internally" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the input from the schema from the `/v1/versions/{version_id}` endpoint.", + "examples": [ + "whole_slide_image" + ] + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the input artifact, provided by the user." + }, + "download_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Download Url", + "description": "The download URL to for the input artifact provided by the user." + } + }, + "type": "object", + "required": [ + "input_artifact_id", + "name" + ], + "title": "InputArtifactResultReadResponse" + }, "ItemCreationRequest": { "properties": { "external_id": { @@ -1964,6 +2273,14 @@ "2024-01-15T10:30:45.123Z" ] }, + "input_artifacts": { + "items": { + "$ref": "#/components/schemas/InputArtifactResultReadResponse" + }, + "type": "array", + "title": "Input Artifacts", + "description": "\nThe input artifact(s) provided by the user. For most applications, this will be one artifact that\ndefines the whole slide image to be processed.\n " + }, "output_artifacts": { "items": { "$ref": "#/components/schemas/OutputArtifactResultReadResponse" @@ -1980,6 +2297,7 @@ "custom_metadata", "state", "output", + "input_artifacts", "output_artifacts" ], "title": "ItemResultReadResponse", @@ -2906,6 +3224,54 @@ ], "title": "ValidationError" }, + "VersionDocumentResponse": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "mime_type": { + "type": "string", + "title": "Mime Type" + }, + "visibility": { + "$ref": "#/components/schemas/VersionDocumentVisibility" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "mime_type", + "visibility", + "created_at", + "updated_at" + ], + "title": "VersionDocumentResponse" + }, + "VersionDocumentVisibility": { + "type": "string", + "enum": [ + "public", + "internal" + ], + "title": "VersionDocumentVisibility" + }, "VersionReadResponse": { "properties": { "version_number": { diff --git a/codegen/out/.openapi-generator/FILES b/codegen/out/.openapi-generator/FILES index ee99c756..56c6dba4 100644 --- a/codegen/out/.openapi-generator/FILES +++ b/codegen/out/.openapi-generator/FILES @@ -14,6 +14,7 @@ aignx/codegen/models/custom_metadata_update_response.py aignx/codegen/models/http_validation_error.py aignx/codegen/models/input_artifact.py aignx/codegen/models/input_artifact_creation_request.py +aignx/codegen/models/input_artifact_result_read_response.py aignx/codegen/models/item_creation_request.py aignx/codegen/models/item_output.py aignx/codegen/models/item_result_read_response.py @@ -37,6 +38,8 @@ aignx/codegen/models/scheduling_response.py aignx/codegen/models/user_read_response.py aignx/codegen/models/validation_error.py aignx/codegen/models/validation_error_loc_inner.py +aignx/codegen/models/version_document_response.py +aignx/codegen/models/version_document_visibility.py aignx/codegen/models/version_read_response.py aignx/codegen/rest.py docs/PublicApi.md diff --git a/codegen/out/aignx/codegen/api/public_api.py b/codegen/out/aignx/codegen/api/public_api.py index fdaaedae..1a74d450 100644 --- a/codegen/out/aignx/codegen/api/public_api.py +++ b/codegen/out/aignx/codegen/api/public_api.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. @@ -30,6 +30,7 @@ from aignx.codegen.models.run_creation_request import RunCreationRequest from aignx.codegen.models.run_creation_response import RunCreationResponse from aignx.codegen.models.run_read_response import RunReadResponse +from aignx.codegen.models.version_document_response import VersionDocumentResponse from aignx.codegen.models.version_read_response import VersionReadResponse from aignx.codegen.api_client import ApiClient, RequestSerialized @@ -2254,11 +2255,11 @@ def _get_run_v1_runs_run_id_get_serialize( @validate_call - def list_applications_v1_applications_get( + def get_version_document( self, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2271,17 +2272,17 @@ def list_applications_v1_applications_get( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> List[ApplicationReadShortResponse]: - """List available applications + ) -> VersionDocumentResponse: + """Get version document metadata - Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + Return metadata for a single public document attached to an application version. - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2304,10 +2305,10 @@ def list_applications_v1_applications_get( :return: Returns the result object. """ # noqa: E501 - _param = self._list_applications_v1_applications_get_serialize( - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2315,8 +2316,8 @@ def list_applications_v1_applications_get( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ApplicationReadShortResponse]", - '401': None, + '200': "VersionDocumentResponse", + '404': None, '422': "HTTPValidationError", } response_data = self.api_client.call_api( @@ -2331,11 +2332,11 @@ def list_applications_v1_applications_get( @validate_call - def list_applications_v1_applications_get_with_http_info( + def get_version_document_with_http_info( self, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2348,17 +2349,17 @@ def list_applications_v1_applications_get_with_http_info( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> ApiResponse[List[ApplicationReadShortResponse]]: - """List available applications + ) -> ApiResponse[VersionDocumentResponse]: + """Get version document metadata - Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + Return metadata for a single public document attached to an application version. - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2381,10 +2382,10 @@ def list_applications_v1_applications_get_with_http_info( :return: Returns the result object. """ # noqa: E501 - _param = self._list_applications_v1_applications_get_serialize( - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2392,8 +2393,8 @@ def list_applications_v1_applications_get_with_http_info( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ApplicationReadShortResponse]", - '401': None, + '200': "VersionDocumentResponse", + '404': None, '422': "HTTPValidationError", } response_data = self.api_client.call_api( @@ -2408,11 +2409,11 @@ def list_applications_v1_applications_get_with_http_info( @validate_call - def list_applications_v1_applications_get_without_preload_content( + def get_version_document_without_preload_content( self, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2426,16 +2427,16 @@ def list_applications_v1_applications_get_without_preload_content( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """List available applications + """Get version document metadata - Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + Return metadata for a single public document attached to an application version. - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2458,10 +2459,10 @@ def list_applications_v1_applications_get_without_preload_content( :return: Returns the result object. """ # noqa: E501 - _param = self._list_applications_v1_applications_get_serialize( - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2469,8 +2470,8 @@ def list_applications_v1_applications_get_without_preload_content( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ApplicationReadShortResponse]", - '401': None, + '200': "VersionDocumentResponse", + '404': None, '422': "HTTPValidationError", } response_data = self.api_client.call_api( @@ -2480,11 +2481,11 @@ def list_applications_v1_applications_get_without_preload_content( return response_data.response - def _list_applications_v1_applications_get_serialize( + def _get_version_document_serialize( self, - page, - page_size, - sort, + application_id, + version, + name, _request_auth, _content_type, _headers, @@ -2494,7 +2495,6 @@ def _list_applications_v1_applications_get_serialize( _host = None _collection_formats: Dict[str, str] = { - 'sort': 'multi', } _path_params: Dict[str, str] = {} @@ -2507,19 +2507,13 @@ def _list_applications_v1_applications_get_serialize( _body_params: Optional[bytes] = None # process the path parameters + if application_id is not None: + _path_params['application_id'] = application_id + if version is not None: + _path_params['version'] = version + if name is not None: + _path_params['name'] = name # process the query parameters - if page is not None: - - _query_params.append(('page', page)) - - if page_size is not None: - - _query_params.append(('page-size', page_size)) - - if sort is not None: - - _query_params.append(('sort', sort)) - # process the header parameters # process the form parameters # process the body parameter @@ -2541,7 +2535,7 @@ def _list_applications_v1_applications_get_serialize( return self.api_client.param_serialize( method='GET', - resource_path='/api/v1/applications', + resource_path='/api/v1/applications/{application_id}/versions/{version}/documents/{name}', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -2558,17 +2552,11 @@ def _list_applications_v1_applications_get_serialize( @validate_call - def list_run_items_v1_runs_run_id_items_get( + def get_version_document_content( self, - run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], - item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, - external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, - state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, - termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2581,29 +2569,17 @@ def list_run_items_v1_runs_run_id_items_get( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> List[ItemResultReadResponse]: - """List Run Items + ) -> None: + """Stream version document content (programmatic) - List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + 307 redirect to a short-lived GCS signed URL for streaming document content. Unlike ``/file``, no ``Content-Disposition`` override is set — GCS serves the object body with its stored ``Content-Type``. Intended for programmatic clients that follow redirects and consume the content directly. Response carries ``Cache-Control: no-store``. - :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) - :type run_id: str - :param item_id__in: Filter for item ids - :type item_id__in: List[str] - :param external_id__in: Filter for items by their external_id from the input payload - :type external_id__in: List[str] - :param state: Filter items by their state - :type state: ItemState - :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. - :type termination_reason: ItemTerminationReason - :param custom_metadata: JSONPath expression to filter items by their custom_metadata - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2626,16 +2602,10 @@ def list_run_items_v1_runs_run_id_items_get( :return: Returns the result object. """ # noqa: E501 - _param = self._list_run_items_v1_runs_run_id_items_get_serialize( - run_id=run_id, - item_id__in=item_id__in, - external_id__in=external_id__in, - state=state, - termination_reason=termination_reason, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_content_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2643,7 +2613,7 @@ def list_run_items_v1_runs_run_id_items_get( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ItemResultReadResponse]", + '307': None, '404': None, '422': "HTTPValidationError", } @@ -2659,17 +2629,11 @@ def list_run_items_v1_runs_run_id_items_get( @validate_call - def list_run_items_v1_runs_run_id_items_get_with_http_info( + def get_version_document_content_with_http_info( self, - run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], - item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, - external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, - state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, - termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2682,29 +2646,17 @@ def list_run_items_v1_runs_run_id_items_get_with_http_info( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> ApiResponse[List[ItemResultReadResponse]]: - """List Run Items + ) -> ApiResponse[None]: + """Stream version document content (programmatic) - List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + 307 redirect to a short-lived GCS signed URL for streaming document content. Unlike ``/file``, no ``Content-Disposition`` override is set — GCS serves the object body with its stored ``Content-Type``. Intended for programmatic clients that follow redirects and consume the content directly. Response carries ``Cache-Control: no-store``. - :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) - :type run_id: str - :param item_id__in: Filter for item ids - :type item_id__in: List[str] - :param external_id__in: Filter for items by their external_id from the input payload - :type external_id__in: List[str] - :param state: Filter items by their state - :type state: ItemState - :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. - :type termination_reason: ItemTerminationReason - :param custom_metadata: JSONPath expression to filter items by their custom_metadata - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2727,16 +2679,10 @@ def list_run_items_v1_runs_run_id_items_get_with_http_info( :return: Returns the result object. """ # noqa: E501 - _param = self._list_run_items_v1_runs_run_id_items_get_serialize( - run_id=run_id, - item_id__in=item_id__in, - external_id__in=external_id__in, - state=state, - termination_reason=termination_reason, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_content_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2744,7 +2690,7 @@ def list_run_items_v1_runs_run_id_items_get_with_http_info( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ItemResultReadResponse]", + '307': None, '404': None, '422': "HTTPValidationError", } @@ -2760,17 +2706,11 @@ def list_run_items_v1_runs_run_id_items_get_with_http_info( @validate_call - def list_run_items_v1_runs_run_id_items_get_without_preload_content( + def get_version_document_content_without_preload_content( self, - run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], - item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, - external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, - state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, - termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2784,28 +2724,16 @@ def list_run_items_v1_runs_run_id_items_get_without_preload_content( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """List Run Items + """Stream version document content (programmatic) - List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + 307 redirect to a short-lived GCS signed URL for streaming document content. Unlike ``/file``, no ``Content-Disposition`` override is set — GCS serves the object body with its stored ``Content-Type``. Intended for programmatic clients that follow redirects and consume the content directly. Response carries ``Cache-Control: no-store``. - :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) - :type run_id: str - :param item_id__in: Filter for item ids - :type item_id__in: List[str] - :param external_id__in: Filter for items by their external_id from the input payload - :type external_id__in: List[str] - :param state: Filter items by their state - :type state: ItemState - :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. - :type termination_reason: ItemTerminationReason - :param custom_metadata: JSONPath expression to filter items by their custom_metadata - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) - :type sort: List[str] + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2828,16 +2756,10 @@ def list_run_items_v1_runs_run_id_items_get_without_preload_content( :return: Returns the result object. """ # noqa: E501 - _param = self._list_run_items_v1_runs_run_id_items_get_serialize( - run_id=run_id, - item_id__in=item_id__in, - external_id__in=external_id__in, - state=state, - termination_reason=termination_reason, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - sort=sort, + _param = self._get_version_document_content_serialize( + application_id=application_id, + version=version, + name=name, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2845,7 +2767,7 @@ def list_run_items_v1_runs_run_id_items_get_without_preload_content( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[ItemResultReadResponse]", + '307': None, '404': None, '422': "HTTPValidationError", } @@ -2856,10 +2778,980 @@ def list_run_items_v1_runs_run_id_items_get_without_preload_content( return response_data.response - def _list_run_items_v1_runs_run_id_items_get_serialize( + def _get_version_document_content_serialize( self, - run_id, - item_id__in, + application_id, + version, + name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if application_id is not None: + _path_params['application_id'] = application_id + if version is not None: + _path_params['version'] = version + if name is not None: + _path_params['name'] = name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2AuthorizationCodeBearer' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/api/v1/applications/{application_id}/versions/{version}/documents/{name}/content', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def get_version_document_file( + self, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Download version document (browser) + + 307 redirect to a short-lived GCS signed URL for downloading a document. The signed URL includes ``response-content-disposition=attachment; filename=\"\"`` so browsers prompt a save-as dialog rather than rendering inline. Response carries ``Cache-Control: no-store``. + + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_version_document_file_serialize( + application_id=application_id, + version=version, + name=name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '307': None, + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_version_document_file_with_http_info( + self, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Download version document (browser) + + 307 redirect to a short-lived GCS signed URL for downloading a document. The signed URL includes ``response-content-disposition=attachment; filename=\"\"`` so browsers prompt a save-as dialog rather than rendering inline. Response carries ``Cache-Control: no-store``. + + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_version_document_file_serialize( + application_id=application_id, + version=version, + name=name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '307': None, + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_version_document_file_without_preload_content( + self, + application_id: StrictStr, + version: StrictStr, + name: StrictStr, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Download version document (browser) + + 307 redirect to a short-lived GCS signed URL for downloading a document. The signed URL includes ``response-content-disposition=attachment; filename=\"\"`` so browsers prompt a save-as dialog rather than rendering inline. Response carries ``Cache-Control: no-store``. + + :param application_id: (required) + :type application_id: str + :param version: (required) + :type version: str + :param name: (required) + :type name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_version_document_file_serialize( + application_id=application_id, + version=version, + name=name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '307': None, + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_version_document_file_serialize( + self, + application_id, + version, + name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if application_id is not None: + _path_params['application_id'] = application_id + if version is not None: + _path_params['version'] = version + if name is not None: + _path_params['name'] = name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2AuthorizationCodeBearer' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/api/v1/applications/{application_id}/versions/{version}/documents/{name}/file', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_applications_v1_applications_get( + self, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> List[ApplicationReadShortResponse]: + """List available applications + + Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_applications_v1_applications_get_serialize( + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ApplicationReadShortResponse]", + '401': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_applications_v1_applications_get_with_http_info( + self, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[List[ApplicationReadShortResponse]]: + """List available applications + + Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_applications_v1_applications_get_serialize( + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ApplicationReadShortResponse]", + '401': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_applications_v1_applications_get_without_preload_content( + self, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List available applications + + Returns the list of the applications, available to the caller. The application is available if any of the versions of the application is assigned to the caller's organization. The response is paginated and sorted according to the provided parameters. + + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `application_id` - `name` - `description` - `regulatory_classes` **Examples:** - `?sort=application_id` - Sort by application_id ascending - `?sort=-name` - Sort by name descending - `?sort=+description&sort=name` - Sort by description ascending, then name descending + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_applications_v1_applications_get_serialize( + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ApplicationReadShortResponse]", + '401': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_applications_v1_applications_get_serialize( + self, + page, + page_size, + sort, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + 'sort': 'multi', + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + if page is not None: + + _query_params.append(('page', page)) + + if page_size is not None: + + _query_params.append(('page-size', page_size)) + + if sort is not None: + + _query_params.append(('sort', sort)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2AuthorizationCodeBearer' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/api/v1/applications', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_run_items_v1_runs_run_id_items_get( + self, + run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], + item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, + external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, + state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, + termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> List[ItemResultReadResponse]: + """List Run Items + + List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + + :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) + :type run_id: str + :param item_id__in: Filter for item ids + :type item_id__in: List[str] + :param external_id__in: Filter for items by their external_id from the input payload + :type external_id__in: List[str] + :param state: Filter items by their state + :type state: ItemState + :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. + :type termination_reason: ItemTerminationReason + :param custom_metadata: JSONPath expression to filter items by their custom_metadata + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_run_items_v1_runs_run_id_items_get_serialize( + run_id=run_id, + item_id__in=item_id__in, + external_id__in=external_id__in, + state=state, + termination_reason=termination_reason, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ItemResultReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_run_items_v1_runs_run_id_items_get_with_http_info( + self, + run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], + item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, + external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, + state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, + termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[List[ItemResultReadResponse]]: + """List Run Items + + List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + + :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) + :type run_id: str + :param item_id__in: Filter for item ids + :type item_id__in: List[str] + :param external_id__in: Filter for items by their external_id from the input payload + :type external_id__in: List[str] + :param state: Filter items by their state + :type state: ItemState + :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. + :type termination_reason: ItemTerminationReason + :param custom_metadata: JSONPath expression to filter items by their custom_metadata + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_run_items_v1_runs_run_id_items_get_serialize( + run_id=run_id, + item_id__in=item_id__in, + external_id__in=external_id__in, + state=state, + termination_reason=termination_reason, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ItemResultReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_run_items_v1_runs_run_id_items_get_without_preload_content( + self, + run_id: Annotated[StrictStr, Field(description="Run id, returned by `POST /v1/runs/` endpoint")], + item_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for item ids")] = None, + external_id__in: Annotated[Optional[List[StrictStr]], Field(description="Filter for items by their external_id from the input payload")] = None, + state: Annotated[Optional[ItemState], Field(description="Filter items by their state")] = None, + termination_reason: Annotated[Optional[ItemTerminationReason], Field(description="Filter items by their termination reason. Only applies to TERMINATED items.")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="JSONPath expression to filter items by their custom_metadata")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending)")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List Run Items + + List items in a run with filtering, sorting, and pagination capabilities. Returns paginated items within a specific run. Results can be filtered by `item_id`, `external_ids`, `custom_metadata`, `terminated_at`, and `termination_reason` using JSONPath expressions. ## JSONPath Metadata Filtering Use PostgreSQL JSONPath expressions to filter items using their custom_metadata. ### Examples: - **Field existence**: `$.case_id` - Results that have a case_id field defined - **Exact value match**: `$.priority ? (@ == \"high\")` - Results with high priority - **Numeric comparison**: `$.confidence_score ? (@ > 0.95)` - Results with high confidence - **Array operations**: `$.flags[*] ? (@ == \"reviewed\")` - Results flagged as reviewed - **Complex conditions**: `$.metrics ? (@.accuracy > 0.9 && @.recall > 0.8)` - Results meeting performance thresholds ## Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations + + :param run_id: Run id, returned by `POST /v1/runs/` endpoint (required) + :type run_id: str + :param item_id__in: Filter for item ids + :type item_id__in: List[str] + :param external_id__in: Filter for items by their external_id from the input payload + :type external_id__in: List[str] + :param state: Filter items by their state + :type state: ItemState + :param termination_reason: Filter items by their termination reason. Only applies to TERMINATED items. + :type termination_reason: ItemTerminationReason + :param custom_metadata: JSONPath expression to filter items by their custom_metadata + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param sort: Sort the items by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `item_id` - `external_id` - `custom_metadata` - `terminated_at` - `termination_reason` **Examples:** - `?sort=item_id` - Sort by id of the item (ascending) - `?sort=-external_id` - Sort by external ID (descending) - `?sort=custom_metadata&sort=-external_id` - Sort by metadata, then by external ID (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_run_items_v1_runs_run_id_items_get_serialize( + run_id=run_id, + item_id__in=item_id__in, + external_id__in=external_id__in, + state=state, + termination_reason=termination_reason, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[ItemResultReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_run_items_v1_runs_run_id_items_get_serialize( + self, + run_id, + item_id__in, external_id__in, state, termination_reason, @@ -2876,8 +3768,399 @@ def _list_run_items_v1_runs_run_id_items_get_serialize( _host = None _collection_formats: Dict[str, str] = { - 'item_id__in': 'multi', - 'external_id__in': 'multi', + 'item_id__in': 'multi', + 'external_id__in': 'multi', + 'sort': 'multi', + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if run_id is not None: + _path_params['run_id'] = run_id + # process the query parameters + if item_id__in is not None: + + _query_params.append(('item_id__in', item_id__in)) + + if external_id__in is not None: + + _query_params.append(('external_id__in', external_id__in)) + + if state is not None: + + _query_params.append(('state', state.value)) + + if termination_reason is not None: + + _query_params.append(('termination_reason', termination_reason.value)) + + if custom_metadata is not None: + + _query_params.append(('custom_metadata', custom_metadata)) + + if page is not None: + + _query_params.append(('page', page)) + + if page_size is not None: + + _query_params.append(('page_size', page_size)) + + if sort is not None: + + _query_params.append(('sort', sort)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2AuthorizationCodeBearer' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/api/v1/runs/{run_id}/items', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_runs_v1_runs_get( + self, + application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, + application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, + external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> List[RunReadResponse]: + """List Runs + + List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + + :param application_id: Optional application ID filter + :type application_id: str + :param application_version: Optional Version Name + :type application_version: str + :param external_id: Optionally filter runs by items with this external ID + :type external_id: str + :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. + :type for_organization: str + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_runs_v1_runs_get_serialize( + application_id=application_id, + application_version=application_version, + external_id=external_id, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + for_organization=for_organization, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[RunReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_runs_v1_runs_get_with_http_info( + self, + application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, + application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, + external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[List[RunReadResponse]]: + """List Runs + + List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + + :param application_id: Optional application ID filter + :type application_id: str + :param application_version: Optional Version Name + :type application_version: str + :param external_id: Optionally filter runs by items with this external ID + :type external_id: str + :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. + :type for_organization: str + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_runs_v1_runs_get_serialize( + application_id=application_id, + application_version=application_version, + external_id=external_id, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + for_organization=for_organization, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[RunReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_runs_v1_runs_get_without_preload_content( + self, + application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, + application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, + external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, + custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, + page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, + page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, + for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, + sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List Runs + + List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + + :param application_id: Optional application ID filter + :type application_id: str + :param application_version: Optional Version Name + :type application_version: str + :param external_id: Optionally filter runs by items with this external ID + :type external_id: str + :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** + :type custom_metadata: str + :param page: + :type page: int + :param page_size: + :type page_size: int + :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. + :type for_organization: str + :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) + :type sort: List[str] + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_runs_v1_runs_get_serialize( + application_id=application_id, + application_version=application_version, + external_id=external_id, + custom_metadata=custom_metadata, + page=page, + page_size=page_size, + for_organization=for_organization, + sort=sort, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[RunReadResponse]", + '404': None, + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_runs_v1_runs_get_serialize( + self, + application_id, + application_version, + external_id, + custom_metadata, + page, + page_size, + for_organization, + sort, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { 'sort': 'multi', } @@ -2891,24 +4174,18 @@ def _list_run_items_v1_runs_run_id_items_get_serialize( _body_params: Optional[bytes] = None # process the path parameters - if run_id is not None: - _path_params['run_id'] = run_id # process the query parameters - if item_id__in is not None: - - _query_params.append(('item_id__in', item_id__in)) - - if external_id__in is not None: + if application_id is not None: - _query_params.append(('external_id__in', external_id__in)) + _query_params.append(('application_id', application_id)) - if state is not None: + if application_version is not None: - _query_params.append(('state', state.value)) + _query_params.append(('application_version', application_version)) - if termination_reason is not None: + if external_id is not None: - _query_params.append(('termination_reason', termination_reason.value)) + _query_params.append(('external_id', external_id)) if custom_metadata is not None: @@ -2922,6 +4199,10 @@ def _list_run_items_v1_runs_run_id_items_get_serialize( _query_params.append(('page_size', page_size)) + if for_organization is not None: + + _query_params.append(('for_organization', for_organization)) + if sort is not None: _query_params.append(('sort', sort)) @@ -2947,7 +4228,7 @@ def _list_run_items_v1_runs_run_id_items_get_serialize( return self.api_client.param_serialize( method='GET', - resource_path='/api/v1/runs/{run_id}/items', + resource_path='/api/v1/runs', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -2964,16 +4245,10 @@ def _list_run_items_v1_runs_run_id_items_get_serialize( @validate_call - def list_runs_v1_runs_get( + def list_version_documents( self, - application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, - application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, - external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + application_id: StrictStr, + version: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2986,27 +4261,15 @@ def list_runs_v1_runs_get( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> List[RunReadResponse]: - """List Runs + ) -> List[VersionDocumentResponse]: + """List version documents - List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + List public documents attached to an application version. Returns only documents with ``visibility=public`` and ``status=uploaded``. - :param application_id: Optional application ID filter + :param application_id: (required) :type application_id: str - :param application_version: Optional Version Name - :type application_version: str - :param external_id: Optionally filter runs by items with this external ID - :type external_id: str - :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. - :type for_organization: str - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) - :type sort: List[str] + :param version: (required) + :type version: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3029,15 +4292,9 @@ def list_runs_v1_runs_get( :return: Returns the result object. """ # noqa: E501 - _param = self._list_runs_v1_runs_get_serialize( + _param = self._list_version_documents_serialize( application_id=application_id, - application_version=application_version, - external_id=external_id, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - for_organization=for_organization, - sort=sort, + version=version, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3045,7 +4302,7 @@ def list_runs_v1_runs_get( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[RunReadResponse]", + '200': "List[VersionDocumentResponse]", '404': None, '422': "HTTPValidationError", } @@ -3061,16 +4318,10 @@ def list_runs_v1_runs_get( @validate_call - def list_runs_v1_runs_get_with_http_info( + def list_version_documents_with_http_info( self, - application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, - application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, - external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + application_id: StrictStr, + version: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -3083,27 +4334,15 @@ def list_runs_v1_runs_get_with_http_info( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> ApiResponse[List[RunReadResponse]]: - """List Runs + ) -> ApiResponse[List[VersionDocumentResponse]]: + """List version documents - List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + List public documents attached to an application version. Returns only documents with ``visibility=public`` and ``status=uploaded``. - :param application_id: Optional application ID filter + :param application_id: (required) :type application_id: str - :param application_version: Optional Version Name - :type application_version: str - :param external_id: Optionally filter runs by items with this external ID - :type external_id: str - :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. - :type for_organization: str - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) - :type sort: List[str] + :param version: (required) + :type version: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3126,15 +4365,9 @@ def list_runs_v1_runs_get_with_http_info( :return: Returns the result object. """ # noqa: E501 - _param = self._list_runs_v1_runs_get_serialize( + _param = self._list_version_documents_serialize( application_id=application_id, - application_version=application_version, - external_id=external_id, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - for_organization=for_organization, - sort=sort, + version=version, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3142,7 +4375,7 @@ def list_runs_v1_runs_get_with_http_info( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[RunReadResponse]", + '200': "List[VersionDocumentResponse]", '404': None, '422': "HTTPValidationError", } @@ -3158,16 +4391,10 @@ def list_runs_v1_runs_get_with_http_info( @validate_call - def list_runs_v1_runs_get_without_preload_content( + def list_version_documents_without_preload_content( self, - application_id: Annotated[Optional[StrictStr], Field(description="Optional application ID filter")] = None, - application_version: Annotated[Optional[StrictStr], Field(description="Optional Version Name")] = None, - external_id: Annotated[Optional[StrictStr], Field(description="Optionally filter runs by items with this external ID")] = None, - custom_metadata: Annotated[Optional[Annotated[str, Field(strict=True, max_length=1000)]], Field(description="Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** ")] = None, - page: Optional[Annotated[int, Field(strict=True, ge=1)]] = None, - page_size: Optional[Annotated[int, Field(le=100, strict=True, ge=5)]] = None, - for_organization: Annotated[Optional[StrictStr], Field(description="Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs.")] = None, - sort: Annotated[Optional[List[StrictStr]], Field(description="Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) ")] = None, + application_id: StrictStr, + version: StrictStr, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -3181,26 +4408,14 @@ def list_runs_v1_runs_get_without_preload_content( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """List Runs + """List version documents - List runs with filtering, sorting, and pagination capabilities. Returns paginated runs that were submitted by the user. + List public documents attached to an application version. Returns only documents with ``visibility=public`` and ``status=uploaded``. - :param application_id: Optional application ID filter + :param application_id: (required) :type application_id: str - :param application_version: Optional Version Name - :type application_version: str - :param external_id: Optionally filter runs by items with this external ID - :type external_id: str - :param custom_metadata: Use PostgreSQL JSONPath expressions to filter runs by their custom_metadata. #### URL Encoding Required **Important**: JSONPath expressions contain special characters that must be URL-encoded when used in query parameters. Most HTTP clients handle this automatically, but when constructing URLs manually, please ensure proper encoding. #### Examples (Clear Format): - **Field existence**: `$.study` - Runs that have a study field defined - **Exact value match**: `$.study ? (@ == \"high\")` - Runs with specific study value - **Numeric comparison**: `$.confidence_score ? (@ > 0.75)` - Runs with confidence score greater than 0.75 - **Array operations**: `$.tags[*] ? (@ == \"draft\")` - Runs with tags array containing \"draft\" - **Complex conditions**: `$.resources ? (@.gpu_count > 2 && @.memory_gb >= 16)` - Runs with high resource requirements #### Examples (URL-Encoded Format): - **Field existence**: `%24.study` - **Exact value match**: `%24.study%20%3F%20(%40%20%3D%3D%20%22high%22)` - **Numeric comparison**: `%24.confidence_score%20%3F%20(%40%20%3E%200.75)` - **Array operations**: `%24.tags%5B*%5D%20%3F%20(%40%20%3D%3D%20%22draft%22)` - **Complex conditions**: `%24.resources%20%3F%20(%40.gpu_count%20%3E%202%20%26%26%20%40.memory_gb%20%3E%3D%2016)` #### Notes - JSONPath expressions are evaluated using PostgreSQL's `@?` operator - The `$.` prefix is automatically added to root-level field references if missing - String values in conditions must be enclosed in double quotes - Use `&&` for AND operations and `||` for OR operations - Regular expressions use `like_regex` with standard regex syntax - **Please remember to URL-encode the entire JSONPath expression when making HTTP requests** - :type custom_metadata: str - :param page: - :type page: int - :param page_size: - :type page_size: int - :param for_organization: Filter runs by organization ID. Available for superadmins (any org) and admins (own org only). When provided, returns all runs for the specified organization instead of only the caller's own runs. - :type for_organization: str - :param sort: Sort the results by one or more fields. Use `+` for ascending and `-` for descending order. **Available fields:** - `run_id` - `application_id` - `version_number` - `custom_metadata` - `submitted_at` - `submitted_by` - `terminated_at` - `termination_reason` **Examples:** - `?sort=submitted_at` - Sort by creation time (ascending) - `?sort=-submitted_at` - Sort by creation time (descending) - `?sort=state&sort=-submitted_at` - Sort by state, then by time (descending) - :type sort: List[str] + :param version: (required) + :type version: str :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3223,15 +4438,9 @@ def list_runs_v1_runs_get_without_preload_content( :return: Returns the result object. """ # noqa: E501 - _param = self._list_runs_v1_runs_get_serialize( + _param = self._list_version_documents_serialize( application_id=application_id, - application_version=application_version, - external_id=external_id, - custom_metadata=custom_metadata, - page=page, - page_size=page_size, - for_organization=for_organization, - sort=sort, + version=version, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3239,7 +4448,7 @@ def list_runs_v1_runs_get_without_preload_content( ) _response_types_map: Dict[str, Optional[str]] = { - '200': "List[RunReadResponse]", + '200': "List[VersionDocumentResponse]", '404': None, '422': "HTTPValidationError", } @@ -3250,16 +4459,10 @@ def list_runs_v1_runs_get_without_preload_content( return response_data.response - def _list_runs_v1_runs_get_serialize( + def _list_version_documents_serialize( self, application_id, - application_version, - external_id, - custom_metadata, - page, - page_size, - for_organization, - sort, + version, _request_auth, _content_type, _headers, @@ -3269,7 +4472,6 @@ def _list_runs_v1_runs_get_serialize( _host = None _collection_formats: Dict[str, str] = { - 'sort': 'multi', } _path_params: Dict[str, str] = {} @@ -3282,39 +4484,11 @@ def _list_runs_v1_runs_get_serialize( _body_params: Optional[bytes] = None # process the path parameters - # process the query parameters if application_id is not None: - - _query_params.append(('application_id', application_id)) - - if application_version is not None: - - _query_params.append(('application_version', application_version)) - - if external_id is not None: - - _query_params.append(('external_id', external_id)) - - if custom_metadata is not None: - - _query_params.append(('custom_metadata', custom_metadata)) - - if page is not None: - - _query_params.append(('page', page)) - - if page_size is not None: - - _query_params.append(('page_size', page_size)) - - if for_organization is not None: - - _query_params.append(('for_organization', for_organization)) - - if sort is not None: - - _query_params.append(('sort', sort)) - + _path_params['application_id'] = application_id + if version is not None: + _path_params['version'] = version + # process the query parameters # process the header parameters # process the form parameters # process the body parameter @@ -3336,7 +4510,7 @@ def _list_runs_v1_runs_get_serialize( return self.api_client.param_serialize( method='GET', - resource_path='/api/v1/runs', + resource_path='/api/v1/applications/{application_id}/versions/{version}/documents', path_params=_path_params, query_params=_query_params, header_params=_header_params, diff --git a/codegen/out/aignx/codegen/api_client.py b/codegen/out/aignx/codegen/api_client.py index b56edae2..1f5b8fc8 100644 --- a/codegen/out/aignx/codegen/api_client.py +++ b/codegen/out/aignx/codegen/api_client.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/configuration.py b/codegen/out/aignx/codegen/configuration.py index ac221d92..007c409c 100644 --- a/codegen/out/aignx/codegen/configuration.py +++ b/codegen/out/aignx/codegen/configuration.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. @@ -502,7 +502,7 @@ def to_debug_report(self) -> str: return "Python SDK Debug Report:\n"\ "OS: {env}\n"\ "Python Version: {pyversion}\n"\ - "Version of the API: 1.4.0\n"\ + "Version of the API: 1.5.0\n"\ "SDK Package Version: 1.0.0".\ format(env=sys.platform, pyversion=sys.version) diff --git a/codegen/out/aignx/codegen/exceptions.py b/codegen/out/aignx/codegen/exceptions.py index 4f7dfe5b..0da9a49e 100644 --- a/codegen/out/aignx/codegen/exceptions.py +++ b/codegen/out/aignx/codegen/exceptions.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/__init__.py b/codegen/out/aignx/codegen/models/__init__.py index 382b6ac7..b6d754dc 100644 --- a/codegen/out/aignx/codegen/models/__init__.py +++ b/codegen/out/aignx/codegen/models/__init__.py @@ -9,6 +9,7 @@ from .organization_read_response import * from .validation_error import * from .application_read_response import * +from .version_document_response import * from .application_read_short_response import * from .output_artifact_scope import * from .scheduling_response import * @@ -20,8 +21,10 @@ from .application_version import * from .http_validation_error import * from .custom_metadata_update_response import * +from .version_document_visibility import * from .user_read_response import * from .run_termination_reason import * +from .input_artifact_result_read_response import * from .input_artifact import * from .output_artifact_result_read_response import * from .version_read_response import * diff --git a/codegen/out/aignx/codegen/models/application_read_response.py b/codegen/out/aignx/codegen/models/application_read_response.py index 9fea4396..23abca29 100644 --- a/codegen/out/aignx/codegen/models/application_read_response.py +++ b/codegen/out/aignx/codegen/models/application_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/application_read_short_response.py b/codegen/out/aignx/codegen/models/application_read_short_response.py index da128fa5..7bfce4e2 100644 --- a/codegen/out/aignx/codegen/models/application_read_short_response.py +++ b/codegen/out/aignx/codegen/models/application_read_short_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/application_version.py b/codegen/out/aignx/codegen/models/application_version.py index b7c6b700..fed544e1 100644 --- a/codegen/out/aignx/codegen/models/application_version.py +++ b/codegen/out/aignx/codegen/models/application_version.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/artifact_output.py b/codegen/out/aignx/codegen/models/artifact_output.py index 150698d7..88b21059 100644 --- a/codegen/out/aignx/codegen/models/artifact_output.py +++ b/codegen/out/aignx/codegen/models/artifact_output.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/artifact_state.py b/codegen/out/aignx/codegen/models/artifact_state.py index b90f2272..da25ba6a 100644 --- a/codegen/out/aignx/codegen/models/artifact_state.py +++ b/codegen/out/aignx/codegen/models/artifact_state.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/artifact_termination_reason.py b/codegen/out/aignx/codegen/models/artifact_termination_reason.py index 6f06896b..2e7573e1 100644 --- a/codegen/out/aignx/codegen/models/artifact_termination_reason.py +++ b/codegen/out/aignx/codegen/models/artifact_termination_reason.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/custom_metadata_update_request.py b/codegen/out/aignx/codegen/models/custom_metadata_update_request.py index 5cd0650b..9143c4df 100644 --- a/codegen/out/aignx/codegen/models/custom_metadata_update_request.py +++ b/codegen/out/aignx/codegen/models/custom_metadata_update_request.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/custom_metadata_update_response.py b/codegen/out/aignx/codegen/models/custom_metadata_update_response.py index c1bf3b6d..2cfc00a6 100644 --- a/codegen/out/aignx/codegen/models/custom_metadata_update_response.py +++ b/codegen/out/aignx/codegen/models/custom_metadata_update_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/http_validation_error.py b/codegen/out/aignx/codegen/models/http_validation_error.py index f3834b74..a54b9606 100644 --- a/codegen/out/aignx/codegen/models/http_validation_error.py +++ b/codegen/out/aignx/codegen/models/http_validation_error.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/input_artifact.py b/codegen/out/aignx/codegen/models/input_artifact.py index 02324f48..9cf65222 100644 --- a/codegen/out/aignx/codegen/models/input_artifact.py +++ b/codegen/out/aignx/codegen/models/input_artifact.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/input_artifact_creation_request.py b/codegen/out/aignx/codegen/models/input_artifact_creation_request.py index 80af637a..4a7844e6 100644 --- a/codegen/out/aignx/codegen/models/input_artifact_creation_request.py +++ b/codegen/out/aignx/codegen/models/input_artifact_creation_request.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/input_artifact_result_read_response.py b/codegen/out/aignx/codegen/models/input_artifact_result_read_response.py new file mode 100644 index 00000000..da7fd382 --- /dev/null +++ b/codegen/out/aignx/codegen/models/input_artifact_result_read_response.py @@ -0,0 +1,104 @@ +# coding: utf-8 + +""" + Aignostics Platform API + + The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. + + The version of the OpenAPI document: 1.5.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class InputArtifactResultReadResponse(BaseModel): + """ + InputArtifactResultReadResponse + """ # noqa: E501 + input_artifact_id: StrictStr = Field(description="The Id of the artifact. Used internally") + name: StrictStr = Field(description="Name of the input from the schema from the `/v1/versions/{version_id}` endpoint.") + metadata: Optional[Dict[str, Any]] = None + download_url: Optional[Annotated[str, Field(min_length=1, strict=True, max_length=2083)]] = None + __properties: ClassVar[List[str]] = ["input_artifact_id", "name", "metadata", "download_url"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of InputArtifactResultReadResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if metadata (nullable) is None + # and model_fields_set contains the field + if self.metadata is None and "metadata" in self.model_fields_set: + _dict['metadata'] = None + + # set to None if download_url (nullable) is None + # and model_fields_set contains the field + if self.download_url is None and "download_url" in self.model_fields_set: + _dict['download_url'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of InputArtifactResultReadResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "input_artifact_id": obj.get("input_artifact_id"), + "name": obj.get("name"), + "metadata": obj.get("metadata"), + "download_url": obj.get("download_url") + }) + return _obj + + diff --git a/codegen/out/aignx/codegen/models/item_creation_request.py b/codegen/out/aignx/codegen/models/item_creation_request.py index 593eff96..3f49dc40 100644 --- a/codegen/out/aignx/codegen/models/item_creation_request.py +++ b/codegen/out/aignx/codegen/models/item_creation_request.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/item_output.py b/codegen/out/aignx/codegen/models/item_output.py index d313e54c..127b0372 100644 --- a/codegen/out/aignx/codegen/models/item_output.py +++ b/codegen/out/aignx/codegen/models/item_output.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/item_result_read_response.py b/codegen/out/aignx/codegen/models/item_result_read_response.py index f4f33417..6c8fe46b 100644 --- a/codegen/out/aignx/codegen/models/item_result_read_response.py +++ b/codegen/out/aignx/codegen/models/item_result_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. @@ -20,6 +20,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List, Optional +from aignx.codegen.models.input_artifact_result_read_response import InputArtifactResultReadResponse from aignx.codegen.models.item_output import ItemOutput from aignx.codegen.models.item_state import ItemState from aignx.codegen.models.item_termination_reason import ItemTerminationReason @@ -43,8 +44,9 @@ class ItemResultReadResponse(BaseModel): error_code: Optional[StrictStr] = None error_message: Optional[StrictStr] = None terminated_at: Optional[datetime] = None + input_artifacts: List[InputArtifactResultReadResponse] = Field(description=" The input artifact(s) provided by the user. For most applications, this will be one artifact that defines the whole slide image to be processed. ") output_artifacts: List[OutputArtifactResultReadResponse] = Field(description=" The list of the results generated by the application algorithm. The number of files and their types depend on the particular application version, call `/v1/versions/{version_id}` to get the details. ") - __properties: ClassVar[List[str]] = ["item_id", "external_id", "custom_metadata", "custom_metadata_checksum", "queue_position_org", "queue_position_platform", "state", "output", "termination_reason", "error_code", "error_message", "terminated_at", "output_artifacts"] + __properties: ClassVar[List[str]] = ["item_id", "external_id", "custom_metadata", "custom_metadata_checksum", "queue_position_org", "queue_position_platform", "state", "output", "termination_reason", "error_code", "error_message", "terminated_at", "input_artifacts", "output_artifacts"] model_config = ConfigDict( populate_by_name=True, @@ -85,6 +87,13 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of each item in input_artifacts (list) + _items = [] + if self.input_artifacts: + for _item_input_artifacts in self.input_artifacts: + if _item_input_artifacts: + _items.append(_item_input_artifacts.to_dict()) + _dict['input_artifacts'] = _items # override the default output from pydantic by calling `to_dict()` of each item in output_artifacts (list) _items = [] if self.output_artifacts: @@ -156,6 +165,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "error_code": obj.get("error_code"), "error_message": obj.get("error_message"), "terminated_at": obj.get("terminated_at"), + "input_artifacts": [InputArtifactResultReadResponse.from_dict(_item) for _item in obj["input_artifacts"]] if obj.get("input_artifacts") is not None else None, "output_artifacts": [OutputArtifactResultReadResponse.from_dict(_item) for _item in obj["output_artifacts"]] if obj.get("output_artifacts") is not None else None }) return _obj diff --git a/codegen/out/aignx/codegen/models/item_state.py b/codegen/out/aignx/codegen/models/item_state.py index 8e4928c1..5dfdcdf3 100644 --- a/codegen/out/aignx/codegen/models/item_state.py +++ b/codegen/out/aignx/codegen/models/item_state.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/item_termination_reason.py b/codegen/out/aignx/codegen/models/item_termination_reason.py index 04070e10..7f6d5e6d 100644 --- a/codegen/out/aignx/codegen/models/item_termination_reason.py +++ b/codegen/out/aignx/codegen/models/item_termination_reason.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/me_read_response.py b/codegen/out/aignx/codegen/models/me_read_response.py index b25865ec..dd63abf4 100644 --- a/codegen/out/aignx/codegen/models/me_read_response.py +++ b/codegen/out/aignx/codegen/models/me_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/organization_read_response.py b/codegen/out/aignx/codegen/models/organization_read_response.py index 7b3b5c4e..0f85155c 100644 --- a/codegen/out/aignx/codegen/models/organization_read_response.py +++ b/codegen/out/aignx/codegen/models/organization_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/output_artifact.py b/codegen/out/aignx/codegen/models/output_artifact.py index dd62295a..cc623029 100644 --- a/codegen/out/aignx/codegen/models/output_artifact.py +++ b/codegen/out/aignx/codegen/models/output_artifact.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py b/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py index 131c0101..fd0069b7 100644 --- a/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py +++ b/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/output_artifact_scope.py b/codegen/out/aignx/codegen/models/output_artifact_scope.py index d1f927a4..8e4d0c6d 100644 --- a/codegen/out/aignx/codegen/models/output_artifact_scope.py +++ b/codegen/out/aignx/codegen/models/output_artifact_scope.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/output_artifact_visibility.py b/codegen/out/aignx/codegen/models/output_artifact_visibility.py index 48a346d0..5e6e492d 100644 --- a/codegen/out/aignx/codegen/models/output_artifact_visibility.py +++ b/codegen/out/aignx/codegen/models/output_artifact_visibility.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_creation_request.py b/codegen/out/aignx/codegen/models/run_creation_request.py index c6e7bfb9..e29fabe6 100644 --- a/codegen/out/aignx/codegen/models/run_creation_request.py +++ b/codegen/out/aignx/codegen/models/run_creation_request.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_creation_response.py b/codegen/out/aignx/codegen/models/run_creation_response.py index 7277899f..9868cc07 100644 --- a/codegen/out/aignx/codegen/models/run_creation_response.py +++ b/codegen/out/aignx/codegen/models/run_creation_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_item_statistics.py b/codegen/out/aignx/codegen/models/run_item_statistics.py index f3d24fa0..fd3a98cf 100644 --- a/codegen/out/aignx/codegen/models/run_item_statistics.py +++ b/codegen/out/aignx/codegen/models/run_item_statistics.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_output.py b/codegen/out/aignx/codegen/models/run_output.py index 29837ab7..521bfd3e 100644 --- a/codegen/out/aignx/codegen/models/run_output.py +++ b/codegen/out/aignx/codegen/models/run_output.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_read_response.py b/codegen/out/aignx/codegen/models/run_read_response.py index 0e3ff1a3..9f1a1d55 100644 --- a/codegen/out/aignx/codegen/models/run_read_response.py +++ b/codegen/out/aignx/codegen/models/run_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_state.py b/codegen/out/aignx/codegen/models/run_state.py index 734e5589..c7cabff9 100644 --- a/codegen/out/aignx/codegen/models/run_state.py +++ b/codegen/out/aignx/codegen/models/run_state.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/run_termination_reason.py b/codegen/out/aignx/codegen/models/run_termination_reason.py index 5e700de2..3b116235 100644 --- a/codegen/out/aignx/codegen/models/run_termination_reason.py +++ b/codegen/out/aignx/codegen/models/run_termination_reason.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/scheduling_request.py b/codegen/out/aignx/codegen/models/scheduling_request.py index 6b3be7d0..71237416 100644 --- a/codegen/out/aignx/codegen/models/scheduling_request.py +++ b/codegen/out/aignx/codegen/models/scheduling_request.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/scheduling_response.py b/codegen/out/aignx/codegen/models/scheduling_response.py index fd647acc..c077ab1d 100644 --- a/codegen/out/aignx/codegen/models/scheduling_response.py +++ b/codegen/out/aignx/codegen/models/scheduling_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/user_read_response.py b/codegen/out/aignx/codegen/models/user_read_response.py index 54212242..74f46eee 100644 --- a/codegen/out/aignx/codegen/models/user_read_response.py +++ b/codegen/out/aignx/codegen/models/user_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/validation_error.py b/codegen/out/aignx/codegen/models/validation_error.py index 17fa7ef3..aa8a684a 100644 --- a/codegen/out/aignx/codegen/models/validation_error.py +++ b/codegen/out/aignx/codegen/models/validation_error.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/validation_error_loc_inner.py b/codegen/out/aignx/codegen/models/validation_error_loc_inner.py index 8f81623f..bc7371a6 100644 --- a/codegen/out/aignx/codegen/models/validation_error_loc_inner.py +++ b/codegen/out/aignx/codegen/models/validation_error_loc_inner.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/models/version_document_response.py b/codegen/out/aignx/codegen/models/version_document_response.py new file mode 100644 index 00000000..8053168e --- /dev/null +++ b/codegen/out/aignx/codegen/models/version_document_response.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + Aignostics Platform API + + The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. + + The version of the OpenAPI document: 1.5.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from aignx.codegen.models.version_document_visibility import VersionDocumentVisibility +from typing import Optional, Set +from typing_extensions import Self + +class VersionDocumentResponse(BaseModel): + """ + VersionDocumentResponse + """ # noqa: E501 + id: StrictStr + name: StrictStr + mime_type: StrictStr + visibility: VersionDocumentVisibility + created_at: datetime + updated_at: datetime + __properties: ClassVar[List[str]] = ["id", "name", "mime_type", "visibility", "created_at", "updated_at"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of VersionDocumentResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of VersionDocumentResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "name": obj.get("name"), + "mime_type": obj.get("mime_type"), + "visibility": obj.get("visibility"), + "created_at": obj.get("created_at"), + "updated_at": obj.get("updated_at") + }) + return _obj + + diff --git a/codegen/out/aignx/codegen/models/version_document_visibility.py b/codegen/out/aignx/codegen/models/version_document_visibility.py new file mode 100644 index 00000000..82e5977a --- /dev/null +++ b/codegen/out/aignx/codegen/models/version_document_visibility.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + Aignostics Platform API + + The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. + + The version of the OpenAPI document: 1.5.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class VersionDocumentVisibility(str, Enum): + """ + VersionDocumentVisibility + """ + + """ + allowed enum values + """ + PUBLIC = 'public' + INTERNAL = 'internal' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of VersionDocumentVisibility from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/codegen/out/aignx/codegen/models/version_read_response.py b/codegen/out/aignx/codegen/models/version_read_response.py index 55e4cf4c..5d7ba6b9 100644 --- a/codegen/out/aignx/codegen/models/version_read_response.py +++ b/codegen/out/aignx/codegen/models/version_read_response.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/aignx/codegen/rest.py b/codegen/out/aignx/codegen/rest.py index 824e7f3d..1237dd27 100644 --- a/codegen/out/aignx/codegen/rest.py +++ b/codegen/out/aignx/codegen/rest.py @@ -5,7 +5,7 @@ The Aignostics Platform is a cloud-based service that enables organizations to access advanced computational pathology applications through a secure API. The platform provides standardized access to Aignostics' portfolio of computational pathology solutions, with Atlas H&E-TME serving as an example of the available API endpoints. To begin using the platform, your organization must first be registered by our business support team. If you don't have an account yet, please contact your account manager or email support@aignostics.com to get started. More information about our applications can be found on [https://platform.aignostics.com](https://platform.aignostics.com). **How to authorize and test API endpoints:** 1. Click the \"Authorize\" button in the right corner below 3. Click \"Authorize\" button in the dialog to log in with your Aignostics Platform credentials 4. After successful login, you'll be redirected back and can use \"Try it out\" on any endpoint **Note**: You only need to authorize once per session. The lock icons next to endpoints will show green when authorized. - The version of the OpenAPI document: 1.4.0 + The version of the OpenAPI document: 1.5.0 Generated by OpenAPI Generator (https://openapi-generator.tech) Do not edit the class manually. diff --git a/codegen/out/docs/PublicApi.md b/codegen/out/docs/PublicApi.md index 82fdb6bd..0524b92c 100644 --- a/codegen/out/docs/PublicApi.md +++ b/codegen/out/docs/PublicApi.md @@ -12,9 +12,13 @@ Method | HTTP request | Description [**get_item_by_run_v1_runs_run_id_items_external_id_get**](PublicApi.md#get_item_by_run_v1_runs_run_id_items_external_id_get) | **GET** /v1/runs/{run_id}/items/{external_id} | Get Item By Run [**get_me_v1_me_get**](PublicApi.md#get_me_v1_me_get) | **GET** /v1/me | Get current user [**get_run_v1_runs_run_id_get**](PublicApi.md#get_run_v1_runs_run_id_get) | **GET** /v1/runs/{run_id} | Get run details +[**get_version_document**](PublicApi.md#get_version_document) | **GET** /v1/applications/{application_id}/versions/{version}/documents/{name} | Get version document metadata +[**get_version_document_content**](PublicApi.md#get_version_document_content) | **GET** /v1/applications/{application_id}/versions/{version}/documents/{name}/content | Stream version document content (programmatic) +[**get_version_document_file**](PublicApi.md#get_version_document_file) | **GET** /v1/applications/{application_id}/versions/{version}/documents/{name}/file | Download version document (browser) [**list_applications_v1_applications_get**](PublicApi.md#list_applications_v1_applications_get) | **GET** /v1/applications | List available applications [**list_run_items_v1_runs_run_id_items_get**](PublicApi.md#list_run_items_v1_runs_run_id_items_get) | **GET** /v1/runs/{run_id}/items | List Run Items [**list_runs_v1_runs_get**](PublicApi.md#list_runs_v1_runs_get) | **GET** /v1/runs | List Runs +[**list_version_documents**](PublicApi.md#list_version_documents) | **GET** /v1/applications/{application_id}/versions/{version}/documents | List version documents [**put_item_custom_metadata_by_run_v1_runs_run_id_items_external_id_custom_metadata_put**](PublicApi.md#put_item_custom_metadata_by_run_v1_runs_run_id_items_external_id_custom_metadata_put) | **PUT** /v1/runs/{run_id}/items/{external_id}/custom-metadata | Put Item Custom Metadata By Run [**put_run_custom_metadata_v1_runs_run_id_custom_metadata_put**](PublicApi.md#put_run_custom_metadata_v1_runs_run_id_custom_metadata_put) | **PUT** /v1/runs/{run_id}/custom-metadata | Put Run Custom Metadata [**read_application_by_id_v1_applications_application_id_get**](PublicApi.md#read_application_by_id_v1_applications_application_id_get) | **GET** /v1/applications/{application_id} | Read Application By Id @@ -644,6 +648,243 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **get_version_document** +> VersionDocumentResponse get_version_document(application_id, version, name) + +Get version document metadata + +Return metadata for a single public document attached to an application version. + +### Example + +* OAuth Authentication (OAuth2AuthorizationCodeBearer): + +```python +import aignx.codegen +from aignx.codegen.models.version_document_response import VersionDocumentResponse +from aignx.codegen.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to /api +# See configuration.py for a list of all supported configuration parameters. +configuration = aignx.codegen.Configuration( + host = "/api" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with aignx.codegen.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = aignx.codegen.PublicApi(api_client) + application_id = 'application_id_example' # str | + version = 'version_example' # str | + name = 'name_example' # str | + + try: + # Get version document metadata + api_response = api_instance.get_version_document(application_id, version, name) + print("The response of PublicApi->get_version_document:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PublicApi->get_version_document: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **application_id** | **str**| | + **version** | **str**| | + **name** | **str**| | + +### Return type + +[**VersionDocumentResponse**](VersionDocumentResponse.md) + +### Authorization + +[OAuth2AuthorizationCodeBearer](../README.md#OAuth2AuthorizationCodeBearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Successful Response | - | +**404** | Document not found, not public, or version not accessible | - | +**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_version_document_content** +> get_version_document_content(application_id, version, name) + +Stream version document content (programmatic) + +307 redirect to a short-lived GCS signed URL for streaming document content. Unlike ``/file``, no ``Content-Disposition`` override is set — GCS serves the object body with its stored ``Content-Type``. Intended for programmatic clients that follow redirects and consume the content directly. Response carries ``Cache-Control: no-store``. + +### Example + +* OAuth Authentication (OAuth2AuthorizationCodeBearer): + +```python +import aignx.codegen +from aignx.codegen.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to /api +# See configuration.py for a list of all supported configuration parameters. +configuration = aignx.codegen.Configuration( + host = "/api" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with aignx.codegen.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = aignx.codegen.PublicApi(api_client) + application_id = 'application_id_example' # str | + version = 'version_example' # str | + name = 'name_example' # str | + + try: + # Stream version document content (programmatic) + api_instance.get_version_document_content(application_id, version, name) + except Exception as e: + print("Exception when calling PublicApi->get_version_document_content: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **application_id** | **str**| | + **version** | **str**| | + **name** | **str**| | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2AuthorizationCodeBearer](../README.md#OAuth2AuthorizationCodeBearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**307** | Temporary redirect to signed GCS URL; GCS serves the object with its stored Content-Type | - | +**404** | Document not found, not public, or version not accessible | - | +**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_version_document_file** +> get_version_document_file(application_id, version, name) + +Download version document (browser) + +307 redirect to a short-lived GCS signed URL for downloading a document. The signed URL includes ``response-content-disposition=attachment; filename=\"\"`` so browsers prompt a save-as dialog rather than rendering inline. Response carries ``Cache-Control: no-store``. + +### Example + +* OAuth Authentication (OAuth2AuthorizationCodeBearer): + +```python +import aignx.codegen +from aignx.codegen.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to /api +# See configuration.py for a list of all supported configuration parameters. +configuration = aignx.codegen.Configuration( + host = "/api" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with aignx.codegen.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = aignx.codegen.PublicApi(api_client) + application_id = 'application_id_example' # str | + version = 'version_example' # str | + name = 'name_example' # str | + + try: + # Download version document (browser) + api_instance.get_version_document_file(application_id, version, name) + except Exception as e: + print("Exception when calling PublicApi->get_version_document_file: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **application_id** | **str**| | + **version** | **str**| | + **name** | **str**| | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2AuthorizationCodeBearer](../README.md#OAuth2AuthorizationCodeBearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**307** | Temporary redirect to signed GCS URL with Content-Disposition: attachment | - | +**404** | Document not found, not public, or version not accessible | - | +**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **list_applications_v1_applications_get** > List[ApplicationReadShortResponse] list_applications_v1_applications_get(page=page, page_size=page_size, sort=sort) @@ -911,6 +1152,85 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **list_version_documents** +> List[VersionDocumentResponse] list_version_documents(application_id, version) + +List version documents + +List public documents attached to an application version. Returns only documents with ``visibility=public`` and ``status=uploaded``. + +### Example + +* OAuth Authentication (OAuth2AuthorizationCodeBearer): + +```python +import aignx.codegen +from aignx.codegen.models.version_document_response import VersionDocumentResponse +from aignx.codegen.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to /api +# See configuration.py for a list of all supported configuration parameters. +configuration = aignx.codegen.Configuration( + host = "/api" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with aignx.codegen.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = aignx.codegen.PublicApi(api_client) + application_id = 'application_id_example' # str | + version = 'version_example' # str | + + try: + # List version documents + api_response = api_instance.list_version_documents(application_id, version) + print("The response of PublicApi->list_version_documents:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PublicApi->list_version_documents: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **application_id** | **str**| | + **version** | **str**| | + +### Return type + +[**List[VersionDocumentResponse]**](VersionDocumentResponse.md) + +### Authorization + +[OAuth2AuthorizationCodeBearer](../README.md#OAuth2AuthorizationCodeBearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Successful Response | - | +**404** | Application version not found or not accessible | - | +**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **put_item_custom_metadata_by_run_v1_runs_run_id_items_external_id_custom_metadata_put** > CustomMetadataUpdateResponse put_item_custom_metadata_by_run_v1_runs_run_id_items_external_id_custom_metadata_put(run_id, external_id, custom_metadata_update_request) From 4291ebed563894ee138c831fa37853c1b0f93d7b Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 14:40:06 +0200 Subject: [PATCH 04/16] feat(platform): add Documents resource for application version release documents [PYSDK-122] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the four /api/v1/applications/{id}/versions/{version}/documents endpoints (list, get metadata, /file, /content) added in OpenAPI v1.5.0 through a new Documents resource on the platform module. * New Documents class with list(), details(), download_to_path(), get_download_url(), and get_content_url() methods. The two redirect endpoints (/file and /content) are resolved with allow_redirects=False using the same pattern as Artifact.get_download_url() so the short-lived presigned GCS URL can be inspected before the body is consumed. * New ApplicationVersionDocument Pydantic wrapper around the codegen VersionDocumentResponse model. * Versions.documents(application_id, application_version) returns a bound Documents instance. * list() and details() are cached with the existing application_version_cache_ttl (5 min); the redirect endpoints are intentionally not cached because the URLs are short-lived. * No CRC32C checksum verification on download — PAPI does not expose a checksum for documents; integrity is bounded by HTTPS and signed-URL TTL. * Documents and ApplicationVersionDocument re-exported from the platform package. Unit tests cover happy-path list/details/download/content-url, 404 → NotFoundException, cache hit + nocache=True bypass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aignostics/platform/__init__.py | 3 + .../platform/resources/applications.py | 389 +++++++++++++++++- .../platform/resources/applications_test.py | 234 ++++++++++- 3 files changed, 619 insertions(+), 7 deletions(-) diff --git a/src/aignostics/platform/__init__.py b/src/aignostics/platform/__init__.py index 1fcaf481..fea6f14f 100644 --- a/src/aignostics/platform/__init__.py +++ b/src/aignostics/platform/__init__.py @@ -96,6 +96,7 @@ get_mime_type_for_artifact, mime_type_to_file_ending, ) +from .resources.applications import ApplicationVersionDocument, Documents from .resources.runs import LIST_APPLICATION_RUNS_MAX_PAGE_SIZE, LIST_APPLICATION_RUNS_MIN_PAGE_SIZE, Artifact, Run __all__ = [ @@ -150,9 +151,11 @@ "Application", "ApplicationSummary", "ApplicationVersion", + "ApplicationVersionDocument", "Artifact", "ArtifactOutput", "Client", + "Documents", "ForbiddenException", "InputArtifact", "InputArtifactData", diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index f507ddf4..8a23b39a 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -1,21 +1,28 @@ """Applications resource module for the Aignostics platform. This module provides classes for interacting with application resources in the Aignostics API. -It includes functionality for listing applications and managing application versions. +It includes functionality for listing applications, managing application versions, +and retrieving application version release documents. """ import builtins import typing as t +from datetime import datetime +from http import HTTPStatus from operator import itemgetter +from pathlib import Path +import requests import semver from aignx.codegen.api.public_api import PublicApi -from aignx.codegen.exceptions import NotFoundException, ServiceException +from aignx.codegen.exceptions import ApiException, NotFoundException, ServiceException from aignx.codegen.models import ApplicationReadResponse as Application from aignx.codegen.models import ApplicationReadShortResponse as ApplicationSummary from aignx.codegen.models import ApplicationVersion as VersionTuple +from aignx.codegen.models import VersionDocumentResponse as VersionDocumentData from aignx.codegen.models import VersionReadResponse as ApplicationVersion from loguru import logger +from pydantic import BaseModel, ConfigDict from tenacity import ( RetryCallState, Retrying, @@ -26,11 +33,22 @@ from urllib3.exceptions import IncompleteRead, PoolError, ProtocolError, ProxyError from urllib3.exceptions import TimeoutError as Urllib3TimeoutError +from aignostics.platform._authentication import get_token from aignostics.platform._operation_cache import cached_operation from aignostics.platform._settings import settings from aignostics.platform.resources.utils import paginate from aignostics.utils import user_agent +_REDIRECT_STATUSES = frozenset({ + HTTPStatus.MOVED_PERMANENTLY, + HTTPStatus.FOUND, + HTTPStatus.SEE_OTHER, + HTTPStatus.TEMPORARY_REDIRECT, + HTTPStatus.PERMANENT_REDIRECT, +}) + +_DOCUMENT_DOWNLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MB + RETRYABLE_EXCEPTIONS = ( ServiceException, Urllib3TimeoutError, @@ -223,6 +241,373 @@ def latest(self, application: Application | str, nocache: bool = False) -> Versi sorted_versions = self.list_sorted(application=application, nocache=nocache) return sorted_versions[0] if sorted_versions else None + def documents(self, application_id: str, application_version: VersionTuple | str) -> "Documents": + """Returns a Documents resource bound to the given application version. + + Args: + application_id (str): The ID of the application (e.g. "heta"). + application_version (VersionTuple | str): The application version, either as a + VersionTuple or a semantic version string (e.g. "1.0.0"). + + Returns: + Documents: A Documents resource bound to the (application_id, version) pair. + """ + if isinstance(application_version, VersionTuple): + version_number = application_version.number + else: + version_number = application_version + return Documents(self._api, application_id=application_id, application_version=version_number) + + +class ApplicationVersionDocument(BaseModel): + """Public release document attached to an application version. + + The Aignostics public API exposes only documents with ``visibility=public`` and + ``status=uploaded``. Internal-visibility documents are not surfaced. + """ + + id: str + name: str + mime_type: str + visibility: str + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(populate_by_name=True, validate_assignment=True) + + @classmethod + def from_response(cls, data: VersionDocumentData) -> "ApplicationVersionDocument": + """Build an ApplicationVersionDocument from the codegen response model.""" + return cls( + id=data.id, + name=data.name, + mime_type=data.mime_type, + visibility=data.visibility.value if hasattr(data.visibility, "value") else str(data.visibility), + created_at=data.created_at, + updated_at=data.updated_at, + ) + + +class Documents: + """Resource class for retrieving release documents attached to an application version. + + Backed by ``GET /api/v1/applications/{application_id}/versions/{version}/documents`` + and the per-document ``/{name}``, ``/{name}/file``, and ``/{name}/content`` endpoints. + + The public API exposes only documents with ``visibility=public`` and + ``status=uploaded``. Internal-visibility documents are not surfaced. + + Document downloads do not carry a CRC32C checksum (unlike run artifacts); + integrity is bounded by HTTPS transport and the signed-URL lifetime. + """ + + def __init__(self, api: PublicApi, application_id: str, application_version: str) -> None: + """Initializes the Documents resource bound to an application version. + + Args: + api (PublicApi): The configured API client. + application_id (str): The ID of the application (e.g. "heta"). + application_version (str): The semantic version number (e.g. "1.0.0"). + """ + self._api = api + self.application_id = application_id + self.application_version = application_version + + def list(self, nocache: bool = False) -> builtins.list[ApplicationVersionDocument]: + """List metadata for all public, uploaded release documents for the bound version. + + Retries on network and server errors. Cached for the configured application-version TTL. + + Args: + nocache (bool): If True, skip reading from cache and fetch fresh data from the API. + The fresh result will still be cached for subsequent calls. Defaults to False. + + Returns: + list[ApplicationVersionDocument]: Metadata for each public, uploaded document. + + Raises: + NotFoundException: When the application version does not exist or is not accessible. + aignx.codegen.exceptions.ApiException: If the API request fails. + """ + + @cached_operation(ttl=settings().application_version_cache_ttl, use_token=True) + def list_with_retry(application_id: str, application_version: str) -> builtins.list[VersionDocumentData]: + return Retrying( + retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), + stop=stop_after_attempt(settings().application_version_retry_attempts), + wait=wait_exponential_jitter( + initial=settings().application_version_retry_wait_min, + max=settings().application_version_retry_wait_max, + ), + before_sleep=_log_retry_attempt, + reraise=True, + )( + lambda: self._api.list_version_documents( + application_id=application_id, + version=application_version, + _request_timeout=settings().application_version_timeout, + _headers={"User-Agent": user_agent()}, + ) + ) + + documents = list_with_retry(self.application_id, self.application_version, nocache=nocache) # type: ignore[call-arg] + return [ApplicationVersionDocument.from_response(doc) for doc in (documents or [])] + + def details(self, document_name: str, nocache: bool = False) -> ApplicationVersionDocument: + """Retrieve metadata for a single release document by name. + + Retries on network and server errors. Cached for the configured application-version TTL. + + Args: + document_name (str): The document filename (e.g. "output_description.pdf"). + nocache (bool): If True, skip reading from cache and fetch fresh data from the API. + The fresh result will still be cached for subsequent calls. Defaults to False. + + Returns: + ApplicationVersionDocument: The document metadata. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + aignx.codegen.exceptions.ApiException: If the API request fails. + """ + + @cached_operation(ttl=settings().application_version_cache_ttl, use_token=True) + def details_with_retry( + application_id: str, application_version: str, document_name: str + ) -> VersionDocumentData: + return Retrying( + retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), + stop=stop_after_attempt(settings().application_version_retry_attempts), + wait=wait_exponential_jitter( + initial=settings().application_version_retry_wait_min, + max=settings().application_version_retry_wait_max, + ), + before_sleep=_log_retry_attempt, + reraise=True, + )( + lambda: self._api.get_version_document( + application_id=application_id, + version=application_version, + name=document_name, + _request_timeout=settings().application_version_timeout, + _headers={"User-Agent": user_agent()}, + ) + ) + + data = details_with_retry( # type: ignore[call-arg] + self.application_id, + self.application_version, + document_name, + nocache=nocache, + ) + return ApplicationVersionDocument.from_response(data) + + def _resolve_redirect_url(self, document_name: str, suffix: str) -> str: + """Issue an unredirected GET to ``/file`` or ``/content`` and return the Location URL. + + The generated client cannot be used directly because urllib3 follows the + redirect automatically and would fetch the document body — losing the + short-lived presigned URL exposed in the ``Location`` header. + + Args: + document_name: The document filename. + suffix: Either ``"file"`` (server sets ``Content-Disposition: attachment``) or + ``"content"`` (no ``Content-Disposition``; for inline programmatic use). + + Returns: + str: A time-limited presigned GCS URL. + + Raises: + NotFoundException: 404 — document not found, not public, or not uploaded. + ApiException: Other 4xx (e.g. 403 forbidden, 410 gone). + ServiceException: 5xx, request timeouts, or connection errors + (after retry attempts have been exhausted). + RuntimeError: 3xx response with no Location header, or any other + unexpected status the API contract does not define. + """ + configuration = self._api.api_client.configuration + host = configuration.host.rstrip("/") + endpoint_url = ( + f"{host}/api/v1/applications/{self.application_id}" + f"/versions/{self.application_version}/documents/{document_name}/{suffix}" + ) + proxy = getattr(configuration, "proxy", None) + ssl_ca_cert = getattr(configuration, "ssl_ca_cert", None) + verify_ssl = getattr(configuration, "verify_ssl", True) + ssl_verify: bool | str = ssl_ca_cert or verify_ssl + # Honor the codegen client's token_provider when set: Client.get_api_client() + # wires it up with use_cache=cache_token, so a user who instantiates + # Client(cache_token=False) does not want us to read/write the token cache. + # Fall back to get_token() only when the configuration was built outside + # of Client (e.g. unit tests with bare PublicApi). + token_provider = getattr(configuration, "token_provider", None) or get_token + + return Retrying( + retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), + stop=stop_after_attempt(settings().application_version_retry_attempts), + wait=wait_exponential_jitter( + initial=settings().application_version_retry_wait_min, + max=settings().application_version_retry_wait_max, + ), + before_sleep=_log_retry_attempt, + reraise=True, + )(lambda: self._fetch_redirect_url(endpoint_url, document_name, ssl_verify, proxy, token_provider)) + + def _fetch_redirect_url( + self, + endpoint_url: str, + document_name: str, + ssl_verify: bool | str, + proxy: str | None, + token_provider: t.Callable[[], str], + ) -> str: + """Issue the GET and return the presigned URL from the 3xx Location header.""" + try: + with requests.get( + endpoint_url, + headers={ + "Authorization": f"Bearer {token_provider()}", + "User-Agent": user_agent(), + }, + allow_redirects=False, + timeout=settings().application_version_timeout, + proxies={"http": proxy, "https": proxy} if proxy else None, + verify=ssl_verify, + stream=True, + ) as response: + if response.status_code in _REDIRECT_STATUSES: + location = response.headers.get("Location") + if not location: + msg = ( + f"Redirect response {response.status_code} from documents endpoint " + f"missing Location header for document '{document_name}' on " + f"application '{self.application_id}' version '{self.application_version}'" + ) + raise RuntimeError(msg) + return location + if response.status_code == HTTPStatus.NOT_FOUND: + raise NotFoundException( + status=HTTPStatus.NOT_FOUND.value, + reason=( + f"Document '{document_name}' not found for application " + f"'{self.application_id}' version '{self.application_version}'" + ), + ) + if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: + raise ServiceException(status=response.status_code, reason=response.reason) + if response.status_code >= HTTPStatus.BAD_REQUEST: + raise ApiException(status=response.status_code, reason=response.reason) + msg = ( + f"Unexpected status {response.status_code} from documents endpoint for " + f"document '{document_name}' on application '{self.application_id}' " + f"version '{self.application_version}'; expected a redirect" + ) + raise RuntimeError(msg) + except requests.Timeout as e: + raise ServiceException( + status=HTTPStatus.SERVICE_UNAVAILABLE.value, + reason="Request timed out", + ) from e + except requests.ConnectionError as e: + raise ServiceException( + status=HTTPStatus.SERVICE_UNAVAILABLE.value, + reason="Connection failed", + ) from e + except requests.RequestException as e: + raise ServiceException( + status=HTTPStatus.SERVICE_UNAVAILABLE.value, + reason=f"Request failed: {e}", + ) from e + + def get_content_url(self, document_name: str) -> str: + """Resolve a fresh, short-lived presigned URL for the inline-content endpoint. + + Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/content`` + with ``allow_redirects=False`` and returns the presigned URL from the redirect + ``Location`` header. The response from the resolved URL is served with the stored + ``Content-Type`` and no ``Content-Disposition`` header — intended for programmatic + clients that consume document content inline. The presigned URL is short-lived; + resolve immediately before fetching. + + Args: + document_name (str): The document filename. + + Returns: + str: A time-limited presigned URL. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + ApiException: Other 4xx errors. + ServiceException: 5xx errors, request timeouts, or connection errors after retries. + RuntimeError: 3xx without a Location header, or unexpected non-3xx status. + """ + return self._resolve_redirect_url(document_name, "content") + + def get_download_url(self, document_name: str) -> str: + """Resolve a fresh, short-lived presigned URL for the file (attachment) endpoint. + + Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/file`` + with ``allow_redirects=False`` and returns the presigned URL from the redirect + ``Location`` header. The response from the resolved URL is served with + ``Content-Disposition: attachment; filename="{name}"`` — intended for browser-style + downloads. The presigned URL is short-lived; resolve immediately before fetching. + + Args: + document_name (str): The document filename. + + Returns: + str: A time-limited presigned URL. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + ApiException: Other 4xx errors. + ServiceException: 5xx errors, request timeouts, or connection errors after retries. + RuntimeError: 3xx without a Location header, or unexpected non-3xx status. + """ + return self._resolve_redirect_url(document_name, "file") + + def download_to_path(self, document_name: str, destination: Path | str) -> Path: + """Download a release document file to a local path. + + Follows the platform ``307`` redirect from the ``/file`` endpoint to a short-lived + GCS signed URL with ``Content-Disposition: attachment; filename="{name}"`` and + streams the response body to disk. + + If ``destination`` is a directory, the file is written as + ``{destination}/{document_name}``. If it is a file path, the file is written there + verbatim. Parent directories are created if they do not yet exist. + + Document downloads do not carry a CRC32C checksum (unlike run artifacts); + integrity is bounded by HTTPS transport and the signed-URL lifetime. + + Args: + document_name (str): The document filename. + destination (Path | str): Target file path or directory to write into. + + Returns: + Path: The absolute path to the written file. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + ApiException: Other 4xx errors. + ServiceException: 5xx errors, request timeouts, or connection errors after retries. + requests.HTTPError: If the signed-URL download itself fails. + """ + destination_path = Path(destination) + if destination_path.is_dir() or (not destination_path.exists() and destination_path.suffix == ""): + destination_path = destination_path / document_name + destination_path = destination_path.resolve() + destination_path.parent.mkdir(parents=True, exist_ok=True) + + signed_url = self.get_download_url(document_name) + with requests.get(signed_url, stream=True, timeout=settings().application_version_timeout) as response: + response.raise_for_status() + with destination_path.open("wb") as out_file: + for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): + if chunk: + out_file.write(chunk) + return destination_path + class Applications: """Resource class for managing applications. diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index 2cf1ab60..a841594b 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -1,16 +1,29 @@ """Tests for the applications resource module. -This module contains unit tests for the Applications and Versions classes, -verifying their functionality for listing applications and application versions. +This module contains unit tests for the Applications, Versions, and Documents +classes, verifying their functionality for listing applications, application +versions, and application version release documents. """ -from unittest.mock import Mock +from datetime import UTC, datetime +from http import HTTPStatus +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch import pytest from aignx.codegen.api.public_api import PublicApi +from aignx.codegen.exceptions import NotFoundException from aignx.codegen.models.application_read_response import ApplicationReadResponse - -from aignostics.platform.resources.applications import Applications, Versions +from aignx.codegen.models.version_document_response import VersionDocumentResponse +from aignx.codegen.models.version_document_visibility import VersionDocumentVisibility + +from aignostics.platform._operation_cache import operation_cache_clear +from aignostics.platform.resources.applications import ( + Applications, + ApplicationVersionDocument, + Documents, + Versions, +) from aignostics.platform.resources.utils import PAGE_SIZE API_ERROR = "API error" @@ -162,3 +175,214 @@ def test_versions_property_returns_versions_instance(applications) -> None: # Assert assert isinstance(versions, Versions) assert versions._api == applications._api + + +# ---------------------------------------------------------------------------------- +# Documents resource tests +# ---------------------------------------------------------------------------------- + + +def _make_doc(name: str = "output_description.pdf") -> VersionDocumentResponse: + """Build a VersionDocumentResponse codegen model for tests.""" + return VersionDocumentResponse( + id="11111111-1111-1111-1111-111111111111", + name=name, + mime_type="application/pdf", + visibility=VersionDocumentVisibility.PUBLIC, + created_at=datetime(2026, 1, 1, 12, 0, tzinfo=UTC), + updated_at=datetime(2026, 1, 2, 12, 0, tzinfo=UTC), + ) + + +@pytest.fixture(autouse=True) +def _clear_operation_cache_before_each_test() -> None: + """Ensure the global operation cache does not leak between tests.""" + operation_cache_clear() + + +@pytest.fixture +def documents(mock_api: Mock) -> Documents: + """Create a Documents instance bound to a fixed (application, version) pair. + + The mock is augmented with a minimal ``api_client.configuration`` so the + redirect-resolving methods (``download_to_path``, ``get_*_url``) can read + host/proxy/SSL settings without hitting the real codegen plumbing. + """ + configuration = MagicMock() + configuration.host = "https://platform.example.com" + configuration.proxy = None + configuration.ssl_ca_cert = None + configuration.verify_ssl = True + configuration.token_provider = lambda: "test-token" # noqa: PIE807 + mock_api.api_client = MagicMock() + mock_api.api_client.configuration = configuration + return Documents(mock_api, application_id="heta", application_version="1.0.0") + + +@pytest.mark.unit +def test_documents_list_returns_wrapped_models(documents: Documents, mock_api: Mock) -> None: + """Documents.list() returns ApplicationVersionDocument instances.""" + mock_api.list_version_documents.return_value = [_make_doc("a.pdf"), _make_doc("b.pdf")] + + result = documents.list() + + assert len(result) == 2 + assert all(isinstance(item, ApplicationVersionDocument) for item in result) + assert {d.name for d in result} == {"a.pdf", "b.pdf"} + assert result[0].visibility == "public" + mock_api.list_version_documents.assert_called_once() + call_kwargs = mock_api.list_version_documents.call_args.kwargs + assert call_kwargs["application_id"] == "heta" + assert call_kwargs["version"] == "1.0.0" + + +@pytest.mark.unit +def test_documents_list_returns_empty_list(documents: Documents, mock_api: Mock) -> None: + """Documents.list() handles an empty response.""" + mock_api.list_version_documents.return_value = [] + + result = documents.list() + + assert result == [] + + +@pytest.mark.unit +def test_documents_list_uses_cache_then_bypasses_with_nocache( + documents: Documents, mock_api: Mock +) -> None: + """list() caches results across calls; nocache=True forces a fresh call.""" + mock_api.list_version_documents.return_value = [_make_doc("a.pdf")] + + # First call hits the API and caches. + documents.list() + # Second call returns cached value. + documents.list() + assert mock_api.list_version_documents.call_count == 1 + + # nocache=True bypasses the cache and re-fetches. + documents.list(nocache=True) + assert mock_api.list_version_documents.call_count == 2 + + +@pytest.mark.unit +def test_documents_details_returns_wrapped_model(documents: Documents, mock_api: Mock) -> None: + """Documents.details() wraps the response in ApplicationVersionDocument.""" + mock_api.get_version_document.return_value = _make_doc("output_description.pdf") + + result = documents.details("output_description.pdf") + + assert isinstance(result, ApplicationVersionDocument) + assert result.name == "output_description.pdf" + assert result.mime_type == "application/pdf" + call_kwargs = mock_api.get_version_document.call_args.kwargs + assert call_kwargs["application_id"] == "heta" + assert call_kwargs["version"] == "1.0.0" + assert call_kwargs["name"] == "output_description.pdf" + + +@pytest.mark.unit +def test_documents_details_propagates_not_found(documents: Documents, mock_api: Mock) -> None: + """Documents.details() propagates a 404 NotFoundException from the codegen client.""" + mock_api.get_version_document.side_effect = NotFoundException(status=404, reason="Not Found") + + with pytest.raises(NotFoundException): + documents.details("missing.pdf") + + +@pytest.mark.unit +def test_documents_get_download_url_resolves_redirect(documents: Documents) -> None: + """get_download_url() returns the Location header from a 307 redirect on /file.""" + mock_response = MagicMock() + mock_response.status_code = HTTPStatus.TEMPORARY_REDIRECT + mock_response.headers = {"Location": "https://signed.example/blob?token=abc"} + mock_response.__enter__.return_value = mock_response + mock_response.__exit__.return_value = False + + with patch( + "aignostics.platform.resources.applications.requests.get", return_value=mock_response + ) as mock_get: + url = documents.get_download_url("output_description.pdf") + + assert url == "https://signed.example/blob?token=abc" + called_url = mock_get.call_args.args[0] + assert called_url.endswith( + "/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file" + ) + assert mock_get.call_args.kwargs["allow_redirects"] is False + + +@pytest.mark.unit +def test_documents_get_content_url_resolves_redirect(documents: Documents) -> None: + """get_content_url() targets the /content variant and returns its Location.""" + mock_response = MagicMock() + mock_response.status_code = HTTPStatus.TEMPORARY_REDIRECT + mock_response.headers = {"Location": "https://signed.example/content?token=def"} + mock_response.__enter__.return_value = mock_response + mock_response.__exit__.return_value = False + + with patch( + "aignostics.platform.resources.applications.requests.get", return_value=mock_response + ) as mock_get: + url = documents.get_content_url("output_description.pdf") + + assert url == "https://signed.example/content?token=def" + assert mock_get.call_args.args[0].endswith( + "/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/content" + ) + + +@pytest.mark.unit +def test_documents_get_download_url_404_raises_not_found(documents: Documents) -> None: + """A 404 from the redirect endpoint is mapped to NotFoundException.""" + mock_response = MagicMock() + mock_response.status_code = HTTPStatus.NOT_FOUND + mock_response.headers = {} + mock_response.reason = "Not Found" + mock_response.__enter__.return_value = mock_response + mock_response.__exit__.return_value = False + + with ( + patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response), + pytest.raises(NotFoundException), + ): + documents.get_download_url("missing.pdf") + + +@pytest.mark.unit +def test_documents_download_to_path_writes_file( + documents: Documents, tmp_path: Path +) -> None: + """download_to_path() resolves the redirect and streams the body to disk.""" + redirect_response = MagicMock() + redirect_response.status_code = HTTPStatus.TEMPORARY_REDIRECT + redirect_response.headers = {"Location": "https://signed.example/blob"} + redirect_response.__enter__.return_value = redirect_response + redirect_response.__exit__.return_value = False + + body_response = MagicMock() + body_response.iter_content.return_value = [b"hello ", b"world"] + body_response.raise_for_status = MagicMock() + body_response.__enter__.return_value = body_response + body_response.__exit__.return_value = False + + with patch( + "aignostics.platform.resources.applications.requests.get", + side_effect=[redirect_response, body_response], + ): + result = documents.download_to_path("output_description.pdf", tmp_path) + + assert result == (tmp_path / "output_description.pdf").resolve() + assert result.read_bytes() == b"hello world" + + +@pytest.mark.unit +def test_versions_documents_returns_documents_resource(mock_api: Mock) -> None: + """Versions.documents() returns a Documents instance bound to the version pair.""" + versions = Versions(mock_api) + + docs = versions.documents("heta", "1.0.0") + + assert isinstance(docs, Documents) + assert docs.application_id == "heta" + assert docs.application_version == "1.0.0" + assert docs._api is mock_api From 969aaa86dbf418adc3b5f994e09f9859bb8c599e Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 14:44:02 +0200 Subject: [PATCH 05/16] feat(application): add CLI version document {list,describe,download} commands [PYSDK-122] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the new platform Documents resource into the application CLI so users can discover and download an application version's public release documents without writing Python. * New `aignostics application version` Typer subgroup (mirrors `run`). * Nested `aignostics application version document` subgroup with: * `list APPLICATION_VERSION_ID` — tabulated metadata (id, name, mime type, timestamps), supports `--format text|json`. * `describe APPLICATION_VERSION_ID DOCUMENT_NAME` — full metadata for one document, supports `--format text|json`. Maps NotFoundException to exit code 2 with `Document '{name}' not found for application version '{ver}'.` * `download APPLICATION_VERSION_ID DOCUMENT_NAME [--output PATH]` — streams the file to disk via the platform 307 redirect; default destination is the current working directory using the document's server-provided filename. APPLICATION_VERSION_ID accepts either `application_id` (latest version is resolved via Versions.latest) or `application_id:version_number` for an explicit semantic version pin. Unit tests cover the four BDD scenarios from TC-APPLICATION-CLI-05: list, describe, describe-not-found, and download. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aignostics/application/_cli.py | 222 +++++++++++++++++++++++ tests/aignostics/application/cli_test.py | 146 +++++++++++++++ 2 files changed, 368 insertions(+) diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index a7d4132d..70eaa0a1 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -19,6 +19,7 @@ DEFAULT_GPU_TYPE, DEFAULT_MAX_GPUS_PER_SLIDE, DEFAULT_NODE_ACQUISITION_TIMEOUT_MINUTES, + Client, ForbiddenException, NotFoundException, RunState, @@ -129,6 +130,37 @@ result_app = typer.Typer() run_app.add_typer(result_app, name="result", help="Download or delete run results.") +version_app = typer.Typer() +cli.add_typer(version_app, name="version", help="Inspect application versions and their release documents.") + +document_app = typer.Typer() +version_app.add_typer( + document_app, + name="document", + help=( + "List, describe, and download public release documents attached to an " + "application version (e.g. output schemas, model manuals)." + ), +) + + +def _parse_application_version_id(application_version_id: str) -> tuple[str, str | None]: + """Parse a CLI APPLICATION_VERSION_ID positional into (application_id, version_number). + + Accepts either ``application_id`` (latest version is resolved later) or + ``application_id:version_number`` (explicit semantic version). + + Args: + application_version_id: The raw CLI argument, e.g. ``"heta"`` or ``"heta:1.0.0"``. + + Returns: + tuple[str, str | None]: (application_id, version_number_or_None_for_latest). + """ + if ":" in application_version_id: + app_id, _, version = application_version_id.partition(":") + return app_id, (version or None) + return application_version_id, None + def _abort_if_system_unhealthy() -> None: health = asyncio.run(SystemService.health_static()) @@ -1523,3 +1555,193 @@ def result_delete( logger.exception(f"Failed to delete run with ID '{run_id}'") console.print(f"[bold red]Error:[/bold red] Failed to delete results for with ID '{run_id}': {e}") sys.exit(1) + + +_APPLICATION_VERSION_ID_HELP = ( + "Application version identifier. Either an application id (e.g. 'heta' — uses the " + "latest version) or 'application_id:version_number' (e.g. 'heta:1.0.0')." +) + + +def _resolve_documents(application_version_id: str) -> tuple[str, str, "object"]: + """Resolve a CLI APPLICATION_VERSION_ID into (application_id, version_number, Documents). + + Returns: + (application_id, version_number, documents_resource). + + Raises: + NotFoundException: If the application or its versions cannot be located. + """ + application_id, version_number = _parse_application_version_id(application_version_id) + client = Client() + if version_number is None: + # Resolve the latest version for the application. + version = client.applications.versions.latest(application=application_id) + if version is None: + raise NotFoundException( + status=404, + reason=f"No versions found for application '{application_id}'.", + ) + version_number = version.number + documents = client.applications.versions.documents(application_id, version_number) + return application_id, version_number, documents + + +@document_app.command("list") +def application_version_document_list( + application_version_id: Annotated[ + str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) + ], + format: Annotated[ # noqa: A002 + str, + typer.Option(help="Output format: 'text' (default) or 'json'"), + ] = "text", +) -> None: + """List public release documents attached to an application version.""" + try: + application_id, version_number, documents = _resolve_documents(application_version_id) + items = documents.list() + except NotFoundException as e: + message = ( + f"No release documents found: application version '{application_version_id}' is unavailable." + ) + logger.warning("{} ({})", message, e) + if format == "json": + print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) + else: + console.print(f"[warning]Warning:[/warning] {message}") + sys.exit(2) + except Exception as e: + logger.exception(f"Failed to list release documents for '{application_version_id}'") + if format == "json": + print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) + else: + console.print( + f"[error]Error:[/error] Failed to list release documents for " + f"'{application_version_id}': {e}" + ) + sys.exit(1) + + if format == "json": + payload = [doc.model_dump(mode="json") for doc in items] + print(json.dumps(payload, indent=2, default=str)) + return + + console.print( + f"[bold]Release documents for {application_id} {version_number}[/bold]" + ) + console.print("=" * 80) + if not items: + console.print("[dim]No public release documents are attached to this version.[/dim]") + return + for doc in items: + console.print(f"[bold]{doc.name}[/bold]") + console.print(f" Id: {doc.id}") + console.print(f" MIME type: {doc.mime_type}") + console.print(f" Created at: {doc.created_at.isoformat()}") + console.print(f" Updated at: {doc.updated_at.isoformat()}") + + +@document_app.command("describe") +def application_version_document_describe( + application_version_id: Annotated[ + str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) + ], + document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], + format: Annotated[ # noqa: A002 + str, + typer.Option(help="Output format: 'text' (default) or 'json'"), + ] = "text", +) -> None: + """Show metadata for a single public release document.""" + try: + application_id, version_number, documents = _resolve_documents(application_version_id) + doc = documents.details(document_name) + except NotFoundException: + message = ( + f"Document '{document_name}' not found for application version " + f"'{application_version_id}'." + ) + logger.warning(message) + if format == "json": + print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) + else: + console.print(f"[warning]Warning:[/warning] {message}") + sys.exit(2) + except Exception as e: + logger.exception( + f"Failed to describe release document '{document_name}' for '{application_version_id}'" + ) + if format == "json": + print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) + else: + console.print( + f"[error]Error:[/error] Failed to describe release document " + f"'{document_name}' for '{application_version_id}': {e}" + ) + sys.exit(1) + + if format == "json": + print(json.dumps(doc.model_dump(mode="json"), indent=2, default=str)) + return + + console.print( + f"[bold]Release document '{doc.name}' on {application_id} {version_number}[/bold]" + ) + console.print("=" * 80) + console.print(f"[bold]Id:[/bold] {doc.id}") + console.print(f"[bold]Name:[/bold] {doc.name}") + console.print(f"[bold]MIME type:[/bold] {doc.mime_type}") + console.print(f"[bold]Visibility:[/bold] {doc.visibility}") + console.print(f"[bold]Created at:[/bold] {doc.created_at.isoformat()}") + console.print(f"[bold]Updated at:[/bold] {doc.updated_at.isoformat()}") + + +@document_app.command("download") +def application_version_document_download( + application_version_id: Annotated[ + str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) + ], + document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], + output: Annotated[ + Path, + typer.Option( + "--output", + help="Destination file path or directory. Defaults to the current working directory.", + file_okay=True, + dir_okay=True, + writable=True, + resolve_path=True, + show_default="", + ), + ] = Path().cwd(), # noqa: B008 +) -> None: + """Download a public release document to a local path. + + The platform serves the document via a short-lived signed URL with + ``Content-Disposition: attachment``; the file is written using the document's name. + Document downloads do not carry a CRC32C checksum (unlike run artifacts); integrity + is bounded by HTTPS transport and the signed-URL lifetime. + """ + try: + _application_id, _version_number, documents = _resolve_documents(application_version_id) + written = documents.download_to_path(document_name, output) + except NotFoundException: + message = ( + f"Document '{document_name}' not found for application version " + f"'{application_version_id}'." + ) + logger.warning(message) + console.print(f"[warning]Warning:[/warning] {message}") + sys.exit(2) + except Exception as e: + logger.exception( + f"Failed to download release document '{document_name}' for '{application_version_id}'" + ) + console.print( + f"[error]Error:[/error] Failed to download release document " + f"'{document_name}' for '{application_version_id}': {e}" + ) + sys.exit(1) + + console.print(f"Downloaded '{document_name}' to {written}") diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index 412e2e8d..806f3d60 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -1668,3 +1668,149 @@ def test_cli_json_format_and_cancel_by_filter_with_dry_run( # noqa: PLR0915, PL f"Run {idx} has unexpected termination reason: {described_run.get('termination_reason')}" ) logger.info("Run {} successfully canceled (state: TERMINATED, reason: CANCELED_BY_USER)", idx) + + +# ---------------------------------------------------------------------------------- +# Application version document CLI tests (TC-APPLICATION-CLI-05) +# ---------------------------------------------------------------------------------- + + +def _make_document_stub(name: str = "output_description.pdf") -> MagicMock: + """Create a stub ApplicationVersionDocument with realistic field values.""" + stub = MagicMock() + stub.id = "11111111-1111-1111-1111-111111111111" + stub.name = name + stub.mime_type = "application/pdf" + stub.visibility = "public" + stub.created_at = datetime(2026, 1, 1, 12, 0, tzinfo=UTC) + stub.updated_at = datetime(2026, 1, 2, 12, 0, tzinfo=UTC) + stub.model_dump.return_value = { + "id": stub.id, + "name": stub.name, + "mime_type": stub.mime_type, + "visibility": stub.visibility, + "created_at": stub.created_at.isoformat(), + "updated_at": stub.updated_at.isoformat(), + } + return stub + + +@pytest.mark.unit +def test_cli_application_version_document_list_success( + runner: CliRunner, record_property +) -> None: + """`application version document list` prints document metadata.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_documents = MagicMock() + fake_documents.list.return_value = [ + _make_document_stub("output_description.pdf"), + _make_document_stub("model_card.pdf"), + ] + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = "1.0.0" + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch("aignostics.application._cli.Client", return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta"]) + + assert result.exit_code == 0 + output = normalize_output(result.output) + assert "output_description.pdf" in output + assert "model_card.pdf" in output + assert "application/pdf" in output + fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") + + +@pytest.mark.unit +def test_cli_application_version_document_describe_success( + runner: CliRunner, record_property +) -> None: + """`application version document describe` prints metadata for a single document.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-02") + fake_documents = MagicMock() + fake_documents.details.return_value = _make_document_stub("output_description.pdf") + fake_client = MagicMock() + fake_client.applications.versions.documents.return_value = fake_documents + + with patch("aignostics.application._cli.Client", return_value=fake_client): + result = runner.invoke( + cli, + ["application", "version", "document", "describe", "heta:1.0.0", "output_description.pdf"], + ) + + assert result.exit_code == 0 + output = normalize_output(result.output) + assert "output_description.pdf" in output + assert "application/pdf" in output + # Explicit version supplied via "heta:1.0.0", so latest() should NOT be called. + fake_client.applications.versions.latest.assert_not_called() + fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") + fake_documents.details.assert_called_once_with("output_description.pdf") + + +@pytest.mark.unit +def test_cli_application_version_document_describe_not_found( + runner: CliRunner, record_property +) -> None: + """`application version document describe` exits 2 with a clear message on 404.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + + fake_documents = MagicMock() + fake_documents.details.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = "1.0.0" + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch("aignostics.application._cli.Client", return_value=fake_client): + result = runner.invoke( + cli, + ["application", "version", "document", "describe", "heta", "missing.pdf"], + ) + + assert result.exit_code == 2 + output = normalize_output(result.output) + assert "Document 'missing.pdf' not found for application version 'heta'." in output + + +@pytest.mark.unit +def test_cli_application_version_document_download_success( + runner: CliRunner, tmp_path: Path, record_property +) -> None: + """`application version document download` writes the file and prints the destination.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") + fake_documents = MagicMock() + expected_path = tmp_path / "output_description.pdf" + fake_documents.download_to_path.return_value = expected_path + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = "1.0.0" + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch("aignostics.application._cli.Client", return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "download", + "heta", + "output_description.pdf", + "--output", + str(tmp_path), + ], + ) + + assert result.exit_code == 0 + output = normalize_output(result.output) + assert str(expected_path) in output + fake_documents.download_to_path.assert_called_once() + args, _ = fake_documents.download_to_path.call_args + assert args[0] == "output_description.pdf" + From 43b382fd27b4d20a17c97388241aa06b7bd93ee2 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 15:01:58 +0200 Subject: [PATCH 06/16] chore: bump SPEC dates and fix lint/test fallout from documents codegen [PYSDK-122] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump Date / Version frontmatter on SPEC-APPLICATION-SERVICE and SPEC_PLATFORM_SERVICE to today's date. * Fix lint findings surfaced by `make lint` after the codegen + Documents resource changes (DOC201 / DOC501 / PLC1901 / PLR6104 / formatting + pyright reportCallIssue on cached_operation's injected `nocache` kwarg). * Add the new required `input_artifacts` field that the v1.5.0 ItemResultReadResponse model now demands to the test helper in tests/aignostics/application/utils_test.py — without it three utils_test cases fail Pydantic validation. `make lint`, `make test_unit`, and `make audit` all pass after this change. `make test_integration` reports one pre-existing GUI-pagination failure that hits the production API without a JWT and was already failing on the parent commit (no relation to this CR). The CHANGELOG.md is generated by git-cliff at release time; the per-CR entry will be produced from the conventional-commit messages in this branch when `make publish-release` runs, so no manual entry is added. Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/SPEC-APPLICATION-SERVICE.md | 4 +- specifications/SPEC_PLATFORM_SERVICE.md | 4 +- src/aignostics/application/_cli.py | 50 +++++-------------- .../platform/resources/applications.py | 42 ++++++++++++---- tests/aignostics/application/cli_test.py | 17 ++----- tests/aignostics/application/utils_test.py | 2 + .../platform/resources/applications_test.py | 22 +++----- 7 files changed, 60 insertions(+), 81 deletions(-) diff --git a/specifications/SPEC-APPLICATION-SERVICE.md b/specifications/SPEC-APPLICATION-SERVICE.md index 0cc6a88a..1b684fe9 100644 --- a/specifications/SPEC-APPLICATION-SERVICE.md +++ b/specifications/SPEC-APPLICATION-SERVICE.md @@ -5,8 +5,8 @@ itemType: Software Item Spec itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-1-3, SWR-APPLICATION-2-3, SWR-APPLICATION-2-4, SHR-APPLICATION-3, SWR-APPLICATION-2-12, SWR-APPLICATION-2-11, SWR-APPLICATION-2-13, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-2-5, SWR-APPLICATION-2-7, SWR-APPLICATION-2-8, SWR-APPLICATION-2-9, SWR-APPLICATION-3-3 Module: Application Layer: Domain Service -Version: 0.2.106 -Date: 2025-09-09 +Version: 0.2.107 +Date: 2026-04-29 --- ## 1. Description diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index c2f4a188..193f27e1 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -5,8 +5,8 @@ itemType: Software Item Spec itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-1-3, SWR-APPLICATION-2-1, SWR-APPLICATION-2-5, SWR-APPLICATION-2-6, SWR-APPLICATION-2-7, SWR-APPLICATION-2-9, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-3-1, SWR-APPLICATION-3-2, SWR-APPLICATION-3-3 Module: Platform Layer: Platform Service -Version: 1.0.0 -Date: 2025-09-09 +Version: 1.1.0 +Date: 2026-04-29 --- ## 1. Description diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index 70eaa0a1..2d5acc72 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -20,6 +20,7 @@ DEFAULT_MAX_GPUS_PER_SLIDE, DEFAULT_NODE_ACQUISITION_TIMEOUT_MINUTES, Client, + Documents, ForbiddenException, NotFoundException, RunState, @@ -1563,7 +1564,7 @@ def result_delete( ) -def _resolve_documents(application_version_id: str) -> tuple[str, str, "object"]: +def _resolve_documents(application_version_id: str) -> tuple[str, str, Documents]: """Resolve a CLI APPLICATION_VERSION_ID into (application_id, version_number, Documents). Returns: @@ -1589,9 +1590,7 @@ def _resolve_documents(application_version_id: str) -> tuple[str, str, "object"] @document_app.command("list") def application_version_document_list( - application_version_id: Annotated[ - str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) - ], + application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], format: Annotated[ # noqa: A002 str, typer.Option(help="Output format: 'text' (default) or 'json'"), @@ -1602,9 +1601,7 @@ def application_version_document_list( application_id, version_number, documents = _resolve_documents(application_version_id) items = documents.list() except NotFoundException as e: - message = ( - f"No release documents found: application version '{application_version_id}' is unavailable." - ) + message = f"No release documents found: application version '{application_version_id}' is unavailable." logger.warning("{} ({})", message, e) if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) @@ -1616,10 +1613,7 @@ def application_version_document_list( if format == "json": print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) else: - console.print( - f"[error]Error:[/error] Failed to list release documents for " - f"'{application_version_id}': {e}" - ) + console.print(f"[error]Error:[/error] Failed to list release documents for '{application_version_id}': {e}") sys.exit(1) if format == "json": @@ -1627,9 +1621,7 @@ def application_version_document_list( print(json.dumps(payload, indent=2, default=str)) return - console.print( - f"[bold]Release documents for {application_id} {version_number}[/bold]" - ) + console.print(f"[bold]Release documents for {application_id} {version_number}[/bold]") console.print("=" * 80) if not items: console.print("[dim]No public release documents are attached to this version.[/dim]") @@ -1644,9 +1636,7 @@ def application_version_document_list( @document_app.command("describe") def application_version_document_describe( - application_version_id: Annotated[ - str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) - ], + application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], format: Annotated[ # noqa: A002 str, @@ -1658,10 +1648,7 @@ def application_version_document_describe( application_id, version_number, documents = _resolve_documents(application_version_id) doc = documents.details(document_name) except NotFoundException: - message = ( - f"Document '{document_name}' not found for application version " - f"'{application_version_id}'." - ) + message = f"Document '{document_name}' not found for application version '{application_version_id}'." logger.warning(message) if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) @@ -1669,9 +1656,7 @@ def application_version_document_describe( console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) except Exception as e: - logger.exception( - f"Failed to describe release document '{document_name}' for '{application_version_id}'" - ) + logger.exception(f"Failed to describe release document '{document_name}' for '{application_version_id}'") if format == "json": print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) else: @@ -1685,9 +1670,7 @@ def application_version_document_describe( print(json.dumps(doc.model_dump(mode="json"), indent=2, default=str)) return - console.print( - f"[bold]Release document '{doc.name}' on {application_id} {version_number}[/bold]" - ) + console.print(f"[bold]Release document '{doc.name}' on {application_id} {version_number}[/bold]") console.print("=" * 80) console.print(f"[bold]Id:[/bold] {doc.id}") console.print(f"[bold]Name:[/bold] {doc.name}") @@ -1699,9 +1682,7 @@ def application_version_document_describe( @document_app.command("download") def application_version_document_download( - application_version_id: Annotated[ - str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP) - ], + application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], output: Annotated[ Path, @@ -1727,17 +1708,12 @@ def application_version_document_download( _application_id, _version_number, documents = _resolve_documents(application_version_id) written = documents.download_to_path(document_name, output) except NotFoundException: - message = ( - f"Document '{document_name}' not found for application version " - f"'{application_version_id}'." - ) + message = f"Document '{document_name}' not found for application version '{application_version_id}'." logger.warning(message) console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) except Exception as e: - logger.exception( - f"Failed to download release document '{document_name}' for '{application_version_id}'" - ) + logger.exception(f"Failed to download release document '{document_name}' for '{application_version_id}'") console.print( f"[error]Error:[/error] Failed to download release document " f"'{document_name}' for '{application_version_id}': {e}" diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index 8a23b39a..736813b3 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -277,7 +277,14 @@ class ApplicationVersionDocument(BaseModel): @classmethod def from_response(cls, data: VersionDocumentData) -> "ApplicationVersionDocument": - """Build an ApplicationVersionDocument from the codegen response model.""" + """Build an ApplicationVersionDocument from the codegen response model. + + Args: + data: The codegen ``VersionDocumentResponse`` returned by the API. + + Returns: + ApplicationVersionDocument: Wrapped, SDK-friendly Pydantic model. + """ return cls( id=data.id, name=data.name, @@ -350,7 +357,7 @@ def list_with_retry(application_id: str, application_version: str) -> builtins.l ) ) - documents = list_with_retry(self.application_id, self.application_version, nocache=nocache) # type: ignore[call-arg] + documents = list_with_retry(self.application_id, self.application_version, nocache=nocache) # type: ignore[call-arg] # pyright: ignore[reportCallIssue] return [ApplicationVersionDocument.from_response(doc) for doc in (documents or [])] def details(self, document_name: str, nocache: bool = False) -> ApplicationVersionDocument: @@ -394,12 +401,8 @@ def details_with_retry( ) ) - data = details_with_retry( # type: ignore[call-arg] - self.application_id, - self.application_version, - document_name, - nocache=nocache, - ) + # The cached_operation decorator injects a `nocache` keyword that pyright/mypy can't see. + data = details_with_retry(self.application_id, self.application_version, document_name, nocache=nocache) # type: ignore[call-arg] # pyright: ignore[reportCallIssue] return ApplicationVersionDocument.from_response(data) def _resolve_redirect_url(self, document_name: str, suffix: str) -> str: @@ -461,7 +464,24 @@ def _fetch_redirect_url( proxy: str | None, token_provider: t.Callable[[], str], ) -> str: - """Issue the GET and return the presigned URL from the 3xx Location header.""" + """Issue the GET and return the presigned URL from the 3xx Location header. + + Args: + endpoint_url: Full ``/file`` or ``/content`` endpoint URL. + document_name: The document filename (used for error messages). + ssl_verify: True/False or CA bundle path, mirroring the codegen client config. + proxy: Optional HTTP/HTTPS proxy URL, mirroring the codegen client config. + token_provider: Callable returning a fresh bearer token. + + Returns: + str: The presigned URL extracted from the ``Location`` header. + + Raises: + NotFoundException: 404 — document not found, not public, or not uploaded. + ApiException: Other 4xx (e.g. 403 forbidden, 410 gone). + ServiceException: 5xx, request timeouts, or connection errors (caught & wrapped). + RuntimeError: 3xx without a Location header, or unexpected non-3xx status. + """ try: with requests.get( endpoint_url, @@ -594,8 +614,8 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: requests.HTTPError: If the signed-URL download itself fails. """ destination_path = Path(destination) - if destination_path.is_dir() or (not destination_path.exists() and destination_path.suffix == ""): - destination_path = destination_path / document_name + if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): + destination_path /= document_name destination_path = destination_path.resolve() destination_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index 806f3d60..98f02b5c 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -1696,9 +1696,7 @@ def _make_document_stub(name: str = "output_description.pdf") -> MagicMock: @pytest.mark.unit -def test_cli_application_version_document_list_success( - runner: CliRunner, record_property -) -> None: +def test_cli_application_version_document_list_success(runner: CliRunner, record_property) -> None: """`application version document list` prints document metadata.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") fake_documents = MagicMock() @@ -1724,9 +1722,7 @@ def test_cli_application_version_document_list_success( @pytest.mark.unit -def test_cli_application_version_document_describe_success( - runner: CliRunner, record_property -) -> None: +def test_cli_application_version_document_describe_success(runner: CliRunner, record_property) -> None: """`application version document describe` prints metadata for a single document.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-02") fake_documents = MagicMock() @@ -1751,9 +1747,7 @@ def test_cli_application_version_document_describe_success( @pytest.mark.unit -def test_cli_application_version_document_describe_not_found( - runner: CliRunner, record_property -) -> None: +def test_cli_application_version_document_describe_not_found(runner: CliRunner, record_property) -> None: """`application version document describe` exits 2 with a clear message on 404.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") from aignx.codegen.exceptions import NotFoundException as ApiNotFound @@ -1778,9 +1772,7 @@ def test_cli_application_version_document_describe_not_found( @pytest.mark.unit -def test_cli_application_version_document_download_success( - runner: CliRunner, tmp_path: Path, record_property -) -> None: +def test_cli_application_version_document_download_success(runner: CliRunner, tmp_path: Path, record_property) -> None: """`application version document download` writes the file and prints the destination.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") fake_documents = MagicMock() @@ -1813,4 +1805,3 @@ def test_cli_application_version_document_download_success( fake_documents.download_to_path.assert_called_once() args, _ = fake_documents.download_to_path.call_args assert args[0] == "output_description.pdf" - diff --git a/tests/aignostics/application/utils_test.py b/tests/aignostics/application/utils_test.py index 049b73d3..1b30a006 100644 --- a/tests/aignostics/application/utils_test.py +++ b/tests/aignostics/application/utils_test.py @@ -138,6 +138,7 @@ def _make_item_result( # noqa: PLR0913 custom_metadata: dict[str, Any] | None = None, custom_metadata_checksum: str | None = None, terminated_at: datetime | None = None, + input_artifacts: list[Any] | None = None, output_artifacts: list[OutputArtifactElement] | None = None, ) -> ItemResult: return ItemResult( @@ -151,6 +152,7 @@ def _make_item_result( # noqa: PLR0913 custom_metadata=custom_metadata, custom_metadata_checksum=custom_metadata_checksum, terminated_at=terminated_at, + input_artifacts=input_artifacts if input_artifacts is not None else [], output_artifacts=output_artifacts if output_artifacts is not None else [], ) diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index a841594b..65209414 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -213,7 +213,7 @@ def documents(mock_api: Mock) -> Documents: configuration.proxy = None configuration.ssl_ca_cert = None configuration.verify_ssl = True - configuration.token_provider = lambda: "test-token" # noqa: PIE807 + configuration.token_provider = lambda: "test-token" mock_api.api_client = MagicMock() mock_api.api_client.configuration = configuration return Documents(mock_api, application_id="heta", application_version="1.0.0") @@ -247,9 +247,7 @@ def test_documents_list_returns_empty_list(documents: Documents, mock_api: Mock) @pytest.mark.unit -def test_documents_list_uses_cache_then_bypasses_with_nocache( - documents: Documents, mock_api: Mock -) -> None: +def test_documents_list_uses_cache_then_bypasses_with_nocache(documents: Documents, mock_api: Mock) -> None: """list() caches results across calls; nocache=True forces a fresh call.""" mock_api.list_version_documents.return_value = [_make_doc("a.pdf")] @@ -298,16 +296,12 @@ def test_documents_get_download_url_resolves_redirect(documents: Documents) -> N mock_response.__enter__.return_value = mock_response mock_response.__exit__.return_value = False - with patch( - "aignostics.platform.resources.applications.requests.get", return_value=mock_response - ) as mock_get: + with patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response) as mock_get: url = documents.get_download_url("output_description.pdf") assert url == "https://signed.example/blob?token=abc" called_url = mock_get.call_args.args[0] - assert called_url.endswith( - "/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file" - ) + assert called_url.endswith("/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file") assert mock_get.call_args.kwargs["allow_redirects"] is False @@ -320,9 +314,7 @@ def test_documents_get_content_url_resolves_redirect(documents: Documents) -> No mock_response.__enter__.return_value = mock_response mock_response.__exit__.return_value = False - with patch( - "aignostics.platform.resources.applications.requests.get", return_value=mock_response - ) as mock_get: + with patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response) as mock_get: url = documents.get_content_url("output_description.pdf") assert url == "https://signed.example/content?token=def" @@ -349,9 +341,7 @@ def test_documents_get_download_url_404_raises_not_found(documents: Documents) - @pytest.mark.unit -def test_documents_download_to_path_writes_file( - documents: Documents, tmp_path: Path -) -> None: +def test_documents_download_to_path_writes_file(documents: Documents, tmp_path: Path) -> None: """download_to_path() resolves the redirect and streams the body to disk.""" redirect_response = MagicMock() redirect_response.status_code = HTTPStatus.TEMPORARY_REDIRECT From 87e49d986e24df3d0ce0f3c4eb9e55b10258dc70 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 15:04:59 +0200 Subject: [PATCH 07/16] fix(application): silence pyright unused-variable warnings in document download Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aignostics/application/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index 2d5acc72..752eeb79 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -1705,7 +1705,7 @@ def application_version_document_download( is bounded by HTTPS transport and the signed-URL lifetime. """ try: - _application_id, _version_number, documents = _resolve_documents(application_version_id) + _, _, documents = _resolve_documents(application_version_id) written = documents.download_to_path(document_name, output) except NotFoundException: message = f"Document '{document_name}' not found for application version '{application_version_id}'." From a5248a15f14278ed117b312864db7a643d22c375 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 15:32:36 +0200 Subject: [PATCH 08/16] refactor(platform): simplify document download and address PR review [PYSDK-122] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the get_content_url / get_download_url public methods on the Documents resource and the _resolve_redirect_url / _fetch_redirect_url machinery that intercepted the platform 307 to extract a signed URL — there were no in-SDK callers for the URL surface, and following the redirect with requests (allow_redirects=True) is sufficient for download_to_path. requests strips the Authorization header on the cross-host hop, so the bearer token is not forwarded to GCS. Also address Copilot review on PR #612: - URL-encode each path segment in download_to_path (application_id, version, document_name) so reserved characters in a document name cannot inject extra path segments. - Split application/version "is unavailable" 404s from document-level 404s in the describe / download CLI commands so error messages stop conflating the two failure modes. - Update SPEC_PLATFORM_SERVICE.md so the resource table and Documents class snippet match the implementation (constructor signature, list() return type, no get_content_url). - Reword TC-APPLICATION-CLI-05 scenario 04 to match actual behaviour: the document file is written using the requested document name, not a server-provided filename. Net change: -167 lines after redirect simplification; +13 / -25 across the remaining files for the encoding and CLI split. Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/SPEC_PLATFORM_SERVICE.md | 36 +-- src/aignostics/application/_cli.py | 15 + .../platform/resources/applications.py | 273 +++++------------- .../application/TC-APPLICATION-CLI-05.feature | 2 +- .../platform/resources/applications_test.py | 93 ++---- 5 files changed, 134 insertions(+), 285 deletions(-) diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index 193f27e1..7e8ff159 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -82,7 +82,7 @@ platform/ | `ApplicationRun` | Class | Run lifecycle and result management | `details()`, `cancel()`, `results()`, `download_to_folder()`, `artifact()`, `get_artifact_download_url()`, `ensure_artifacts_downloaded()` | | `Artifact` | Class | Per-artifact handle for resolving fresh presigned download URLs via the `/api/v1/runs/{run_id}/artifacts/{artifact_id}/file` endpoint | `get_download_url()` | | `Versions` | Class | Application version management | `list()`, `list_sorted()`, `latest()`, `details()`, `documents()` | -| `Documents` | Class | Application version release document management | `list()`, `details()`, `download_to_path()`, `get_content_url()` | +| `Documents` | Class | Application version release document management | `list()`, `details()`, `download_to_path()` | | `Runs` | Class | Application run management and creation | `create()`, `list()` / `list_data()`, `__call__()` | | `utils` | Module | Resource utility functions and pagination helpers | `paginate()` | @@ -314,15 +314,15 @@ class Versions: class Documents: """Resource class for retrieving release documents attached to an application version. - Backed by ``GET /api/v1/applications/{application_id}/versions/{version_id}/documents`` - and the per-document ``/{name}``, ``/{name}/file``, and ``/{name}/content`` endpoints. + Backed by ``GET /api/v1/applications/{application_id}/versions/{version}/documents`` + and the per-document ``/{name}`` and ``/{name}/file`` endpoints. The public API exposes only documents with ``visibility=public`` and ``status=uploaded``. """ - def __init__(self, api: PublicApi, application_version_id: str) -> None: - """Initializes the Documents resource bound to an application version.""" + def __init__(self, api: PublicApi, application_id: str, application_version: str) -> None: + """Initializes the Documents resource bound to an (application_id, version) pair.""" - def list(self, nocache: bool = False) -> Iterator[ApplicationVersionDocument]: + def list(self, nocache: bool = False) -> list[ApplicationVersionDocument]: """List metadata for all public, uploaded release documents for the bound version. Args: @@ -342,10 +342,16 @@ class Documents: def download_to_path(self, document_name: str, destination: Path | str) -> Path: """Downloads the document file to a local path. - Follows the platform ``307`` redirect from the ``/file`` endpoint to a short-lived - GCS signed URL with ``Content-Disposition: attachment; filename="{name}"`` and - writes the response body to disk. Returns the absolute path to the written file. - The presigned URL is short-lived; this method resolves and consumes it in a single call. + Issues a single ``GET`` against the ``/file`` endpoint and follows the + platform ``307`` redirect to a short-lived GCS signed URL, streaming the + response body to disk. The bearer token is stripped on the cross-host hop + by ``requests`` and is therefore not forwarded to the storage backend. + Returns the absolute path to the written file. + + If ``destination`` is a directory, the file is written as + ``{destination}/{document_name}``; the requested document name is the + canonical filename and is used regardless of any ``Content-Disposition`` + served by the storage backend. Note: Document downloads do not carry a CRC32C checksum (unlike run artifacts); integrity is bounded by HTTPS transport and the signed-URL lifetime. @@ -353,16 +359,6 @@ class Documents: Raises: NotFoundException: When the document does not exist, is not public, or is not uploaded. """ - - def get_content_url(self, document_name: str) -> str: - """Resolves a fresh, short-lived presigned URL for the inline-content endpoint. - - Calls ``GET /api/v1/applications/{application_id}/versions/{version_id}/documents/{name}/content`` - with ``allow_redirects=False`` and returns the presigned URL from the redirect - ``Location`` header. Unlike ``download_to_path``, the response from the resolved - URL is served with the stored ``Content-Type`` and no ``Content-Disposition``, - intended for programmatic clients that consume content inline. - """ ``` ```python diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index 752eeb79..13a10fa7 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -1646,6 +1646,15 @@ def application_version_document_describe( """Show metadata for a single public release document.""" try: application_id, version_number, documents = _resolve_documents(application_version_id) + except NotFoundException as e: + message = f"Application version '{application_version_id}' is unavailable." + logger.warning("{} ({})", message, e) + if format == "json": + print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) + else: + console.print(f"[warning]Warning:[/warning] {message}") + sys.exit(2) + try: doc = documents.details(document_name) except NotFoundException: message = f"Document '{document_name}' not found for application version '{application_version_id}'." @@ -1706,6 +1715,12 @@ def application_version_document_download( """ try: _, _, documents = _resolve_documents(application_version_id) + except NotFoundException as e: + message = f"Application version '{application_version_id}' is unavailable." + logger.warning("{} ({})", message, e) + console.print(f"[warning]Warning:[/warning] {message}") + sys.exit(2) + try: written = documents.download_to_path(document_name, output) except NotFoundException: message = f"Document '{document_name}' not found for application version '{application_version_id}'." diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index 736813b3..a2cd42a1 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -11,11 +11,12 @@ from http import HTTPStatus from operator import itemgetter from pathlib import Path +from urllib.parse import quote import requests import semver from aignx.codegen.api.public_api import PublicApi -from aignx.codegen.exceptions import ApiException, NotFoundException, ServiceException +from aignx.codegen.exceptions import NotFoundException, ServiceException from aignx.codegen.models import ApplicationReadResponse as Application from aignx.codegen.models import ApplicationReadShortResponse as ApplicationSummary from aignx.codegen.models import ApplicationVersion as VersionTuple @@ -39,14 +40,6 @@ from aignostics.platform.resources.utils import paginate from aignostics.utils import user_agent -_REDIRECT_STATUSES = frozenset({ - HTTPStatus.MOVED_PERMANENTLY, - HTTPStatus.FOUND, - HTTPStatus.SEE_OTHER, - HTTPStatus.TEMPORARY_REDIRECT, - HTTPStatus.PERMANENT_REDIRECT, -}) - _DOCUMENT_DOWNLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MB RETRYABLE_EXCEPTIONS = ( @@ -405,34 +398,53 @@ def details_with_retry( data = details_with_retry(self.application_id, self.application_version, document_name, nocache=nocache) # type: ignore[call-arg] # pyright: ignore[reportCallIssue] return ApplicationVersionDocument.from_response(data) - def _resolve_redirect_url(self, document_name: str, suffix: str) -> str: - """Issue an unredirected GET to ``/file`` or ``/content`` and return the Location URL. + def download_to_path(self, document_name: str, destination: Path | str) -> Path: + """Download a release document file to a local path. + + Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/file``, + which returns a ``307`` redirect to a short-lived GCS signed URL serving the file + with ``Content-Disposition: attachment; filename="{name}"``. ``requests`` follows + the redirect automatically and strips the bearer ``Authorization`` header on the + cross-host hop, so the credential is not forwarded to GCS. - The generated client cannot be used directly because urllib3 follows the - redirect automatically and would fetch the document body — losing the - short-lived presigned URL exposed in the ``Location`` header. + If ``destination`` is a directory, the file is written as + ``{destination}/{document_name}``; the requested document name is the canonical + filename and is used regardless of any ``Content-Disposition`` served by the + storage backend. If ``destination`` is a file path, the file is written there + verbatim. Parent directories are created if they do not yet exist. + + Document downloads do not carry a CRC32C checksum (unlike run artifacts); + integrity is bounded by HTTPS transport and the signed-URL lifetime. Args: - document_name: The document filename. - suffix: Either ``"file"`` (server sets ``Content-Disposition: attachment``) or - ``"content"`` (no ``Content-Disposition``; for inline programmatic use). + document_name (str): The document filename. + destination (Path | str): Target file path or directory to write into. Returns: - str: A time-limited presigned GCS URL. + Path: The absolute path to the written file. Raises: - NotFoundException: 404 — document not found, not public, or not uploaded. - ApiException: Other 4xx (e.g. 403 forbidden, 410 gone). - ServiceException: 5xx, request timeouts, or connection errors - (after retry attempts have been exhausted). - RuntimeError: 3xx response with no Location header, or any other - unexpected status the API contract does not define. + NotFoundException: When the document does not exist, is not public, or is not uploaded. + ServiceException: 5xx errors, request timeouts, or connection errors after retries. + requests.HTTPError: For other 4xx errors or signed-URL download failures. """ + destination_path = Path(destination) + if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): + destination_path /= document_name + destination_path = destination_path.resolve() + destination_path.parent.mkdir(parents=True, exist_ok=True) + configuration = self._api.api_client.configuration host = configuration.host.rstrip("/") + # Each path segment is encoded individually so that reserved characters + # (spaces, '#', '?', '/', ...) inside a document name cannot inject extra + # path segments or query strings into the URL. + encoded_application_id = quote(self.application_id, safe="") + encoded_version = quote(self.application_version, safe="") + encoded_document_name = quote(document_name, safe="") endpoint_url = ( - f"{host}/api/v1/applications/{self.application_id}" - f"/versions/{self.application_version}/documents/{document_name}/{suffix}" + f"{host}/api/v1/applications/{encoded_application_id}" + f"/versions/{encoded_version}/documents/{encoded_document_name}/file" ) proxy = getattr(configuration, "proxy", None) ssl_ca_cert = getattr(configuration, "ssl_ca_cert", None) @@ -445,7 +457,41 @@ def _resolve_redirect_url(self, document_name: str, suffix: str) -> str: # of Client (e.g. unit tests with bare PublicApi). token_provider = getattr(configuration, "token_provider", None) or get_token - return Retrying( + def stream_once() -> None: + try: + with requests.get( + endpoint_url, + headers={ + "Authorization": f"Bearer {token_provider()}", + "User-Agent": user_agent(), + }, + allow_redirects=True, + timeout=settings().application_version_timeout, + proxies={"http": proxy, "https": proxy} if proxy else None, + verify=ssl_verify, + stream=True, + ) as response: + if response.status_code == HTTPStatus.NOT_FOUND: + raise NotFoundException( + status=HTTPStatus.NOT_FOUND.value, + reason=( + f"Document '{document_name}' not found for application " + f"'{self.application_id}' version '{self.application_version}'" + ), + ) + if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: + raise ServiceException(status=response.status_code, reason=response.reason) + response.raise_for_status() + with destination_path.open("wb") as out_file: + for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): + if chunk: + out_file.write(chunk) + except requests.Timeout as e: + raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Request timed out") from e + except requests.ConnectionError as e: + raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Connection failed") from e + + Retrying( retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), stop=stop_after_attempt(settings().application_version_retry_attempts), wait=wait_exponential_jitter( @@ -454,178 +500,7 @@ def _resolve_redirect_url(self, document_name: str, suffix: str) -> str: ), before_sleep=_log_retry_attempt, reraise=True, - )(lambda: self._fetch_redirect_url(endpoint_url, document_name, ssl_verify, proxy, token_provider)) - - def _fetch_redirect_url( - self, - endpoint_url: str, - document_name: str, - ssl_verify: bool | str, - proxy: str | None, - token_provider: t.Callable[[], str], - ) -> str: - """Issue the GET and return the presigned URL from the 3xx Location header. - - Args: - endpoint_url: Full ``/file`` or ``/content`` endpoint URL. - document_name: The document filename (used for error messages). - ssl_verify: True/False or CA bundle path, mirroring the codegen client config. - proxy: Optional HTTP/HTTPS proxy URL, mirroring the codegen client config. - token_provider: Callable returning a fresh bearer token. - - Returns: - str: The presigned URL extracted from the ``Location`` header. - - Raises: - NotFoundException: 404 — document not found, not public, or not uploaded. - ApiException: Other 4xx (e.g. 403 forbidden, 410 gone). - ServiceException: 5xx, request timeouts, or connection errors (caught & wrapped). - RuntimeError: 3xx without a Location header, or unexpected non-3xx status. - """ - try: - with requests.get( - endpoint_url, - headers={ - "Authorization": f"Bearer {token_provider()}", - "User-Agent": user_agent(), - }, - allow_redirects=False, - timeout=settings().application_version_timeout, - proxies={"http": proxy, "https": proxy} if proxy else None, - verify=ssl_verify, - stream=True, - ) as response: - if response.status_code in _REDIRECT_STATUSES: - location = response.headers.get("Location") - if not location: - msg = ( - f"Redirect response {response.status_code} from documents endpoint " - f"missing Location header for document '{document_name}' on " - f"application '{self.application_id}' version '{self.application_version}'" - ) - raise RuntimeError(msg) - return location - if response.status_code == HTTPStatus.NOT_FOUND: - raise NotFoundException( - status=HTTPStatus.NOT_FOUND.value, - reason=( - f"Document '{document_name}' not found for application " - f"'{self.application_id}' version '{self.application_version}'" - ), - ) - if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: - raise ServiceException(status=response.status_code, reason=response.reason) - if response.status_code >= HTTPStatus.BAD_REQUEST: - raise ApiException(status=response.status_code, reason=response.reason) - msg = ( - f"Unexpected status {response.status_code} from documents endpoint for " - f"document '{document_name}' on application '{self.application_id}' " - f"version '{self.application_version}'; expected a redirect" - ) - raise RuntimeError(msg) - except requests.Timeout as e: - raise ServiceException( - status=HTTPStatus.SERVICE_UNAVAILABLE.value, - reason="Request timed out", - ) from e - except requests.ConnectionError as e: - raise ServiceException( - status=HTTPStatus.SERVICE_UNAVAILABLE.value, - reason="Connection failed", - ) from e - except requests.RequestException as e: - raise ServiceException( - status=HTTPStatus.SERVICE_UNAVAILABLE.value, - reason=f"Request failed: {e}", - ) from e - - def get_content_url(self, document_name: str) -> str: - """Resolve a fresh, short-lived presigned URL for the inline-content endpoint. - - Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/content`` - with ``allow_redirects=False`` and returns the presigned URL from the redirect - ``Location`` header. The response from the resolved URL is served with the stored - ``Content-Type`` and no ``Content-Disposition`` header — intended for programmatic - clients that consume document content inline. The presigned URL is short-lived; - resolve immediately before fetching. - - Args: - document_name (str): The document filename. - - Returns: - str: A time-limited presigned URL. - - Raises: - NotFoundException: When the document does not exist, is not public, or is not uploaded. - ApiException: Other 4xx errors. - ServiceException: 5xx errors, request timeouts, or connection errors after retries. - RuntimeError: 3xx without a Location header, or unexpected non-3xx status. - """ - return self._resolve_redirect_url(document_name, "content") - - def get_download_url(self, document_name: str) -> str: - """Resolve a fresh, short-lived presigned URL for the file (attachment) endpoint. - - Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/file`` - with ``allow_redirects=False`` and returns the presigned URL from the redirect - ``Location`` header. The response from the resolved URL is served with - ``Content-Disposition: attachment; filename="{name}"`` — intended for browser-style - downloads. The presigned URL is short-lived; resolve immediately before fetching. - - Args: - document_name (str): The document filename. - - Returns: - str: A time-limited presigned URL. - - Raises: - NotFoundException: When the document does not exist, is not public, or is not uploaded. - ApiException: Other 4xx errors. - ServiceException: 5xx errors, request timeouts, or connection errors after retries. - RuntimeError: 3xx without a Location header, or unexpected non-3xx status. - """ - return self._resolve_redirect_url(document_name, "file") - - def download_to_path(self, document_name: str, destination: Path | str) -> Path: - """Download a release document file to a local path. - - Follows the platform ``307`` redirect from the ``/file`` endpoint to a short-lived - GCS signed URL with ``Content-Disposition: attachment; filename="{name}"`` and - streams the response body to disk. - - If ``destination`` is a directory, the file is written as - ``{destination}/{document_name}``. If it is a file path, the file is written there - verbatim. Parent directories are created if they do not yet exist. - - Document downloads do not carry a CRC32C checksum (unlike run artifacts); - integrity is bounded by HTTPS transport and the signed-URL lifetime. - - Args: - document_name (str): The document filename. - destination (Path | str): Target file path or directory to write into. - - Returns: - Path: The absolute path to the written file. - - Raises: - NotFoundException: When the document does not exist, is not public, or is not uploaded. - ApiException: Other 4xx errors. - ServiceException: 5xx errors, request timeouts, or connection errors after retries. - requests.HTTPError: If the signed-URL download itself fails. - """ - destination_path = Path(destination) - if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): - destination_path /= document_name - destination_path = destination_path.resolve() - destination_path.parent.mkdir(parents=True, exist_ok=True) - - signed_url = self.get_download_url(document_name) - with requests.get(signed_url, stream=True, timeout=settings().application_version_timeout) as response: - response.raise_for_status() - with destination_path.open("wb") as out_file: - for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): - if chunk: - out_file.write(chunk) + )(stream_once) return destination_path diff --git a/tests/aignostics/application/TC-APPLICATION-CLI-05.feature b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature index 3e04a98f..22f69e3a 100644 --- a/tests/aignostics/application/TC-APPLICATION-CLI-05.feature +++ b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature @@ -41,4 +41,4 @@ Feature: Application Version Release Documents Given the user has access to an application version with a public release document When the user requests download of that document to a local destination Then the system shall follow the platform redirect to the signed storage URL - And the system shall write the document file using the server-provided filename + And the system shall write the document file using the requested document name diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index 65209414..515e7037 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -204,9 +204,9 @@ def _clear_operation_cache_before_each_test() -> None: def documents(mock_api: Mock) -> Documents: """Create a Documents instance bound to a fixed (application, version) pair. - The mock is augmented with a minimal ``api_client.configuration`` so the - redirect-resolving methods (``download_to_path``, ``get_*_url``) can read - host/proxy/SSL settings without hitting the real codegen plumbing. + The mock is augmented with a minimal ``api_client.configuration`` so + ``download_to_path`` can read host/proxy/SSL settings without hitting the + real codegen plumbing. """ configuration = MagicMock() configuration.host = "https://platform.example.com" @@ -287,69 +287,11 @@ def test_documents_details_propagates_not_found(documents: Documents, mock_api: documents.details("missing.pdf") -@pytest.mark.unit -def test_documents_get_download_url_resolves_redirect(documents: Documents) -> None: - """get_download_url() returns the Location header from a 307 redirect on /file.""" - mock_response = MagicMock() - mock_response.status_code = HTTPStatus.TEMPORARY_REDIRECT - mock_response.headers = {"Location": "https://signed.example/blob?token=abc"} - mock_response.__enter__.return_value = mock_response - mock_response.__exit__.return_value = False - - with patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response) as mock_get: - url = documents.get_download_url("output_description.pdf") - - assert url == "https://signed.example/blob?token=abc" - called_url = mock_get.call_args.args[0] - assert called_url.endswith("/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file") - assert mock_get.call_args.kwargs["allow_redirects"] is False - - -@pytest.mark.unit -def test_documents_get_content_url_resolves_redirect(documents: Documents) -> None: - """get_content_url() targets the /content variant and returns its Location.""" - mock_response = MagicMock() - mock_response.status_code = HTTPStatus.TEMPORARY_REDIRECT - mock_response.headers = {"Location": "https://signed.example/content?token=def"} - mock_response.__enter__.return_value = mock_response - mock_response.__exit__.return_value = False - - with patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response) as mock_get: - url = documents.get_content_url("output_description.pdf") - - assert url == "https://signed.example/content?token=def" - assert mock_get.call_args.args[0].endswith( - "/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/content" - ) - - -@pytest.mark.unit -def test_documents_get_download_url_404_raises_not_found(documents: Documents) -> None: - """A 404 from the redirect endpoint is mapped to NotFoundException.""" - mock_response = MagicMock() - mock_response.status_code = HTTPStatus.NOT_FOUND - mock_response.headers = {} - mock_response.reason = "Not Found" - mock_response.__enter__.return_value = mock_response - mock_response.__exit__.return_value = False - - with ( - patch("aignostics.platform.resources.applications.requests.get", return_value=mock_response), - pytest.raises(NotFoundException), - ): - documents.get_download_url("missing.pdf") - - @pytest.mark.unit def test_documents_download_to_path_writes_file(documents: Documents, tmp_path: Path) -> None: - """download_to_path() resolves the redirect and streams the body to disk.""" - redirect_response = MagicMock() - redirect_response.status_code = HTTPStatus.TEMPORARY_REDIRECT - redirect_response.headers = {"Location": "https://signed.example/blob"} - redirect_response.__enter__.return_value = redirect_response - redirect_response.__exit__.return_value = False - + """download_to_path() follows the platform redirect and streams the body to disk.""" body_response = MagicMock() + body_response.status_code = HTTPStatus.OK body_response.iter_content.return_value = [b"hello ", b"world"] body_response.raise_for_status = MagicMock() body_response.__enter__.return_value = body_response @@ -357,12 +299,33 @@ def test_documents_download_to_path_writes_file(documents: Documents, tmp_path: with patch( "aignostics.platform.resources.applications.requests.get", - side_effect=[redirect_response, body_response], - ): + return_value=body_response, + ) as mock_get: result = documents.download_to_path("output_description.pdf", tmp_path) assert result == (tmp_path / "output_description.pdf").resolve() assert result.read_bytes() == b"hello world" + # Single request to the platform endpoint, requests follows the 307 internally. + mock_get.assert_called_once() + called_url = mock_get.call_args.args[0] + assert called_url.endswith("/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file") + assert mock_get.call_args.kwargs["allow_redirects"] is True + + +@pytest.mark.unit +def test_documents_download_to_path_404_raises_not_found(documents: Documents, tmp_path: Path) -> None: + """A 404 from the documents endpoint is mapped to NotFoundException.""" + response = MagicMock() + response.status_code = HTTPStatus.NOT_FOUND + response.reason = "Not Found" + response.__enter__.return_value = response + response.__exit__.return_value = False + + with ( + patch("aignostics.platform.resources.applications.requests.get", return_value=response), + pytest.raises(NotFoundException), + ): + documents.download_to_path("missing.pdf", tmp_path) @pytest.mark.unit From 3898887ad43478973435625779334a8bfd05806d Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 16:07:16 +0200 Subject: [PATCH 09/16] refactor(platform): address SonarCloud findings on documents PR [PYSDK-122] Split Documents.download_to_path into three helpers (_resolve_destination_path, _build_document_endpoint_url, _stream_document) so cognitive complexity drops below the S3776 threshold. Lift recurring test literals into module-level constants where it improves clarity, and apply NOSONAR with rule + reason for literals where a constant would be less informative than the literal itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aignostics/application/_cli.py | 6 +- .../platform/resources/applications.py | 149 ++++++++++++------ tests/aignostics/application/cli_test.py | 33 ++-- .../platform/resources/applications_test.py | 27 ++-- 4 files changed, 135 insertions(+), 80 deletions(-) diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index 13a10fa7..f2415a00 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -1602,7 +1602,7 @@ def application_version_document_list( items = documents.list() except NotFoundException as e: message = f"No release documents found: application version '{application_version_id}' is unavailable." - logger.warning("{} ({})", message, e) + logger.warning("{} ({})", message, e) # NOSONAR python:S1192: loguru positional format string is conventional if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) else: @@ -1648,7 +1648,7 @@ def application_version_document_describe( application_id, version_number, documents = _resolve_documents(application_version_id) except NotFoundException as e: message = f"Application version '{application_version_id}' is unavailable." - logger.warning("{} ({})", message, e) + logger.warning("{} ({})", message, e) # NOSONAR python:S1192 if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) else: @@ -1717,7 +1717,7 @@ def application_version_document_download( _, _, documents = _resolve_documents(application_version_id) except NotFoundException as e: message = f"Application version '{application_version_id}' is unavailable." - logger.warning("{} ({})", message, e) + logger.warning("{} ({})", message, e) # NOSONAR python:S1192 console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) try: diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index a2cd42a1..111774ca 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -428,25 +428,14 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: ServiceException: 5xx errors, request timeouts, or connection errors after retries. requests.HTTPError: For other 4xx errors or signed-URL download failures. """ - destination_path = Path(destination) - if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): - destination_path /= document_name - destination_path = destination_path.resolve() - destination_path.parent.mkdir(parents=True, exist_ok=True) - + destination_path = self._resolve_destination_path(destination, document_name) configuration = self._api.api_client.configuration - host = configuration.host.rstrip("/") - # Each path segment is encoded individually so that reserved characters - # (spaces, '#', '?', '/', ...) inside a document name cannot inject extra - # path segments or query strings into the URL. - encoded_application_id = quote(self.application_id, safe="") - encoded_version = quote(self.application_version, safe="") - encoded_document_name = quote(document_name, safe="") - endpoint_url = ( - f"{host}/api/v1/applications/{encoded_application_id}" - f"/versions/{encoded_version}/documents/{encoded_document_name}/file" + endpoint_url = self._build_document_endpoint_url( + host=configuration.host.rstrip("/"), + application_id=self.application_id, + version=self.application_version, + document_name=document_name, ) - proxy = getattr(configuration, "proxy", None) ssl_ca_cert = getattr(configuration, "ssl_ca_cert", None) verify_ssl = getattr(configuration, "verify_ssl", True) ssl_verify: bool | str = ssl_ca_cert or verify_ssl @@ -456,40 +445,7 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: # Fall back to get_token() only when the configuration was built outside # of Client (e.g. unit tests with bare PublicApi). token_provider = getattr(configuration, "token_provider", None) or get_token - - def stream_once() -> None: - try: - with requests.get( - endpoint_url, - headers={ - "Authorization": f"Bearer {token_provider()}", - "User-Agent": user_agent(), - }, - allow_redirects=True, - timeout=settings().application_version_timeout, - proxies={"http": proxy, "https": proxy} if proxy else None, - verify=ssl_verify, - stream=True, - ) as response: - if response.status_code == HTTPStatus.NOT_FOUND: - raise NotFoundException( - status=HTTPStatus.NOT_FOUND.value, - reason=( - f"Document '{document_name}' not found for application " - f"'{self.application_id}' version '{self.application_version}'" - ), - ) - if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: - raise ServiceException(status=response.status_code, reason=response.reason) - response.raise_for_status() - with destination_path.open("wb") as out_file: - for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): - if chunk: - out_file.write(chunk) - except requests.Timeout as e: - raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Request timed out") from e - except requests.ConnectionError as e: - raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Connection failed") from e + proxy = getattr(configuration, "proxy", None) Retrying( retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), @@ -500,9 +456,98 @@ def stream_once() -> None: ), before_sleep=_log_retry_attempt, reraise=True, - )(stream_once) + )( + lambda: self._stream_document( + url=endpoint_url, + destination_path=destination_path, + document_name=document_name, + token_provider=token_provider, + ssl_verify=ssl_verify, + proxy=proxy, + ) + ) + return destination_path + + @staticmethod + def _resolve_destination_path(destination: Path | str, document_name: str) -> Path: + """Resolve the on-disk path to write a document to and ensure its parent exists. + + Returns: + Path: The absolute, parent-created destination path. + """ + destination_path = Path(destination) + if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): + destination_path /= document_name + destination_path = destination_path.resolve() + destination_path.parent.mkdir(parents=True, exist_ok=True) return destination_path + @staticmethod + def _build_document_endpoint_url(host: str, application_id: str, version: str, document_name: str) -> str: + """Build the document file endpoint URL with each path segment encoded individually. + + Per-segment encoding ensures reserved characters (spaces, '#', '?', '/', ...) inside + a document name cannot inject extra path segments or query strings into the URL. + + Returns: + str: The fully-qualified ``/api/v1/applications/.../documents/.../file`` URL. + """ + encoded_application_id = quote(application_id, safe="") + encoded_version = quote(version, safe="") + encoded_document_name = quote(document_name, safe="") + return ( + f"{host}/api/v1/applications/{encoded_application_id}" + f"/versions/{encoded_version}/documents/{encoded_document_name}/file" + ) + + def _stream_document( # noqa: PLR0913, PLR0917 -- private helper, splitting params would require a thin DTO + self, + url: str, + destination_path: Path, + document_name: str, + token_provider: t.Callable[[], str], + ssl_verify: bool | str, + proxy: str | None, + ) -> None: + """Stream a single document download to disk, mapping HTTP errors to platform exceptions. + + Raises: + NotFoundException: When the platform returns 404 for the document. + ServiceException: For 5xx responses, request timeouts, or connection errors. + """ + try: + with requests.get( + url, + headers={ + "Authorization": f"Bearer {token_provider()}", + "User-Agent": user_agent(), + }, + allow_redirects=True, + timeout=settings().application_version_timeout, + proxies={"http": proxy, "https": proxy} if proxy else None, + verify=ssl_verify, + stream=True, + ) as response: + if response.status_code == HTTPStatus.NOT_FOUND: + raise NotFoundException( + status=HTTPStatus.NOT_FOUND.value, + reason=( + f"Document '{document_name}' not found for application " + f"'{self.application_id}' version '{self.application_version}'" + ), + ) + if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: + raise ServiceException(status=response.status_code, reason=response.reason) + response.raise_for_status() + with destination_path.open("wb") as out_file: + for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): + if chunk: + out_file.write(chunk) + except requests.Timeout as e: + raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Request timed out") from e + except requests.ConnectionError as e: + raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Connection failed") from e + class Applications: """Resource class for managing applications. diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index 98f02b5c..b0350c6b 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -48,6 +48,9 @@ RUN_CSV_FILENAME = "run.csv" +DOCUMENT_OUTPUT_DESCRIPTION_PDF = "output_description.pdf" +APPLICATION_CLI_CLIENT_PATCH_TARGET = "aignostics.application._cli.Client" + # Full SPOT_0 CSV - single source of truth for all run submissions in this test file. CSV_CONTENT_SPOT0 = ( "external_id;checksum_base64_crc32c;resolution_mpp;width_px;height_px;" @@ -1675,12 +1678,12 @@ def test_cli_json_format_and_cancel_by_filter_with_dry_run( # noqa: PLR0915, PL # ---------------------------------------------------------------------------------- -def _make_document_stub(name: str = "output_description.pdf") -> MagicMock: +def _make_document_stub(name: str = DOCUMENT_OUTPUT_DESCRIPTION_PDF) -> MagicMock: """Create a stub ApplicationVersionDocument with realistic field values.""" stub = MagicMock() stub.id = "11111111-1111-1111-1111-111111111111" stub.name = name - stub.mime_type = "application/pdf" + stub.mime_type = "application/pdf" # NOSONAR python:S1192: standard MIME type literal is clearer than a constant stub.visibility = "public" stub.created_at = datetime(2026, 1, 1, 12, 0, tzinfo=UTC) stub.updated_at = datetime(2026, 1, 2, 12, 0, tzinfo=UTC) @@ -1701,7 +1704,7 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") fake_documents = MagicMock() fake_documents.list.return_value = [ - _make_document_stub("output_description.pdf"), + _make_document_stub(DOCUMENT_OUTPUT_DESCRIPTION_PDF), _make_document_stub("model_card.pdf"), ] fake_client = MagicMock() @@ -1710,12 +1713,12 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents - with patch("aignostics.application._cli.Client", return_value=fake_client): + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke(cli, ["application", "version", "document", "list", "heta"]) assert result.exit_code == 0 output = normalize_output(result.output) - assert "output_description.pdf" in output + assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output assert "model_card.pdf" in output assert "application/pdf" in output fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") @@ -1726,24 +1729,24 @@ def test_cli_application_version_document_describe_success(runner: CliRunner, re """`application version document describe` prints metadata for a single document.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-02") fake_documents = MagicMock() - fake_documents.details.return_value = _make_document_stub("output_description.pdf") + fake_documents.details.return_value = _make_document_stub(DOCUMENT_OUTPUT_DESCRIPTION_PDF) fake_client = MagicMock() fake_client.applications.versions.documents.return_value = fake_documents - with patch("aignostics.application._cli.Client", return_value=fake_client): + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( cli, - ["application", "version", "document", "describe", "heta:1.0.0", "output_description.pdf"], + ["application", "version", "document", "describe", "heta:1.0.0", DOCUMENT_OUTPUT_DESCRIPTION_PDF], ) assert result.exit_code == 0 output = normalize_output(result.output) - assert "output_description.pdf" in output + assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output assert "application/pdf" in output # Explicit version supplied via "heta:1.0.0", so latest() should NOT be called. fake_client.applications.versions.latest.assert_not_called() fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") - fake_documents.details.assert_called_once_with("output_description.pdf") + fake_documents.details.assert_called_once_with(DOCUMENT_OUTPUT_DESCRIPTION_PDF) @pytest.mark.unit @@ -1760,7 +1763,7 @@ def test_cli_application_version_document_describe_not_found(runner: CliRunner, fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents - with patch("aignostics.application._cli.Client", return_value=fake_client): + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( cli, ["application", "version", "document", "describe", "heta", "missing.pdf"], @@ -1776,7 +1779,7 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm """`application version document download` writes the file and prints the destination.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") fake_documents = MagicMock() - expected_path = tmp_path / "output_description.pdf" + expected_path = tmp_path / DOCUMENT_OUTPUT_DESCRIPTION_PDF fake_documents.download_to_path.return_value = expected_path fake_client = MagicMock() latest_version = MagicMock() @@ -1784,7 +1787,7 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents - with patch("aignostics.application._cli.Client", return_value=fake_client): + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( cli, [ @@ -1793,7 +1796,7 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm "document", "download", "heta", - "output_description.pdf", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, "--output", str(tmp_path), ], @@ -1804,4 +1807,4 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm assert str(expected_path) in output fake_documents.download_to_path.assert_called_once() args, _ = fake_documents.download_to_path.call_args - assert args[0] == "output_description.pdf" + assert args[0] == DOCUMENT_OUTPUT_DESCRIPTION_PDF diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index 515e7037..b1a1e610 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -28,6 +28,8 @@ API_ERROR = "API error" +DOCUMENT_OUTPUT_DESCRIPTION_PDF = "output_description.pdf" + @pytest.fixture def mock_api() -> Mock: @@ -182,7 +184,7 @@ def test_versions_property_returns_versions_instance(applications) -> None: # ---------------------------------------------------------------------------------- -def _make_doc(name: str = "output_description.pdf") -> VersionDocumentResponse: +def _make_doc(name: str = DOCUMENT_OUTPUT_DESCRIPTION_PDF) -> VersionDocumentResponse: """Build a VersionDocumentResponse codegen model for tests.""" return VersionDocumentResponse( id="11111111-1111-1111-1111-111111111111", @@ -222,7 +224,10 @@ def documents(mock_api: Mock) -> Documents: @pytest.mark.unit def test_documents_list_returns_wrapped_models(documents: Documents, mock_api: Mock) -> None: """Documents.list() returns ApplicationVersionDocument instances.""" - mock_api.list_version_documents.return_value = [_make_doc("a.pdf"), _make_doc("b.pdf")] + mock_api.list_version_documents.return_value = [ + _make_doc("a.pdf"), + _make_doc("b.pdf"), + ] # NOSONAR python:S1192: arbitrary placeholder filename, a constant adds no clarity result = documents.list() @@ -249,7 +254,7 @@ def test_documents_list_returns_empty_list(documents: Documents, mock_api: Mock) @pytest.mark.unit def test_documents_list_uses_cache_then_bypasses_with_nocache(documents: Documents, mock_api: Mock) -> None: """list() caches results across calls; nocache=True forces a fresh call.""" - mock_api.list_version_documents.return_value = [_make_doc("a.pdf")] + mock_api.list_version_documents.return_value = [_make_doc("a.pdf")] # NOSONAR python:S1192 # First call hits the API and caches. documents.list() @@ -265,17 +270,17 @@ def test_documents_list_uses_cache_then_bypasses_with_nocache(documents: Documen @pytest.mark.unit def test_documents_details_returns_wrapped_model(documents: Documents, mock_api: Mock) -> None: """Documents.details() wraps the response in ApplicationVersionDocument.""" - mock_api.get_version_document.return_value = _make_doc("output_description.pdf") + mock_api.get_version_document.return_value = _make_doc(DOCUMENT_OUTPUT_DESCRIPTION_PDF) - result = documents.details("output_description.pdf") + result = documents.details(DOCUMENT_OUTPUT_DESCRIPTION_PDF) assert isinstance(result, ApplicationVersionDocument) - assert result.name == "output_description.pdf" + assert result.name == DOCUMENT_OUTPUT_DESCRIPTION_PDF assert result.mime_type == "application/pdf" call_kwargs = mock_api.get_version_document.call_args.kwargs assert call_kwargs["application_id"] == "heta" assert call_kwargs["version"] == "1.0.0" - assert call_kwargs["name"] == "output_description.pdf" + assert call_kwargs["name"] == DOCUMENT_OUTPUT_DESCRIPTION_PDF @pytest.mark.unit @@ -301,14 +306,16 @@ def test_documents_download_to_path_writes_file(documents: Documents, tmp_path: "aignostics.platform.resources.applications.requests.get", return_value=body_response, ) as mock_get: - result = documents.download_to_path("output_description.pdf", tmp_path) + result = documents.download_to_path(DOCUMENT_OUTPUT_DESCRIPTION_PDF, tmp_path) - assert result == (tmp_path / "output_description.pdf").resolve() + assert result == (tmp_path / DOCUMENT_OUTPUT_DESCRIPTION_PDF).resolve() assert result.read_bytes() == b"hello world" # Single request to the platform endpoint, requests follows the 307 internally. mock_get.assert_called_once() called_url = mock_get.call_args.args[0] - assert called_url.endswith("/api/v1/applications/heta/versions/1.0.0/documents/output_description.pdf/file") + assert called_url.endswith( + f"/api/v1/applications/heta/versions/1.0.0/documents/{DOCUMENT_OUTPUT_DESCRIPTION_PDF}/file" + ) assert mock_get.call_args.kwargs["allow_redirects"] is True From 0f6292bf942d57595245bfd8d8bc89c3a6adc651 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 17:19:53 +0200 Subject: [PATCH 10/16] feat(platform): add Documents.read_content() for /content endpoint [PYSDK-122] Adds the in-memory read variant of release-document download backed by GET .../documents/{name}/content. Unlike /file (which carries a Content-Disposition: attachment header for browser downloads), /content lets the GCS-stored Content-Type pass through and is intended for programmatic consumers that want the raw bytes (small JSON manifests, license text, etc.). Refactors _build_document_endpoint_url to take a suffix parameter and _stream_document to take a writer callable instead of opening the file itself, so download_to_path and read_content share the request, retry, and error-mapping path. Updates the Documents SPEC to advertise the new method. Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/SPEC_PLATFORM_SERVICE.md | 20 ++- .../platform/resources/applications.py | 157 ++++++++++++++---- .../platform/resources/applications_test.py | 41 +++++ 3 files changed, 179 insertions(+), 39 deletions(-) diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index 7e8ff159..bbe0631f 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -82,7 +82,7 @@ platform/ | `ApplicationRun` | Class | Run lifecycle and result management | `details()`, `cancel()`, `results()`, `download_to_folder()`, `artifact()`, `get_artifact_download_url()`, `ensure_artifacts_downloaded()` | | `Artifact` | Class | Per-artifact handle for resolving fresh presigned download URLs via the `/api/v1/runs/{run_id}/artifacts/{artifact_id}/file` endpoint | `get_download_url()` | | `Versions` | Class | Application version management | `list()`, `list_sorted()`, `latest()`, `details()`, `documents()` | -| `Documents` | Class | Application version release document management | `list()`, `details()`, `download_to_path()` | +| `Documents` | Class | Application version release document management | `list()`, `details()`, `download_to_path()`, `read_content()` | | `Runs` | Class | Application run management and creation | `create()`, `list()` / `list_data()`, `__call__()` | | `utils` | Module | Resource utility functions and pagination helpers | `paginate()` | @@ -306,7 +306,7 @@ class Versions: def details(self, application_version: ApplicationVersion | str) -> ApplicationVersion: """Retrieves details for a specific application version.""" - def documents(self, application_version: ApplicationVersion | str) -> "Documents": + def documents(self, application_id: str, application_version: ApplicationVersion | str) -> "Documents": """Returns a Documents resource bound to the given application version.""" ``` @@ -315,7 +315,7 @@ class Documents: """Resource class for retrieving release documents attached to an application version. Backed by ``GET /api/v1/applications/{application_id}/versions/{version}/documents`` - and the per-document ``/{name}`` and ``/{name}/file`` endpoints. + and the per-document ``/{name}``, ``/{name}/file``, and ``/{name}/content`` endpoints. The public API exposes only documents with ``visibility=public`` and ``status=uploaded``. """ @@ -356,6 +356,20 @@ class Documents: Note: Document downloads do not carry a CRC32C checksum (unlike run artifacts); integrity is bounded by HTTPS transport and the signed-URL lifetime. + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + """ + + def read_content(self, document_name: str) -> bytes: + """Fetches the document's raw content into memory. + + Issues a single ``GET`` against the ``/content`` endpoint and follows the + platform ``307`` redirect to a short-lived GCS signed URL. Unlike ``/file``, + no ``Content-Disposition`` override is set — GCS serves the body with its + stored ``Content-Type`` and ``Cache-Control: no-store``. Intended for small + documents (JSON manifests, license text, etc.) where holding the bytes in + memory is appropriate; prefer ``download_to_path`` for large files. + Raises: NotFoundException: When the document does not exist, is not public, or is not uploaded. """ diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index 111774ca..265149c4 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -9,6 +9,7 @@ import typing as t from datetime import datetime from http import HTTPStatus +from io import BytesIO from operator import itemgetter from pathlib import Path from urllib.parse import quote @@ -429,23 +430,20 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: requests.HTTPError: For other 4xx errors or signed-URL download failures. """ destination_path = self._resolve_destination_path(destination, document_name) - configuration = self._api.api_client.configuration - endpoint_url = self._build_document_endpoint_url( - host=configuration.host.rstrip("/"), - application_id=self.application_id, - version=self.application_version, - document_name=document_name, + endpoint_url, token_provider, ssl_verify, proxy = self._prepare_document_request( + document_name=document_name, suffix="file" ) - ssl_ca_cert = getattr(configuration, "ssl_ca_cert", None) - verify_ssl = getattr(configuration, "verify_ssl", True) - ssl_verify: bool | str = ssl_ca_cert or verify_ssl - # Honor the codegen client's token_provider when set: Client.get_api_client() - # wires it up with use_cache=cache_token, so a user who instantiates - # Client(cache_token=False) does not want us to read/write the token cache. - # Fall back to get_token() only when the configuration was built outside - # of Client (e.g. unit tests with bare PublicApi). - token_provider = getattr(configuration, "token_provider", None) or get_token - proxy = getattr(configuration, "proxy", None) + + def _stream_to_disk() -> None: + with destination_path.open("wb") as out_file: + self._stream_document( + url=endpoint_url, + write_chunk=out_file.write, + document_name=document_name, + token_provider=token_provider, + ssl_verify=ssl_verify, + proxy=proxy, + ) Retrying( retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), @@ -456,16 +454,7 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: ), before_sleep=_log_retry_attempt, reraise=True, - )( - lambda: self._stream_document( - url=endpoint_url, - destination_path=destination_path, - document_name=document_name, - token_provider=token_provider, - ssl_verify=ssl_verify, - proxy=proxy, - ) - ) + )(_stream_to_disk) return destination_path @staticmethod @@ -483,33 +472,77 @@ def _resolve_destination_path(destination: Path | str, document_name: str) -> Pa return destination_path @staticmethod - def _build_document_endpoint_url(host: str, application_id: str, version: str, document_name: str) -> str: - """Build the document file endpoint URL with each path segment encoded individually. + def _build_document_endpoint_url( + host: str, application_id: str, version: str, document_name: str, suffix: str + ) -> str: + """Build a per-document endpoint URL with each path segment encoded individually. Per-segment encoding ensures reserved characters (spaces, '#', '?', '/', ...) inside a document name cannot inject extra path segments or query strings into the URL. + Args: + host: API host (without trailing slash). + application_id: Application ID. + version: Application version (semver string). + document_name: Document filename. + suffix: Endpoint variant — ``"file"`` for browser-attachment downloads or + ``"content"`` for programmatic raw-content streaming. + Returns: - str: The fully-qualified ``/api/v1/applications/.../documents/.../file`` URL. + str: The fully-qualified ``/api/v1/applications/.../documents/.../{suffix}`` URL. """ encoded_application_id = quote(application_id, safe="") encoded_version = quote(version, safe="") encoded_document_name = quote(document_name, safe="") return ( f"{host}/api/v1/applications/{encoded_application_id}" - f"/versions/{encoded_version}/documents/{encoded_document_name}/file" + f"/versions/{encoded_version}/documents/{encoded_document_name}/{suffix}" + ) + + def _prepare_document_request( + self, document_name: str, suffix: str + ) -> tuple[str, t.Callable[[], str], bool | str, str | None]: + """Resolve the endpoint URL and the codegen client's transport settings for a document. + + Honors the codegen client's ``token_provider`` when set: ``Client.get_api_client()`` + wires it up with ``use_cache=cache_token``, so a user who instantiates + ``Client(cache_token=False)`` does not want us to read/write the token cache. + Falls back to ``get_token()`` only when the configuration was built outside of + ``Client`` (e.g. unit tests with bare ``PublicApi``). + + Returns: + tuple of (endpoint_url, token_provider, ssl_verify, proxy). + """ + configuration = self._api.api_client.configuration + endpoint_url = self._build_document_endpoint_url( + host=configuration.host.rstrip("/"), + application_id=self.application_id, + version=self.application_version, + document_name=document_name, + suffix=suffix, ) + ssl_ca_cert = getattr(configuration, "ssl_ca_cert", None) + verify_ssl = getattr(configuration, "verify_ssl", True) + ssl_verify: bool | str = ssl_ca_cert or verify_ssl + token_provider = getattr(configuration, "token_provider", None) or get_token + proxy = getattr(configuration, "proxy", None) + return endpoint_url, token_provider, ssl_verify, proxy def _stream_document( # noqa: PLR0913, PLR0917 -- private helper, splitting params would require a thin DTO self, url: str, - destination_path: Path, + write_chunk: t.Callable[[bytes], object], document_name: str, token_provider: t.Callable[[], str], ssl_verify: bool | str, proxy: str | None, ) -> None: - """Stream a single document download to disk, mapping HTTP errors to platform exceptions. + """Stream a single document download into a caller-provided sink. + + ``write_chunk`` is invoked for each non-empty body chunk; the caller decides + where the bytes go (file on disk, in-memory buffer, ...). Return type is + ``object`` so both ``BinaryIO.write`` and ``BytesIO.write`` (which return + the number of bytes written) are accepted without a cast. Raises: NotFoundException: When the platform returns 404 for the document. @@ -539,15 +572,67 @@ def _stream_document( # noqa: PLR0913, PLR0917 -- private helper, splitting par if response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: raise ServiceException(status=response.status_code, reason=response.reason) response.raise_for_status() - with destination_path.open("wb") as out_file: - for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): - if chunk: - out_file.write(chunk) + for chunk in response.iter_content(chunk_size=_DOCUMENT_DOWNLOAD_CHUNK_SIZE): + if chunk: + write_chunk(chunk) except requests.Timeout as e: raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Request timed out") from e except requests.ConnectionError as e: raise ServiceException(status=HTTPStatus.SERVICE_UNAVAILABLE.value, reason="Connection failed") from e + def read_content(self, document_name: str) -> bytes: + """Fetch a release document's raw content into memory. + + Calls ``GET /api/v1/applications/{application_id}/versions/{version}/documents/{name}/content``, + which returns a ``307`` redirect to a short-lived GCS signed URL. Unlike ``/file``, + no ``Content-Disposition`` override is set — GCS serves the object body with its + stored ``Content-Type`` and ``Cache-Control: no-store``. + + Use this for small documents (JSON manifests, license text, etc.) where holding + the bytes in memory is appropriate. For large files, prefer ``download_to_path``, + which streams directly to disk. + + Document downloads do not carry a CRC32C checksum (unlike run artifacts); + integrity is bounded by HTTPS transport and the signed-URL lifetime. + + Args: + document_name (str): The document filename. + + Returns: + bytes: The raw document content. + + Raises: + NotFoundException: When the document does not exist, is not public, or is not uploaded. + ServiceException: 5xx errors, request timeouts, or connection errors after retries. + requests.HTTPError: For other 4xx errors or signed-URL download failures. + """ + endpoint_url, token_provider, ssl_verify, proxy = self._prepare_document_request( + document_name=document_name, suffix="content" + ) + + def _stream_to_buffer() -> bytes: + buffer = BytesIO() + self._stream_document( + url=endpoint_url, + write_chunk=buffer.write, + document_name=document_name, + token_provider=token_provider, + ssl_verify=ssl_verify, + proxy=proxy, + ) + return buffer.getvalue() + + return Retrying( + retry=retry_if_exception_type(exception_types=RETRYABLE_EXCEPTIONS), + stop=stop_after_attempt(settings().application_version_retry_attempts), + wait=wait_exponential_jitter( + initial=settings().application_version_retry_wait_min, + max=settings().application_version_retry_wait_max, + ), + before_sleep=_log_retry_attempt, + reraise=True, + )(_stream_to_buffer) + class Applications: """Resource class for managing applications. diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index b1a1e610..a1de910a 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -335,6 +335,47 @@ def test_documents_download_to_path_404_raises_not_found(documents: Documents, t documents.download_to_path("missing.pdf", tmp_path) +@pytest.mark.unit +def test_documents_read_content_returns_bytes(documents: Documents) -> None: + """read_content() follows the /content redirect and returns the body as bytes.""" + body_response = MagicMock() + body_response.status_code = HTTPStatus.OK + body_response.iter_content.return_value = [b"hello ", b"world"] + body_response.raise_for_status = MagicMock() + body_response.__enter__.return_value = body_response + body_response.__exit__.return_value = False + + with patch( + "aignostics.platform.resources.applications.requests.get", + return_value=body_response, + ) as mock_get: + result = documents.read_content(DOCUMENT_OUTPUT_DESCRIPTION_PDF) + + assert result == b"hello world" + mock_get.assert_called_once() + called_url = mock_get.call_args.args[0] + assert called_url.endswith( + f"/api/v1/applications/heta/versions/1.0.0/documents/{DOCUMENT_OUTPUT_DESCRIPTION_PDF}/content" + ) + assert mock_get.call_args.kwargs["allow_redirects"] is True + + +@pytest.mark.unit +def test_documents_read_content_404_raises_not_found(documents: Documents) -> None: + """A 404 from the /content endpoint is mapped to NotFoundException.""" + response = MagicMock() + response.status_code = HTTPStatus.NOT_FOUND + response.reason = "Not Found" + response.__enter__.return_value = response + response.__exit__.return_value = False + + with ( + patch("aignostics.platform.resources.applications.requests.get", return_value=response), + pytest.raises(NotFoundException), + ): + documents.read_content("missing.pdf") + + @pytest.mark.unit def test_versions_documents_returns_documents_resource(mock_api: Mock) -> None: """Versions.documents() returns a Documents instance bound to the version pair.""" From e73ecdd01fcd14c946b06c18f9856e63c3c9a5c3 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Wed, 29 Apr 2026 17:20:18 +0200 Subject: [PATCH 11/16] fix: test after model update --- tests/aignostics/application/cli_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index b0350c6b..2ab35e4f 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -927,6 +927,7 @@ def test_cli_run_describe_json_includes_items(runner: CliRunner) -> None: termination_reason=ItemTerminationReason.SUCCEEDED, error_message=None, error_code=None, + input_artifacts=[], output_artifacts=[], ) From b313a643c92e6fdcb42174b85a6bb3899a58d167 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Thu, 30 Apr 2026 08:01:11 +0200 Subject: [PATCH 12/16] test(application): cover document CLI error paths and JSON output [PYSDK-122] Adds 15 unit tests for the document list/describe/download CLI commands covering the unresolvable-version path, the missing-document path, and generic-failure path in both text and JSON output formats. Reuses test literals via shared module-level constants to keep SonarCloud's S1192 duplicate-string check quiet. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/aignostics/application/cli_test.py | 422 ++++++++++++++++++++++- 1 file changed, 411 insertions(+), 11 deletions(-) diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index 2ab35e4f..5c80c1a9 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -49,8 +49,16 @@ RUN_CSV_FILENAME = "run.csv" DOCUMENT_OUTPUT_DESCRIPTION_PDF = "output_description.pdf" +DOCUMENT_MODEL_CARD_PDF = "model_card.pdf" +DOCUMENT_MISSING_PDF = "missing.pdf" APPLICATION_CLI_CLIENT_PATCH_TARGET = "aignostics.application._cli.Client" +# Stub values reused across the document CLI tests. +DOCUMENT_TEST_FAILURE_MESSAGE = "kaboom" # canonical exception body for unexpected-failure paths +DOCUMENT_LATEST_VERSION_NUMBER = "1.0.0" +DOCUMENT_ERROR_CODE_NOT_FOUND = "not_found" # JSON-error contract: missing resource +DOCUMENT_ERROR_CODE_FAILED = "failed" # JSON-error contract: unexpected failure + # Full SPOT_0 CSV - single source of truth for all run submissions in this test file. CSV_CONTENT_SPOT0 = ( "external_id;checksum_base64_crc32c;resolution_mpp;width_px;height_px;" @@ -1706,11 +1714,11 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record fake_documents = MagicMock() fake_documents.list.return_value = [ _make_document_stub(DOCUMENT_OUTPUT_DESCRIPTION_PDF), - _make_document_stub("model_card.pdf"), + _make_document_stub(DOCUMENT_MODEL_CARD_PDF), ] fake_client = MagicMock() latest_version = MagicMock() - latest_version.number = "1.0.0" + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents @@ -1720,9 +1728,9 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record assert result.exit_code == 0 output = normalize_output(result.output) assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output - assert "model_card.pdf" in output - assert "application/pdf" in output - fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") + assert DOCUMENT_MODEL_CARD_PDF in output + assert "application/pdf" in output # NOSONAR python:S1192: standard MIME type literal is clearer than a constant + fake_client.applications.versions.documents.assert_called_once_with("heta", DOCUMENT_LATEST_VERSION_NUMBER) @pytest.mark.unit @@ -1743,10 +1751,10 @@ def test_cli_application_version_document_describe_success(runner: CliRunner, re assert result.exit_code == 0 output = normalize_output(result.output) assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output - assert "application/pdf" in output + assert "application/pdf" in output # NOSONAR python:S1192: standard MIME type literal is clearer than a constant # Explicit version supplied via "heta:1.0.0", so latest() should NOT be called. fake_client.applications.versions.latest.assert_not_called() - fake_client.applications.versions.documents.assert_called_once_with("heta", "1.0.0") + fake_client.applications.versions.documents.assert_called_once_with("heta", DOCUMENT_LATEST_VERSION_NUMBER) fake_documents.details.assert_called_once_with(DOCUMENT_OUTPUT_DESCRIPTION_PDF) @@ -1760,19 +1768,19 @@ def test_cli_application_version_document_describe_not_found(runner: CliRunner, fake_documents.details.side_effect = ApiNotFound(status=404, reason="Not Found") fake_client = MagicMock() latest_version = MagicMock() - latest_version.number = "1.0.0" + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( cli, - ["application", "version", "document", "describe", "heta", "missing.pdf"], + ["application", "version", "document", "describe", "heta", DOCUMENT_MISSING_PDF], ) assert result.exit_code == 2 output = normalize_output(result.output) - assert "Document 'missing.pdf' not found for application version 'heta'." in output + assert f"Document '{DOCUMENT_MISSING_PDF}' not found for application version 'heta'." in output @pytest.mark.unit @@ -1784,7 +1792,7 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm fake_documents.download_to_path.return_value = expected_path fake_client = MagicMock() latest_version = MagicMock() - latest_version.number = "1.0.0" + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents @@ -1809,3 +1817,395 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm fake_documents.download_to_path.assert_called_once() args, _ = fake_documents.download_to_path.call_args assert args[0] == DOCUMENT_OUTPUT_DESCRIPTION_PDF + + +@pytest.mark.unit +def test_cli_application_version_document_list_json_success(runner: CliRunner, record_property) -> None: + """`application version document list --format json` emits a JSON array of documents.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_documents = MagicMock() + fake_documents.list.return_value = [ + _make_document_stub(DOCUMENT_OUTPUT_DESCRIPTION_PDF), + _make_document_stub(DOCUMENT_MODEL_CARD_PDF), + ] + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta", "--format", "json"]) + + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert isinstance(payload, list) + assert len(payload) == 2 + assert payload[0]["name"] == DOCUMENT_OUTPUT_DESCRIPTION_PDF + assert payload[1]["name"] == DOCUMENT_MODEL_CARD_PDF + assert ( + payload[0]["mime_type"] == "application/pdf" + ) # NOSONAR python:S1192: standard MIME type literal is clearer than a constant + + +@pytest.mark.unit +def test_cli_application_version_document_list_json_empty(runner: CliRunner, record_property) -> None: + """`application version document list --format json` emits an empty array when none attached.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_documents = MagicMock() + fake_documents.list.return_value = [] + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta", "--format", "json"]) + + assert result.exit_code == 0 + assert json.loads(result.stdout) == [] + + +@pytest.mark.unit +def test_cli_application_version_document_list_resolve_not_found_text(runner: CliRunner, record_property) -> None: + """`application version document list` exits 2 when no versions exist (text format).""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_client = MagicMock() + # `latest()` returning None triggers `_resolve_documents` to raise NotFoundException. + fake_client.applications.versions.latest.return_value = None + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta"]) + + assert result.exit_code == 2 + output = normalize_output(result.output) + assert "No release documents found" in output + assert "'heta'" in output + + +@pytest.mark.unit +def test_cli_application_version_document_list_resolve_not_found_json(runner: CliRunner, record_property) -> None: + """`application version document list --format json` emits structured error on 404.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_client = MagicMock() + fake_client.applications.versions.latest.return_value = None + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta", "--format", "json"]) + + assert result.exit_code == 2 + payload = json.loads(result.stderr) + assert payload["error"] == DOCUMENT_ERROR_CODE_NOT_FOUND + assert "heta" in payload["message"] + + +@pytest.mark.unit +def test_cli_application_version_document_list_failed_text(runner: CliRunner, record_property) -> None: + """`application version document list` exits 1 with an error message on unexpected failure.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_documents = MagicMock() + fake_documents.list.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta"]) + + assert result.exit_code == 1 + output = normalize_output(result.output) + assert "Failed to list release documents for 'heta'" in output + assert DOCUMENT_TEST_FAILURE_MESSAGE in output + + +@pytest.mark.unit +def test_cli_application_version_document_list_failed_json(runner: CliRunner, record_property) -> None: + """`application version document list --format json` emits structured error on failure.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + fake_documents = MagicMock() + fake_documents.list.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke(cli, ["application", "version", "document", "list", "heta", "--format", "json"]) + + assert result.exit_code == 1 + payload = json.loads(result.stderr) + assert payload["error"] == DOCUMENT_ERROR_CODE_FAILED + assert DOCUMENT_TEST_FAILURE_MESSAGE in payload["message"] + + +@pytest.mark.unit +def test_cli_application_version_document_describe_json_success(runner: CliRunner, record_property) -> None: + """`application version document describe --format json` emits a single JSON object.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-02") + fake_documents = MagicMock() + fake_documents.details.return_value = _make_document_stub(DOCUMENT_OUTPUT_DESCRIPTION_PDF) + fake_client = MagicMock() + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "describe", + "heta:1.0.0", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--format", + "json", + ], + ) + + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert payload["name"] == DOCUMENT_OUTPUT_DESCRIPTION_PDF + assert ( + payload["mime_type"] == "application/pdf" + ) # NOSONAR python:S1192: standard MIME type literal is clearer than a constant + assert payload["visibility"] == "public" + + +@pytest.mark.unit +def test_cli_application_version_document_describe_resolve_not_found_text(runner: CliRunner, record_property) -> None: + """`describe` exits 2 when the application version cannot be resolved (text format).""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + fake_client = MagicMock() + fake_client.applications.versions.latest.return_value = None + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + ["application", "version", "document", "describe", "heta", DOCUMENT_OUTPUT_DESCRIPTION_PDF], + ) + + assert result.exit_code == 2 + output = normalize_output(result.output) + assert "Application version 'heta' is unavailable." in output + + +@pytest.mark.unit +def test_cli_application_version_document_describe_resolve_not_found_json(runner: CliRunner, record_property) -> None: + """`describe --format json` emits structured error when version cannot be resolved.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + fake_client = MagicMock() + fake_client.applications.versions.latest.return_value = None + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "describe", + "heta", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--format", + "json", + ], + ) + + assert result.exit_code == 2 + payload = json.loads(result.stderr) + assert payload["error"] == DOCUMENT_ERROR_CODE_NOT_FOUND + assert "heta" in payload["message"] + + +@pytest.mark.unit +def test_cli_application_version_document_describe_not_found_json(runner: CliRunner, record_property) -> None: + """`describe --format json` emits structured error when the document is missing.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + + fake_documents = MagicMock() + fake_documents.details.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "describe", + "heta", + DOCUMENT_MISSING_PDF, + "--format", + "json", + ], + ) + + assert result.exit_code == 2 + payload = json.loads(result.stderr) + assert payload["error"] == DOCUMENT_ERROR_CODE_NOT_FOUND + assert DOCUMENT_MISSING_PDF in payload["message"] + + +@pytest.mark.unit +def test_cli_application_version_document_describe_failed_text(runner: CliRunner, record_property) -> None: + """`describe` exits 1 with an error message on an unexpected failure (text format).""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + fake_documents = MagicMock() + fake_documents.details.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + ["application", "version", "document", "describe", "heta", DOCUMENT_OUTPUT_DESCRIPTION_PDF], + ) + + assert result.exit_code == 1 + output = normalize_output(result.output) + assert "Failed to describe release document" in output + assert DOCUMENT_TEST_FAILURE_MESSAGE in output + + +@pytest.mark.unit +def test_cli_application_version_document_describe_failed_json(runner: CliRunner, record_property) -> None: + """`describe --format json` emits structured error on an unexpected failure.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + fake_documents = MagicMock() + fake_documents.details.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "describe", + "heta", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--format", + "json", + ], + ) + + assert result.exit_code == 1 + payload = json.loads(result.stderr) + assert payload["error"] == DOCUMENT_ERROR_CODE_FAILED + assert DOCUMENT_TEST_FAILURE_MESSAGE in payload["message"] + + +@pytest.mark.unit +def test_cli_application_version_document_download_resolve_not_found( + runner: CliRunner, tmp_path: Path, record_property +) -> None: + """`download` exits 2 when the application version cannot be resolved.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") + fake_client = MagicMock() + fake_client.applications.versions.latest.return_value = None + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "download", + "heta", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--output", + str(tmp_path), + ], + ) + + assert result.exit_code == 2 + output = normalize_output(result.output) + assert "Application version 'heta' is unavailable." in output + + +@pytest.mark.unit +def test_cli_application_version_document_download_not_found( + runner: CliRunner, tmp_path: Path, record_property +) -> None: + """`download` exits 2 with a clear message when the document does not exist.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + + fake_documents = MagicMock() + fake_documents.download_to_path.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "download", + "heta", + DOCUMENT_MISSING_PDF, + "--output", + str(tmp_path), + ], + ) + + assert result.exit_code == 2 + output = normalize_output(result.output) + assert f"Document '{DOCUMENT_MISSING_PDF}' not found for application version 'heta'." in output + + +@pytest.mark.unit +def test_cli_application_version_document_download_failed(runner: CliRunner, tmp_path: Path, record_property) -> None: + """`download` exits 1 with an error message on an unexpected failure.""" + record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") + fake_documents = MagicMock() + fake_documents.download_to_path.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) + fake_client = MagicMock() + latest_version = MagicMock() + latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER + fake_client.applications.versions.latest.return_value = latest_version + fake_client.applications.versions.documents.return_value = fake_documents + + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + result = runner.invoke( + cli, + [ + "application", + "version", + "document", + "download", + "heta", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--output", + str(tmp_path), + ], + ) + + assert result.exit_code == 1 + output = normalize_output(result.output) + assert "Failed to download release document" in output + assert DOCUMENT_TEST_FAILURE_MESSAGE in output From b8788e084be28b2002c1cc480bf1ace4b652fe4e Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Thu, 30 Apr 2026 09:21:25 +0200 Subject: [PATCH 13/16] refactor(platform): address SonarCloud findings on documents PR [PYSDK-122] Replace non-standard `# noqa: ... -- justification` form with the documented ruff syntax plus a preceding rationale comment, and lift the duplicated "Not Found" / "a.pdf" string literals into module-level test constants so the duplicate-literal smell is resolved without NOSONAR suppressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aignostics/platform/resources/applications.py | 3 ++- tests/aignostics/application/cli_test.py | 7 ++++--- tests/aignostics/platform/resources/applications_test.py | 9 +++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index 265149c4..edae1978 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -528,7 +528,8 @@ def _prepare_document_request( proxy = getattr(configuration, "proxy", None) return endpoint_url, token_provider, ssl_verify, proxy - def _stream_document( # noqa: PLR0913, PLR0917 -- private helper, splitting params would require a thin DTO + # Private helper; splitting params would require a thin DTO. + def _stream_document( # noqa: PLR0913, PLR0917 self, url: str, write_chunk: t.Callable[[bytes], object], diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index 5c80c1a9..dbc0d2fb 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -39,6 +39,7 @@ ) MESSAGE_RUN_NOT_FOUND = "Warning: Run with ID '4711' not found" +API_REASON_NOT_FOUND = "Not Found" TEST_APPLICATION_DEADLINE_SECONDS = 60 * 45 # 45 minutes TEST_APPLICATION_DUE_DATE_SECONDS = 60 * 10 # 10 minutes @@ -1765,7 +1766,7 @@ def test_cli_application_version_document_describe_not_found(runner: CliRunner, from aignx.codegen.exceptions import NotFoundException as ApiNotFound fake_documents = MagicMock() - fake_documents.details.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() latest_version = MagicMock() latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER @@ -2028,7 +2029,7 @@ def test_cli_application_version_document_describe_not_found_json(runner: CliRun from aignx.codegen.exceptions import NotFoundException as ApiNotFound fake_documents = MagicMock() - fake_documents.details.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() latest_version = MagicMock() latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER @@ -2151,7 +2152,7 @@ def test_cli_application_version_document_download_not_found( from aignx.codegen.exceptions import NotFoundException as ApiNotFound fake_documents = MagicMock() - fake_documents.download_to_path.side_effect = ApiNotFound(status=404, reason="Not Found") + fake_documents.download_to_path.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() latest_version = MagicMock() latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index a1de910a..c8e186e8 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -29,6 +29,7 @@ API_ERROR = "API error" DOCUMENT_OUTPUT_DESCRIPTION_PDF = "output_description.pdf" +DOC_FILENAME_A = "a.pdf" @pytest.fixture @@ -225,15 +226,15 @@ def documents(mock_api: Mock) -> Documents: def test_documents_list_returns_wrapped_models(documents: Documents, mock_api: Mock) -> None: """Documents.list() returns ApplicationVersionDocument instances.""" mock_api.list_version_documents.return_value = [ - _make_doc("a.pdf"), + _make_doc(DOC_FILENAME_A), _make_doc("b.pdf"), - ] # NOSONAR python:S1192: arbitrary placeholder filename, a constant adds no clarity + ] result = documents.list() assert len(result) == 2 assert all(isinstance(item, ApplicationVersionDocument) for item in result) - assert {d.name for d in result} == {"a.pdf", "b.pdf"} + assert {d.name for d in result} == {DOC_FILENAME_A, "b.pdf"} assert result[0].visibility == "public" mock_api.list_version_documents.assert_called_once() call_kwargs = mock_api.list_version_documents.call_args.kwargs @@ -254,7 +255,7 @@ def test_documents_list_returns_empty_list(documents: Documents, mock_api: Mock) @pytest.mark.unit def test_documents_list_uses_cache_then_bypasses_with_nocache(documents: Documents, mock_api: Mock) -> None: """list() caches results across calls; nocache=True forces a fresh call.""" - mock_api.list_version_documents.return_value = [_make_doc("a.pdf")] # NOSONAR python:S1192 + mock_api.list_version_documents.return_value = [_make_doc(DOC_FILENAME_A)] # First call hits the API and caches. documents.list() From a7761ca299df0579e25af9fcce0ed298d6eaf36b Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Thu, 30 Apr 2026 10:19:50 +0200 Subject: [PATCH 14/16] fixup! refactor(platform): address SonarCloud findings on documents PR [PYSDK-122] --- .../platform/resources/applications_test.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index c8e186e8..25d2d19c 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -27,10 +27,12 @@ from aignostics.platform.resources.utils import PAGE_SIZE API_ERROR = "API error" +API_REASON_NOT_FOUND = "Not Found" DOCUMENT_OUTPUT_DESCRIPTION_PDF = "output_description.pdf" +DOCUMENT_MISSING_PDF = "missing.pdf" DOC_FILENAME_A = "a.pdf" - +REQUESTS_GET_PATCH_TARGET = "aignostics.platform.resources.applications.requests.get" @pytest.fixture def mock_api() -> Mock: @@ -287,10 +289,10 @@ def test_documents_details_returns_wrapped_model(documents: Documents, mock_api: @pytest.mark.unit def test_documents_details_propagates_not_found(documents: Documents, mock_api: Mock) -> None: """Documents.details() propagates a 404 NotFoundException from the codegen client.""" - mock_api.get_version_document.side_effect = NotFoundException(status=404, reason="Not Found") + mock_api.get_version_document.side_effect = NotFoundException(status=404, reason=API_REASON_NOT_FOUND) with pytest.raises(NotFoundException): - documents.details("missing.pdf") + documents.details(DOCUMENT_MISSING_PDF) @pytest.mark.unit @@ -304,7 +306,7 @@ def test_documents_download_to_path_writes_file(documents: Documents, tmp_path: body_response.__exit__.return_value = False with patch( - "aignostics.platform.resources.applications.requests.get", + REQUESTS_GET_PATCH_TARGET, return_value=body_response, ) as mock_get: result = documents.download_to_path(DOCUMENT_OUTPUT_DESCRIPTION_PDF, tmp_path) @@ -325,15 +327,15 @@ def test_documents_download_to_path_404_raises_not_found(documents: Documents, t """A 404 from the documents endpoint is mapped to NotFoundException.""" response = MagicMock() response.status_code = HTTPStatus.NOT_FOUND - response.reason = "Not Found" + response.reason = API_REASON_NOT_FOUND response.__enter__.return_value = response response.__exit__.return_value = False with ( - patch("aignostics.platform.resources.applications.requests.get", return_value=response), + patch(REQUESTS_GET_PATCH_TARGET, return_value=response), pytest.raises(NotFoundException), ): - documents.download_to_path("missing.pdf", tmp_path) + documents.download_to_path(DOCUMENT_MISSING_PDF, tmp_path) @pytest.mark.unit @@ -347,7 +349,7 @@ def test_documents_read_content_returns_bytes(documents: Documents) -> None: body_response.__exit__.return_value = False with patch( - "aignostics.platform.resources.applications.requests.get", + REQUESTS_GET_PATCH_TARGET, return_value=body_response, ) as mock_get: result = documents.read_content(DOCUMENT_OUTPUT_DESCRIPTION_PDF) @@ -366,15 +368,15 @@ def test_documents_read_content_404_raises_not_found(documents: Documents) -> No """A 404 from the /content endpoint is mapped to NotFoundException.""" response = MagicMock() response.status_code = HTTPStatus.NOT_FOUND - response.reason = "Not Found" + response.reason = API_REASON_NOT_FOUND response.__enter__.return_value = response response.__exit__.return_value = False with ( - patch("aignostics.platform.resources.applications.requests.get", return_value=response), + patch(REQUESTS_GET_PATCH_TARGET, return_value=response), pytest.raises(NotFoundException), ): - documents.read_content("missing.pdf") + documents.read_content(DOCUMENT_MISSING_PDF) @pytest.mark.unit From 4cac6f19ca9daa68774f99896480eae734a1bf01 Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Thu, 30 Apr 2026 15:58:28 +0200 Subject: [PATCH 15/16] chore: address review comments --- specifications/SPEC-APPLICATION-SERVICE.md | 8 +- src/aignostics/application/_cli.py | 105 ++++++------------ .../platform/resources/applications.py | 68 +++++++----- tests/aignostics/application/cli_test.py | 83 ++++++-------- .../platform/resources/applications_test.py | 22 +++- 5 files changed, 133 insertions(+), 153 deletions(-) diff --git a/specifications/SPEC-APPLICATION-SERVICE.md b/specifications/SPEC-APPLICATION-SERVICE.md index 1b684fe9..6dd42cf1 100644 --- a/specifications/SPEC-APPLICATION-SERVICE.md +++ b/specifications/SPEC-APPLICATION-SERVICE.md @@ -410,14 +410,14 @@ uvx aignostics application [subcommand] [options] - `list`: List all available applications with filtering - `describe`: Get detailed information about a specific application - `dump-schemata`: Export application schemata -- `version document list APPLICATION_VERSION_ID`: List public release documents attached to an application version -- `version document describe APPLICATION_VERSION_ID DOCUMENT_NAME`: Show metadata for a single public release document -- `version document download APPLICATION_VERSION_ID DOCUMENT_NAME [--output PATH]`: Download a public release document to a local path +- `version document list`: List public release documents attached to an application version +- `version document describe`: Show metadata for a single public release document +- `version document download`: Download a public release document to a local path - `run execute`: Combined prepare, upload, and submit workflow - `run prepare`: Generate metadata from source directory - `run upload`: Upload files to cloud storage - `run submit`: Submit application run -- `run list [--for-organization ORG_ID]`: List application runs; supports listing all runs for an organization with `--for-organization` (only available to org admins) +- `run list`: List application runs - `run describe`: Get detailed run information - `run cancel`: Cancel running application - `run result download`: Download run results diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index f2415a00..7bf481ba 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -20,7 +20,6 @@ DEFAULT_MAX_GPUS_PER_SLIDE, DEFAULT_NODE_ACQUISITION_TIMEOUT_MINUTES, Client, - Documents, ForbiddenException, NotFoundException, RunState, @@ -145,24 +144,6 @@ ) -def _parse_application_version_id(application_version_id: str) -> tuple[str, str | None]: - """Parse a CLI APPLICATION_VERSION_ID positional into (application_id, version_number). - - Accepts either ``application_id`` (latest version is resolved later) or - ``application_id:version_number`` (explicit semantic version). - - Args: - application_version_id: The raw CLI argument, e.g. ``"heta"`` or ``"heta:1.0.0"``. - - Returns: - tuple[str, str | None]: (application_id, version_number_or_None_for_latest). - """ - if ":" in application_version_id: - app_id, _, version = application_version_id.partition(":") - return app_id, (version or None) - return application_version_id, None - - def _abort_if_system_unhealthy() -> None: health = asyncio.run(SystemService.health_static()) if not health: @@ -1558,50 +1539,25 @@ def result_delete( sys.exit(1) -_APPLICATION_VERSION_ID_HELP = ( - "Application version identifier. Either an application id (e.g. 'heta' — uses the " - "latest version) or 'application_id:version_number' (e.g. 'heta:1.0.0')." -) - - -def _resolve_documents(application_version_id: str) -> tuple[str, str, Documents]: - """Resolve a CLI APPLICATION_VERSION_ID into (application_id, version_number, Documents). - - Returns: - (application_id, version_number, documents_resource). - - Raises: - NotFoundException: If the application or its versions cannot be located. - """ - application_id, version_number = _parse_application_version_id(application_version_id) - client = Client() - if version_number is None: - # Resolve the latest version for the application. - version = client.applications.versions.latest(application=application_id) - if version is None: - raise NotFoundException( - status=404, - reason=f"No versions found for application '{application_id}'.", - ) - version_number = version.number - documents = client.applications.versions.documents(application_id, version_number) - return application_id, version_number, documents - - @document_app.command("list") def application_version_document_list( - application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], + application_id: Annotated[ + str, + typer.Argument(help="Id of application to list release documents for."), + ], + application_version: ApplicationVersionOption = None, format: Annotated[ # noqa: A002 str, typer.Option(help="Output format: 'text' (default) or 'json'"), ] = "text", ) -> None: """List public release documents attached to an application version.""" + version_ref = f"{application_id}:{application_version}" if application_version else application_id try: - application_id, version_number, documents = _resolve_documents(application_version_id) + documents = Client().applications.versions.documents(application_id, application_version) items = documents.list() except NotFoundException as e: - message = f"No release documents found: application version '{application_version_id}' is unavailable." + message = f"No release documents found: application version '{version_ref}' is unavailable." logger.warning("{} ({})", message, e) # NOSONAR python:S1192: loguru positional format string is conventional if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) @@ -1609,11 +1565,11 @@ def application_version_document_list( console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) except Exception as e: - logger.exception(f"Failed to list release documents for '{application_version_id}'") + logger.exception(f"Failed to list release documents for '{version_ref}'") if format == "json": print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) else: - console.print(f"[error]Error:[/error] Failed to list release documents for '{application_version_id}': {e}") + console.print(f"[error]Error:[/error] Failed to list release documents for '{version_ref}': {e}") sys.exit(1) if format == "json": @@ -1621,7 +1577,7 @@ def application_version_document_list( print(json.dumps(payload, indent=2, default=str)) return - console.print(f"[bold]Release documents for {application_id} {version_number}[/bold]") + console.print(f"[bold]Release documents for {version_ref.replace(':', ' ')}[/bold]") console.print("=" * 80) if not items: console.print("[dim]No public release documents are attached to this version.[/dim]") @@ -1636,18 +1592,23 @@ def application_version_document_list( @document_app.command("describe") def application_version_document_describe( - application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], + application_id: Annotated[ + str, + typer.Argument(help="Id of application to describe release documents for."), + ], document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], + application_version: ApplicationVersionOption = None, format: Annotated[ # noqa: A002 str, typer.Option(help="Output format: 'text' (default) or 'json'"), ] = "text", ) -> None: """Show metadata for a single public release document.""" + version_ref = f"{application_id}:{application_version}" if application_version else application_id try: - application_id, version_number, documents = _resolve_documents(application_version_id) + documents = Client().applications.versions.documents(application_id, application_version) except NotFoundException as e: - message = f"Application version '{application_version_id}' is unavailable." + message = f"Application version '{version_ref}' is unavailable." logger.warning("{} ({})", message, e) # NOSONAR python:S1192 if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) @@ -1657,7 +1618,7 @@ def application_version_document_describe( try: doc = documents.details(document_name) except NotFoundException: - message = f"Document '{document_name}' not found for application version '{application_version_id}'." + message = f"Document '{document_name}' not found for application version '{version_ref}'." logger.warning(message) if format == "json": print(json.dumps({"error": "not_found", "message": message}), file=sys.stderr) @@ -1665,13 +1626,12 @@ def application_version_document_describe( console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) except Exception as e: - logger.exception(f"Failed to describe release document '{document_name}' for '{application_version_id}'") + logger.exception(f"Failed to describe release document '{document_name}' for '{version_ref}'") if format == "json": print(json.dumps({"error": "failed", "message": str(e)}), file=sys.stderr) else: console.print( - f"[error]Error:[/error] Failed to describe release document " - f"'{document_name}' for '{application_version_id}': {e}" + f"[error]Error:[/error] Failed to describe release document '{document_name}' for '{version_ref}': {e}" ) sys.exit(1) @@ -1679,20 +1639,23 @@ def application_version_document_describe( print(json.dumps(doc.model_dump(mode="json"), indent=2, default=str)) return - console.print(f"[bold]Release document '{doc.name}' on {application_id} {version_number}[/bold]") + console.print(f"[bold]Release document '{doc.name}' on {version_ref.replace(':', ' ')}[/bold]") console.print("=" * 80) console.print(f"[bold]Id:[/bold] {doc.id}") console.print(f"[bold]Name:[/bold] {doc.name}") console.print(f"[bold]MIME type:[/bold] {doc.mime_type}") - console.print(f"[bold]Visibility:[/bold] {doc.visibility}") console.print(f"[bold]Created at:[/bold] {doc.created_at.isoformat()}") console.print(f"[bold]Updated at:[/bold] {doc.updated_at.isoformat()}") @document_app.command("download") def application_version_document_download( - application_version_id: Annotated[str, typer.Argument(..., help=_APPLICATION_VERSION_ID_HELP)], + application_id: Annotated[ + str, + typer.Argument(help="Id of application to download release documents for."), + ], document_name: Annotated[str, typer.Argument(..., help="Document filename (e.g. 'output_description.pdf').")], + application_version: ApplicationVersionOption = None, output: Annotated[ Path, typer.Option( @@ -1713,25 +1676,25 @@ def application_version_document_download( Document downloads do not carry a CRC32C checksum (unlike run artifacts); integrity is bounded by HTTPS transport and the signed-URL lifetime. """ + version_ref = f"{application_id}:{application_version}" if application_version else application_id try: - _, _, documents = _resolve_documents(application_version_id) + documents = Client().applications.versions.documents(application_id, application_version) except NotFoundException as e: - message = f"Application version '{application_version_id}' is unavailable." + message = f"Application version '{version_ref}' is unavailable." logger.warning("{} ({})", message, e) # NOSONAR python:S1192 console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) try: written = documents.download_to_path(document_name, output) except NotFoundException: - message = f"Document '{document_name}' not found for application version '{application_version_id}'." + message = f"Document '{document_name}' not found for application version '{version_ref}'." logger.warning(message) console.print(f"[warning]Warning:[/warning] {message}") sys.exit(2) except Exception as e: - logger.exception(f"Failed to download release document '{document_name}' for '{application_version_id}'") + logger.exception(f"Failed to download release document '{document_name}' for '{version_ref}'") console.print( - f"[error]Error:[/error] Failed to download release document " - f"'{document_name}' for '{application_version_id}': {e}" + f"[error]Error:[/error] Failed to download release document '{document_name}' for '{version_ref}': {e}" ) sys.exit(1) diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index edae1978..dcbbad0b 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -86,6 +86,36 @@ def __init__(self, api: PublicApi) -> None: """ self._api = api + def _get_application_version_validated( + self, application_id: str, application_version: VersionTuple | str | None + ) -> str: + """Validate and extract the version string from a VersionTuple or str. + + Args: + application_id (str): The ID of the application. + application_version (VersionTuple | str | None): The version to validate. + + Returns: + str: The validated version string. + + Raises: + ValueError: If the version is not a valid semver string. + NotFoundException: If the version is None and no versions are found for the application. + """ + # Handle version resolution and validation first (not retried) + if application_version is None: + application_version = self.latest(application=application_id) + if application_version is None: + message = f"No versions found for application '{application_id}'." + raise NotFoundException(message) + application_version = application_version.number + elif isinstance(application_version, VersionTuple): + application_version = application_version.number + elif application_version and not semver.Version.is_valid(application_version): + message = f"Invalid version format: '{application_version}' not compliant with semantic versioning." + raise ValueError(message) + return application_version + def list(self, application: Application | str, nocache: bool = False) -> builtins.list[VersionTuple]: """Find all versions for a specific application. @@ -147,18 +177,7 @@ def details( NotFoundException: If the application or version is not found. aignx.codegen.exceptions.ApiException: If the API request fails. """ - # Handle version resolution and validation first (not retried) - if application_version is None: - application_version = self.latest(application=application_id) - if application_version is None: - message = f"No versions found for application '{application_id}'." - raise NotFoundException(message) - application_version = application_version.number - elif isinstance(application_version, VersionTuple): - application_version = application_version.number - elif application_version and not semver.Version.is_valid(application_version): - message = f"Invalid version format: '{application_version}' not compliant with semantic versioning." - raise ValueError(message) + application_version = self._get_application_version_validated(application_id, application_version) # Make the API call with retry logic and caching @cached_operation(ttl=settings().application_version_cache_ttl, use_token=True) @@ -235,22 +254,20 @@ def latest(self, application: Application | str, nocache: bool = False) -> Versi sorted_versions = self.list_sorted(application=application, nocache=nocache) return sorted_versions[0] if sorted_versions else None - def documents(self, application_id: str, application_version: VersionTuple | str) -> "Documents": + def documents(self, application_id: str, application_version: VersionTuple | str | None) -> "Documents": """Returns a Documents resource bound to the given application version. Args: application_id (str): The ID of the application (e.g. "heta"). - application_version (VersionTuple | str): The application version, either as a - VersionTuple or a semantic version string (e.g. "1.0.0"). + application_version (VersionTuple | str | None): The application version, either as a + VersionTuple, a semantic version string (e.g. "1.0.0"), or None to use the latest version. Returns: Documents: A Documents resource bound to the (application_id, version) pair. """ - if isinstance(application_version, VersionTuple): - version_number = application_version.number - else: - version_number = application_version - return Documents(self._api, application_id=application_id, application_version=version_number) + application_version = self._get_application_version_validated(application_id, application_version) + + return Documents(self._api, application_id=application_id, application_version=application_version) class ApplicationVersionDocument(BaseModel): @@ -263,7 +280,6 @@ class ApplicationVersionDocument(BaseModel): id: str name: str mime_type: str - visibility: str created_at: datetime updated_at: datetime @@ -283,7 +299,6 @@ def from_response(cls, data: VersionDocumentData) -> "ApplicationVersionDocument id=data.id, name=data.name, mime_type=data.mime_type, - visibility=data.visibility.value if hasattr(data.visibility, "value") else str(data.visibility), created_at=data.created_at, updated_at=data.updated_at, ) @@ -302,17 +317,20 @@ class Documents: integrity is bounded by HTTPS transport and the signed-URL lifetime. """ - def __init__(self, api: PublicApi, application_id: str, application_version: str) -> None: + def __init__(self, api: PublicApi, application_id: str, application_version: str | VersionTuple) -> None: """Initializes the Documents resource bound to an application version. Args: api (PublicApi): The configured API client. application_id (str): The ID of the application (e.g. "heta"). - application_version (str): The semantic version number (e.g. "1.0.0"). + application_version (str | VersionTuple): The semantic version number (e.g. "1.0.0") or a VersionTuple. """ self._api = api self.application_id = application_id - self.application_version = application_version + if isinstance(application_version, str): + self.application_version = application_version + else: + self.application_version = application_version.number def list(self, nocache: bool = False) -> builtins.list[ApplicationVersionDocument]: """List metadata for all public, uploaded release documents for the bound version. diff --git a/tests/aignostics/application/cli_test.py b/tests/aignostics/application/cli_test.py index dbc0d2fb..1b06abe4 100644 --- a/tests/aignostics/application/cli_test.py +++ b/tests/aignostics/application/cli_test.py @@ -1694,14 +1694,12 @@ def _make_document_stub(name: str = DOCUMENT_OUTPUT_DESCRIPTION_PDF) -> MagicMoc stub.id = "11111111-1111-1111-1111-111111111111" stub.name = name stub.mime_type = "application/pdf" # NOSONAR python:S1192: standard MIME type literal is clearer than a constant - stub.visibility = "public" stub.created_at = datetime(2026, 1, 1, 12, 0, tzinfo=UTC) stub.updated_at = datetime(2026, 1, 2, 12, 0, tzinfo=UTC) stub.model_dump.return_value = { "id": stub.id, "name": stub.name, "mime_type": stub.mime_type, - "visibility": stub.visibility, "created_at": stub.created_at.isoformat(), "updated_at": stub.updated_at.isoformat(), } @@ -1718,9 +1716,6 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record _make_document_stub(DOCUMENT_MODEL_CARD_PDF), ] fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1731,7 +1726,7 @@ def test_cli_application_version_document_list_success(runner: CliRunner, record assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output assert DOCUMENT_MODEL_CARD_PDF in output assert "application/pdf" in output # NOSONAR python:S1192: standard MIME type literal is clearer than a constant - fake_client.applications.versions.documents.assert_called_once_with("heta", DOCUMENT_LATEST_VERSION_NUMBER) + fake_client.applications.versions.documents.assert_called_once_with("heta", None) @pytest.mark.unit @@ -1746,15 +1741,22 @@ def test_cli_application_version_document_describe_success(runner: CliRunner, re with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( cli, - ["application", "version", "document", "describe", "heta:1.0.0", DOCUMENT_OUTPUT_DESCRIPTION_PDF], + [ + "application", + "version", + "document", + "describe", + "heta", + DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--application-version", + DOCUMENT_LATEST_VERSION_NUMBER, + ], ) assert result.exit_code == 0 output = normalize_output(result.output) assert DOCUMENT_OUTPUT_DESCRIPTION_PDF in output assert "application/pdf" in output # NOSONAR python:S1192: standard MIME type literal is clearer than a constant - # Explicit version supplied via "heta:1.0.0", so latest() should NOT be called. - fake_client.applications.versions.latest.assert_not_called() fake_client.applications.versions.documents.assert_called_once_with("heta", DOCUMENT_LATEST_VERSION_NUMBER) fake_documents.details.assert_called_once_with(DOCUMENT_OUTPUT_DESCRIPTION_PDF) @@ -1768,9 +1770,6 @@ def test_cli_application_version_document_describe_not_found(runner: CliRunner, fake_documents = MagicMock() fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1792,9 +1791,6 @@ def test_cli_application_version_document_download_success(runner: CliRunner, tm expected_path = tmp_path / DOCUMENT_OUTPUT_DESCRIPTION_PDF fake_documents.download_to_path.return_value = expected_path fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1830,9 +1826,6 @@ def test_cli_application_version_document_list_json_success(runner: CliRunner, r _make_document_stub(DOCUMENT_MODEL_CARD_PDF), ] fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1856,9 +1849,6 @@ def test_cli_application_version_document_list_json_empty(runner: CliRunner, rec fake_documents = MagicMock() fake_documents.list.return_value = [] fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1870,11 +1860,12 @@ def test_cli_application_version_document_list_json_empty(runner: CliRunner, rec @pytest.mark.unit def test_cli_application_version_document_list_resolve_not_found_text(runner: CliRunner, record_property) -> None: - """`application version document list` exits 2 when no versions exist (text format).""" + """`application version document list` exits 2 when the application version cannot be resolved.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + fake_client = MagicMock() - # `latest()` returning None triggers `_resolve_documents` to raise NotFoundException. - fake_client.applications.versions.latest.return_value = None + fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke(cli, ["application", "version", "document", "list", "heta"]) @@ -1882,15 +1873,17 @@ def test_cli_application_version_document_list_resolve_not_found_text(runner: Cl assert result.exit_code == 2 output = normalize_output(result.output) assert "No release documents found" in output - assert "'heta'" in output + assert "heta" in output @pytest.mark.unit def test_cli_application_version_document_list_resolve_not_found_json(runner: CliRunner, record_property) -> None: """`application version document list --format json` emits structured error on 404.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-01") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + fake_client = MagicMock() - fake_client.applications.versions.latest.return_value = None + fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke(cli, ["application", "version", "document", "list", "heta", "--format", "json"]) @@ -1908,9 +1901,6 @@ def test_cli_application_version_document_list_failed_text(runner: CliRunner, re fake_documents = MagicMock() fake_documents.list.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1929,9 +1919,6 @@ def test_cli_application_version_document_list_failed_json(runner: CliRunner, re fake_documents = MagicMock() fake_documents.list.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -1960,8 +1947,10 @@ def test_cli_application_version_document_describe_json_success(runner: CliRunne "version", "document", "describe", - "heta:1.0.0", + "heta", DOCUMENT_OUTPUT_DESCRIPTION_PDF, + "--application-version", + DOCUMENT_LATEST_VERSION_NUMBER, "--format", "json", ], @@ -1973,15 +1962,16 @@ def test_cli_application_version_document_describe_json_success(runner: CliRunne assert ( payload["mime_type"] == "application/pdf" ) # NOSONAR python:S1192: standard MIME type literal is clearer than a constant - assert payload["visibility"] == "public" @pytest.mark.unit def test_cli_application_version_document_describe_resolve_not_found_text(runner: CliRunner, record_property) -> None: """`describe` exits 2 when the application version cannot be resolved (text format).""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + fake_client = MagicMock() - fake_client.applications.versions.latest.return_value = None + fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( @@ -1998,8 +1988,10 @@ def test_cli_application_version_document_describe_resolve_not_found_text(runner def test_cli_application_version_document_describe_resolve_not_found_json(runner: CliRunner, record_property) -> None: """`describe --format json` emits structured error when version cannot be resolved.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-03") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + fake_client = MagicMock() - fake_client.applications.versions.latest.return_value = None + fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( @@ -2031,9 +2023,6 @@ def test_cli_application_version_document_describe_not_found_json(runner: CliRun fake_documents = MagicMock() fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -2064,9 +2053,6 @@ def test_cli_application_version_document_describe_failed_text(runner: CliRunner fake_documents = MagicMock() fake_documents.details.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -2088,9 +2074,6 @@ def test_cli_application_version_document_describe_failed_json(runner: CliRunner fake_documents = MagicMock() fake_documents.details.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -2120,8 +2103,10 @@ def test_cli_application_version_document_download_resolve_not_found( ) -> None: """`download` exits 2 when the application version cannot be resolved.""" record_property("tested-item-id", "TC-APPLICATION-CLI-05-04") + from aignx.codegen.exceptions import NotFoundException as ApiNotFound + fake_client = MagicMock() - fake_client.applications.versions.latest.return_value = None + fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): result = runner.invoke( @@ -2154,9 +2139,6 @@ def test_cli_application_version_document_download_not_found( fake_documents = MagicMock() fake_documents.download_to_path.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): @@ -2186,9 +2168,6 @@ def test_cli_application_version_document_download_failed(runner: CliRunner, tmp fake_documents = MagicMock() fake_documents.download_to_path.side_effect = RuntimeError(DOCUMENT_TEST_FAILURE_MESSAGE) fake_client = MagicMock() - latest_version = MagicMock() - latest_version.number = DOCUMENT_LATEST_VERSION_NUMBER - fake_client.applications.versions.latest.return_value = latest_version fake_client.applications.versions.documents.return_value = fake_documents with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index 25d2d19c..6256a8db 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -34,6 +34,7 @@ DOC_FILENAME_A = "a.pdf" REQUESTS_GET_PATCH_TARGET = "aignostics.platform.resources.applications.requests.get" + @pytest.fixture def mock_api() -> Mock: """Create a mock ExternalsApi object for testing. @@ -237,7 +238,6 @@ def test_documents_list_returns_wrapped_models(documents: Documents, mock_api: M assert len(result) == 2 assert all(isinstance(item, ApplicationVersionDocument) for item in result) assert {d.name for d in result} == {DOC_FILENAME_A, "b.pdf"} - assert result[0].visibility == "public" mock_api.list_version_documents.assert_called_once() call_kwargs = mock_api.list_version_documents.call_args.kwargs assert call_kwargs["application_id"] == "heta" @@ -390,3 +390,23 @@ def test_versions_documents_returns_documents_resource(mock_api: Mock) -> None: assert docs.application_id == "heta" assert docs.application_version == "1.0.0" assert docs._api is mock_api + + +@pytest.mark.unit +def test_versions_documents_resolves_none_to_latest(mock_api: Mock) -> None: + """Versions.documents(None) resolves to the latest version number.""" + from unittest.mock import patch + + from aignostics.platform.resources.applications import Versions as _Versions + from aignostics.platform.resources.applications import VersionTuple + + latest = Mock(spec=VersionTuple) + latest.number = "2.3.1" + + versions = _Versions(mock_api) + with patch.object(versions, "latest", return_value=latest): + docs = versions.documents("heta", None) + + assert isinstance(docs, Documents) + assert docs.application_id == "heta" + assert docs.application_version == "2.3.1" From 99ecbe85b9f3fd746d73eaa202c81b9cfd78f85a Mon Sep 17 00:00:00 2001 From: Arne Baumann Date: Mon, 4 May 2026 10:17:17 +0200 Subject: [PATCH 16/16] Remove option to set file name for downloading release documents --- specifications/SPEC_PLATFORM_SERVICE.md | 12 +++++++----- src/aignostics/application/_cli.py | 4 ++-- src/aignostics/platform/resources/applications.py | 13 ++++++++++--- .../application/TC-APPLICATION-CLI-05.feature | 5 ++--- .../platform/resources/applications_test.py | 10 ++++++++++ 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index bbe0631f..b71afafd 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -306,8 +306,9 @@ class Versions: def details(self, application_version: ApplicationVersion | str) -> ApplicationVersion: """Retrieves details for a specific application version.""" - def documents(self, application_id: str, application_version: ApplicationVersion | str) -> "Documents": - """Returns a Documents resource bound to the given application version.""" + def documents(self, application_id: str, application_version: VersionTuple | str | None) -> "Documents": + """Returns a Documents resource bound to the given application version. + Pass None to resolve the latest version automatically.""" ``` ```python @@ -340,7 +341,7 @@ class Documents: """ def download_to_path(self, document_name: str, destination: Path | str) -> Path: - """Downloads the document file to a local path. + """Downloads the document file to a local directory. Issues a single ``GET`` against the ``/file`` endpoint and follows the platform ``307`` redirect to a short-lived GCS signed URL, streaming the @@ -348,8 +349,8 @@ class Documents: by ``requests`` and is therefore not forwarded to the storage backend. Returns the absolute path to the written file. - If ``destination`` is a directory, the file is written as - ``{destination}/{document_name}``; the requested document name is the + ``destination`` must be an existing directory; the file is written as + ``{destination}/{document_name}``. The requested document name is the canonical filename and is used regardless of any ``Content-Disposition`` served by the storage backend. @@ -357,6 +358,7 @@ class Documents: integrity is bounded by HTTPS transport and the signed-URL lifetime. Raises: + ValueError: When ``destination`` is not an existing directory. NotFoundException: When the document does not exist, is not public, or is not uploaded. """ diff --git a/src/aignostics/application/_cli.py b/src/aignostics/application/_cli.py index 7bf481ba..9ee52785 100644 --- a/src/aignostics/application/_cli.py +++ b/src/aignostics/application/_cli.py @@ -1660,8 +1660,8 @@ def application_version_document_download( Path, typer.Option( "--output", - help="Destination file path or directory. Defaults to the current working directory.", - file_okay=True, + help="Destination directory. Defaults to the current working directory.", + file_okay=False, dir_okay=True, writable=True, resolve_path=True, diff --git a/src/aignostics/platform/resources/applications.py b/src/aignostics/platform/resources/applications.py index dcbbad0b..9805fce7 100644 --- a/src/aignostics/platform/resources/applications.py +++ b/src/aignostics/platform/resources/applications.py @@ -437,7 +437,7 @@ def download_to_path(self, document_name: str, destination: Path | str) -> Path: Args: document_name (str): The document filename. - destination (Path | str): Target file path or directory to write into. + destination (Path | str): Target directory to write into. Returns: Path: The absolute path to the written file. @@ -481,10 +481,17 @@ def _resolve_destination_path(destination: Path | str, document_name: str) -> Pa Returns: Path: The absolute, parent-created destination path. + + Raises: + ValueError: If the destination is an existing file or a non-existent path + with an existing parent that is a file. """ destination_path = Path(destination) - if destination_path.is_dir() or (not destination_path.exists() and not destination_path.suffix): - destination_path /= document_name + if destination_path.is_file() or (destination_path.exists() and not destination_path.is_dir()): + msg = f"Destination '{destination}' is an existing file. Please provide a directory or a non-existent path." + raise ValueError(msg) + + destination_path /= document_name destination_path = destination_path.resolve() destination_path.parent.mkdir(parents=True, exist_ok=True) return destination_path diff --git a/tests/aignostics/application/TC-APPLICATION-CLI-05.feature b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature index 22f69e3a..d6603a9b 100644 --- a/tests/aignostics/application/TC-APPLICATION-CLI-05.feature +++ b/tests/aignostics/application/TC-APPLICATION-CLI-05.feature @@ -13,7 +13,6 @@ Feature: Application Version Release Documents Given the user has access to an application version with release documents attached When the user requests the list of release documents for the application version Then the system shall return metadata for documents with public visibility and uploaded status - And the system shall exclude documents with internal visibility or pending status @tests:SPEC-APPLICATION-SERVICE @tests:SPEC-PLATFORM-SERVICE @@ -37,8 +36,8 @@ Feature: Application Version Release Documents @tests:SPEC-PLATFORM-SERVICE @tests:SWR-APPLICATION-1-3 @id:TC-APPLICATION-CLI-05-04 - Scenario: System downloads a release document file to a local path + Scenario: System downloads a release document file to a local directory Given the user has access to an application version with a public release document - When the user requests download of that document to a local destination + When the user requests download of that document to a local directory Then the system shall follow the platform redirect to the signed storage URL And the system shall write the document file using the requested document name diff --git a/tests/aignostics/platform/resources/applications_test.py b/tests/aignostics/platform/resources/applications_test.py index 6256a8db..6c7f3143 100644 --- a/tests/aignostics/platform/resources/applications_test.py +++ b/tests/aignostics/platform/resources/applications_test.py @@ -338,6 +338,16 @@ def test_documents_download_to_path_404_raises_not_found(documents: Documents, t documents.download_to_path(DOCUMENT_MISSING_PDF, tmp_path) +@pytest.mark.unit +def test_documents_download_to_path_rejects_non_directory(documents: Documents, tmp_path: Path) -> None: + """download_to_path() raises ValueError when destination is not an existing directory.""" + file_path = tmp_path / "some_file.pdf" + file_path.write_bytes(b"content") + + with pytest.raises(ValueError, match="must be a directory"): + documents.download_to_path(DOCUMENT_OUTPUT_DESCRIPTION_PDF, file_path) + + @pytest.mark.unit def test_documents_read_content_returns_bytes(documents: Documents) -> None: """read_content() follows the /content redirect and returns the body as bytes."""