diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4e1064b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: Run linter on PR + +on: + pull_request: + branches: [ master ] + +jobs: + CheckLinting: + timeout-minutes: 5 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install uv + run: | + curl -Ls https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Create virtual environment and install dependencies + run: | + uv venv + source .venv/bin/activate + uv pip install -e ".[dev]" + + - name: Lint + run: | + source .venv/bin/activate + make lint + diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 7078218..fb86310 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -32,12 +32,6 @@ jobs: source .venv/bin/activate uv pip install -e ".[dev]" - - name: Lint - continue-on-error: true - run: | - source .venv/bin/activate - make lint - - name: Start prism run: | make prism-start diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf450e..e205fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Version 0.6.0 - 2026-01-27 + +- Add update_validation method +- Add get_validation_task method +- Add update_agent_run method +- Add create_hook_run method +- Add update_hook_run method +- Add update_action_run method +- Change input parameter name from validation_task_id to task_id in update_validation_task method + ## Version 0.5.0 - 2026-01-16 - Add access_token and access_token_expiration as input arguments to Credentials constructor diff --git a/Makefile b/Makefile index 90689ac..75dc81e 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ prism-start: --detach \ -p 4010:4010 \ stoplight/prism:5.5.0 mock -d -h 0.0.0.0 \ - https://raw.githubusercontent.com/LucidtechAI/cradl-docs/master/static/oas.json \ + https://public.cradl.ai/api/openapi.json \ > /tmp/prism.cid diff --git a/pyproject.toml b/pyproject.toml index 2f7766d..d11bdba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cradl" -version = "0.5.0" +version = "0.6.0" description = "Python SDK for Cradl" authors = [{ name = "Cradl", email = "hello@cradl.ai" }] readme = "README.md" diff --git a/src/cradl/__init__.py b/src/cradl/__init__.py index f743d77..b0186f0 100644 --- a/src/cradl/__init__.py +++ b/src/cradl/__init__.py @@ -16,10 +16,13 @@ def transition_handler(f): """ - Decorator used to manage transition states. The decorator assumes that the environment variables TRANSITION_ID and EXECUTION_ID exist, - and ensures that the transition execution is updated after the handler is invoked. The return value of the handler can either be a `result` - or a tuple `(result, status)`. If no status is provided, the handler is assumed to have run successfully. If the handler throws an exception, - the status of the transition execution will be set to 'failed'. Status must be one of 'succeeded', 'rejected', 'retry' or 'failed'. + Decorator used to manage transition states. + The decorator assumes that the environment variables TRANSITION_ID and EXECUTION_ID exist, + and ensures that the transition execution is updated after the handler is invoked. + The return value of the handler can either be a `result` or a tuple `(result, status)`. + If no status is provided, the handler is assumed to have run successfully. If the handler throws an exception, + the status of the transition execution will be set to 'failed'. + Status must be one of 'succeeded', 'rejected', 'retry' or 'failed'. >>> @transition_handler >>> def my_handler(las_client: las.Client, event: dict): diff --git a/src/cradl/backoff.py b/src/cradl/backoff.py index 42831d9..3b2913c 100644 --- a/src/cradl/backoff.py +++ b/src/cradl/backoff.py @@ -1,13 +1,13 @@ import functools import time -from typing import Union, Type, Callable +from typing import Optional, Union, Type, Callable def exponential_backoff( exceptions: Union[tuple[Type[Exception]], Type[Exception]], base_wait: int = 1, - max_time: float = None, - max_tries: int = None, + max_time: Optional[float] = None, + max_tries: Optional[int] = None, rate: int = 2, giveup: Callable = lambda e: False, ) -> Callable: diff --git a/src/cradl/client.py b/src/cradl/client.py index ed53cdc..dc13349 100644 --- a/src/cradl/client.py +++ b/src/cradl/client.py @@ -7,8 +7,8 @@ from typing import Callable, Dict, List, Optional, Sequence, Union from urllib.parse import urlparse, quote -import requests -from requests.exceptions import RequestException +import requests # type: ignore +from requests.exceptions import RequestException # type: ignore from .credentials import Credentials, guess_credentials from .content import parse_content @@ -65,7 +65,7 @@ def _make_request( raise EmptyRequestError kwargs = {'params': params} - None if body is None else kwargs.update({'data': json.dumps(body)}) + None if body is None else kwargs.update({'data': json.dumps(body)}) # type: ignore uri = urlparse(f'{self.credentials.api_endpoint}{path}') headers = { @@ -94,7 +94,7 @@ def _make_fileserver_request( kwargs = {'params': query_params} if content: - kwargs.update({'data': content}) + kwargs.update({'data': content}) # type: ignore uri = urlparse(file_url) headers = {'Authorization': f'Bearer {self.credentials.access_token}'} @@ -196,6 +196,8 @@ def update_app_client(self, app_client_id, **optional_args) -> Dict: :param app_client_id: Id of the appClient :type app_client_id: str + :param metadata: Dictionary that can be used to store additional information + :type metadata: dict, optional :param name: Name of the appClient :type name: str, optional :param description: Description of the appClient @@ -230,118 +232,6 @@ def delete_app_client(self, app_client_id: str) -> Dict: """ return self._make_request(requests.delete, f'/appClients/{app_client_id}') - def create_asset(self, content: Content, **optional_args) -> Dict: - """Creates an asset, calls the POST /assets endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.create_asset(b'') - - :param content: Content to POST - :type content: Content - :param name: Name of the asset - :type name: str, optional - :param description: Description of the asset - :type description: str, optional - :return: Asset response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - content, _ = parse_content(content) - body = { - 'content': content, - **optional_args, - } - return self._make_request(requests.post, '/assets', body=body) - - def list_assets(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: - """List assets available, calls the GET /assets endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_assets() - - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Assets response from REST API without the content of each asset - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, '/assets', params=params) - - def get_asset(self, asset_id: str) -> Dict: - """Get asset, calls the GET /assets/{assetId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_asset(asset_id='') - - :param asset_id: Id of the asset - :type asset_id: str - :return: Asset response from REST API with content - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.get, f'/assets/{asset_id}') - - def update_asset(self, asset_id: str, **optional_args) -> Dict: - """Updates an asset, calls the PATCH /assets/{assetId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.update_asset('', content=b'') - - :param asset_id: Id of the asset - :type asset_id: str - :param content: Content to PATCH - :type content: Content, optional - :param name: Name of the asset - :type name: str, optional - :param description: Description of the asset - :type description: str, optional - :return: Asset response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - content = optional_args.get('content') - - if content: - parsed_content, _ = parse_content(content) - optional_args['content'] = parsed_content - - return self._make_request(requests.patch, f'/assets/{asset_id}', body=optional_args) - - def delete_asset(self, asset_id: str) -> Dict: - """Delete the asset with the provided asset_id, calls the DELETE /assets/{assetId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.delete_asset('') - - :param asset_id: Id of the asset - :type asset_id: str - :return: Asset response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.delete, f'/assets/{asset_id}') - def create_payment_method(self, **optional_args) -> Dict: """Creates a payment_method, calls the POST /paymentMethods endpoint. @@ -393,7 +283,7 @@ def update_payment_method( self, payment_method_id: str, *, - stripe_setup_intent_secret: str = None, + stripe_setup_intent_secret: Optional[str] = None, **optional_args ) -> Dict: """Updates a payment_method, calls the PATCH /paymentMethods/{paymentMethodId} endpoint. @@ -490,19 +380,18 @@ def update_dataset(self, dataset_id, metadata: Optional[dict] = None, **optional :param dataset_id: Id of the dataset :type dataset_id: str + :param metadata: Dictionary that can be used to store additional information + :type metadata: dict, optional :param name: Name of the dataset :type name: str, optional :param description: Description of the dataset :type description: str, optional - :param metadata: Dictionary that can be used to store additional information - :type metadata: dict, optional :return: Dataset response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - body = dictstrip({'metadata': metadata}) body.update(**optional_args) return self._make_request(requests.patch, f'/datasets/{dataset_id}', body=body) @@ -525,82 +414,18 @@ def delete_dataset(self, dataset_id: str, delete_documents: bool = False) -> Dic return self._make_request(requests.delete, f'/datasets/{dataset_id}') - def create_transformation(self, dataset_id, operations) -> Dict: - """Creates a transformation on a dataset, calls the POST /datasets/{datasetId}/transformations endpoint. - - :param dataset_id: Id of the dataset - :type dataset_id: str - :param operations: Operations to perform on the dataset - :type operations: dict - :return: Transformation response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - - body = {'operations': operations} - return self._make_request(requests.post, f'/datasets/{dataset_id}/transformations', body=body) - - def list_transformations( - self, - dataset_id, - *, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - status: Optional[Queryparam] = None, - ) -> Dict: - """List transformations, calls the GET /datasets/{datasetId}/transformations endpoint. - - :param dataset_id: Id of the dataset - :type dataset_id: str - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :param status: Statuses of the transformations - :type status: Queryparam, optional - :return: Transformations response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - 'status': status, - } - return self._make_request(requests.get, f'/datasets/{dataset_id}/transformations', params=dictstrip(params)) - - def delete_transformation(self, dataset_id: str, transformation_id: str) -> Dict: - """Delete the transformation with the provided transformation_id, - calls the DELETE /datasets/{datasetId}/transformations/{transformationId} endpoint. - - :param dataset_id: Id of the dataset - :type dataset_id: str - :param transformation_id: Id of the transformation - :type transformation_id: str - :return: Transformation response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.delete, f'/datasets/{dataset_id}/transformations/{transformation_id}') - def create_document( self, content: Content, *, consent_id: Optional[str] = None, - dataset_id: str = None, - description: str = None, - ground_truth: Sequence[Dict[str, str]] = None, + dataset_id: Optional[str] = None, + description: Optional[str] = None, + ground_truth: Optional[Sequence[Dict[str, str]]] = None, metadata: Optional[dict] = None, - name: str = None, - agent_run_id: str = None, - retention_in_days: int = None, + name: Optional[str] = None, + agent_run_id: Optional[str] = None, + retention_in_days: Optional[int] = None, ) -> Dict: """Creates a document, calls the POST /documents endpoint. @@ -623,6 +448,8 @@ def create_document( :type retention_in_days: int, optional :param metadata: Dictionary that can be used to store additional information :type metadata: dict, optional + :param name: Name of the document + :type name: str, optional :return: Document response from REST API :rtype: dict @@ -810,10 +637,11 @@ def get_document( def update_document( self, document_id: str, - ground_truth: Sequence[Dict[str, Union[Optional[str], bool]]] = None, # For backwards compatibility reasons, this is placed before the * + # For backwards compatibility reasons, ground truth is placed before the * + ground_truth: Optional[Sequence[Dict[str, Union[Optional[str], bool]]]] = None, *, metadata: Optional[dict] = None, - dataset_id: str = None, + dataset_id: Optional[str] = None, ) -> Dict: """Update ground truth for a document, calls the PATCH /documents/{documentId} endpoint. Updating ground truth means adding the ground truth data for the particular document. @@ -861,10 +689,6 @@ def delete_document(self, document_id: str) -> Dict: def list_logs( self, *, - workflow_id: Optional[str] = None, - workflow_execution_id: Optional[str] = None, - transition_id: Optional[str] = None, - transition_execution_id: Optional[str] = None, max_results: Optional[int] = None, next_token: Optional[str] = None, ) -> Dict: @@ -874,14 +698,6 @@ def list_logs( >>> client = Client() >>> client.list_logs() - :param workflow_id: Only show logs from this workflow - :type workflow_id: str, optional - :param workflow_execution_id: Only show logs from this workflow execution - :type workflow_execution_id: str, optional - :param transition_id: Only show logs from this transition - :type transition_id: str, optional - :param transition_execution_id: Only show logs from this transition execution - :type transition_execution_id: str, optional :param max_results: Maximum number of results to be returned :type max_results: int, optional :param next_token: A unique token for each page, use the returned token to retrieve the next page. @@ -896,10 +712,6 @@ def list_logs( params = { 'maxResults': max_results, 'nextToken': next_token, - 'workflowId': workflow_id, - 'workflowExecutionId': workflow_execution_id, - 'transitionId': transition_id, - 'transitionExecutionId': transition_execution_id, } return self._make_request(requests.get, url, params=dictstrip(params)) @@ -1143,349 +955,135 @@ def delete_model(self, model_id: str) -> Dict: """ return self._make_request(requests.delete, f'/models/{model_id}') - def create_data_bundle(self, model_id, dataset_ids, **optional_args) -> Dict: - """Creates a data bundle, calls the POST /models/{modelId}/dataBundles endpoint. - - :param model_id: Id of the model - :type model_id: str - :param dataset_ids: Dataset Ids that will be included in the data bundle - :type dataset_ids: List[str] - :param name: Name of the data bundle - :type name: str, optional - :param description: Description of the data bundle - :type description: str, optional - :return: Data Bundle response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - - body = {'datasetIds': dataset_ids} - body.update(**optional_args) - return self._make_request(requests.post, f'/models/{model_id}/dataBundles', body=body) - - def get_data_bundle(self, model_id: str, data_bundle_id: str) -> Dict: - """Get data bundle, calls the GET /models/{modelId}/dataBundles/{dataBundleId} endpoint. + def get_organization(self, organization_id: str) -> Dict: + """Get an organization, calls the GET /organizations/{organizationId} endpoint. - :param model_id: ID of the model - :type model_id: str - :param data_bundle_id: ID of the data_bundle - :type data_bundle_id: str - :return: DataBundle response from REST API + :param organization_id: The Id of the organization + :type organization_id: str + :return: Organization response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - return self._make_request(requests.get, f'/models/{model_id}/dataBundles/{data_bundle_id}') + return self._make_request(requests.get, f'/organizations/{organization_id}') - def create_training( + def update_organization( self, - model_id, - data_bundle_ids, + organization_id: str, *, - metadata: Optional[dict] = None, - data_scientist_assistance: Optional[bool] = None, + payment_method_id: Optional[str] = None, + document_retention_in_days: Optional[int] = None, **optional_args, ) -> Dict: - """Requests a training, calls the POST /models/{modelId}/trainings endpoint. + """Updates an organization, calls the PATCH /organizations/{organizationId} endpoint. - :param model_id: Id of the model - :type model_id: str - :param data_bundle_ids: Data bundle ids that will be used for training - :type data_bundle_ids: List[str] - :param name: Name of the data bundle + :param organization_id: Id of organization + :type organization_id: str, optional + :param payment_method_id: Id of paymentMethod to use + :type payment_method_id: str, optional + :param name: Name of the organization :type name: str, optional - :param description: Description of the training + :param description: Description of the organization :type description: str, optional - :param metadata: Dictionary that can be used to store additional information - :type metadata: dict, optional - :param data_scientist_assistance: Request that one of Cradl's data scientists reviews and optimizes your training - :type data_scientist_assistance: bool, optional - :return: Training response from REST API + :return: Organization response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ + body = {**optional_args} + if payment_method_id: + body['paymentMethodId'] = payment_method_id - body = dictstrip({ - 'dataBundleIds': data_bundle_ids, - 'dataScientistAssistance': data_scientist_assistance, - 'metadata': metadata, - }) - body.update(**optional_args) - return self._make_request(requests.post, f'/models/{model_id}/trainings', body=body) + if document_retention_in_days: + body['documentRetentionInDays'] = document_retention_in_days + + return self._make_request(requests.patch, f'/organizations/{organization_id}', body=body) + + def create_prediction( + self, + document_id: str, + model_id: str, + *, + training_id: Optional[str] = None, + preprocess_config: Optional[dict] = None, + postprocess_config: Optional[dict] = None, + run_async: Optional[bool] = None, + agent_run_id: Optional[str] = None, + ) -> Dict: + """Create a prediction on a document using specified model, calls the POST /predictions endpoint. - def get_training(self, model_id: str, training_id: str, statistics_last_n_days: Optional[int] = None) -> Dict: - """Get training, calls the GET /models/{modelId}/trainings/{trainingId} endpoint. + >>> from cradl.client import Client + >>> client = Client() + >>> client.create_prediction(document_id='', model_id='') - :param model_id: ID of the model + :param document_id: Id of the document to run inference and create a prediction on + :type document_id: str + :param model_id: Id of the model to use for predictions :type model_id: str - :param training_id: ID of the training + :param training_id: Id of training to use for predictions :type training_id: str - :param statistics_last_n_days: Integer between 1 and 30 - :type statistics_last_n_days: int, optional - :return: Training response from REST API + :param preprocess_config: Preprocessing configuration for prediction. + { + 'autoRotate': True | False (optional) + 'maxPages': 1 - 3 (optional) + 'imageQuality': 'LOW' | 'HIGH' (optional) + 'pages': List with up to 3 page-indices to process (optional) + 'rotation': 0, 90, 180 or 270 (optional) + } + Examples: + {'pages': [0, 1, 5], 'autoRotate': True} + {'pages': [0, 1, -1], 'rotation': 90, 'imageQuality': 'HIGH'} + {'maxPages': 3, 'imageQuality': 'LOW'} + :type preprocess_config: dict, optional + :param postprocess_config: Post processing configuration for prediction. + { + 'strategy': 'BEST_FIRST' | 'BEST_N_PAGES', (required) + 'outputFormat': 'v1' | 'v2', (optional) + 'parameters': { (required if strategy=BEST_N_PAGES, omit otherwise) + 'n': int, (required if strategy=BEST_N_PAGES, omit otherwise) + 'collapse': True | False (optional if strategy=BEST_N_PAGES, omit otherwise) + } + } + Examples: + {'strategy': 'BEST_FIRST', 'outputFormat': 'v2'} + {'strategy': 'BEST_N_PAGES', 'parameters': {'n': 3}} + {'strategy': 'BEST_N_PAGES', 'parameters': {'n': 3, 'collapse': False}} + :type postprocess_config: dict, optional + :param run_async: If True run the prediction async, if False run sync. if omitted run synchronously. + :type run_async: bool + :return: Prediction response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - params = {'statisticsLastNDays': statistics_last_n_days} - return self._make_request(requests.get, f'/models/{model_id}/trainings/{training_id}', params=params) + body = { + 'documentId': document_id, + 'modelId': model_id, + 'trainingId': training_id, + 'preprocessConfig': preprocess_config, + 'postprocessConfig': postprocess_config, + 'async': run_async, + 'agentRunId': agent_run_id, + } + return self._make_request(requests.post, '/predictions', body=dictstrip(body)) + + def list_predictions( + self, + *, + max_results: Optional[int] = None, + next_token: Optional[str] = None, + order: Optional[str] = None, + sort_by: Optional[str] = None, + model_id: Optional[str] = None, + ) -> Dict: + """List predictions available, calls the GET /predictions endpoint. - def list_trainings(self, model_id, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: - """List trainings available, calls the GET /models/{modelId}/trainings endpoint. - - :param model_id: Id of the model - :type model_id: str - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Trainings response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, f'/models/{model_id}/trainings', params=params) - - def update_training( - self, - model_id: str, - training_id: str, - **optional_args, - ) -> Dict: - """Updates a training, calls the PATCH /models/{modelId}/trainings/{trainingId} endpoint. - - :param model_id: Id of the model - :type model_id: str - :param training_id: Id of the training - :type training_id: str - :param deployment_environment_id: Id of deploymentEnvironment - :type deployment_environment_id: str, optional - :param name: Name of the training - :type name: str, optional - :param description: Description of the training - :type description: str, optional - :param metadata: Dictionary that can be used to store additional information - :type metadata: dict, optional - :return: Training response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = {} - if 'deployment_environment_id' in optional_args: - body['deploymentEnvironmentId'] = optional_args.pop('deployment_environment_id') - body.update(optional_args) - - return self._make_request(requests.patch, f'/models/{model_id}/trainings/{training_id}', body=body) - - def list_data_bundles( - self, - model_id, - *, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - ) -> Dict: - """List data bundles available, calls the GET /models/{modelId}/dataBundles endpoint. - - :param model_id: Id of the model - :type model_id: str - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Data Bundles response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, f'/models/{model_id}/dataBundles', params=params) - - def update_data_bundle( - self, - model_id: str, - data_bundle_id: str, - **optional_args, - ) -> Dict: - """Updates a data bundle, calls the PATCH /models/{modelId}/dataBundles/{dataBundleId} endpoint. - - :param model_id: Id of the model - :type model_id: str - :param data_bundle_id: Id of the data bundle - :type data_bundle_id: str - :param name: Name of the data bundle - :type name: str, optional - :param description: Description of the data bundle - :type description: str, optional - :return: Data Bundle response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.patch, f'/models/{model_id}/dataBundles/{data_bundle_id}', body=optional_args) - - def delete_data_bundle(self, model_id: str, data_bundle_id: str) -> Dict: - """Delete the data bundle with the provided data_bundle_id, - calls the DELETE /models/{modelId}/dataBundles/{dataBundleId} endpoint. - - :param model_id: Id of the model - :type model_id: str - :param data_bundle_id: Id of the data bundle - :type data_bundle_id: str - :return: Data Bundle response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.delete, f'/models/{model_id}/dataBundles/{data_bundle_id}') - - def get_organization(self, organization_id: str) -> Dict: - """Get an organization, calls the GET /organizations/{organizationId} endpoint. - - :param organization_id: The Id of the organization - :type organization_id: str - :return: Organization response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.get, f'/organizations/{organization_id}') - - def update_organization( - self, - organization_id: str, - *, - payment_method_id: str = None, - document_retention_in_days: int = None, - **optional_args, - ) -> Dict: - """Updates an organization, calls the PATCH /organizations/{organizationId} endpoint. - - :param organization_id: Id of organization - :type organization_id: str, optional - :param payment_method_id: Id of paymentMethod to use - :type payment_method_id: str, optional - :param name: Name of the organization - :type name: str, optional - :param description: Description of the organization - :type description: str, optional - :return: Organization response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = {**optional_args} - if payment_method_id: - body['paymentMethodId'] = payment_method_id - - if document_retention_in_days: - body['documentRetentionInDays'] = document_retention_in_days - - return self._make_request(requests.patch, f'/organizations/{organization_id}', body=body) - - def create_prediction( - self, - document_id: str, - model_id: str, - *, - training_id: Optional[str] = None, - preprocess_config: Optional[dict] = None, - postprocess_config: Optional[dict] = None, - run_async: Optional[bool] = None, - agent_run_id: Optional[str] = None, - ) -> Dict: - """Create a prediction on a document using specified model, calls the POST /predictions endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.create_prediction(document_id='', model_id='') - - :param document_id: Id of the document to run inference and create a prediction on - :type document_id: str - :param model_id: Id of the model to use for predictions - :type model_id: str - :param training_id: Id of training to use for predictions - :type training_id: str - :param preprocess_config: Preprocessing configuration for prediction. - { - 'autoRotate': True | False (optional) - 'maxPages': 1 - 3 (optional) - 'imageQuality': 'LOW' | 'HIGH' (optional) - 'pages': List with up to 3 page-indices to process (optional) - 'rotation': 0, 90, 180 or 270 (optional) - } - Examples: - {'pages': [0, 1, 5], 'autoRotate': True} - {'pages': [0, 1, -1], 'rotation': 90, 'imageQuality': 'HIGH'} - {'maxPages': 3, 'imageQuality': 'LOW'} - :type preprocess_config: dict, optional - :param postprocess_config: Post processing configuration for prediction. - { - 'strategy': 'BEST_FIRST' | 'BEST_N_PAGES', (required) - 'outputFormat': 'v1' | 'v2', (optional) - 'parameters': { (required if strategy=BEST_N_PAGES, omit otherwise) - 'n': int, (required if strategy=BEST_N_PAGES, omit otherwise) - 'collapse': True | False (optional if strategy=BEST_N_PAGES, omit otherwise) - } - } - Examples: - {'strategy': 'BEST_FIRST', 'outputFormat': 'v2'} - {'strategy': 'BEST_N_PAGES', 'parameters': {'n': 3}} - {'strategy': 'BEST_N_PAGES', 'parameters': {'n': 3, 'collapse': False}} - :type postprocess_config: dict, optional - :param run_async: If True run the prediction async, if False run sync. if omitted run synchronously. - :type run_async: bool - :return: Prediction response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = { - 'documentId': document_id, - 'modelId': model_id, - 'trainingId': training_id, - 'preprocessConfig': preprocess_config, - 'postprocessConfig': postprocess_config, - 'async': run_async, - 'agentRunId': agent_run_id, - } - return self._make_request(requests.post, '/predictions', body=dictstrip(body)) - - def list_predictions( - self, - *, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - order: Optional[str] = None, - sort_by: Optional[str] = None, - model_id: Optional[str] = None, - ) -> Dict: - """List predictions available, calls the GET /predictions endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_predictions() + >>> from cradl.client import Client + >>> client = Client() + >>> client.list_predictions() :param max_results: Maximum number of results to be returned :type max_results: int, optional @@ -1552,684 +1150,71 @@ def list_plans( *, owner: Optional[Queryparam] = None, max_results: Optional[int] = None, - next_token: Optional[str] = None, - ) -> Dict: - """List plans available, calls the GET /plans endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_plans() - - :param owner: Organizations to retrieve plans from - :type owner: Queryparam, optional - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Plans response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - 'owner': owner, - } - return self._make_request(requests.get, '/plans', params=dictstrip(params)) - - def get_deployment_environment(self, deployment_environment_id: str) -> Dict: - """Get information about a specific DeploymentEnvironment, calls the - GET /deploymentEnvironments/{deploymentEnvironmentId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_deployment_environment('') - - :param deployment_environment_id: Id of the DeploymentEnvironment - :type deployment_environment_id: str - :return: DeploymentEnvironment response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - - return self._make_request(requests.get, f'/deploymentEnvironments/{quote(deployment_environment_id, safe="")}') - - def list_deployment_environments( - self, - *, - owner: Optional[Queryparam] = None, - max_results: Optional[int] = None, - next_token: Optional[str] = None - ) -> Dict: - """List DeploymentEnvironments available, calls the GET /deploymentEnvironments endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_deployment_environments() - - :param owner: Organizations to retrieve DeploymentEnvironments from - :type owner: Queryparam, optional - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: DeploymentEnvironments response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'owner': owner, - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, '/deploymentEnvironments', params=dictstrip(params)) - - def create_secret(self, data: dict, **optional_args) -> Dict: - """Creates a secret, calls the POST /secrets endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> data = {'username': '', 'password': ''} - >>> client.create_secret(data, description='') - - :param data: Dict containing the data you want to keep secret - :type data: str - :param name: Name of the secret - :type name: str, optional - :param description: Description of the secret - :type description: str, optional - :return: Secret response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = { - 'data': data, - **optional_args, - } - return self._make_request(requests.post, '/secrets', body=body) - - def list_secrets(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: - """List secrets available, calls the GET /secrets endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_secrets() - - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Secrets response from REST API without the username of each secret - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, '/secrets', params=params) - - def update_secret(self, secret_id: str, *, data: Optional[dict] = None, **optional_args) -> Dict: - """Updates a secret, calls the PATCH /secrets/secretId endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> data = {'username': '', 'password': ''} - >>> client.update_secret('', data, description='') - - :param secret_id: Id of the secret - :type secret_id: str - :param data: Dict containing the data you want to keep secret - :type data: dict, optional - :param name: Name of the secret - :type name: str, optional - :param description: Description of the secret - :type description: str, optional - :return: Secret response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = dictstrip({'data': data}) - body.update(**optional_args) - return self._make_request(requests.patch, f'/secrets/{secret_id}', body=body) - - def delete_secret(self, secret_id: str) -> Dict: - """Delete the secret with the provided secret_id, calls the DELETE /secrets/{secretId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.delete_secret('') - - :param secret_id: Id of the secret - :type secret_id: str - :return: Secret response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.delete, f'/secrets/{secret_id}') - - def create_transition( - self, - transition_type: str, - *, - parameters: Optional[dict] = None, - **optional_args, - ) -> Dict: - """Creates a transition, calls the POST /transitions endpoint. - - >>> import json - >>> from pathlib import Path - >>> from cradl.client import Client - >>> client = Client() - >>> # A typical docker transition - >>> docker_params = { - >>> 'imageUrl': '', - >>> 'credentials': {'username': '', 'password': ''} - >>> } - >>> client.create_transition('docker', params=docker_params) - >>> # A manual transition with UI - >>> assets = {'jsRemoteComponent': 'cradl:asset:', '': 'cradl:asset:'} - >>> manual_params = {'assets': assets} - >>> client.create_transition('manual', params=manual_params) - - :param transition_type: Type of transition "docker"|"manual" - :type transition_type: str - :param name: Name of the transition - :type name: str, optional - :param parameters: Parameters to the corresponding transition type - :type parameters: dict, optional - :param description: Description of the transition - :type description: str, optional - :return: Transition response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = dictstrip({ - 'transitionType': transition_type, - 'parameters': parameters, - }) - body.update(**optional_args) - return self._make_request(requests.post, '/transitions', body=body) - - def list_transitions( - self, - *, - transition_type: Optional[Queryparam] = None, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - ) -> Dict: - """List transitions, calls the GET /transitions endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_transitions('') - - :param transition_type: Types of transitions - :type transition_type: Queryparam, optional - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Transitions response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - url = '/transitions' - params = { - 'transitionType': transition_type, - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, url, params=dictstrip(params)) - - def get_transition(self, transition_id: str) -> Dict: - """Get the transition with the provided transition_id, calls the GET /transitions/{transitionId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_transition('') - - :param transition_id: Id of the transition - :type transition_id: str - :return: Transition response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.get, f'/transitions/{transition_id}') - - def update_transition( - self, - transition_id: str, - *, - assets: Optional[dict] = None, - cpu: Optional[int] = None, - memory: Optional[int] = None, - image_url: Optional[str] = None, - lambda_id: Optional[str] = None, - **optional_args, - ) -> Dict: - """Updates a transition, calls the PATCH /transitions/{transitionId} endpoint. - - >>> import json - >>> from pathlib import Path - >>> from cradl.client import Client - >>> client = Client() - >>> client.update_transition('', name='', description='') - - :param transition_id: Id of the transition - :type transition_id: str - :param name: Name of the transition - :type name: str, optional - :param description: Description of the transition - :type description: str, optional - :param assets: A dictionary where the values are assetIds that can be used in a manual transition - :type assets: dict, optional - :param environment: Environment variables to use for a docker transition - :type environment: dict, optional - :param environment_secrets: \ - A list of secretIds that contains environment variables to use for a docker transition - :type environment_secrets: list, optional - :param cpu: Number of CPU units to use for a docker transition - :type cpu: int, optional - :param memory: Memory in MiB to use for a docker transition - :type memory: int, optional - :param image_url: Docker image url to use for a docker transition - :type image_url: str, optional - :param lambda_id: Lambda ID to use for a lambda transition - :type lambda_id: str, optional - :param secret_id: Secret containing a username and password if image_url points to a private docker image - :type secret_id: str, optional - :return: Transition response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = {} - parameters = dictstrip({ - 'assets': assets, - 'cpu': cpu, - 'imageUrl': image_url, - 'lambdaId': lambda_id, - 'memory': memory, - }) - - if 'environment' in optional_args: - parameters['environment'] = optional_args.pop('environment') - if 'environment_secrets' in optional_args: - parameters['environmentSecrets'] = optional_args.pop('environment_secrets') - if 'secret_id' in optional_args: - parameters['secretId'] = optional_args.pop('secret_id') - if parameters: - body['parameters'] = parameters - - body.update(**optional_args) - return self._make_request(requests.patch, f'/transitions/{transition_id}', body=body) - - def execute_transition(self, transition_id: str) -> Dict: - """Start executing a manual transition, calls the POST /transitions/{transitionId}/executions endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.execute_transition('') - - :param transition_id: Id of the transition - :type transition_id: str - :return: Transition execution response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - endpoint = f'/transitions/{transition_id}/executions' - return self._make_request(requests.post, endpoint, body={}) - - def delete_transition(self, transition_id: str) -> Dict: - """Delete the transition with the provided transition_id, calls the DELETE /transitions/{transitionId} endpoint. - Will fail if transition is in use by one or more workflows. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.delete_transition('') - - :param transition_id: Id of the transition - :type transition_id: str - :return: Transition response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.delete, f'/transitions/{transition_id}') - - def list_transition_executions( - self, - transition_id: str, - *, - status: Optional[Queryparam] = None, - execution_id: Optional[Queryparam] = None, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - sort_by: Optional[str] = None, - order: Optional[str] = None, - ) -> Dict: - """List executions in a transition, calls the GET /transitions/{transitionId}/executions endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_transition_executions('', '') - - :param transition_id: Id of the transition - :type transition_id: str - :param status: Statuses of the executions - :type status: Queryparam, optional - :param order: Order of the executions, either 'ascending' or 'descending' - :type order: str, optional - :param sort_by: the sorting variable of the executions, either 'endTime', or 'startTime' - :type sort_by: str, optional - :param execution_id: Ids of the executions - :type execution_id: Queryparam, optional - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Transition executions responses from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - url = f'/transitions/{transition_id}/executions' - params = { - 'status': status, - 'executionId': execution_id, - 'maxResults': max_results, - 'nextToken': next_token, - 'order': order, - 'sortBy': sort_by, - } - return self._make_request(requests.get, url, params=dictstrip(params)) - - def get_transition_execution(self, transition_id: str, execution_id: str) -> Dict: - """Get an execution of a transition, calls the GET /transitions/{transitionId}/executions/{executionId} endpoint - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_transition_execution('', '') - - :param transition_id: Id of the transition - :type transition_id: str - :param execution_id: Id of the executions - :type execution_id: str - :return: Transition execution responses from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - url = f'/transitions/{transition_id}/executions/{execution_id}' - return self._make_request(requests.get, url) - - def update_transition_execution( - self, - transition_id: str, - execution_id: str, - status: str, - *, - output: Optional[dict] = None, - error: Optional[dict] = None, - start_time: Optional[Union[str, datetime]] = None, - ) -> Dict: - """Ends the processing of the transition execution, - calls the PATCH /transitions/{transition_id}/executions/{execution_id} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> output = {...} - >>> client.update_transition_execution('', '', 'succeeded', output) - >>> error = {"message": 'The execution could not be processed due to ...'} - >>> client.update_transition_execution('', '', 'failed', error) - - :param transition_id: Id of the transition that performs the execution - :type transition_id: str - :param execution_id: Id of the execution to update - :type execution_id: str - :param status: Status of the execution 'succeeded|failed' - :type status: str - :param output: Output from the execution, required when status is 'succeded' - :type output: dict, optional - :param error: Error from the execution, required when status is 'failed', needs to contain 'message' - :type error: dict, optional - :param start_time: start time that will replace the original start time of the execution - :type start_time: str, optional - :return: Transition execution response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - - url = f'/transitions/{transition_id}/executions/{execution_id}' - body = { - 'status': status, - 'output': output, - 'error': error, - 'startTime': datetimestr(start_time), - } - return self._make_request(requests.patch, url, body=dictstrip(body)) - - def send_heartbeat(self, transition_id: str, execution_id: str) -> Dict: - """Send heartbeat for a manual execution to signal that we are still working on it. - Must be done at minimum once every 60 seconds or the transition execution will time out, - calls the POST /transitions/{transitionId}/executions/{executionId}/heartbeats endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.send_heartbeat('', '') - - :param transition_id: Id of the transition - :type transition_id: str - :param execution_id: Id of the transition execution - :type execution_id: str - :return: Empty response - :rtype: None - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - endpoint = f'/transitions/{transition_id}/executions/{execution_id}/heartbeats' - return self._make_request(requests.post, endpoint, body={}) - - def create_user(self, email: str, *, app_client_id, **optional_args) -> Dict: - """Creates a new user, calls the POST /users endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.create_user('', name='John Doe') - - :param email: Email to the new user - :type email: str - :param role_ids: List of roles to assign user - :type role_ids: str, optional - :return: User response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - body = { - 'email': email, - 'appClientId': app_client_id, - **optional_args, - } - if 'role_ids' in body: - body['roleIds'] = body.pop('role_ids') or [] - - return self._make_request(requests.post, '/users', body=dictstrip(body)) - - def list_users(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: - """List users, calls the GET /users endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.list_users() - - :param max_results: Maximum number of results to be returned - :type max_results: int, optional - :param next_token: A unique token for each page, use the returned token to retrieve the next page. - :type next_token: str, optional - :return: Users response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - params = { - 'maxResults': max_results, - 'nextToken': next_token, - } - return self._make_request(requests.get, '/users', params=params) - - def get_user(self, user_id: str) -> Dict: - """Get information about a specific user, calls the GET /users/{user_id} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_user('') - - :param user_id: Id of the user - :type user_id: str - :return: User response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.get, f'/users/{user_id}') - - def update_user(self, user_id: str, **optional_args) -> Dict: - """Updates a user, calls the PATCH /users/{userId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.update_user('', name='John Doe') - - :param user_id: Id of the user - :type user_id: str - :param role_ids: List of roles to assign user - :type role_ids: str, optional - :return: User response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - if 'role_ids' in optional_args: - optional_args['roleIds'] = optional_args.pop('role_ids') or [] - - return self._make_request(requests.patch, f'/users/{user_id}', body=optional_args) - - def delete_user(self, user_id: str) -> Dict: - """Delete the user with the provided user_id, calls the DELETE /users/{userId} endpoint. + next_token: Optional[str] = None, + ) -> Dict: + """List plans available, calls the GET /plans endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.delete_user('') + >>> client.list_plans() - :param user_id: Id of the user - :type user_id: str - :return: User response from REST API + :param owner: Organizations to retrieve plans from + :type owner: Queryparam, optional + :param max_results: Maximum number of results to be returned + :type max_results: int, optional + :param next_token: A unique token for each page, use the returned token to retrieve the next page. + :type next_token: str, optional + :return: Plans response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - return self._make_request(requests.delete, f'/users/{user_id}') + params = { + 'maxResults': max_results, + 'nextToken': next_token, + 'owner': owner, + } + return self._make_request(requests.get, '/plans', params=dictstrip(params)) - def create_workflow( - self, - specification: dict, - *, - email_config: Optional[dict] = None, - error_config: Optional[dict] = None, - completed_config: Optional[dict] = None, - metadata: Optional[dict] = None, - **optional_args, - ) -> Dict: - """Creates a new workflow, calls the POST /workflows endpoint. - Check out Cradl's tutorials for more info on how to create a workflow. + def create_secret(self, data: dict, **optional_args) -> Dict: + """Creates a secret, calls the POST /secrets endpoint. >>> from cradl.client import Client - >>> from pathlib import Path >>> client = Client() - >>> specification = {'language': 'ASL', 'version': '1.0.0', 'definition': {...}} - >>> error_config = {'email': ''} - >>> client.create_workflow(specification, error_config=error_config) - - :param specification: Specification of the workflow, \ - currently supporting ASL: https://states-language.net/spec.html - :type specification: dict - :param email_config: Create workflow with email input - :type email_config: dict, optional - :param error_config: Configuration of error handler - :type error_config: dict, optional - :param completed_config: Configuration of a job to run whenever a workflow execution ends - :type completed_config: dict, optional - :param metadata: Dictionary that can be used to store additional information - :type metadata: dict, optional - :param name: Name of the workflow + >>> data = {'username': '', 'password': ''} + >>> client.create_secret(data, description='') + + :param data: Dict containing the data you want to keep secret + :type data: str + :param name: Name of the secret :type name: str, optional - :param description: Description of the workflow + :param description: Description of the secret :type description: str, optional - :return: Workflow response from REST API + :return: Secret response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - body = dictstrip({ - 'completedConfig': completed_config, - 'emailConfig': email_config, - 'errorConfig': error_config, - 'metadata': metadata, - 'specification': specification, - }) - body.update(**optional_args) - - return self._make_request(requests.post, '/workflows', body=body) + body = { + 'data': data, + **optional_args, + } + return self._make_request(requests.post, '/secrets', body=body) - def list_workflows(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: - """List workflows, calls the GET /workflows endpoint. + def list_secrets(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: + """List secrets available, calls the GET /secrets endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.list_workflows() + >>> client.list_secrets() :param max_results: Maximum number of results to be returned :type max_results: int, optional :param next_token: A unique token for each page, use the returned token to retrieve the next page. :type next_token: str, optional - :return: Workflows response from REST API + :return: Secrets response from REST API without the username of each secret :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ @@ -2239,248 +1224,154 @@ def list_workflows(self, *, max_results: Optional[int] = None, next_token: Optio 'maxResults': max_results, 'nextToken': next_token, } - return self._make_request(requests.get, '/workflows', params=params) - - def get_workflow(self, workflow_id: str) -> Dict: - """Get the workflow with the provided workflow_id, calls the GET /workflows/{workflowId} endpoint. - - >>> from cradl.client import Client - >>> client = Client() - >>> client.get_workflow('') - - :param workflow_id: Id of the workflow - :type workflow_id: str - :return: Workflow response from REST API - :rtype: dict - - :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ - :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` - """ - return self._make_request(requests.get, f'/workflows/{workflow_id}') + return self._make_request(requests.get, '/secrets', params=params) - def update_workflow( - self, - workflow_id: str, - *, - error_config: Optional[dict] = None, - completed_config: Optional[dict] = None, - metadata: Optional[dict] = None, - status: Optional[str] = None, - **optional_args, - ) -> Dict: - """Updates a workflow, calls the PATCH /workflows/{workflowId} endpoint. + def update_secret(self, secret_id: str, *, data: Optional[dict] = None, **optional_args) -> Dict: + """Updates a secret, calls the PATCH /secrets/secretId endpoint. - >>> import json - >>> from pathlib import Path >>> from cradl.client import Client >>> client = Client() - >>> client.update_workflow('', name='', description='') + >>> data = {'username': '', 'password': ''} + >>> client.update_secret('', data, description='') - :param workflow_id: Id of the workflow - :param error_config: Configuration of error handler - :type error_config: dict, optional - :param completed_config: Configuration of a job to run whenever a workflow execution ends - :type completed_config: dict, optional - :param metadata: Dictionary that can be used to store additional information - :type metadata: dict, optional - :type name: str - :param name: Name of the workflow + :param secret_id: Id of the secret + :type secret_id: str + :param data: Dict containing the data you want to keep secret + :type data: dict, optional + :param name: Name of the secret :type name: str, optional - :param description: Description of the workflow + :param description: Description of the secret :type description: str, optional - :param email_config: Update workflow with email input - :type email_config: dict, optional - :param status: Set status of workflow to development or production - :type status: str, optional - :return: Workflow response from REST API + :return: Secret response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - body = dictstrip({ - 'completedConfig': completed_config, - 'errorConfig': error_config, - 'metadata': metadata, - 'status': status, - }) - - if 'email_config' in optional_args: - optional_args['emailConfig'] = optional_args.pop('email_config') + body = dictstrip({'data': data}) body.update(**optional_args) + return self._make_request(requests.patch, f'/secrets/{secret_id}', body=body) - return self._make_request(requests.patch, f'/workflows/{workflow_id}', body=body) - - def delete_workflow(self, workflow_id: str) -> Dict: - """Delete the workflow with the provided workflow_id, calls the DELETE /workflows/{workflowId} endpoint. + def delete_secret(self, secret_id: str) -> Dict: + """Delete the secret with the provided secret_id, calls the DELETE /secrets/{secretId} endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.delete_workflow('') + >>> client.delete_secret('') - :param workflow_id: Id of the workflow - :type workflow_id: str - :return: Workflow response from REST API + :param secret_id: Id of the secret + :type secret_id: str + :return: Secret response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - return self._make_request(requests.delete, f'/workflows/{workflow_id}') + return self._make_request(requests.delete, f'/secrets/{secret_id}') - def execute_workflow(self, workflow_id: str, content: dict) -> Dict: - """Start a workflow execution, calls the POST /workflows/{workflowId}/executions endpoint. + def create_user(self, email: str, *, role_ids=None, **optional_args) -> Dict: + """Creates a new user, calls the POST /users endpoint. >>> from cradl.client import Client - >>> from pathlib import Path >>> client = Client() - >>> content = {...} - >>> client.execute_workflow('', content) + >>> client.create_user('', name='John Doe') - :param workflow_id: Id of the workflow - :type workflow_id: str - :param content: Input to the first step of the workflow - :type content: dict - :return: Workflow execution response from REST API + :param email: Email to the new user + :type email: str + :param role_ids: List of roles to assign user + :type role_ids: list, optional + :return: User response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - endpoint = f'/workflows/{workflow_id}/executions' - return self._make_request(requests.post, endpoint, body={'input': content}) + body = { + 'email': email, + 'roleIds': role_ids, + **optional_args, + } - def list_workflow_executions( - self, - workflow_id: str, - *, - status: Optional[Queryparam] = None, - sort_by: Optional[str] = None, - order: Optional[str] = None, - max_results: Optional[int] = None, - next_token: Optional[str] = None, - from_start_time: Optional[Union[str, datetime]] = None, - to_start_time: Optional[Union[str, datetime]] = None, - ) -> Dict: - """List executions in a workflow, calls the GET /workflows/{workflowId}/executions endpoint. + return self._make_request(requests.post, '/users', body=dictstrip(body)) + + def list_users(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: + """List users, calls the GET /users endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.list_workflow_executions('', '') + >>> client.list_users() - :param workflow_id: Id of the workflow - :type workflow_id: str - :param order: Order of the executions, either 'ascending' or 'descending' - :type order: str, optional - :param sort_by: the sorting variable of the executions, either 'endTime', or 'startTime' - :type sort_by: str, optional - :param status: Statuses of the executions - :type status: Queryparam, optional :param max_results: Maximum number of results to be returned :type max_results: int, optional :param next_token: A unique token for each page, use the returned token to retrieve the next page. :type next_token: str, optional - :param from_start_time: Specify a datetime range for start_time with from_start_time as lower bound - :type from_start_time: str or datetime, optional - :param to_start_time: Specify a datetime range for start_time with to_start_time as upper bound - :type to_start_time: str or datetime, optional - :return: Workflow executions responses from REST API + :return: Users response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - url = f'/workflows/{workflow_id}/executions' params = { - 'status': status, - 'order': order, - 'sortBy': sort_by, 'maxResults': max_results, 'nextToken': next_token, } + return self._make_request(requests.get, '/users', params=params) - if any([from_start_time, to_start_time]): - params['fromStartTime'] = datetimestr(from_start_time) - params['toStartTime'] = datetimestr(to_start_time) - - return self._make_request(requests.get, url, params=params) - - def get_workflow_execution(self, workflow_id: str, execution_id: str) -> Dict: - """Get a workflow execution, calls the GET /workflows/{workflow_id}/executions/{execution_id} endpoint. + def get_user(self, user_id: str) -> Dict: + """Get information about a specific user, calls the GET /users/{user_id} endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.get_workflow_execution('', '') + >>> client.get_user('') - :param workflow_id: Id of the workflow that performs the execution - :type workflow_id: str - :param execution_id: Id of the execution to get - :type execution_id: str - :return: Workflow execution response from REST API + :param user_id: Id of the user + :type user_id: str + :return: User response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - url = f'/workflows/{workflow_id}/executions/{execution_id}' - return self._make_request(requests.get, url) + return self._make_request(requests.get, f'/users/{user_id}') - def update_workflow_execution( - self, - workflow_id: str, - execution_id: str, - *, - next_transition_id: Optional[str] = None, - status: Optional[str] = None, - ) -> Dict: - """Retry or end the processing of a workflow execution, - calls the PATCH /workflows/{workflow_id}/executions/{execution_id} endpoint. + def update_user(self, user_id: str, **optional_args) -> Dict: + """Updates a user, calls the PATCH /users/{userId} endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.update_workflow_execution('', '', '') + >>> client.update_user('', name='John Doe') - :param workflow_id: Id of the workflow that performs the execution - :type workflow_id: str - :param execution_id: Id of the execution to update - :type execution_id: str - :param next_transition_id: the next transition to transition into, to end the workflow-execution, \ - use: cradl:transition:commons-failed - :type next_transition_id: str, optional - :param status: Update the execution with this status, can only update from succeeded to completed and vice versa - :type status: str, optional - :return: Workflow execution response from REST API + :param user_id: Id of the user + :type user_id: str + :param role_ids: List of roles to assign user + :type role_ids: list, optional + :return: User response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - url = f'/workflows/{workflow_id}/executions/{execution_id}' - body = { - 'nextTransitionId': next_transition_id, - 'status': status, - } - return self._make_request(requests.patch, url, body=dictstrip(body)) + if 'role_ids' in optional_args: + optional_args['roleIds'] = optional_args.pop('role_ids') or [] + + return self._make_request(requests.patch, f'/users/{user_id}', body=optional_args) - def delete_workflow_execution(self, workflow_id: str, execution_id: str) -> Dict: - """Deletes the execution with the provided execution_id from workflow_id, - calls the DELETE /workflows/{workflowId}/executions/{executionId} endpoint. + def delete_user(self, user_id: str) -> Dict: + """Delete the user with the provided user_id, calls the DELETE /users/{userId} endpoint. >>> from cradl.client import Client >>> client = Client() - >>> client.delete_workflow_execution('', '') + >>> client.delete_user('') - :param workflow_id: Id of the workflow - :type workflow_id: str - :param execution_id: Id of the execution - :type execution_id: str - :return: WorkflowExecution response from REST API + :param user_id: Id of the user + :type user_id: str + :return: User response from REST API :rtype: dict :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` """ - return self._make_request(requests.delete, f'/workflows/{workflow_id}/executions/{execution_id}') + return self._make_request(requests.delete, f'/users/{user_id}') def create_role(self, permissions: List[Dict], **optional_args) -> Dict: """Creates a role, calls the POST /roles endpoint. @@ -2534,7 +1425,6 @@ def update_role(self, role_id: str, *, permissions: Optional[List[Dict]] = None, body.update(**optional_args) return self._make_request(requests.patch, f'/roles/{role_id}', body=body) - def list_roles(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: """List roles available, calls the GET /roles endpoint. @@ -2647,8 +1537,24 @@ def create_validation( body.update(**optional_args) return self._make_request(requests.post, '/validations', body=body) + def update_validation(self, validation_id: str, *, config: Optional[dict] = None, **optional_args) -> Dict: + """Update the validation with the provided validation_id, calls the PATCH /validations/{validation_id} endpoint. + + :param validation_id: Id of the validation + :type validation_id: str + :param config: New configuration for the validation + :type config: str + :return: Validation response from REST API + :rtype: dict + + :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + """ + body = {'config': config, **optional_args} + return self._make_request(requests.patch, f'/validations/{validation_id}', body=body) + def delete_validation(self, validation_id: str) -> Dict: - """Delete the validation with the provided validation_id, calls the DELETE /validations/{validation_id} endpoint. + """Delete the validation defined by validation_id, calls the DELETE /validations/{validation_id} endpoint. >>> from cradl.client import Client >>> client = Client() @@ -2670,7 +1576,7 @@ def create_validation_task( input: dict, *, metadata: Optional[dict] = None, - agent_run_id: str = None, + agent_run_id: Optional[str] = None, **optional_args, ) -> Dict: """Creates a validation, calls the POST /validations endpoint. @@ -2702,7 +1608,7 @@ def create_validation_task( def update_validation_task( self, validation_id: str, - validation_task_id: str, + task_id: str, output: dict, status: str, *, @@ -2713,8 +1619,8 @@ def update_validation_task( :param validation_id: Id of the validation :type validation_id: str - :param validation_task_id: Id of the validation task - :type validation_task_id: str + :param task_id: Id of the validation task + :type task_id: str :param output: Dictionary that can be used to store additional information :type output: dict, required if status is present, otherwise optional :param status: Status of the task @@ -2735,7 +1641,7 @@ def update_validation_task( body.update(**optional_args) return self._make_request( requests_fn=requests.patch, - path=f'/validations/{validation_id}/tasks/{validation_task_id}', + path=f'/validations/{validation_id}/tasks/{task_id}', body=body, ) @@ -2770,6 +1676,21 @@ def list_validation_tasks( }) return self._make_request(requests.get, f'/validations/{validation_id}/tasks', params=dictstrip(params)) + def get_validation_task(self, validation_id: str, task_id: str) -> Dict: + """Get a validation, calls the GET /validations/{validationId}/tasks/{taskId} endpoint. + + :param validation_id: Id of the validation + :type validation_id: str + :param task_id: Id of the validation task + :type task_id: str + :return: ValidationTask response from REST API + :rtype: dict + + :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + """ + return self._make_request(requests.get, f'/validations/{validation_id}/tasks/{task_id}') + def create_agent( self, *, @@ -2778,15 +1699,15 @@ def create_agent( metadata: Optional[dict] = None, resource_ids: Optional[list[str]] = None, ) -> Dict: - """Get agent, calls the POST /agents endpoint. + """Create agent, calls the POST /agents endpoint. - :param name: Name of the dataset + :param name: Name of the agent :type name: str, optional - :param description: Description of the dataset + :param description: Description of the agent :type description: str, optional :param metadata: Dictionary that can be used to store additional information :type metadata: dict, optional - :param resource_ids: Description of the dataset + :param resource_ids: List of resource ids for hooks, actions and validations in the agent :type resource_ids: list[str], optional :return: Agent response from REST API :rtype: dict @@ -2823,17 +1744,17 @@ def update_agent( resource_ids: Optional[list[str]] = None, **optional_args, ) -> Dict: - """Get agent, calls the PATCH /agents/{agentId} endpoint. + """Update agent, calls the PATCH /agents/{agentId} endpoint. :param agent_id: Id of the agent :type agent_id: str - :param name: Name of the dataset + :param name: Name of the agent :type name: str, optional - :param description: Description of the dataset + :param description: Description of the agent :type description: str, optional :param metadata: Dictionary that can be used to store additional information :type metadata: dict, optional - :param resource_ids: Description of the dataset + :param resource_ids: List of resource ids for hooks, actions and validations in the agent :type resource_ids: list[str], optional :return: Agent response from REST API :rtype: dict @@ -2880,11 +1801,13 @@ def list_agents(self, *, max_results: Optional[int] = None, next_token: Optional } return self._make_request(requests.get, '/agents', params=params) - def create_agent_run(self, agent_id: str, *, variables: dict = None) -> Dict: + def create_agent_run(self, agent_id: str, *, variables: Optional[dict] = None) -> Dict: """Create agent run, calls the POST /agents/{agentId}/runs endpoint. :param agent_id: Id of the agent :type agent_id: str + :param variables: additional variables to pass to the agent run + :type agent_id: dict :return: Agent response from REST API :rtype: dict @@ -2948,6 +1871,23 @@ def get_agent_run(self, agent_id: str, run_id: str, *, get_variables: bool = Fal ).decode()) return agent_run + def update_agent_run(self, agent_id: str, run_id: str, status: str) -> Dict: + """Update agent run, calls the PATCH /agents/{agentId}/runs/{runId} endpoint. + + :param agent_id: Id of the agent + :type agent_id: str + :param run_id: Id of the run + :type run_id: str + :param status: New status of the agent run + :type status: str + :return: AgentRun response from REST API + :rtype: dict + + :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + """ + return self._make_request(requests.patch, f'/agents/{agent_id}/runs/{run_id}', body={'status': status}) + def delete_agent_run(self, agent_id: str, run_id: str) -> Dict: """Delete agent run, calls the DELETE /agents/{agentId}/runs/{runId} endpoint. @@ -3032,19 +1972,19 @@ def create_hook( name: Optional[str] = None, ) -> Dict: - """Get hook, calls the POST /hooks endpoint. + """Create hook, calls the POST /hooks endpoint. :param trigger: What will trigger the hook to be run :type trigger: str :param function_id: Id of the function to evaluate whether to run the false or true action - :type function_id: str + :type function_id: str, optional :param true_action_id: Id of the action that will happen when hook run evaluates to true - :type true_action_id: str + :type true_action_id: str, optional :param enabled: If the hook is enabled or not :type enabled: bool, optional - :param name: Name of the dataset + :param name: Name of the hook :type name: str, optional - :param description: Description of the dataset + :param description: Description of the hook :type description: str, optional :param config: Dictionary that can be sent as input to true_action_id and false_action_id :type config: dict, optional @@ -3097,9 +2037,9 @@ def update_hook( **optional_args, ) -> Dict: - """Get hook, calls the PATCH /hooks/{hookId} endpoint. + """Update hook, calls the PATCH /hooks/{hookId} endpoint. - :param hook_id: Id of the hook the hook belongs to + :param hook_id: Id of the hook :type hook_id: str :param trigger: What will trigger the hook to be run :type trigger: str, optional @@ -3107,9 +2047,9 @@ def update_hook( :type true_action_id: str, optional :param enabled: If the hook is enabled or not :type enabled: bool, optional - :param name: Name of the dataset + :param name: Name of the hook :type name: str, optional - :param description: Description of the dataset + :param description: Description of the hook :type description: str, optional :param config: Dictionary that can be sent as input to true_action_id and false_action_id :type config: dict, optional @@ -3166,7 +2106,7 @@ def list_hook_runs( return self._make_request(requests.get, f'/hooks/{hook_id}/runs', params=dictstrip(params)) def get_hook_run(self, hook_id: str, run_id: str) -> Dict: - """Get hook, calls the GET /hooks/{hookId}/runs/{runId} endpoint. + """Get hook run, calls the GET /hooks/{hookId}/runs/{runId} endpoint. :param hook_id: Id of the hook :type hook_id: str @@ -3180,6 +2120,21 @@ def get_hook_run(self, hook_id: str, run_id: str) -> Dict: """ return self._make_request(requests.get, f'/hooks/{hook_id}/runs/{run_id}') + def update_hook_run(self, hook_id: str, run_id: str, **optional_args) -> Dict: + """Update hook run, calls the PATCH /hooks/{hookId}/runs/{runId} endpoint. + + :param hook_id: Id of the hook + :type hook_id: str + :param run_id: Id of the run + :type run_id: str + :return: Hook response from REST API + :rtype: dict + + :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + """ + return self._make_request(requests.patch, f'/hooks/{hook_id}/runs/{run_id}', body=optional_args) + def list_actions(self, *, max_results: Optional[int] = None, next_token: Optional[str] = None) -> Dict: """List actions available, calls the GET /actions endpoint. @@ -3230,11 +2185,11 @@ def create_action( :type function_id: str :param enabled: If the action is enabled or not :type enabled: bool, optional - :param name: Name of the dataset + :param name: Name of the action :type name: str, optional - :param description: Description of the dataset + :param description: Description of the action :type description: str, optional - :param config: Dictionary that can be sent as input to true_action_id and false_action_id + :param config: Dictionary that can be sent as input to the function :type config: dict, optional :param metadata: Dictionary that can be used to store additional information :type metadata: dict, optional @@ -3282,24 +2237,24 @@ def update_action( **optional_args, ) -> Dict: - """Get action, calls the PATCH /actions/{actionId} endpoint. + """Update action, calls the PATCH /actions/{actionId} endpoint. - :param action_id: Id of the action the action belongs to + :param action_id: Id of the action :type action_id: str :param enabled: If the action is enabled or not :type enabled: bool, optional :param function_id: Id of the function to run - :type function_id: str - :param name: Name of the dataset + :type function_id: str, optional + :param name: Name of the action :type name: str, optional - :param description: Description of the dataset + :param description: Description of the action :type description: str, optional - :param config: Dictionary that can be sent as input to true_action_id and false_action_id + :param config: Dictionary that can be sent as input to the function :type config: dict, optional :param metadata: Dictionary that can be used to store additional information :type metadata: dict, optional :param secret_id: Id of the secret to expand as input to functionId - :type secret_id: str + :type secret_id: str, optional :return: Action response from REST API :rtype: dict @@ -3393,3 +2348,34 @@ def create_action_run( }) return self._make_request(requests.post, f'/actions/{action_id}/runs', body=body) + def update_action_run( + self, + action_id: str, + run_id: str, + *, + output: Optional[dict] = None, + status: Optional[str] = None, + **optional_args, + ) -> Dict: + """Update action run, calls the PATCH /actions/{actionId}/runs/{runId} endpoint. + + :param action_id: Id of the action + :type action_id: str + :param run_id: Id of the run + :type run_id: str + :param output: output dictionary of the action run + :type output: dict + :param status: New status of the action run + :type status: str + :return: Action response from REST API + :rtype: dict + + :raises: :py:class:`~cradl.InvalidCredentialsException`, :py:class:`~cradl.TooManyRequestsException`,\ + :py:class:`~cradl.LimitExceededException`, :py:class:`requests.exception.RequestException` + """ + body = dictstrip({ + 'output': output, + 'status': status, + }) + body.update(**optional_args) + return self._make_request(requests.patch, f'/actions/{action_id}/runs/{run_id}', body=body) diff --git a/src/cradl/credentials.py b/src/cradl/credentials.py index 42a331b..954dbb2 100644 --- a/src/cradl/credentials.py +++ b/src/cradl/credentials.py @@ -4,10 +4,9 @@ from base64 import b64decode from os.path import exists, expanduser from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import Optional, Tuple -import requests -from requests.auth import HTTPBasicAuth +import requests # type: ignore from .log import setup_logging @@ -38,9 +37,9 @@ def __init__( client_secret: str, auth_endpoint: str, api_endpoint: str, - cached_profile: str = None, + cached_profile: Optional[str] = None, cache_path: Path = Path(expanduser('~/.cradl/token-cache.json')), - access_token: str = None, + access_token: Optional[str] = None, ): if not all([client_id, client_secret, auth_endpoint, api_endpoint]): raise MissingCredentials @@ -95,7 +94,7 @@ def _get_client_credentials(self) -> Tuple[str, int]: else: url = f'https://{self.auth_endpoint}/token?grant_type=client_credentials' headers = {'Content-Type': 'application/x-www-form-urlencoded'} - auth = HTTPBasicAuth(self.client_id, self.client_secret) + auth = requests.auth.HTTPBasicAuth(self.client_id, self.client_secret) response = requests.post(url, headers=headers, auth=auth) response.raise_for_status() @@ -133,7 +132,7 @@ def write_token_to_cache(cached_profile, token, cache_path: Path): cache_path.write_text(json.dumps(cache, indent=2)) -def read_from_environ() -> List[Optional[str]]: +def read_from_environ() -> dict[str, Optional[str]]: """Read the following environment variables and return them: - CRADL_CLIENT_ID - CRADL_CLIENT_SECRET @@ -141,8 +140,8 @@ def read_from_environ() -> List[Optional[str]]: - CRADL_API_ENDPOINT - CRADL_ACCESS_TOKEN (optional) - :return: List of client_id, client_secret, auth_endpoint, api_endpoint - :rtype: List[Optional[str]]""" + :return: Dictionary of client_id, client_secret, auth_endpoint, api_endpoint, cached_profile + :rtype: dict[str, Optional[str]]""" return dict( access_token=os.environ.get('CRADL_ACCESS_TOKEN'), @@ -153,8 +152,10 @@ def read_from_environ() -> List[Optional[str]]: ) -def read_from_file(credentials_path: str = expanduser('~/.cradl/credentials.json'), - profile: str = 'default') -> List[Optional[str]]: +def read_from_file( + credentials_path: str = expanduser('~/.cradl/credentials.json'), + profile: str = 'default', +) -> dict[str, Optional[str]]: """Read a json file and return credentials from it. Defaults to '~/.cradl/credentials.json'. :param credentials_path: Path to read credentials from. @@ -162,8 +163,8 @@ def read_from_file(credentials_path: str = expanduser('~/.cradl/credentials.json :param profile: profile to read credentials from. :type profile: str - :return: List of client_id, client_secret, auth_endpoint, api_endpoint, cached_profile - :rtype: List[Optional[str]]""" + :return: Dictionary of client_id, client_secret, auth_endpoint, api_endpoint, cached_profile + :rtype: dict[str, Optional[str]]""" if not exists(credentials_path): raise MissingCredentials @@ -196,13 +197,13 @@ def guess_credentials(profile=None) -> Credentials: if profile: try: - return Credentials(**read_from_file(profile=profile)) - except: + return Credentials(**read_from_file(profile=profile)) # type: ignore + except Exception: raise MissingCredentials(f'Could not find valid credentials for {profile} in ~/.cradl/credentials.json') for guesser in [read_from_environ, read_from_file]: try: - return Credentials(**guesser()) # Will raise TypeError if incomplete + return Credentials(**guesser()) # type: ignore except (TypeError, MissingCredentials): continue raise MissingCredentials diff --git a/tests/service.py b/tests/service.py index 21171fe..371afac 100644 --- a/tests/service.py +++ b/tests/service.py @@ -6,10 +6,6 @@ def create_app_client_id(): return f'las:app-client:{uuid4().hex}' -def create_asset_id(): - return f'las:asset:{uuid4().hex}' - - def create_dataset_id(): return f'las:dataset:{uuid4().hex}' @@ -38,22 +34,10 @@ def create_prediction_id(): return f'las:prediction:{uuid4().hex}' -def create_deployment_environment_id(): - return f'las:deployment-environment:{uuid4().hex}' - - def create_plan_id(): return f'las:plan:{uuid4().hex}' -def create_data_bundle_id(): - return f'las:model-data-bundle:{uuid4().hex}' - - -def create_training_id(): - return f'las:model-training:{uuid4().hex}' - - def create_organization_id(): return f'las:organization:{uuid4().hex}' @@ -62,34 +46,14 @@ def create_secret_id(): return f'las:secret:{uuid4().hex}' -def create_transition_id(): - return f'las:transition:{uuid4().hex}' - - -def create_transition_execution_id(): - return f'las:transition-execution:{uuid4().hex}' - - def create_user_id(): return f'las:user:{uuid4().hex}' -def create_workflow_id(): - return f'las:workflow:{uuid4().hex}' - - def create_role_id(): return f'las:role:{uuid4().hex}' -def create_workflow_execution_id(): - return f'las:workflow-execution:{uuid4().hex}' - - -def create_transformation_id(): - return f'las:dataset-transformation:{uuid4().hex}' - - def create_error_config(): return {'email': 'foo@bar.com'} @@ -144,3 +108,39 @@ def postprocess_config(): def document_path(): return Path(__file__) + + +def create_action_id(): + return f'cradl:action:{uuid4().hex}' + + +def create_action_run_id(): + return f'cradl:action-run:{uuid4().hex}' + + +def create_hook_id(): + return f'cradl:hook:{uuid4().hex}' + + +def create_hook_run_id(): + return f'cradl:hook-run:{uuid4().hex}' + + +def create_validation_id(): + return f'cradl:validation:{uuid4().hex}' + + +def create_validation_task_id(): + return f'cradl:validation-task:{uuid4().hex}' + + +def create_agent_id(): + return f'cradl:agent:{uuid4().hex}' + + +def create_agent_run_id(): + return f'cradl:agent-run:{uuid4().hex}' + + +def create_function_id(): + return f'cradl:function:{uuid4().hex}' diff --git a/tests/test_action_runs.py b/tests/test_action_runs.py new file mode 100644 index 0000000..85417da --- /dev/null +++ b/tests/test_action_runs.py @@ -0,0 +1,47 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_action_run(run): + assert 'runId' in run, 'Missing runId in action run' + assert 'actionId' in run, 'Missing actionId in action run' + + +def test_create_action_run(client: Client): + action_id = service.create_action_id() + run = client.create_action_run(action_id, input={'foo': 'bar'}) + assert_action_run(run) + + +def test_list_action_runs(client: Client): + action_id = service.create_action_id() + response = client.list_action_runs(action_id) + assert 'runs' in response, 'Missing runs in response' + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_action_runs_with_pagination(client: Client, max_results, next_token): + action_id = service.create_action_id() + response = client.list_action_runs(action_id, max_results=max_results, next_token=next_token) + assert 'runs' in response, 'Missing runs in response' + + +def test_get_action_run(client: Client): + action_id = service.create_action_id() + run_id = service.create_action_run_id() + run = client.get_action_run(action_id, run_id) + assert_action_run(run) + + +def test_update_action_run(client: Client): + action_id = service.create_action_id() + run_id = service.create_action_run_id() + run = client.update_action_run(action_id, run_id, status='succeeded', output={'result': 'ok'}) + assert_action_run(run) diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 0000000..5a5b08b --- /dev/null +++ b/tests/test_actions.py @@ -0,0 +1,59 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_action(action): + assert 'actionId' in action, 'Missing actionId in action' + assert 'functionId' in action, 'Missing functionId in action' + + +def test_create_action(client: Client): + action = client.create_action( + function_id=service.create_function_id(), + name='Test Action', + description='A test action', + config={'foo': 'bar'}, + ) + assert_action(action) + + +def test_list_actions(client: Client): + response = client.list_actions() + assert 'actions' in response, 'Missing actions in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for action in response['actions']: + assert_action(action) + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_actions_with_pagination(client: Client, max_results, next_token): + response = client.list_actions(max_results=max_results, next_token=next_token) + assert 'actions' in response, 'Missing actions in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for action in response['actions']: + assert_action(action) + + +def test_get_action(client: Client): + action_id = service.create_action_id() + action = client.get_action(action_id) + assert_action(action) + + +def test_update_action(client: Client): + action_id = service.create_action_id() + action = client.update_action(action_id, name='Updated Action', enabled=True) + assert_action(action) + + +def test_delete_action(client: Client): + action_id = service.create_action_id() + action = client.delete_action(action_id) + assert_action(action) diff --git a/tests/test_agent_runs.py b/tests/test_agent_runs.py new file mode 100644 index 0000000..657c351 --- /dev/null +++ b/tests/test_agent_runs.py @@ -0,0 +1,47 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_agent_run(run): + assert 'runId' in run, 'Missing runId in agent run' + assert 'agentId' in run, 'Missing agentId in agent run' + + +def test_create_agent_run(client: Client): + agent_id = service.create_agent_id() + run = client.create_agent_run(agent_id, variables={'foo': 'bar'}) + assert_agent_run(run) + + +def test_list_agent_runs(client: Client): + agent_id = service.create_agent_id() + response = client.list_agent_runs(agent_id) + assert 'runs' in response, 'Missing runs in response' + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_agent_runs_with_pagination(client: Client, max_results, next_token): + agent_id = service.create_agent_id() + response = client.list_agent_runs(agent_id, max_results=max_results, next_token=next_token) + assert 'runs' in response, 'Missing runs in response' + + +def test_get_agent_run(client: Client): + agent_id = service.create_agent_id() + run_id = service.create_agent_run_id() + run = client.get_agent_run(agent_id, run_id) + assert_agent_run(run) + + +def test_update_agent_run(client: Client): + agent_id = service.create_agent_id() + run_id = service.create_agent_run_id() + run = client.update_agent_run(agent_id, run_id, status='archived') + assert_agent_run(run) diff --git a/tests/test_agents.py b/tests/test_agents.py new file mode 100644 index 0000000..df4f4cc --- /dev/null +++ b/tests/test_agents.py @@ -0,0 +1,63 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_agent(agent): + assert 'agentId' in agent, 'Missing agentId in agent' + assert 'resourceIds' in agent, 'Missing resourceIds in agent' + + +def test_create_agent(client: Client): + agent = client.create_agent( + name='Test Agent', + description='A test agent', + resource_ids=[ + service.create_action_id(), + service.create_hook_id(), + service.create_validation_id(), + service.create_model_id(), + ], + ) + assert_agent(agent) + + +def test_list_agents(client: Client): + response = client.list_agents() + assert 'agents' in response, 'Missing agents in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for agent in response['agents']: + assert_agent(agent) + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_agents_with_pagination(client: Client, max_results, next_token): + response = client.list_agents(max_results=max_results, next_token=next_token) + assert 'agents' in response, 'Missing agents in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for agent in response['agents']: + assert_agent(agent) + + +def test_get_agent(client: Client): + agent_id = service.create_agent_id() + agent = client.get_agent(agent_id) + assert_agent(agent) + + +def test_update_agent(client: Client): + agent_id = service.create_agent_id() + agent = client.update_agent(agent_id, name='Updated Agent', resource_ids=[service.create_action_id()]) + assert_agent(agent) + + +def test_delete_agent(client: Client): + agent_id = service.create_agent_id() + agent = client.delete_agent(agent_id) + assert_agent(agent) diff --git a/tests/test_app_clients.py b/tests/test_app_clients.py index a688885..49f52c0 100644 --- a/tests/test_app_clients.py +++ b/tests/test_app_clients.py @@ -6,12 +6,14 @@ from . import service, util +@pytest.mark.skip(reason='This resource is currently not intended for use with the new API') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) def test_create_app_client(client: Client, name_and_description): response = client.create_app_client(role_ids=[service.create_role_id()], **name_and_description) assert_app_client(response) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) def test_create_app_client_without_secret(client: Client, name_and_description): response = client.create_app_client( @@ -25,11 +27,13 @@ def test_create_app_client_without_secret(client: Client, name_and_description): assert_app_client(response) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_app_clients(client: Client): response = client.list_app_clients() assert 'appClients' in response, 'Missing appClients in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') @pytest.mark.parametrize('max_results,next_token', [ (random.randint(1, 100), None), (random.randint(1, 100), 'foo'), @@ -41,12 +45,14 @@ def test_list_app_clients_with_pagination(client: Client, max_results, next_toke assert 'nextToken' in response, 'Missing nextToken in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_delete_app_client(client: Client): app_client_id = service.create_app_client_id() response = client.delete_app_client(app_client_id) assert_app_client(response) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(at_least_one=True)) def test_update_app_client(client: Client, name_and_description): response = client.update_app_client( @@ -63,8 +69,8 @@ def assert_app_client(response): assert 'description' in response, 'Missing description in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_get_app_client(client: Client): app_client_id = service.create_app_client_id() response = client.get_app_client(app_client_id) assert_app_client(response) - \ No newline at end of file diff --git a/tests/test_assets.py b/tests/test_assets.py deleted file mode 100644 index 59f7804..0000000 --- a/tests/test_assets.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -import random -from pathlib import Path - -import pytest -from cradl.client import Client - -from . import service, util - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) -def test_create_asset(client: Client, name_and_description): - content = Path('tests/remote_component.js').read_bytes() - response = client.create_asset(content, **name_and_description) - assert 'assetId' in response, 'Missing assetId in response' - - -def test_list_assets(client: Client): - response = client.list_assets() - logging.info(response) - assert 'assets' in response, 'Missing assets in response' - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_assets_with_pagination(client: Client, max_results, next_token): - response = client.list_assets(max_results=max_results, next_token=next_token) - assert 'assets' in response, 'Missing assets in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def test_get_asset(client: Client): - asset_id = service.create_asset_id() - response = client.get_asset(asset_id) - assert 'assetId' in response, 'Missing assetId in response' - assert 'content' in response, 'Missing content in response' - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) -def test_update_asset(client: Client, name_and_description): - asset_id = service.create_asset_id() - content = Path('tests/remote_component.js').read_bytes() - response = client.update_asset(asset_id, content=content, **name_and_description) - assert 'assetId' in response, 'Missing assetId in response' - - -def test_delete_asset(client: Client): - asset_id = service.create_asset_id() - response = client.delete_asset(asset_id) - assert 'assetId' in response, 'Missing assetId in response' - assert 'content' in response, 'Missing content in response' - diff --git a/tests/test_client.py b/tests/test_client.py index 8e82e83..d9bfb22 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -30,24 +30,25 @@ def test_invalid_credentials( client_with_access_token, ): - client = client_with_access_token + _client = client_with_access_token consent_id = service.create_consent_id() document_id = service.create_document_id() model_id = service.create_model_id() + api_endpoint = _client.credentials.api_endpoint with requests_mock.Mocker() as m: - m.post('/'.join([client.credentials.api_endpoint, 'documents']), status_code=error_code, content=error_content) - m.post('/'.join([client.credentials.api_endpoint, 'predictions']), status_code=error_code, content=error_content) - m.delete('/'.join([client.credentials.api_endpoint, 'documents']), status_code=error_code, content=error_content) + m.post('/'.join([api_endpoint, 'documents']), status_code=error_code, content=error_content) + m.post('/'.join([api_endpoint, 'predictions']), status_code=error_code, content=error_content) + m.delete('/'.join([api_endpoint, 'documents']), status_code=error_code, content=error_content) with pytest.raises(error_name): - client.create_document(content, consent_id=consent_id) + _client.create_document(content, consent_id=consent_id) with pytest.raises(error_name): - client.create_prediction(document_id, model_id) + _client.create_prediction(document_id, model_id) with pytest.raises(error_name): - client.delete_documents(consent_id=consent_id) + _client.delete_documents(consent_id=consent_id) @pytest.mark.parametrize('content', [ diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 5070275..e06eb96 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -64,6 +64,7 @@ def mock_read_from_environ(): 'client_secret': 'foobar', } + def equal_credentials(c0, c1): return all([ c0.client_id == c1.client_id, @@ -72,6 +73,7 @@ def equal_credentials(c0, c1): c0.api_endpoint == c1.api_endpoint, ]) + @pytest.mark.parametrize('section', ['default']) def test_guess_credentials(section, credentials_path): credentials_from_env = Credentials(**mock_read_from_environ()) @@ -121,6 +123,7 @@ def test_credentials_with_cache(cache_path, token, section): assert credentials._token == (token['access_token'], token['expires_in']) assert credentials.cached_profile == section + @pytest.mark.parametrize('use_cache', [True]) def test_read_from_file_with_cache(credentials_path, section, token, cache_path): write_token_to_cache(section, (token['access_token'], token['expires_in']), cache_path) @@ -129,6 +132,7 @@ def test_read_from_file_with_cache(credentials_path, section, token, cache_path) assert credentials.cached_profile == section assert credentials._token == (token['access_token'], token['expires_in']) + @pytest.mark.parametrize('use_cache', [False]) def test_read_from_file_with_no_cache(credentials_path, section, token, cache_path): write_token_to_cache(section, (token['access_token'], token['expires_in']), cache_path) diff --git a/tests/test_data_bundles.py b/tests/test_data_bundles.py deleted file mode 100644 index e693692..0000000 --- a/tests/test_data_bundles.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging -import random - -import pytest -from cradl.client import Client - -from . import service, util - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) -def test_create_data_bundle(client: Client, name_and_description): - dataset_ids = [service.create_dataset_id() for _ in range(10)] - response = client.create_data_bundle(service.create_model_id(), dataset_ids, **name_and_description) - assert_data_bundle(response) - - -def test_get_data_bundle(client: Client): - response = client.get_data_bundle( - service.create_model_id(), - service.create_data_bundle_id(), - ) - assert_data_bundle(response) - - -def test_list_data_bundles(client: Client): - response = client.list_data_bundles(service.create_model_id()) - logging.info(response) - assert 'dataBundles' in response, 'Missing dataBundles in response' - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_data_bundles_with_pagination(client: Client, max_results, next_token): - response = client.list_data_bundles(service.create_model_id(), max_results=max_results, next_token=next_token) - assert 'dataBundles' in response, 'Missing dataBundles in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def test_delete_data_bundle(client: Client): - response = client.delete_data_bundle(service.create_model_id(), service.create_data_bundle_id()) - assert_data_bundle(response) - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(at_least_one=True)) -def test_update_data_bundle(client: Client, name_and_description): - response = client.update_data_bundle( - model_id=service.create_model_id(), - data_bundle_id=service.create_data_bundle_id(), - **name_and_description - ) - assert_data_bundle(response) - - -def assert_data_bundle(response): - assert 'modelId' in response, 'Missing modelId in response' - assert 'dataBundleId' in response, 'Missing dataBundleId in response' - assert 'name' in response, 'Missing name in response' - assert 'description' in response, 'Missing description in response' - assert 'createdTime' in response, 'Missing createdTime in response' - assert 'status' in response, 'Missing status in response' diff --git a/tests/test_datasets.py b/tests/test_datasets.py index e57bacf..96b65c1 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -7,6 +7,7 @@ from . import service, util +@pytest.mark.skip(reason='Datasets are not used in new API for now') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) @pytest.mark.parametrize('metadata', [util.metadata(), None]) def test_create_dataset(client: Client, name_and_description, metadata): @@ -14,12 +15,14 @@ def test_create_dataset(client: Client, name_and_description, metadata): assert_dataset(response) +@pytest.mark.skip(reason='Datasets are not used in new API for now') def test_list_datasets(client: Client): response = client.list_datasets() logging.info(response) assert 'datasets' in response, 'Missing datasets in response' +@pytest.mark.skip(reason='Datasets are not used in new API for now') @pytest.mark.parametrize('max_results,next_token', [ (random.randint(1, 100), None), (random.randint(1, 100), 'foo'), @@ -31,12 +34,14 @@ def test_list_datasets_with_pagination(client: Client, max_results, next_token): assert 'nextToken' in response, 'Missing nextToken in response' +@pytest.mark.skip(reason='Datasets are not used in new API for now') def test_delete_dataset(client: Client): dataset_id = service.create_dataset_id() response = client.delete_dataset(dataset_id) assert_dataset(response) +@pytest.mark.skip(reason='Datasets are not used in new API for now') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(at_least_one=True)) @pytest.mark.parametrize('metadata', [util.metadata(), None]) def test_update_dataset(client: Client, name_and_description, metadata): diff --git a/tests/test_deployment_environments.py b/tests/test_deployment_environments.py deleted file mode 100644 index 555ba39..0000000 --- a/tests/test_deployment_environments.py +++ /dev/null @@ -1,39 +0,0 @@ -import random - -import pytest -from cradl.client import Client - -from . import service - - -def assert_deployment_environment(deployment_environment): - assert 'organizationId' in deployment_environment, 'Missing organizationId in deployment_environment' - assert 'deploymentEnvironmentId' in deployment_environment, 'Missing deploymentEnvironmentId in deployment_environment' - assert 'description' in deployment_environment, 'Missing description in deployment_environment' - assert 'name' in deployment_environment, 'Missing name in deployment_environment' - - -def test_list_deployment_environments(client: Client): - response = client.list_deployment_environments() - assert 'deploymentEnvironments' in response, 'Missing deploymentEnvironments in response' - for deployment_environment in response['deploymentEnvironments']: - assert_deployment_environment(deployment_environment) - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_deployment_environments_with_pagination(client: Client, max_results, next_token): - response = client.list_deployment_environments(max_results=max_results, next_token=next_token) - assert 'deploymentEnvironments' in response, 'Missing deploymentEnvironments in response' - assert 'nextToken' in response, 'Missing nextToken in response' - for deployment_environment in response['deploymentEnvironments']: - assert_deployment_environment(deployment_environment) - - -def test_get_deployment_environment(client: Client): - deployment_environment_id = service.create_deployment_environment_id() - deployment_environment = client.get_deployment_environment(deployment_environment_id) - assert_deployment_environment(deployment_environment) diff --git a/tests/test_documents.py b/tests/test_documents.py index 1a10fe9..18d2e4f 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -95,7 +95,7 @@ def test_update_document(static_client: Client, metadata): post_document_id_response = static_client.update_document( document_id, - ground_truth=ground_truth, + ground_truth=ground_truth, # type: ignore dataset_id=service.create_dataset_id(), metadata=metadata, ) diff --git a/tests/test_hook_runs.py b/tests/test_hook_runs.py new file mode 100644 index 0000000..382df88 --- /dev/null +++ b/tests/test_hook_runs.py @@ -0,0 +1,41 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_hook_run(run): + assert 'runId' in run, 'Missing runId in hook run' + assert 'hookId' in run, 'Missing hookId in hook run' + + +def test_list_hook_runs(client: Client): + hook_id = service.create_hook_id() + response = client.list_hook_runs(hook_id) + assert 'runs' in response, 'Missing runs in response' + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_hook_runs_with_pagination(client: Client, max_results, next_token): + hook_id = service.create_hook_id() + response = client.list_hook_runs(hook_id, max_results=max_results, next_token=next_token) + assert 'runs' in response, 'Missing runs in response' + + +def test_get_hook_run(client: Client): + hook_id = service.create_hook_id() + run_id = service.create_hook_run_id() + run = client.get_hook_run(hook_id, run_id) + assert_hook_run(run) + + +def test_update_hook_run(client: Client): + hook_id = service.create_hook_id() + run_id = service.create_hook_run_id() + run = client.update_hook_run(hook_id, run_id, status='succeeded') + assert_hook_run(run) diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 0000000..42fae78 --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,60 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_hook(hook): + assert 'hookId' in hook, 'Missing hookId in hook' + assert 'name' in hook, 'Missing name in hook' + assert 'trigger' in hook, 'Missing trigger in hook' + + +def test_create_hook(client: Client): + hook = client.create_hook( + trigger='Document is Created', + name='Test Hook', + description='A test hook', + config={'foo': 'bar'}, + ) + assert_hook(hook) + + +def test_list_hooks(client: Client): + response = client.list_hooks() + assert 'hooks' in response, 'Missing hooks in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for hook in response['hooks']: + assert_hook(hook) + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_hooks_with_pagination(client: Client, max_results, next_token): + response = client.list_hooks(max_results=max_results, next_token=next_token) + assert 'hooks' in response, 'Missing hooks in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for hook in response['hooks']: + assert_hook(hook) + + +def test_get_hook(client: Client): + hook_id = service.create_hook_id() + hook = client.get_hook(hook_id) + assert_hook(hook) + + +def test_update_hook(client: Client): + hook_id = service.create_hook_id() + hook = client.update_hook(hook_id, name='Updated Hook', enabled=True) + assert_hook(hook) + + +def test_delete_hook(client: Client): + hook_id = service.create_hook_id() + hook = client.delete_hook(hook_id) + assert_hook(hook) diff --git a/tests/test_logs.py b/tests/test_logs.py index cc94a6f..da7c0c6 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -1,4 +1,3 @@ -import pytest from cradl.client import Client from . import service diff --git a/tests/test_model.py b/tests/test_model.py index be38afb..35c8d3f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -90,5 +90,3 @@ def test_delete_model(client: Client): model_id = service.create_model_id() response = client.delete_model(model_id) assert 'modelId' in response, 'Missing modelId in response' - - diff --git a/tests/test_organization.py b/tests/test_organization.py index 1264300..9a33a84 100644 --- a/tests/test_organization.py +++ b/tests/test_organization.py @@ -44,9 +44,12 @@ def assert_organization(response): assert 'monthlyNumberOfDocumentsCreated' in response, 'Missing monthlyNumberOfDocumentsCreated in response' assert 'monthlyNumberOfPredictionsAllowed' in response, 'Missing monthlyNumberOfPredictionsAllowed in response' assert 'monthlyNumberOfPredictionsCreated' in response, 'Missing monthlyNumberOfPredictionsCreated in response' - assert 'monthlyNumberOfTransitionExecutionsAllowed' in response, 'Missing monthlyNumberOfTransitionExecutionsAllowed in response' - assert 'monthlyNumberOfTransitionExecutionsCreated' in response, 'Missing monthlyNumberOfTransitionExecutionsCreated in response' - assert 'monthlyNumberOfWorkflowExecutionsAllowed' in response, 'Missing monthlyNumberOfWorkflowExecutionsAllowed in response' - assert 'monthlyNumberOfWorkflowExecutionsCreated' in response, 'Missing monthlyNumberOfWorkflowExecutionsCreated in response' + assert 'monthlyNumberOfTransitionExecutionsAllowed' in response, \ + 'Missing monthlyNumberOfTransitionExecutionsAllowed in response' + assert 'monthlyNumberOfTransitionExecutionsCreated' in response, \ + 'Missing monthlyNumberOfTransitionExecutionsCreated in response' + assert 'monthlyNumberOfWorkflowExecutionsAllowed' in response, \ + 'Missing monthlyNumberOfWorkflowExecutionsAllowed in response' + assert 'monthlyNumberOfWorkflowExecutionsCreated' in response, \ + 'Missing monthlyNumberOfWorkflowExecutionsCreated in response' assert 'monthlyUsageSummary' in response, 'Missing monthlyUsageSummary in response' - diff --git a/tests/test_payment_methods.py b/tests/test_payment_methods.py index c6400dd..a70cea2 100644 --- a/tests/test_payment_methods.py +++ b/tests/test_payment_methods.py @@ -7,12 +7,14 @@ from . import service, util +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) def test_create_payment_method(client: Client, name_and_description): response = client.create_payment_method(**name_and_description) assert_payment_method(response) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_payment_methods(client: Client): response = client.list_payment_methods() logging.info(response) @@ -24,12 +26,14 @@ def test_list_payment_methods(client: Client): (random.randint(1, 100), 'foo'), (None, None), ]) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_payment_methods_with_pagination(client: Client, max_results, next_token): response = client.list_payment_methods(max_results=max_results, next_token=next_token) assert 'paymentMethods' in response, 'Missing payment_methods in response' assert 'nextToken' in response, 'Missing nextToken in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_delete_payment_method(client: Client): payment_method_id = service.create_payment_method_id() response = client.delete_payment_method(payment_method_id) @@ -37,6 +41,7 @@ def test_delete_payment_method(client: Client): @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(at_least_one=True)) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_update_payment_method(client: Client, name_and_description): response = client.update_payment_method(service.create_payment_method_id(), **name_and_description) assert_payment_method(response) diff --git a/tests/test_plans.py b/tests/test_plans.py index c1da032..0bc371b 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -15,6 +15,7 @@ def assert_plan(plan): assert 'name' in plan, 'Missing name in plan' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_plans(client: Client): response = client.list_plans() assert 'plans' in response, 'Missing plans in response' @@ -22,6 +23,7 @@ def test_list_plans(client: Client): assert_plan(plan) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') @pytest.mark.parametrize('max_results,next_token', [ (random.randint(1, 100), None), (random.randint(1, 100), 'foo'), @@ -35,6 +37,7 @@ def test_list_plans_with_pagination(client: Client, max_results, next_token): assert_plan(plan) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_get_plan(client: Client): plan_id = service.create_plan_id() plan = client.get_plan(plan_id) diff --git a/tests/test_secrets.py b/tests/test_secrets.py index bb8497b..0bfd5eb 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -8,12 +8,14 @@ @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_create_secret(client: Client, name_and_description): data = {'username': 'foo', 'password': 'bar'} response = client.create_secret(data, **name_and_description) assert 'secretId' in response, 'Missing secretId in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_secrets(client: Client): response = client.list_secrets() logging.info(response) @@ -25,6 +27,7 @@ def test_list_secrets(client: Client): (random.randint(1, 100), 'foo'), (None, None), ]) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_list_secrets_with_pagination(client: Client, max_results, next_token): response = client.list_secrets(max_results=max_results, next_token=next_token) assert 'secrets' in response, 'Missing secrets in response' @@ -32,6 +35,7 @@ def test_list_secrets_with_pagination(client: Client, max_results, next_token): @pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_update_secret(client: Client, name_and_description): secret_id = service.create_secret_id() data = {'username': 'foo', 'password': 'bar'} @@ -39,8 +43,8 @@ def test_update_secret(client: Client, name_and_description): assert 'secretId' in response, 'Missing secretId in response' +@pytest.mark.skip(reason='This resource is currently not intended to use in the new API') def test_delete_secret(client: Client): secret_id = service.create_secret_id() response = client.delete_secret(secret_id) assert 'secretId' in response, 'Missing secretId in response' - diff --git a/tests/test_training.py b/tests/test_training.py deleted file mode 100644 index 9161c3d..0000000 --- a/tests/test_training.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -import random - -import pytest -from cradl.client import Client - -from . import service, util - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) -@pytest.mark.parametrize('metadata', [util.metadata(), None]) -def test_create_training(client: Client, name_and_description, metadata): - data_bundle_ids = [service.create_data_bundle_id() for _ in range(10)] - response = client.create_training( - service.create_model_id(), - data_bundle_ids, - metadata=metadata, - **name_and_description, - ) - assert_training(response) - - -def test_get_training(client: Client): - response = client.get_training( - service.create_model_id(), - service.create_training_id(), - ) - assert_training(response) - - -def test_list_trainings(client: Client): - response = client.list_trainings(service.create_model_id()) - assert 'trainings' in response, 'Missing dataBundles in response' - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_trainings_with_pagination(client: Client, max_results, next_token): - response = client.list_trainings(service.create_model_id(), max_results=max_results, next_token=next_token) - assert 'trainings' in response, 'Missing dataBundles in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def assert_training(response): - assert 'modelId' in response, 'Missing modelId in response' - assert 'trainingId' in response, 'Missing trainingId in response' - assert 'dataBundleIds' in response, 'Missing dataBundleIds in response' - assert 'name' in response, 'Missing name in response' - assert 'description' in response, 'Missing description in response' - assert 'createdTime' in response, 'Missing createdTime in response' diff --git a/tests/test_transformations.py b/tests/test_transformations.py deleted file mode 100644 index 64dc50e..0000000 --- a/tests/test_transformations.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging -import random - -import pytest -from cradl.client import Client - -from . import service, util -from .util import assert_in_response - - -@pytest.mark.parametrize('operations', [[{'type': 'remove-duplicates', 'options': {}}]]) -@pytest.mark.parametrize('dataset_id', [service.create_dataset_id()]) -def test_create_transformation(client: Client, dataset_id, operations): - response = client.create_transformation(dataset_id, operations) - assert_transformation(response) - - -@pytest.mark.parametrize('status', [None, 'failed', 'succeeded', 'running']) -def test_list_transformations(client: Client, status): - response = client.list_transformations(service.create_dataset_id(), status=status) - for transformation in response['transformations']: - assert_transformation(transformation) - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_transformations_with_pagination(client: Client, max_results, next_token): - response = client.list_transformations(service.create_dataset_id(), max_results=max_results, next_token=next_token) - assert_in_response('transformations', response) - assert_in_response('nextToken', response) - for transformation in response['transformations']: - assert_transformation(transformation) - - -def test_delete_transformation(client: Client): - response = client.delete_transformation(service.create_dataset_id(), service.create_transformation_id()) - assert_transformation(response) - - -def assert_transformation(response): - assert_in_response('transformationId', response) - assert_in_response('datasetId', response) - assert_in_response('operations', response) - assert_in_response('createdBy', response) - assert_in_response('createdTime', response) - assert_in_response('status', response) diff --git a/tests/test_transitions.py b/tests/test_transitions.py deleted file mode 100644 index 64f3128..0000000 --- a/tests/test_transitions.py +++ /dev/null @@ -1,252 +0,0 @@ -import logging -import random -from datetime import datetime, timezone -from unittest.mock import patch, ANY - -import cradl -import pytest -from cradl.client import Client - -from . import service, util - - -@pytest.mark.parametrize('transition_type,parameters', [ - ('docker', {'imageUrl': 'python3.8'}), - ('manual', None), - ('manual', {'assets': {'jsRemoteComponent': service.create_asset_id()}}), -]) -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations()) -def test_create_transition(client: Client, transition_type, parameters, name_and_description): - response = client.create_transition( - transition_type, - parameters=parameters, - **name_and_description, - ) - logging.info(response) - assert_transition(response) - - -def test_get_transition(client: Client): - response = client.get_transition(service.create_transition_id()) - logging.info(response) - assert_transition(response) - - -def test_delete_transition(client: Client): - response = client.delete_transition(service.create_transition_id()) - logging.info(response) - assert_transition(response) - - -@pytest.mark.parametrize('transition_type', [['docker'], ['manual'], 'docker', 'manual', None]) -def test_list_transitions(client: Client, transition_type): - response = client.list_transitions(transition_type=transition_type) - logging.info(response) - assert 'transitions' in response, 'Missing transitions in response' - if transition_type: - assert 'transitionType' in response, 'Missing transitionType in response' - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_transitions_with_pagination(client: Client, max_results, next_token): - response = client.list_transitions(max_results=max_results, next_token=next_token) - logging.info(response) - assert 'transitions' in response, 'Missing transitions in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def test_get_transition_execution(client: Client): - response = client.get_transition_execution( - service.create_transition_id(), - service.create_transition_execution_id(), - ) - logging.info(response) - assert 'transitionId' in response, 'Missing transitionId in response' - assert 'executionId' in response, 'Missing executionId in response' - assert 'status' in response, 'Missing status in response' - - -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(True)) -def test_update_transition(client: Client, name_and_description): - response = client.update_transition(service.create_transition_id(), **name_and_description) - logging.info(response) - assert_transition(response) - - -@pytest.mark.parametrize('assets', [{'foo': service.create_asset_id()}]) -def test_update_transition_parameters_manual(client: Client, assets): - response = client.update_transition( - service.create_transition_id(), - assets=assets, - ) - logging.info(response) - assert_transition(response) - - -@pytest.mark.parametrize(('cpu', 'memory', 'image_url', 'secret_id'), [ - (256, 512, 'python3.8', service.create_secret_id()), - (256, 1024, 'python3.9', service.create_secret_id()), - (256, 512, 'python3.8', None), - (256, 1024, 'python3.9', None), -]) -@pytest.mark.parametrize(('environment', 'environment_secrets'), [ - (None, [service.create_secret_id(), service.create_secret_id()]), - ({'foo': 'bar'}, None), - ({'foo': 'bar'}, [service.create_secret_id(), service.create_secret_id()]), - (None, None), -]) -def test_update_transition_parameters_docker( - client: Client, - environment, - environment_secrets, - cpu, - memory, - image_url, - secret_id, -): - response = client.update_transition( - service.create_transition_id(), - environment=environment, - environment_secrets=environment_secrets, - cpu=cpu, - memory=memory, - image_url=image_url, - secret_id=secret_id, - ) - logging.info(response) - assert_transition(response) - - -def test_execute_transition(client: Client): - response = client.execute_transition(service.create_transition_id()) - logging.info(response) - assert 'transitionId' in response, 'Missing transitionId in response' - assert 'executionId' in response, 'Missing executionId in response' - assert 'status' in response, 'Missing status in response' - - -@pytest.mark.parametrize('status,output,error', [ - ('succeeded', {'foo': 'bar'}, None), - ('failed', None, {'message': 'foobar'}), -]) -@pytest.mark.parametrize( - 'start_time', [ - datetime.utcnow().astimezone(timezone.utc), - datetime.utcnow(), - datetime.utcnow().astimezone(timezone.utc).isoformat(), - None - ]) -def test_update_transition_execution(client: Client, status, output, error, start_time): - transition_id = service.create_transition_id() - execution_id = service.create_transition_execution_id() - response = client.update_transition_execution( - transition_id, - execution_id, - status, - output=output, - error=error, - start_time=start_time, - ) - logging.info(response) - assert 'transitionId' in response, 'Missing transitionId in response' - assert 'executionId' in response, 'Missing executionId in response' - assert 'status' in response, 'Missing status in response' - - -def test_send_heartbeat(client: Client): - transition_id = service.create_transition_id() - execution_id = service.create_transition_execution_id() - response = client.send_heartbeat(transition_id, execution_id) - logging.info(response) - assert response == {'Your request executed successfully': '204'} - - -def assert_transition(response): - assert 'name' in response, 'Missing name in response' - assert 'transitionId' in response, 'Missing transitionId in response' - assert 'transitionType' in response, 'Missing transitionType in response' - - -@pytest.fixture -def execution_env(): - return { - 'TRANSITION_ID': 'xyz', - 'EXECUTION_ID': 'xyz', - } - - -@patch('cradl.Client.get_transition_execution') -@patch('cradl.Client.update_transition_execution') -def test_transition_handler_updated_successfully(update_transition_exc, get_transition_exc, execution_env): - output = {'result': 'All good'} - - @cradl.transition_handler - def sample_handler(cradl_client: Client, event: dict): - return output - - with patch.dict(cradl.os.environ, values=execution_env): - sample_handler() - - update_transition_exc.assert_called_once_with( - execution_id=execution_env['EXECUTION_ID'], - transition_id=execution_env['TRANSITION_ID'], - status='succeeded', - output=ANY, - ) - - -@patch('cradl.Client.get_transition_execution') -@patch('cradl.Client.update_transition_execution') -def test_transition_handler_updated_on_failure(update_transition_exc, get_transition_exc, execution_env): - @cradl.transition_handler - def sample_handler(cradl_client: Client, event: dict): - raise RuntimeError('Some error') - - with patch.dict(cradl.os.environ, values=execution_env): - with pytest.raises(RuntimeError): - sample_handler() - - update_transition_exc.assert_called_once_with( - execution_id=execution_env['EXECUTION_ID'], - transition_id=execution_env['TRANSITION_ID'], - status='failed', - error=ANY, - ) - - -@pytest.mark.parametrize('status', ['succeeded', 'rejected', 'failed']) -@patch('cradl.Client.get_transition_execution') -@patch('cradl.Client.update_transition_execution') -def test_transition_handler_custom_status(update_transition_exc, get_transition_exc, execution_env, status): - result = 'Rejected task' - status = 'rejected' - - @cradl.transition_handler - def sample_handler(cradl_client: Client, event: dict): - return result, status - - with patch.dict(cradl.os.environ, values=execution_env): - if status == 'failed': - with pytest.raises(RuntimeError): - sample_handler() - else: - sample_handler() - - if status == 'succeeded': - update_transition_exc.assert_called_once_with( - execution_id=execution_env['EXECUTION_ID'], - transition_id=execution_env['TRANSITION_ID'], - status=status, - output=result - ) - else: - update_transition_exc.assert_called_once_with( - execution_id=execution_env['EXECUTION_ID'], - transition_id=execution_env['TRANSITION_ID'], - status=status, - error=ANY, - ) diff --git a/tests/test_users.py b/tests/test_users.py index 0b0eb41..b020e0e 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -14,11 +14,7 @@ def assert_user(user): def test_create_user(client: Client): email = 'foo@bar.com' - user = client.create_user( - email, - app_client_id=service.create_app_client_id(), - role_ids=[service.create_role_id()], - ) + user = client.create_user(email, role_ids=[service.create_role_id()]) assert_user(user) diff --git a/tests/test_validation_tasks.py b/tests/test_validation_tasks.py new file mode 100644 index 0000000..4bb687d --- /dev/null +++ b/tests/test_validation_tasks.py @@ -0,0 +1,47 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_validation_task(task): + assert 'taskId' in task, 'Missing taskId in validation task' + assert 'validationId' in task, 'Missing validationId in validation task' + + +def test_create_validation_task(client: Client): + validation_id = service.create_validation_id() + task = client.create_validation_task(validation_id, input={'foo': 'bar'}) + assert_validation_task(task) + + +def test_list_validation_tasks(client: Client): + validation_id = service.create_validation_id() + response = client.list_validation_tasks(validation_id) + assert 'tasks' in response, 'Missing tasks in response' + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_validation_tasks_with_pagination(client: Client, max_results, next_token): + validation_id = service.create_validation_id() + response = client.list_validation_tasks(validation_id, max_results=max_results, next_token=next_token) + assert 'tasks' in response, 'Missing tasks in response' + + +def test_get_validation_task(client: Client): + validation_id = service.create_validation_id() + task_id = service.create_validation_task_id() + task = client.get_validation_task(validation_id, task_id) + assert_validation_task(task) + + +def test_update_validation_task(client: Client): + validation_id = service.create_validation_id() + task_id = service.create_validation_task_id() + task = client.update_validation_task(validation_id, task_id, status='succeeded', output={'result': 'ok'}) + assert_validation_task(task) diff --git a/tests/test_validations.py b/tests/test_validations.py new file mode 100644 index 0000000..f021f7d --- /dev/null +++ b/tests/test_validations.py @@ -0,0 +1,58 @@ +import random +import pytest +from cradl.client import Client + +from . import service + + +def assert_validation(validation): + assert 'validationId' in validation, 'Missing validationId in validation' + assert 'name' in validation, 'Missing name in validation' + + +def test_create_validation(client: Client): + validation = client.create_validation( + name='Test Validation', + description='A test validation', + config={'foo': 'bar'}, + ) + assert_validation(validation) + + +def test_list_validations(client: Client): + response = client.list_validations() + assert 'validations' in response, 'Missing validations in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for validation in response['validations']: + assert_validation(validation) + + +@pytest.mark.parametrize('max_results,next_token', [ + (random.randint(1, 100), None), + (random.randint(1, 100), 'foo'), + (None, None), +]) +def test_list_validations_with_pagination(client: Client, max_results, next_token): + response = client.list_validations(max_results=max_results, next_token=next_token) + assert 'validations' in response, 'Missing validations in response' + assert 'nextToken' in response, 'Missing nextToken in response' + for validation in response['validations']: + assert_validation(validation) + + +def test_get_validation(client: Client): + validation_id = service.create_validation_id() + validation = client.get_validation(validation_id) + assert_validation(validation) + + +def test_update_validation(client: Client): + validation_id = service.create_validation_id() + validation = client.update_validation(validation_id, config={'foo': 'bar'}) + assert_validation(validation) + + +def test_delete_validation(client: Client): + validation_id = service.create_validation_id() + validation = client.delete_validation(validation_id) + assert_validation(validation) diff --git a/tests/test_workflows.py b/tests/test_workflows.py deleted file mode 100644 index 230fe52..0000000 --- a/tests/test_workflows.py +++ /dev/null @@ -1,167 +0,0 @@ -import random -from datetime import datetime, timezone - -import pytest -from cradl.client import Client - -from . import service, util - - -@pytest.mark.parametrize('email_config', [None, service.create_email_config()]) -@pytest.mark.parametrize('error_config', [None, service.create_error_config()]) -@pytest.mark.parametrize('completed_config', [None, service.create_completed_config()]) -@pytest.mark.parametrize('metadata', [util.metadata(), None]) -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(True)) -def test_create_workflow(client: Client, email_config, error_config, completed_config, metadata, name_and_description): - specification = {'definition': {}} - response = client.create_workflow( - specification, - email_config=email_config, - error_config=error_config, - completed_config=completed_config, - metadata=metadata, - **name_and_description, - ) - assert_workflow(response) - - -def test_list_workflows(client: Client): - response = client.list_workflows() - assert 'workflows' in response, 'Missing workflows in response' - for workflow in response['workflows']: - assert_workflow(workflow) - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_workflows_with_pagination(client: Client, max_results, next_token): - response = client.list_workflows(max_results=max_results, next_token=next_token) - assert 'workflows' in response, 'Missing workflows in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def test_get_workflow(client: Client): - workflow_id = service.create_workflow_id() - response = client.get_workflow(workflow_id) - assert_workflow(response) - - -@pytest.mark.parametrize('email_config', [None, service.create_email_config()]) -@pytest.mark.parametrize('error_config', [None, service.create_error_config()]) -@pytest.mark.parametrize('completed_config', [None, service.create_completed_config()]) -@pytest.mark.parametrize('metadata', [util.metadata(), None]) -@pytest.mark.parametrize('name_and_description', util.name_and_description_combinations(True)) -@pytest.mark.parametrize('status', [None, 'development', 'production']) -def test_update_workflow( - client: Client, - email_config, - error_config, - completed_config, - metadata, - name_and_description, - status, -): - optional_args = {'email_config': email_config, **name_and_description} - response = client.update_workflow( - service.create_workflow_id(), - completed_config=completed_config, - error_config=error_config, - metadata=metadata, - status=status, - **optional_args, - ) - assert_workflow(response) - - -@pytest.mark.parametrize(('from_start_time', 'to_start_time'), [ - (datetime(2022, 1, 1).astimezone(timezone.utc), datetime(2023, 1, 1).astimezone(timezone.utc)), - (datetime(2022, 1, 1).astimezone(timezone.utc), None), - (None, datetime(2023, 1, 1).astimezone(timezone.utc)), - (datetime(2022, 1, 1).astimezone(timezone.utc).isoformat(), datetime(2023, 1, 1).astimezone(timezone.utc).isoformat()), - (datetime(2022, 1, 1).astimezone(timezone.utc).isoformat(), None), - (None, datetime(2023, 1, 1).astimezone(timezone.utc).isoformat()), - (None, None), -]) -@pytest.mark.parametrize('status', [ - ['succeeded'], - ['failed'], - 'running', - None, -]) -def test_list_workflow_executions(client: Client, status, from_start_time, to_start_time): - workflow_id = service.create_workflow_id() - response = client.list_workflow_executions( - workflow_id=workflow_id, - status=status, - from_start_time=from_start_time, - to_start_time=to_start_time, - ) - assert 'workflowId' in response, 'Missing workflowId in response' - assert 'executions' in response, 'Missing executions in response' - - -@pytest.mark.parametrize('max_results,next_token', [ - (random.randint(1, 100), None), - (random.randint(1, 100), 'foo'), - (None, None), -]) -def test_list_workflow_executions_with_pagination(client: Client, max_results, next_token): - workflow_id = service.create_workflow_id() - response = client.list_workflow_executions(workflow_id=workflow_id, max_results=max_results, next_token=next_token) - assert 'workflowId' in response, 'Missing workflowId in response' - assert 'executions' in response, 'Missing executions in response' - assert 'nextToken' in response, 'Missing nextToken in response' - - -def test_execute_workflow(client: Client): - workflow_id = service.create_workflow_id() - response = client.execute_workflow(workflow_id, content={}) - assert_workflow_execution(response) - - -def test_delete_workflow_execution(client: Client): - workflow_id = service.create_workflow_id() - execution_id = service.create_workflow_execution_id() - response = client.delete_workflow_execution(workflow_id, execution_id) - assert_workflow_execution(response) - - -def test_delete_workflow(client: Client): - workflow_id = service.create_workflow_id() - response = client.delete_workflow(workflow_id) - assert_workflow(response) - - -def test_get_workflow_execution(client: Client): - response = client.get_workflow_execution(service.create_workflow_id(), service.create_workflow_execution_id()) - assert_workflow_execution(response) - - -@pytest.mark.parametrize('optional_args', [ - {'next_transition_id': service.create_transition_id()}, - {'status': 'completed'}, -]) -def test_update_workflow_execution(client: Client, optional_args): - response = client.update_workflow_execution( - service.create_workflow_id(), - service.create_workflow_execution_id(), - **optional_args, - ) - assert_workflow_execution(response) - - -def assert_workflow(response): - assert 'workflowId' in response, 'Missing workflowId in response' - assert 'name' in response, 'Missing name in response' - assert 'description' in response, 'Missing description in response' - - -def assert_workflow_execution(response): - assert 'workflowId' in response, 'Missing workflowId in response' - assert 'executionId' in response, 'Missing executionId in response' - assert 'startTime' in response, 'Missing startTime in response' - assert 'endTime' in response, 'Missing endTime in response' - assert 'transitionExecutions' in response, 'Missing transitionExecutions in response' diff --git a/tests/util.py b/tests/util.py index aef762c..59c0de5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -24,4 +24,3 @@ def metadata(): def assert_in_response(field, response): assert field in response, f'Missing {field} in response' - \ No newline at end of file