diff --git a/README.md b/README.md index eab466b..b6bccca 100644 --- a/README.md +++ b/README.md @@ -67,29 +67,28 @@ step is to create a new core client and then create a new node. ```python core_client = flame_hub.CoreClient(base_url="http://localhost:3000/core/", auth=auth) -my_node = core_client.create_node(name="my-node", realm_id=master_realm) +my_node = core_client.create_node(name="my-node", realmId=master_realm) print(my_node.model_dump_json(indent=2)) ``` ```console { - "external_name": null, + "externalName": null, "hidden": false, "name": "my-node", - "realm_id": "794f2375-f043-4789-bd0c-e5534e8deeaa", - "registry_id": null, + "realmId": "794f2375-f043-4789-bd0c-e5534e8deeaa", + "registryId": null, "type": "default", "id": "03636152-e6a8-4e01-994e-18b2b0c3a935", - "public_key": null, + "publicKey": null, "online": false, "registry": null, - "registry_project_id": null, - "registry_project": null, - "robot_id": null, - "client_id": "2d3e19b4-6708-4279-b2a7-34ad42638e4b", - "created_at": "2025-05-19T15:43:57.859000Z", - "updated_at": "2025-05-19T15:43:57.859000Z" + "registryProjectId": null, + "registryProject": null, + "clientId": "2d3e19b4-6708-4279-b2a7-34ad42638e4b", + "createdAt": "2025-05-19T15:43:57.859000Z", + "updatedAt": "2025-05-19T15:43:57.859000Z" } ``` diff --git a/docs/clients_api.rst b/docs/clients_api.rst index 323ca0e..fd9f16b 100644 --- a/docs/clients_api.rst +++ b/docs/clients_api.rst @@ -14,7 +14,9 @@ Clients .. autoclass:: flame_hub.CoreClient :members: :undoc-members: + :private-members: _unwrap_single_resource .. autoclass:: flame_hub.StorageClient :members: :undoc-members: + :private-members: _unwrap_single_resource diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 6d2dfc6..b1ed2e9 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -190,8 +190,8 @@ left unset, the client will sort in ascending order by default. .. code-block:: python - nodes = core_client.find_nodes(sort={"by": "created_at"}) - sedon = core_client.find_nodes(sort={"by": "created_at", "order": "descending"}) + nodes = core_client.find_nodes(sort={"by": "createdAt"}) + sedon = core_client.find_nodes(sort={"by": "createdAt", "order": "descending"}) assert nodes == sedon[::-1] @@ -322,7 +322,7 @@ It is also possible to retrieve all names of includable properties for a specifi .. code-block:: - ('registry', 'registry_project') + ('registry', 'registryProject') Overriding authentication per request @@ -343,7 +343,7 @@ token as a string. # Or use a dedicated authenticator for this request only. core_client.create_node( name="my-node", - realm_id=master_realm, + realmId=master_realm, auth=flame_hub.auth.PasswordAuth( username="admin", password="start123", @@ -372,18 +372,18 @@ possible, including status code and additional information in the response body. core_client = flame_hub.CoreClient(base_url="http://localhost:3000/core/", auth=auth) try: - core_client.create_node(name="my-new-node", realm_id=str(uuid4())) + core_client.create_node(name="my-new-node", realmId=str(uuid4())) except flame_hub.HubAPIError as e: print(e) print(e.error_response.model_dump_json(indent=2)) .. code-block:: console - received status code 400 (undefined): Can't find realm entity by realm_id + received status code 400 (undefined): Can't find realm entity by realmId { "status_code": 400, "code": "undefined", - "message": "Can't find realm entity by realm_id" + "message": "Can't find realm entity by realmId" } In this example a :py:exc:`.HubAPIError` is raised because there is no realm with an ID that matches the dynamically @@ -406,14 +406,14 @@ Hub instance. from flame_hub.models import UpdateNode, UNSET - update_node = UpdateNode(hidden=False, external_name=None, type=UNSET) + update_node = UpdateNode(hidden=False, externalName=None, type=UNSET) print(update_node.model_dump_json(indent=2, exclude_defaults=True)) .. code-block:: console { "hidden": false, - "external_name": null + "externalName": null } Check out all implemented models :doc:`here `. diff --git a/docs/utility_functions_api.rst b/docs/utility_functions_api.rst index 5646b0c..10067a7 100644 --- a/docs/utility_functions_api.rst +++ b/docs/utility_functions_api.rst @@ -10,3 +10,5 @@ Utility functions .. autofunction:: flame_hub._base_client.obtain_uuid_from .. autofunction:: flame_hub._base_client.uuid_validator + +.. autofunction:: flame_hub._base_client.unwrap_enveloped_resource diff --git a/flame_hub/_auth_client.py b/flame_hub/_auth_client.py index 1102018..5f87375 100644 --- a/flame_hub/_auth_client.py +++ b/flame_hub/_auth_client.py @@ -19,6 +19,7 @@ ResourceListResult, AuthParam, BaseKwargs, + unwrap_enveloped_resource, ) from flame_hub._defaults import DEFAULT_AUTH_BASE_URL @@ -261,12 +262,9 @@ def _unwrap_single_resource(self, body: t.Any) -> t.Any: See Also -------- - :py:meth:`.BaseClient._unwrap_single_resource` + :py:meth:`.BaseClient._unwrap_single_resource`, :py:func:`.unwrap_enveloped_resource` """ - if not isinstance(body, dict) or "data" not in body: - raise ValueError("response body is not wrapped in a data property, authup 1.0.0-beta.57 or newer required") - - return body["data"] + return unwrap_enveloped_resource(body, "authup 1.0.0-beta.57") def get_realms(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Realm]: return self._get_all_resources(Realm, "realms", **params) diff --git a/flame_hub/_base_client.py b/flame_hub/_base_client.py index 401660f..788c240 100644 --- a/flame_hub/_base_client.py +++ b/flame_hub/_base_client.py @@ -413,6 +413,39 @@ def resolve_auth(auth: AuthParam) -> ClientAuth | PasswordAuth | StaticAuth | No return auth +def unwrap_enveloped_resource(body: t.Any, requirement: str) -> t.Any: + """Extract the resource object from a :python:`{"data": ..., "meta": ...}` record envelope. + + ``meta`` holds response-scoped extras such as the queryable schema of the endpoint and is discarded. + + Parameters + ---------- + body : :py:obj:`~typing.Any` + Deserialized response body of a request which targets a single resource. + requirement : :py:class:`str` + Name and version of the service which introduced the envelope. Named in the error message so that an + outdated deployment is recognizable from the exception alone. + + Returns + ------- + :py:obj:`~typing.Any` + The object which is validated with the resource model. + + Raises + ------ + :py:exc:`ValueError` + If ``body`` does not carry a ``data`` property, which is the case for services older than ``requirement``. + + See Also + -------- + :py:meth:`.BaseClient._unwrap_single_resource` + """ + if not isinstance(body, dict) or "data" not in body: + raise ValueError(f"response body is not wrapped in a data property, {requirement} or newer required") + + return body["data"] + + class BaseClient(object): """The base class for other client classes. @@ -447,9 +480,8 @@ def __init__( def _unwrap_single_resource(self, body: t.Any) -> t.Any: """Extract the resource object from the body of a single-resource response. - The FLAME Hub core and storage services respond to single-resource requests with the resource object itself, - which is why this implementation returns ``body`` unchanged. Clients whose service wraps the resource in an - envelope override this method. + This implementation returns ``body`` unchanged. Clients whose service wraps the resource in an envelope + override this method, usually by delegating to :py:func:`.unwrap_enveloped_resource`. Parameters ---------- @@ -464,7 +496,7 @@ def _unwrap_single_resource(self, body: t.Any) -> t.Any: See Also -------- :py:meth:`._get_single_resource`, :py:meth:`._create_resource`, :py:meth:`._update_resource`,\ - :py:meth:`.AuthClient._unwrap_single_resource` + :py:func:`.unwrap_enveloped_resource`, :py:meth:`.AuthClient._unwrap_single_resource` """ return body @@ -624,6 +656,7 @@ def _create_resource( resource: BaseModel, *path: str, expected_code: int = httpx.codes.CREATED.value, + envelope: bool = True, **params: te.Unpack[BaseKwargs], ) -> ResourceT: """Create a resource of a certain type at the specified path. @@ -645,6 +678,10 @@ def _create_resource( Path to the endpoint where the resource should be created. expected_code : :py:class:`int` The expected status code of the response from the ``POST`` request. This defaults to ``201``. + envelope : :py:class:`bool` + Whether the endpoint wraps its response in a record envelope. This defaults to :any:`True`. Pass + :any:`False` for the endpoints which deliberately respond with a bare object, such as the client and + registry credential routes. Returns ------- @@ -661,7 +698,7 @@ def _create_resource( r = self._request("POST", *path, expected_code=expected_code, json=resource.model_dump(mode="json"), **params) - return resource_type(**self._unwrap_single_resource(r.json())) + return resource_type(**(self._unwrap_single_resource(r.json()) if envelope else r.json())) def _get_single_resource( self, @@ -669,6 +706,7 @@ def _get_single_resource( *path: str | UuidIdentifiable, include: IncludeParams | None = None, expected_code: int = httpx.codes.OK.value, + envelope: bool = True, **params: te.Unpack[GetKwargs], ) -> ResourceT | None: """Get a single resource of a certain type at the specified path. @@ -693,6 +731,10 @@ def _get_single_resource( :doc:`model specifications ` which resources can be included in other resources. expected_code : :py:class:`int` The expected status code of the response from the ``GET`` request. This defaults to ``200``. + envelope : :py:class:`bool` + Whether the endpoint wraps its response in a record envelope. This defaults to :any:`True`. Pass + :any:`False` for the endpoints which deliberately respond with a bare object, such as the client and + registry credential routes. **params : :py:obj:`~typing.Unpack` [:py:class:`.GetKwargs`] Further keyword arguments for adding optional fields to a response and returning meta information. @@ -730,7 +772,7 @@ def _get_single_resource( else: raise - return resource_type(**self._unwrap_single_resource(r.json())) + return resource_type(**(self._unwrap_single_resource(r.json()) if envelope else r.json())) def _update_resource( self, @@ -738,6 +780,7 @@ def _update_resource( resource: BaseModel, *path: str | UuidIdentifiable, expected_code: int = httpx.codes.ACCEPTED.value, + envelope: bool = True, **params: te.Unpack[BaseKwargs], ) -> ResourceT: """Update a resource of a certain type at the specified path. @@ -761,6 +804,10 @@ def _update_resource( ``id`` attribute. expected_code : :py:class:`int` The expected status code of the response from the ``POST`` request. This defaults to ``202``. + envelope : :py:class:`bool` + Whether the endpoint wraps its response in a record envelope. This defaults to :any:`True`. Pass + :any:`False` for the endpoints which deliberately respond with a bare object, such as the client and + registry credential routes. Returns ------- @@ -784,7 +831,7 @@ def _update_resource( **params, ) - return resource_type(**self._unwrap_single_resource(r.json())) + return resource_type(**(self._unwrap_single_resource(r.json()) if envelope else r.json())) def _delete_resource( self, diff --git a/flame_hub/_core_client.py b/flame_hub/_core_client.py index 55164b1..fb67b5f 100644 --- a/flame_hub/_core_client.py +++ b/flame_hub/_core_client.py @@ -24,6 +24,7 @@ ResourceListResult, AuthParam, BaseKwargs, + unwrap_enveloped_resource, ) from flame_hub._defaults import DEFAULT_CORE_BASE_URL from flame_hub._storage_client import Bucket, BucketFile @@ -34,21 +35,21 @@ class CreateRegistry(BaseModel): name: str host: str - account_name: str | None - account_secret: t.Annotated[str | None, IsOptionalField] = None + accountName: str | None + accountSecret: t.Annotated[str | None, IsOptionalField] = None class Registry(CreateRegistry): id: uuid.UUID - created_at: datetime - updated_at: datetime + createdAt: datetime + updatedAt: datetime class UpdateRegistry(BaseModel): name: str | UNSET_T = UNSET host: str | UNSET_T = UNSET - account_name: str | None | UNSET_T = UNSET - account_secret: str | None | UNSET_T = UNSET + accountName: str | None | UNSET_T = UNSET + accountSecret: str | None | UNSET_T = UNSET RegistryProjectType = t.Literal["default", "aggregator", "incoming", "outgoing", "masterImages", "node"] @@ -57,94 +58,94 @@ class UpdateRegistry(BaseModel): class CreateRegistryProject(BaseModel): name: str type: RegistryProjectType - registry_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - external_name: str - account_name: str | None - account_secret: t.Annotated[str | None, IsOptionalField] = None + registryId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + externalName: str + accountName: str | None + accountSecret: t.Annotated[str | None, IsOptionalField] = None class RegistryProject(CreateRegistryProject): id: uuid.UUID public: bool - external_id: str | None - account_id: str | None - webhook_name: str | None - webhook_exists: bool | None - realm_id: uuid.UUID | None + externalId: str | None + accountId: str | None + webhookName: str | None + webhookExists: bool | None + realmId: uuid.UUID | None registry: t.Annotated[Registry, IsIncludable] = None - created_at: datetime - updated_at: datetime + createdAt: datetime + updatedAt: datetime class UpdateRegistryProject(BaseModel): name: str | UNSET_T = UNSET type: RegistryProjectType | UNSET_T = UNSET - registry_id: t.Annotated[uuid.UUID | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET - external_name: str | UNSET_T = UNSET - account_name: str | None | UNSET_T = UNSET - account_secret: str | None | UNSET_T = UNSET + registryId: t.Annotated[uuid.UUID | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET + externalName: str | UNSET_T = UNSET + accountName: str | None | UNSET_T = UNSET + accountSecret: str | None | UNSET_T = UNSET NodeType = t.Literal["aggregator", "default"] class CreateNode(BaseModel): - external_name: str | None + externalName: str | None hidden: bool | None name: str - realm_id: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] - registry_id: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] + realmId: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] + registryId: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] type: NodeType | None class Node(CreateNode): id: uuid.UUID - public_key: str | None + publicKey: str | None online: bool registry: t.Annotated[Registry | None, IsIncludable] = None - registry_project_id: uuid.UUID | None - registry_project: t.Annotated[RegistryProject | None, IsIncludable] = None - client_id: uuid.UUID | None - created_at: datetime - updated_at: datetime + registryProjectId: uuid.UUID | None + registryProject: t.Annotated[RegistryProject | None, IsIncludable] = None + clientId: uuid.UUID | None + createdAt: datetime + updatedAt: datetime class UpdateNode(BaseModel): hidden: bool | UNSET_T = UNSET - external_name: str | None | UNSET_T = UNSET + externalName: str | None | UNSET_T = UNSET type: NodeType | UNSET_T = UNSET - public_key: str | None | UNSET_T = UNSET - realm_id: t.Annotated[uuid.UUID | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET - registry_id: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET + publicKey: str | None | UNSET_T = UNSET + realmId: t.Annotated[uuid.UUID | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET + registryId: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET class NodeRegistryCredentials(BaseModel): host: str - external_name: str - account_name: str | None - account_secret: str | None + externalName: str + accountName: str | None + accountSecret: str | None class ClientCredentials(BaseModel): id: uuid.UUID secret: str | None name: str - display_name: str + displayName: str class UpdateClientCredentials(BaseModel): secret: str | None | UNSET_T = UNSET name: str | UNSET_T = UNSET - display_name: str | UNSET_T = UNSET + displayName: str | UNSET_T = UNSET class MasterImageGroup(BaseModel): id: uuid.UUID name: str path: str - virtual_path: str - created_at: datetime - updated_at: datetime + virtualPath: str + createdAt: datetime + updatedAt: datetime class MasterImageCommandArgument(te.TypedDict): @@ -173,65 +174,65 @@ def ensure_position_none(value: t.Any) -> t.Any: class MasterImage(BaseModel): id: uuid.UUID path: str | None - virtual_path: str - group_virtual_path: str - build_status: ProcessStatus | None - build_progress: int | None + virtualPath: str + groupVirtualPath: str + buildStatus: ProcessStatus | None + buildProgress: int | None name: str command: str | None - command_arguments: t.Annotated[list[MasterImageCommandArgument] | None, BeforeValidator(ensure_position_none)] - created_at: datetime - updated_at: datetime + commandArguments: t.Annotated[list[MasterImageCommandArgument] | None, BeforeValidator(ensure_position_none)] + createdAt: datetime + updatedAt: datetime class CreateProject(BaseModel): description: str | None - master_image_id: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] + masterImageId: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] name: str - display_name: str | None + displayName: str | None class Project(CreateProject): id: uuid.UUID analyses: int nodes: int - master_image: t.Annotated[MasterImage | None, IsIncludable] = None - created_at: datetime - updated_at: datetime - realm_id: uuid.UUID - user_id: uuid.UUID | None + masterImage: t.Annotated[MasterImage | None, IsIncludable] = None + createdAt: datetime + updatedAt: datetime + realmId: uuid.UUID + userId: uuid.UUID | None class UpdateProject(BaseModel): description: str | None | UNSET_T = UNSET - master_image_id: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET + masterImageId: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET name: str | UNSET_T = UNSET - display_name: str | None | UNSET_T = UNSET + displayName: str | None | UNSET_T = UNSET ProjectNodeApprovalStatus = t.Literal["rejected", "approved"] class CreateProjectNode(BaseModel): - node_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - project_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + nodeId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + projectId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] class ProjectNode(CreateProjectNode): id: uuid.UUID - approval_status: ProjectNodeApprovalStatus | None + approvalStatus: ProjectNodeApprovalStatus | None comment: str | None - created_at: datetime - updated_at: datetime + createdAt: datetime + updatedAt: datetime node: t.Annotated[Node, IsIncludable] = None project: t.Annotated[Project, IsIncludable] = None - project_realm_id: uuid.UUID - node_realm_id: uuid.UUID + projectRealmId: uuid.UUID + nodeRealmId: uuid.UUID class UpdateProjectNode(BaseModel): comment: str | None | UNSET_T = UNSET - approval_status: ProjectNodeApprovalStatus | None | UNSET_T = UNSET + approvalStatus: ProjectNodeApprovalStatus | None | UNSET_T = UNSET LogLevel = t.Literal["emerg", "alert", "crit", "error", "warn", "notice", "info", "debug"] @@ -250,11 +251,11 @@ class Log(BaseModel): class CreateAnalysis(BaseModel): description: str | None name: str | None - display_name: str | None - project_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - master_image_id: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] - registry_id: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] - image_command_arguments: t.Annotated[ + displayName: str | None + projectId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + masterImageId: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] + registryId: t.Annotated[uuid.UUID | None, Field(), WrapValidator(uuid_validator)] + imageCommandArguments: t.Annotated[ list[MasterImageCommandArgument], Field(default_factory=list), BeforeValidator(lambda args: [] if args is None else ensure_position_none(args)), @@ -264,40 +265,40 @@ class CreateAnalysis(BaseModel): class Analysis(CreateAnalysis): id: uuid.UUID nodes: int - nodes_approved: int - configuration_locked: bool - configuration_entrypoint_valid: bool - configuration_image_valid: bool - configuration_node_aggregator_valid: bool - configuration_node_default_valid: bool - configuration_nodes_valid: bool - build_status: ProcessStatus | None - build_nodes_valid: bool - build_progress: int | None - build_hash: str | None - build_os: str | None - build_size: int | None - distribution_status: ProcessStatus | None - distribution_progress: int | None - execution_status: ProcessStatus | None - execution_progress: int | None - created_at: datetime - updated_at: datetime + nodesApproved: int + configurationLocked: bool + configurationEntrypointValid: bool + configurationImageValid: bool + configurationNodeAggregatorValid: bool + configurationNodeDefaultValid: bool + configurationNodesValid: bool + buildStatus: ProcessStatus | None + buildNodesValid: bool + buildProgress: int | None + buildHash: str | None + buildOs: str | None + buildSize: int | None + distributionStatus: ProcessStatus | None + distributionProgress: int | None + executionStatus: ProcessStatus | None + executionProgress: int | None + createdAt: datetime + updatedAt: datetime registry: t.Annotated[Registry | None, IsIncludable] = None - realm_id: uuid.UUID - user_id: uuid.UUID - client_id: uuid.UUID | None - project_id: uuid.UUID + realmId: uuid.UUID + userId: uuid.UUID + clientId: uuid.UUID | None + projectId: uuid.UUID project: t.Annotated[Project, IsIncludable] = None - master_image: t.Annotated[MasterImage | None, IsIncludable] = None + masterImage: t.Annotated[MasterImage | None, IsIncludable] = None class UpdateAnalysis(BaseModel): description: str | None | UNSET_T = UNSET name: str | UNSET_T = UNSET - display_name: str | None | UNSET_T = UNSET - master_image_id: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET - image_command_arguments: ( + displayName: str | None | UNSET_T = UNSET + masterImageId: t.Annotated[uuid.UUID | None | UNSET_T, Field(), WrapValidator(uuid_validator)] = UNSET + imageCommandArguments: ( t.Annotated[ list[MasterImageCommandArgument], BeforeValidator(lambda args: ensure_position_none(args)), @@ -318,8 +319,8 @@ class UpdateAnalysis(BaseModel): class CreateAnalysisNode(BaseModel): - analysis_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - node_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + analysisId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + nodeId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] AnalysisNodeApprovalStatus = t.Literal["rejected", "approved"] @@ -327,30 +328,30 @@ class CreateAnalysisNode(BaseModel): class AnalysisNode(CreateAnalysisNode): id: uuid.UUID - approval_status: AnalysisNodeApprovalStatus | None - execution_status: ProcessStatus | None - execution_progress: int | None + approvalStatus: AnalysisNodeApprovalStatus | None + executionStatus: ProcessStatus | None + executionProgress: int | None comment: str | None - artifact_tag: str | None - artifact_digest: str | None - created_at: datetime - updated_at: datetime + artifactTag: str | None + artifactDigest: str | None + createdAt: datetime + updatedAt: datetime analysis: t.Annotated[Analysis, IsIncludable] = None node: t.Annotated[Node, IsIncludable] = None - analysis_realm_id: uuid.UUID - node_realm_id: uuid.UUID + analysisRealmId: uuid.UUID + nodeRealmId: uuid.UUID class UpdateAnalysisNode(BaseModel): comment: str | None | UNSET_T = UNSET - approval_status: AnalysisNodeApprovalStatus | None | UNSET_T = UNSET - execution_status: ProcessStatus | None | UNSET_T = UNSET - execution_progress: int | None | UNSET_T = UNSET + approvalStatus: AnalysisNodeApprovalStatus | None | UNSET_T = UNSET + executionStatus: ProcessStatus | None | UNSET_T = UNSET + executionProgress: int | None | UNSET_T = UNSET class CreateAnalysisNodeLog(BaseModel): - analysis_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - node_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + analysisId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + nodeId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] code: str | None status: str | None message: str @@ -365,35 +366,35 @@ class AnalysisBucketType(str, Enum): class CreateAnalysisBucket(BaseModel): type: AnalysisBucketType - bucket_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - analysis_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + bucketId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + analysisId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] class AnalysisBucket(CreateAnalysisBucket): id: uuid.UUID - created_at: datetime - updated_at: datetime + createdAt: datetime + updatedAt: datetime analysis: t.Annotated[Analysis, IsIncludable] = None - realm_id: uuid.UUID + realmId: uuid.UUID class CreateAnalysisBucketFile(BaseModel): path: str - bucket_file_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - bucket_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] - analysis_bucket_id: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + bucketFileId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + bucketId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] + analysisBucketId: t.Annotated[uuid.UUID, Field(), WrapValidator(uuid_validator)] root: bool class AnalysisBucketFile(CreateAnalysisBucketFile): id: uuid.UUID - created_at: datetime - updated_at: datetime - analysis_bucket: t.Annotated[AnalysisBucket, IsIncludable] = None - realm_id: uuid.UUID - user_id: uuid.UUID | None - client_id: uuid.UUID | None - analysis_id: uuid.UUID + createdAt: datetime + updatedAt: datetime + analysisBucket: t.Annotated[AnalysisBucket, IsIncludable] = None + realmId: uuid.UUID + userId: uuid.UUID | None + clientId: uuid.UUID | None + analysisId: uuid.UUID analysis: t.Annotated[Analysis, IsIncludable] = None @@ -420,6 +421,27 @@ def __init__( ): super().__init__(base_url, auth, **kwargs) + def _unwrap_single_resource(self, body: t.Any) -> t.Any: + """Extract the resource object from the core service's record envelope. + + Since ``0.13.0`` the FLAME Hub responds to record requests with :python:`{"data": ..., "meta": ...}` instead + of the resource object itself, mirroring the envelope that list responses have always used. ``meta`` holds + response-scoped extras such as the queryable schema of the endpoint and is discarded. + + The credential endpoints keep responding with a bare object and pass :python:`envelope=False` instead of + going through this method. + + Raises + ------ + :py:exc:`ValueError` + If ``body`` does not carry a ``data`` property, which is the case for FLAME Hub versions before ``0.13.0``. + + See Also + -------- + :py:meth:`.BaseClient._unwrap_single_resource`, :py:func:`.unwrap_enveloped_resource` + """ + return unwrap_enveloped_resource(body, "FLAME Hub 0.13.0") + def get_nodes(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Node]: return self._get_all_resources(Node, "nodes", include=get_includable_names(Node), **params) @@ -429,9 +451,9 @@ def find_nodes(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListResult[N def create_node( self, name: str, - realm_id: Realm | str | uuid.UUID | None = None, - registry_id: Registry | uuid.UUID | str | None = None, - external_name: str | None = None, + realmId: Realm | str | uuid.UUID | None = None, + registryId: Registry | uuid.UUID | str | None = None, + externalName: str | None = None, node_type: NodeType = "default", hidden: bool = False, **params: te.Unpack[BaseKwargs], @@ -440,55 +462,55 @@ def create_node( Node, CreateNode( name=name, - realm_id=realm_id, - external_name=external_name, + realmId=realmId, + externalName=externalName, hidden=hidden, - registry_id=registry_id, + registryId=registryId, type=node_type, ), "nodes", **params, ) - def get_node(self, node_id: Node | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Node | None: - return self._get_single_resource(Node, "nodes", node_id, include=get_includable_names(Node), **params) + def get_node(self, nodeId: Node | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Node | None: + return self._get_single_resource(Node, "nodes", nodeId, include=get_includable_names(Node), **params) def delete_node( self, - node_id: Node | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): - self._delete_resource("nodes", node_id, **params) + self._delete_resource("nodes", nodeId, **params) def update_node( self, - node_id: Node | uuid.UUID | str, - external_name: str | None | UNSET_T = UNSET, + nodeId: Node | uuid.UUID | str, + externalName: str | None | UNSET_T = UNSET, hidden: bool | UNSET_T = UNSET, node_type: NodeType | UNSET_T = UNSET, - realm_id: Realm | str | uuid.UUID | UNSET_T = UNSET, - registry_id: Registry | str | uuid.UUID | None | UNSET_T = UNSET, - public_key: str | None | UNSET_T = UNSET, + realmId: Realm | str | uuid.UUID | UNSET_T = UNSET, + registryId: Registry | str | uuid.UUID | None | UNSET_T = UNSET, + publicKey: str | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> Node: return self._update_resource( Node, UpdateNode( - external_name=external_name, + externalName=externalName, hidden=hidden, type=node_type, - public_key=public_key, - realm_id=realm_id, - registry_id=registry_id, + publicKey=publicKey, + realmId=realmId, + registryId=registryId, ), "nodes", - node_id, + nodeId, **params, ) def get_node_registry_credentials( self, - node_id: Node | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, **params: te.Unpack[GetKwargs], ) -> NodeRegistryCredentials | None: """Returns the node's registry project credentials.""" @@ -496,15 +518,16 @@ def get_node_registry_credentials( return self._get_single_resource( NodeRegistryCredentials, "nodes", - node_id, + nodeId, "registry", "credentials", + envelope=False, **params, ) def get_node_client_credentials( self, - node_id: Node | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, **params: te.Unpack[GetKwargs], ) -> ClientCredentials | None: """Returns the node's client credentials.""" @@ -512,18 +535,19 @@ def get_node_client_credentials( return self._get_single_resource( ClientCredentials, "nodes", - node_id, + nodeId, "client", "credentials", + envelope=False, **params, ) def update_node_client_credentials( self, - node_id: Node | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, secret: str | None | UNSET_T = UNSET, name: str | UNSET_T = UNSET, - display_name: str | UNSET_T = UNSET, + displayName: str | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> ClientCredentials: """Update the node's client credentials. If ``secret`` is set to :any:`None`, then the Hub will create and set @@ -534,13 +558,14 @@ def update_node_client_credentials( UpdateClientCredentials( secret=secret, name=name, - display_name=display_name, + displayName=displayName, ), "nodes", - node_id, + nodeId, "client", "credentials", expected_code=httpx.codes.OK.value, + envelope=False, **params, ) @@ -548,9 +573,9 @@ def get_master_image_groups(self, **params: te.Unpack[GetKwargs]) -> ResourceLis return self._get_all_resources(MasterImageGroup, "master-image-groups", **params) def get_master_image_group( - self, master_image_group_id: MasterImageGroup | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, masterImageGroupId: MasterImageGroup | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> MasterImageGroup | None: - return self._get_single_resource(MasterImageGroup, "master-image-groups", master_image_group_id, **params) + return self._get_single_resource(MasterImageGroup, "master-image-groups", masterImageGroupId, **params) def find_master_image_groups(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListResult[MasterImageGroup]: return self._find_all_resources(MasterImageGroup, "master-image-groups", **params) @@ -559,9 +584,9 @@ def get_master_images(self, **params: te.Unpack[GetKwargs]) -> ResourceListResul return self._get_all_resources(MasterImage, "master-images", **params) def get_master_image( - self, master_image_id: MasterImage | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, masterImageId: MasterImage | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> MasterImage | None: - return self._get_single_resource(MasterImage, "master-images", master_image_id, **params) + return self._get_single_resource(MasterImage, "master-images", masterImageId, **params) def find_master_images(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListResult[MasterImage]: return self._find_all_resources(MasterImage, "master-images", **params) @@ -586,7 +611,7 @@ def sync_master_images(self, **params: te.Unpack[BaseKwargs]): **params, ) - def build_master_image(self, master_image_id: MasterImage | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + def build_master_image(self, masterImageId: MasterImage | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): """This method will command the Hub to start building a master image. Note that building a master image could take some time. """ @@ -596,15 +621,15 @@ def build_master_image(self, master_image_id: MasterImage | uuid.UUID | str, **p "master-images", "command", expected_code=httpx.codes.ACCEPTED.value, - json={"command": "build", "id": str(obtain_uuid_from(master_image_id))}, + json={"command": "build", "id": str(obtain_uuid_from(masterImageId))}, **params, ) def create_project( self, name: str, - display_name: str | None = None, - master_image_id: MasterImage | uuid.UUID | str | None = None, + displayName: str | None = None, + masterImageId: MasterImage | uuid.UUID | str | None = None, description: str | None = None, **params: te.Unpack[BaseKwargs], ) -> Project: @@ -612,53 +637,51 @@ def create_project( Project, CreateProject( name=name, - master_image_id=master_image_id, + masterImageId=masterImageId, description=description, - display_name=display_name, + displayName=displayName, ), "projects", **params, ) - def delete_project(self, project_id: Project | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): - self._delete_resource("projects", project_id, **params) + def delete_project(self, projectId: Project | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + self._delete_resource("projects", projectId, **params) - def get_project(self, project_id: Project | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Project | None: + def get_project(self, projectId: Project | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Project | None: return self._get_single_resource( - Project, "projects", project_id, include=get_includable_names(Project), **params + Project, "projects", projectId, include=get_includable_names(Project), **params ) def update_project( self, - project_id: Project | uuid.UUID | str, + projectId: Project | uuid.UUID | str, description: str | None | UNSET_T = UNSET, - master_image_id: MasterImage | str | uuid.UUID | None | UNSET_T = UNSET, + masterImageId: MasterImage | str | uuid.UUID | None | UNSET_T = UNSET, name: str | UNSET_T = UNSET, - display_name: str | None | UNSET_T = UNSET, + displayName: str | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> Project: return self._update_resource( Project, - UpdateProject( - description=description, master_image_id=master_image_id, name=name, display_name=display_name - ), + UpdateProject(description=description, masterImageId=masterImageId, name=name, displayName=displayName), "projects", - project_id, + projectId, **params, ) def create_project_node( - self, project_id: Project | uuid.UUID | str, node_id: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs] + self, projectId: Project | uuid.UUID | str, nodeId: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs] ) -> ProjectNode: return self._create_resource( ProjectNode, - CreateProjectNode(project_id=project_id, node_id=node_id), + CreateProjectNode(projectId=projectId, nodeId=nodeId), "project-nodes", **params, ) - def delete_project_node(self, project_node_id: ProjectNode | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): - self._delete_resource("project-nodes", project_node_id, **params) + def delete_project_node(self, projectNodeId: ProjectNode | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + self._delete_resource("project-nodes", projectNodeId, **params) def get_project_nodes(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[ProjectNode]: return self._get_all_resources( @@ -671,55 +694,55 @@ def find_project_nodes(self, **params: te.Unpack[FindAllKwargs]) -> ResourceList ) def get_project_node( - self, project_node_id: ProjectNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, projectNodeId: ProjectNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> ProjectNode | None: return self._get_single_resource( - ProjectNode, "project-nodes", project_node_id, include=get_includable_names(ProjectNode), **params + ProjectNode, "project-nodes", projectNodeId, include=get_includable_names(ProjectNode), **params ) def update_project_node( self, - project_node_id: ProjectNode | uuid.UUID | str, + projectNodeId: ProjectNode | uuid.UUID | str, comment: str | None | UNSET_T = UNSET, - approval_status: ProjectNodeApprovalStatus | None | UNSET_T = UNSET, + approvalStatus: ProjectNodeApprovalStatus | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ): return self._update_resource( ProjectNode, - UpdateProjectNode(comment=comment, approval_status=approval_status), + UpdateProjectNode(comment=comment, approvalStatus=approvalStatus), "project-nodes", - project_node_id, + projectNodeId, **params, ) def create_analysis( self, - project_id: Project | uuid.UUID | str, + projectId: Project | uuid.UUID | str, name: str | None = None, - display_name: str | None = None, + displayName: str | None = None, description: str | None = None, - master_image_id: MasterImage | uuid.UUID | str | None = None, - registry_id: Registry | uuid.UUID | str | None = None, - image_command_arguments: list[MasterImageCommandArgument] | None = None, + masterImageId: MasterImage | uuid.UUID | str | None = None, + registryId: Registry | uuid.UUID | str | None = None, + imageCommandArguments: list[MasterImageCommandArgument] | None = None, **params: te.Unpack[BaseKwargs], ) -> Analysis: return self._create_resource( Analysis, CreateAnalysis( - project_id=project_id, + projectId=projectId, name=name, - display_name=display_name, + displayName=displayName, description=description, - master_image_id=master_image_id, - registry_id=registry_id, - image_command_arguments=image_command_arguments, + masterImageId=masterImageId, + registryId=registryId, + imageCommandArguments=imageCommandArguments, ), "analyses", **params, ) - def delete_analysis(self, analysis_id: Analysis | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): - self._delete_resource("analyses", analysis_id, **params) + def delete_analysis(self, analysisId: Analysis | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + self._delete_resource("analyses", analysisId, **params) def get_analyses(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Analysis]: return self._get_all_resources(Analysis, "analyses", include=get_includable_names(Analysis), **params) @@ -727,56 +750,56 @@ def get_analyses(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Ana def find_analyses(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListResult[Analysis]: return self._find_all_resources(Analysis, "analyses", include=get_includable_names(Analysis), **params) - def get_analysis(self, analysis_id: Analysis | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Analysis | None: + def get_analysis(self, analysisId: Analysis | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Analysis | None: return self._get_single_resource( - Analysis, "analyses", analysis_id, include=get_includable_names(Analysis), **params + Analysis, "analyses", analysisId, include=get_includable_names(Analysis), **params ) def update_analysis( self, - analysis_id: Analysis | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, name: str | None | UNSET_T = UNSET, - display_name: str | None | UNSET_T = UNSET, + displayName: str | None | UNSET_T = UNSET, description: str | None | UNSET_T = UNSET, - master_image_id: MasterImage | uuid.UUID | str | None | UNSET_T = UNSET, - image_command_arguments: list[MasterImageCommandArgument] | UNSET_T = UNSET, + masterImageId: MasterImage | uuid.UUID | str | None | UNSET_T = UNSET, + imageCommandArguments: list[MasterImageCommandArgument] | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> Analysis: return self._update_resource( Analysis, UpdateAnalysis( name=name, - display_name=display_name, + displayName=displayName, description=description, - master_image_id=master_image_id, - image_command_arguments=image_command_arguments, + masterImageId=masterImageId, + imageCommandArguments=imageCommandArguments, ), "analyses", - analysis_id, + analysisId, **params, ) def send_analysis_command( self, - analysis_id: Analysis | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, command: AnalysisCommand, **params: te.Unpack[BaseKwargs], ) -> Analysis: r = self._request( "POST", "analyses", - obtain_uuid_from(analysis_id), + obtain_uuid_from(analysisId), "command", expected_code=httpx.codes.ACCEPTED.value, json={"command": command}, **params, ) - return Analysis(**r.json()) + return Analysis(**self._unwrap_single_resource(r.json())) def get_analysis_client_credentials( self, - analysis_id: Analysis | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, **params: te.Unpack[GetKwargs], ) -> ClientCredentials | None: """Returns the client credentials of the analysis.""" @@ -784,18 +807,19 @@ def get_analysis_client_credentials( return self._get_single_resource( ClientCredentials, "analyses", - analysis_id, + analysisId, "client", "credentials", + envelope=False, **params, ) def update_analysis_client_credentials( self, - analysis_id: Analysis | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, secret: str | None | UNSET_T = UNSET, name: str | UNSET_T = UNSET, - display_name: str | UNSET_T = UNSET, + displayName: str | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> ClientCredentials: """Update the client credentials of the analysis. If ``secret`` is set to :any:`None`, then the Hub will create @@ -806,56 +830,57 @@ def update_analysis_client_credentials( UpdateClientCredentials( secret=secret, name=name, - display_name=display_name, + displayName=displayName, ), "analyses", - analysis_id, + analysisId, "client", "credentials", expected_code=httpx.codes.OK.value, + envelope=False, **params, ) def create_analysis_node( - self, analysis_id: Analysis | uuid.UUID | str, node_id: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs] + self, analysisId: Analysis | uuid.UUID | str, nodeId: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs] ) -> AnalysisNode: return self._create_resource( AnalysisNode, - CreateAnalysisNode(analysis_id=analysis_id, node_id=node_id), + CreateAnalysisNode(analysisId=analysisId, nodeId=nodeId), "analysis-nodes", **params, ) - def delete_analysis_node(self, analysis_node_id: AnalysisNode | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): - self._delete_resource("analysis-nodes", analysis_node_id, **params) + def delete_analysis_node(self, analysisNodeId: AnalysisNode | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + self._delete_resource("analysis-nodes", analysisNodeId, **params) def update_analysis_node( self, - analysis_node_id: AnalysisNode | uuid.UUID | str, + analysisNodeId: AnalysisNode | uuid.UUID | str, comment: str | None | UNSET_T = UNSET, - approval_status: AnalysisNodeApprovalStatus | None | UNSET_T = UNSET, - execution_status: ProcessStatus | None | UNSET_T = UNSET, - execution_progress: int | None | UNSET_T = UNSET, + approvalStatus: AnalysisNodeApprovalStatus | None | UNSET_T = UNSET, + executionStatus: ProcessStatus | None | UNSET_T = UNSET, + executionProgress: int | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> AnalysisNode: return self._update_resource( AnalysisNode, UpdateAnalysisNode( comment=comment, - approval_status=approval_status, - execution_status=execution_status, - execution_progress=execution_progress, + approvalStatus=approvalStatus, + executionStatus=executionStatus, + executionProgress=executionProgress, ), "analysis-nodes", - analysis_node_id, + analysisNodeId, **params, ) def get_analysis_node( - self, analysis_node_id: AnalysisNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, analysisNodeId: AnalysisNode | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> AnalysisNode | None: return self._get_single_resource( - AnalysisNode, "analysis-nodes", analysis_node_id, include=get_includable_names(AnalysisNode), **params + AnalysisNode, "analysis-nodes", analysisNodeId, include=get_includable_names(AnalysisNode), **params ) def get_analysis_nodes(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[AnalysisNode]: @@ -870,8 +895,8 @@ def find_analysis_nodes(self, **params: te.Unpack[FindAllKwargs]) -> ResourceLis def create_analysis_node_log( self, - analysis_id: Analysis | uuid.UUID | str, - node_id: Node | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, level: LogLevel, message: str, status: str | None = None, @@ -881,8 +906,8 @@ def create_analysis_node_log( return self._create_resource( Log, CreateAnalysisNodeLog( - analysis_id=analysis_id, - node_id=node_id, + analysisId=analysisId, + nodeId=nodeId, level=level, message=message, status=status, @@ -895,8 +920,8 @@ def create_analysis_node_log( def delete_analysis_node_logs( self, - analysis_id: Analysis | uuid.UUID | str, - node_id: Node | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, + nodeId: Node | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): self._request( @@ -905,8 +930,8 @@ def delete_analysis_node_logs( expected_code=httpx.codes.ACCEPTED.value, params=build_filter_params( { - "analysis_id": str(obtain_uuid_from(analysis_id)), - "node_id": str(obtain_uuid_from(node_id)), + "analysisId": str(obtain_uuid_from(analysisId)), + "nodeId": str(obtain_uuid_from(nodeId)), } ), **params, @@ -918,16 +943,16 @@ def find_analysis_node_logs(self, **params: te.Unpack[FindAllKwargs]) -> Resourc def create_analysis_bucket( self, bucket_type: AnalysisBucketType, - bucket_id: Bucket | uuid.UUID | str, - analysis_id: Analysis | uuid.UUID | str, + bucketId: Bucket | uuid.UUID | str, + analysisId: Analysis | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ) -> AnalysisBucket: return self._create_resource( AnalysisBucket, CreateAnalysisBucket( type=bucket_type, - bucket_id=bucket_id, - analysis_id=analysis_id, + bucketId=bucketId, + analysisId=analysisId, ), "analysis-buckets", **params, @@ -935,10 +960,10 @@ def create_analysis_bucket( def delete_analysis_bucket( self, - analysis_bucket_id: AnalysisBucket | uuid.UUID | str, + analysisBucketId: AnalysisBucket | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): - self._delete_resource("analysis-buckets", analysis_bucket_id, **params) + self._delete_resource("analysis-buckets", analysisBucketId, **params) def get_analysis_buckets(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[AnalysisBucket]: return self._get_all_resources( @@ -951,12 +976,12 @@ def find_analysis_buckets(self, **params: te.Unpack[FindAllKwargs]) -> ResourceL ) def get_analysis_bucket( - self, analysis_bucket_id: AnalysisBucket | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, analysisBucketId: AnalysisBucket | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> AnalysisBucket | None: return self._get_single_resource( AnalysisBucket, "analysis-buckets", - analysis_bucket_id, + analysisBucketId, include=get_includable_names(AnalysisBucket), **params, ) @@ -972,38 +997,38 @@ def find_analysis_bucket_files(self, **params: te.Unpack[FindAllKwargs]) -> Reso ) def get_analysis_bucket_file( - self, analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, analysisBucketFileId: AnalysisBucketFile | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> AnalysisBucketFile | None: return self._get_single_resource( AnalysisBucketFile, "analysis-bucket-files", - analysis_bucket_file_id, + analysisBucketFileId, include=get_includable_names(AnalysisBucketFile), **params, ) def delete_analysis_bucket_file( self, - analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, + analysisBucketFileId: AnalysisBucketFile | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): - self._delete_resource("analysis-bucket-files", analysis_bucket_file_id, **params) + self._delete_resource("analysis-bucket-files", analysisBucketFileId, **params) def create_analysis_bucket_file( self, path: str, - bucket_file_id: BucketFile | uuid.UUID | str, - bucket_id: Bucket | uuid.UUID | str, - analysis_bucket_id: AnalysisBucket | uuid.UUID | str, + bucketFileId: BucketFile | uuid.UUID | str, + bucketId: Bucket | uuid.UUID | str, + analysisBucketId: AnalysisBucket | uuid.UUID | str, is_entrypoint: bool = False, **params: te.Unpack[BaseKwargs], ) -> AnalysisBucketFile: return self._create_resource( AnalysisBucketFile, CreateAnalysisBucketFile( - bucket_file_id=bucket_file_id, - bucket_id=bucket_id, - analysis_bucket_id=analysis_bucket_id, + bucketFileId=bucketFileId, + bucketId=bucketId, + analysisBucketId=analysisBucketId, path=path, root=is_entrypoint, ), @@ -1013,7 +1038,7 @@ def create_analysis_bucket_file( def update_analysis_bucket_file( self, - analysis_bucket_file_id: AnalysisBucketFile | uuid.UUID | str, + analysisBucketFileId: AnalysisBucketFile | uuid.UUID | str, is_entrypoint: bool | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> AnalysisBucketFile: @@ -1021,7 +1046,7 @@ def update_analysis_bucket_file( AnalysisBucketFile, UpdateAnalysisBucketFile(root=is_entrypoint), "analysis-bucket-files", - analysis_bucket_file_id, + analysisBucketFileId, **params, ) @@ -1029,41 +1054,41 @@ def create_registry( self, name: str, host: str, - account_name: str | None = None, - account_secret: str | None = None, + accountName: str | None = None, + accountSecret: str | None = None, **params: te.Unpack[BaseKwargs], ) -> Registry: return self._create_resource( Registry, - CreateRegistry(name=name, host=host, account_name=account_name, account_secret=account_secret), + CreateRegistry(name=name, host=host, accountName=accountName, accountSecret=accountSecret), "registries", **params, ) - def get_registry(self, registry_id: Registry | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Registry | None: - return self._get_single_resource(Registry, "registries", registry_id, **params) + def get_registry(self, registryId: Registry | uuid.UUID | str, **params: te.Unpack[GetKwargs]) -> Registry | None: + return self._get_single_resource(Registry, "registries", registryId, **params) def delete_registry( self, - registry_id: Registry | uuid.UUID | str, + registryId: Registry | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): - self._delete_resource("registries", registry_id, **params) + self._delete_resource("registries", registryId, **params) def update_registry( self, - registry_id: Registry | uuid.UUID | str, + registryId: Registry | uuid.UUID | str, name: str | UNSET_T = UNSET, host: str | UNSET_T = UNSET, - account_name: str | None | UNSET_T = UNSET, - account_secret: str | None | UNSET_T = UNSET, + accountName: str | None | UNSET_T = UNSET, + accountSecret: str | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> Registry: return self._update_resource( Registry, - UpdateRegistry(name=name, host=host, account_name=account_name, account_secret=account_secret), + UpdateRegistry(name=name, host=host, accountName=accountName, accountSecret=accountSecret), "registries", - registry_id, + registryId, **params, ) @@ -1075,7 +1100,7 @@ def find_registries(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListRes def send_registry_command( self, - registry_id: Registry | uuid.UUID | str, + registryId: Registry | uuid.UUID | str, command: RegistryCommand, **params: te.Unpack[BaseKwargs], ): @@ -1085,7 +1110,7 @@ def send_registry_command( "registry", "command", expected_code=httpx.codes.ACCEPTED.value, - json={"command": command, "id": str(obtain_uuid_from(registry_id))}, + json={"command": command, "id": str(obtain_uuid_from(registryId))}, **params, ) @@ -1093,10 +1118,10 @@ def create_registry_project( self, name: str, registry_project_type: RegistryProjectType, - registry_id: Registry | uuid.UUID | str, - external_name: str, - account_name: str | None = None, - account_secret: str | None = None, + registryId: Registry | uuid.UUID | str, + externalName: str, + accountName: str | None = None, + accountSecret: str | None = None, **params: te.Unpack[BaseKwargs], ) -> RegistryProject: return self._create_resource( @@ -1104,42 +1129,42 @@ def create_registry_project( CreateRegistryProject( name=name, type=registry_project_type, - registry_id=registry_id, - external_name=external_name, - account_name=account_name, - account_secret=account_secret, + registryId=registryId, + externalName=externalName, + accountName=accountName, + accountSecret=accountSecret, ), "registry-projects", **params, ) def get_registry_project( - self, registry_project_id: RegistryProject | uuid.UUID | str, **params: te.Unpack[GetKwargs] + self, registryProjectId: RegistryProject | uuid.UUID | str, **params: te.Unpack[GetKwargs] ) -> RegistryProject | None: return self._get_single_resource( RegistryProject, "registry-projects", - registry_project_id, + registryProjectId, include=get_includable_names(RegistryProject), **params, ) def delete_registry_project( self, - registry_project_id: RegistryProject | uuid.UUID | str, + registryProjectId: RegistryProject | uuid.UUID | str, **params: te.Unpack[BaseKwargs], ): - self._delete_resource("registry-projects", registry_project_id, **params) + self._delete_resource("registry-projects", registryProjectId, **params) def update_registry_project( self, - registry_project_id: RegistryProject | uuid.UUID | str, + registryProjectId: RegistryProject | uuid.UUID | str, name: str | UNSET_T = UNSET, registry_project_type: RegistryProjectType | UNSET_T = UNSET, - registry_id: Registry | uuid.UUID | str | UNSET_T = UNSET, - external_name: str | UNSET_T = UNSET, - account_name: str | None | UNSET_T = UNSET, - account_secret: str | None | UNSET_T = UNSET, + registryId: Registry | uuid.UUID | str | UNSET_T = UNSET, + externalName: str | UNSET_T = UNSET, + accountName: str | None | UNSET_T = UNSET, + accountSecret: str | None | UNSET_T = UNSET, **params: te.Unpack[BaseKwargs], ) -> RegistryProject: return self._update_resource( @@ -1147,13 +1172,13 @@ def update_registry_project( UpdateRegistryProject( name=name, type=registry_project_type, - registry_id=registry_id, - external_name=external_name, - account_name=account_name, - account_secret=account_secret, + registryId=registryId, + externalName=externalName, + accountName=accountName, + accountSecret=accountSecret, ), "registry-projects", - registry_project_id, + registryProjectId, **params, ) @@ -1173,12 +1198,12 @@ def find_registry_projects(self, **params: te.Unpack[FindAllKwargs]) -> Resource **params, ) - def delete_analysis_logs(self, analysis_id: Analysis | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): + def delete_analysis_logs(self, analysisId: Analysis | uuid.UUID | str, **params: te.Unpack[BaseKwargs]): self._request( "DELETE", "analysis-logs", expected_code=httpx.codes.ACCEPTED.value, - params=build_filter_params({"analysis_id": str(obtain_uuid_from(analysis_id))}), + params=build_filter_params({"analysisId": str(obtain_uuid_from(analysisId))}), **params, ) diff --git a/flame_hub/_storage_client.py b/flame_hub/_storage_client.py index 4cc18f2..c5a7a15 100644 --- a/flame_hub/_storage_client.py +++ b/flame_hub/_storage_client.py @@ -18,6 +18,7 @@ ResourceListResult, AuthParam, BaseKwargs, + unwrap_enveloped_resource, ) from flame_hub._defaults import DEFAULT_STORAGE_BASE_URL @@ -29,11 +30,11 @@ class CreateBucket(BaseModel): class Bucket(CreateBucket): id: uuid.UUID - created_at: datetime - updated_at: datetime - actor_id: uuid.UUID | None - actor_type: str | None - realm_id: uuid.UUID | None + createdAt: datetime + updatedAt: datetime + actorId: uuid.UUID | None + actorType: str | None + realmId: uuid.UUID | None class BucketFile(BaseModel): @@ -43,12 +44,12 @@ class BucketFile(BaseModel): hash: str directory: str size: int | None - created_at: datetime - updated_at: datetime - actor_type: str - actor_id: uuid.UUID - realm_id: uuid.UUID - bucket_id: uuid.UUID + createdAt: datetime + updatedAt: datetime + actorType: str + actorId: uuid.UUID + realmId: uuid.UUID + bucketId: uuid.UUID bucket: t.Annotated[Bucket, IsIncludable] = None @@ -88,11 +89,32 @@ def __init__( ): super().__init__(base_url, auth, **kwargs) + def _unwrap_single_resource(self, body: t.Any) -> t.Any: + """Extract the resource object from the storage service's record envelope. + + Since ``0.13.0`` the FLAME Hub responds to record requests with :python:`{"data": ..., "meta": ...}` instead + of the resource object itself, mirroring the envelope that list responses have always used. ``meta`` holds + response-scoped extras such as the queryable schema of the endpoint and is discarded. + + The bucket upload endpoint responds with a collection rather than a record and is parsed as a + :py:class:`.ResourceList` instead of going through this method. + + Raises + ------ + :py:exc:`ValueError` + If ``body`` does not carry a ``data`` property, which is the case for FLAME Hub versions before ``0.13.0``. + + See Also + -------- + :py:meth:`.BaseClient._unwrap_single_resource`, :py:func:`.unwrap_enveloped_resource` + """ + return unwrap_enveloped_resource(body, "FLAME Hub 0.13.0") + def create_bucket(self, name: str, region: str | None = None, **params: te.Unpack[BaseKwargs]) -> Bucket: return self._create_resource(Bucket, CreateBucket(name=name, region=region), "buckets", **params) - def delete_bucket(self, bucket_id: Bucket | str | uuid.UUID, **params: te.Unpack[BaseKwargs]): - self._delete_resource("buckets", bucket_id, **params) + def delete_bucket(self, bucketId: Bucket | str | uuid.UUID, **params: te.Unpack[BaseKwargs]): + self._delete_resource("buckets", bucketId, **params) def get_buckets(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Bucket]: return self._get_all_resources(Bucket, "buckets", **params) @@ -100,19 +122,19 @@ def get_buckets(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[Buck def find_buckets(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListResult[Bucket]: return self._find_all_resources(Bucket, "buckets", **params) - def get_bucket(self, bucket_id: Bucket | str | uuid.UUID, **params: te.Unpack[GetKwargs]) -> Bucket | None: - return self._get_single_resource(Bucket, "buckets", bucket_id, **params) + def get_bucket(self, bucketId: Bucket | str | uuid.UUID, **params: te.Unpack[GetKwargs]) -> Bucket | None: + return self._get_single_resource(Bucket, "buckets", bucketId, **params) def stream_bucket_tarball( self, - bucket_id: Bucket | str | uuid.UUID, + bucketId: Bucket | str | uuid.UUID, chunk_size: int = 1024, **params: te.Unpack[BaseKwargs], ) -> t.Iterator[bytes]: r = self._request( "GET", "buckets", - str(obtain_uuid_from(bucket_id)), + str(obtain_uuid_from(bucketId)), "stream", expected_code=httpx.codes.OK.value, stream=True, @@ -127,7 +149,7 @@ def stream_bucket_tarball( def upload_to_bucket( self, - bucket_id: Bucket | str | uuid.UUID, + bucketId: Bucket | str | uuid.UUID, *upload_file: UploadFile, **params: te.Unpack[BaseKwargs], ) -> list[BucketFile]: @@ -139,7 +161,7 @@ def upload_to_bucket( r = self._request( "POST", "buckets", - str(obtain_uuid_from(bucket_id)), + str(obtain_uuid_from(bucketId)), "upload", expected_code=httpx.codes.CREATED.value, files=upload_file_dict, @@ -148,14 +170,14 @@ def upload_to_bucket( return ResourceList[BucketFile](**r.json()).data - def delete_bucket_file(self, bucket_file_id: BucketFile | str | uuid.UUID, **params: te.Unpack[BaseKwargs]): - self._delete_resource("bucket-files", bucket_file_id, **params) + def delete_bucket_file(self, bucketFileId: BucketFile | str | uuid.UUID, **params: te.Unpack[BaseKwargs]): + self._delete_resource("bucket-files", bucketFileId, **params) def get_bucket_file( - self, bucket_file_id: BucketFile | str | uuid.UUID, **params: te.Unpack[GetKwargs] + self, bucketFileId: BucketFile | str | uuid.UUID, **params: te.Unpack[GetKwargs] ) -> BucketFile | None: return self._get_single_resource( - BucketFile, "bucket-files", bucket_file_id, include=get_includable_names(BucketFile), **params + BucketFile, "bucket-files", bucketFileId, include=get_includable_names(BucketFile), **params ) def get_bucket_files(self, **params: te.Unpack[GetKwargs]) -> ResourceListResult[BucketFile]: @@ -166,14 +188,14 @@ def find_bucket_files(self, **params: te.Unpack[FindAllKwargs]) -> ResourceListR def stream_bucket_file( self, - bucket_file_id: BucketFile | str | uuid.UUID, + bucketFileId: BucketFile | str | uuid.UUID, chunk_size: int = 1024, **params: te.Unpack[BaseKwargs], ) -> t.Iterator[bytes]: r = self._request( "GET", "bucket-files", - str(obtain_uuid_from(bucket_file_id)), + str(obtain_uuid_from(bucketFileId)), "stream", expected_code=httpx.codes.OK.value, stream=True, diff --git a/tests/test_base_client.py b/tests/test_base_client.py index 3b82281..95a7199 100644 --- a/tests/test_base_client.py +++ b/tests/test_base_client.py @@ -25,7 +25,7 @@ ) from flame_hub.auth import ClientAuth, PasswordAuth, StaticAuth from flame_hub.types import FilterOperator -from flame_hub.models import Node, Realm, User, Bucket, RefreshToken +from flame_hub.models import Node, Realm, User, Bucket, RefreshToken, ClientCredentials from tests.helpers import next_random_string @@ -228,18 +228,17 @@ def new_mock_client(body: t.Any, status_code: int = httpx.codes.OK.value) -> htt NODE_JSON = { "id": "5a0a3b0e-e4a0-4a41-9d7f-e2b9dcdcf9c1", "name": "my-node", - "external_name": None, + "externalName": None, "hidden": False, - "realm_id": "9d6b7a44-3f66-4f8b-9d2e-9dd0d2d7b2c1", - "registry_id": None, + "realmId": "9d6b7a44-3f66-4f8b-9d2e-9dd0d2d7b2c1", + "registryId": None, "type": "default", - "public_key": None, + "publicKey": None, "online": False, - "registry_project_id": None, - "robot_id": None, - "client_id": None, - "created_at": "2026-07-20T09:25:01.000Z", - "updated_at": "2026-07-20T09:25:01.000Z", + "registryProjectId": None, + "clientId": None, + "createdAt": "2026-07-20T09:25:01.000Z", + "updatedAt": "2026-07-20T09:25:01.000Z", } @@ -272,12 +271,94 @@ def test_auth_client_keeps_list_response_untouched(): def test_base_client_keeps_single_resource_untouched(): - # the FLAME Hub core and storage services respond with the resource itself + # the base client makes no assumption about the envelope, so it passes the body through unchanged client = BaseClient(base_url="http://localhost", client=new_mock_client(NODE_JSON)) assert client._get_single_resource(Node, "nodes", NODE_JSON["id"]) == Node(**NODE_JSON) +BUCKET_JSON = { + "id": "0a6ff30e-2f6f-4a09-9d67-33f5b5a4bb3e", + "name": "my-bucket", + "region": None, + "createdAt": "2026-07-20T09:25:01.000Z", + "updatedAt": "2026-07-20T09:25:01.000Z", + "actorId": None, + "actorType": None, + "realmId": None, +} + +CLIENT_CREDENTIALS_JSON = { + "id": "9b5d0b2c-19d7-4b7f-9a9a-4b1f0aa0f9de", + "secret": "s3cr3t", + "name": "my-node", + "displayName": "My Node", +} + + +@pytest.mark.parametrize( + "status_code,call_client", + [ + (httpx.codes.OK.value, lambda c: c.get_node(NODE_JSON["id"])), + (httpx.codes.CREATED.value, lambda c: c.create_node(NODE_JSON["name"], NODE_JSON["realmId"])), + (httpx.codes.ACCEPTED.value, lambda c: c.update_node(NODE_JSON["id"], hidden=False)), + ], +) +def test_core_client_unwraps_single_resource(status_code, call_client): + # the FLAME Hub wraps records as {"data": ..., "meta": ...} since 0.13.0 + core_client = flame_hub.CoreClient(client=new_mock_client({"data": NODE_JSON, "meta": {}}, status_code)) + + assert call_client(core_client) == Node(**NODE_JSON) + + +def test_core_client_rejects_unwrapped_single_resource(): + core_client = flame_hub.CoreClient(client=new_mock_client(NODE_JSON)) + + with pytest.raises(ValueError, match="data property"): + core_client.get_node(NODE_JSON["id"]) + + +def test_core_client_unwraps_analysis_command_response(): + # POST /analyses/:id/command reads the body itself rather than going through _get_single_resource + core_client = flame_hub.CoreClient(client=new_mock_client(NODE_JSON, httpx.codes.ACCEPTED.value)) + + with pytest.raises(ValueError, match="data property"): + core_client.send_analysis_command(uuid.uuid4(), "configurationLock") + + +@pytest.mark.parametrize( + "call_client", + [ + lambda c: c.get_node_client_credentials(NODE_JSON["id"]), + lambda c: c.get_analysis_client_credentials(uuid.uuid4()), + ], +) +def test_core_client_keeps_credentials_flat(call_client): + # the credential routes are among the endpoints which deliberately respond without an envelope + core_client = flame_hub.CoreClient(client=new_mock_client(CLIENT_CREDENTIALS_JSON)) + + assert call_client(core_client) == ClientCredentials(**CLIENT_CREDENTIALS_JSON) + + +def test_core_client_keeps_list_response_untouched(): + core_client = flame_hub.CoreClient(client=new_mock_client({"data": [NODE_JSON], "meta": {"total": 1}})) + + assert core_client.get_nodes() == [Node(**NODE_JSON)] + + +def test_storage_client_unwraps_single_resource(): + storage_client = flame_hub.StorageClient(client=new_mock_client({"data": BUCKET_JSON, "meta": {}})) + + assert storage_client.get_bucket(BUCKET_JSON["id"]) == Bucket(**BUCKET_JSON) + + +def test_storage_client_rejects_unwrapped_single_resource(): + storage_client = flame_hub.StorageClient(client=new_mock_client(BUCKET_JSON)) + + with pytest.raises(ValueError, match="data property"): + storage_client.get_bucket(BUCKET_JSON["id"]) + + @pytest.mark.integration @pytest.mark.parametrize( "resource_type,base_url_fixture_name,path", diff --git a/tests/test_core.py b/tests/test_core.py index f604259..38963b1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -37,30 +37,28 @@ def sync_master_images(core_client): def master_image(core_client): default_master_image = os.getenv("PYTEST_DEFAULT_MASTER_IMAGE", "python/base") - if len(core_client.find_master_images(filter={"virtual_path": default_master_image})) != 1: + if len(core_client.find_master_images(filter={"virtualPath": default_master_image})) != 1: sync_master_images(core_client) def _check_default_master_image_available(): - assert len(core_client.find_master_images(filter={"virtual_path": default_master_image})) == 1 + assert len(core_client.find_master_images(filter={"virtualPath": default_master_image})) == 1 assert_eventually(_check_default_master_image_available, max_retries=10, delay_millis=1000) - return core_client.find_master_images(filter={"virtual_path": default_master_image})[0] + return core_client.find_master_images(filter={"virtualPath": default_master_image})[0] @pytest.fixture(scope="module") def master_image_group(core_client, master_image): - if len(core_client.find_master_image_groups(filter={"virtual_path": master_image.group_virtual_path})) != 1: + if len(core_client.find_master_image_groups(filter={"virtualPath": master_image.groupVirtualPath})) != 1: sync_master_images(core_client) def _check_default_master_image_group_available(): - assert ( - len(core_client.find_master_image_groups(filter={"virtual_path": master_image.group_virtual_path})) == 1 - ) + assert len(core_client.find_master_image_groups(filter={"virtualPath": master_image.groupVirtualPath})) == 1 assert_eventually(_check_default_master_image_group_available, max_retries=10, delay_millis=1000) - return core_client.find_master_image_groups(filter={"virtual_path": master_image.group_virtual_path})[0] + return core_client.find_master_image_groups(filter={"virtualPath": master_image.groupVirtualPath})[0] @pytest.fixture() @@ -103,7 +101,7 @@ def project_node_includables(): def analysis(core_client, project, master_image): new_analysis = core_client.create_analysis( project, - master_image_id=master_image.id, + masterImageId=master_image.id, ) yield new_analysis core_client.delete_analysis(new_analysis) @@ -116,7 +114,7 @@ def analysis_includables(): @pytest.fixture() def analysis_node(core_client, analysis, project_node): - new_analysis_node = core_client.create_analysis_node(analysis.id, project_node.node_id) + new_analysis_node = core_client.create_analysis_node(analysis.id, project_node.nodeId) yield new_analysis_node core_client.delete_analysis_node(new_analysis_node) @@ -129,11 +127,11 @@ def analysis_node_includables(): @pytest.fixture() def analysis_code_bucket(core_client, analysis): def _wait_for_buckets(): - assert len(core_client.find_analysis_buckets(filter={"analysis_id": analysis.id})) != 0 + assert len(core_client.find_analysis_buckets(filter={"analysisId": analysis.id})) != 0 assert_eventually(_wait_for_buckets) - analysis_buckets = core_client.find_analysis_buckets(filter={"analysis_id": analysis.id}) + analysis_buckets = core_client.find_analysis_buckets(filter={"analysisId": analysis.id}) code_buckets = [bucket for bucket in analysis_buckets if bucket.type == AnalysisBucketType.CODE] assert len(code_buckets) == 1 @@ -154,7 +152,7 @@ def analysis_bucket_file(core_client, storage_client, analysis_code_bucket, rng_ # Upload example file to referenced bucket. file_name = next_random_string() bucket_files = storage_client.upload_to_bucket( - analysis_code_bucket.bucket_id, {"file_name": file_name, "content": rng_bytes} + analysis_code_bucket.bucketId, {"file_name": file_name, "content": rng_bytes} ) assert len(bucket_files) == 1 @@ -163,14 +161,14 @@ def analysis_bucket_file(core_client, storage_client, analysis_code_bucket, rng_ def _wait_for_analysis_bucket_file(): analysis_bucket_files = core_client.find_analysis_bucket_files( - filter={"analysis_id": analysis_code_bucket.analysis_id} + filter={"analysisId": analysis_code_bucket.analysisId} ) assert len(analysis_bucket_files) == 1 assert_eventually(_wait_for_analysis_bucket_file) analysis_bucket_file = core_client.find_analysis_bucket_files( - filter={"analysis_id": analysis_code_bucket.analysis_id} + filter={"analysisId": analysis_code_bucket.analysisId} ).pop() yield analysis_bucket_file @@ -191,12 +189,12 @@ def configured_analysis(core_client, registry, analysis, master_realm, analysis_ for node_type in t.get_args(NodeType): new_node = core_client.create_node( name=next_random_string(), - realm_id=master_realm, + realmId=master_realm, node_type=node_type, - registry_id=registry.id, + registryId=registry.id, ) nodes.append(new_node) - core_client.create_project_node(analysis.project_id, new_node) + core_client.create_project_node(analysis.projectId, new_node) core_client.create_analysis_node(analysis, new_node) # Mark analysis bucket file as the entrypoint. @@ -224,11 +222,11 @@ def analysis_log(core_client, configured_analysis): core_client.send_analysis_command(configured_analysis, "buildStart") def _check_analysis_logs_present(): - assert len(core_client.find_analysis_logs(filter={"analysis_id": configured_analysis.id})) > 0 + assert len(core_client.find_analysis_logs(filter={"analysisId": configured_analysis.id})) > 0 assert_eventually(_check_analysis_logs_present) - return core_client.find_analysis_logs(filter={"analysis_id": configured_analysis.id})[0] + return core_client.find_analysis_logs(filter={"analysisId": configured_analysis.id})[0] @pytest.fixture() @@ -236,8 +234,8 @@ def registry(core_client): new_registry = core_client.create_registry( name=next_random_string(), host=next_random_string(), - # account_name=next_random_string(), - account_secret=next_random_string(), + # accountName=next_random_string(), + accountSecret=next_random_string(), ) yield new_registry core_client.delete_registry(new_registry) @@ -253,8 +251,8 @@ def registry_project(core_client, registry): new_registry_project = core_client.create_registry_project( name=next_random_string(), registry_project_type="default", - registry_id=registry, - external_name=next_random_string(charset=string.ascii_lowercase + string.digits), + registryId=registry, + externalName=next_random_string(charset=string.ascii_lowercase + string.digits), ) yield new_registry_project core_client.delete_registry_project(new_registry_project) @@ -298,39 +296,39 @@ def test_get_node_not_found(core_client): def test_update_node(core_client, node): # only accepts lowercase letters and numbers new_name = next_random_string(charset=string.ascii_lowercase + string.digits) - new_node = core_client.update_node(node.id, external_name=new_name) + new_node = core_client.update_node(node.id, externalName=new_name) assert node != new_node - assert new_node.external_name == new_name + assert new_node.externalName == new_name @pytest.mark.xfail(reason="Hub returns properties of another registry") def test_get_node_registry_credentials(core_client, node, registry): - core_client.update_node(node.id, registry_id=registry.id) + core_client.update_node(node.id, registryId=registry.id) credentials = core_client.get_node_registry_credentials(node.id) assert credentials.host == registry.host - assert credentials.account_name == registry.account_name - assert credentials.account_secret == registry.account_secret + assert credentials.accountName == registry.accountName + assert credentials.accountSecret == registry.accountSecret def test_get_node_client_credentials(core_client, auth_client, node): - client = auth_client.get_client(client_id=node.client_id) - credentials = core_client.get_node_client_credentials(node_id=node.id) + client = auth_client.get_client(clientId=node.clientId) + credentials = core_client.get_node_client_credentials(nodeId=node.id) assert credentials.id == client.id assert credentials.name == client.name - assert credentials.display_name == client.display_name + assert credentials.displayName == client.displayName def test_update_node_client_credentials(core_client, node): new_name, new_display_name, new_secret = next_random_string().lower(), next_random_string(), next_random_string() new_credentials = core_client.update_node_client_credentials( - node_id=node.id, name=new_name, display_name=new_display_name, secret=new_secret + nodeId=node.id, name=new_name, displayName=new_display_name, secret=new_secret ) assert new_credentials.name == new_name - assert new_credentials.display_name == new_display_name + assert new_credentials.displayName == new_display_name assert new_credentials.secret == new_secret @@ -378,11 +376,11 @@ def test_get_project_not_found(core_client): def test_update_project(core_client, project): new_name = next_random_string() new_display_name = next_random_string() - new_node = core_client.update_project(project.id, name=new_name, display_name=new_display_name) + new_node = core_client.update_project(project.id, name=new_name, displayName=new_display_name) assert project != new_node assert new_node.name.lower() == new_name.lower() - assert new_node.display_name.lower() == new_display_name.lower() + assert new_node.displayName.lower() == new_display_name.lower() def test_get_project_nodes(core_client, project_node, project_node_includables): @@ -393,8 +391,8 @@ def test_get_project_nodes(core_client, project_node, project_node_includables): def test_find_project_nodes(core_client, project_node, project_node_includables): - # Use "project_id" instead of "id" because filtering for ids does not work. - project_nodes_find = core_client.find_project_nodes(filter={"project_id": project_node.project_id}) + # Use "projectId" instead of "id" because filtering for ids does not work. + project_nodes_find = core_client.find_project_nodes(filter={"projectId": project_node.projectId}) assert [project_node.id] == [pn.id for pn in project_nodes_find] assert all( @@ -412,9 +410,9 @@ def test_get_project_node(core_client, project_node, project_node_includables): def test_update_project_node(core_client, project_node): new_comment = next_random_string() - new_project_node = core_client.update_project_node(project_node.id, approval_status="rejected", comment=new_comment) + new_project_node = core_client.update_project_node(project_node.id, approvalStatus="rejected", comment=new_comment) - assert new_project_node.approval_status == "rejected" + assert new_project_node.approvalStatus == "rejected" assert new_project_node != project_node @@ -460,14 +458,14 @@ def test_update_analysis(core_client, analysis): new_analysis = core_client.update_analysis( analysis.id, name=new_name, - display_name=new_display_name, - image_command_arguments=args, + displayName=new_display_name, + imageCommandArguments=args, ) assert analysis != new_analysis assert new_analysis.name.lower() == new_name.lower() - assert new_analysis.display_name.lower() == new_display_name.lower() - assert new_analysis.image_command_arguments == args # Note that args is modified during updating the analysis. + assert new_analysis.displayName.lower() == new_display_name.lower() + assert new_analysis.imageCommandArguments == args # Note that args is modified during updating the analysis. def test_create_analysis_with_arguments(core_client, project, master_image): @@ -477,19 +475,19 @@ def test_create_analysis_with_arguments(core_client, project, master_image): ] analysis = core_client.create_analysis( project, - master_image_id=master_image.id, - image_command_arguments=args, + masterImageId=master_image.id, + imageCommandArguments=args, ) core_client.delete_analysis(analysis) def test_unlock_analysis(core_client, configured_analysis): assert ( - core_client.send_analysis_command(configured_analysis.id, command="configurationUnlock").configuration_locked + core_client.send_analysis_command(configured_analysis.id, command="configurationUnlock").configurationLocked is False ) assert ( - core_client.send_analysis_command(configured_analysis.id, command="configurationLock").configuration_locked + core_client.send_analysis_command(configured_analysis.id, command="configurationLock").configurationLocked is True ) @@ -497,32 +495,32 @@ def test_unlock_analysis(core_client, configured_analysis): def test_build_analysis(core_client, configured_analysis): assert ( core_client.send_analysis_command( - analysis_id=configured_analysis.id, + analysisId=configured_analysis.id, command="buildStart", - ).build_status + ).buildStatus == "starting" ) def _wait_for_successful_build(): - analysis = core_client.get_analysis(analysis_id=configured_analysis.id) - assert analysis.build_status == "executed" + analysis = core_client.get_analysis(analysisId=configured_analysis.id) + assert analysis.buildStatus == "executed" assert_eventually(_wait_for_successful_build) def test_get_analysis_client_credentials(core_client, auth_client, analysis): - client = auth_client.get_client(client_id=analysis.client_id) - credentials = core_client.get_analysis_client_credentials(analysis_id=analysis.id) + client = auth_client.get_client(clientId=analysis.clientId) + credentials = core_client.get_analysis_client_credentials(analysisId=analysis.id) assert credentials.id == client.id assert credentials.name == client.name - assert credentials.display_name == client.display_name + assert credentials.displayName == client.displayName def test_update_analysis_client_credentials(core_client, analysis): new_name, new_display_name, new_secret = next_random_string().lower(), next_random_string(), next_random_string() new_credentials = core_client.update_analysis_client_credentials( - analysis_id=analysis.id, name=new_name, display_name=new_display_name, secret=new_secret + analysisId=analysis.id, name=new_name, displayName=new_display_name, secret=new_secret ) assert new_credentials.secret == new_secret @@ -533,13 +531,13 @@ def test_update_analysis_node(core_client, analysis_node): status = random.choice(t.get_args(ProcessStatus)) new_analysis_node = core_client.update_analysis_node( analysis_node.id, - execution_status=status, - execution_progress=progress, + executionStatus=status, + executionProgress=progress, ) assert analysis_node != new_analysis_node - assert new_analysis_node.execution_status == status - assert new_analysis_node.execution_progress == progress + assert new_analysis_node.executionStatus == status + assert new_analysis_node.executionProgress == progress def test_get_analysis_nodes(core_client, analysis_node, analysis_node_includables): @@ -552,8 +550,8 @@ def test_get_analysis_nodes(core_client, analysis_node, analysis_node_includable def test_find_analysis_nodes(core_client, analysis_node, analysis_node_includables): - # Use "analysis_id" instead of "id" because filtering for ids does not work. - analysis_nodes_find = core_client.find_analysis_nodes(filter={"analysis_id": analysis_node.analysis_id}) + # Use "analysisId" instead of "id" because filtering for ids does not work. + analysis_nodes_find = core_client.find_analysis_nodes(filter={"analysisId": analysis_node.analysisId}) assert [analysis_node.id] == [an.id for an in analysis_nodes_find] assert all( @@ -576,32 +574,32 @@ def test_get_analysis_node_not_found(core_client): @pytest.mark.xfail(reason="Deletion of analysis node logs does not work") def test_analysis_node_logs(core_client, analysis_node): log = core_client.create_analysis_node_log( - analysis_id=analysis_node.analysis_id, - node_id=analysis_node.node_id, + analysisId=analysis_node.analysisId, + nodeId=analysis_node.nodeId, level=random.choice(t.get_args(LogLevel)), message=next_random_string(), ) def _check_analysis_node_logs_present(): found_logs = core_client.find_analysis_node_logs( - filter={"analysis_id": analysis_node.analysis_id, "node_id": analysis_node.node_id} + filter={"analysisId": analysis_node.analysisId, "nodeId": analysis_node.nodeId} ) assert len(found_logs) == 1 assert_eventually(_check_analysis_node_logs_present) new_log = core_client.find_analysis_node_logs( - filter={"analysis_id": analysis_node.analysis_id, "node_id": analysis_node.node_id} + filter={"analysisId": analysis_node.analysisId, "nodeId": analysis_node.nodeId} )[0] assert log == new_log - core_client.delete_analysis_node_logs(analysis_id=analysis_node.analysis_id, node_id=analysis_node.node_id) + core_client.delete_analysis_node_logs(analysisId=analysis_node.analysisId, nodeId=analysis_node.nodeId) assert ( len( core_client.find_analysis_node_logs( - filter={"analysis_id": analysis_node.analysis_id, "node_id": analysis_node.node_id} + filter={"analysisId": analysis_node.analysisId, "nodeId": analysis_node.nodeId} ) ) == 0 @@ -630,8 +628,8 @@ def test_get_analysis_buckets(core_client, analysis_code_bucket, analysis_bucket def test_find_analysis_buckets(core_client, analysis_code_bucket, analysis_bucket_includables): - # Use "analysis_id" instead of "id" because filtering for ids does not work. - analysis_buckets_find = core_client.find_analysis_buckets(filter={"analysis_id": analysis_code_bucket.analysis_id}) + # Use "analysisId" instead of "id" because filtering for ids does not work. + analysis_buckets_find = core_client.find_analysis_buckets(filter={"analysisId": analysis_code_bucket.analysisId}) assert analysis_code_bucket.id in [bucket.id for bucket in analysis_buckets_find] assert all( @@ -667,9 +665,9 @@ def test_get_analysis_bucket_files(core_client, analysis_bucket_file, analysis_b def test_find_analysis_bucket_files(core_client, analysis_bucket_file, analysis_bucket_file_includables): - # Use "analysis_id" instead of "id" because filtering for ids does not work. + # Use "analysisId" instead of "id" because filtering for ids does not work. analysis_bucket_files_find = core_client.find_analysis_bucket_files( - filter={"analysis_id": analysis_bucket_file.analysis_id} + filter={"analysisId": analysis_bucket_file.analysisId} ) assert [analysis_bucket_file.id] == [abf.id for abf in analysis_bucket_files_find] @@ -726,7 +724,7 @@ def test_registry_setup(core_client, registry): core_client.send_registry_command(registry.id, command="setup") def _check_setup(): - registry_projects = core_client.find_registry_projects(filter={"registry_id": registry.id}) + registry_projects = core_client.find_registry_projects(filter={"registryId": registry.id}) assert len(registry_projects) == 3 assert {"incoming", "outgoing", "masterImages"} == set(rp.type for rp in registry_projects) @@ -780,6 +778,6 @@ def test_update_registry_project(core_client, registry_project): @pytest.mark.xfail(reason="Bug in Hub, see https://github.com/PrivateAIM/hub/issues/1181.") def test_delete_analysis_logs(core_client, analysis_log): - core_client.delete_analysis_logs(analysis_id=analysis_log.labels["analysis_id"]) + core_client.delete_analysis_logs(analysisId=analysis_log.labels["analysisId"]) - assert len(core_client.find_analysis_logs(filter={"analysis_id": analysis_log.labels["analysis_id"]})) == 0 + assert len(core_client.find_analysis_logs(filter={"analysisId": analysis_log.labels["analysisId"]})) == 0 diff --git a/tests/test_storage.py b/tests/test_storage.py index e7ade35..c403e10 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -55,7 +55,7 @@ def test_get_bucket(storage_client, bucket): def test_stream_bucket_tarball(storage_client, bucket_file, rng_bytes): - tarball_bytes = next(storage_client.stream_bucket_tarball(bucket_file.bucket_id)) + tarball_bytes = next(storage_client.stream_bucket_tarball(bucket_file.bucketId)) with tarfile.open(fileobj=BytesIO(tarball_bytes), mode="r") as tar: members = tar.getmembers()