diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9edd927 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,38 @@ +name: Building and publishing documentation + +on: + push: + branches: [master] + +jobs: + docs: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install requirements + run: | + pip install -r requirements.txt + pip install -r tests/requirements.txt + + - name: Install app + run: pip install . + + - name: Create markdown documents + run: | + lazydocs \ + --output-path="./docs/docstrings" \ + --overview-file="README.md" \ + --src-base-url="https://github.com/ohsu-comp-bio/py-tes/blob/master/" + --validate \ + "./tes" + + - name: Build docs + run: mkdocs build diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e01138f..da1ad1a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: py-test_file +name: Linting and testing on: [pull_request] diff --git a/.gitignore b/.gitignore index 18dd7bf..2623257 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ eggs/ # Misc test_tmp *venv* +docs/docstrings # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python diff --git a/README.md b/README.md index 356cb5c..575c0c6 100644 --- a/README.md +++ b/README.md @@ -42,112 +42,7 @@ cli.cancel_task(task_id) tasks_list = cli.list_tasks(view="MINIMAL") # default view ``` -### How to... +### Documentation -> Makes use of the objects above... - -#### ...export a model to a dictionary - -```python -task_dict = task.as_dict(drop_empty=False) -``` - -`task_dict` contents: - -```console -{'id': None, 'state': None, 'name': None, 'description': None, 'inputs': None, 'outputs': None, 'resources': None, 'executors': [{'image': 'alpine', 'command': ['echo', 'hello'], 'workdir': None, 'stdin': None, 'stdout': None, 'stderr': None, 'env': None}], 'volumes': None, 'tags': None, 'logs': None, 'creation_time': None} -``` - -#### ...export a model to JSON - -```python -task_json = task.as_json() # also accepts `drop_empty` arg -``` - -`task_json` contents: - -```console -{"executors": [{"image": "alpine", "command": ["echo", "hello"]}]} -``` - -#### ...pretty print a model - -```python -print(task.as_json(indent=3)) # keyword args are passed to `json.dumps()` -``` - -Output: - -```json -{ - "executors": [ - { - "image": "alpine", - "command": [ - "echo", - "hello" - ] - } - ] -} -``` - -#### ...access a specific task from the task list - -```python -specific_task = tasks_list.tasks[5] -``` - -`specific_task` contents: - -```console -Task(id='393K43', state='COMPLETE', name=None, description=None, inputs=None, outputs=None, resources=None, executors=None, volumes=None, tags=None, logs=None, creation_time=None) -``` - -#### ...iterate over task list items - -```python -for t in tasks_list[:3]: - print(t.as_json(indent=3)) -``` - -Output: - -```console -{ - "id": "task_A2GFS4", - "state": "RUNNING" -} -{ - "id": "task_O8G1PZ", - "state": "CANCELED" -} -{ - "id": "task_W246I6", - "state": "COMPLETE" -} -``` - -#### ...instantiate a model from a JSON representation - -```python -task_from_json = tes.client.unmarshal(task_json, tes.Task) -``` - -`task_from_json` contents: - -```console -Task(id=None, state=None, name=None, description=None, inputs=None, outputs=None, resources=None, executors=[Executor(image='alpine', command=['echo', 'hello'], workdir=None, stdin=None, stdout=None, stderr=None, env=None)], volumes=None, tags=None, logs=None, creation_time=None) -``` - -Which is equivalent to `task`: - -```python -print(task_from_json == task) -``` - -Output: - -```console -True -``` +For additional details, recipes and an API reference, read the +[docs](https://ohsu-comp-bio.github.io/py-tes). diff --git a/docs/.pages b/docs/.pages new file mode 100644 index 0000000..e8b6473 --- /dev/null +++ b/docs/.pages @@ -0,0 +1,3 @@ +nav: + - Overview: README.md + - ... diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f494a3f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,149 @@ +py-tes +====== + +_py-tes_ is a library for interacting with servers implementing the [GA4GH Task Execution Schema](https://github.com/ga4gh/task-execution-schemas). + + +### Install + +Available on [PyPI](https://pypi.org/project/py-tes/). + +``` +pip install py-tes +``` + +### Example + +```python +import tes + +# define task +task = tes.Task( + executors=[ + tes.Executor( + image="alpine", + command=["echo", "hello"] + ) + ] +) + +# create client +cli = tes.HTTPClient("https://funnel.example.com", timeout=5) + +# access endpoints +service_info = cli.get_service_info() +task_id = cli.create_task(task) +task_info = cli.get_task(task_id, view="BASIC") +cli.cancel_task(task_id) +tasks_list = cli.list_tasks(view="MINIMAL") # default view +``` + +### How to... + +> Makes use of the objects above... + +#### ...export a model to a dictionary + +```python +task_dict = task.as_dict(drop_empty=False) +``` + +`task_dict` contents: + +```console +{'id': None, 'state': None, 'name': None, 'description': None, 'inputs': None, 'outputs': None, 'resources': None, 'executors': [{'image': 'alpine', 'command': ['echo', 'hello'], 'workdir': None, 'stdin': None, 'stdout': None, 'stderr': None, 'env': None}], 'volumes': None, 'tags': None, 'logs': None, 'creation_time': None} +``` + +#### ...export a model to JSON + +```python +task_json = task.as_json() # also accepts `drop_empty` arg +``` + +`task_json` contents: + +```console +{"executors": [{"image": "alpine", "command": ["echo", "hello"]}]} +``` + +#### ...pretty print a model + +```python +print(task.as_json(indent=3)) # keyword args are passed to `json.dumps()` +``` + +Output: + +```json +{ + "executors": [ + { + "image": "alpine", + "command": [ + "echo", + "hello" + ] + } + ] +} +``` + +#### ...access a specific task from the task list + +```python +specific_task = tasks_list.tasks[5] +``` + +`specific_task` contents: + +```console +Task(id='393K43', state='COMPLETE', name=None, description=None, inputs=None, outputs=None, resources=None, executors=None, volumes=None, tags=None, logs=None, creation_time=None) +``` + +#### ...iterate over task list items + +```python +for t in tasks_list[:3]: + print(t.as_json(indent=3)) +``` + +Output: + +```console +{ + "id": "task_A2GFS4", + "state": "RUNNING" +} +{ + "id": "task_O8G1PZ", + "state": "CANCELED" +} +{ + "id": "task_W246I6", + "state": "COMPLETE" +} +``` + +#### ...instantiate a model from a JSON representation + +```python +task_from_json = tes.client.unmarshal(task_json, tes.Task) +``` + +`task_from_json` contents: + +```console +Task(id=None, state=None, name=None, description=None, inputs=None, outputs=None, resources=None, executors=[Executor(image='alpine', command=['echo', 'hello'], workdir=None, stdin=None, stdout=None, stderr=None, env=None)], volumes=None, tags=None, logs=None, creation_time=None) +``` + +Which is equivalent to `task`: + +```python +print(task_from_json == task) +``` + +Output: + +```console +True +``` diff --git a/docs/docstring/.pages b/docs/docstring/.pages new file mode 100644 index 0000000..db48efa --- /dev/null +++ b/docs/docstring/.pages @@ -0,0 +1,4 @@ +title: API Reference +nav: + - Overview: README.md + - ... diff --git a/docs/docstring/README.md b/docs/docstring/README.md new file mode 100644 index 0000000..9c86184 --- /dev/null +++ b/docs/docstring/README.md @@ -0,0 +1,48 @@ + + +# API Overview + +## Modules + +- [`client`](./client.md#module-client): TES access methods and helper functions. +- [`models`](./models.md#module-models): TES models, converters, validators and helpers. +- [`utils`](./utils.md#module-utils): Exceptions and utilities. + +## Classes + +- [`client.HTTPClient`](./client.md#class-httpclient): HTTP client class for interacting with the TES API. +- [`models.Base`](./models.md#class-base): `attrs` base class for all TES and helper models. +- [`models.CancelTaskRequest`](./models.md#class-canceltaskrequest): `attrs` model class for `POST /tasks/{id}:cancel` request parameters. +- [`models.CancelTaskResponse`](./models.md#class-canceltaskresponse): TES `tesCancelTaskResponse` `attrs` model class. +- [`models.CreateTaskResponse`](./models.md#class-createtaskresponse): TES `tesCreateTaskResponse` `attrs` model class. +- [`models.Executor`](./models.md#class-executor): TES `tesExecutor` `attrs` model class. +- [`models.ExecutorLog`](./models.md#class-executorlog): TES `tesExecutorLog` `attrs` model class. +- [`models.GetTaskRequest`](./models.md#class-gettaskrequest): `attrs` model class for `GET /tasks/{id}` request parameters. +- [`models.Input`](./models.md#class-input): TES `tesInput` `attrs` model class. +- [`models.ListTasksRequest`](./models.md#class-listtasksrequest): `attrs` model class for `GET /tasks` request parameters. +- [`models.ListTasksResponse`](./models.md#class-listtasksresponse): TES `tesListTasksResponse` `attrs` model class. +- [`models.Output`](./models.md#class-output): TES `tesOutput` `attrs` model class. +- [`models.OutputFileLog`](./models.md#class-outputfilelog): TES `tesOutputFileLog` `attrs` model class. +- [`models.Resources`](./models.md#class-resources): TES `tesResources` `attrs` model class. +- [`models.ServiceInfo`](./models.md#class-serviceinfo): TES `tesServiceInfo` `attrs` model class. +- [`models.ServiceInfoRequest`](./models.md#class-serviceinforequest): `attrs` model class for `GET /service-info` request parameters. +- [`models.Task`](./models.md#class-task): TES `tesTask` `attrs` model class. +- [`models.TaskLog`](./models.md#class-tasklog): TES `tesTaskLog` `attrs` model class. +- [`utils.TimeoutError`](./utils.md#class-timeouterror) +- [`utils.UnmarshalError`](./utils.md#class-unmarshalerror): Raised when a JSON string cannot be unmarshalled to a TES model. + +## Functions + +- [`client.process_url`](./client.md#function-process_url) +- [`models.datetime_json_handler`](./models.md#function-datetime_json_handler): JSON handler for `datetime` objects. +- [`models.int64conv`](./models.md#function-int64conv): Convert string to `int64`. +- [`models.list_of`](./models.md#function-list_of): `attrs` validator for lists of a given type. +- [`models.strconv`](./models.md#function-strconv): Explicitly cast a string-like value or list thereof to string(s). +- [`models.timestampconv`](./models.md#function-timestampconv): Convert string to `datetime`. +- [`utils.camel_to_snake`](./utils.md#function-camel_to_snake): Converts camelCase to snake_case. +- [`utils.unmarshal`](./utils.md#function-unmarshal): Unmarshal a JSON string to a TES model. + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/docs/docstring/client.md b/docs/docstring/client.md new file mode 100644 index 0000000..b6cd3d8 --- /dev/null +++ b/docs/docstring/client.md @@ -0,0 +1,186 @@ + + + + +# module `client` +TES access methods and helper functions. + + +--- + + + +## function `process_url` + +```python +process_url(value) +``` + + + + + + +--- + + + +## class `HTTPClient` +HTTP client class for interacting with the TES API. + + + +### method `__init__` + +```python +__init__( + url, + timeout: int = 10, + user: Any = None, + password: Any = None, + token: Any = None +) → None +``` + +Method generated by attrs for class HTTPClient. + + + + +--- + + + +### method `cancel_task` + +```python +cancel_task(task_id: str) → None +``` + +Access method for `POST /tasks/{id}:cancel`. + + + +**Args:** + + - `task_id`: TES Task ID. + +--- + + + +### method `create_task` + +```python +create_task(task: Task) → CreateTaskResponse +``` + +Access method for `POST /tasks`. + + + +**Args:** + + - `task`: `tes.models.Task` instance. + + + +**Returns:** + `tes.models.CreateTaskResponse` instance. + + + +**Raises:** + + - `TypeError`: If `task` is not a `tes.models.Task` instance. + +--- + + + +### method `get_service_info` + +```python +get_service_info() → ServiceInfo +``` + +Access method for `GET /service-info`. + + + +**Returns:** + `tes.models.ServiceInfo` instance. + +--- + + + +### method `get_task` + +```python +get_task(task_id: str, view: str = 'BASIC') → Task +``` + +Access method for `GET /tasks/{id}`. + + + +**Args:** + + - `task_id`: TES Task ID. + - `view`: Task info verbosity. One of `MINIMAL`, `BASIC` and `FULL`. + + + +**Returns:** + `tes.models.Task` instance. + +--- + + + +### method `list_tasks` + +```python +list_tasks( + view: str = 'MINIMAL', + page_size: Optional[int] = None, + page_token: Optional[str] = None +) → ListTasksResponse +``` + +Access method for `GET /tasks`. + + + +**Args:** + + - `view`: Task info verbosity. One of `MINIMAL`, `BASIC` and `FULL`. + - `page_size`: Number of tasks to return. + - `page_token`: Token to retrieve the next page of tasks. + + + +**Returns:** + `tes.models.ListTasksResponse` instance. + +--- + + + +### method `wait` + +```python +wait(task_id: str, timeout=None) → Task +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/docs/docstring/models.md b/docs/docstring/models.md new file mode 100644 index 0000000..05cc326 --- /dev/null +++ b/docs/docstring/models.md @@ -0,0 +1,1052 @@ + + + + +# module `models` +TES models, converters, validators and helpers. + + +--- + + + +## function `list_of` + +```python +list_of(_type: Any) → _ListOfValidator +``` + +`attrs` validator for lists of a given type. + + + +**Args:** + + - `_type`: Type to validate. + + + +**Returns:** + `attrs` validator for the given type. + + +--- + + + +## function `strconv` + +```python +strconv(value: Any) → Any +``` + +Explicitly cast a string-like value or list thereof to string(s). + + + +**Args:** + + - `value`: Value to convert. + + + +**Returns:** + Converted value. If `value` is a list, all elements are converted to strings. If `value` is not string-like, it will be returned as is. + + +--- + + + +## function `int64conv` + +```python +int64conv(value: Optional[str]) → Optional[int] +``` + +Convert string to `int64`. + + + +**Args:** + + - `value`: String to convert. + + + +**Returns:** + Converted value. + + +--- + + + +## function `timestampconv` + +```python +timestampconv(value: Optional[str]) → Optional[datetime] +``` + +Convert string to `datetime`. + + + +**Args:** + + - `value`: String to convert. + + + +**Returns:** + Converted value. + + +--- + + + +## function `datetime_json_handler` + +```python +datetime_json_handler(x: Any) → str +``` + +JSON handler for `datetime` objects. + + + +**Args:** + + - `x`: Object to convert. + + + +**Returns:** + Converted object. + + + +**Raises:** + + - `TypeError`: If `x` is not a `datetime` object. + + +--- + + + +## class `Base` +`attrs` base class for all TES and helper models. + + + +### method `__init__` + +```python +__init__() → None +``` + +Method generated by attrs for class Base. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `Input` +TES `tesInput` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + url: Any = None, + path: Any = None, + type: str = 'FILE', + name: Any = None, + description: Any = None, + content: Any = None +) → None +``` + +Method generated by attrs for class Input. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `Output` +TES `tesOutput` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + url: Any = None, + path: Any = None, + type: str = 'FILE', + name: Any = None, + description: Any = None +) → None +``` + +Method generated by attrs for class Output. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `Resources` +TES `tesResources` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + cpu_cores: Optional[int] = None, + ram_gb: Optional[float, int] = None, + disk_gb: Optional[float, int] = None, + preemptible: Optional[bool] = None, + zones: Any = None +) → None +``` + +Method generated by attrs for class Resources. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `Executor` +TES `tesExecutor` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + image: Any, + command: Any, + workdir: Any = None, + stdin: Any = None, + stdout: Any = None, + stderr: Any = None, + env: Optional[Dict] = None +) → None +``` + +Method generated by attrs for class Executor. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `ExecutorLog` +TES `tesExecutorLog` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + start_time: Optional[str] = None, + end_time: Optional[str] = None, + stdout: Any = None, + stderr: Any = None, + exit_code: Optional[int] = None +) → None +``` + +Method generated by attrs for class ExecutorLog. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `OutputFileLog` +TES `tesOutputFileLog` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + url: Any = None, + path: Any = None, + size_bytes: Optional[str] = None +) → None +``` + +Method generated by attrs for class OutputFileLog. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `TaskLog` +TES `tesTaskLog` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + start_time: Optional[str] = None, + end_time: Optional[str] = None, + metadata: Optional[Dict] = None, + logs: Optional[List[ExecutorLog]] = None, + outputs: Optional[List[OutputFileLog]] = None, + system_logs: Optional[List[str]] = None +) → None +``` + +Method generated by attrs for class TaskLog. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `Task` +TES `tesTask` `attrs` model class. + + + +### method `__init__` + +```python +__init__( + id: Any = None, + state: Optional[str] = None, + name: Any = None, + description: Any = None, + inputs: Optional[List[Input]] = None, + outputs: Optional[List[Output]] = None, + resources: Optional[Resources] = None, + executors: Optional[List[Executor]] = None, + volumes: Optional[List[str]] = None, + tags: Optional[Dict] = None, + logs: Optional[List[TaskLog]] = None, + creation_time: Optional[str] = None +) → None +``` + +Method generated by attrs for class Task. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + +--- + + + +### method `is_valid` + +```python +is_valid() → Tuple[bool, Optional[TypeError]] +``` + +Validate a `Task` model instance. + + + +**Returns:** + A tuple containing a boolean indicating whether the model is valid, and a `TypeError` if the model is invalid, or `None` if it is. + + +--- + + + +## class `GetTaskRequest` +`attrs` model class for `GET /tasks/{id}` request parameters. + + + +### method `__init__` + +```python +__init__(id: Any, view: Optional[str] = None) → None +``` + +Method generated by attrs for class GetTaskRequest. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `CreateTaskResponse` +TES `tesCreateTaskResponse` `attrs` model class. + + + +### method `__init__` + +```python +__init__(id: Any) → None +``` + +Method generated by attrs for class CreateTaskResponse. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `ServiceInfoRequest` +`attrs` model class for `GET /service-info` request parameters. + + + +### method `__init__` + +```python +__init__() → None +``` + +Method generated by attrs for class ServiceInfoRequest. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `ServiceInfo` +TES `tesServiceInfo` `attrs` model class. + + + +### method `__init__` + +```python +__init__(name: Any = None, doc: Any = None, storage: Any = None) → None +``` + +Method generated by attrs for class ServiceInfo. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `CancelTaskRequest` +`attrs` model class for `POST /tasks/{id}:cancel` request parameters. + + + +### method `__init__` + +```python +__init__(id: Any) → None +``` + +Method generated by attrs for class CancelTaskRequest. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `CancelTaskResponse` +TES `tesCancelTaskResponse` `attrs` model class. + + + +### method `__init__` + +```python +__init__() → None +``` + +Method generated by attrs for class CancelTaskResponse. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `ListTasksRequest` +`attrs` model class for `GET /tasks` request parameters. + + + +### method `__init__` + +```python +__init__( + project: Any = None, + name_prefix: Any = None, + page_size: Optional[int] = None, + page_token: Any = None, + view: Optional[str] = None +) → None +``` + +Method generated by attrs for class ListTasksRequest. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + +--- + + + +## class `ListTasksResponse` +TES `tesListTasksResponse` `attrs` model class. + + + +### method `__init__` + +```python +__init__(tasks: Optional[List[Task]] = None, next_page_token: Any = None) → None +``` + +Method generated by attrs for class ListTasksResponse. + + + + +--- + + + +### method `as_dict` + +```python +as_dict(drop_empty: bool = True) → Dict[str, Any] +``` + + + + + +--- + + + +### method `as_json` + +```python +as_json(drop_empty: bool = True, **kwargs) → str +``` + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/docs/docstring/utils.md b/docs/docstring/utils.md new file mode 100644 index 0000000..926d36c --- /dev/null +++ b/docs/docstring/utils.md @@ -0,0 +1,117 @@ + + + + +# module `utils` +Exceptions and utilities. + + +--- + + + +## function `camel_to_snake` + +```python +camel_to_snake(name: str) → str +``` + +Converts camelCase to snake_case. + + + +**Args:** + + - `name`: String to convert. + + + +**Returns:** + Converted string. + + +--- + + + +## function `unmarshal` + +```python +unmarshal(j: Any, o: Type, convert_camel_case=True) → Any +``` + +Unmarshal a JSON string to a TES model. + + + +**Args:** + + - `j`: JSON string or dictionary to unmarshal. + - `o`: TES model to unmarshal to. + - `convert_camel_case`: Convert values in `j` from camelCase to snake_case. + + + +**Returns:** + Unmarshalled TES model. + + + +**Raises:** + + - `UnmarshalError`: If `j` cannot be unmarshalled to `o`. + + +--- + + + +## class `UnmarshalError` +Raised when a JSON string cannot be unmarshalled to a TES model. + + + +### method `__init__` + +```python +__init__(*args, **kwargs) +``` + + + + + + + + + +--- + + + +## class `TimeoutError` + + + + + + +### method `__init__` + +```python +__init__(*args, **kwargs) +``` + + + + + + + + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..906634c --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,15 @@ +site_name: py-tes documentation +site_url: https://ohsu-comp-bio.github.io/py-tes +site_author: Kyle Ellrott + +repo_name: py-tes +repo_url: https://github.com/ohsu-comp-bio/py-tes +edit_uri: edit/master/docs +docs_dir: docs + +theme: + name: material + +plugins: + - awesome-pages + - search diff --git a/tes/client.py b/tes/client.py index 2cf8d81..51b852b 100644 --- a/tes/client.py +++ b/tes/client.py @@ -1,3 +1,5 @@ +"""TES access methods and helper functions.""" + import re import requests import time @@ -19,6 +21,7 @@ def process_url(value): @attrs class HTTPClient(object): + """HTTP client class for interacting with the TES API.""" url: str = attrib(converter=process_url) timeout: int = attrib(default=10, validator=instance_of(int)) user: Optional[str] = attrib( @@ -30,6 +33,17 @@ class HTTPClient(object): @url.validator # type: ignore def __check_url(self, attribute, value): + """Validate URL scheme of TES instance. + + `attrs` validator function for `HTTPClient.url`. + + Args: + attribute: Attribute being validated. + value: Attribute value. + + Raises: + ValueError: If URL scheme is unsupported. + """ u = urlparse(value) if u.scheme not in ["http", "https"]: raise ValueError( @@ -38,6 +52,11 @@ def __check_url(self, attribute, value): ) def get_service_info(self) -> ServiceInfo: + """Access method for `GET /service-info`. + + Returns: + `tes.models.ServiceInfo` instance. + """ kwargs: Dict[str, Any] = self._request_params() response: requests.Response = requests.get( f"{self.url}/v1/tasks/service-info", @@ -46,6 +65,17 @@ def get_service_info(self) -> ServiceInfo: return unmarshal(response.json(), ServiceInfo) def create_task(self, task: Task) -> CreateTaskResponse: + """Access method for `POST /tasks`. + + Args: + task: `tes.models.Task` instance. + + Returns: + `tes.models.CreateTaskResponse` instance. + + Raises: + TypeError: If `task` is not a `tes.models.Task` instance. + """ if isinstance(task, Task): msg = task.as_json() else: @@ -60,6 +90,15 @@ def create_task(self, task: Task) -> CreateTaskResponse: return unmarshal(response.json(), CreateTaskResponse).id def get_task(self, task_id: str, view: str = "BASIC") -> Task: + """Access method for `GET /tasks/{id}`. + + Args: + task_id: TES Task ID. + view: Task info verbosity. One of `MINIMAL`, `BASIC` and `FULL`. + + Returns: + `tes.models.Task` instance. + """ req: GetTaskRequest = GetTaskRequest(task_id, view) payload: Dict[str, Optional[str]] = {"view": req.view} kwargs: Dict[str, Any] = self._request_params(params=payload) @@ -70,6 +109,11 @@ def get_task(self, task_id: str, view: str = "BASIC") -> Task: return unmarshal(response.json(), Task) def cancel_task(self, task_id: str) -> None: + """Access method for `POST /tasks/{id}:cancel`. + + Args: + task_id: TES Task ID. + """ req: CancelTaskRequest = CancelTaskRequest(task_id) kwargs: Dict[str, Any] = self._request_params() response: requests.Response = requests.post( @@ -82,6 +126,16 @@ def list_tasks( self, view: str = "MINIMAL", page_size: Optional[int] = None, page_token: Optional[str] = None ) -> ListTasksResponse: + """Access method for `GET /tasks`. + + Args: + view: Task info verbosity. One of `MINIMAL`, `BASIC` and `FULL`. + page_size: Number of tasks to return. + page_token: Token to retrieve the next page of tasks. + + Returns: + `tes.models.ListTasksResponse` instance. + """ req = ListTasksRequest( view=view, page_size=page_size, @@ -123,6 +177,14 @@ def _request_params( self, data: Optional[str] = None, params: Optional[Dict] = None ) -> Dict[str, Any]: + """Compile request parameters. + + Args: + data: JSON payload to be sent in the request body. + + Returns: + Dictionary of request parameters. + """ kwargs: Dict[str, Any] = {} kwargs['timeout'] = self.timeout kwargs['headers'] = {} diff --git a/tes/models.py b/tes/models.py index 8550291..2c891cb 100644 --- a/tes/models.py +++ b/tes/models.py @@ -1,3 +1,5 @@ +"""TES models, converters, validators and helpers.""" + from __future__ import absolute_import, print_function, unicode_literals import dateutil.parser @@ -12,12 +14,12 @@ @attrs class _ListOfValidator(object): + """`attrs` validator class for lists.""" + type: Type = attrib() def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ + """We use a callable class to be able to change the ``__repr__``.""" if not all([isinstance(n, self.type) for n in value]): raise TypeError( "'{attr.name}' must be a list of {self.type!r} (got {value!r} " @@ -30,10 +32,26 @@ def __repr__(self) -> str: def list_of(_type: Any) -> _ListOfValidator: + """`attrs` validator for lists of a given type. + + Args: + _type: Type to validate. + + Returns: + `attrs` validator for the given type. + """ return _ListOfValidator(_type) def _drop_none(obj: Any) -> Any: + """Drop `None` values from a nested data structure. + + Args: + obj: Object to process. + + Returns: + Object with `None` values removed. + """ if isinstance(obj, (list, tuple, set)): return type(obj)(_drop_none(x) for x in obj if x is not None) elif isinstance(obj, dict): @@ -46,6 +64,15 @@ def _drop_none(obj: Any) -> Any: def strconv(value: Any) -> Any: + """Explicitly cast a string-like value or list thereof to string(s). + + Args: + value: Value to convert. + + Returns: + Converted value. If `value` is a list, all elements are converted to + strings. If `value` is not string-like, it will be returned as is. + """ if isinstance(value, (tuple, list)): if all([isinstance(n, str) for n in value]): return [str(n) for n in value] @@ -60,18 +87,45 @@ def strconv(value: Any) -> Any: # since an int64 value is encoded as a string in json we need to handle # conversion def int64conv(value: Optional[str]) -> Optional[int]: + """Convert string to `int64`. + + Args: + value: String to convert. + + Returns: + Converted value. + """ if value is not None: return int(value) return value def timestampconv(value: Optional[str]) -> Optional[datetime]: + """Convert string to `datetime`. + + Args: + value: String to convert. + + Returns: + Converted value. + """ if value is not None: return dateutil.parser.parse(value) return value def datetime_json_handler(x: Any) -> str: + """JSON handler for `datetime` objects. + + Args: + x: Object to convert. + + Returns: + Converted object. + + Raises: + TypeError: If `x` is not a `datetime` object. + """ if isinstance(x, datetime): return x.isoformat() raise TypeError("Unknown type") @@ -79,6 +133,7 @@ def datetime_json_handler(x: Any) -> str: @attrs class Base(object): + """`attrs` base class for all TES and helper models.""" def as_dict(self, drop_empty: bool = True) -> Dict[str, Any]: obj = asdict(self) @@ -96,6 +151,8 @@ def as_json(self, drop_empty: bool = True, **kwargs) -> str: @attrs class Input(Base): + """TES `tesInput` `attrs` model class.""" + url: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -118,6 +175,8 @@ class Input(Base): @attrs class Output(Base): + """TES `tesOutput` `attrs` model class.""" + url: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -137,6 +196,8 @@ class Output(Base): @attrs class Resources(Base): + """TES `tesResources` `attrs` model class.""" + cpu_cores: Optional[int] = attrib( default=None, validator=optional(instance_of(int)) ) @@ -156,6 +217,8 @@ class Resources(Base): @attrs class Executor(Base): + """TES `tesExecutor` `attrs` model class.""" + image: str = attrib( converter=strconv, validator=instance_of(str) ) @@ -181,6 +244,8 @@ class Executor(Base): @attrs class ExecutorLog(Base): + """TES `tesExecutorLog` `attrs` model class.""" + start_time: datetime = attrib( default=None, converter=timestampconv, @@ -204,6 +269,8 @@ class ExecutorLog(Base): @attrs class OutputFileLog(Base): + """TES `tesOutputFileLog` `attrs` model class.""" + url: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -217,6 +284,8 @@ class OutputFileLog(Base): @attrs class TaskLog(Base): + """TES `tesTaskLog` `attrs` model class.""" + start_time: datetime = attrib( default=None, converter=timestampconv, @@ -243,6 +312,8 @@ class TaskLog(Base): @attrs class Task(Base): + """TES `tesTask` `attrs` model class.""" + id: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -287,6 +358,13 @@ class Task(Base): ) def is_valid(self) -> Tuple[bool, Union[None, TypeError]]: + """Validate a `Task` model instance. + + Returns: + A tuple containing a boolean indicating whether the model is + valid, and a `TypeError` if the model is invalid, or `None` if it + is. + """ errs = [] if self.executors is None or len(self.executors) == 0: errs.append("Must provide one or more Executors") @@ -353,6 +431,8 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]: @attrs class GetTaskRequest(Base): + """`attrs` model class for `GET /tasks/{id}` request parameters.""" + id: str = attrib( converter=strconv, validator=instance_of(str) ) @@ -363,6 +443,8 @@ class GetTaskRequest(Base): @attrs class CreateTaskResponse(Base): + """TES `tesCreateTaskResponse` `attrs` model class.""" + id: str = attrib( converter=strconv, validator=instance_of(str) ) @@ -370,11 +452,13 @@ class CreateTaskResponse(Base): @attrs class ServiceInfoRequest(Base): - pass + """`attrs` model class for `GET /service-info` request parameters.""" @attrs class ServiceInfo(Base): + """TES `tesServiceInfo` `attrs` model class.""" + name: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -388,6 +472,8 @@ class ServiceInfo(Base): @attrs class CancelTaskRequest(Base): + """`attrs` model class for `POST /tasks/{id}:cancel` request parameters.""" + id: str = attrib( converter=strconv, validator=instance_of(str) ) @@ -395,11 +481,13 @@ class CancelTaskRequest(Base): @attrs class CancelTaskResponse(Base): - pass + """TES `tesCancelTaskResponse` `attrs` model class.""" @attrs class ListTasksRequest(Base): + """`attrs` model class for `GET /tasks` request parameters.""" + project: Optional[str] = attrib( default=None, converter=strconv, validator=optional(instance_of(str)) ) @@ -419,6 +507,8 @@ class ListTasksRequest(Base): @attrs class ListTasksResponse(Base): + """TES `tesListTasksResponse` `attrs` model class.""" + tasks: Optional[List[Task]] = attrib( default=None, validator=optional(list_of(Task)) ) diff --git a/tes/utils.py b/tes/utils.py index 199543b..c5d4ac2 100644 --- a/tes/utils.py +++ b/tes/utils.py @@ -1,3 +1,5 @@ +"""Exceptions and utilities.""" + import json import re @@ -12,11 +14,20 @@ def camel_to_snake(name: str) -> str: + """Converts camelCase to snake_case. + + Args: + name: String to convert. + + Returns: + Converted string. + """ s1 = first_cap_re.sub(r'\1_\2', name) return all_cap_re.sub(r'\1_\2', s1).lower() class UnmarshalError(Exception): + """Raised when a JSON string cannot be unmarshalled to a TES model.""" def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) @@ -27,6 +38,19 @@ def __init__(self, *args, **kwargs): def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any: + """Unmarshal a JSON string to a TES model. + + Args: + j: JSON string or dictionary to unmarshal. + o: TES model to unmarshal to. + convert_camel_case: Convert values in `j` from camelCase to snake_case. + + Returns: + Unmarshalled TES model. + + Raises: + UnmarshalError: If `j` cannot be unmarshalled to `o`. + """ if isinstance(j, str): m = json.loads(j) elif isinstance(j, dict): diff --git a/tests/requirements.txt b/tests/requirements.txt index 45208ca..e86edc1 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,10 @@ coverage>=6.5.0 coveralls>=3.3.1 flake8>=5.0.4 +lazydocs>=0.4.8 +mkdocs>=1.2.4 +mkdocs-awesome-pages-plugin>=2.8.0 +mkdocs-material>=9.0.12 +pydocstyle>=6.3.0 pytest>=7.2.1 requests_mock>=1.10.0