From 6a03539907c204d50e452469cad6eb5202f8b9d3 Mon Sep 17 00:00:00 2001 From: Liam Beckman Date: Fri, 6 Dec 2024 15:37:43 -0800 Subject: [PATCH] =?UTF-8?q?`develop`=20=E2=86=92=20`master`:=201.1.0=20Rel?= =?UTF-8?q?ease=20update=20=F0=9F=90=8D=20=E2=9C=A8=20=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add required files for docs site: https://py-tes.readthedocs.io * Update test suite to remove unittest dependency in favor of pytest (`feature/pytest`) (#70) * Update ReadTheDocs Page * Update tests to use pytest "style/format" tests * Add missing `pytest` import * Update models and test files * Fix linting error in test_client.py * Fix lint error in docs/conf.py * Update version to 1.1.0 in tes/__init__.py * Add `setuptools` to `.readthedocs.yaml` to find the `tes` package * Add initial example Colab notebooks to README * Update README.md * Add release links to README.md --- .github/workflows/docs.yml | 3 +- .github/workflows/tests.yml | 18 +- .readthedocs.yaml | 34 ++ README.md | 135 ++++--- docs/Makefile | 177 +++++++++ docs/_static/custom.css | 83 +++++ docs/_static/custom.js | 28 ++ docs/_static/logo-ga4gh-dark.png | Bin 0 -> 24286 bytes docs/_static/logo-ga4gh-light.png | Bin 0 -> 147514 bytes docs/_static/logo-snake.svg | 109 ++++++ docs/_static/sphinx-argparse.css | 6 + docs/_templates/page.html | 10 + docs/api/tes.rst | 34 ++ docs/api_docs/tes.rst | 34 ++ docs/conf.py | 371 +++++++++++++++++++ docs/index.rst | 77 ++++ docs/requirements.txt | 17 +- examples/example.py | 26 ++ examples/v1_0_0.ipynb | 137 +++++++ examples/v1_1_0.ipynb | 137 +++++++ requirements.txt | 8 +- setup.py | 5 +- tes/__init__.py | 2 +- tes/models.py | 4 +- tes/utils.py | 2 +- tests/integration/test_funnel.py | 72 ++-- tests/test_client.py | 594 ++++++++++++++---------------- tests/test_models.py | 307 ++++++++------- tests/test_utils.py | 509 +++++++++++++------------ 29 files changed, 2113 insertions(+), 826 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/custom.js create mode 100644 docs/_static/logo-ga4gh-dark.png create mode 100644 docs/_static/logo-ga4gh-light.png create mode 100644 docs/_static/logo-snake.svg create mode 100644 docs/_static/sphinx-argparse.css create mode 100644 docs/_templates/page.html create mode 100644 docs/api/tes.rst create mode 100644 docs/api_docs/tes.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 examples/example.py create mode 100644 examples/v1_0_0.ipynb create mode 100644 examples/v1_1_0.ipynb diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 26c23f9..5172e0a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,4 +33,5 @@ jobs: tes - name: Build docs - run: mkdocs build + run: mkdocs build && mkdocs gh-deploy --force + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8425a66..72b2116 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,19 @@ jobs: strategy: fail-fast: false matrix: - version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + # TODO: Handle multiple TES versions here, currently we're only testing against TES 1.1 + # - py-tes v1.0 — TES 1.0 + # - py-tes v1.1 — TES 1.1 + # tes-version: + # - "1.0" + # - "1.1" + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" steps: - name: Check out code @@ -18,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.version }} + python-version: ${{ matrix.python-version }} - name: Install requirements run: | @@ -42,6 +54,6 @@ jobs: - name: Run integration tests run: | - /bin/bash -c "$(curl -fsSL https://github.com/ohsu-comp-bio/funnel/releases/download/0.11.0-rc.5/install.sh)" -- 0.11.0-rc.5 + /bin/bash -c "$(curl -fsSL https://github.com/ohsu-comp-bio/funnel/releases/latest/download/install.sh)" funnel server --LocalStorage.AllowedDirs $HOME run & pytest tests/integration diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..5ad5180 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,34 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: setuptools + path: . + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index d35a00c..ff37eb3 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,58 @@ # py-tes 🐍 -[![GitHub Actions Test Status](https://img.shields.io/github/actions/workflow/status/ohsu-comp-bio/py-tes/tests.yml?logo=github)](https://github.com/ohsu-comp-bio/py-tes/actions) [![image](https://coveralls.io/repos/github/ohsu-comp-bio/py-tes/badge.svg?branch=master)](https://coveralls.io/github/ohsu-comp-bio/py-tes?branch=master) [![image](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Build Status][build-badge]][build] -*py-tes* is a library for interacting with servers implementing the -[GA4GH Task Execution -Schema](https://github.com/ga4gh/task-execution-schemas). +[![Test Coverage][coverage-badge]][coverage] -## Install ⚡ +[![License][license-badge]][license] -Available on [PyPI](https://pypi.org/project/py-tes/). +[![PyPI][pypi-badge]][pypi] - pip install py-tes +[build-badge]: https://img.shields.io/github/actions/workflow/status/ohsu-comp-bio/py-tes/tests.yml?logo=github +[build]: https://github.com/ohsu-comp-bio/py-tes/actions +[coverage-badge]: https://coveralls.io/repos/github/ohsu-comp-bio/py-tes/badge.svg?branch=master +[coverage]: https://coveralls.io/github/ohsu-comp-bio/py-tes?branch=master +[license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg +[license]: https://opensource.org/licenses/MIT +[pypi-badge]: https://img.shields.io/pypi/v/py-tes +[pypi]: https://pypi.org/project/py-tes/ -## Example ✍️ +_py-tes_ is a library for interacting with servers implementing the [GA4GH Task Execution Schema](https://github.com/ga4gh/task-execution-schemas). +# Quick Start ⚡ -``` python +| TES version | py-tes version | Example Notebook (_Coming soon!_) | +|-----------------|------------------------|-----------------------------------------------| +| [1.1][tes-v1.1] | [1.1.0][py-tes-v1.1.0] | [![Open in Colab][colab-badge]][colab-v1.1.0] | +| [1.0][tes-v1.1] | [1.0.0][py-tes-v1.0.0] | [![Open in Colab][colab-badge]][colab-v1.0.0] | + +[tes-v1.1]: https://github.com/ga4gh/task-execution-schemas/releases/tag/v1.1 +[tes-v1.0]: https://github.com/ga4gh/task-execution-schemas/releases/tag/v1.1 + +[py-tes-v1.1.0]: https://github.com/ohsu-comp-bio/py-tes/releases/tag/1.1.0 +[py-tes-v1.0.0]: https://github.com/ohsu-comp-bio/py-tes/releases/tag/1.0.0 + +[colab-badge]: https://colab.research.google.com/assets/colab-badge.svg +[colab-v1.1.0]: https://colab.research.google.com/github/ohsu-comp-bio/py-tes/blob/develop/examples/v1_1_0.ipynb +[colab-v1.0.0]: https://colab.research.google.com/github/ohsu-comp-bio/py-tes/blob/develop/examples/v1_0_0.ipynb + +# Installation 🌀 + +Install `py-tes` from [PyPI](https://pypi.org/project/py-tes/) and run it in your script: + +```sh +➜ pip install py-tes + +➜ python example.py +``` + +## example.py 🐍 + +```py import tes +import json -# define task +# Define task task = tes.Task( executors=[ tes.Executor( @@ -28,89 +62,90 @@ task = tes.Task( ] ) -# create client -cli = tes.HTTPClient("https://tes.example.com", timeout=5) +# Create client +cli = tes.HTTPClient("http://localhost:8000", timeout=5) -# access endpoints -service_info = cli.get_service_info() +# Create and run task task_id = cli.create_task(task) +cli.wait(task_id, timeout=5) + +# Fetch task info task_info = cli.get_task(task_id, view="BASIC") -cli.cancel_task(task_id) -tasks_list = cli.list_tasks(view="MINIMAL") # default view +j = json.loads(task_info.as_json()) + +# Pretty print task info +print(json.dumps(j, indent=2)) ``` -## How to... +# How to... > Makes use of the objects above... -### ...export a model to a dictionary +## ...export a model to a dictionary -``` python +```python task_dict = task.as_dict(drop_empty=False) ``` `task_dict` contents: -``` console +```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 +## ...export a model to JSON -``` python +```python task_json = task.as_json() # also accepts `drop_empty` arg ``` `task_json` contents: -``` console +```console {"executors": [{"image": "alpine", "command": ["echo", "hello"]}]} ``` -### ...pretty print a model +## ...pretty print a model -``` python +```python print(task.as_json(indent=3)) # keyword args are passed to `json.dumps()` ``` Output: -``` json +```json { - "executors": [ - { - "image": "alpine", - "command": [ - "echo", - "hello" - ] - } - ] + "executors": [ + { + "image": "alpine", + "command": ["echo", "hello"] + } + ] } ``` -### ...access a specific task from the task list +## ...access a specific task from the task list -``` python +```py specific_task = tasks_list.tasks[5] ``` `specific_task` contents: -``` console +```sh 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 +## ...iterate over task list items -``` python +```py for t in tasks_list[:3]: print(t.as_json(indent=3)) ``` Output: -``` console +```sh { "id": "task_A2GFS4", "state": "RUNNING" @@ -125,31 +160,31 @@ Output: } ``` -### ...instantiate a model from a JSON representation +## ...instantiate a model from a JSON representation -``` python +```py task_from_json = tes.client.unmarshal(task_json, tes.Task) ``` `task_from_json` contents: -``` console +```sh 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 +```py print(task_from_json == task) ``` Output: -``` console +```sh True ``` -## Additional Resources 📚 +# Additional Resources 📚 - [ga4gh-tes](https://github.com/microsoft/ga4gh-tes) : C# implementation of the GA4GH TES API; provides distributed batch task execution on Microsoft Azure @@ -161,8 +196,8 @@ True - [Nextflow](https://www.nextflow.io/): Nextflow enables scalable and reproducible scientific workflows using software containers. It allows the adaptation of pipelines written in the most common scripting languages. -- [GA4GH TES](https://www.ga4gh.org/product/task-execution-service-tes/): Main page for the Task Execution Schema — a standardized schema and API for describing batch execution tasks. +- [GA4GH TES](https://www.ga4gh.org/product/task-execution-service-tes/): Main page for the Task Execution Schema — a standardized schema and API for describing batch execution tasks. -- [TES GitHub](https://github.com/ga4gh/task-execution-schemas): Source repo for the Task Execution Schema +- [TES GitHub](https://github.com/ga4gh/task-execution-schemas): Source repo for the Task Execution Schema -- [Awesome TES](https://github.com/ohsu-comp-bio/awesome-tes): A curated list of awesome GA4GH TES projects and programs +- [Awesome TES](https://github.com/ohsu-comp-bio/awesome-tes): A curated list of awesome GA4GH TES projects and programs diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..35844d9 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Snakemake.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Snakemake.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Snakemake" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Snakemake" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..99e61fe --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,83 @@ +.image-reference img { + display: inline; + background: none !important; +} + +.image-reference { + border-bottom: none !important; +} + +:root { + --color-links: #047857; + --color-brand: #047857; +} + +#content { + overflow-x: auto; +} + +.sticky.top-16 { + overflow: unset; + } + +/* Light theme styles (default) */ +body.dark-theme { + background-color: black !important; +} + +/* Dark theme styles */ +body.light-theme { + background-color: blue !important; +} + +pre.light-theme .kn { color: #2838b0 !important} /* Keyword.Namespace */ +pre.light-theme .kp { color: #2838b0 !important} /* Keyword.Pseudo */ +pre.light-theme .kr { color: #2838b0 !important} /* Keyword.Reserved */ +pre.light-theme .kt { color: #2838b0; font-style: italic !important} /* Keyword.Type */ +pre.light-theme .m { color: #444444 !important} /* Literal.Number */ +pre.light-theme .s { color: #b83838 !important} /* Literal.String */ +pre.light-theme .na { color: #388038 !important} /* Name.Attribute */ +pre.light-theme .nb { color: #388038 !important} /* Name.Builtin */ +pre.light-theme .nc { color: #287088 !important} /* Name.Class */ +pre.light-theme .no { color: #b85820 !important} /* Name.Constant */ +pre.light-theme .nd { color: #287088 !important} /* Name.Decorator */ +pre.light-theme .ni { color: #709030 !important} /* Name.Entity */ +pre.light-theme .ne { color: #908828 !important} /* Name.Exception */ +pre.light-theme .nf { color: #785840 !important} /* Name.Function */ +pre.light-theme .nl { color: #289870 !important} /* Name.Label */ +pre.light-theme .nn { color: #289870 !important} /* Name.Namespace */ +pre.light-theme .nt { color: #2838b0 !important} /* Name.Tag */ +pre.light-theme .nv { color: #b04040 !important} /* Name.Variable */ +pre.light-theme .ow { color: #a848a8 !important} /* Operator.Word */ +pre.light-theme .pm { color: #888888 !important} /* Punctuation.Marker */ +pre.light-theme .w { color: #a89028 !important} /* Text.Whitespace */ +pre.light-theme .mb { color: #444444 !important} /* Literal.Number.Bin */ +pre.light-theme .mf { color: #444444 !important} /* Literal.Number.Float */ +pre.light-theme .mh { color: #444444 !important} /* Literal.Number.Hex */ +pre.light-theme .mi { color: #444444 !important} /* Literal.Number.Integer */ +pre.light-theme .mo { color: #444444 !important} /* Literal.Number.Oct */ +pre.light-theme .sa { color: #444444 !important} /* Literal.String.Affix */ +pre.light-theme .sb { color: #b83838 !important} /* Literal.String.Backtick */ +pre.light-theme .sc { color: #a848a8 !important} /* Literal.String.Char */ +pre.light-theme .dl { color: #b85820 !important} /* Literal.String.Delimiter */ +pre.light-theme .sd { color: #b85820; font-style: italic !important} /* Literal.String.Doc */ +pre.light-theme .s2 { color: #b83838 !important} /* Literal.String.Double */ +pre.light-theme .se { color: #709030 !important} /* Literal.String.Escape */ +pre.light-theme .sh { color: #b83838 !important} /* Literal.String.Heredoc */ +pre.light-theme .si { color: #b83838; text-decoration: underline !important} /* Literal.String.Interpol */ +pre.light-theme .sx { color: #a848a8 !important} /* Literal.String.Other */ +pre.light-theme .sr { color: #a848a8 !important} /* Literal.String.Regex */ +pre.light-theme .s1 { color: #b83838 !important} /* Literal.String.Single */ +pre.light-theme .ss { color: #b83838 !important} /* Literal.String.Symbol */ +pre.light-theme .bp { color: #388038; font-style: italic !important} /* Name.Builtin.Pseudo */ +pre.light-theme .fm { color: #b85820 !important} /* Name.Function.Magic */ +pre.light-theme .vc { color: #b04040 !important} /* Name.Variable.Class */ +pre.light-theme .vg { color: #908828 !important} /* Name.Variable.Global */ +pre.light-theme .vi { color: #b04040 !important} /* Name.Variable.Instance */ +pre.light-theme .vm { color: #b85820 !important} /* Name.Variable.Magic */ +pre.light-theme .il { color: #444444 !important} /* Literal.Number.Integer.Long */ +pre.light-theme .n { color: #444444 !important} /* Misc */ +pre.light-theme .o { color: #444 !important} /* Misc */ +pre.light-theme .p { color: #444 !important} /* Misc */ +pre.light-theme .k { color: #a848a8 !important} /* Misc */ +pre.light-theme .kc { color: #2838b0 !important} /* Misc */ \ No newline at end of file diff --git a/docs/_static/custom.js b/docs/_static/custom.js new file mode 100644 index 0000000..295b9b5 --- /dev/null +++ b/docs/_static/custom.js @@ -0,0 +1,28 @@ +// Select the button using its class, assuming it's the only one with this class +const nav = document.querySelector(".flex.items-center.space-x-1"); +console.log(nav); + +const themeToggleButton = nav.childNodes[3]; +console.log(themeToggleButton); + +mode = localStorage.getItem('darkMode'); +setTheme(mode); + +// Add an event listener to the button +themeToggleButton.addEventListener('click', function() { + mode = mode === 'light' ? 'dark' : 'light' + setTheme(mode); +}); + +function setTheme(mode) { + var pres = document.body.getElementsByTagName("pre"); + for (let pre of pres) { + if (mode === 'dark') { + pre.classList.add('dark-theme'); + pre.classList.remove('light-theme'); + } else { + pre.classList.add('light-theme'); + pre.classList.remove('dark-theme'); + } + } +} diff --git a/docs/_static/logo-ga4gh-dark.png b/docs/_static/logo-ga4gh-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a18b4700337e8f8010a257eeb22eea571c15a77b GIT binary patch literal 24286 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

C$rRqKB}u;}SuYWG@9)n0f`3hKI{S zH|@pG!H7*Xb7&*4wQ3^PH{HGYsMh=#f`Huk-m4E?9TLT-H&|A(Cg3z|;1Z2lUNiDa zDSw)g%H{+7bLy7X3-h**L;#R|>uRLFFlf$?>NT+Da6B4k&@8{X%A=jA5F-jsXnfH- z=T29pL)P20nSMna`u16B;Zz<&^!0KxtcC0|e_Rfeh~xKzq?jk3)Xc?TsLpZ^%+FS- zCTq?zge}}WAmTB_v#>+$(u&F_?|1M3Y&DUWJ4}hbQE>!9c`aFZ-N};QTc+)S>3h+5 zaOP!fI|HZ8O53kGc?2(I$-NueA@m6}-bQ||RI|I$lRDIgGLNW5u0>KxeDkYD?SVfq zZTNMc#I#1z-)ZbAH#YD1Oxx-A7^AYmK?Z9}rJ<{f@P`*5-{ zRy0D9;nYP0Q2%f~%rL3N>EM1mg0yCLIw|zvVm+Y+0}T7~zW4TAU`&im1c@2OrbN|W z3eo>GS;#k1XfJg5Tc$hocPX+#Y2$Coa#8j!N|TjjPwCnjUB|#y3A8Qet!Yf=LUA0k zC3nG;FooE8-GHx>WF(U)h_TvPvCn*>qb+1sK=fPc;(ZAN@!7Dv$+r%pc#4Bml zaix4-r5O9s38HmuxsyO>$ThV1dTT&!8j34Jt7<1_oL-kM{%vg)P(_C37Zt0;E*k+O z|4|Y9TnWLnckzZUi7eGG66bg-c@GlkfmlhEdk?*-n9dfk-~*+X*ntX(_7eyEQ*<

YqugQv&SwWKCaRW=zB;e^5 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

I_0C|2t5ama`H>wt=7lof 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 nj2ajvWifWL_miuIY&Vx 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 z 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%0DTN2wWq=YJWisk_SlRhR=-+JbsA8~)gS4v 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 + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/sphinx-argparse.css b/docs/_static/sphinx-argparse.css new file mode 100644 index 0000000..70ce1ab --- /dev/null +++ b/docs/_static/sphinx-argparse.css @@ -0,0 +1,6 @@ +.wy-table-responsive table td { + white-space: normal !important; +} +.wy-table-responsive { + overflow: visible !important; +} diff --git a/docs/_templates/page.html b/docs/_templates/page.html new file mode 100644 index 0000000..453f385 --- /dev/null +++ b/docs/_templates/page.html @@ -0,0 +1,10 @@ +{% extends "!page.html" %} + +{{ super() }} + +{% block extrahead %} + +{% endblock %} diff --git a/docs/api/tes.rst b/docs/api/tes.rst new file mode 100644 index 0000000..77c2a26 --- /dev/null +++ b/docs/api/tes.rst @@ -0,0 +1,34 @@ +py-tes +=========== + +.. toctree:: + :caption: API Docs + :name: api_docs + :hidden: + :maxdepth: 1 + + api_docs/tes + +tes.client module +----------------- + +.. automodule:: tes.client + :members: + :undoc-members: + :show-inheritance: + +tes.models module +----------------- + +.. automodule:: tes.models + :members: + :undoc-members: + :show-inheritance: + +tes.utils module +---------------- + +.. automodule:: tes.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api_docs/tes.rst b/docs/api_docs/tes.rst new file mode 100644 index 0000000..77c2a26 --- /dev/null +++ b/docs/api_docs/tes.rst @@ -0,0 +1,34 @@ +py-tes +=========== + +.. toctree:: + :caption: API Docs + :name: api_docs + :hidden: + :maxdepth: 1 + + api_docs/tes + +tes.client module +----------------- + +.. automodule:: tes.client + :members: + :undoc-members: + :show-inheritance: + +tes.models module +----------------- + +.. automodule:: tes.models + :members: + :undoc-members: + :show-inheritance: + +tes.utils module +---------------- + +.. automodule:: tes.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8842ce3 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Snakemake documentation build configuration file, created by +# sphinx-quickstart on Sat Feb 1 16:01:02 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +from sphinxawesome_theme.postprocess import Icons +import tes + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath("../")) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinxarg.ext", + "sphinx.ext.autosectionlabel", + "myst_parser", + "sphinxawesome_theme.highlighting", +] + +html_css_files = ["custom.css"] +html_js_files = ["custom.js"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = [".rst", ".md"] + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "py-tes" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = tes.__version__ + +if os.environ.get("READTHEDOCS") == "True": + # Because Read The Docs modifies conf.py, versioneer gives a "dirty" + # version like "5.10.0+0.g28674b1.dirty" that is cleaned here. + version = version.partition("+0.g")[0] + +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build", "apidocs"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "material" +pygments_dark_style = "material" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinxawesome_theme" +html_theme_options = { + "show_breadcrumbs": True, + "logo_light": "logo-ga4gh-light.png", + "logo_dark": "logo-ga4gh-dark.png", + "main_nav_links": { + "About": "https://github.com/ohsu-comp-bio/py-tes", + "Docs": "https://github.com/ohsu-comp-bio/py-tes", + "GitHub": "https://github.com/ohsu-comp-bio/py-tes", + }, + "awesome_external_links": True, + "awesome_headerlinks": True, + "show_prev_next": False, + "extra_header_link_icons": { + "repository on GitHub": { + "link": "https://github.com/ohsu-comp-bio/py-tes", + "icon": ( + '' + '' + ), + }, + }, +} +html_permalinks_icon = Icons.permalinks_icon +# html_theme_options = { +# "show_nav_level": 2, +# "header_links_before_dropdown": 0, +# "external_links": [ +# { +# "text": "Snakemake plugin catalog", +# "alt": "Snakemake plugin catalog", +# "href": "https://snakemake.github.io/snakemake-plugin-catalog", +# }, +# { +# "text": "Snakemake workflow catalog", +# "alt": "Snakemake workflow catalog", +# "href": "https://snakemake.github.io/snakemake-workflow-catalog", +# }, +# { +# "text": "Snakemake wrappers", +# "alt": "Snakemake wrappers", +# "href": "https://snakemake-wrappers.readthedocs.io", +# }, +# ], +# } +# html_theme_options = { +# "primary_color": "emerald", +# "secondary_color": "emerald", +# "dark_logo": "logo-snake.svg", +# "light_logo": "logo-snake.svg", +# "navigation_style": "plain", +# "sidebar_links": [ +# { +# "text": "Snakemake plugin catalog", +# "alt": "Snakemake plugin catalog", +# "href": "https://snakemake.github.io/snakemake-plugin-catalog", +# }, +# { +# "text": "Snakemake workflow catalog", +# "alt": "Snakemake workflow catalog", +# "href": "https://snakemake.github.io/snakemake-workflow-catalog", +# }, +# { +# "text": "Snakemake wrappers", +# "alt": "Snakemake wrappers", +# "href": "https://snakemake-wrappers.readthedocs.io", +# }, +# ], +# } + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = ["_static/css"] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {"index": "index.html"} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +# htmlhelp_basename = "Snakemakedoc" + + +# -- Options for LaTeX output --------------------------------------------- + +# latex_elements = { +# # The paper size ('letterpaper' or 'a4paper'). +# #'papersize': 'letterpaper', +# # The font size ('10pt', '11pt' or '12pt'). +# #'pointsize': '10pt', +# # Additional stuff for the LaTeX preamble. +# #'preamble': '', +# } + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +# latex_documents = [ +# ("index", "Snakemake.tex", "Snakemake Documentation", "Johannes Koester", "manual"), +# ] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +# man_pages = [("index", "snakemake", "Snakemake Documentation", ["Johannes Koester"], 1)] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +# texinfo_documents = [ +# ( +# "index", +# "Snakemake", +# "Snakemake Documentation", +# "Johannes Koester", +# "Snakemake", +# "One line description of project.", +# "Miscellaneous", +# ), +# ] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +def setup(app): + app.add_css_file("sphinx-argparse.css") diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..db2e882 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,77 @@ +.. automodule:: tes + +.. _manual-main: + +====== +py-tes +====== + +.. image:: https://travis-ci.org/ohsu-comp-bio/py-tes.svg?branch=master + :target: https://travis-ci.org/ohsu-comp-bio/py-tes +.. image:: https://coveralls.io/repos/github/ohsu-comp-bio/py-tes/badge.svg?branch=master + :target: https://coveralls.io/github/ohsu-comp-bio/py-tes?branch=master +.. image:: https://img.shields.io/badge/License-MIT-yellow.svg + :target: https://opensource.org/licenses/MIT + +*py-tes* is a library for interacting with servers implementing the +`GA4GH Task Execution +Schema `__. + +Install +~~~~~~~ + +Available on `PyPI `__. + +:: + + pip install py-tes + +Example +~~~~~~~ + +:: + + import tes + + task = tes.Task( + executors=[ + tes.Executor( + image="alpine", + command=["echo", "hello"] + ) + ] + ) + + cli = tes.HTTPClient("http://funnel.example.com", timeout=5) + task_id = cli.create_task(task) + res = cli.get_task(task_id) + cli.cancel_task(task_id) + + +.. _main-support: + +------- +Support +------- + +* For releases, see :ref:`Changelog `. +* Check :ref:`frequently asked questions (FAQ) `. +* For **bugs and feature requests**, please use the `issue tracker `_. +* For **contributions**, visit py-tes on `Github `_ and read the :ref:`guidelines `. + +.. _main-resources: + +--------- +Resources +--------- + +`Snakemake Wrappers Repository `_ + The Snakemake Wrapper Repository is a collection of reusable wrappers that allow to quickly use popular tools from Snakemake rules and workflows. + +.. toctree:: + :caption: API + :name: api_docs + :hidden: + :maxdepth: 1 + + api_docs/tes diff --git a/docs/requirements.txt b/docs/requirements.txt index 1833352..639402e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,12 @@ -lazydocs>=0.4.8 -mkdocs>=1.4.2 -mkdocs-awesome-pages-plugin>=2.8.0 -mkdocs-material>=9.0.12 -pydocstyle>=6.3.0 +sphinx >=3 +sphinxcontrib-napoleon +sphinx-argparse +docutils +myst-parser +configargparse +appdirs +immutables +sphinxawesome-theme +snakemake-interface-common +snakemake-interface-executor-plugins +snakemake-interface-storage-plugins \ No newline at end of file diff --git a/examples/example.py b/examples/example.py new file mode 100644 index 0000000..47e2b04 --- /dev/null +++ b/examples/example.py @@ -0,0 +1,26 @@ +import tes +import json + +# Define task +task = tes.Task( + executors=[ + tes.Executor( + image="alpine", + command=["echo", "hello"] + ) + ] +) + +# Create client +cli = tes.HTTPClient("http://localhost:8000", timeout=5) + +# Create and run task +task_id = cli.create_task(task) +cli.wait(task_id, timeout=5) + +# Fetch task info +task_info = cli.get_task(task_id, view="BASIC") +j = json.loads(task_info.as_json()) + +# Pretty print task info +print(json.dumps(j, indent=2)) diff --git a/examples/v1_0_0.ipynb b/examples/v1_0_0.ipynb new file mode 100644 index 0000000..5b8f196 --- /dev/null +++ b/examples/v1_0_0.ipynb @@ -0,0 +1,137 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "LmPwTqX3JR5p", + "outputId": "08655348-909d-4be5-9473-195631aed7dd" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting py-tes==1.1.0rc3\n", + " Downloading py_tes-1.1.0rc3-py3-none-any.whl.metadata (6.5 kB)\n", + "Requirement already satisfied: attrs>=17.4.0 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (24.2.0)\n", + "Requirement already satisfied: future>=0.16.0 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (1.0.0)\n", + "Requirement already satisfied: python-dateutil>=2.6.1 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (2.8.2)\n", + "Requirement already satisfied: requests>=2.18.2 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (2.32.3)\n", + "Collecting sphinx_rtd_theme (from py-tes==1.1.0rc3)\n", + " Downloading sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl.metadata (4.4 kB)\n", + "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.6.1->py-tes==1.1.0rc3) (1.16.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (3.4.0)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (2.2.3)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (2024.8.30)\n", + "Requirement already satisfied: sphinx<9,>=6 in /usr/local/lib/python3.10/dist-packages (from sphinx_rtd_theme->py-tes==1.1.0rc3) (8.1.3)\n", + "Requirement already satisfied: docutils<0.22,>0.18 in /usr/local/lib/python3.10/dist-packages (from sphinx_rtd_theme->py-tes==1.1.0rc3) (0.21.2)\n", + "Collecting sphinxcontrib-jquery<5,>=4 (from sphinx_rtd_theme->py-tes==1.1.0rc3)\n", + " Downloading sphinxcontrib_jquery-4.1-py2.py3-none-any.whl.metadata (2.6 kB)\n", + "Requirement already satisfied: sphinxcontrib-applehelp>=1.0.7 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-devhelp>=1.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-htmlhelp>=2.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.1.0)\n", + "Requirement already satisfied: sphinxcontrib-jsmath>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.0.1)\n", + "Requirement already satisfied: sphinxcontrib-qthelp>=1.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-serializinghtml>=1.1.9 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: Jinja2>=3.1 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (3.1.4)\n", + "Requirement already satisfied: Pygments>=2.17 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.18.0)\n", + "Requirement already satisfied: snowballstemmer>=2.2 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.2.0)\n", + "Requirement already satisfied: babel>=2.13 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.16.0)\n", + "Requirement already satisfied: alabaster>=0.7.14 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.0.0)\n", + "Requirement already satisfied: imagesize>=1.3 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.4.1)\n", + "Requirement already satisfied: packaging>=23.0 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (24.2)\n", + "Requirement already satisfied: tomli>=2 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.2.1)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from Jinja2>=3.1->sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (3.0.2)\n", + "Downloading py_tes-1.1.0rc3-py3-none-any.whl (12 kB)\n", + "Downloading sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl (7.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m7.7/7.7 MB\u001b[0m \u001b[31m37.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading sphinxcontrib_jquery-4.1-py2.py3-none-any.whl (121 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m121.1/121.1 kB\u001b[0m \u001b[31m7.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hInstalling collected packages: sphinxcontrib-jquery, sphinx_rtd_theme, py-tes\n", + "Successfully installed py-tes-1.1.0rc3 sphinx_rtd_theme-3.0.2 sphinxcontrib-jquery-4.1\n" + ] + } + ], + "source": [ + "!pip install py-tes==1.0.0" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 373 + }, + "id": "nhMb-oGFKTms", + "outputId": "d4a059f6-0384-4051-eabb-b1d628cb3499" + }, + "outputs": [ + { + "ename": "HTTPError", + "evalue": "404 Client Error: Not Found for url: https://development.aced-idp.org/funnel/tasks", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mHTTPError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# Create and run task\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0mtask_id\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcli\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcreate_task\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0mcli\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwait\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/tes/client.py\u001b[0m in \u001b[0;36mcreate_task\u001b[0;34m(self, task)\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_request_params\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 169\u001b[0m \u001b[0mpaths\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mappend_suffixes_to_url\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0murls\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m\"/tasks\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 170\u001b[0;31m response = send_request(paths=paths, method='post',\n\u001b[0m\u001b[1;32m 171\u001b[0m kwargs_requests=kwargs)\n\u001b[1;32m 172\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0munmarshal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjson\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mCreateTaskResponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/tes/client.py\u001b[0m in \u001b[0;36msend_request\u001b[0;34m(paths, method, kwargs_requests, **kwargs)\u001b[0m\n\u001b[1;32m 90\u001b[0m raise requests.exceptions.HTTPError(\n\u001b[1;32m 91\u001b[0m f\"No response received; HTTP Exceptions: {http_exceptions}\")\n\u001b[0;32m---> 92\u001b[0;31m \u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_for_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 93\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 94\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/requests/models.py\u001b[0m in \u001b[0;36mraise_for_status\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1022\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1023\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mhttp_error_msg\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1024\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mHTTPError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhttp_error_msg\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1025\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1026\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mHTTPError\u001b[0m: 404 Client Error: Not Found for url: https://development.aced-idp.org/funnel/tasks" + ] + } + ], + "source": [ + "import tes\n", + "import json\n", + "\n", + "# Define task\n", + "task = tes.Task(\n", + " executors=[\n", + " tes.Executor(\n", + " image=\"alpine\",\n", + " command=[\"echo\", \"hello\"]\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Create client\n", + "# TODO: Replace https://tes.example.com with live TES API site\n", + "cli = tes.HTTPClient(\"https://tes.example.com\", timeout=5)\n", + "\n", + "# Create and run task\n", + "task_id = cli.create_task(task)\n", + "cli.wait(task_id, timeout=5)\n", + "\n", + "# Fetch task info\n", + "task_info = cli.get_task(task_id, view=\"BASIC\")\n", + "j = json.loads(task_info.as_json())\n", + "\n", + "# Pretty print task info\n", + "print(json.dumps(j, indent=2))" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/v1_1_0.ipynb b/examples/v1_1_0.ipynb new file mode 100644 index 0000000..95b2cdd --- /dev/null +++ b/examples/v1_1_0.ipynb @@ -0,0 +1,137 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LmPwTqX3JR5p", + "outputId": "08655348-909d-4be5-9473-195631aed7dd", + "collapsed": true + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Collecting py-tes==1.1.0rc3\n", + " Downloading py_tes-1.1.0rc3-py3-none-any.whl.metadata (6.5 kB)\n", + "Requirement already satisfied: attrs>=17.4.0 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (24.2.0)\n", + "Requirement already satisfied: future>=0.16.0 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (1.0.0)\n", + "Requirement already satisfied: python-dateutil>=2.6.1 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (2.8.2)\n", + "Requirement already satisfied: requests>=2.18.2 in /usr/local/lib/python3.10/dist-packages (from py-tes==1.1.0rc3) (2.32.3)\n", + "Collecting sphinx_rtd_theme (from py-tes==1.1.0rc3)\n", + " Downloading sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl.metadata (4.4 kB)\n", + "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.6.1->py-tes==1.1.0rc3) (1.16.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (3.4.0)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (2.2.3)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests>=2.18.2->py-tes==1.1.0rc3) (2024.8.30)\n", + "Requirement already satisfied: sphinx<9,>=6 in /usr/local/lib/python3.10/dist-packages (from sphinx_rtd_theme->py-tes==1.1.0rc3) (8.1.3)\n", + "Requirement already satisfied: docutils<0.22,>0.18 in /usr/local/lib/python3.10/dist-packages (from sphinx_rtd_theme->py-tes==1.1.0rc3) (0.21.2)\n", + "Collecting sphinxcontrib-jquery<5,>=4 (from sphinx_rtd_theme->py-tes==1.1.0rc3)\n", + " Downloading sphinxcontrib_jquery-4.1-py2.py3-none-any.whl.metadata (2.6 kB)\n", + "Requirement already satisfied: sphinxcontrib-applehelp>=1.0.7 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-devhelp>=1.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-htmlhelp>=2.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.1.0)\n", + "Requirement already satisfied: sphinxcontrib-jsmath>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.0.1)\n", + "Requirement already satisfied: sphinxcontrib-qthelp>=1.0.6 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: sphinxcontrib-serializinghtml>=1.1.9 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.0.0)\n", + "Requirement already satisfied: Jinja2>=3.1 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (3.1.4)\n", + "Requirement already satisfied: Pygments>=2.17 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.18.0)\n", + "Requirement already satisfied: snowballstemmer>=2.2 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.2.0)\n", + "Requirement already satisfied: babel>=2.13 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.16.0)\n", + "Requirement already satisfied: alabaster>=0.7.14 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.0.0)\n", + "Requirement already satisfied: imagesize>=1.3 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (1.4.1)\n", + "Requirement already satisfied: packaging>=23.0 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (24.2)\n", + "Requirement already satisfied: tomli>=2 in /usr/local/lib/python3.10/dist-packages (from sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (2.2.1)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from Jinja2>=3.1->sphinx<9,>=6->sphinx_rtd_theme->py-tes==1.1.0rc3) (3.0.2)\n", + "Downloading py_tes-1.1.0rc3-py3-none-any.whl (12 kB)\n", + "Downloading sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl (7.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m7.7/7.7 MB\u001b[0m \u001b[31m37.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading sphinxcontrib_jquery-4.1-py2.py3-none-any.whl (121 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m121.1/121.1 kB\u001b[0m \u001b[31m7.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hInstalling collected packages: sphinxcontrib-jquery, sphinx_rtd_theme, py-tes\n", + "Successfully installed py-tes-1.1.0rc3 sphinx_rtd_theme-3.0.2 sphinxcontrib-jquery-4.1\n" + ] + } + ], + "source": [ + "!pip install py-tes==1.1.0" + ] + }, + { + "cell_type": "code", + "source": [ + "import tes\n", + "import json\n", + "\n", + "# Define task\n", + "task = tes.Task(\n", + " executors=[\n", + " tes.Executor(\n", + " image=\"alpine\",\n", + " command=[\"echo\", \"hello\"]\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Create client\n", + "# TODO: Replace https://tes.example.com with live TES API site\n", + "cli = tes.HTTPClient(\"https://tes.example.com\", timeout=5)\n", + "\n", + "# Create and run task\n", + "task_id = cli.create_task(task)\n", + "cli.wait(task_id, timeout=5)\n", + "\n", + "# Fetch task info\n", + "task_info = cli.get_task(task_id, view=\"BASIC\")\n", + "j = json.loads(task_info.as_json())\n", + "\n", + "# Pretty print task info\n", + "print(json.dumps(j, indent=2))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 373 + }, + "id": "nhMb-oGFKTms", + "outputId": "d4a059f6-0384-4051-eabb-b1d628cb3499" + }, + "execution_count": 26, + "outputs": [ + { + "output_type": "error", + "ename": "HTTPError", + "evalue": "404 Client Error: Not Found for url: https://development.aced-idp.org/funnel/tasks", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mHTTPError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# Create and run task\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0mtask_id\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcli\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcreate_task\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0mcli\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwait\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtask_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/tes/client.py\u001b[0m in \u001b[0;36mcreate_task\u001b[0;34m(self, task)\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_request_params\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 169\u001b[0m \u001b[0mpaths\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mappend_suffixes_to_url\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0murls\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m\"/tasks\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 170\u001b[0;31m response = send_request(paths=paths, method='post',\n\u001b[0m\u001b[1;32m 171\u001b[0m kwargs_requests=kwargs)\n\u001b[1;32m 172\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0munmarshal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjson\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mCreateTaskResponse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/tes/client.py\u001b[0m in \u001b[0;36msend_request\u001b[0;34m(paths, method, kwargs_requests, **kwargs)\u001b[0m\n\u001b[1;32m 90\u001b[0m raise requests.exceptions.HTTPError(\n\u001b[1;32m 91\u001b[0m f\"No response received; HTTP Exceptions: {http_exceptions}\")\n\u001b[0;32m---> 92\u001b[0;31m \u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_for_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 93\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 94\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/requests/models.py\u001b[0m in \u001b[0;36mraise_for_status\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1022\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1023\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mhttp_error_msg\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1024\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mHTTPError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhttp_error_msg\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1025\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1026\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mHTTPError\u001b[0m: 404 Client Error: Not Found for url: https://development.aced-idp.org/funnel/tasks" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 907fd76..c815130 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -attrs>=22.2.0 -python-dateutil>=2.8.2 -requests>=2.28.2 +attrs>=17.4.0 +future>=0.16.0 +python-dateutil>=2.6.1 +requests>=2.18.2 +sphinx_rtd_theme diff --git a/setup.py b/setup.py index 3b0898b..ee1af33 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ def find_version(*file_paths): packages=find_packages(exclude=["tests*"]), python_requires=">=3.7, <4", install_requires=read("requirements.txt").splitlines(), + tests_require=read("tests/requirements.txt").splitlines(), zip_safe=True, classifiers=[ "Development Status :: 4 - Beta", @@ -49,8 +50,8 @@ def find_version(*file_paths): "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" - "Programming Language :: Python :: 3.10" + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11" ], ) diff --git a/tes/__init__.py b/tes/__init__.py index 28efe4e..4583576 100644 --- a/tes/__init__.py +++ b/tes/__init__.py @@ -36,4 +36,4 @@ "ServiceInfo" ] -__version__ = "1.1.0-rc.1" +__version__ = "1.1.0" diff --git a/tes/models.py b/tes/models.py index e5cdf6e..873389f 100644 --- a/tes/models.py +++ b/tes/models.py @@ -74,7 +74,7 @@ def strconv(value: Any) -> Any: """ if isinstance(value, (tuple, list)): if all([isinstance(n, str) for n in value]): - return [str(n) for n in value] + return type(value)(str(n) for n in value) else: return value elif isinstance(value, str): @@ -110,6 +110,8 @@ def timestampconv(value: Optional[str]) -> Optional[datetime]: """ if value is None: return value + if isinstance(value, datetime): + return value return dateutil.parser.parse(value) diff --git a/tes/utils.py b/tes/utils.py index 1f0f4f4..5031589 100644 --- a/tes/utils.py +++ b/tes/utils.py @@ -56,7 +56,7 @@ def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any: try: m = json.loads(j) except json.decoder.JSONDecodeError: - pass + raise UnmarshalError("Unable to decode JSON string: %s" % j) elif j is None: return None else: diff --git a/tests/integration/test_funnel.py b/tests/integration/test_funnel.py index b56be4c..aa7631f 100644 --- a/tests/integration/test_funnel.py +++ b/tests/integration/test_funnel.py @@ -1,35 +1,55 @@ -import unittest +import pytest import tes -class TestTESClient(unittest.TestCase): - def setUp(self): - self.cli = tes.HTTPClient("http://localhost:8000", timeout=5) - self.task = tes.Task( - executors=[ - tes.Executor( - image="alpine", - command=["echo", "hello"] - ) - ] - ) +@pytest.fixture +def tes_client(): + return tes.HTTPClient("http://localhost:8000", timeout=5) - def test_task_creation(self): - # Test service info retrieval - service_info = self.cli.get_service_info() - self.assertIsNotNone(service_info) - # Test task creation - task_id = self.cli.create_task(self.task) - self.assertIsNotNone(task_id) +@pytest.fixture +def task(): + return tes.Task(executors=[tes.Executor(image="alpine", command=["echo", "hello"])]) - # Wait for task to complete - _ = self.cli.wait(task_id) - # Test task info retrieval - task_info = self.cli.get_task(task_id, view="BASIC") - self.assertIsNotNone(task_info) +def test_service_info(tes_client): + service_info = tes_client.get_service_info() + assert service_info is not None -if __name__ == '__main__': - unittest.main() +def test_task_creation(tes_client, task): + task_id = tes_client.create_task(task) + assert task_id is not None + + _ = tes_client.wait(task_id) + + task_info = tes_client.get_task(task_id, view="BASIC") + assert task_info is not None + + +def test_task_status(tes_client, task): + task_id = tes_client.create_task(task) + assert task_id is not None + + status = tes_client.get_task(task_id, view="MINIMAL").state + assert status in [ + "QUEUED", + "INITIALIZING", + "RUNNING", + "COMPLETE", + "CANCELED", + "EXECUTOR_ERROR", + "SYSTEM_ERROR", + "UNKNOWN", + ] + + +def test_task_logs(tes_client, task): + task_id = tes_client.create_task(task) + assert task_id is not None + + _ = tes_client.wait(task_id) + + logs = tes_client.get_task(task_id, view="FULL").logs + assert logs is not None + assert len(logs) > 0 diff --git a/tests/test_client.py b/tests/test_client.py index 45cd9dc..a6a22b7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,7 +1,6 @@ import pytest import requests import requests_mock -import unittest import uuid from tes.client import append_suffixes_to_url, HTTPClient, send_request @@ -9,346 +8,309 @@ from tes.utils import TimeoutError -class TestHTTPClient(unittest.TestCase): - task = Task( - executors=[ - Executor( - image="alpine", - command=["echo", "hello"] - ) - ] - ) - mock_id = str(uuid.uuid4()) - mock_url = "http://fakehost:8000" - cli = HTTPClient(mock_url, timeout=5) - - def test_cli(self): - cli = HTTPClient(url="http://fakehost:8000//", timeout=5) - self.assertEqual(cli.url, "http://fakehost:8000") - self.assertEqual(cli.urls, [ - "http://fakehost:8000/ga4gh/tes/v1", - "http://fakehost:8000/v1", - "http://fakehost:8000"] - ) - self.assertEqual(cli.timeout, 5) +@pytest.fixture +def task(): + return Task(executors=[Executor(image="alpine", command=["echo", "hello"])]) - with self.assertRaises(TypeError): - cli = HTTPClient(url=8000, timeout=5) # type: ignore - with self.assertRaises(TypeError): - HTTPClient(url="http://fakehost:8000", timeout="5") # type: ignore +@pytest.fixture +def mock_id(): + return str(uuid.uuid4()) - with self.assertRaises(ValueError): - HTTPClient(url="fakehost:8000", timeout=5) - with self.assertRaises(ValueError): - HTTPClient(url="htpp://fakehost:8000", timeout=5) # type: ignore +@pytest.fixture +def mock_url(): + return "http://fakehost:8000" - def test_create_task(self): - with requests_mock.Mocker() as m: - m.post( - "%s/ga4gh/tes/v1/tasks" % (self.mock_url), - status_code=200, - json={"id": self.mock_id} - ) - self.cli.create_task(self.task) - self.assertEqual(m.last_request.text, self.task.as_json()) - self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout) - m.post( - "%s/ga4gh/tes/v1/tasks" % (self.mock_url), - status_code=500 - ) - with self.assertRaises(requests.HTTPError): - self.cli.create_task(self.task) +@pytest.fixture +def cli(mock_url): + return HTTPClient(mock_url, timeout=5) - with self.assertRaises(TypeError): - self.cli.create_task('not_a_task_object') # type: ignore - def test_get_task(self): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks/%s" % (self.mock_url, self.mock_id), - status_code=200, - json={ - "id": self.mock_id, - "state": "RUNNING", - } - ) - self.cli.get_task(self.mock_id, "MINIMAL") - self.assertEqual( - m.last_request.url, - "%s/ga4gh/tes/v1/tasks/%s?view=MINIMAL" % ( - self.mock_url, self.mock_id - ) - ) - self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout) +def test_cli(): + cli = HTTPClient(url="http://fakehost:8000//", timeout=5) + assert cli.url == "http://fakehost:8000" + assert cli.urls == [ + "http://fakehost:8000/ga4gh/tes/v1", + "http://fakehost:8000/v1", + "http://fakehost:8000", + ] + assert cli.timeout == 5 - m.get( - requests_mock.ANY, - status_code=404 - ) - with self.assertRaises(requests.HTTPError): - self.cli.get_task(self.mock_id) + with pytest.raises(TypeError): + HTTPClient(url=8000, timeout=5) # type: ignore - def test_list_tasks(self): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks" % (self.mock_url), - status_code=200, - json={ - "tasks": [] - } - ) - self.cli.list_tasks() - self.assertEqual( - m.last_request.url, - "%s/ga4gh/tes/v1/tasks?view=MINIMAL" % (self.mock_url) - ) - self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout) + with pytest.raises(TypeError): + HTTPClient(url="http://fakehost:8000", timeout="5") # type: ignore - # empty response - m.get( - "%s/ga4gh/tes/v1/tasks" % (self.mock_url), - status_code=200, - json={} - ) - self.cli.list_tasks() - self.assertEqual( - m.last_request.url, - "%s/ga4gh/tes/v1/tasks?view=MINIMAL" % (self.mock_url) - ) + with pytest.raises(ValueError): + HTTPClient(url="fakehost:8000", timeout=5) - m.get( - "%s/ga4gh/tes/v1/tasks" % (self.mock_url), - status_code=500 - ) - with self.assertRaises(requests.HTTPError): - self.cli.list_tasks() + with pytest.raises(ValueError): + HTTPClient(url="htpp://fakehost:8000", timeout=5) # type: ignore - def test_cancel_task(self): - with requests_mock.Mocker() as m: - m.post( - "%s/ga4gh/tes/v1/tasks/%s:cancel" % ( - self.mock_url, self.mock_id), - status_code=200, - json={} - ) - self.cli.cancel_task(self.mock_id) - self.assertEqual( - m.last_request.url, - "%s/ga4gh/tes/v1/tasks/%s:cancel" % ( - self.mock_url, self.mock_id) - ) - self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout) - m.post( - "%s/ga4gh/tes/v1/tasks/%s:cancel" % ( - self.mock_url, self.mock_id), - status_code=500 - ) - with self.assertRaises(requests.HTTPError): - self.cli.cancel_task(self.mock_id) +def test_create_task(cli, task, mock_id, mock_url): + with requests_mock.Mocker() as m: + m.post(f"{mock_url}/ga4gh/tes/v1/tasks", status_code=200, json={"id": mock_id}) + cli.create_task(task) + assert m.last_request.text == task.as_json() + assert m.last_request.timeout == cli.timeout - m.post( - requests_mock.ANY, - status_code=404, - json={} - ) - with self.assertRaises(requests.HTTPError): - self.cli.cancel_task(self.mock_id) + m.post(f"{mock_url}/ga4gh/tes/v1/tasks", status_code=500) + with pytest.raises(requests.HTTPError): + cli.create_task(task) - def test_get_service_info(self): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/service-info" % (self.mock_url), - status_code=200, - json={} - ) - self.cli.get_service_info() - self.assertEqual( - m.last_request.url, - "%s/ga4gh/tes/v1/service-info" % (self.mock_url) - ) - self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout) + with pytest.raises(TypeError): + cli.create_task("not_a_task_object") # type: ignore - m.get( - "%s/ga4gh/tes/v1/service-info" % (self.mock_url), - status_code=500 - ) - with self.assertRaises(requests.HTTPError): - self.cli.get_service_info() - - def test_wait(self): - with self.assertRaises(TimeoutError): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks/%s" % (self.mock_url, self.mock_id), - status_code=200, - json={ - "id": self.mock_id, - "state": "RUNNING", - } - ) - self.cli.wait(self.mock_id, timeout=1) - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks/%s" % (self.mock_url, self.mock_id), - [ - {"status_code": 200, - "json": {"id": self.mock_id, "state": "INITIALIZING"}}, - {"status_code": 200, - "json": {"id": self.mock_id, "state": "RUNNING"}}, - {"status_code": 200, - "json": {"id": self.mock_id, "state": "COMPLETE"}} - ] - ) - self.cli.wait(self.mock_id, timeout=2) +def test_get_task(cli, mock_id, mock_url): + with requests_mock.Mocker() as m: + m.get( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}", + status_code=200, + json={ + "id": mock_id, + "state": "RUNNING", + }, + ) + cli.get_task(mock_id, "MINIMAL") + assert ( + m.last_request.url + == f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}?view=MINIMAL" + ) + assert m.last_request.timeout == cli.timeout - def test_wait_exception(self): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks/%s" % (self.mock_url, self.mock_id), - status_code=200, - json={ - "Error": "Error", - } - ) - with self.assertRaises(Exception): - self.cli.wait(self.mock_id, timeout=2) + m.get(requests_mock.ANY, status_code=404) + with pytest.raises(requests.HTTPError): + cli.get_task(mock_id) - def test_wait_no_state_change(self): - with requests_mock.Mocker() as m: - m.get( - "%s/ga4gh/tes/v1/tasks/%s" % (self.mock_url, self.mock_id), - [ - {"status_code": 200, "json": {"id": self.mock_id, "state": "RUNNING"}}, - {"status_code": 200, "json": {"id": self.mock_id, "state": "RUNNING"}}, - # Continues to return RUNNING state - ] - ) - with self.assertRaises(TimeoutError): - self.cli.wait(self.mock_id, timeout=2) - - def test_request_params(self): - - cli = HTTPClient(url="http://fakehost:8000", timeout=5) - vals = cli._request_params() - self.assertAlmostEqual(vals["timeout"], 5) - self.assertEqual(vals["headers"]["Content-type"], "application/json") - self.assertRaises(KeyError, lambda: vals["headers"]["Authorization"]) - self.assertRaises(KeyError, lambda: vals["auth"]) - self.assertRaises(KeyError, lambda: vals["data"]) - self.assertRaises(KeyError, lambda: vals["params"]) - - cli = HTTPClient(url="http://fakehost:8000", user="user", - password="password", token="token") - vals = cli._request_params(data='{"json": "string"}', - params={"query_param": "value"}) - self.assertAlmostEqual(vals["timeout"], 10) - self.assertEqual(vals["headers"]["Content-type"], "application/json") - self.assertEqual(vals["headers"]["Authorization"], "Bearer token") - self.assertEqual(vals["auth"], ("user", "password")) - self.assertEqual(vals["data"], '{"json": "string"}') - self.assertEqual(vals["params"], {"query_param": "value"}) - - def test_append_suffixes_to_url(self): - urls = ["http://example.com", "http://example.com/"] - urls_order = ["http://example1.com", "http://example2.com"] - suffixes = ["foo", "/foo", "foo/", "/foo/"] - no_suffixes = ["", "/", "//", "///"] - suffixes_order = ["1", "2"] - - results = append_suffixes_to_url(urls=urls, suffixes=suffixes) - assert len(results) == len(urls) * len(suffixes) - assert all(url == 'http://example.com/foo' for url in results) - - results = append_suffixes_to_url(urls=urls, suffixes=no_suffixes) - assert len(results) == len(urls) * len(no_suffixes) - assert all(url == 'http://example.com' for url in results) - - results = append_suffixes_to_url(urls=urls_order, suffixes=suffixes_order) - assert len(results) == len(urls_order) * len(suffixes_order) - assert results[0] == 'http://example1.com/1' - assert results[1] == 'http://example1.com/2' - assert results[2] == 'http://example2.com/1' - assert results[3] == 'http://example2.com/2' - - def test_send_request(self): - mock_url = "http://example.com" - mock_id = "mock_id" - mock_urls = append_suffixes_to_url([mock_url], ["/suffix", "/"]) - - # invalid method - with pytest.raises(ValueError): - send_request(paths=mock_urls, method="invalid") - - # errors for all paths - with requests_mock.Mocker() as m: - m.get(requests_mock.ANY, exc=requests.exceptions.ConnectTimeout) - with pytest.raises(requests.HTTPError): - send_request(paths=mock_urls) - # error on first path, 200 on second - with requests_mock.Mocker() as m: - m.get(mock_urls[0], exc=requests.exceptions.ConnectTimeout) - m.get(mock_urls[1], status_code=200) - response = send_request(paths=mock_urls) - assert response.status_code == 200 - assert m.last_request.url.rstrip('/') == f"{mock_url}" +def test_list_tasks(cli, mock_url): + with requests_mock.Mocker() as m: + m.get(f"{mock_url}/ga4gh/tes/v1/tasks", status_code=200, json={"tasks": []}) + cli.list_tasks() + assert m.last_request.url == f"{mock_url}/ga4gh/tes/v1/tasks?view=MINIMAL" + assert m.last_request.timeout == cli.timeout - # error on first path, 404 on second - with requests_mock.Mocker() as m: - m.get(mock_urls[0], exc=requests.exceptions.ConnectTimeout) - m.get(mock_urls[1], status_code=404) - with pytest.raises(requests.HTTPError): - send_request(paths=mock_urls) + # empty response + m.get(f"{mock_url}/ga4gh/tes/v1/tasks", status_code=200, json={}) + cli.list_tasks() + assert m.last_request.url == f"{mock_url}/ga4gh/tes/v1/tasks?view=MINIMAL" - # 404 on first path, error on second - with requests_mock.Mocker() as m: - m.get(mock_urls[0], status_code=404) - m.get(mock_urls[1], exc=requests.exceptions.ConnectTimeout) - with pytest.raises(requests.HTTPError): - send_request(paths=mock_urls) + m.get(f"{mock_url}/ga4gh/tes/v1/tasks", status_code=500) + with pytest.raises(requests.HTTPError): + cli.list_tasks() - # 404 on first path, 200 on second - with requests_mock.Mocker() as m: - m.get(mock_urls[0], status_code=404) - m.get(mock_urls[1], status_code=200) - response = send_request(paths=mock_urls) - assert response.status_code == 200 - assert m.last_request.url.rstrip('/') == f"{mock_url}" - # POST 200 - with requests_mock.Mocker() as m: - m.post(f"{mock_url}/suffix/foo/{mock_id}:bar", status_code=200) - paths = append_suffixes_to_url(mock_urls, ["/foo/{id}:bar"]) - response = send_request(paths=paths, method="post", json={}, - id=mock_id) - assert response.status_code == 200 - assert m.last_request.url == f"{mock_url}/suffix/foo/{mock_id}:bar" - - # GET 200 - with requests_mock.Mocker() as m: - m.get(f"{mock_url}/suffix/foo/{mock_id}", status_code=200) - paths = append_suffixes_to_url(mock_urls, ["/foo/{id}"]) - response = send_request(paths=paths, id=mock_id) - assert response.status_code == 200 - assert m.last_request.url == f"{mock_url}/suffix/foo/{mock_id}" +def test_cancel_task(cli, mock_id, mock_url): + with requests_mock.Mocker() as m: + m.post( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}:cancel", status_code=200, json={} + ) + cli.cancel_task(mock_id) + assert m.last_request.url == f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}:cancel" + assert m.last_request.timeout == cli.timeout + + m.post(f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}:cancel", status_code=500) + with pytest.raises(requests.HTTPError): + cli.cancel_task(mock_id) + + m.post(requests_mock.ANY, status_code=404, json={}) + with pytest.raises(requests.HTTPError): + cli.cancel_task(mock_id) - # POST 404 - with requests_mock.Mocker() as m: - m.post(requests_mock.ANY, status_code=404, json={}) - paths = append_suffixes_to_url(mock_urls, ["/foo"]) - with pytest.raises(requests.HTTPError): - send_request(paths=paths, method="post", json={}) - assert m.last_request.url == f"{mock_url}/foo" - # GET 500 +def test_get_service_info(cli, mock_url): + with requests_mock.Mocker() as m: + m.get(f"{mock_url}/ga4gh/tes/v1/service-info", status_code=200, json={}) + cli.get_service_info() + assert m.last_request.url == f"{mock_url}/ga4gh/tes/v1/service-info" + assert m.last_request.timeout == cli.timeout + + m.get(f"{mock_url}/ga4gh/tes/v1/service-info", status_code=500) + with pytest.raises(requests.HTTPError): + cli.get_service_info() + + +def test_wait(cli, mock_id, mock_url): + with pytest.raises(TimeoutError): with requests_mock.Mocker() as m: - m.get(f"{mock_url}/suffix/foo", status_code=500) - paths = append_suffixes_to_url(mock_urls, ["/foo"]) - with pytest.raises(requests.HTTPError): - send_request(paths=paths) - assert m.last_request.url == f"{mock_url}/suffix/foo" + m.get( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}", + status_code=200, + json={ + "id": mock_id, + "state": "RUNNING", + }, + ) + cli.wait(mock_id, timeout=1) + + with requests_mock.Mocker() as m: + m.get( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}", + [ + {"status_code": 200, "json": {"id": mock_id, "state": "INITIALIZING"}}, + {"status_code": 200, "json": {"id": mock_id, "state": "RUNNING"}}, + {"status_code": 200, "json": {"id": mock_id, "state": "COMPLETE"}}, + ], + ) + cli.wait(mock_id, timeout=2) + + +def test_wait_exception(cli, mock_id, mock_url): + with requests_mock.Mocker() as m: + m.get( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}", + status_code=200, + json={ + "Error": "Error", + }, + ) + with pytest.raises(Exception): + cli.wait(mock_id, timeout=2) + + +def test_wait_no_state_change(cli, mock_id, mock_url): + with requests_mock.Mocker() as m: + m.get( + f"{mock_url}/ga4gh/tes/v1/tasks/{mock_id}", + [ + {"status_code": 200, "json": {"id": mock_id, "state": "RUNNING"}}, + {"status_code": 200, "json": {"id": mock_id, "state": "RUNNING"}}, + # Continues to return RUNNING state + ], + ) + with pytest.raises(TimeoutError): + cli.wait(mock_id, timeout=2) + + +def test_request_params(): + cli = HTTPClient(url="http://fakehost:8000", timeout=5) + vals = cli._request_params() + assert vals["timeout"] == 5 + assert vals["headers"]["Content-type"] == "application/json" + with pytest.raises(KeyError): + _ = vals["headers"]["Authorization"] + with pytest.raises(KeyError): + _ = vals["auth"] + with pytest.raises(KeyError): + _ = vals["data"] + with pytest.raises(KeyError): + _ = vals["params"] + + cli = HTTPClient( + url="http://fakehost:8000", user="user", password="password", token="token" + ) + vals = cli._request_params( + data='{"json": "string"}', params={"query_param": "value"} + ) + assert vals["timeout"] == 10 + assert vals["headers"]["Content-type"] == "application/json" + assert vals["headers"]["Authorization"] == "Bearer token" + assert vals["auth"] == ("user", "password") + assert vals["data"] == '{"json": "string"}' + assert vals["params"] == {"query_param": "value"} + + +def test_append_suffixes_to_url(): + urls = ["http://example.com", "http://example.com/"] + urls_order = ["http://example1.com", "http://example2.com"] + suffixes = ["foo", "/foo", "foo/", "/foo/"] + no_suffixes = ["", "/", "//", "///"] + suffixes_order = ["1", "2"] + + results = append_suffixes_to_url(urls=urls, suffixes=suffixes) + assert len(results) == len(urls) * len(suffixes) + assert all(url == "http://example.com/foo" for url in results) + + results = append_suffixes_to_url(urls=urls, suffixes=no_suffixes) + assert len(results) == len(urls) * len(no_suffixes) + assert all(url == "http://example.com" for url in results) + + results = append_suffixes_to_url(urls=urls_order, suffixes=suffixes_order) + assert len(results) == len(urls_order) * len(suffixes_order) + assert results[0] == "http://example1.com/1" + assert results[1] == "http://example1.com/2" + assert results[2] == "http://example2.com/1" + assert results[3] == "http://example2.com/2" + + +def test_send_request(): + mock_url = "http://example.com" + mock_id = "mock_id" + mock_urls = append_suffixes_to_url([mock_url], ["/suffix", "/"]) + + # invalid method + with pytest.raises(ValueError): + send_request(paths=mock_urls, method="invalid") + + # errors for all paths + with requests_mock.Mocker() as m: + m.get(requests_mock.ANY, exc=requests.exceptions.ConnectTimeout) + with pytest.raises(requests.HTTPError): + send_request(paths=mock_urls) + + # error on first path, 200 on second + with requests_mock.Mocker() as m: + m.get(mock_urls[0], exc=requests.exceptions.ConnectTimeout) + m.get(mock_urls[1], status_code=200) + response = send_request(paths=mock_urls) + assert response.status_code == 200 + assert m.last_request.url.rstrip("/") == f"{mock_url}" + + # error on first path, 404 on second + with requests_mock.Mocker() as m: + m.get(mock_urls[0], exc=requests.exceptions.ConnectTimeout) + m.get(mock_urls[1], status_code=404) + with pytest.raises(requests.HTTPError): + send_request(paths=mock_urls) + + # 404 on first path, error on second + with requests_mock.Mocker() as m: + m.get(mock_urls[0], status_code=404) + m.get(mock_urls[1], exc=requests.exceptions.ConnectTimeout) + with pytest.raises(requests.HTTPError): + send_request(paths=mock_urls) + + # 404 on first path, 200 on second + with requests_mock.Mocker() as m: + m.get(mock_urls[0], status_code=404) + m.get(mock_urls[1], status_code=200) + response = send_request(paths=mock_urls) + assert response.status_code == 200 + assert m.last_request.url.rstrip("/") == f"{mock_url}" + + # POST 200 + with requests_mock.Mocker() as m: + m.post(f"{mock_url}/suffix/foo/{mock_id}:bar", status_code=200) + paths = append_suffixes_to_url(mock_urls, ["/foo/{id}:bar"]) + response = send_request(paths=paths, method="post", json={}, id=mock_id) + assert response.status_code == 200 + assert m.last_request.url == f"{mock_url}/suffix/foo/{mock_id}:bar" + + # GET 200 + with requests_mock.Mocker() as m: + m.get(f"{mock_url}/suffix/foo/{mock_id}", status_code=200) + paths = append_suffixes_to_url(mock_urls, ["/foo/{id}"]) + response = send_request(paths=paths, id=mock_id) + assert response.status_code == 200 + assert m.last_request.url == f"{mock_url}/suffix/foo/{mock_id}" + + # POST 404 + with requests_mock.Mocker() as m: + m.post(requests_mock.ANY, status_code=404, json={}) + paths = append_suffixes_to_url(mock_urls, ["/foo"]) + with pytest.raises(requests.HTTPError): + send_request(paths=paths, method="post", json={}) + assert m.last_request.url == f"{mock_url}/foo" + + # GET 500 + with requests_mock.Mocker() as m: + m.get(f"{mock_url}/suffix/foo", status_code=500) + paths = append_suffixes_to_url(mock_urls, ["/foo"]) + with pytest.raises(requests.HTTPError): + send_request(paths=paths) + assert m.last_request.url == f"{mock_url}/suffix/foo" diff --git a/tests/test_models.py b/tests/test_models.py index c1cebd7..fc64f5a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -import unittest - from copy import deepcopy +from datetime import datetime + +import pytest from tes.models import ( Executor, @@ -21,14 +22,7 @@ ) -task_valid = Task( - executors=[ - Executor( - image="alpine", - command=["echo", "hello"] - ) - ] -) +task_valid = Task(executors=[Executor(image="alpine", command=["echo", "hello"])]) datetm = "2018-01-01T00:00:00Z" @@ -52,29 +46,15 @@ stdin="/abs/path", stdout="/abs/path", stderr="/abs/path", - env={"VAR": "value"} + env={"VAR": "value"}, ), - Executor( - image="alpine", - command=["echo", "worls"] - ) + Executor(image="alpine", command=["echo", "worls"]), ], inputs=[ - Input( - url="s3:/some/path", - path="/abs/path" - ), - Input( - content="foo", - path="/abs/path" - ) - ], - outputs=[ - Output( - url="s3:/some/path", - path="/abs/path" - ) + Input(url="s3:/some/path", path="/abs/path"), + Input(content="foo", path="/abs/path"), ], + outputs=[Output(url="s3:/some/path", path="/abs/path")], volumes=[], tags={"key": "value", "key2": "value2"}, logs=[ @@ -88,23 +68,20 @@ end_time=datetm, # type: ignore exit_code=0, stdout="hello", - stderr="world" + stderr="world", ) ], outputs=[ OutputFileLog( url="s3:/some/path", path="/abs/path", - size_bytes=int64conv(123) # type: ignore + size_bytes=int64conv(123), # type: ignore ) ], - system_logs=[ - "some system log message", - "some other system log message" - ] + system_logs=["some system log message", "some other system log message"], ) ], - creation_time=datetm # type: ignore + creation_time=datetm, # type: ignore ) task_invalid = Task( @@ -115,136 +92,146 @@ stdin="relative/path", stdout="relative/path", stderr="relative/path", - env={1: 2} - ) - ], - inputs=[ - Input( - url="s3:/some/path", - content="foo" - ), - Input( - path="relative/path" - ) - ], - outputs=[ - Output(), - Output( - url="s3:/some/path", - path="relative/path" + env={1: 2}, ) ], - volumes=['/abs/path', 'relative/path'], - tags={1: 2} + inputs=[Input(url="s3:/some/path", content="foo"), Input(path="relative/path")], + outputs=[Output(), Output(url="s3:/some/path", path="relative/path")], + volumes=["/abs/path", "relative/path"], + tags={1: 2}, ) -expected = { - "executors": [ - { - "image": "alpine", - "command": ["echo", "hello"] - } - ] -} +expected = {"executors": [{"image": "alpine", "command": ["echo", "hello"]}]} -class TestModels(unittest.TestCase): - - def test_list_of(self): - validator = list_of(str) - self.assertEqual(list_of(str), validator) - self.assertEqual( - repr(validator), - ">" +def test_list_of(): + validator = list_of(str) + assert list_of(str) == validator + assert repr(validator) == ">" + with pytest.raises(TypeError): + Input(url="s3:/some/path", path="/opt/foo", content=123) # type: ignore + with pytest.raises(TypeError): + Task( + inputs=[Input(url="s3:/some/path", path="/opt/foo"), "foo"] # type: ignore ) - with self.assertRaises(TypeError): - Input( - url="s3:/some/path", - path="/opt/foo", - content=123 # type: ignore - ) - with self.assertRaises(TypeError): - Task( - inputs=[ - Input( - url="s3:/some/path", path="/opt/foo" - ), - "foo" # type: ignore - ] - ) - - def test_drop_none(self): - self.assertEqual(_drop_none({}), {}) - self.assertEqual(_drop_none({"foo": None}), {}) - self.assertEqual(_drop_none({"foo": 1}), {"foo": 1}) - self.assertEqual(_drop_none({"foo": None, "bar": 1}), {"bar": 1}) - self.assertEqual(_drop_none({"foo": [1, None, 2]}), {"foo": [1, 2]}) - self.assertEqual(_drop_none({"foo": {"bar": None}}), {"foo": {}}) - self.assertEqual( - _drop_none({"foo": {"bar": None}, "baz": 1}), - {"foo": {}, "baz": 1} + + +def test_drop_none(): + assert _drop_none({}) == {} + assert _drop_none({"foo": None}) == {} + assert _drop_none({"foo": 1}) == {"foo": 1} + assert _drop_none({"foo": None, "bar": 1}) == {"bar": 1} + assert _drop_none({"foo": [1, None, 2]}) == {"foo": [1, 2]} + assert _drop_none({"foo": {"bar": None}}) == {"foo": {}} + assert _drop_none({"foo": {"bar": None}, "baz": 1}) == {"foo": {}, "baz": 1} + + +def test_strconv(): + assert strconv("foo") == "foo" + assert strconv(["foo", "bar"]) == ["foo", "bar"] + assert strconv(("foo", "bar")) == ("foo", "bar") + assert strconv(1) == 1 + assert strconv([1]) == [1] + + +def test_int64conv(): + assert int64conv("1") == 1 + assert int64conv("-1") == -1 + assert int64conv(None) is None + + +def test_timestampconv(): + tm = timestampconv("2018-02-01T00:00:00Z") + assert tm is not None + assert tm.year == 2018 + assert tm.month == 2 + assert tm.day == 1 + assert tm.hour == 0 + assert tm.timestamp() == 1517443200.0 + assert timestampconv(None) is None + + +def test_datetime_json_handler(): + tm = timestampconv("2018-02-01T00:00:00Z") + tm_iso = "2018-02-01T00:00:00+00:00" + assert tm is not None + assert datetime_json_handler(tm) == tm_iso + with pytest.raises(TypeError): + datetime_json_handler(None) + with pytest.raises(TypeError): + datetime_json_handler("abc") + with pytest.raises(TypeError): + datetime_json_handler(2001) + with pytest.raises(TypeError): + datetime_json_handler(tm_iso) + + +def test_as_dict(): + task = deepcopy(task_valid) + assert task.as_dict() == expected + with pytest.raises(KeyError): + task.as_dict()["inputs"] + assert task.as_dict(drop_empty=False)["inputs"] is None + + +def test_as_json(): + task = deepcopy(task_valid) + assert task.as_json() == json.dumps(expected) + + +def test_is_valid(): + task = deepcopy(task_valid) + assert task.is_valid()[0] + + task = deepcopy(task_valid_full) + assert task.is_valid()[0] + + task = deepcopy(task_invalid) + task.executors[0].image = None # type: ignore + task.executors[0].command = None # type: ignore + assert not task.is_valid()[0] + + task = deepcopy(task_invalid) + task.executors = None + assert not task.is_valid()[0] + + +def test_task_creation(): + task = Task( + id="test_id", + state="RUNNING", + name="test_task", + description="test description", + executors=[Executor(image="python:3.8", command=["python", "--version"])], + creation_time=datetime.now(), + ) + assert task.id == "test_id" + assert task.state == "RUNNING" + assert task.name == "test_task" + assert task.description == "test description" + assert len(task.executors) == 1 + assert task.executors[0].image == "python:3.8" + assert task.executors[0].command == ["python", "--version"] + assert task.creation_time is not None + + +def test_task_invalid_state(): + with pytest.raises(ValueError): + Task( + id="test_id", + state="INVALID_STATE", # type: ignore + name="test_task", + description="test description", + executors=[Executor(image="python:3.8", command=["python", "--version"])], + creation_time=datetime.now(), ) - def test_strconv(self): - self.assertTrue(strconv("foo"), u"foo") - self.assertTrue(strconv(["foo", "bar"]), [u"foo", u"bar"]) - self.assertTrue(strconv(("foo", "bar")), (u"foo", u"bar")) - self.assertTrue(strconv(1), 1) - self.assertTrue(strconv([1]), [1]) - - def test_int64conv(self): - self.assertEqual(int64conv("1"), 1) - self.assertEqual(int64conv("-1"), -1) - self.assertIsNone(int64conv(None)) - - def test_timestampconv(self): - tm = timestampconv("2018-02-01T00:00:00Z") - self.assertIsNotNone(tm) - assert tm is not None - self.assertAlmostEqual(tm.year, 2018) - self.assertAlmostEqual(tm.month, 2) - self.assertAlmostEqual(tm.day, 1) - self.assertAlmostEqual(tm.hour, 0) - self.assertAlmostEqual(tm.timestamp(), 1517443200.0) - self.assertIsNone(timestampconv(None)) - - def test_datetime_json_handler(self): - tm = timestampconv("2018-02-01T00:00:00Z") - tm_iso = '2018-02-01T00:00:00+00:00' - assert tm is not None - self.assertEqual(datetime_json_handler(tm), tm_iso) - with self.assertRaises(TypeError): - datetime_json_handler(None) - with self.assertRaises(TypeError): - datetime_json_handler("abc") - with self.assertRaises(TypeError): - datetime_json_handler(2001) - with self.assertRaises(TypeError): - datetime_json_handler(tm_iso) - - def test_as_dict(self): - task = deepcopy(task_valid) - self.assertEqual(task.as_dict(), expected) - with self.assertRaises(KeyError): - task.as_dict()['inputs'] - self.assertIsNone(task.as_dict(drop_empty=False)['inputs']) - - def test_as_json(self): - task = deepcopy(task_valid) - self.assertEqual(task.as_json(), json.dumps(expected)) - - def test_is_valid(self): - task = deepcopy(task_valid) - self.assertTrue(task.is_valid()[0]) - - task = deepcopy(task_valid_full) - self.assertTrue(task.is_valid()[0]) - - task = deepcopy(task_invalid) - task.executors[0].image = None # type: ignore - task.executors[0].command = None # type: ignore - self.assertFalse(task.is_valid()[0]) - - task = deepcopy(task_invalid) - task.executors = None - self.assertFalse(task.is_valid()[0]) + +def test_executor_missing_image(): + with pytest.raises(TypeError): + Executor(command=["python", "--version"]) + + +def test_executor_missing_command(): + with pytest.raises(TypeError): + Executor(image="python:3.8") diff --git a/tests/test_utils.py b/tests/test_utils.py index b7fe5ed..0a8849c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,6 @@ -from __future__ import absolute_import, print_function, unicode_literals - -import dateutil.parser import json -import unittest - +import dateutil.parser +import pytest from tes.utils import camel_to_snake, unmarshal, UnmarshalError from tes.models import ( CancelTaskRequest, @@ -24,257 +21,255 @@ ) -class TestUtils(unittest.TestCase): - - def test_camel_to_snake(self): - case1 = "FooBar" - case2 = "fooBar" - case3 = "foo_bar" - self.assertEqual(camel_to_snake(case1), "foo_bar") - self.assertEqual(camel_to_snake(case2), "foo_bar") - self.assertEqual(camel_to_snake(case3), "foo_bar") - - def test_unmarshal(self): - - # test unmarshalling with no or minimal contents - try: - unmarshal( - CancelTaskRequest(id="foo").as_json(), - CancelTaskRequest - ) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(CancelTaskResponse().as_json(), CancelTaskResponse) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal( - CreateTaskResponse(id="foo").as_json(), - CreateTaskResponse - ) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(Executor( - image="alpine", command=["echo", "hello"]).as_json(), - Executor - ) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(ExecutorLog().as_json(), ExecutorLog) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal( - GetTaskRequest(id="foo", view="BASIC").as_json(), - GetTaskRequest - ) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(Input().as_json(), Input) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(ListTasksRequest().as_json(), ListTasksRequest) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(ListTasksResponse().as_json(), ListTasksResponse) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(Output().as_json(), Output) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(OutputFileLog().as_json(), OutputFileLog) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(Resources().as_json(), Resources) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(ServiceInfo().as_json(), ServiceInfo) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(Task().as_json(), Task) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - try: - unmarshal(TaskLog().as_json(), TaskLog) - except Exception: - self.fail("Raised ExceptionType unexpectedly!") - - # test special cases - self.assertIsNone(unmarshal(None, Input)) - with self.assertRaises(TypeError): - unmarshal([], Input) - with self.assertRaises(TypeError): - unmarshal(1, Input) - with self.assertRaises(TypeError): - unmarshal(1.3, Input) - with self.assertRaises(TypeError): - unmarshal(True, Input) - with self.assertRaises(TypeError): - unmarshal('foo', Input) - - # test with some interesting contents - test_invalid_dict = {"foo": "bar"} - test_invalid_str = json.dumps(test_invalid_dict) - with self.assertRaises(UnmarshalError): - unmarshal(test_invalid_dict, CreateTaskResponse) - with self.assertRaises(UnmarshalError): - unmarshal(test_invalid_str, CreateTaskResponse) - - test_simple_dict = { - "url": "file://test_file", - "path": "/mnt/test_file", - "type": "FILE" - } - test_simple_str = json.dumps(test_simple_dict) - o1 = unmarshal(test_simple_dict, Input) - o2 = unmarshal(test_simple_str, Input, convert_camel_case=False) - self.assertTrue(isinstance(o1, Input)) - self.assertTrue(isinstance(o2, Input)) - self.assertEqual(o1, o2) - self.assertEqual(o1.as_dict(), test_simple_dict) - self.assertEqual(o1.as_json(), test_simple_str) - - test_complex_dict = { - "name": "test", - "inputs": [ - { - "url": "file:///storage/inputs/test_file", - "path": "/mnt/test_file", - "type": "FILE" - } - ], - "outputs": [ - { - "url": "file:///storage/outputs/test_outputfile", - "path": "/mnt/test_outputfile", - "type": "FILE" - } - ], - "executors": [ - { - "image": "alpine", - "command": ["echo", "hello"], - "env": {"HOME": "/home/"} - } - ], - "logs": [ - { - "start_time": "2017-10-09T17:05:00.0Z", - "end_time": "2017-10-09T17:40:30.0Z", - "metadata": {"testmeta": "testvalue"}, - "logs": [ - { - "start_time": "2017-10-09T17:06:30.0Z", - "end_time": "2017-10-09T17:39:50.0Z", - "exit_code": 0, - "stdout": "hello", - "stderr": "", - } - ], - "outputs": [ - { - "url": "file:///storage/outputs/test_outputfile", - "path": "/mnt/test_outputfile", - "size_bytes": "3333" - } - ], - "system_logs": [ - "level='info' msg='Download started' \ - timestamp='2018-05-04T09:12:42.391262682-07:00' \ - task_attempt='0' executor_index='0' \ - url='swift://biostream/protograph'" - ] - } - ], - "resources": { - "cpu_cores": 1, - "ram_gb": 2, - "disk_gb": 3, - "preemptible": True, - "zones": ["us-east-1", "us-west-1"] - }, - "creation_time": "2017-10-09T17:00:00.0Z" - } - - test_complex_str = json.dumps(test_complex_dict) - o1 = unmarshal(test_complex_dict, Task) - o2 = unmarshal(test_complex_str, Task) - self.assertTrue(isinstance(o1, Task)) - self.assertTrue(isinstance(o2, Task)) - self.assertAlmostEqual(o1, o2) - expected = test_complex_dict.copy() - - # handle expected conversions - expected["logs"][0]["outputs"][0]["size_bytes"] = int( - expected["logs"][0]["outputs"][0]["size_bytes"] - ) - expected["logs"][0]["start_time"] = dateutil.parser.parse( - expected["logs"][0]["start_time"] - ) - expected["logs"][0]["end_time"] = dateutil.parser.parse( - expected["logs"][0]["end_time"] - ) - expected["logs"][0]["logs"][0]["start_time"] = dateutil.parser.parse( - expected["logs"][0]["logs"][0]["start_time"] - ) - expected["logs"][0]["logs"][0]["end_time"] = dateutil.parser.parse( - expected["logs"][0]["logs"][0]["end_time"] - ) - expected["creation_time"] = dateutil.parser.parse( - expected["creation_time"] - ) - self.assertEqual(o1.as_dict(), expected) - - def test_unmarshal_types(self): - - empty_log_dict = { - 'id': 'c55qjplpsjir0oo1kdj0', - 'state': 'QUEUED', - 'name': 'toil-bbc72af7-e11a-4831-9392-669ea6c309a1-0', - 'executors': [{ - 'image': - 'testImage', - 'command': [ - '_toil_kubernetes_executor', - 'gAWVGwAAAAAAAAB9lIwHY29tbWFuZJSMCnNsZWVwIDEwMDCUcy4=' - ] - }], - 'logs': [{}], - 'creation_time': "2017-10-09T17:00:00" - } - - expected = empty_log_dict.copy() - expected["creation_time"] = dateutil.parser.parse( - expected["creation_time"] - ) +def test_camel_to_snake(): + assert camel_to_snake("FooBar") == "foo_bar" + assert camel_to_snake("fooBar") == "foo_bar" + assert camel_to_snake("foo_bar") == "foo_bar" + - empty_log_str = json.dumps(empty_log_dict) - o1 = unmarshal(empty_log_dict, Task) +def test_unmarshal(): + # test unmarshalling with no or minimal contents + try: + unmarshal(CancelTaskRequest(id="foo").as_json(), CancelTaskRequest) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") - self.assertEqual(o1.as_dict(), expected) - self.assertEqual(o1.as_json(), empty_log_str) + try: + unmarshal(CancelTaskResponse().as_json(), CancelTaskResponse) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(CreateTaskResponse(id="foo").as_json(), CreateTaskResponse) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal( + Executor(image="alpine", command=["echo", "hello"]).as_json(), Executor + ) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(ExecutorLog().as_json(), ExecutorLog) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(GetTaskRequest(id="foo", view="BASIC").as_json(), GetTaskRequest) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(Input().as_json(), Input) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(ListTasksRequest().as_json(), ListTasksRequest) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(ListTasksResponse().as_json(), ListTasksResponse) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(Output().as_json(), Output) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(OutputFileLog().as_json(), OutputFileLog) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(Resources().as_json(), Resources) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(ServiceInfo().as_json(), ServiceInfo) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(Task().as_json(), Task) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + try: + unmarshal(TaskLog().as_json(), TaskLog) + except Exception: + pytest.fail("Raised ExceptionType unexpectedly!") + + # test special cases + assert unmarshal(None, Input) is None + with pytest.raises(TypeError): + unmarshal([], Input) + with pytest.raises(TypeError): + unmarshal(1, Input) + with pytest.raises(TypeError): + unmarshal(1.3, Input) + with pytest.raises(TypeError): + unmarshal(True, Input) + with pytest.raises(UnmarshalError): + unmarshal("foo", Input) + + # test with some interesting contents + test_invalid_dict = {"foo": "bar"} + test_invalid_str = json.dumps(test_invalid_dict) + with pytest.raises(UnmarshalError): + unmarshal(test_invalid_dict, CreateTaskResponse) + with pytest.raises(UnmarshalError): + unmarshal(test_invalid_str, CreateTaskResponse) + + test_simple_dict = { + "url": "file://test_file", + "path": "/mnt/test_file", + "type": "FILE", + } + test_simple_str = json.dumps(test_simple_dict) + o1 = unmarshal(test_simple_dict, Input) + o2 = unmarshal(test_simple_str, Input, convert_camel_case=False) + assert isinstance(o1, Input) + assert isinstance(o2, Input) + assert o1 == o2 + assert o1.as_dict() == test_simple_dict + assert o1.as_json() == test_simple_str + + test_complex_dict = { + "name": "test", + "inputs": [ + { + "url": "file:///storage/inputs/test_file", + "path": "/mnt/test_file", + "type": "FILE", + } + ], + "outputs": [ + { + "url": "file:///storage/outputs/test_outputfile", + "path": "/mnt/test_outputfile", + "type": "FILE", + } + ], + "executors": [ + {"image": "alpine", "command": ["echo", "hello"], "env": {"HOME": "/home/"}} + ], + "logs": [ + { + "start_time": "2017-10-09T17:05:00.0Z", + "end_time": "2017-10-09T17:40:30.0Z", + "metadata": {"testmeta": "testvalue"}, + "logs": [ + { + "start_time": "2017-10-09T17:06:30.0Z", + "end_time": "2017-10-09T17:39:50.0Z", + "exit_code": 0, + "stdout": "hello", + "stderr": "", + } + ], + "outputs": [ + { + "url": "file:///storage/outputs/test_outputfile", + "path": "/mnt/test_outputfile", + "size_bytes": "3333", + } + ], + "system_logs": [ + "level='info' msg='Download started' \ + timestamp='2018-05-04T09:12:42.391262682-07:00' \ + task_attempt='0' executor_index='0' \ + url='swift://biostream/protograph'" + ], + } + ], + "resources": { + "cpu_cores": 1, + "ram_gb": 2, + "disk_gb": 3, + "preemptible": True, + "zones": ["us-east-1", "us-west-1"], + }, + "creation_time": "2017-10-09T17:00:00.0Z", + } + + test_complex_str = json.dumps(test_complex_dict) + o1 = unmarshal(test_complex_dict, Task) + o2 = unmarshal(test_complex_str, Task) + assert isinstance(o1, Task) + assert isinstance(o2, Task) + assert o1 == o2 + expected = test_complex_dict.copy() + + # handle expected conversions + expected["logs"][0]["outputs"][0]["size_bytes"] = int( + expected["logs"][0]["outputs"][0]["size_bytes"] + ) + expected["logs"][0]["start_time"] = dateutil.parser.parse( + expected["logs"][0]["start_time"] + ) + expected["logs"][0]["end_time"] = dateutil.parser.parse( + expected["logs"][0]["end_time"] + ) + expected["logs"][0]["logs"][0]["start_time"] = dateutil.parser.parse( + expected["logs"][0]["logs"][0]["start_time"] + ) + expected["logs"][0]["logs"][0]["end_time"] = dateutil.parser.parse( + expected["logs"][0]["logs"][0]["end_time"] + ) + expected["creation_time"] = dateutil.parser.parse(expected["creation_time"]) + assert o1.as_dict() == expected + + +def test_unmarshal_types(): + empty_log_dict = { + "id": "c55qjplpsjir0oo1kdj0", + "state": "QUEUED", + "name": "toil-bbc72af7-e11a-4831-9392-669ea6c309a1-0", + "executors": [ + { + "image": "testImage", + "command": [ + "_toil_kubernetes_executor", + "gAWVGwAAAAAAAAB9lIwHY29tbWFuZJSMCnNsZWVwIDEwMDCUcy4=", + ], + } + ], + "logs": [{}], + "creation_time": "2017-10-09T17:00:00", + } + + expected = empty_log_dict.copy() + expected["creation_time"] = dateutil.parser.parse(expected["creation_time"]) + + empty_log_str = json.dumps(empty_log_dict) + o1 = unmarshal(empty_log_dict, Task) + + assert o1.as_dict() == expected + assert o1.as_json() == empty_log_str + + +def test_unmarshal_additional_cases(): + # Additional test cases for more coverage + test_dict_with_extra_fields = {"id": "foo", "extra_field": "extra_value"} + with pytest.raises(UnmarshalError): + unmarshal(test_dict_with_extra_fields, CancelTaskRequest) + + test_dict_with_nested_objects = { + "id": "foo", + "executors": [{"image": "alpine", "command": ["echo", "hello"]}], + } + result = unmarshal(test_dict_with_nested_objects, Task) + assert isinstance(result, Task) + assert result.executors[0].image == "alpine" + assert result.executors[0].command == ["echo", "hello"] + + test_dict_with_invalid_json = '{"id": "foo", "invalid_json": }' + with pytest.raises(UnmarshalError): + unmarshal(test_dict_with_invalid_json, CancelTaskRequest)