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