diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3fc52846..00000000 --- a/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - def __repr__ diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 7634b09f..00000000 --- a/.dockerignore +++ /dev/null @@ -1,40 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*/__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -doc/ -develop-eggs/ -dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg -config/ -*.html -venv/ -htmlcov/ -.coverage -coverage.xml -.tox/ -docs/_build/ -_static/ -_template/ -html/ -doctrees/ -build/lib/ -*.swp -.idea diff --git a/.github/workflows/binder-cache.yml b/.github/workflows/binder-cache.yml deleted file mode 100644 index d75a5da4..00000000 --- a/.github/workflows/binder-cache.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Cache Binder - -on: - push: - branches: - - master - -jobs: - Create-MyBinderOrg-Cache: - runs-on: ubuntu-latest - steps: - - name: cache binder build on mybinder.org - uses: jupyterhub/repo2docker-action@master - with: - NO_PUSH: true - MYBINDERORG_TAG: ${{ github.event.ref }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 60cc3d6a..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: CI - -on: - push: - branches: - - master - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - pull_request: - branches: - - '*' - -jobs: - tests: - name: Tests - # Pin to 20.04 as we still require Python 3.6 - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - - steps: - - - name: Checkout - uses: actions/checkout@v1 - - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Tox - run: | - python -m pip install --upgrade pip - pip install tox - - - name: Run Tox - run: tox -e py - if: ${{matrix.python-version != 3.6 }} - - - name: Run Tox with coverage - run: tox -e clean,py,stats_xml - if: ${{ matrix.python-version == 3.6 }} - - - name: Upload to CodeCov - uses: codecov/codecov-action@v1 - with: - file: ./coverage.xml - if: ${{ matrix.python-version == 3.6 }} - - docs: - name: Docs - # Pin to 20.04 as we still require Python 3.6 - runs-on: ubuntu-20.04 - - steps: - - name: Checkout - uses: actions/checkout@v1 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - - name: Install Tox - run: | - python -m pip install --upgrade pip - pip install tox - - - name: Generate Docs - run: tox -e docs - - lint: - name: Lint - # Pin to 20.04 as we still require Python 3.6 - runs-on: ubuntu-20.04 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - - name: Install Tox - run: | - python -m pip install --upgrade pip - pip install tox - - - name: Lint - run: tox -e lint - - build-and-publish: - name: Build and Publish - # Pin to 20.04 as we still require Python 3.6 - runs-on: ubuntu-20.04 - needs: [tests, docs, lint] - if: startsWith(github.ref, 'refs/tags') - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - - name: Install pypa/build - run: | - python -m pip install build --user - - - name: Build binary wheel and source tarball - run: | - python -m build --sdist --wheel --outdir dist/ - - - name: Get tag version - id: get_tag_version - run: | - echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - echo ::set-output name=VERSION_NO_PREFIX::${GITHUB_REF#refs/tags/v} - - - name: Create Release Message - id: release_message - run: | - # Substitutions required to get multi-line working. - # See https://github.community/t/set-output-truncates-multiline-strings/16852 - MESSAGE="$(git --no-pager log -1 --pretty=format:'%b')" - MESSAGE="${MESSAGE//'%'/'%25'}" - MESSAGE="${MESSAGE//$'\n'/'%0A'}" - MESSAGE="${MESSAGE//$'\r'/'%0D'}" - echo ::set-output name=BODY::$MESSAGE - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.get_tag_version.outputs.VERSION }} - release_name: ${{ steps.get_tag_version.outputs.VERSION }} - body: ${{ steps.release_message.outputs.BODY }} - draft: true - prerelease: false - - - name: Upload Release tar gzip - id: upload-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./dist/transcriptic-${{ steps.get_tag_version.outputs.VERSION_NO_PREFIX }}.tar.gz - asset_name: transcriptic-${{ steps.get_tag_version.outputs.VERSION_NO_PREFIX }}.tar.gz - asset_content_type: application/gzip - - - name: Upload Release wheel - id: upload-release-asset-wheel - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./dist/transcriptic-${{ steps.get_tag_version.outputs.VERSION_NO_PREFIX }}-py3-none-any.whl - asset_name: transcriptic-${{ steps.get_tag_version.outputs.VERSION_NO_PREFIX }}-py3-none-any.whl - asset_content_type: application/x-wheel+zip - - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 84b0e2ca..00000000 --- a/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*/__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -doc/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg -config/ -*.html -venv/ -htmlcov/ -.coverage -coverage.xml -.tox/ -docs/_build/ -_static/ -_template/ -html/ -doctrees/ -build/lib/ -*.swp -.idea diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index f0a04ff2..00000000 --- a/.isort.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[settings] -profile = black -atomic = true -include_trailing_comma = true -lines_after_imports = 2 -lines_between_types = 1 -use_parentheses = true -src_paths = ["transcriptic", "test"] -filter_files = true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index bd16b5b7..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.1.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/psf/black - rev: 20.8b1 - hooks: - - id: black - language_version: python3.6 -- repo: https://github.com/timothycrosley/isort - rev: 5.6.4 - hooks: - - id: isort -- repo: https://github.com/pycqa/pylint - rev: pylint-2.5.2 - hooks: - - id: pylint - args: - - --disable=W,R,C - - --rcfile=.pylintrc - - --jobs=1 - name: Pylint - language: system - types: [python] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 66ef0099..00000000 --- a/.pylintrc +++ /dev/null @@ -1,21 +0,0 @@ -[MASTER] - -load-plugins= - pylint.extensions.docparams, - -# Ignore non-python files -ignore-patterns= - ^.*\.(?!py$)[^.]+$ - - -[MESSAGES CONTROL] - -disable= - fixme, - wildcard-import, - too-many-arguments, - too-many-lines, - -[FORMAT] - -max-line-length=88 diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 34928825..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -# Python env setup to use docs -python: - version: 3.6 - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index c8846654..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,1387 +0,0 @@ -Changelog -========= - -v9.6.3 ----------- - -Fixed ------ -- Allow empty body in put post patch requests with rsa requests - -v9.6.2 ----------- - -Fixed ------ -- Always rebuild authorization headers even on redirects - -Updated -------- -- Pin CI environment to Ubuntu 20.04 to support running Py 36 tests - -v9.6.1 ------- - -Updated ---------- -- Transcriptic CLI to use "strateos" not "transcriptic" as default API root - - -Updated -------- -- Preferring Bearer token over user token when configuring Connection session. -- Using json-api runs endpoint to fetch project runs instead of previous project-runs endpoint. Change was made to - avoid possible timeouts, and improve efficiency. Fields returned from the original project-runs api call were limited - to the fields used by the cli. This may be a BREAKING CHANGE if using `config.runs` method. - -v9.6.0 ------- - -Fixed ------ -- requests dependency to requests>2.21.0,<3 -- upload-release command to release archive requires the `user_id` of the session. -- Linting, equivalence, and docstring issues. - -Updated -------- -- Updated pillow version <=8,<9 -- Pandas version to >=1, <2 to support Python 3.9 - -Added -~~~~~ -- Python 3.9 support - - -v9.5.0 ------- - -Updated -~~~~~~~ -- Jinja2 version >=3.0 to be compatible with the data-processor-service. - -Fixed ------ -- upgraded base docker image to 3.7 to fix nbgitpuller error - -v9.4.2 ------- - -Added -~~~~~ -- Stdout the detail error_info of the `generation_errors` in the launch request. - -v9.4.1 ------- - -Added -~~~~~ -- optional predecessor_id argument in commands.launch. and config.submit_launch_request - If passed, will populate on web. - -v9.4.0 ------- - -Added -~~~~~ -- New `-e/--exclude` and `-i/--include` flags to `exec`. It filters out the autoprotocol - instructions in the backend. - -v9.3.1 ------- - -Fixed -~~~~~ - -- Adapt the backend url resolution with the new configuration of the frontend client. - -v9.3.0 ------- - -Added -~~~~~ -- A new `-tc-suggestion/--time-constraints-are-suggestion` flag to `exec`. -- A new `--no-redirect` flag to `exec`. It allows the endpoint of the scle test - workcell instance to be used, instead of the client dashboard. - -Fixed -~~~~~ - -- The workcell id in `exec` was forced to be `wcN`. There is now no restrictions. - -Updated -~~~~~~~ - -- Added support for sessions and absolute time constraint in `exec` CLI command. - Added "--sessionId", "--schedule-at", and "--schedule-delay" flags. -- The api url expected by the `exec` method has been changed to be the url of - the new dashboard (unless `--no-redirect` is used). It has the shape: - `base_url/facility/workcell`. It does not require `http` to be added anymore. - - -v9.2.0 ----------- - -Added -~~~~~ -- A new `generated_containers` attribute to the `Instruction` object - -v9.1.0 ----------- - -Added -~~~~~ -- A new `exec` command to send autoprotocol to a test workcell -- isort for automatic import sorting -- Example initial tests for `commands` file using `responses` pattern, starting with - `submit` and `projects`. -- Deprecation warning for existing `-i` option for `projects` command. -- Binder build cache step -- All API requests will now pass the organization context as a request header - -Fixed -~~~~~ - -- Issue with CodeCov for GitHub action CI -- `-i` option for `projects` command did not output anything to console when called from - cli. -- Pinned numpy to <=1.19.5 due to an incompatibility issue with numpy 1.20.0 on python 3.7 - -Updated -~~~~~~~ - -- Added new option "--names" to `projects` CLI command. This is meant as a better - named and more intuitive replacement for the existing `-i` option. -- Returned more explicit error statuses for `projects` and `submit` commands. -- Remove notebooks directory as we break it out into a `separate repository `_ -- Plumbed test posargs through to allow local execution of specific test files. -- Autoprotocol dependency to >=7.6.1,<8 for `Instruction` `informatics` attribute - -v9.0.0 ------- - -Added -~~~~~ - -- example notebook for Analysis package exploration -- sample Absorbance and Kinetics datasets -- `transcriptic.sampledata` module for enabling mocked Jupyter object exploration without establishing an explicit connection -- example notebook for Jupyter object exploration -- Downloads badge to keep track of usage - -Updated -~~~~~~~ - -- Migrated from travis to github actions as a CI backend -- Remove unused `scipy` dependency -- Break out Jupyter objects into individual files. This affects direct imports from - `transcriptic.jupyter.objects` - - -v8.1.2 ------- - -Fixed -~~~~~ - -- Issue with bash syntax for Travis config - - -v8.1.1 ------- - -Added -~~~~~ - -- Codecov configuration for coverage -- Binder badge and updated Dockerfile - -Fixed -~~~~~ - -- Repeated deploy issue with Travis config -- Encoding error when using dataset upload API with request signing enabled -- Bad request error when making calls to non-Strateos endpoints (e.g. S3) with authorization headers set - - -v8.1.0 ------- - -Added -~~~~~ - -- Support bearer token authentication - -Updated -~~~~~~~ - -- Pin black version to 20.8b1 for local dev env consistency -- Remove util.robotize/humanize and change callers to use autoprotocol directly - -v8.0.0 ------- - -Added -~~~~~ - -- .readthedocs.yml configuration for docs building, corresponding badge -- pre-commit framework and linting -- auto-deploy functionality -- Fish completions -- Autoprotocol dependency for ``analysis`` package - -Updated -~~~~~~~ - -- Transitioned all .md files to .rst files -- Doc dependencies, Sphinx to 2.4, releases to 1.6.3, sphinx-rtd-theme to 0.4.3 -- Test dependencies, pytest to 5.4, pylint to 2.5.2, tox to 3.15 -- Travis build reorganized to distinct jobs -- Support for Python 3.8 -- Made adding autocomplete functionality more explicit -- Base CLI test framework -- Standardize on kebab-case for cli commands -- Plotly dependency to 1.13 -- Matplotlib dependency to 3.0.3 -- Spectrophotometry plots now render offline -- Dataset object html representation increased - -Fixed -~~~~~ - -- Kinetics.Spectrophotometry.plot() function now works again -- Spectrophotometry.Absorbance/Fluorescence/Luminescence plot() works - again - -Removed -~~~~~~~ - -- References to Phabricator -- Support for Python 3.5 - -v7.1.0 ------- - - -Added -~~~~~ - -- Add optional Run title to Launch command - - -Fixed -~~~~~ - -- Removal of ``can_submit_autoprotocol`` feature group in the default - ``.transcriptic`` - -v7.0.0 ------- - - -Added -~~~~~ - -- zsh auto-completion support - - -Updated -~~~~~~~ - -- Support only Python >=3.5, drop Python 2 support -- Pin dependencies -- Email references to point towards strateos - -v6.0.0 ------- - - -Added -~~~~~ - -- Added ``Connection.from_default_config()`` method and tests -- Added ``Connection.modify_aliquot_properties()`` for aliquot property - managment - - -Updated -~~~~~~~ - -- Lint and docs, test cleanup -- Starter work on testing ``Connection`` methods. -- Updated dependencies to only support python 2.7 and python >=3.5 - -v5.6.0 ------- - - -Updated -~~~~~~~ - -- run tox tests against python 3.5 instead of 3.4 - - -Added -~~~~~ - -- lint and build docs with tox -- DataObject class which should help ease the transition from Datasets - to DataObjects with regards to fetching data. - - -Fixed -~~~~~ - -- doc and lint errors - -v5.5.1 ------- - - -Fixed -~~~~~ - -- Docstring building - -v5.5.0 ------- - - -Added -~~~~~ - -- ``attachments`` attribute on ``Dataset`` - - -Fixed -~~~~~ - -- Analyzed Dataset content-disposition - -v5.4.1 ------- - - -Updated -~~~~~~~ - -- Separated out the CLI logic into programatically callable functions. - -v5.4.0 ------- - - -Added -~~~~~ - -- Ability to filter by package id when using transcriptic launch - -v5.3.10 -------- - - -Updated -~~~~~~~ - -- Made ``transcriptic analyze`` command visible to all - -v5.3.9 ------- - - -Updated -~~~~~~~ - -- Analyze handles missing pricing information. - -v5.3.8 ------- - - -Updated -~~~~~~~ - -- Jinja2 dependency made less strict - -v5.3.7 ------- - - -Fixed -~~~~~ - -- Fixed dataset and release uploading. - -v5.3.6 ------- - - -Fixed -~~~~~ - -- Fixed encoding bug with Python 3 - -v5.3.5 ------- - - -Fixed -~~~~~ - -- Fixed backwards compatibility bug with using ``makedirs`` with Python - 2 - -v5.3.4 ------- - - -Updated -~~~~~~~ - -- Added ``transcriptic generate_protocol `` that generates a - scaffold of a python protocol. - -v5.3.3 ------- - - -Updated -~~~~~~~ - -- ``transcriptic summarize`` now has an optional ``--html`` argument. - When specified it will return a url to view the autoprotocol. - -v5.3.2 ------- - - -Updated -~~~~~~~ - -- ``transcriptic select_org`` now has an optional ``organization`` - argument. When specified, i.e. ``transcriptic select_org my_org``, - it’ll skip the prompt and set the organization value to ``my_org`` - directly. - -v5.3.1 ------- - - -Updated -~~~~~~~ - -- ``transcriptic login`` now properly respects the ``--api-root`` - option and persists the result into the dotfile - -v5.3.0 ------- - - -Updated -~~~~~~~ - -- ``transcriptic launch --save_input`` now outputs the same type of - JSON ### Added -- ``test`` flag to ``transcriptic launch``, enabling the submission of - test runs via the launch command - -v5.2.0 ------- - - -Added -~~~~~ - -- ``warp_events``, a new property of the ``Instruction`` object is - added. This provides information on discrete monitoring events ### - Updated -- Instruction object now has an ``Id`` field ### Fixed -- Fixed issue with broken direct imports of Jupyter objects - (e.g. ``from transcriptic import Run``) - -v5.1.0 ------- - - -Updated -~~~~~~~ - -- Shifted non-core cli dependencies (i.e. those used in analysis) to - the ``extras_require`` field -- Shifted relative imports in base ``__init__`` file to make this - possible -- Shifted ``objects`` to a separate Jupyter module, but preserved - existing relative imports path for backwards compatibility -- Documentation updated to reflect the changes - -v5.0.4 ------- - - -Fixed -~~~~~ - -- Error with ``transcriptic launch --local`` when a file is provided - -v5.0.3 ------- - - -Fixed -~~~~~ - -- FileNotFound incompatibility error for Python2 (when ~/.transcriptic - file isn’t specified) - -v5.0.2 ------- - - -Fixed -~~~~~ - -- Made cookie updates actually update headers - -v5.0.1 ------- - - -Fixed -~~~~~ - -- in ``Connection.upload_dataset()``, only convert io.StringIO instance - to bytes, not StringIO.StringIO instance -- Issue with ``upload-release`` - -v5.0.0 ------- - - -Added -~~~~~ - -- Added concept of HiddenOption and email and token as input parameters - ### Updated -- Use ``Sessions`` object for maintaining persistent api connection -- Reworked env_args and headers setting and getting to be clearer and - more consistent -- CLI now automatically fits flags in the order of: –flag, environment - variable, .transcriptic -- More formal support for cookie-based authentication ### Fixed -- Improvements to the way non-unique projects are handled -- Improved error handling for Py2 ### Removed -- ``use_environ`` flag is now deprecated in ``Connection``. Please - specify environment parameters directly -- ``organization`` is now deprecated from ``Connection``. Please use - ``organization_id`` instead - -v4.3.0 ------- - - -Updated -~~~~~~~ - -- Reworked the structure of ``run.data`` to be more verbose - -v4.2.1 ------- - - -Added -~~~~~ - -- ``transcriptic upload_dataset`` to CLI - -v4.2.0 ------- - - -Added -~~~~~ - -- ``upload_dataset`` to api object and surrounding infrastructure ### - Updated -- Dataset object is now initialized via a more stable route ### Fixed -- Reworked ``run.data`` route based on changes to web response - -v4.1.2 ------- - - -Fixed -~~~~~ - -- Quick bugfix to ``run.data`` route due to breaking web change - -v4.1.1 ------- - - -Fixed -~~~~~ - -- Minor bug with default behavior with ``select_org`` prompt in - ``select_org`` and ``login`` - -v4.1.0 ------- - - -Added -~~~~~ - -- ``transcriptic payments`` to view payment methods and their - corresponding ids -- ``--payment`` flag to ``launch`` and ``submit`` to allow - specification of payment methods ### Updated -- ``transcriptic launch`` now presents and the price and asks for a - confirmation before proceeding. ``--accept_quote`` flag is added - which will override the confirmation - -v4.0.1 ------- - - -Fixed -~~~~~ - -- Remote behavior of ``transcriptic protocols`` -- Missing ``container`` key in Dataset initialization now returns a - warning instead of an error - -v4.0.0 ------- - - -Added -~~~~~ - -- Conditional display of views based on enabled feature_flags ### - Updated -- Default behavior of ``protocols`` and ``launch`` to remote instead - -v3.12.0 -------- - - -Added -~~~~~ - -- New –json flag for runs, projects and protocols for fetching JSON ### - Fixed -- Fixed bug in PlateRead that caused data overwrites if multiple - instances of the same group_label were present - -v3.11.0 -------- - - -Updated -~~~~~~~ - -- Handling of 403 routes -- Documentation to reflect permissions changes -- Minor rework of launch_request - -v3.10.3 -------- - - -Fixed -~~~~~ - -- Bug with launch_request - -v3.10.2 -------- - - -Fixed -~~~~~ - -- AP2EN_test failures still requiring protocol -- object.py requirement for ``autoprotocol.container_types`` - -v3.10.1 -------- - - -Fixed -~~~~~ - -- Minor bugfix for ``_parse_protocol`` - -v3.10.0 -------- - - -Updated -~~~~~~~ - -- Removed setup.py requirement for ``autoprotocol-python`` - -v3.9.2 ------- - - -Fixed -~~~~~ - -- Bugfix to resolve error caused by attempting to print unicode - characters on the CLI. - -v3.9.1 ------- - - -Fixed -~~~~~ - -- Bugfix to remove ``data_keys`` from Absorbance function, which is no - longer returned from webapp - -v3.9.0 ------- - - -Added -~~~~~ - -- Add raw_data property to the ``Dataset`` object -- Add ability to cross reference aliquots with their data using the - ``Dataset`` object - -v3.8.0 ------- - - -Added -~~~~~ - -- Ability to add ``--dye_test`` flag to ``transcriptic preview`` to - convert a run into a water/dye test - -v3.7.1 ------- - - -Fixed -~~~~~ - -- Fixed minor bug in launching local protocols with - ``transcriptic launch`` - -v3.7.0 ------- - - -Added -~~~~~ - -- Ability to browse your inventory using the ``transcriptic inventory`` - command E.g. ``transcriptic inventory water`` -- Ability to launch protocols remotely using the ``--remote`` flag. - E.g. ``transcriptic launch Pipetting --remote`` -- Ability to view available remote protocols for launching using - ``transcriptic protocols --remote`` -- Ability for ``transcriptic summarize`` to retrieve resource strings - with the ``--lookup`` flag - - -Fixed -~~~~~ - -- resources route has been updated to match web return -- Ap2En for dispense and provision -- resources route now accepts resource IDs - -v3.6.0 ------- - - -Added -~~~~~ - -- Object helpers to allow more natural property access. E.g. - ``myRun.instructions.Instructions`` = ``myRun.Instructions`` - - -Updated -~~~~~~~ - -- Misc formatting changes for HTML representation - - -Fixed -~~~~~ - -- Underyling ``handle_response`` code to be more robust - -v3.5.1 ------- - - -Added -~~~~~ - -- Row index of the Container.aliquots DataFrame object now corresponds - to the well index - - -Fixed -~~~~~ - -- Stored volume in the Container.aliquots DataFrame as a Unit object - instead of unicode - -v3.5.0 ------- - - -Added -~~~~~ - -- timeout property for Run objects -- data_ids property for Run objects - - -Updated -~~~~~~~ - -- data property for Run objects gives more informative errors when - failing due to timeout -- ``.monitoring`` method is now shifted to the Instruction object from - the Run object -- Optional parameters can now be handled by ``get_route`` ### Fixed -- Existing route for monitoring data - -v3.4.3 ------- - - -Fixed -~~~~~ - -- Made local commands robust to lack of internet access - -v3.4.2 ------- - - -Fixed -~~~~~ - -- Broaden exception clause for general Python compatibility - -v3.4.1 ------- - - -Added -~~~~~ - -- Usage analytics support to CLI ### Updated -- Minor documentation fixes - -v3.4.0 ------- - - -Added -~~~~~ - -- ``transcriptic select_org`` in CLI now allows you to switch - organizations without re-authenticating -- ``User-agent`` information to headers -- ``Run.containers`` to return a list of containers used within the run - -v3.3.1 ------- - - -Fixed -~~~~~ - -- Updated ``transcriptic runs`` route to reflect reality - -v3.3.0 ------- - - -Added -~~~~~ - -- Ability for ``api.get_zip`` to handle larger zip-files by downloading - to a local file -- ``cover`` and ``storage`` attributes to Container object -- Ability to construct and visualize a given protocol’s job tree using - a flag on the CLI ### Updated -- Updated english’s summarize to handle all currently-implemented - instructions - -v3.2.5 ------- - - -Fixed -~~~~~ - -- Fixed initialization of Container object - -v3.2.4 ------- - - -Added -~~~~~ - -- Helper function ``flatmap`` into util ### Fixed -- Fixed resources route in CLI. ``transcriptic resources 'query'`` now - works - -v3.2.3 ------- - - -Updated -~~~~~~~ - -- Simplified ``Container._parse_container_type`` to use matching AP-Py - container-type object whenever possible - -v3.2.2 ------- - - -Added -~~~~~ - -- additional documentation for ``Connection`` object ### Updated -- update relevant documentation.rst files - -v3.2.1 ------- - - -Updated -~~~~~~~ - -- Updated “url” reference in run attributes to use “id” instead, - in-line with a web update ### Fixed -- Update docs/requirements.txt to be PEP440 compatible - -v3.2.0 ------- - - -Updated -~~~~~~~ - -- Reworked ``Instruction`` object -- Reworked ``Run.instructions`` to return a Dataframe of - ``Instruction`` objects -- ``Aliquot`` object has been reworked into Container object as an - ``aliquots`` property - - -Removed -~~~~~~~ - -- ``Resource`` object has been removed from the library as its - currently unused - - -Fixed -~~~~~ - -- Change check for ImagePlate to be more generic -- Setup now requires plotly 1.9.6 (for plotly offline/ipython - compatibility reasons) - -v3.1.0 ------- - - -Added -~~~~~ - -- Tab completion for CLI (enabled by sourcing - ``transcriptic_complete.sh``) -- New API route for getting zipfiles: ``api.get_zip`` -- Made -h option synonymous with –help - -v3.0.2 ------- - - -Updated -~~~~~~~ - -- Setup now requires plotly 1.9.6 or greater - -v3.0.1 ------- - - -Fixed -~~~~~ - -- Better handling of Datasets with no ``well_map`` property in - kinetics.spectrophotometry - -v3.0.0 ------- - - -Added -~~~~~ - -- New documentation for the new testing framework and how to write - tests -- Added Dockerfile for running Transcriptic containers. Compatible with - CI tools (e.g. Jenkins) as well -- New documentation added and hosted on - http://transcriptic.readthedocs.io/en/latest/ - - -Updated -~~~~~~~ - -- Migrated the test framework from vanilla unittest2 to py.test -- Rewrote documentation structure and added misc. documentation related - changes -- ``api`` module has been removed and merged into ``config`` module. - The Connection object now handles all api calls. -- All references to ``ctx`` has been renamed to ``api`` - - -Fixed -~~~~~ - -- Fixed bug in spectrophotometry handling attributes -- Fixed compatibility issue with running ``transcriptic preview`` on - python3 - -v2.3.1 ------- - - -Updated -~~~~~~~ - -- Transcriptic CLI subcommands: compile, init, preview, summarize no - longer require login - - -Fixed -~~~~~ - -- ``transcriptic runs`` command now works in CLI - -v2.3.0 ------- - - -Added -~~~~~ - -- ``__version__`` variable for checking version. Enable version - checking in CLI using ``transcriptic --version`` -- New Analysis module: Kinetics; ``Kinetics`` base object and - ``Kinetics.Spectrophotometry`` for analyzing kinetics-based data such - as growth curves -- Expose additional properties of Dataset object: ``operation``, - ``container``, ``data_type`` - -v2.2.1 ------- - - -Updated -~~~~~~~ - -- Objects module has been heavily reworked and documentation added. - This is especially true for Project, Run and Dataset objects - - -Fixed -~~~~~ - -- Fixed package related CLI issues - -v2.2.0 ------- - - -Added -~~~~~ - -- ``api`` module for handling all calls including responses and - exceptions -- ``Connection`` object now mirrors most of the CLI functionality -- basic test infrastructure and examples for testing API module - - -Updated -~~~~~~~ - -- all separate requests, context or connection object calls are now - consolidated and re-routed to go through the api and routes module - - -Removed -~~~~~~~ - -- all direct api calls (get, put, push, pull) are removed from - Connection. Users are encouraged to use the corresponding calls from - the ``api`` module instead - -v2.1.2 ------- - - -Fixed -~~~~~ - -- Change in datasets route - - -Updated -~~~~~~~ - -- Removed additional shadowed variable names - -v2.1.1 ------- - - -Added -~~~~~ - -- ``imaging`` module with ``ImagePlate`` as the first class for - representing plate images. Focus is placed on IPython rendering -- PIL dependency for image manipulation - -v2.1.0 ------- - - -Updated -~~~~~~~ - -- Major refactor of code to be in-line with PEP8 -- Removed unnecessary modules and renamed shadowed variables - -v2.0.11 -------- - - -Updated -~~~~~~~ - -- Updated behavior of ``transcriptic login`` to be clearer and to - return appropriate error messages - - -Fixed -~~~~~ - -- print statement for launch - -v2.0.10 -------- - - -Added -~~~~~ - -- pypi tags for setup.py such as ``classifiers`` and ``license`` - - -Fixed -~~~~~ - -- Updated Container object to automatically populate safe_min_volume_ul - - -Removed -~~~~~~~ - -- Unused dependency: scikit-learn - -v2.0.9 ------- - - -Added -~~~~~ - -- Updated manifest json parsing to deserialize into an OrderedDict, - preserving key order, which enables quick launch inputs to be ordered - -v2.0.8 ------- - - -Added -~~~~~ - -- ``launch`` command now supports –save_input option to save the - protocol input as a local file - - -Fixed -~~~~~ - -- ``launch`` command now properly supported either a project name or - project id for the ``project`` option -- typo AutoProtocol -> Autoprotocol - -v2.0.7 ------- - - -Added -~~~~~ - -- ``launch`` command to configure and run protocols without needing to - package and upload them first - -v2.0.6 ------- - - -Fixed -~~~~~ - -- RMSE calculation in spectrophotometry.py now reports correct RMSE -- transcriptic submit now correctly parses new autopick group -- containter attributes are correctly requested from transcriptic via - spectrophotometry.py - -v2.0.5 ------- - - -Added -~~~~~ - -- List runs in a specific project using the - ``transcriptic runs `__ to - here, converted that code to a Python Client Library, -- CLI functionality has not changed other than renaming some commands: - - - ``release`` –> ``build-release`` - - ``upload`` –> ``upload-release`` - - ``new-project`` –> ``create-project`` - - ``new-package`` –> ``create-package`` - - ``run`` –> ``compile`` diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 1859fc56..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,121 +0,0 @@ -Contributing -============ - -Structure ---------- - -The Transcriptic Python Library (TxPy) is separated into two main -portions: the command line interface (CLI) and the Jupyter notebook -interface. - -Both the CLI and the notebook interface uses the base ``Connection`` -object for making relevant application programming interface (API) calls -to Transcriptic. The ``Connection`` object itself uses the ``routes`` -module to figure out the relevant routes and passes that onto the -``api`` module for making these calls. - -The base functionality of the CLI is handled by the ``cli`` module which -is the front-facing interface for users. The ``english`` module provides -autoprotocol parsing functionalities, and the ``util`` module provides -additional generic helper functions. - -The base functionality of the notebook interface is exposed through the -``objects`` module. Additional analysis of these objects is provided -through the ``analysis`` module. In general, html representations should -be provided as much as possible. - -For analysis purposes, we prefer using Pandas DataFrames and NumPy -arrays for representing and slicing data. For plotting, matplotlib and -plotly is preferred. - -Version Compatibility ---------------------- - -TxPy is written with Python 3.6+ compatibility in mind. Python 2 is no -longer officially supported. - -General Setup -------------- - -Use of virtual environment to isolate the development environment is -highly recommended. There are several tools available such as -`conda `__ -and `pyenv `__. - -After activating your desired virtualenv, install the dependencies using -the snippet below - -:: - - pip install -e '.[test, docs]' - pre-commit install - -Styling and Documentation -------------------------- - -All code written should follow the `PEP8 -standard `__ - -For documentation purposes, we follow `NumPy style doc -strings `__ - -We use `pre-commit `__ as our linting and -auto-formatting framework. Lint is checked with -`pylint `__ and auto-formatting is done with -`black `__. This is -automatically executed as part of the ``git commit`` and ``git push`` -workflows. You may also execute it manually using the snippet below. - -:: - - pre-commit run - -Testing -------- - -For testing purposes, we write tests in the ``test`` folder in the -`pytest `__ format. We -also use `tox `__ for automating -tests. - -The ``tox`` command is run by CI and is currently configured to run -the main module tests, generate a coverage report and build -documentation. - -Generally, please ensure that all tests pass when you execute ``tox`` in -the root folder. - -:: - - cd $TXPY_ROOT_DIR - tox - -If you’re using `pyenv `__ to manage -python versions, ensure you have all the tested environments in your -``.python-version`` file. i.e.\ ``pyenv local 3.6.12 3.7.9 3.8.7`` - -Running Specific Tests -~~~~~~~~~~~~~~~~~~~~~~ - -Specific tests are controlled by the ``tox.ini`` configuration file. - -To run just the main module tests, execute ``python setup.py test`` in -the root folder. This is specified by the main ``[testenv]`` flag in -``tox.ini``. - -To run a specific test, execute ``python setup.py test -a path/to/test.py``. -Using tox, ``tox -e py36 -- -a path/to/test.py``. - -To build the docs locally, execute -``sphinx-build -W -b html -d tmp/doctrees . -d tmp/html`` in the -``docs`` directory. This is specified by the ``[testenv:docs]`` flag in -``tox.ini``. - -Pull Requests -------------- - -To contribute, please submit a pull-request to the `Github -repository `__. - -Before submitting the request, please ensure that all tests pass by -running ``tox`` in the main directory. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9869c8e6..00000000 --- a/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM python:3.7-slim-buster - -MAINTAINER Strateos - -# Default userid=1000 as that is the first non-root userid on linux -ARG NB_UID=1000 -ARG NB_USER=txpy - -# Dependencies for scientific libraries -RUN apt-get update --fix-missing && \ - apt-get install -y \ - pkg-config \ - libjpeg-dev \ - zlib1g-dev \ - libblas-dev \ - liblapack-dev \ - gfortran \ - wget \ - git \ - && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - - -# Change default install directory of pip and eggs -RUN mkdir /pip_cache && \ - mkdir /python_eggs -ENV XDG_CONFIG_HOME /pip_cache -ENV PYTHON_EGG_CACHE /python_eggs - -# Install Jupyter, nbgitpuller for separate notebook/environment -RUN pip install --no-cache-dir notebook==5.* && \ - pip install nbgitpuller==1.* - -# Install TxPy -RUN pip install 'transcriptic[jupyter, analysis]' - -# Add user txpy with specified uid -RUN useradd -u $NB_UID -m -s /bin/bash $NB_USER -ENV HOME /home/$NB_USER -WORKDIR /home/$NB_USER - -RUN chown -R $NB_USER /home/$NB_USER -USER $NB_USER - -ENTRYPOINT [] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 93e05cb7..00000000 --- a/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2020, Strateos Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of Transcriptic nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL TRANSCRIPTIC BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e117cd98..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-include transcriptic/templates * diff --git a/README.md b/README.md new file mode 100644 index 00000000..7a0eba97 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Notebooks + +This branch only contains sample notebooks. Please refer to the `master` branch if you're interested in the base library. diff --git a/README.rst b/README.rst deleted file mode 100644 index e66c5e7b..00000000 --- a/README.rst +++ /dev/null @@ -1,152 +0,0 @@ -Transcriptic Python Library -=========================== - -|PyPI Version| |Build Status| |Documentation| |Code Coverage| |Downloads| |Binder| - -The Transcriptic Python Library (TxPy) provides a Python interface for -managing Transcriptic organizations, projects, runs, datasets and more. -One can either interface with our library through the bundled command -line interface (CLI) or through a Jupyter notebook using a Python -client. - -We recommend using the Jupyter interface as it provides a nice rendering -and presentation of the objects, as well as provide additional analysis -and properties functions specific to the Transcriptic objects. - -Transcriptic is the robotic cloud laboratory for the life sciences. -https://www.transcriptic.com - -Setup ------ - -Organization -~~~~~~~~~~~~ - -TxPy is separated into three main components: 1) Core. The core modules -provide a barebones client for making calls to the Transcriptic webapp -to create and obtain data. This can be done via the ``api`` object or -via the command-line using the CLI. 2) Jupyter. This module provides a -Jupyter-centric means for interacting with objects returned from the -Transcriptic webapp such as Run, Project and Dataset. 3) Analysis. This -module provides some basic analysis wrappers around datasets returned -from the webapp using standard Python scientific libraries. - -Installation -~~~~~~~~~~~~ - -For a barebones CLI install, you’ll do: - -.. code-block:: bash - - pip install transcriptic - -We recommend installing the ``jupyter`` module for Jupyter-centric -navigation: - -.. code-block:: bash - - pip install transcriptic[jupyter] - -Lastly, we recommend installing the ``analysis`` module for a -full-fledged experience: - -.. code-block:: bash - - pip install transcriptic[analysis] - -Alternatively, if you’re interested in contributing or living at the -edge: - -.. code-block:: bash - - git clone https://github.com/strateos/transcriptic.git - cd transcriptic - pip install .[jupyter,analysis] - -to upgrade to the latest version using pip or check whether you’re -already up to date: - -.. code-block:: bash - - pip install transcriptic --upgrade - -Then, login to your Transcriptic account: - -.. code-block:: bash - - $ transcriptic login - Email: me@example.com - Password: - Logged in as me@example.com (example-lab) - -Tab Completion -~~~~~~~~~~~~~~ - -To enable auto-completion for the Transcriptic CLI, you’ll need to -download an appropriate auto-complete file and add it your shell -configuration. - -Here’s an example script for installing it on a bash shell in your -``~/.config`` directory. - -.. code-block:: bash - - export INSTALL_DIR=~/.config && mkdir -p $INSTALL_DIR - curl -L https://raw.githubusercontent.com/strateos/transcriptic/master/autocomplete/bash.sh > $INSTALL_DIR/tx_complete.sh && chmod +x $INSTALL_DIR/tx_complete.sh - echo ". $INSTALL_DIR/tx_complete.sh" >> ~/.bash_profile - -- Ubuntu and Fedora note: Modify your ``~/.bashrc`` instead of - ``~/.bash_profile`` -- Zsh note: Use ``autocomplete/zsh.sh`` instead of ``bash.sh``. Modify - your ``~/.zshrc`` instead of ``~/.bash_profile`` -- Fish note: Use ``autocomplete/fish.sh`` instead of ``bash.sh``. - Change ``$INSTALL_DIR`` to ``~/.config/fish/completions`` and rename - ``tx-complete.sh`` to ``tx-complete.fish``. Skip the last step. - -Documentation -------------- - -CLI -~~~ - -See the `Transcriptic Developer -Documentation `__ -for detailed information about how to use this package, including -learning about how to package protocols and build releases. - -Jupyter -~~~~~~~ - -Click on the |Binder| icon to open an interactive notebook environment -for using the library. - -Developer -~~~~~~~~~ - -View `Developer Specific -Documentation `__ - -Permissions ------------ - -Note that direct analysis and submission of Autoprotocol is currently -restricted. Please contact sales@strateos.com if you would like to do -so. - -Contributing ------------- - -Read `Contributing `__ for more information on contributing to TxPy. - -.. |PyPI Version| image:: https://img.shields.io/pypi/v/transcriptic.svg?maxAge=86400 - :target: https://pypi.python.org/pypi/transcriptic -.. |Build Status| image:: https://github.com/strateos/transcriptic/workflows/CI/badge.svg?branch=master - :target: https://github.com/strateos/transcriptic/actions?query=workflow%3ACI+branch%3Amaster -.. |Documentation| image:: https://readthedocs.org/projects/transcriptic/badge/?version=latest - :target: http://transcriptic.readthedocs.io/en/latest/?badge=latest -.. |Code Coverage| image:: https://codecov.io/gh/strateos/transcriptic/branch/master/graph/badge.svg - :target: https://codecov.io/gh/strateos/transcriptic -.. |Downloads| image:: https://img.shields.io/pypi/dm/transcriptic?logo=pypi - :target: https://transcriptic.readthedocs.io/en/latest -.. |Binder| image:: https://mybinder.org/badge_logo.svg - :target: https://mybinder.org/v2/gh/strateos/transcriptic/master?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fopen-strateos%252Ftxpy_jupyter_notebooks%26urlpath%3Dtree%252Ftxpy_jupyter_notebooks%252Findex.ipynb%26branch%3Dmain diff --git a/analysis.ipynb b/analysis.ipynb new file mode 100644 index 00000000..4da8ef9c --- /dev/null +++ b/analysis.ipynb @@ -0,0 +1,1080 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initializing a Connection\n", + "\n", + "We'll be using a MockConnection object and some sampledata for this example. Please feel free to follow along with your local credentials and switching the ids with the relevant object-ids." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic import connect\n", + "\n", + "api = connect()\n", + "\n", + "# If you receive an `Unable to find .transcriptic` file error, please try\n", + "# `transcriptic login` into the commandline." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the rest of the demo, we'll be using the sampledata. If using your credentials, please do not execute the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic import connect\n", + "\n", + "api = connect(mocked=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Spectrophotometry Object" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.analysis.spectrophotometry import *\n", + "# Use sample absorbance dataset\n", + "from transcriptic.sampledata.dataset import load_sample_absorbance_dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
A1A2A3B1B2B3C1C2C3
00.050.040.061.211.131.322.222.152.37
\n", + "
" + ], + "text/plain": [ + " A1 A2 A3 B1 B2 B3 C1 C2 C3\n", + "0 0.05 0.04 0.06 1.21 1.13 1.32 2.22 2.15 2.37" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_dataset = load_sample_absorbance_dataset()\n", + "sample_dataset.data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we have an absorbance dataset, with 9 readings. \n", + "Assuming we have additional intent information, we can furnish that via the construction of the Absorbance object.\n", + "\n", + "Here, we break it further down into\n", + "- A1 to A3 wells containing Control readings\n", + "- B1 to B3 wells containing a Sample\n", + "- C1 to C3 wells containing another Sample" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "Absorbance?" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "absorbance_obj = Absorbance(\n", + " sample_dataset,\n", + " [\"control\", \"sample1\", \"sample2\"],\n", + " [[0, 1, 2], [12, 13, 14], [24, 25, 26]]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
controlsample1sample2
00.051.212.22
10.041.132.15
20.061.322.37
\n", + "
" + ], + "text/plain": [ + " control sample1 sample2\n", + "0 0.05 1.21 2.22\n", + "1 0.04 1.13 2.15\n", + "2 0.06 1.32 2.37" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# this returns a pandas df for easy manipulation\n", + "absorbance_obj.df" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1.21\n", + "1 1.13\n", + "2 1.32\n", + "Name: sample1, dtype: float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# For example, we can index by column\n", + "absorbance_obj.df[\"sample1\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "control 0.05\n", + "sample1 1.21\n", + "sample2 2.22\n", + "Name: 0, dtype: float64" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Or by row\n", + "absorbance_obj.df.loc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "control 0.002000\n", + "sample1 0.000782\n", + "sample2 0.000500\n", + "dtype: float64" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# there are simple common statistics\n", + "absorbance_obj.cv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, matplotlib is used as a backend." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# a simple plot function for quick visualization defaults to a matplotlib box plot with std. dev.\n", + "absorbance_obj.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# different plot-types are also natively supported, with argument passing supported\n", + "absorbance_obj.plot(plot_type=\"bar\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice experimental parameters such as the wavelength used (600nm) and the operation (absorbance) were automatically inferred from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " R^2: [1.]\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# A beer's law function for determining concentration for fits\n", + "absorbance_obj.beers_law(conc_list=[0, 1, 2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the R^2 fit here is 1 since we only have 3 points. Feel free to try this with a dataset with more concentrations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Kinetics Object" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.analysis.kinetics import *\n", + "# Use sample absorbance dataset\n", + "from transcriptic.sampledata.dataset import load_sample_kinetics_datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['absorbance', 'absorbance', 'absorbance', 'absorbance', 'absorbance']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
A1A2A3B1B2B3C1C2C3
00.050.040.061.211.131.322.222.152.37
\n", + "
" + ], + "text/plain": [ + " A1 A2 A3 B1 B2 B3 C1 C2 C3\n", + "0 0.05 0.04 0.06 1.21 1.13 1.32 2.22 2.15 2.37" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kinetics_datasets = load_sample_kinetics_datasets()\n", + "print([dataset.operation for dataset in kinetics_datasets])\n", + "kinetics_datasets[0].data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we have a series of kinematics datasets of OD600 reads taken every hour. The format of this data is similar to the sample absorbance dataset we were using above." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "Spectrophotometry?" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "growth_curve = Spectrophotometry(kinetics_datasets)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'absorbance'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "growth_curve.operation" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
2020-06-01 15:40:55.044000-07:002020-06-01 16:40:55.044000-07:002020-06-01 17:40:55.044000-07:002020-06-01 18:40:55.044000-07:002020-06-01 19:40:55.044000-07:00
A10.050.0450.0440.0520.038
A20.040.0340.0490.0540.034
A30.060.0580.0680.0510.059
B11.211.3971.5632.3042.775
B21.131.4811.8722.4053.041
B31.321.3421.7702.2873.111
C12.222.4102.5742.6792.893
C22.152.2302.4342.8613.052
C32.372.3812.5742.7462.986
\n", + "
" + ], + "text/plain": [ + " 2020-06-01 15:40:55.044000-07:00 2020-06-01 16:40:55.044000-07:00 \\\n", + "A1 0.05 0.045 \n", + "A2 0.04 0.034 \n", + "A3 0.06 0.058 \n", + "B1 1.21 1.397 \n", + "B2 1.13 1.481 \n", + "B3 1.32 1.342 \n", + "C1 2.22 2.410 \n", + "C2 2.15 2.230 \n", + "C3 2.37 2.381 \n", + "\n", + " 2020-06-01 17:40:55.044000-07:00 2020-06-01 18:40:55.044000-07:00 \\\n", + "A1 0.044 0.052 \n", + "A2 0.049 0.054 \n", + "A3 0.068 0.051 \n", + "B1 1.563 2.304 \n", + "B2 1.872 2.405 \n", + "B3 1.770 2.287 \n", + "C1 2.574 2.679 \n", + "C2 2.434 2.861 \n", + "C3 2.574 2.746 \n", + "\n", + " 2020-06-01 19:40:55.044000-07:00 \n", + "A1 0.038 \n", + "A2 0.034 \n", + "A3 0.059 \n", + "B1 2.775 \n", + "B2 3.041 \n", + "B3 3.111 \n", + "C1 2.893 \n", + "C2 3.052 \n", + "C3 2.986 " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "growth_curve.readings" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "growth_curve.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "growth_curve.plot?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also pass in various options. For example, here we group the samples by the row readings and change the title and ylabel." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "growth_curve.plot(groupby=\"row\", title=\"Growth Curve of Samples\", ylabel=\"OD600 readings\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/autocomplete/bash.sh b/autocomplete/bash.sh deleted file mode 100644 index 7c26625a..00000000 --- a/autocomplete/bash.sh +++ /dev/null @@ -1,8 +0,0 @@ -_transcriptic_completion() { - COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ - COMP_CWORD=$COMP_CWORD \ - _TRANSCRIPTIC_COMPLETE=complete $1 ) ) - return 0 -} - -complete -F _transcriptic_completion -o default transcriptic; diff --git a/autocomplete/fish.sh b/autocomplete/fish.sh deleted file mode 100644 index c8880ae6..00000000 --- a/autocomplete/fish.sh +++ /dev/null @@ -1 +0,0 @@ -complete --no-files --command transcriptic --arguments "(env _TRANSCRIPTIC_COMPLETE=complete_fish COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) transcriptic)"; diff --git a/autocomplete/zsh.sh b/autocomplete/zsh.sh deleted file mode 100644 index f92e6442..00000000 --- a/autocomplete/zsh.sh +++ /dev/null @@ -1,28 +0,0 @@ -_transcriptic_completion() { - local -a completions - local -a completions_with_descriptions - local -a response - response=("${(@f)$( env COMP_WORDS="${words[*]}" \ - COMP_CWORD=$((CURRENT-1)) \ - _TRANSCRIPTIC_COMPLETE="complete_zsh" \ - transcriptic )}") - - for key descr in ${(kv)response}; do - if [[ "$descr" == "_" ]]; then - completions+=("$key") - else - completions_with_descriptions+=("$key":"$descr") - fi - done - - if [ -n "$completions_with_descriptions" ]; then - _describe -V unsorted completions_with_descriptions -U -Q - fi - - if [ -n "$completions" ]; then - compadd -U -V unsorted -Q -a completions - fi - compstate[insert]="automenu" -} - -compdef _transcriptic_completion transcriptic; diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 801bbcdf..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,192 +0,0 @@ -# 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 http://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 coverage 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 " applehelp to make an Apple Help Book" - @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)" - @echo " coverage to run coverage check of 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/transcriptic.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/transcriptic.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/transcriptic" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/transcriptic" - @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." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.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/favicon.ico b/docs/_static/favicon.ico deleted file mode 100644 index 6eef2927..00000000 Binary files a/docs/_static/favicon.ico and /dev/null differ diff --git a/docs/_static/transcriptic.css b/docs/_static/transcriptic.css deleted file mode 100644 index 5b3dd970..00000000 --- a/docs/_static/transcriptic.css +++ /dev/null @@ -1,29 +0,0 @@ -.wy-side-nav-search{ - background-color: none; - background-image: radial-gradient(98% 100%, #242e36 0%,#0e202e 100%); -} -.wy-side-nav-search>a img.logo, .wy-side-nav-search .wy-dropdown>a img.logo{ - max-width: 80%; -} -.rst-content dl:not(.docutils) dt { - border-top: solid 3px #242e36; - color: #2d5b8e; -} -.rst-content .viewcode-link, .rst-content .viewcode-back{ - color: #242e36; -} -dd>p:first-child{ - font-weight: 600; -} -.section a, -.section a:visited, -.toctree-wrapper a, -.toctree-wrapper a:visited, -.wy-breadcrumbs a, -.wy-breadcrumbs a:visited{ - color: #2d5b8e; -} -.section a:hover, -.toctree-wrapper a:hover { - color: #ffc40c; -} diff --git a/docs/_static/transcriptic.svg b/docs/_static/transcriptic.svg deleted file mode 100644 index 9b15d8ed..00000000 --- a/docs/_static/transcriptic.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - full-logo - Created with Sketch Beta. - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/analysis.rst b/docs/analysis.rst deleted file mode 100644 index 17e38112..00000000 --- a/docs/analysis.rst +++ /dev/null @@ -1,26 +0,0 @@ -transcriptic.analysis ---------------------- - -Kinetics -~~~~~~~~ - -.. automodule:: transcriptic.analysis.kinetics - :members: - :undoc-members: - :show-inheritance: - -Imaging -~~~~~~~ - -.. automodule:: transcriptic.analysis.imaging - :members: - :undoc-members: - :show-inheritance: - -Spectrophotometry -~~~~~~~~~~~~~~~~~ - -.. automodule:: transcriptic.analysis.spectrophotometry - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 565b0521..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index c6427678..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,352 +0,0 @@ -# -*- coding: utf-8 -*- -# -# transcriptic documentation build configuration file, created by -# sphinx-quickstart on Tue Oct 27 15:01:12 2015. -# -# 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 os -import sys - -from mock import Mock as MagicMock - - -class Mock(MagicMock): - @classmethod - def __getattr__(cls, name): - return Mock() - - -# All imported modules containing C components must be mocked (added to the -# MOCK_MODULES list below) -MOCK_MODULES = [ - "numpy", - "matplotlib", - "matplotlib.pyplot", - "sklearn.grid_search", - "sklearn.externals", - "plotly", - "plotly.graph_objs", - "matplotlib.gridspec", - "scikit-learn", - "pandas", - "plotly.plotly", - "plotly.tools", - "Ipython.display", -] -sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) - - -# 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("../transcriptic")) -sys.path.insert(0, os.path.abspath("../transcriptic/analysis")) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "2.4" - -# 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.viewcode", - "sphinx.ext.napoleon", -] - -# Napoleon settings -napoleon_google_docstring = False -napoleon_numpy_docstring = True -napoleon_include_private_with_doc = False -napoleon_include_special_with_doc = True -napoleon_use_admonition_for_examples = False -napoleon_use_admonition_for_notes = True -napoleon_use_admonition_for_references = False -napoleon_use_ivar = False -napoleon_use_param = True -napoleon_use_rtype = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = u"TxPy" -copyright = u"2020, Max Hodak, Tali Herzka, Jeremy Apthorp, Yang Choo, Peter Lee" - - -exec(open("../transcriptic/version.py").read()) -# 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 = __version__ # pylint: disable=undefined-variable -# The full version, including alpha/beta/rc tags. -release = __version__ # pylint: disable=undefined-variable - -# 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"] - -# 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 = "sphinx" - -# 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 -todo_include_todos = 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 = "default" - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = "TxPy Docs" - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "_static/transcriptic.svg" - -# 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 = "_static/favicon.ico" - -# 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 = [] - -# 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 = {} - -# 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 = "TxPydoc" - - -# -- 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", - "transcriptic.tex", - u"transcriptic Documentation", - u"Max Hodak, Tali Herzka, Jeremy Apthorp, Yang Choo, Peter Lee", - "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", - "transcriptic", - u"transcriptic Documentation", - [u"Max Hodak, Tali Herzka, Jeremy Apthorp, Yang Choo, Peter Lee"], - 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", - "transcriptic", - u"transcriptic Documentation", - u"Max Hodak, Tali Herzka, Jeremy Apthorp, Yang Choo, Peter Lee", - "transcriptic", - "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 - - -# Example configuration for intersphinx: refer to the Python standard library. -# intersphinx_mapping = {'http://docs.python.org/': None} - - -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - - extensions.append("sphinx_rtd_theme") - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - - def setup(app): - app.add_stylesheet("transcriptic.css") - - -else: - html_context = { - "css_files": [ - "https://media.readthedocs.org/css/sphinx_rtd_theme.css", - "https://media.readthedocs.org/css/readthedocs-doc-embed.css", - "_static/transcriptic.css", - ] - } - - -# otherwise, readthedocs.org uses their theme by default, so no need to specify it -# 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 diff --git a/docs/config.rst b/docs/config.rst deleted file mode 100644 index 5daec304..00000000 --- a/docs/config.rst +++ /dev/null @@ -1,10 +0,0 @@ -transcriptic.config ---------------------- - -Connection -~~~~~~~~~~ - -.. autoclass:: transcriptic.config.Connection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053e..00000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index d733958e..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -========================================================= -`Transcriptic` Python Client Library (TxPy) documentation -========================================================= - -.. toctree:: - :hidden: - - Objects - Analysis - Config - Contributing - Changelog - -.. include:: ../README.rst - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/objects.rst b/docs/objects.rst deleted file mode 100644 index 31beca01..00000000 --- a/docs/objects.rst +++ /dev/null @@ -1,49 +0,0 @@ -transcriptic.jupyter ----------------------------- - -Project -~~~~~~~~~~~~~~~~ - -.. autoclass:: transcriptic.jupyter.Project - - -Project.runs() -^^^^^^^^^^^^^^ -.. automethod:: transcriptic.jupyter.Project.runs - -Project.submit() -^^^^^^^^^^^^^^^^ -.. automethod:: transcriptic.jupyter.Project.submit - - -Run -~~~~~~~~~~~~ - -.. autoclass:: transcriptic.jupyter.Run - :members: - :undoc-members: - :show-inheritance: - -Dataset -~~~~~~~~~~~~~~~~ - -.. autoclass:: transcriptic.jupyter.Dataset - :members: - :undoc-members: - :show-inheritance: - -Instruction -~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: transcriptic.jupyter.Instruction - :members: - :undoc-members: - :show-inheritance: - -Container -~~~~~~~~~~~~~~~~ - -.. autoclass:: transcriptic.jupyter.Container - :members: - :undoc-members: - :show-inheritance: diff --git a/objects.ipynb b/objects.ipynb new file mode 100644 index 00000000..3c31bebe --- /dev/null +++ b/objects.ipynb @@ -0,0 +1,1162 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initializing a Connection\n", + "\n", + "We'll be using a MockConnection object and some sampledata for this example. Please feel free to follow along with your local credentials and switching the ids with the relevant object-ids." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic import connect\n", + "\n", + "api = connect()\n", + "\n", + "# If you receive an `Unable to find .transcriptic` file error, please try\n", + "# `transcriptic login` into the commandline." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the rest of the demo, we'll be using the sampledata. If using your credentials, please do not execute the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic import connect\n", + "\n", + "api = connect(mocked=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'sample-org'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Initialized organization\n", + "api.organization_id" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': 'p123',\n", + " 'name': 'sample project',\n", + " 'created_at': '2020-10-01T00:00:00.000-07:00',\n", + " 'updated_at': '2020-10-01T00:59:59.100-07:00'}]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## List projects\n", + "api.projects()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Project" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.sampledata import load_sample_project" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "my_project = load_sample_project()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "## Bring up documentation\n", + "my_project?" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'sample project'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_project.name" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idName
0r123Sample Run
\n", + "
" + ], + "text/plain": [ + " id Name\n", + "0 r123 Sample Run" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## View runs in the project as a pandas DataFrame\n", + "my_project.runs()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Typically, you would initialize a project directly with the given Project-id." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.jupyter import Project" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "my_project = Project(\"p123\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.sampledata import load_sample_run" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "my_run = load_sample_run()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "## Bring up documentation\n", + "my_run?" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'r123'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_run.id" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'p123'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_run.project_id" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Instruction Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameIdStartedCompletedInstructions
0acoustic_transferi1232020-06-01T15:39:50.873-07:002020-06-01T15:39:55.049-07:00<transcriptic.jupyter.objects.Instruction obje...
1absorbancei1242020-06-01T15:40:50.873-07:002020-06-01T15:40:55.049-07:00<transcriptic.jupyter.objects.Instruction obje...
\n", + "
" + ], + "text/plain": [ + " Name Id Started \\\n", + "0 acoustic_transfer i123 2020-06-01T15:39:50.873-07:00 \n", + "1 absorbance i124 2020-06-01T15:40:50.873-07:00 \n", + "\n", + " Completed \\\n", + "0 2020-06-01T15:39:55.049-07:00 \n", + "1 2020-06-01T15:40:55.049-07:00 \n", + "\n", + " Instructions \n", + "0 \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameWarpIdCompletedStartedParams
0AcousticLiquidHandler.Transferw1232020-06-01T15:39:54.989-07:002020-06-01T15:39:50.791-07:00{'sourceContainer': {'id': 'ct123', 'cType': '...
\n", + "" + ], + "text/plain": [ + " Name WarpId Completed \\\n", + "0 AcousticLiquidHandler.Transfer w123 2020-06-01T15:39:54.989-07:00 \n", + "\n", + " Started \\\n", + "0 2020-06-01T15:39:50.791-07:00 \n", + "\n", + " Params \n", + "0 {'sourceContainer': {'id': 'ct123', 'cType': '... " + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Examine warp-specifics\n", + "my_inst.warps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Container Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameContainerIdTypeStatusStorage ConditionContainers
0Echo Source Platect123384-echoavailablecold_4Container(Echo Source Plate)
1VbottomPlatect12496-well-v-bottomavailablecold_4Container(VbottomPlate)
\n", + "
" + ], + "text/plain": [ + " Name ContainerId Type Status \\\n", + "0 Echo Source Plate ct123 384-echo available \n", + "1 VbottomPlate ct124 96-well-v-bottom available \n", + "\n", + " Storage Condition Containers \n", + "0 cold_4 Container(Echo Source Plate) \n", + "1 cold_4 Container(VbottomPlate) " + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_run.containers" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "# Access column of Containers\n", + "my_containers = my_run.containers.Containers\n", + "cont_123 = my_containers.loc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Container(Echo Source Plate)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cont_123" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameIdVolume
Well Index
0Noneaq1egnpw5q5ythw99:microliter
\n", + "
" + ], + "text/plain": [ + " Name Id Volume\n", + "Well Index \n", + "0 None aq1egnpw5q5ythw 99:microliter" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Examine aliqutos of this container\n", + "cont_123.aliquots" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ContainerType(name='384-well Echo plate', is_tube=False, well_count=384, well_depth_mm=None, well_volume_ul=Unit(65.0, 'microliter'), well_coating=None, sterile=None, cover_types=['universal'], seal_types=['foil', 'ultra-clear'], capabilities=['liquid_handle', 'seal', 'spin', 'incubate', 'dispense', 'cover'], shortname='384-echo', col_count=24, dead_volume_ul=Unit(15, 'microliter'), safe_min_volume_ul=Unit(15, 'microliter'), true_max_vol_ul=Unit(135, 'microliter'), vendor='Labcyte', cat_no='PP-0200', prioritize_seal_or_cover='seal')" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View the autoprotocol container-type\n", + "cont_123.container_type" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cold_4'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View storage status\n", + "cont_123.storage" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Dataset Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attempting to fetch 1 datasets...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameDataTypeOperationAnalysisToolDatasets
0OD600platereaderabsorbanceNone<transcriptic.jupyter.objects.Dataset object a...
\n", + "
" + ], + "text/plain": [ + " Name DataType Operation AnalysisTool \\\n", + "0 OD600 platereader absorbance None \n", + "\n", + " Datasets \n", + "0 \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
A1A2A3B1B2B3C1C2C3
00.050.040.061.211.131.322.222.152.37
\n", + "" + ], + "text/plain": [ + " A1 A2 A3 B1 B2 B3 C1 C2 C3\n", + "0 0.05 0.04 0.06 1.21 1.13 1.32 2.22 2.15 2.37" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_run.Datasets.loc[0].data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some basic analysis is supported on datasets. See the Analysis notebook for more examples." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.analysis.spectrophotometry import Absorbance" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "Absorbance?" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs_dataset = Absorbance(\n", + " my_run.Datasets.loc[0],\n", + " [\"control\", \"sample1\", \"sample2\"],\n", + " group_wells = [[0,1,2], [12, 13, 14], [24, 25, 26]]\n", + ")\n", + "abs_dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
controlsample1sample2
00.051.212.22
10.041.132.15
20.061.322.37
\n", + "
" + ], + "text/plain": [ + " control sample1 sample2\n", + "0 0.05 1.21 2.22\n", + "1 0.04 1.13 2.15\n", + "2 0.06 1.32 2.37" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs_dataset.df" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "abs_dataset.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Typically, you would initialize a project directly with the given Run-id." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "from transcriptic.jupyter import Run" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "# You can select your desired run-id from the project runs\n", + "my_run_id = my_project.runs().loc[0].id" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "my_run = Run(my_run_id)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index e42ffff0..00000000 --- a/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -minversion = 2.3.3 -python_files = "test/*_test.py" "test/test_*.py" -python_classes = Test -norecursedirs = ".tox" "build" "docs" ".eggs" -addopts = --doctest-glob='*_test.py' diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b7e47898..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliases] -test=pytest diff --git a/setup.py b/setup.py deleted file mode 100644 index ef3ecbc8..00000000 --- a/setup.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -import sys - -from setuptools import setup -from setuptools.command.test import test as TestCommand - - -# Load version -exec(open("transcriptic/version.py").read()) - - -# Test Runner (reference: https://docs.pytest.org/en/latest/goodpractices.html) -class PyTest(TestCommand): - user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = "--cov=transcriptic --cov-report=term" - - def run_tests(self): - import shlex - - # import here, cause outside the eggs aren't loaded - import pytest - - errno = pytest.main(shlex.split(self.pytest_args)) - sys.exit(errno) - - -# Test and Documentation dependencies -test_deps = [ - "coverage>=4.5, <5", - "jsonschema>=2.6, <3", - "mock>=3, <4", - "pre-commit>=2.4, <3", - "pylint==2.5.2", # should be consistent with .pre-commit-config.yaml - "pytest>=5.4, <6", - "pytest-cov>=2, <3", - "tox>=3.15, <4", - "responses>=0.13.4", -] - -doc_deps = [ - "Sphinx>=2.4, <3", - "mock>=3, <4", - "releases>=1.6.3, <2", - "sphinx_rtd_theme>=0.4.3, <1", -] - -# Extra module dependencies -jupyter_deps = ["pandas>=1, <2", "responses>=0.12.0,<1", "jupyter>=1.0.0, <2"] - -analysis_deps = [ - "autoprotocol>=7.6.1,<8", - "matplotlib>=3,<4", - # Version 1.21 upwards only support Python >= 3.7 - "numpy>=1.14,<=1.20.3", - "pandas>=1,<2", - "pillow>=8,<9", - "plotly>=1.13,<2", -] - - -setup( - name="transcriptic", - description="Transcriptic CLI & Python Client Library", - url="https://github.com/transcriptic/transcriptic", - version=__version__, # pylint: disable=undefined-variable - packages=["transcriptic", "transcriptic.jupyter", "transcriptic.analysis"], - include_package_data=True, - tests_require=test_deps, - python_requires=">=3.6", - install_requires=[ - "Click>=7.0,<8", - "httpsig==1.3.0", - "requests>2.21.0,<3", - "pycryptodome==3.9.6", - "python-magic>=0.4,<1", - "Jinja2>=3.0,<4", - "responses>=0.13.4", - ], - extras_require={ - "jupyter": jupyter_deps, - "analysis": analysis_deps, - "docs": doc_deps, - "test": test_deps, - }, - cmdclass={"pytest": PyTest}, - entry_points=""" - [console_scripts] - transcriptic=transcriptic.cli:cli - """, - license="BSD", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Operating System :: Unix", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Software Development", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], -) diff --git a/test/AP2En_test.py b/test/AP2En_test.py deleted file mode 100644 index 8df2e697..00000000 --- a/test/AP2En_test.py +++ /dev/null @@ -1,470 +0,0 @@ -import json -import unittest - -import pytest - -from transcriptic import english - - -class AP2EnTestCase(unittest.TestCase): - def test_web_example(self): - - with open("./test/resources/web_example.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - forest, [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]] - ) - - def test_med_json_job_tree(self): - - pjsonString = """{ - "refs": { - "3-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "5-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "4-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "1-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "0-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "6-384-pcr": { - "new": "384-pcr", - "discard": true - }, - "2-384-pcr": { - "new": "384-pcr", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "3-384-pcr/383", - "from": "3-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "3-384-pcr/382", - "from": "3-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "5-384-pcr/383", - "from": "5-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "5-384-pcr/382", - "from": "5-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "4-384-pcr/383", - "from": "4-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "4-384-pcr/382", - "from": "4-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "1-384-pcr/383", - "from": "1-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "1-384-pcr/382", - "from": "1-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "0-384-pcr/383", - "from": "0-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "0-384-pcr/382", - "from": "0-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "6-384-pcr/383", - "from": "6-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "6-384-pcr/382", - "from": "6-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "2-384-pcr/383", - "from": "2-384-pcr/0" - } - ], - "x_tip_type": "filtered50" - }, - { - "transfer": [ - { - "volume": "10.0:microliter", - "to": "2-384-pcr/382", - "from": "2-384-pcr/1" - } - ], - "x_tip_type": "filtered50" - } - ], - "op": "pipette" - } - ] - } - """ - pjson = json.loads(pjsonString) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - forest, - [[1, [2]], [3, [4]], [5, [6]], [7, [8]], [9, [10]], [11, [12]], [13, [14]]], - ) - - def test_measure_suite(self): - """ - Desired Output: - 1. Measure concentration of 2.0 microliters DNA source aliquots - 2. Measure mass of test_plate2 - 3. Measure volume of 12 wells from test_plate - 4. Measure volume of 8 wells from test_plate2 - """ - - with open("./test/resources/measure_suite.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Measure concentration of 2.0 microliters DNA source aliquots of test_plate2", - "Measure mass of test_plate2", - "Measure volume of 12 wells from test_plate", - "Measure volume of 8 wells from test_plate2", - ], - ) - self.assertEqual(forest, [[1, [2, [4]]], [3]]) - - def test_mag_incubate(self): - """ - Desired Output: - 1. Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0 - 2. Distribute from test/1 into wells test/7, test/8, test/9 - 3. Distribute from test/2 into wells test/10 - 4. Distribute from test/0 into wells test/1 - 5. Magnetically incubate pcr_0 for 30.0 minutes with a tip position of 1.5 - """ - - with open("./test/resources/mag_incubate.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0", - "Distribute from test/1 into wells test/7, test/8, test/9", - "Distribute from test/2 into wells test/10", - "Distribute from test/0 into wells test/1", - "Magnetically incubate pcr_0 for 30.0 minutes with a tip position of 1.5", - ], - ) - self.assertEqual(forest, [[1, [5]], [2, [4]], [3]]) - - def test_mag_mix(self): - """ - Desired Output: - 1. Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0 - 2. Distribute from test/1 into wells test/7, test/8, test/9 - 3. Distribute from test/2 into wells test/10 - 4. Distribute from test/0 into wells test/1 - 5. Magnetically mix pcr_0 beads for 30.0 seconds at an amplitude of 0 - """ - - with open("./test/resources/mag_mix.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0", - "Distribute from test/1 into wells test/7, test/8, test/9", - "Distribute from test/2 into wells test/10", - "Distribute from test/0 into wells test/1", - "Magnetically mix pcr_0 beads for 30.0 seconds at an amplitude of 0", - ], - ) - self.assertEqual(forest, [[1, [5]], [2, [4]], [3]]) - - def test_mag_dry(self): - """ - Desired Output: - 1. Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0 - 2. Distribute from test/1 into wells test/7, test/8, test/9 - 3. Distribute from test/2 into wells test/10 - 4. Distribute from test/0 into wells test/1 - 5. Magnetically dry pcr_0 for 30.0 minutes - """ - - with open("./test/resources/mag_dry.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0", - "Distribute from test/1 into wells test/7, test/8, test/9", - "Distribute from test/2 into wells test/10", - "Distribute from test/0 into wells test/1", - "Magnetically dry pcr_0 for 30.0 minutes", - ], - ) - self.assertEqual(forest, [[1, [5]], [2, [4]], [3]]) - - def test_mag_collect(self): - """ - Desired Output: - 1. Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0 - 2. Distribute from test/1 into wells test/7, test/8, test/9 - 3. Distribute from test/2 into wells test/10 - 4. Distribute from test/0 into wells test/1 - 5. Magnetically collect pcr_0 beads for 5 cycles with a pause duration of 30.0 seconds - """ - - with open("./test/resources/mag_collect.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Magnetically release pcr_0 beads for 30.0 seconds at an amplitude of 0", - "Distribute from test/1 into wells test/7, test/8, test/9", - "Distribute from test/2 into wells test/10", - "Distribute from test/0 into wells test/1", - "Magnetically collect pcr_0 beads for 5 cycles with a pause duration of 30.0 seconds", - ], - ) - self.assertEqual(forest, [[1, [5]], [2, [4]], [3]]) - - def test_purify(self): - """ - Desired Output: - 1. Perform gel purification on the 0.8% agarose gel with band range(s) 0-10 - 2. Perform gel purification on the 0.8% agarose gel with band range(s) 0-10 - 3. Perform gel purification on the 0.8% agarose gel with band range(s) 0-10 - """ - - with open("./test/resources/purify.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Perform gel purification on the 0.8% agarose gel with band range(s) 0-10", - "Perform gel purification on the 0.8% agarose gel with band range(s) 0-10", - "Perform gel purification on the 0.8% agarose gel with band range(s) 0-10", - ], - ) - self.assertEqual(forest, [[1, [2, [3]]]]) - - def test_dispense_suite(self): - """ - Desired Output: - 1. Dispense 100 microliters of water to the full plate of sample_plate5 - 2. Dispense corresponding amounts of water to 12 column(s) of sample_plate5 - 3. Dispense 50 microliters of reagent with resource ID rs17gmh5wafm5p to the full plate of sample_plate5 - """ - - with open("./test/resources/dispense.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Dispense 100 microliters of water to the full plate of sample_plate5", - "Dispense corresponding amounts of water to 12 column(s) of sample_plate5", - "Dispense 50 microliters of resource with resource ID rs17gmh5wafm5p to the full plate of sample_plate5", - ], - ) - self.assertEqual(forest, [[1, [2, [3]]]]) - - def test_illumina(self): - """ - Desired Output: - 1. Illumina sequence wells test_plate6/0, test_plate6/1 with library size 34 - """ - - with open("./test/resources/illumina.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Illumina sequence wells test_plate6/0, test_plate6/1 with library size 34" - ], - ) - self.assertEqual(forest, [[1]]) - - def test_flow(self): - """ - Desired Output: - 1. Perform flow cytometry on well0 with the respective FSC and SSC channel parameters - """ - - with open("./test/resources/flow.json") as data_file: - pjson = json.load(data_file) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - parsed_output = parser_instance.parsed_output - steps = parser_instance.object_list - forest = parser_instance.forest_list - - self.assertEqual( - parsed_output, - [ - "Perform flow cytometry on test_plate/0 with the respective FSC and SSC channel parameters" - ], - ) - self.assertEqual(forest, [[1]]) diff --git a/test/README.md b/test/README.md deleted file mode 100644 index cfca0913..00000000 --- a/test/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Testing - -## Overview - -We use the [pytest](http://pytest.org/latest/getting-started.html) framework for writing tests. A helpful resource for a -list of supported assertions can be found [here](https://pytest.org/latest/assert.html). -We also use [tox](https://tox.readthedocs.org/en/latest/) for automating tests. - -Ensure that all tests pass when you run `tox` in the main folder. -Alternatively, one can run `py.test ` if you are trying to test a specific module. - -## Structure -Helper functions should go into the helpers folder, and autoprotocol json in the autoprotocol folder. -The `conftest.py` file contains any pytest related configurations. - -## Fixtures -[Pytest Fixtures](https://pytest.org/latest/fixture.html) are used through out the package to reduce the need for -duplicated code. This is also used for environment setup/teardown effects. -There are a couple of common fixtures shared throughout the test package. -- `test_api`: This sets up a test Connection and monkeypatches the api `_req_call` function with an offline/mock -equivalent and can be used in conjunction with the `response_db` fixture -- `response_db`, `json_db`: These fixtures are used for holding our MockResponse objects and Json objects respectively. - -## Writing API Tests - -###Background Information -For API tests, we overwrite the base call command with a mock call that draws from the `mockDB` dictionary. -To mock responses, we use the `MockResponse` class, which basically mirrors the `Response` class from the -requests library. The three key fields to mock here are the `status_code`, `json` and `text` fields. -Similar to the `Response` object, the `status_code` corresponds to the HTML response codes (e.g. 404, 201), -the `json` method corresponds to the `json` response which is usually returned when the call is succesful. -Lastly, the `text` field is what's commonly used for displaying logs/error messages. - -### Example -Writing an `api test`: -1. First define a test class if necessary (e.g. `TestAnalyze`). This should hold all tests related to the functionality -you are trying to test. -2. Add module wide fixtures in the `conftest.py` file. For example, the `test_api` fixture is something which will we -will use for most testing and its found there. -3. If there is a common set of setup steps you intend to do for each test function within the class, define an -initialization function and decorate it with the `@pytest.fixture(autouse=True)` decorator. -In this case, we define an `init_db` function which uses both the `json_db` and `response_db` fixtures for -getting/loading a set of json and MockResponses. -4. Write your test function, feeding in all the required fixtures. For example, here I would like to test the default -responses for an `analyze` call. I know I require the `test_api`, `json_db` and `response_db` fixtures, so I list them -as arguments. -5. To test any mocked API calls, I'll need the `mockRoute` function. This mocks any `call`s made to a specific `route` -with a MockResponse object. If `max_calls` is specified, the `response` will be returned `max_calls` number of times. -Note that if the same route is mocked multiple times, responses will join a queue. -If `max_calls` is not specified, the response will be set as the default response of that route. The default response - will then be returned when there are no more elements in the queue. -6. Here, my route is given by `test_api.get_route('analyze_run')` and the call is `post`. So let's mock a route for testing - 404 responses. -`mockRoute(post, test_api.get_route('analyze_run'), response_db["mock404"], max_calls=1)`. -7. To test if a 404 is indeed returned, I use the pytest syntax for checking exceptions. And I call `analyze_run` with -an invalid protocol json - `with pytest.raises(Exception):` - `test_api.analyze_run(json_db['invalidTransfer'])` - -The final code block can be seen in the `TestAnalyze` class, in the `api_test` module diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/analysis/kinetics_test.py b/test/analysis/kinetics_test.py deleted file mode 100644 index 32800f0e..00000000 --- a/test/analysis/kinetics_test.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -from pandas import DatetimeTZDtype -from transcriptic.analysis.kinetics import * -from transcriptic.sampledata.connection import MockConnection -from transcriptic.sampledata.dataset import load_sample_kinetics_datasets - - -class TestKinetics: - @pytest.fixture(scope="class", autouse=True) - def init_mock_connection(self): - yield MockConnection() - - def test_spectrophotometry_object(self): - sample_datasets = load_sample_kinetics_datasets() - - spectrophotometry_obj = Spectrophotometry(sample_datasets) - - assert spectrophotometry_obj.operation == "absorbance" - - readings = spectrophotometry_obj.readings - assert isinstance(readings.columns.dtype, DatetimeTZDtype) - - # there isn't a good way to test this currently, let's ensure the calls work - spectrophotometry_obj.plot() - spectrophotometry_obj.plot( - wells=["A1", "A2", "B1", "B2", "C1", "C2"], - groupby="row", - title="Test title", - xlabel="test xlabel", - ylabel="test ylabel", - ) diff --git a/test/analysis/spectrophotometry_test.py b/test/analysis/spectrophotometry_test.py deleted file mode 100644 index 0cef9a3e..00000000 --- a/test/analysis/spectrophotometry_test.py +++ /dev/null @@ -1,48 +0,0 @@ -from contextlib import contextmanager - -import pandas as pd -import pytest - -from transcriptic.analysis.spectrophotometry import * -from transcriptic.sampledata.connection import MockConnection -from transcriptic.sampledata.dataset import load_sample_absorbance_dataset - - -@contextmanager -def mpl_plot_was_rendered(): - """Helper function to ensure that the tested function generates a matplotlib plot""" - figs_before = plt.gcf().number - yield - figs_after = plt.gcf().number - assert figs_after > figs_before - - -class TestSpectrophotometry: - @pytest.fixture(scope="class", autouse=True) - def init_mock_connection(self): - yield MockConnection() - - def test_absorbance_object(self): - sample_dataset = load_sample_absorbance_dataset() - - abs_dataset = Absorbance( - sample_dataset, - ["control", "sample1", "sample2"], - [[0, 1, 2], [12, 13, 14], [24, 25, 26]], - ) - - assert abs_dataset.op_type == "absorbance" - assert abs_dataset.df.equals( - pd.DataFrame.from_dict( - { - "control": [0.05, 0.04, 0.06], - "sample1": [1.21, 1.13, 1.32], - "sample2": [2.22, 2.15, 2.37], - } - ) - ) - assert len(abs_dataset.cv) == 3 - with mpl_plot_was_rendered(): - abs_dataset.plot() - with mpl_plot_was_rendered(): - abs_dataset.beers_law(conc_list=[0, 1, 2]) diff --git a/test/api_test.py b/test/api_test.py deleted file mode 100644 index fdd583e2..00000000 --- a/test/api_test.py +++ /dev/null @@ -1,74 +0,0 @@ -import pytest - -from transcriptic.config import AnalysisException - -from .helpers.mockAPI import mockRoute - - -class TestAnalyze: - @pytest.fixture(autouse=True) - def init_db(self, json_db, response_db): - # Load all the protocol and response json we may use for this test class - json_db.load("invalidTransfer") - json_db.load("invalidTransfer_response") - json_db.load("singleTransfer") - json_db.load("singleTransfer_response") - - # Load all our mock responses - response_db.load(name="mock404", status_code=404, text="404") - response_db.load(name="mock400", status_code=400, text="400") - response_db.load( - name="mock422", status_code=422, json=json_db["invalidTransfer_response"] - ) - response_db.load( - name="mock200", status_code=200, json=json_db["singleTransfer_response"] - ) - - def testDefaultResponses(self, test_api, json_db, response_db): - # Setup default parameters for route mocking - route = test_api.get_route("analyze_run") - call = "post" - - mockRoute(call, route, response_db["mock404"], max_calls=1) - with pytest.raises(Exception): - test_api.analyze_run(json_db["invalidTransfer"]) - - mockRoute(call, route, response_db["mock400"], max_calls=1) - with pytest.raises(Exception): - test_api.analyze_run(json_db["invalidTransfer"]) - - mockRoute(call, route, response_db["mock422"], max_calls=1) - with pytest.raises(AnalysisException): - test_api.analyze_run(json_db["invalidTransfer"]) - - mockRoute(call, route, response_db["mock200"], max_calls=1) - assert ( - test_api.analyze_run(json_db["singleTransfer"]) - == json_db["singleTransfer_response"] - ) - - def testGetOrganizations(self, test_api, json_db, response_db): - # Setup parameters for route mocking - route = test_api.get_route("get_organizations") - call = "get" - - mockRoute(call, route, response_db["mock404"], max_calls=1) - with pytest.raises(Exception): - test_api.organizations() - - mockRoute(call, route, response_db["mock200"], max_calls=1) - assert test_api.organizations() == json_db["singleTransfer_response"] - - def testGetOrganization(self, test_api, json_db, response_db): - # Setup parameters for route mocking - route = test_api.get_route("get_organization", org_id="mock") - call = "get" - - mockRoute(call, route, response_db["mock404"], max_calls=1) - with pytest.raises(Exception): - test_api.get_organization(org_id="mock") - - mockRoute(call, route, response_db["mock200"], max_calls=1) - assert (test_api.get_organization(org_id="mock")).json() == json_db[ - "singleTransfer_response" - ] diff --git a/test/autoprotocol/invalidTransfer.json b/test/autoprotocol/invalidTransfer.json deleted file mode 100644 index 417c28d9..00000000 --- a/test/autoprotocol/invalidTransfer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "refs": { - "test_plate_96": { - "new": "96-pcr", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - { - "transfer": [ - { - "from": "test_plate_96/0", - "to": "test_plate_96/1", - "volume": "175.0:microliter" - } - ] - } - ], - "op": "pipette" - } - ] -} diff --git a/test/autoprotocol/invalidTransfer_response.json b/test/autoprotocol/invalidTransfer_response.json deleted file mode 100644 index ec077cb1..00000000 --- a/test/autoprotocol/invalidTransfer_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "protocol": [ - { - "context": { - "instruction": 0 - }, - "message": "175.0 uL exceeds the maximum well volume for 'test_plate_96': max volume for '96-pcr' is 160.0 uL" - } - ] -} diff --git a/test/autoprotocol/singleTransfer.json b/test/autoprotocol/singleTransfer.json deleted file mode 100644 index 65eea621..00000000 --- a/test/autoprotocol/singleTransfer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "refs": { - "test_plate_96": { - "new": "96-pcr", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - { - "transfer": [ - { - "from": "test_plate_96/0", - "to": "test_plate_96/1", - "volume": "155.0:microliter" - } - ] - } - ], - "op": "pipette" - } - ] -} diff --git a/test/autoprotocol/singleTransfer_response.json b/test/autoprotocol/singleTransfer_response.json deleted file mode 100644 index 12958eb1..00000000 --- a/test/autoprotocol/singleTransfer_response.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "results": {}, - "pending_shipment_ids": [], - "canceled_at": null, - "created_at": null, - "request_type": "protocol", - "aborted_at": null, - "billing_valid?": true, - "conversation_id": null, - "title": null, - "completed_at": null, - "successors": [], - "accepted_at": "2016-04-14T11:43:54.976-07:00", - "quote": { - "items": [ - { - "taxable": false, - "cost": "0.091", - "quantity": 1, - "title": "Workcell Time", - "run_id": null, - "run_credit_applicable": true - }, - { - "taxable": false, - "cost": "3.077", - "quantity": 1, - "title": "Reagents & Consumables", - "run_id": null, - "run_credit_applicable": false - } - ], - "breakdown": { - "children": [ - { - "children": [ - { - "taxable": true, - "total": "2.963", - "name": "Ref 'test_plate_96' (96-pcr)", - "run_credit_applicable": false - } - ], - "total": "2.963", - "name": "Containers" - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "taxable": true, - "total": "0.114", - "name": "Disposable Tips", - "run_credit_applicable": false - }, - { - "taxable": true, - "total": "0.091", - "name": "Pipetting", - "run_credit_applicable": true - } - ], - "total": "0.205", - "name": "Liquid Handling" - } - ], - "total": "0.205", - "name": "Instruction 0: pipette" - } - ], - "total": "0.205", - "name": "Instructions" - } - ], - "total": "3.168", - "name": "Total" - }, - "tax_rate": "9.0" - }, - "committed_at": null, - "draft_quote": null, - "started_at": null, - "total_cost": "3.168", - "properties": {}, - "id": null, - "project": { - "organization": { - "subdomain": "transcriptic", - "id": "org13", - "name": "Transcriptic" - }, - "url": null, - "name": null, - "archived_at": null - }, - "progress": 0, - "can_cancel?": true, - "refs": [ - { - "destiny": { - "discard": true - }, - "container_type": { - "col_count": 12, - "well_depth_mm": "14.6", - "sterile": null, - "well_type": null, - "well_volume_ul": "160.0", - "is_tube": false, - "name": "96-well PCR plate", - "capabilities": [ - "magnetic_separate", - "thermocycle", - "qrt_thermocycle", - "spin", - "incubate", - "seal", - "image_plate", - "sanger_sequence", - "miniprep_destination", - "stamp", - "flash_freeze", - "dispense", - "echo_dest", - "magnetic_transfer_pcr" - ], - "well_coating": null, - "shortname": "96-pcr", - "well_count": 96, - "id": "96-pcr" - }, - "container_id": null, - "name": "test_plate_96", - "new_container_type": "96-pcr" - } - ], - "instructions": [ - { - "started_at": null, - "data_name": null, - "completed_at": null, - "warp_ids": [], - "data": null, - "warps": [], - "id": null, - "operation": { - "op": "pipette", - "groups": [ - { - "transfer": [ - { - "from": "test_plate_96/0", - "to": "test_plate_96/1", - "volume": "155.0:microliter" - } - ] - } - ] - }, - "relevant_resources": [], - "sequence_no": 0 - } - ], - "warnings": [], - "flagged": false, - "test_mode": false, - "status": "accepted", - "updated_at": null, - "issued_at": "2016-04-14T11:43:54.976-07:00", - "owner": { - "profile_photo_attachment_url": null, - "email": "yangchoo@transcriptic.com", - "id": "u17saeyc8c7hk", - "name": "yang choo" - }, - "has_quote?": true -} diff --git a/test/cli_test.py b/test/cli_test.py deleted file mode 100644 index 9940e7f8..00000000 --- a/test/cli_test.py +++ /dev/null @@ -1,136 +0,0 @@ -import json - -import pytest - -from transcriptic import commands -from transcriptic.cli import cli - -from .helpers.fixtures import * - - -def test_kebab_case(cli_test_runner): - result = cli_test_runner.invoke(cli, ["--help"]) - command_str = result.output.split("Commands:\n")[1] - commands = [x.strip() for x in command_str.split("\n") if x != ""] - is_kebab = lambda x: x.islower() and ("_" not in x) - assert all([is_kebab(cmd) for cmd in commands]) - - -def test_login_nonexistent_key_path(cli_test_runner): - result = cli_test_runner.invoke( - cli, - ["login", "--rsa-key", "/temp/path/invalid_key.pem"], - input="\n".join(["email@foo.com", "barpw"]), - ) - assert "Invalid value for '--rsa-key'" in result.stderr - - -def test_login_random_file_for_key(cli_test_runner, tmpdir_factory): - path = tmpdir_factory.mktemp("foo").join("random-file") - with open(str(path), "w") as fh: - fh.write("this is not an rsa key") - result = cli_test_runner.invoke( - cli, - ["login", "--rsa-key", str(path)], - input="\n".join(["email@foo.com", "barpw"]), - ) - assert ( - "Error loading RSA key: Could not parse the specified RSA Key, " - "ensure it is a PRIVATE key in PEM format" in result.stderr - ) - assert result.exit_code == 1 - - -def test_login_public_key(cli_test_runner, temp_ssh_key): - (private_key_path, public_key_path) = temp_ssh_key - result = cli_test_runner.invoke( - cli, - ["login", "--rsa-key", public_key_path], - input="\n".join(["email@foo.com", "barpw"]), - ) - assert "Error connecting to host: This is not a private key" in result.output - assert result.exit_code == 1 - - -def test_projects_exception(cli_test_runner, monkeypatch): - def mocked_exception(api, i, json_flag, names_only): - raise RuntimeError("Some runtime error") - - monkeypatch.setattr(commands, "projects", mocked_exception) - - result = cli_test_runner.invoke( - cli, - ["projects"], - ) - assert result.stderr == ( - "There was an error listing the projects in your " - "organization. Make sure your login details are correct.\n" - ) - - -def test_projects_names_only(cli_test_runner, monkeypatch): - mocked_return = {"p123": "Foo"} - monkeypatch.setattr( - commands, "projects", lambda api, i, json_flag, names_only: mocked_return - ) - - result = cli_test_runner.invoke( - cli, - ["projects", "--names"], - ) - assert result.output == f"{mocked_return}\n" - - -def test_projects_json_flag(cli_test_runner, monkeypatch): - mocked_return = [{"archived_at": "some datetime", "id": "p123", "name": "Foo"}] - monkeypatch.setattr( - commands, "projects", lambda api, i, json_flag, names_only: mocked_return - ) - - result = cli_test_runner.invoke( - cli, - ["projects", "--json"], - ) - assert result.output == f"{json.dumps(mocked_return)}\n" - - -def test_projects_default(cli_test_runner, monkeypatch): - mocked_return = {"p123": "Foo (archived)"} - monkeypatch.setattr( - commands, "projects", lambda api, i, json_flag, names_only: mocked_return - ) - - result = cli_test_runner.invoke( - cli, - ["projects"], - ) - assert result.output == ( - "\n" - " PROJECTS:\n" - " \n" - " PROJECT NAME | PROJECT ID \n" - "--------------------------------------------------------------------------------\n" - "Foo (archived) | p123 \n" - "--------------------------------------------------------------------------------\n" - ) - - -def test_submit_exception_handling(cli_test_runner, monkeypatch): - runtime_error = "Some runtime error message" - - def mocked_exception(api, file, project, title, test, pm): - raise RuntimeError(runtime_error) - - monkeypatch.setattr(commands, "submit", mocked_exception) - result = cli_test_runner.invoke(cli, ["submit", "--project", "some project"]) - assert result.stderr == f"{runtime_error}\n" - assert result.exit_code == 1 - - -def test_submit_success(cli_test_runner, monkeypatch): - mock_url = "http://mock-api/mock/p123/runs/r123" - monkeypatch.setattr( - commands, "submit", lambda api, file, project, title, test, pm: mock_url - ) - result = cli_test_runner.invoke(cli, ["submit", "--project", "some project"]) - assert result.output == f"Run created: {mock_url}\n" diff --git a/test/commands/__init__.py b/test/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/commands/exec_test.py b/test/commands/exec_test.py deleted file mode 100644 index 2b612b93..00000000 --- a/test/commands/exec_test.py +++ /dev/null @@ -1,200 +0,0 @@ -import json - -import requests - -from transcriptic.cli import cli - -from ..helpers.fixtures import * -from ..helpers.mockAPI import MockResponse - - -# Structure of the response object from SCLE -def queue_test_success_res(sessionId="testSessionId"): - return {"success": True, "sessionId": sessionId} - - -fake_valid_URL = "something.bar.foo" - - -def app_config_res(): - return {"hostManifest": {"lab": {"workcell": {"url": fake_valid_URL}}}} - - -def mock_api_endpoint(): - return "foo.bar.baz/lab/workcell" - - -def mockget(*args, **kwargs): - return MockResponse(0, app_config_res(), json.dumps(app_config_res())) - - -@pytest.fixture -def ap_file(tmpdir_factory): - """Make a temp autoprotocol file""" - path = tmpdir_factory.mktemp("foo").join("ap.json") - with open(str(path), "w") as f: - payload = { - "instructions": [ - {"op": "provision", "x_human": True}, - {"op": "uncover"}, - {"op": "spin"}, - {"op": "cover"}, - ] - } - f.write(json.dumps(payload)) - return path - - -def test_unspecified_api(cli_test_runner, ap_file): - result = cli_test_runner.invoke(cli, ["exec", str(ap_file)]) - assert result.exit_code != 0 - assert "Missing option '--api'" in result.stderr - - -def test_good_autoprotocol(cli_test_runner, monkeypatch, ap_file): - def mockpost(*args, **kwargs): - return MockResponse( - 0, queue_test_success_res(), json.dumps(queue_test_success_res()) - ) - - monkeypatch.setattr(requests, "post", mockpost) - monkeypatch.setattr(requests, "get", mockget) - result = cli_test_runner.invoke( - cli, ["exec", str(ap_file), "-a", mock_api_endpoint()] - ) - assert result.exit_code == 0 - assert ( - f"Success. View http://{mock_api_endpoint()}/dashboard to see the scheduling outcome." - in result.output - ) - - -def test_bad_autoprotocol(cli_test_runner): - result = cli_test_runner.invoke( - cli, ["exec", "bad-file-handle", "-a", mock_api_endpoint()] - ) - assert result.exit_code != 0 - assert "Invalid value for '[AUTOPROTOCOL]': Could not open file" in result.stderr - - -def test_bad_deviceset(cli_test_runner, ap_file): - result = cli_test_runner.invoke( - cli, - [ - "exec", - str(ap_file), - "--device-set", - "bad-file-handle", - "-a", - mock_api_endpoint(), - ], - ) - assert result.exit_code != 0 - assert ( - "Invalid value for '--device-set' / '-d': Could not open file: bad-file-handle:" - in result.stderr - ) - - -def test_bad_api_response(cli_test_runner, monkeypatch, ap_file): - def mockpost(*args, **kwargs): - return MockResponse(0, "not-json", "not-json") - - monkeypatch.setattr(requests, "post", mockpost) - monkeypatch.setattr(requests, "get", mockget) - result = cli_test_runner.invoke( - cli, ["exec", str(ap_file), "-a", mock_api_endpoint()] - ) - assert result.exit_code != 0 - assert "Error: " in result.stderr - - -def test_good_workcell(cli_test_runner, monkeypatch, ap_file): - def mockpost(*args, **kwargs): - return MockResponse( - 0, queue_test_success_res(), json.dumps(queue_test_success_res()) - ) - - monkeypatch.setattr(requests, "post", mockpost) - monkeypatch.setattr(requests, "get", mockget) - result = cli_test_runner.invoke( - cli, ["exec", str(ap_file), "-a", mock_api_endpoint(), "-w", "wc3"] - ) - assert result.exit_code == 0 - assert ( - f"Success. View http://{mock_api_endpoint()}/dashboard to see the scheduling outcome." - in result.output - ) - - -def test_bad_workcell(cli_test_runner, monkeypatch, ap_file): - result = cli_test_runner.invoke( - cli, ["exec", str(ap_file), "-a", mock_api_endpoint(), "-w", "hello.world"] - ) - assert result.exit_code != 0 - assert "Error: " in result.stderr - - -def test_session_id(cli_test_runner, monkeypatch, ap_file): - sessionId = "hi_there" - - def mockpost(*args, **kwargs): - return MockResponse( - 0, - queue_test_success_res(sessionId), - json.dumps(queue_test_success_res(sessionId)), - ) - - monkeypatch.setattr(requests, "post", mockpost) - monkeypatch.setattr(requests, "get", mockget) - result = cli_test_runner.invoke( - cli, - [ - "exec", - str(ap_file), - "-a", - mock_api_endpoint(), - "-s", - sessionId, - ], - ) - assert result.exit_code == 0 - assert ( - f"Success. View http://{mock_api_endpoint()}/dashboard to see the scheduling outcome." - in result.output - ) - - -def test_too_many_workcell_definition_arguments(cli_test_runner, monkeypatch, ap_file): - - result = cli_test_runner.invoke( - cli, - ["exec", str(ap_file), "-a", mock_api_endpoint(), "-s", "anthing", "-w", "wc0"], - ) - assert result.exit_code != 0 - assert "Error: --workcell-id, --session-id are mutually exclusive." in result.stderr - - -def test_invalid_filters(cli_test_runner, monkeypatch, ap_file): - invalid_command = [ - "-e", - "10", - "-i", - "10", - "-e", - "2-1", - "-i", - "3-6", - "-e", - "anything", - ] - invalid_filters = set(filter(lambda v: not v.startswith("-"), invalid_command)) - result = cli_test_runner.invoke( - cli, - ["exec", str(ap_file), "-a", mock_api_endpoint()] + invalid_command, - ) - assert result.exit_code != 0 - assert "Error: invalid filters" in result.stderr - assert "(number of instructions: 4)" in result.stderr - for inv in invalid_filters: - assert inv in result.stderr diff --git a/test/commands/launch_test.py b/test/commands/launch_test.py deleted file mode 100644 index c6d1e159..00000000 --- a/test/commands/launch_test.py +++ /dev/null @@ -1,233 +0,0 @@ -import pytest -import responses - -from transcriptic import commands -from transcriptic.util import PreviewParameters - - -class TestLaunch: - """ - Note: Underlying helper functions are tested separately in `TestUtils` class. This - uses monkeypatching to mock out those functions. - """ - - project_id = "p123" - launch_request_id = "lr123" - quick_launch_id = "quick123" - protocolname = "protocolname" - title = "launch_title" - run_id = "r123" - mock_baseurl = "http://mock-api/mock" - - @pytest.fixture(scope="function") - def simple_manifest(self, simple_protocol): - yield {"format": "python", "license": "MIT", "protocols": [simple_protocol]} - - @pytest.fixture(scope="function") - def simple_protocol(self): - yield { - "name": self.protocolname, - "display_name": "displayname", - "categories": [], - "description": "desc", - "version": "1.0.0", - "command_string": "python3 -m protocol.main", - "inputs": {}, - "preview": {"parameters": {}, "refs": {}}, - } - - @pytest.fixture - def autoprotocol(self): - """Make a temp autoprotocol file""" - yield { - "instructions": [ - {"op": "provision", "x_human": True}, - {"op": "uncover"}, - {"op": "spin"}, - {"op": "cover"}, - ] - } - - @pytest.fixture(scope="function") - def run_protocol(self, autoprotocol): - return lambda api, manifest, protocol_obj, inputs: autoprotocol - - @pytest.fixture(scope="function") - def load_manifest(self, simple_manifest): - return lambda: simple_manifest - - @pytest.fixture(scope="function") - def load_protocol(self, simple_protocol): - return lambda manifest, protocol_name: simple_protocol - - @pytest.fixture(scope="function") - def format_url(self): - return lambda path: f"{self.mock_baseurl}/{self.project_id}/runs/{self.run_id}" - - @pytest.fixture(scope="function") - def _get_quick_launch(self): - return lambda api, protocol_obj, project: { - "raw_inputs": {"parameters": {"config": {"foo": "bar"}}, "refs": {}} - } - - @pytest.fixture(scope="function") - def _get_launch_request(self): - return lambda api, params, protocol_obj, test: [ - self.launch_request_id, - {"generation_errors": []}, - ] - - @responses.activate - def test_local( - self, - monkeypatch, - test_connection, - _get_launch_request, - _get_quick_launch, - format_url, - load_manifest, - load_protocol, - run_protocol, - local=True, - ): - monkeypatch.setattr(commands, "load_manifest", load_manifest) - monkeypatch.setattr(commands, "load_protocol", load_protocol) - monkeypatch.setattr( - commands, "get_project_id", lambda api, project: self.project_id - ) - monkeypatch.setattr(commands, "_get_launch_request", _get_launch_request) - monkeypatch.setattr(commands, "_get_quick_launch", _get_quick_launch) - monkeypatch.setattr(test_connection, "url", format_url) - monkeypatch.setattr(commands, "run_protocol", run_protocol) - responses.add( - responses.POST, - test_connection.get_route( - "create_quick_launch", project_id=self.project_id - ), - json={"id": self.quick_launch_id}, - ) - responses.add( - responses.POST, - test_connection.get_route( - "resolve_quick_launch_inputs", - project_id=self.project_id, - quick_launch_id=self.quick_launch_id, - ), - json={"inputs": {"parameters": {}, "refs": {}}}, - ) - # Test that it will run without error since the autoprotocol generated - # gets dumped out in the terminal and not returned - commands.launch( - test_connection, - protocol=self.protocolname, - project=self.project_id, - title=self.title, - save_input=False, - local=local, - accept_quote=True, - params=None, - ) - - @responses.activate - def test_not_local( - self, - monkeypatch, - test_connection, - _get_launch_request, - _get_quick_launch, - format_url, - local=False, - ): - monkeypatch.setattr( - commands, "get_project_id", lambda api, project: self.project_id - ) - monkeypatch.setattr(commands, "_get_launch_request", _get_launch_request) - monkeypatch.setattr(commands, "_get_quick_launch", _get_quick_launch) - monkeypatch.setattr(test_connection, "url", format_url) - - responses.add( - responses.GET, - test_connection.get_route("get_protocols", org_id="mock"), - json=[ - { - "id": "protocol123", - "name": self.protocolname, - "created_at": "today", - "package_name": "pkgname", - "package_id": "pkg123", - "release_id": "rel123", - "license": "MIT", - "published": True, - "display_name": "displayname", - "categories": [], - "description": "desc", - "version": "1.0.0", - "command_string": "python3 -m protocol.main", - "inputs": {}, - "preview": {"parameters": {}, "refs": {}}, - } - ], - ) - responses.add( - responses.POST, - test_connection.get_route( - "submit_launch_request", - launch_request_id=self.launch_request_id, - project_id=self.project_id, - protocol_id="protocol123", - title=self.title, - ), - json={"id": f"{self.run_id}"}, - ) - - actual = commands.launch( - test_connection, - protocol=self.protocolname, - project=self.project_id, - title=self.title, - save_input=False, - local=local, - accept_quote=True, - params=None, - ) - expected = f"{self.mock_baseurl}/{self.project_id}/runs/{self.run_id}" - assert actual == expected - - @responses.activate - def test_save_preview( - self, - monkeypatch, - test_connection, - load_manifest, - load_protocol, - _get_launch_request, - _get_quick_launch, - format_url, - ): - monkeypatch.setattr(commands, "load_manifest", load_manifest) - monkeypatch.setattr(commands, "load_protocol", load_protocol) - monkeypatch.setattr(commands, "_get_quick_launch", _get_quick_launch) - manifest = commands.load_manifest() - protocol_obj = commands.load_protocol( - manifest=manifest, protocol_name=self.protocolname - ) - quick_launch = commands._get_quick_launch( - test_connection, protocol_obj, self.project_id - ) - params = dict(parameters=quick_launch["raw_inputs"]) - pp = PreviewParameters(test_connection, params["parameters"], protocol_obj) - - # Assert that the PreviewParameters.preview does not match current manfest object - assert manifest["protocols"][0].get("preview") == protocol_obj.get("preview") - assert manifest["protocols"][0].get("preview") != pp.preview.get("preview") - - # Merge PreviewParameters.preview into a copy of the manifest - pp.merge(manifest) - - # Assert that the merge placed the PreviewParameters.preview into the merged_manifest - assert pp.merged_manifest["protocols"][0].get("preview") != protocol_obj.get( - "preview" - ) - assert pp.merged_manifest["protocols"][0].get("preview") == pp.preview.get( - "preview" - ) diff --git a/test/commands/projects_test.py b/test/commands/projects_test.py deleted file mode 100644 index 04788b16..00000000 --- a/test/commands/projects_test.py +++ /dev/null @@ -1,89 +0,0 @@ -import json - -import pytest -import responses - -from transcriptic import commands - - -class TestProjects: - @responses.activate - def test_projects_invalid(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json="some verbose error", - status=404, - ) - - with pytest.raises(RuntimeError): - commands.projects(test_connection) - - @responses.activate - def test_projects_names_only(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json={ - "projects": [ - {"archived_at": "some datetime", "id": "p123", "name": "Foo"} - ] - }, - ) - - actual = commands.projects(test_connection, names_only=True) - - expected = {"p123": "Foo"} - assert actual == expected - - @responses.activate - def test_projects_deprecated_i(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json={ - "projects": [ - {"archived_at": "some datetime", "id": "p123", "name": "Foo"} - ] - }, - ) - - with pytest.warns(FutureWarning): - actual = commands.projects(test_connection, i=True) - - expected = {"p123": "Foo"} - assert actual == expected - - @responses.activate - def test_projects_json_flag(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json={ - "projects": [ - {"archived_at": "some datetime", "id": "p123", "name": "Foo"} - ] - }, - ) - - actual = commands.projects(test_connection, json_flag=True) - - expected = [{"archived_at": "some datetime", "id": "p123", "name": "Foo"}] - assert actual == expected - - @responses.activate - def test_projects_default_return(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json={ - "projects": [ - {"archived_at": "some datetime", "id": "p123", "name": "Foo"} - ] - }, - ) - - actual = commands.projects(test_connection) - - expected = {"p123": "Foo (archived)"} - assert actual == expected diff --git a/test/commands/submit_test.py b/test/commands/submit_test.py deleted file mode 100644 index a7e32216..00000000 --- a/test/commands/submit_test.py +++ /dev/null @@ -1,87 +0,0 @@ -import json - -import pytest -import responses - -from transcriptic import commands - - -class TestSubmit: - """ - Note: Underlying helper functions are tested separately in `TestUtils` class. This - uses monkeypatching to mock out those functions. - """ - - @pytest.fixture - def valid_json_file(self, tmpdir): - path = tmpdir.mkdir("foo").join("valid-input.json") - path.write(json.dumps({"refs": {}, "instructions": []})) - yield path - - def test_invalid_pm(self, monkeypatch, test_connection): - monkeypatch.setattr(commands, "is_valid_payment_method", lambda api, pm: False) - - with pytest.raises(RuntimeError) as error: - commands.submit(test_connection, "some file", "some project", pm="invalid") - - assert f"{error.value}" == ( - "Payment method is invalid. Please specify a payment " - "method from `transcriptic payments` or not specify the " - "`--payment` flag to use the default payment method." - ) - - def test_invalid_project(self, monkeypatch, test_connection): - monkeypatch.setattr(commands, "get_project_id", lambda api, project: False) - - with pytest.raises(RuntimeError) as error: - commands.submit(test_connection, "some file", "invalid_project") - - assert f"{error.value}" == "Invalid project invalid_project specified" - - def test_invalid_file(self, monkeypatch, test_connection, tmpdir): - path = tmpdir.mkdir("foo").join("invalid-input.txt") - path.write("this is not json") - - monkeypatch.setattr(commands, "get_project_id", lambda api, project: "p123") - - with pytest.raises(RuntimeError) as error: - commands.submit(test_connection, path, "project name") - - assert ( - f"{error.value}" - == "Error: Could not submit since your manifest.json file is improperly " - "formatted." - ) - - @responses.activate - def test_valid_submission(self, monkeypatch, test_connection, valid_json_file): - monkeypatch.setattr(commands, "get_project_id", lambda api, project: "p123") - - responses.add( - responses.POST, - test_connection.get_route("submit_run", project_id="p123"), - json={"id": "r123"}, - ) - - actual = commands.submit(test_connection, valid_json_file, "project name") - - expected = "http://mock-api/mock/p123/runs/r123" - assert actual == expected - - @responses.activate - def test_submit_exception_handling( - self, monkeypatch, test_connection, valid_json_file - ): - monkeypatch.setattr(commands, "get_project_id", lambda api, project: "p123") - - responses.add( - responses.POST, - test_connection.get_route("submit_run", project_id="p123"), - json="some verbose error", - status=404, - ) - - with pytest.raises(RuntimeError) as error: - commands.submit(test_connection, valid_json_file, "project name") - - assert "Error: Couldn't create run (404)" in f"{error.value}" diff --git a/test/commands/utils_test.py b/test/commands/utils_test.py deleted file mode 100644 index 830de149..00000000 --- a/test/commands/utils_test.py +++ /dev/null @@ -1,40 +0,0 @@ -import responses - -from transcriptic import commands - - -class TestUtils: - @responses.activate - def test_valid_payment_method_true(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_payment_methods"), - json=[{"id": "someId", "is_valid": True}], - ) - assert commands.is_valid_payment_method(test_connection, "someId") - - @responses.activate - def test_valid_payment_method_false(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_payment_methods"), - json=[{"id": "someId", "is_valid": True}], - ) - assert not commands.is_valid_payment_method(test_connection, "invalidId") - - @responses.activate - def test_get_project_id(self, test_connection): - responses.add( - responses.GET, - test_connection.get_route("get_projects"), - json={ - "projects": [ - {"archived_at": "some datetime", "id": "p123", "name": "Foo"} - ] - }, - ) - - actual = commands.get_project_id(test_connection, "Foo") - - expected = "p123" - assert actual == expected diff --git a/test/config_test.py b/test/config_test.py deleted file mode 100644 index 26ddc218..00000000 --- a/test/config_test.py +++ /dev/null @@ -1,594 +0,0 @@ -import json -import re -import tempfile -import unittest - -from email.utils import formatdate - -import pytest -import requests -import responses -import transcriptic.config - - -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock - - -class ConnectionInitTests(unittest.TestCase): - def test_inits_valid(self): - with tempfile.NamedTemporaryFile() as config_file: - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - }, - f, - ) - - transcriptic.config.Connection.from_file(config_file.name) - - def test_inits_invalid(self): - with tempfile.NamedTemporaryFile() as config_file: - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - # "token": "foobarinvalid", (missing token) - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - }, - f, - ) - - # TODO(meawoppl) - assertRaisesRegexp deprecated in py3 - # Move it to assertRaisesRegex() - with self.assertRaisesRegexp(OSError, "token"): - transcriptic.config.Connection.from_file(config_file.name) - - def get_mocked_connection(self): - # NOTE(meawoppl) - This could be the start of a testing pattern - # for this module. - class FakeReturn: - status_code = 200 - - def json(self): - return {} - - session = transcriptic.config.initialize_default_session() - session.put = Mock(return_value=FakeReturn()) - connection = transcriptic.config.Connection( - email="foo@transcriptic.com", - token="bar", - organization_id="txid", - api_root="https://fake.transcriptic.com", - session=session, - ) - return session, connection - - def test_aliquot_modify(self): - session, connection = self.get_mocked_connection() - - aliquot_id = "23534245" - props = {"prop1": "val1"} - connection.modify_aliquot_properties(aliquot_id, props) - session.put.assert_called_with( - "https://fake.transcriptic.com/api/aliquots/23534245/modify_properties", - json={"id": aliquot_id, "data": {"delete": [], "set": props}}, - ) - self.assertTrue( - session.headers.get("X-Organization-Id") == connection.organization_id - ) - self.assertTrue( - session.headers.get("X-Organization-Id") == connection.env_args["org_id"] - ) - - def test_signing(self): - # Set up a connection with a key from a file - with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file: - with open(key_file.name, "w") as kf: - kf.write( - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDIK/IzSkBEuwKjYQo/ri4iKTTkr+FDtJetI7dYoz0//U5z7Vbu\nZQWncDNc38wMKidf2bWA+MTSWcYVUTlivp0y98MTLPsR6oJ9RwLggA2lFlCIjmdV\nUow/MmhWg0vX/SkThxS/F5I41GTrNIU3ZVZwGbmQ8hbyKCBYtbEHJWqATwIDAQAB\nAoGAHtYmSaB2ph/pGCIq4gSDNuACNfiiSzvW4eVOqWj8Vo8/NrypV7BYXqL6RqRz\nWqxjxHBVdbjdGUqbKU2J+ZxDuwCREsxQipjq+hM9aPpgjNJg4dz6yuc5mnUdOr9M\nR+zFjnnOJx98HGjuzLDXdBNYVSZFcDWj70Fjln/z5AjBYQECQQDijaHEcvDJOUmL\nDNyAYbjK811kFGpmglQBiZ257L47IP6jgqN544siHGnI7rykt+1upGfB2q8uQSIb\njNJKKsa3AkEA4jB9PXE8EooJ/eax2UsuwXt9LAgRabFurAJtadpAeeFBIIMSwBXU\n7APMfB3cQOnBlodnyrQ56mIOWPcSdN+7KQJAHr+4aBBtq/IRkETjnK0mxqz3TQEU\nW+tueXLzLGv8ecwFo620gHOoy61tki8M/ZJVMIIx7va+dhmzBmg7loNtywJAZUdy\n/K0USfTXToIaxoJcmDQUM0AVk+7n8EtR9KDOWASdpdIq9imQYnG9ASJZuhMxJJbS\nybfzatinNfzDneOEKQJBAMLOhHHbskUuuU9oDUl8sbrsreglQuoq1hvlB1uVskpi\nqMEIXSBwxAlxwmiAQLgS4hZY+cmQ3v5hCberMaZRPZ8=\n-----END RSA PRIVATE KEY-----\n" - ) - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - "rsa_key": key_file.name, - }, - f, - ) - - connection = transcriptic.config.Connection.from_file(config_file.name) - - # Test that GET signature matches the above key (given a hardcoded date header) - get_request = requests.Request( - "GET", - "http://foo:5555/get", - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_get = connection.session.prepare_request(get_request) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - - get_sig = prepared_get.headers["authorization"] - self.assertEqual( - re.search(r'keyId="(.+?)"', get_sig).group(1), "somebody@transcriptic.com" - ) - self.assertEqual( - re.search(r'algorithm="(.+?)"', get_sig).group(1), "rsa-sha256" - ) - self.assertEqual( - re.search(r'signature="(.+?)"', get_sig).group(1), - "GfcAtyV0+CKDkxjsREXYAm6RP0WdIYaN5RamlNfIYZ7e847KAydQf2ylYIcsj9CS5BIOiBi5JBoC6n51NSbxU+kQcSv2nzSsq3rBpTFFMHUhTPdrfeHsH4IBvgMWZZHmHvyE7UXVqhLssVzMIm/oGTnprPMWiTcsKjEhe+DsQT4=", - ) - self.assertEqual( - set(re.search(r'headers="(.+?)"', get_sig).group(1).split(" ")), - {"(request-target)", "date", "host"}, - ) - - # Test that POST signature matches the above key (given a hardcoded body & date header) - post_request = requests.Request( - "POST", - "http://foo:5555/get", - data="Some body content", - headers={ - "Date": formatdate(timeval=1588638873, localtime=False, usegmt=True) - }, - ) - prepared_post = connection.session.prepare_request(post_request) - - post_sig = prepared_post.headers["authorization"] - self.assertEqual( - re.search(r'keyId="(.+?)"', post_sig).group(1), "somebody@transcriptic.com" - ) - self.assertEqual( - re.search(r'algorithm="(.+?)"', post_sig).group(1), "rsa-sha256" - ) - self.assertEqual( - re.search(r'signature="(.+?)"', post_sig).group(1), - "TnixnCg4eT7nQhYQP1PNZHv5MVhHnWKf6MQb+cyS2tfw6++yuSaZS/4kz9nuvAbjZ1CsLWBKBDDIQWZqQXEjGUH/mXQ3KYDXhREyl0aDv2NgxKBcAsSooDa9nfA9zI7OeFa2dYzF81oOB4L4PDkbV3Bojw4mQYJf0eLW6FL1yTI=", - ) - self.assertEqual( - set(re.search(r'headers="(.+?)"', post_sig).group(1).split(" ")), - {"(request-target)", "date", "host", "content-length", "digest"}, - ) - - def test_signing_post_no_body_in_request(self): - # Set up a connection with a key from a file - with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file: - with open(key_file.name, "w") as kf: - kf.write( - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDIK/IzSkBEuwKjYQo/ri4iKTTkr+FDtJetI7dYoz0//U5z7Vbu\nZQWncDNc38wMKidf2bWA+MTSWcYVUTlivp0y98MTLPsR6oJ9RwLggA2lFlCIjmdV\nUow/MmhWg0vX/SkThxS/F5I41GTrNIU3ZVZwGbmQ8hbyKCBYtbEHJWqATwIDAQAB\nAoGAHtYmSaB2ph/pGCIq4gSDNuACNfiiSzvW4eVOqWj8Vo8/NrypV7BYXqL6RqRz\nWqxjxHBVdbjdGUqbKU2J+ZxDuwCREsxQipjq+hM9aPpgjNJg4dz6yuc5mnUdOr9M\nR+zFjnnOJx98HGjuzLDXdBNYVSZFcDWj70Fjln/z5AjBYQECQQDijaHEcvDJOUmL\nDNyAYbjK811kFGpmglQBiZ257L47IP6jgqN544siHGnI7rykt+1upGfB2q8uQSIb\njNJKKsa3AkEA4jB9PXE8EooJ/eax2UsuwXt9LAgRabFurAJtadpAeeFBIIMSwBXU\n7APMfB3cQOnBlodnyrQ56mIOWPcSdN+7KQJAHr+4aBBtq/IRkETjnK0mxqz3TQEU\nW+tueXLzLGv8ecwFo620gHOoy61tki8M/ZJVMIIx7va+dhmzBmg7loNtywJAZUdy\n/K0USfTXToIaxoJcmDQUM0AVk+7n8EtR9KDOWASdpdIq9imQYnG9ASJZuhMxJJbS\nybfzatinNfzDneOEKQJBAMLOhHHbskUuuU9oDUl8sbrsreglQuoq1hvlB1uVskpi\nqMEIXSBwxAlxwmiAQLgS4hZY+cmQ3v5hCberMaZRPZ8=\n-----END RSA PRIVATE KEY-----\n" - ) - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - "rsa_key": key_file.name, - }, - f, - ) - - connection = transcriptic.config.Connection.from_file(config_file.name) - - # Test that GET signature matches the above key (given a hardcoded date header) - get_request = requests.Request( - "GET", - "http://foo:5555/get", - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_get = connection.session.prepare_request(get_request) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - - get_sig = prepared_get.headers["authorization"] - self.assertEqual( - re.search(r'keyId="(.+?)"', get_sig).group(1), "somebody@transcriptic.com" - ) - self.assertEqual( - re.search(r'algorithm="(.+?)"', get_sig).group(1), "rsa-sha256" - ) - self.assertEqual( - re.search(r'signature="(.+?)"', get_sig).group(1), - "GfcAtyV0+CKDkxjsREXYAm6RP0WdIYaN5RamlNfIYZ7e847KAydQf2ylYIcsj9CS5BIOiBi5JBoC6n51NSbxU+kQcSv2nzSsq3rBpTFFMHUhTPdrfeHsH4IBvgMWZZHmHvyE7UXVqhLssVzMIm/oGTnprPMWiTcsKjEhe+DsQT4=", - ) - self.assertEqual( - set(re.search(r'headers="(.+?)"', get_sig).group(1).split(" ")), - {"(request-target)", "date", "host"}, - ) - - # Test that POST signature matches the above key (given a hardcoded body & date header) - post_request = requests.Request( - "POST", - "http://foo:5555/get", - data=None, - headers={ - "Date": formatdate(timeval=1588638873, localtime=False, usegmt=True) - }, - ) - prepared_post = connection.session.prepare_request(post_request) - - post_sig = prepared_post.headers["authorization"] - self.assertEqual( - re.search(r'keyId="(.+?)"', post_sig).group(1), "somebody@transcriptic.com" - ) - self.assertEqual( - re.search(r'algorithm="(.+?)"', post_sig).group(1), "rsa-sha256" - ) - self.assertEqual( - re.search(r'signature="(.+?)"', post_sig).group(1), - "leQ7TO8IYbOKouKjqbwvk5uYrkmmO64aWBxxWGDMA41Kn3k0/9r6L4XWLGmMIjSqzQmFHF8Sdtz7pCh8YaQLHzXzwY0+R43jlJGLiL7WzgzWegHsnY2NwGOdGPWbVqAP3oCmr2lQtzl+k39ySW3Tpd+t8c0+VDCYOOV/kIAISVw=", - ) - self.assertEqual( - set(re.search(r'headers="(.+?)"', post_sig).group(1).split(" ")), - {"(request-target)", "date", "host", "content-length", "digest"}, - ) - - @responses.activate - def test_signing_auth_header_on_redirects(self): - # Set up a connection with a key from a file - with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file: - with open(key_file.name, "w") as kf: - kf.write( - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDIK/IzSkBEuwKjYQo/ri4iKTTkr+FDtJetI7dYoz0//U5z7Vbu\nZQWncDNc38wMKidf2bWA+MTSWcYVUTlivp0y98MTLPsR6oJ9RwLggA2lFlCIjmdV\nUow/MmhWg0vX/SkThxS/F5I41GTrNIU3ZVZwGbmQ8hbyKCBYtbEHJWqATwIDAQAB\nAoGAHtYmSaB2ph/pGCIq4gSDNuACNfiiSzvW4eVOqWj8Vo8/NrypV7BYXqL6RqRz\nWqxjxHBVdbjdGUqbKU2J+ZxDuwCREsxQipjq+hM9aPpgjNJg4dz6yuc5mnUdOr9M\nR+zFjnnOJx98HGjuzLDXdBNYVSZFcDWj70Fjln/z5AjBYQECQQDijaHEcvDJOUmL\nDNyAYbjK811kFGpmglQBiZ257L47IP6jgqN544siHGnI7rykt+1upGfB2q8uQSIb\njNJKKsa3AkEA4jB9PXE8EooJ/eax2UsuwXt9LAgRabFurAJtadpAeeFBIIMSwBXU\n7APMfB3cQOnBlodnyrQ56mIOWPcSdN+7KQJAHr+4aBBtq/IRkETjnK0mxqz3TQEU\nW+tueXLzLGv8ecwFo620gHOoy61tki8M/ZJVMIIx7va+dhmzBmg7loNtywJAZUdy\n/K0USfTXToIaxoJcmDQUM0AVk+7n8EtR9KDOWASdpdIq9imQYnG9ASJZuhMxJJbS\nybfzatinNfzDneOEKQJBAMLOhHHbskUuuU9oDUl8sbrsreglQuoq1hvlB1uVskpi\nqMEIXSBwxAlxwmiAQLgS4hZY+cmQ3v5hCberMaZRPZ8=\n-----END RSA PRIVATE KEY-----\n" - ) - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - "rsa_key": key_file.name, - }, - f, - ) - - connection = transcriptic.config.Connection.from_file(config_file.name) - - get_request = requests.Request( - "GET", - "http://foo:5555/get", - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_get = connection.session.prepare_request(get_request) - - # Setup redirect and simulate request - resp = requests.Response() - resp.headers["location"] = "http://foo:5555/redirect/get" - resp.request = prepared_get - resp.status_code = 302 - responses.add(responses.GET, "http://foo:5555/redirect/get") - - next_resp = next(connection.session.resolve_redirects(resp, prepared_get)) - # Ensure headers are properly populated - get_sig = next_resp.request.headers["authorization"] - self.assertEqual( - re.search(r'keyId="(.+?)"', get_sig).group(1), "somebody@transcriptic.com" - ) - self.assertEqual( - re.search(r'algorithm="(.+?)"', get_sig).group(1), "rsa-sha256" - ) - self.assertEqual( - re.search(r'signature="(.+?)"', get_sig).group(1), - "DdfePPvN88mmIq9l4crfwPm80SKV/iiaBht+28iwlRd+SuuB95VJJ5M+PCD8X0gEqKlwvfYHhgXFMJybMBFS2Z9Syv4wGHpM5FxEXi57mD69kW73Whkh3ROzr6fq39CdoK06BaJnQYNtZfSg7R0fgjFUImbVScQksQrYwQ3yVF4=", - ) - self.assertEqual( - set(re.search(r'headers="(.+?)"', get_sig).group(1).split(" ")), - {"(request-target)", "date", "host"}, - ) - - def test_signing_auth_header_not_set_when_calling_non_api_root_endpoint(self): - # Set up a connection with a key from a file - with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file: - with open(key_file.name, "w") as kf: - kf.write( - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDIK/IzSkBEuwKjYQo/ri4iKTTkr+FDtJetI7dYoz0//U5z7Vbu\nZQWncDNc38wMKidf2bWA+MTSWcYVUTlivp0y98MTLPsR6oJ9RwLggA2lFlCIjmdV\nUow/MmhWg0vX/SkThxS/F5I41GTrNIU3ZVZwGbmQ8hbyKCBYtbEHJWqATwIDAQAB\nAoGAHtYmSaB2ph/pGCIq4gSDNuACNfiiSzvW4eVOqWj8Vo8/NrypV7BYXqL6RqRz\nWqxjxHBVdbjdGUqbKU2J+ZxDuwCREsxQipjq+hM9aPpgjNJg4dz6yuc5mnUdOr9M\nR+zFjnnOJx98HGjuzLDXdBNYVSZFcDWj70Fjln/z5AjBYQECQQDijaHEcvDJOUmL\nDNyAYbjK811kFGpmglQBiZ257L47IP6jgqN544siHGnI7rykt+1upGfB2q8uQSIb\njNJKKsa3AkEA4jB9PXE8EooJ/eax2UsuwXt9LAgRabFurAJtadpAeeFBIIMSwBXU\n7APMfB3cQOnBlodnyrQ56mIOWPcSdN+7KQJAHr+4aBBtq/IRkETjnK0mxqz3TQEU\nW+tueXLzLGv8ecwFo620gHOoy61tki8M/ZJVMIIx7va+dhmzBmg7loNtywJAZUdy\n/K0USfTXToIaxoJcmDQUM0AVk+7n8EtR9KDOWASdpdIq9imQYnG9ASJZuhMxJJbS\nybfzatinNfzDneOEKQJBAMLOhHHbskUuuU9oDUl8sbrsreglQuoq1hvlB1uVskpi\nqMEIXSBwxAlxwmiAQLgS4hZY+cmQ3v5hCberMaZRPZ8=\n-----END RSA PRIVATE KEY-----\n" - ) - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - "rsa_key": key_file.name, - }, - f, - ) - - connection = transcriptic.config.Connection.from_file(config_file.name) - - get_request = requests.Request( - "GET", - "http://bar:5555/get", - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_get = connection.session.prepare_request(get_request) - self.assertFalse("authorization" in prepared_get.headers) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_get.headers["X-User-Email"] == connection.email) - - def test_signing_request_body_already_encoded(self): - # Set up a connection with a key from a file - with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file: - with open(key_file.name, "w") as kf: - kf.write( - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDIK/IzSkBEuwKjYQo/ri4iKTTkr+FDtJetI7dYoz0//U5z7Vbu\nZQWncDNc38wMKidf2bWA+MTSWcYVUTlivp0y98MTLPsR6oJ9RwLggA2lFlCIjmdV\nUow/MmhWg0vX/SkThxS/F5I41GTrNIU3ZVZwGbmQ8hbyKCBYtbEHJWqATwIDAQAB\nAoGAHtYmSaB2ph/pGCIq4gSDNuACNfiiSzvW4eVOqWj8Vo8/NrypV7BYXqL6RqRz\nWqxjxHBVdbjdGUqbKU2J+ZxDuwCREsxQipjq+hM9aPpgjNJg4dz6yuc5mnUdOr9M\nR+zFjnnOJx98HGjuzLDXdBNYVSZFcDWj70Fjln/z5AjBYQECQQDijaHEcvDJOUmL\nDNyAYbjK811kFGpmglQBiZ257L47IP6jgqN544siHGnI7rykt+1upGfB2q8uQSIb\njNJKKsa3AkEA4jB9PXE8EooJ/eax2UsuwXt9LAgRabFurAJtadpAeeFBIIMSwBXU\n7APMfB3cQOnBlodnyrQ56mIOWPcSdN+7KQJAHr+4aBBtq/IRkETjnK0mxqz3TQEU\nW+tueXLzLGv8ecwFo620gHOoy61tki8M/ZJVMIIx7va+dhmzBmg7loNtywJAZUdy\n/K0USfTXToIaxoJcmDQUM0AVk+7n8EtR9KDOWASdpdIq9imQYnG9ASJZuhMxJJbS\nybfzatinNfzDneOEKQJBAMLOhHHbskUuuU9oDUl8sbrsreglQuoq1hvlB1uVskpi\nqMEIXSBwxAlxwmiAQLgS4hZY+cmQ3v5hCberMaZRPZ8=\n-----END RSA PRIVATE KEY-----\n" - ) - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - "rsa_key": key_file.name, - }, - f, - ) - - connection = transcriptic.config.Connection.from_file(config_file.name) - - # Verify that when `json` is set in the request, the authorization header is still generated without error - # and confirm that the request body is already encoded as bytes - post_request = requests.Request( - "POST", - "http://foo:5555/get", - json={"foo": "bar"}, - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_post = connection.session.prepare_request(post_request) - self.assertTrue("authorization" in prepared_post.headers) - self.assertTrue( - prepared_post.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_post.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_post.headers["X-User-Email"] == connection.email) - self.assertTrue(isinstance(prepared_post.body, bytes)) - - # Verify that when `data` is set in the request, the authorization header is generated without error - # and confirm that the request body is not encoded - post_request = requests.Request( - "POST", - "http://foo:5555/get", - data={"foo": "bar"}, - headers={ - "Date": formatdate(timeval=1588628873, localtime=False, usegmt=True) - }, - ) - prepared_post = connection.session.prepare_request(post_request) - self.assertTrue("authorization" in prepared_post.headers) - self.assertTrue( - prepared_post.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_post.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_post.headers["X-User-Email"] == connection.email) - self.assertFalse(isinstance(prepared_post.body, bytes)) - - def test_bearer_token(self): - """Verify that the authorization header is set when a bearer token is provided""" - - bearer_token = ( - "Bearer eyJraWQiOiJWcmVsOE9zZ0JXaUpHeEpMeFJ4bE1UaVwvbjgyc1hwWktUaTd2UExUNFQ0T" - "T0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2IiwidG9r" - "ZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJ3ZWJcL2dldCB3ZWJcL3Bvc3QiLCJhdXRoX3RpbWUi" - "OjE1OTM3MjM1NDgsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9u" - "YXdzLmNvbVwvdXMtZWFzdC0xX1d6aEZzTGlPRyIsImV4cCI6MTU5MzcyNzE0OCwiaWF0IjoxNTkz" - "NzIzNTQ4LCJ2ZXJzaW9uIjoyLCJqdGkiOiI4Njk5ZDEwYy05Mjg4LTQ0YmEtODIxNi01OTJjZGU3" - "MDBhY2MiLCJjbGllbnRfaWQiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2In0.YA_yiD-x6UuB" - "MShprUbUKuB_DO6ogCtd5srfgpJA6Ve_qsf8n19nVMmFsZBy3GxzN92P1ZXiFY99FfNPohhQtaRR" - "hpeUkir08hgJN2bEHCJ5Ym8r9mr9mlwSG6FoiedgLaUVGwJujD9c2rcA83NEo8ayTyfCynF2AZ2p" - "MxLHvqOYtvscGMiMzIwlZfJV301iKUVgPODJM5lpJ4iKCpOy2ByCl2_KL1uxIxgMkglpB-i7kgJc" - "-WmYoJFoN88D89ugnEoAxNfK14N4_RyEkrLNGape9kew79nUeR6fWbVFLiGDDu25_9z-7VB-GGGk" - "7L_Hb7YgVJ5W2FwESnkDvV1T4Q" - ) - - connection = transcriptic.Connection( - email="somebody@transcriptic.com", - bearer_token=bearer_token, - organization_id="transcriptic", - api_root="http://foo:5555", - user_id="ufoo2", - ) - - get_request = requests.Request("GET", "http://foo:5555/get") - prepared_get = connection.session.prepare_request(get_request) - - authorization_header_value = prepared_get.headers["authorization"] - self.assertEqual(bearer_token, authorization_header_value) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_get.headers["X-User-Email"] == connection.email) - - def test_bearer_token_not_set_when_calling_non_api_root_endpoint(self): - """Verify that the authorization header is NOT set when non-API root is called""" - - bearer_token = ( - "Bearer eyJraWQiOiJWcmVsOE9zZ0JXaUpHeEpMeFJ4bE1UaVwvbjgyc1hwWktUaTd2UExUNFQ0T" - "T0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2IiwidG9r" - "ZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJ3ZWJcL2dldCB3ZWJcL3Bvc3QiLCJhdXRoX3RpbWUi" - "OjE1OTM3MjM1NDgsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9u" - "YXdzLmNvbVwvdXMtZWFzdC0xX1d6aEZzTGlPRyIsImV4cCI6MTU5MzcyNzE0OCwiaWF0IjoxNTkz" - "NzIzNTQ4LCJ2ZXJzaW9uIjoyLCJqdGkiOiI4Njk5ZDEwYy05Mjg4LTQ0YmEtODIxNi01OTJjZGU3" - "MDBhY2MiLCJjbGllbnRfaWQiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2In0.YA_yiD-x6UuB" - "MShprUbUKuB_DO6ogCtd5srfgpJA6Ve_qsf8n19nVMmFsZBy3GxzN92P1ZXiFY99FfNPohhQtaRR" - "hpeUkir08hgJN2bEHCJ5Ym8r9mr9mlwSG6FoiedgLaUVGwJujD9c2rcA83NEo8ayTyfCynF2AZ2p" - "MxLHvqOYtvscGMiMzIwlZfJV301iKUVgPODJM5lpJ4iKCpOy2ByCl2_KL1uxIxgMkglpB-i7kgJc" - "-WmYoJFoN88D89ugnEoAxNfK14N4_RyEkrLNGape9kew79nUeR6fWbVFLiGDDu25_9z-7VB-GGGk" - "7L_Hb7YgVJ5W2FwESnkDvV1T4Q" - ) - - connection = transcriptic.Connection( - email="somebody@transcriptic.com", - bearer_token=bearer_token, - organization_id="transcriptic", - api_root="http://foo:5555", - user_id="ufoo2", - ) - - get_request = requests.Request("GET", "http://bar:5555/get") - prepared_get = connection.session.prepare_request(get_request) - self.assertFalse("authorization" in prepared_get.headers) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_get.headers["X-User-Email"] == connection.email) - - def test_malformed_bearer_token(self): - """Verify that an exception is thrown when a malformed JWT bearer token is provided""" - - bearer_token = "Bearer myBigBadBearerToken" - - with pytest.raises(ValueError, match="Malformed JWT Bearer Token"): - transcriptic.Connection( - email="somebody@transcriptic.com", - bearer_token=bearer_token, - organization_id="transcriptic", - api_root="http://foo:5555", - user_id="ufoo2", - ) - - def test_bearer_token_supersedes_user_token(self): - """Verify that the user token and bearer token are mutually exclusive and that - bearer token supersedes user token""" - - user_token = "userTokenFoo" - bearer_token = ( - "Bearer eyJraWQiOiJWcmVsOE9zZ0JXaUpHeEpMeFJ4bE1UaVwvbjgyc1hwWktUaTd2UExUNFQ0T" - "T0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2IiwidG9r" - "ZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJ3ZWJcL2dldCB3ZWJcL3Bvc3QiLCJhdXRoX3RpbWUi" - "OjE1OTM3MjM1NDgsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9u" - "YXdzLmNvbVwvdXMtZWFzdC0xX1d6aEZzTGlPRyIsImV4cCI6MTU5MzcyNzE0OCwiaWF0IjoxNTkz" - "NzIzNTQ4LCJ2ZXJzaW9uIjoyLCJqdGkiOiI4Njk5ZDEwYy05Mjg4LTQ0YmEtODIxNi01OTJjZGU3" - "MDBhY2MiLCJjbGllbnRfaWQiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2In0.YA_yiD-x6UuB" - "MShprUbUKuB_DO6ogCtd5srfgpJA6Ve_qsf8n19nVMmFsZBy3GxzN92P1ZXiFY99FfNPohhQtaRR" - "hpeUkir08hgJN2bEHCJ5Ym8r9mr9mlwSG6FoiedgLaUVGwJujD9c2rcA83NEo8ayTyfCynF2AZ2p" - "MxLHvqOYtvscGMiMzIwlZfJV301iKUVgPODJM5lpJ4iKCpOy2ByCl2_KL1uxIxgMkglpB-i7kgJc" - "-WmYoJFoN88D89ugnEoAxNfK14N4_RyEkrLNGape9kew79nUeR6fWbVFLiGDDu25_9z-7VB-GGGk" - "7L_Hb7YgVJ5W2FwESnkDvV1T4Q" - ) - - with tempfile.NamedTemporaryFile() as config_file: - with open(config_file.name, "w") as f: - json.dump( - { - "email": "somebody@transcriptic.com", - "token": user_token, - "bearer_token": bearer_token, - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": [ - "can_submit_autoprotocol", - "can_upload_packages", - ], - }, - f, - ) - connection = transcriptic.config.Connection.from_file(config_file.name) - - get_request = requests.Request("GET", "http://foo:5555/get") - prepared_get = connection.session.prepare_request(get_request) - - self.assertTrue("authorization" in prepared_get.headers) - self.assertTrue("x-user-token" not in prepared_get.headers) - self.assertTrue( - connection.session.auth.token == prepared_get.headers["authorization"] - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.organization_id - ) - self.assertTrue( - prepared_get.headers["X-Organization-Id"] == connection.env_args["org_id"] - ) - self.assertTrue(prepared_get.headers["X-User-Email"] == connection.email) diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index cab9073e..00000000 --- a/test/conftest.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import sys - -import pytest - -from .helpers.mockAPI import MockResponse -from .helpers.util import load_protocol - - -sys.path.append(os.path.join(os.path.dirname(__file__), "helpers")) - - -# TODO: Migrate tests utilizing this to use the `TestConnection` pattern instead. -@pytest.fixture() -def test_api(monkeypatch): - from transcriptic.config import Connection - - from .helpers.mockAPI import _req_call as mockCall - - api = Connection(email="mock@api.com", organization_id="mock", api_root="mock-api") - monkeypatch.setattr(api, "_req_call", mockCall) - return api - - -class JsonDB: - """Contains dictionary of json objects used for testing purposes""" - - def __init__(self): - self.json_db = {} - - def __getitem__(self, name): - return self.json_db[name] - - def load(self, name=None, test_dir=None, path=None): - self.json_db[name] = load_protocol(name, test_dir, path) - - def reset(self): - self.json_db = {} - - -@pytest.fixture(scope="module") -def json_db(): - return JsonDB() - - -class ResponseDB: - """ - Contains dictionary of MockResponse used for testing purposes - TODO: Simplify this by using `@responses.activate` pattern - """ - - def __init__(self): - self.mock_responses = {} - - def __getitem__(self, name): - return self.mock_responses[name] - - def load(self, name, status_code=None, json=None, text=None): - self.mock_responses[name] = MockResponse(status_code, json, text) - - def reset(self): - self.mock_responses = {} - - -@pytest.fixture(scope="module") -def response_db(): - return ResponseDB() - - -@pytest.fixture(scope="module") -def test_connection(): - from transcriptic.config import Connection - - return Connection( - email="mock@api.com", organization_id="mock", api_root="http://mock-api" - ) diff --git a/test/helpers/__init__.py b/test/helpers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/helpers/fixtures.py b/test/helpers/fixtures.py deleted file mode 100644 index 5eb96027..00000000 --- a/test/helpers/fixtures.py +++ /dev/null @@ -1,45 +0,0 @@ -import json - -import pytest - -from click.testing import CliRunner -from Crypto.PublicKey import RSA - - -@pytest.fixture(scope="session") -def temp_tx_dotfile(tmpdir_factory): - path = tmpdir_factory.mktemp("config").join(".transcriptic") - config = { - "email": "somebody@transcriptic.com", - "token": "foobarinvalid", - "organization_id": "transcriptic", - "api_root": "http://foo:5555", - "analytics": True, - "user_id": "ufoo2", - "feature_groups": ["can_submit_autoprotocol", "can_upload_packages"], - } - with open(str(path), "w") as f: - json.dump(config, f) - return path - - -@pytest.fixture -def cli_test_runner(temp_tx_dotfile): - runner = CliRunner( - env={"TRANSCRIPTIC_CONFIG": str(temp_tx_dotfile)}, mix_stderr=False - ) - yield runner - - -@pytest.fixture() -def temp_ssh_key(tmpdir_factory): - dir_path = tmpdir_factory.mktemp("ssh") - private_key_path = str(dir_path.join("private.pem")) - public_key_path = str(dir_path.join("public.pem")) - private_key = RSA.generate(2048) - with open(private_key_path, "wb") as private_key_file: - private_key_file.write(private_key.exportKey("PEM")) - pubkey = private_key.publickey() - with open(public_key_path, "wb") as public_key_file: - public_key_file.write(pubkey.exportKey("OpenSSH")) - yield private_key_path, public_key_path diff --git a/test/helpers/mockAPI.py b/test/helpers/mockAPI.py deleted file mode 100644 index b04ca630..00000000 --- a/test/helpers/mockAPI.py +++ /dev/null @@ -1,58 +0,0 @@ -from builtins import object -from collections import deque - - -"""Keys in mockDB correspond to (method, route) calls""" -mockDB = dict() - - -class MockResponse(object): - """Mocks requests.Response""" - - def __init__(self, status_code=None, json=None, text=None): - self._status_code = status_code - self._text = text - self._json = json - - @property - def status_code(self): - return self._status_code - - @property - def text(self): - return self._text - - def json(self): - return self._json - - -def _req_call(method, route, **kwargs): - key = (method, route) - if key in mockDB: - if len(mockDB[key]["call_queue"]) == 0: - if mockDB[key]["default"]: - return mockDB[key]["default"] - else: - raise RuntimeError( - "Method: {method}, Route: {route} has run out of max calls." - ) - else: - return mockDB[key]["call_queue"].popleft() - else: - raise RuntimeError( - "Method: {method} and Route: {route} needs to be mocked.".format(**locals()) - ) - - -def mockRoute(method, route, response, max_calls=None): - if not isinstance(response, MockResponse): - raise TypeError("Response needs to be a MockResponse object.") - key = (method, route) - if key not in mockDB: - mockDB[key] = {} - mockDB[key]["default"] = None - mockDB[key]["call_queue"] = deque() - if max_calls: - mockDB[key]["call_queue"].extend([response] * max_calls) - else: - mockDB[key]["default"] = response diff --git a/test/helpers/protocol_response.py b/test/helpers/protocol_response.py deleted file mode 100644 index 26b1de38..00000000 --- a/test/helpers/protocol_response.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import print_function - -import json -import os - -import requests - - -def protocol_response(method, protocol_path=None, response_path=None, **kwargs): - """ - Helper function for getting protocol response and dumping json to response path - Caveat Emptor: Does not do any additional checks on the response object, just dumps to json if possible - """ - from transcriptic import api - - if not api: - from transcriptic.config import Connection - - api = Connection.from_file("~/.transcriptic") - protocol = json.loads(open(protocol_path).read()) - - response = requests.post( - api.get_route(method), - headers=api.session.headers, - data=json.dumps({"protocol": protocol}), - ) - with open(response_path, "w") as out_file: - json.dump(response.json(), out_file, indent=2) - - -if __name__ == "__main__": - # Using optparse, to support Python 2 - from optparse import OptionParser - - usage = "usage: %prog [options] method -i 'myProtocol.json'" - parser = OptionParser(usage=usage) - parser.add_option( - "-i", - "--input", - action="store", - type="str", - dest="protocol_path", - help="Path to protocol.json (required)", - default=None, - ) - parser.add_option( - "-o", - "--output", - action="store", - type="str", - dest="response_path", - help="output path for response json. If not specified, goes to protocol_response.json", - default=None, - ) - (options, args) = parser.parse_args() - - if len(args) != 1: - raise RuntimeError("Please provide a method argument. Example: analyze_run") - - if not options.protocol_path: - raise RuntimeError("The input path to the protocol is required") - if not os.path.isfile(options.protocol_path): - raise RuntimeError(f"{options.protocol_path} is an invalid protocol path") - - if not options.response_path: - options.response_path = ( - options.protocol_path.split(".json")[0] + "_response.json" - ) - try: - protocol_response(args[0], options.protocol_path, options.response_path) - print(f"File succesfully generated: {options.response_path}") - except Exception as e: - print(f"Ran into {e} when generating file.") diff --git a/test/helpers/util.py b/test/helpers/util.py deleted file mode 100644 index 19d60a90..00000000 --- a/test/helpers/util.py +++ /dev/null @@ -1,10 +0,0 @@ -import json - - -def load_protocol(name=None, test_dir=None, path=None): - if not test_dir: - test_dir = "test/autoprotocol/" - if not path: - return json.loads(open(test_dir + name + ".json").read()) - else: - return json.loads(open(path).read()) diff --git a/test/resources/dispense.json b/test/resources/dispense.json deleted file mode 100644 index 97411f21..00000000 --- a/test/resources/dispense.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "refs": { - "sample_plate5": { - "new": "96-flat", - "store": { - "where": "warm_37" - } - } - }, - "instructions": [ - { - "reagent": "water", - "object": "sample_plate5", - "columns": [ - { - "column": 0, - "volume": "100:microliter" - }, - { - "column": 1, - "volume": "100:microliter" - }, - { - "column": 2, - "volume": "100:microliter" - }, - { - "column": 3, - "volume": "100:microliter" - }, - { - "column": 4, - "volume": "100:microliter" - }, - { - "column": 5, - "volume": "100:microliter" - }, - { - "column": 6, - "volume": "100:microliter" - }, - { - "column": 7, - "volume": "100:microliter" - }, - { - "column": 8, - "volume": "100:microliter" - }, - { - "column": 9, - "volume": "100:microliter" - }, - { - "column": 10, - "volume": "100:microliter" - }, - { - "column": 11, - "volume": "100:microliter" - } - ], - "op": "dispense" - }, - { - "reagent": "water", - "object": "sample_plate5", - "columns": [ - { - "column": 0, - "volume": "10:microliter" - }, - { - "column": 1, - "volume": "20:microliter" - }, - { - "column": 2, - "volume": "30:microliter" - }, - { - "column": 3, - "volume": "40:microliter" - }, - { - "column": 4, - "volume": "50:microliter" - }, - { - "column": 5, - "volume": "60:microliter" - }, - { - "column": 6, - "volume": "70:microliter" - }, - { - "column": 7, - "volume": "80:microliter" - }, - { - "column": 8, - "volume": "90:microliter" - }, - { - "column": 9, - "volume": "100:microliter" - }, - { - "column": 10, - "volume": "110:microliter" - }, - { - "column": 11, - "volume": "120:microliter" - } - ], - "op": "dispense" - }, - { - "object": "sample_plate5", - "op": "dispense", - "columns": [ - { - "column": 0, - "volume": "50:microliter" - }, - { - "column": 1, - "volume": "50:microliter" - }, - { - "column": 2, - "volume": "50:microliter" - }, - { - "column": 3, - "volume": "50:microliter" - }, - { - "column": 4, - "volume": "50:microliter" - }, - { - "column": 5, - "volume": "50:microliter" - }, - { - "column": 6, - "volume": "50:microliter" - }, - { - "column": 7, - "volume": "50:microliter" - }, - { - "column": 8, - "volume": "50:microliter" - }, - { - "column": 9, - "volume": "50:microliter" - }, - { - "column": 10, - "volume": "50:microliter" - }, - { - "column": 11, - "volume": "50:microliter" - } - ], - "resource_id": "rs17gmh5wafm5p" - } - ] -} diff --git a/test/resources/flow.json b/test/resources/flow.json deleted file mode 100644 index 37062f94..00000000 --- a/test/resources/flow.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "refs": { - "test_plate": { - "new": "96-pcr", - "discard": true - } - }, - "instructions": [ - { - "dataref": "test_ref", - "channels": { - "FSC": { - "area": true, - "voltage_range": { - "high": "280:volt", - "low": "230:volt" - }, - "weight": false, - "height": true - }, - "SSC": { - "area": true, - "voltage_range": { - "high": "280:volt", - "low": "230:volt" - }, - "weight": false, - "height": true - } - }, - "op": "flow_analyze", - "samples": [ - { - "volume": "100:microliter", - "captured_events": 9, - "well": "test_plate/0" - } - ], - "negative_controls": [ - { - "volume": "100:microliter", - "captured_events": 5, - "well": "test_plate/0", - "channel": [ - { - "area": true, - "voltage_range": { - "high": "280:volt", - "low": "230:volt" - }, - "weight": false, - "height": true - }, - { - "area": true, - "voltage_range": { - "high": "280:volt", - "low": "230:volt" - }, - "weight": false, - "height": true - } - ] - } - ] - } - ] -} diff --git a/test/resources/illumina.json b/test/resources/illumina.json deleted file mode 100644 index 0b1c5f83..00000000 --- a/test/resources/illumina.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "refs": { - "test_plate6": { - "new": "96-pcr", - "discard": true - } - }, - "instructions": [ - { - "dataref": "dataref", - "index": "none", - "lanes": [ - { - "object": "test_plate6/0", - "library_concentration": 1.0 - }, - { - "object": "test_plate6/1", - "library_concentration": 2 - } - ], - "flowcell": "PE", - "mode": "mid", - "sequencer": "nextseq", - "library_size": 34, - "op": "illumina_sequence" - } - ] -} diff --git a/test/resources/mag_collect.json b/test/resources/mag_collect.json deleted file mode 100644 index c659b1e1..00000000 --- a/test/resources/mag_collect.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "refs": { - "pcr_6": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_5": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_4": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_3": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_2": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_1": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_0": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "test": { - "new": "96-flat", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - [ - { - "release": { - "duration": "30.0:second", - "object": "pcr_0", - "frequency": "1.0:hertz", - "center": 0.05, - "amplitude": 0 - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - }, - { - "groups": [ - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/7" - }, - { - "volume": "30.0:microliter", - "well": "test/8" - }, - { - "volume": "30.0:microliter", - "well": "test/9" - } - ], - "from": "test/1", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/10" - } - ], - "from": "test/2", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "5.0:microliter", - "well": "test/1" - } - ], - "from": "test/0", - "allow_carryover": false - } - } - ], - "op": "pipette" - }, - { - "groups": [ - [ - { - "collect": { - "bottom_position": 0.05, - "object": "pcr_0", - "cycles": 5, - "pause_duration": "30.0:second" - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - } - ] -} diff --git a/test/resources/mag_dry.json b/test/resources/mag_dry.json deleted file mode 100644 index 603aa10b..00000000 --- a/test/resources/mag_dry.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "refs": { - "pcr_6": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_5": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_4": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_3": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_2": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_1": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_0": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "test": { - "new": "96-flat", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - [ - { - "release": { - "duration": "30.0:second", - "object": "pcr_0", - "frequency": "1.0:hertz", - "center": 0.05, - "amplitude": 0 - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - }, - { - "groups": [ - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/7" - }, - { - "volume": "30.0:microliter", - "well": "test/8" - }, - { - "volume": "30.0:microliter", - "well": "test/9" - } - ], - "from": "test/1", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/10" - } - ], - "from": "test/2", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "5.0:microliter", - "well": "test/1" - } - ], - "from": "test/0", - "allow_carryover": false - } - } - ], - "op": "pipette" - }, - { - "groups": [ - [ - { - "dry": { - "duration": "30.0:minute", - "object": "pcr_0" - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - } - ] -} diff --git a/test/resources/mag_incubate.json b/test/resources/mag_incubate.json deleted file mode 100644 index 61cf98f6..00000000 --- a/test/resources/mag_incubate.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "refs": { - "pcr_6": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_5": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_4": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_3": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_2": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_1": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_0": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "test": { - "new": "96-flat", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - [ - { - "release": { - "duration": "30.0:second", - "object": "pcr_0", - "frequency": "1.0:hertz", - "center": 0.05, - "amplitude": 0 - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - }, - { - "groups": [ - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/7" - }, - { - "volume": "30.0:microliter", - "well": "test/8" - }, - { - "volume": "30.0:microliter", - "well": "test/9" - } - ], - "from": "test/1", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/10" - } - ], - "from": "test/2", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "5.0:microliter", - "well": "test/1" - } - ], - "from": "test/0", - "allow_carryover": false - } - } - ], - "op": "pipette" - }, - { - "groups": [ - [ - { - "incubate": { - "duration": "30.0:minute", - "tip_position": 1.5, - "object": "pcr_0", - "magnetize": false, - "temperature": "30.0:celsius" - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - } - ] -} diff --git a/test/resources/mag_mix.json b/test/resources/mag_mix.json deleted file mode 100644 index 056e71b8..00000000 --- a/test/resources/mag_mix.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "refs": { - "pcr_6": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_5": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_4": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_3": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_2": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_1": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "pcr_0": { - "new": "96-pcr", - "store": { - "where": "cold_20" - } - }, - "test": { - "new": "96-flat", - "discard": true - } - }, - "instructions": [ - { - "groups": [ - [ - { - "release": { - "duration": "30.0:second", - "object": "pcr_0", - "frequency": "1.0:hertz", - "center": 0.05, - "amplitude": 0 - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - }, - { - "groups": [ - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/7" - }, - { - "volume": "30.0:microliter", - "well": "test/8" - }, - { - "volume": "30.0:microliter", - "well": "test/9" - } - ], - "from": "test/1", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "30.0:microliter", - "well": "test/10" - } - ], - "from": "test/2", - "allow_carryover": true - } - }, - { - "distribute": { - "to": [ - { - "volume": "5.0:microliter", - "well": "test/1" - } - ], - "from": "test/0", - "allow_carryover": false - } - } - ], - "op": "pipette" - }, - { - "groups": [ - [ - { - "mix": { - "center": 1.0, - "object": "pcr_0", - "frequency": "60.0:hertz", - "amplitude": 0, - "duration": "30.0:second", - "magnetize": false - } - } - ] - ], - "magnetic_head": "96-pcr", - "op": "magnetic_transfer" - } - ] -} diff --git a/test/resources/measure_suite.json b/test/resources/measure_suite.json deleted file mode 100644 index 115d053b..00000000 --- a/test/resources/measure_suite.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "refs": { - "test_plate2": { - "new": "96-flat", - "discard": true - }, - "test_plate": { - "new": "96-flat", - "store": { - "where": "cold_4" - } - } - }, - "instructions": [ - { - "volume": "2.0:microliter", - "dataref": "mc_test", - "object": [ - "test_plate2/0", - "test_plate2/1", - "test_plate2/2", - "test_plate2/3", - "test_plate2/4", - "test_plate2/5", - "test_plate2/6", - "test_plate2/7", - "test_plate2/8", - "test_plate2/9", - "test_plate2/10", - "test_plate2/11", - "test_plate2/12", - "test_plate2/13", - "test_plate2/14", - "test_plate2/15", - "test_plate2/16", - "test_plate2/17", - "test_plate2/18", - "test_plate2/19", - "test_plate2/20", - "test_plate2/21", - "test_plate2/22", - "test_plate2/23", - "test_plate2/24", - "test_plate2/25", - "test_plate2/26", - "test_plate2/27", - "test_plate2/28", - "test_plate2/29", - "test_plate2/30", - "test_plate2/31", - "test_plate2/32", - "test_plate2/33", - "test_plate2/34", - "test_plate2/35", - "test_plate2/36", - "test_plate2/37", - "test_plate2/38", - "test_plate2/39", - "test_plate2/40", - "test_plate2/41", - "test_plate2/42", - "test_plate2/43", - "test_plate2/44", - "test_plate2/45", - "test_plate2/46", - "test_plate2/47", - "test_plate2/48", - "test_plate2/49", - "test_plate2/50", - "test_plate2/51", - "test_plate2/52", - "test_plate2/53", - "test_plate2/54", - "test_plate2/55", - "test_plate2/56", - "test_plate2/57", - "test_plate2/58", - "test_plate2/59", - "test_plate2/60", - "test_plate2/61", - "test_plate2/62", - "test_plate2/63", - "test_plate2/64", - "test_plate2/65", - "test_plate2/66", - "test_plate2/67", - "test_plate2/68", - "test_plate2/69", - "test_plate2/70", - "test_plate2/71", - "test_plate2/72", - "test_plate2/73", - "test_plate2/74", - "test_plate2/75", - "test_plate2/76", - "test_plate2/77", - "test_plate2/78", - "test_plate2/79", - "test_plate2/80", - "test_plate2/81", - "test_plate2/82", - "test_plate2/83", - "test_plate2/84", - "test_plate2/85", - "test_plate2/86", - "test_plate2/87", - "test_plate2/88", - "test_plate2/89", - "test_plate2/90", - "test_plate2/91", - "test_plate2/92", - "test_plate2/93", - "test_plate2/94", - "test_plate2/95" - ], - "op": "measure_concentration", - "measurement": "DNA" - }, - { - "dataref": "test_ref", - "object": [ - "test_plate2" - ], - "op": "measure_mass" - }, - { - "dataref": "test_ref", - "object": [ - "test_plate/0", - "test_plate/1", - "test_plate/2", - "test_plate/3", - "test_plate/4", - "test_plate/5", - "test_plate/6", - "test_plate/7", - "test_plate/8", - "test_plate/9", - "test_plate/10", - "test_plate/11" - ], - "op": "measure_volume" - }, - { - "dataref": "test_ref2", - "object": [ - "test_plate2/1", - "test_plate2/2", - "test_plate2/3", - "test_plate2/4", - "test_plate2/5", - "test_plate2/6", - "test_plate2/7", - "test_plate2/8" - ], - "op": "measure_volume" - } - ] -} diff --git a/test/resources/purify.json b/test/resources/purify.json deleted file mode 100644 index 406eca40..00000000 --- a/test/resources/purify.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "refs": { - "sample_wells": { - "new": "96-pcr", - "discard": true - }, - "extract_Well(Container(sample_wells), 13, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 0, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 14, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 12, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 17, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 4, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 9, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 6, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 19, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 8, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 1, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 15, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 18, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 5, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 16, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 11, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 3, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 10, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 7, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - }, - "extract_Well(Container(sample_wells), 2, None)": { - "new": "micro-1.5", - "store": { - "where": "cold_4" - } - } - }, - "instructions": [ - { - "dataref": "gel_purify_test_0", - "matrix": "size_select(8,0.8%)", - "ladder": "ladder1", - "volume": "10.0:microliter", - "objects": [ - "sample_wells/0", - "sample_wells/1", - "sample_wells/2", - "sample_wells/3", - "sample_wells/4", - "sample_wells/5", - "sample_wells/6", - "sample_wells/7" - ], - "extract": [ - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 0, None)/0", - "lane": 0, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 1, None)/0", - "lane": 1, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 2, None)/0", - "lane": 2, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 3, None)/0", - "lane": 3, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 4, None)/0", - "lane": 4, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 5, None)/0", - "lane": 5, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 6, None)/0", - "lane": 6, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 7, None)/0", - "lane": 7, - "elution_buffer": "water" - } - ], - "op": "gel_purify" - }, - { - "dataref": "gel_purify_test_1", - "matrix": "size_select(8,0.8%)", - "ladder": "ladder1", - "volume": "10.0:microliter", - "objects": [ - "sample_wells/8", - "sample_wells/9", - "sample_wells/10", - "sample_wells/11", - "sample_wells/12", - "sample_wells/13", - "sample_wells/14", - "sample_wells/15" - ], - "extract": [ - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 8, None)/0", - "lane": 0, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 9, None)/0", - "lane": 1, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 10, None)/0", - "lane": 2, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 11, None)/0", - "lane": 3, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 12, None)/0", - "lane": 4, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 13, None)/0", - "lane": 5, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 14, None)/0", - "lane": 6, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 15, None)/0", - "lane": 7, - "elution_buffer": "water" - } - ], - "op": "gel_purify" - }, - { - "dataref": "gel_purify_test_2", - "matrix": "size_select(8,0.8%)", - "ladder": "ladder1", - "volume": "10.0:microliter", - "objects": [ - "sample_wells/16", - "sample_wells/17", - "sample_wells/18", - "sample_wells/19" - ], - "extract": [ - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 16, None)/0", - "lane": 0, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 17, None)/0", - "lane": 1, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 18, None)/0", - "lane": 2, - "elution_buffer": "water" - }, - { - "elution_volume": "5.0:microliter", - "band_size_range": { - "max_bp": 10, - "min_bp": 0 - }, - "destination": "extract_Well(Container(sample_wells), 19, None)/0", - "lane": 3, - "elution_buffer": "water" - } - ], - "op": "gel_purify" - } - ] -} diff --git a/test/resources/web_example.json b/test/resources/web_example.json deleted file mode 100644 index d63f6ea1..00000000 --- a/test/resources/web_example.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "refs": { - "bacteria": { - "new": "micro-1.5", - "discard": true - }, - "test_plate": { - "new": "96-flat", - "store": { - "where": "cold_4" - } - } - }, - "instructions": [ - { - "reagent": "lb-broth-noAB", - "object": "test_plate", - "columns": [ - { - "column": 0, - "volume": "50:microliter" - }, - { - "column": 1, - "volume": "50:microliter" - }, - { - "column": 2, - "volume": "50:microliter" - }, - { - "column": 3, - "volume": "50:microliter" - }, - { - "column": 4, - "volume": "50:microliter" - }, - { - "column": 5, - "volume": "50:microliter" - }, - { - "column": 6, - "volume": "50:microliter" - }, - { - "column": 7, - "volume": "50:microliter" - }, - { - "column": 8, - "volume": "50:microliter" - }, - { - "column": 9, - "volume": "50:microliter" - }, - { - "column": 10, - "volume": "50:microliter" - }, - { - "column": 11, - "volume": "50:microliter" - } - ], - "op": "dispense" - }, - { - "groups": [ - { - "transfer": [ - { - "volume": "1.0:microliter", - "to": "test_plate/0", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "3.0:microliter", - "to": "test_plate/1", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "5.0:microliter", - "to": "test_plate/2", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "7.0:microliter", - "to": "test_plate/3", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "9.0:microliter", - "to": "test_plate/4", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "11.0:microliter", - "to": "test_plate/5", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "13.0:microliter", - "to": "test_plate/6", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "15.0:microliter", - "to": "test_plate/7", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "17.0:microliter", - "to": "test_plate/8", - "from": "bacteria/0" - } - ] - }, - { - "transfer": [ - { - "volume": "19.0:microliter", - "to": "test_plate/9", - "from": "bacteria/0" - } - ] - } - ], - "op": "pipette" - } - ] -} diff --git a/test/sampledata/connection_test.py b/test/sampledata/connection_test.py deleted file mode 100644 index 51203b9d..00000000 --- a/test/sampledata/connection_test.py +++ /dev/null @@ -1,183 +0,0 @@ -import pandas as pd -import pytest -import requests - -from transcriptic.sampledata import load_sample_container -from transcriptic.sampledata.connection import MockConnection -from transcriptic.sampledata.container import sample_container_attr -from transcriptic.sampledata.dataset import ( - ABSORBANCE_DATASETS, - load_sample_dataset, - sample_dataset_attr, -) -from transcriptic.sampledata.project import load_sample_project, sample_project_attr -from transcriptic.sampledata.run import load_sample_run, sample_run_attr -from transcriptic.util import load_sampledata_json - - -class TestMockConnection: - def test_defaults(self): - assert MockConnection().organization_id == "sample-org" - - def test_not_registered(self): - with pytest.raises( - requests.exceptions.ConnectionError, match="Mocked route not implemented" - ): - MockConnection().project(project_id="invalid-project") - with pytest.raises( - requests.exceptions.ConnectionError, - match="Connection refused by Responses - the call doesn't " - "match any registered mock.", - ): - MockConnection(verbose=True).project(project_id="invalid-project") - - def test_registered_responses(self): - mock_connection = MockConnection() - assert mock_connection.project(project_id="p123") == sample_project_attr - assert ( - mock_connection._get_object(obj_id="p123", obj_type="project") - == sample_project_attr - ) - assert ( - mock_connection.projects() - == load_sampledata_json("sample-org-projects.json")["projects"] - ) - assert mock_connection.runs(project_id="p123") == [ - {**data["attributes"], "id": data["id"]} - for data in load_sampledata_json("p123-runs.json")["data"] - ] - assert mock_connection._get_object(obj_id="r123") == sample_run_attr - - def test_jupyter_project(self): - from transcriptic import Project - - mock_connection = MockConnection() - - with pytest.raises( - TypeError, match="invalid-id is not found in your projects." - ): - Project("invalid-id") - - project = Project("p123") - assert project.id == "p123" - assert project.name == "sample project" - assert project.connection == mock_connection - assert project.attributes == sample_project_attr - - project_runs = project.runs() - assert type(project_runs) == pd.DataFrame - assert len(project_runs) == 1 - assert project_runs.loc[0].id == "r123" - assert project_runs.loc[0].Name == "Sample Run" - - def test_jupyter_run(self): - from transcriptic import Run - - mock_connection = MockConnection() - - with pytest.raises( - requests.exceptions.ConnectionError, match="Mocked route not implemented" - ): - Run("invalid-id") - - run = Run("r123") - assert run.id == "r123" - assert run.connection == mock_connection - assert run.attributes == sample_run_attr - - containers = run.containers - assert len(containers) == 2 - assert containers.loc[0].ContainerId == "ct123" - assert containers.loc[1].ContainerId == "ct124" - - instructions = run.instructions - assert len(instructions) == 2 - assert instructions.loc[0].Id == "i123" - assert instructions.loc[1].Id == "i124" - - # all instructions have a `generated_container` attribute, if none generated, it is empty list - expected_generated_containers = [ - {"id": "ct123", "label": "container_generated"} - ] - assert ( - instructions.loc[0].Instructions.generated_containers - == expected_generated_containers - ) - assert instructions.loc[1].Instructions.generated_containers == [] - - data = run.data - assert len(data) == 1 - assert data.loc[0].Name == "OD600" - - datasets = run.Datasets - assert len(datasets) == 1 - - def test_jupyter_container(self): - from transcriptic import Container - - mock_connection = MockConnection() - - with pytest.raises( - requests.exceptions.ConnectionError, match="Mocked route not implemented" - ): - Container("invalid-id") - - container = Container("ct123") - assert container.id == "ct123" - assert container.connection == mock_connection - assert container.attributes == sample_container_attr - - def test_jupyter_dataset(self): - from transcriptic import Dataset - - mock_connection = MockConnection() - - with pytest.raises( - requests.exceptions.ConnectionError, match="Mocked route not implemented" - ): - Dataset("invalid-id") - - dataset = Dataset("d123") - assert dataset.id == "d123" - assert dataset.connection == mock_connection - assert dataset.attributes == sample_dataset_attr - - assert dataset.analysis_tool is None - assert dataset.analysis_tool_version is None - assert dataset.attachments == {} - assert dataset.container.name == "VbottomPlate" - assert dataset.data_type == "platereader" - assert dataset.operation == "absorbance" - assert dataset.raw_data == { - "a1": [0.05], - "a2": [0.04], - "a3": [0.06], - "b1": [1.21], - "b2": [1.13], - "b3": [1.32], - "c1": [2.22], - "c2": [2.15], - "c3": [2.37], - } - assert len(dataset.data.columns) == 9 - - def test_load_sample_objects(self): - mock_connection = MockConnection() - - project = load_sample_project() - assert project.attributes == sample_project_attr - assert project.connection == mock_connection - - run = load_sample_run() - assert run.attributes == sample_run_attr - assert run.connection == mock_connection - - for dataset_id in ABSORBANCE_DATASETS: - dataset = load_sample_dataset(dataset_id) - assert dataset.attributes == load_sampledata_json(f"{dataset_id}.json") - assert dataset.connection == mock_connection - - for container_id in ["ct123", "ct124"]: - container = load_sample_container(container_id) - assert container.attributes == load_sampledata_json(f"{container_id}.json") - assert container.connection == mock_connection diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 715748a7..00000000 --- a/tox.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tox] -envlist = clean, py36, py37, py38, py39, stats, lint, docs - -[testenv] -commands = python setup.py test {posargs} -deps = .[test, jupyter, analysis] - -[testenv:clean] -commands = coverage erase - -[testenv:stats] -commands = coverage report -m --rcfile={toxinidir}/.coveragerc - -[testenv:stats_xml] -# Used in CI for generating xml for codecov -commands = coverage xml --rcfile={toxinidir}/.coveragerc - -[testenv:lint] -deps = .[test, docs, jupyter, analysis] -commands = pre-commit run --all-files - -[testenv:docs] -basepython = python -changedir = docs -deps = .[docs, analysis] -commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html diff --git a/transcriptic/__init__.py b/transcriptic/__init__.py deleted file mode 100644 index ff35d852..00000000 --- a/transcriptic/__init__.py +++ /dev/null @@ -1,193 +0,0 @@ -from .config import Connection -from .version import __version__ - - -api = None - -""" -Transcriptic -============ - -The Transcriptic library is separated into three components: -1) Core. The core modules provide a barebones client for making calls to -the Transcriptic webapp to create and obtain data. This can be done via the -`api` object or via the command-line using the CLI. -2) Jupyter. This module provides a Jupyter-centric means for interacting with -objects returned from the Transcriptic webapp such as Run, Project and Dataset. -3) Analysis. This module provides some basic analysis wrappers around datasets -returned from the webapp using standard Python scientific libraries. - -The __init__ file contains a bunch of entry functions to facilitate easy access -to the Jupyter library. -""" - -""" Allow direct import of Jupyter objects such as `Run` directly if the -required imports are present -""" -try: - from .commands import ProtocolPreview - from .jupyter import Container, Dataset, Project, Run -except ImportError as e: - pass - - -def run(run_id): - """ - Creates a Run object for the specified run-id - - Parameters - ---------- - run_id: str - Id of Run, e.g. r123456789 - Returns - ------ - Run object: Run - Transcriptic representation of a Run object - """ - from .jupyter import Run - - return Run(run_id) - - -def project(project_id): - """ - Creates a Project object for the specified project-id - - Parameters - ---------- - project_id: str - Id of Project, e.g. p123456789 - Returns - ------ - Project object: Project - Transcriptic representation of a Project object - """ - from .jupyter import Project - - return Project(project_id) - - -def container(container_id): - """ - Creates a Container object for the specified container-id - - Parameters - ---------- - container_id: str - Id of Container, e.g. ct123456789 - Returns - ------ - Container object: Container - Transcriptic representation of a Container object - """ - from .jupyter import Container - - return Container(container_id) - - -def preview(protocol): - """ - Creates a protocol preview object for the specified protocol - - Parameters - ---------- - protocol: dictionary - protocol JSON in dictionary format - Returns - ------ - Protocol object: ProtocolPreview - Transcriptic representation of a Protocol object - """ - from .commands import ProtocolPreview - - return ProtocolPreview(protocol, api=api) - - -def analyze(protocol, test_mode=False): - """ - Analyze a given protocol - - Parameters - ---------- - protocol: dict - Autoprotocol JSON in dictionary format - test_mode: boolean - whether protocol should be analyzed under test mode - Returns - ------ - Analysis result: dict - Raw result of the analysis - """ - return api.analyze_run(protocol, test_mode) - - -def submit(protocol, project_id, title=None, test_mode=False): - """ - Submit a given protocol - - Parameters - ---------- - protocol: dict - Autoprotocol JSON in dictionary format - project_id: str - Project to submit Autoprotocol to - title: str - Name of Run - test_mode: boolean - whether protocol should be submitted as a test run - Returns - ------ - Submission result: dict - Raw result of the submission - """ - return api.submit_run( - protocol, project_id=project_id, title=title, test_mode=test_mode - ) - - -def dataset(data_id, key="*"): - """ - Get dataset for a given data_id - - Parameters - ---------- - data_id - Id of desired dataset, e.g. d123456789 - key - Key of desired sub-fields of dataset - - Returns - ------ - Data: dict - Data in JSON form - """ - return api.dataset(data_id=data_id, key=key) - - -def connect(transcriptic_path="~/.transcriptic", mocked=False): - """ - Instantiates a Connection based on the specified path, and overwrites the - existing `api` object with this Connection - - Parameters - ---------- - transcriptic_path - Path to transcriptic dot-file - mocked - If specified, instantiates a mocked connection - """ - # TODO: Mirror login code from CLI - if mocked is True: - from transcriptic.sampledata.connection import MockConnection - - api = MockConnection() - return api - else: - try: - api = Connection.from_file(transcriptic_path) - return api - except (OSError, IOError): - print( - "Unable to find .transcriptic file, please ensure the right path" - " is provided" - ) diff --git a/transcriptic/analysis/__init__.py b/transcriptic/analysis/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/transcriptic/analysis/imaging.py b/transcriptic/analysis/imaging.py deleted file mode 100644 index 64ddec04..00000000 --- a/transcriptic/analysis/imaging.py +++ /dev/null @@ -1,71 +0,0 @@ -from io import BytesIO - -from transcriptic import api - - -try: - from PIL import Image -except ImportError: - raise ImportError( - "Please run `pip install transcriptic[analysis] if you " - "would like to use the Transcriptic analysis module." - ) - - -class ImagePlate(object): - """ - An ImagePlate object generalizes the parsing of datasets derived from the - plate camera for easy visualization. - - Parameters - ---------- - dataset: dataset - Single dataset selected from datasets object - - Attributes - ---------- - raw: BytesIO - Raw buffer of image bytes - image: PIL.Image - Image object as rendered by PIL - """ - - def __init__(self, dataset): - if dataset.attributes["instruction"]["operation"]["op"] != "image_plate": - raise RuntimeError("No image_plate operation found for given dataset.") - self.id = dataset.id - self.raw = BytesIO() - req = api.raw_image_data(data_id=self.id) - # Buffer download of data - chunk_sz = 512 - for chunk in req.iter_content(chunk_sz): - self.raw.write(chunk) - # Reset seek to 0 - self.raw.seek(0) - self.image = Image.open(self.raw) - - def display(self): - """ - Displays the original full-sized image. Helpful when used in an IPython - kernel - - Returns - ------- - HTML - Returns a HTML iframe of the full-size image which is rendered nicely in IPython (if IPython is present) - """ - try: - # pylint: disable=import-error - from IPython.display import HTML - - return HTML( - """""" - % api.get_route("view_raw_image", data_id=self.id) - ) - - except: - # If IPython module is not present or unable to show, display using - # default PIL image show - self.image.show() diff --git a/transcriptic/analysis/kinetics.py b/transcriptic/analysis/kinetics.py deleted file mode 100644 index 415ba796..00000000 --- a/transcriptic/analysis/kinetics.py +++ /dev/null @@ -1,237 +0,0 @@ -try: - import pandas as pd - import plotly as py - import plotly.graph_objs as go -except ImportError: - raise ImportError( - "Please run `pip install transcriptic[analysis] if you " - "would like to use the Transcriptic analysis module." - ) - - -class _Kinetics(object): - """ - A Kinetics object generalizes the parsing of a time series of datasets - Parameters - ---------- - datasets: List[dataset] - List of Datasets - """ - - def __init__(self, datasets): - self.datasets = datasets - self.readings = pd.concat([ds.data for ds in datasets]) - self.readings.index = pd.to_datetime( - [ds.attributes["warp"]["completed_at"] for ds in datasets] - ) - self.readings = self.readings.transpose() - - -class Spectrophotometry(_Kinetics): - """ - A Spectrophotomery object is used to analyze a kinetic series of PlateRead datasets - - Attributes - ---------- - properties: DataFrame - DataFrame of aliquot properties for each well, useful for groupby operations during plots - readings: DataFrame - DataFrame of readings for each well at different time points - operation: str - Operation used for generating these growth curves (e.g. Absorbance) - - """ - - def __init__(self, datasets): - """ - Parameters - ---------- - datasets: List[dataset] - List of Datasets objects. Currently restricted to those generated by 'absorbance', 'fluorescence' - and 'luminescence' operations - """ - operation_set = set([ds.operation for ds in datasets]) - if len(operation_set) > 1: - raise RuntimeError("Input Datasets must all be of the same type.") - self.operation = operation_set.pop() - if self.operation not in ["absorbance", "fluorescence", "luminescence"]: - raise RuntimeError( - f"{self.operation} has to be of type absorbance, " - f"fluorescence or luminescence" - ) - super(Spectrophotometry, self).__init__(datasets) - # Assume that well names are consistent across all runs - ref_dataset = datasets[0] - ref_container = ref_dataset.container - # Check if well_map is defined - if len(ref_container.well_map) != 0: - self.properties = pd.DataFrame.from_dict( - ref_container.well_map, orient="index" - ) - else: - self.properties = pd.DataFrame.from_dict( - { - ref_container.container_type.robotize(x): x - for x in ref_dataset.data.columns - if x not in ["GAIN"] - }, - orient="index", - ) - self.properties.columns = ["name"] - self.properties.insert( - 1, - "column", - (self.properties.index % ref_container.container_type.col_count), - ) - self.properties.insert( - 1, "row", (self.properties.index // ref_container.container_type.col_count) - ) - self.properties.row = self.properties.row.apply( - lambda x: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[x] - ) - self.properties.index = [ - ref_container.container_type.humanize(int(x)) - for x in list(self.properties.index) - ] - - def plot( - self, - wells="*", - groupby=None, - title=None, - xlabel=None, - ylabel=None, - max_legend_len=20, - ): - """ - This generates a plot of the kinetics curve. Note that this function is meant for use under a Jupyter notebook - environment - - Example Usage: - - .. code-block:: python - - from transcriptic.analysis.kinetics import Spectrophotometry - growth_curve = Spectrophotometry(myRun.data.Datasets) - growth_curve.plot(wells=["A1", "A2", "B1", "B2"]) - growth_curve.plot(wells=["A1", "A2", "B1", "B2"], groupby="row", title="Row Groups") - growth_curve.plot(wells=["A1", "A2", "B1", "B2"], groupby="name", ylabel="Absorbance Units") - growth_curve.plot(groupby="name", max_legend_len=40) - - Parameters - ---------- - wells: Optional[list or str] - If not specified, this plots all the wells associated with the Datasets given. Otherwise, specifiy - a list of well indices (["A1", "B1"]) or a specific well ("A1") - groupby: Optional[str] - When specified, this groups the wells with the same property value together. On the plot, each group will - be represented by a single curve with the mean values and error bars of 1 std. dev. away from the mean - title: Optional[str] - Plot title. Default: "Kinectics Curve (`run-id`)" - xlabel: Optional[str] - Plot x-axis label. Default: "Time" - ylabel: Optional[str] - Plot y-axis label. Default: "`Operation` (`Wavelength`)" - max_legend_len - Maximum number of characters for the legend labels before truncating. Default: 20 - - Returns - ------- - IPlot - Plotly iplot object. Will be rendered nicely in Jupyter notebook instance - """ - # TODO: Shift init_notebook_mode() to start of notebook instance - py.offline.init_notebook_mode() - - if isinstance(wells, str): - if wells != "*": - wells = [wells] - else: - well_readings = self.readings - wells = list(self.properties.index) - if isinstance(wells, list): - well_readings = self.readings.loc[wells] - - if not groupby: - traces = [ - go.Scatter( - x=self.readings.columns, - y=well_readings.loc[well], - name=self.properties["name"].loc[well], - ) - for well in wells - ] - else: - if groupby not in self.properties.columns: - raise ValueError( - f"'{groupby}' not found in the properties table. " - f"Please specify a column which exists" - ) - grouped = self.properties.groupby(groupby) - index_list = [grouped.get_group(group).index for group in grouped.groups] - reading_map = [] - for indx in index_list: - common_set = set(well_readings.index).intersection(set(indx)) - if len(common_set) != 0: - reading_map.append(well_readings.loc[common_set]) - if len(reading_map) != 0: - traces = [ - go.Scatter( - x=self.readings.columns, - y=reading.mean(), - name=self._truncate_name( - self.properties[groupby].loc[reading.iloc[0].name], - max_legend_len, - ), - error_y=dict(type="data", array=reading.std(), visible=True), - ) - for reading in reading_map - ] - else: - raise ValueError( - f"No common groups found for specified groupby: {groupby}" - ) - - # Assume all data is generated from the same run-id for now - if not title: - title = f"Kinetics Curve ({self.datasets[0].attributes['instruction']['run']['id']})" - if not xlabel: - xlabel = "Time" - if not ylabel: - if self.operation == "absorbance": - ylabel = f"RAU ({self.datasets[0].attributes['instruction']['operation']['wavelength']})" - elif self.operation == "fluorescence": - ylabel = ( - f"RFU ({self.datasets[0].attributes['instruction']['operation']['excitation']}/" - f"{self.datasets[0].attributes['instruction']['operation']['emission']})" - ) - elif self.operation == "luminescence": - ylabel = "Luminescence" - - layout = go.Layout( - title=title, - xaxis=dict( - title=xlabel, - titlefont=dict( - family="Courier New, monospace", size=18, color="#7f7f7f" - ), - ), - yaxis=dict( - title=ylabel, - titlefont=dict( - family="Courier New, monospace", size=18, color="#7f7f7f" - ), - ), - legend=dict(x=100, y=1), - ) - - fig = go.Figure(data=traces, layout=layout) - return py.offline.iplot(fig) - - @staticmethod - def _truncate_name(string, max_len=20): - """Truncates string to max_len number of characters, adds ellipses instead if its too long""" - if len(string) > max_len: - return string[: (max_len - 3)] + "..." - else: - return string diff --git a/transcriptic/analysis/spectrophotometry.py b/transcriptic/analysis/spectrophotometry.py deleted file mode 100644 index 46c52f82..00000000 --- a/transcriptic/analysis/spectrophotometry.py +++ /dev/null @@ -1,376 +0,0 @@ -from autoprotocol.container_type import ContainerType -from transcriptic import dataset as get_dataset - - -try: - import matplotlib.pyplot as plt - import numpy as np - import pandas - import plotly as py - import plotly.tools as tls -except ImportError: - raise ImportError( - "Please run `pip install transcriptic[analysis] if you " - "would like to use the Transcriptic analysis module." - ) - - -class _PlateRead(object): - - """ - A PlateRead object generalizes the parsing of datasets derived from the - plate reader for easy statistical analysis and visualization. - - Refer to the Absorbance, Fluorescence and Luminescence objects for more - information. - - """ - - def __init__( - self, - op_type, - dataset, - group_labels, - group_wells=None, - control_reading=None, - name=None, - ): - self.name = name - self.dataset = dataset - self.control_reading = control_reading - self.op_type = op_type - if self.op_type not in ["absorbance", "fluorescence", "luminescence"]: - raise RuntimeError("Data given is not from a spectrophotometry operation.") - if self.op_type != (self.dataset.attributes["instruction"]["operation"]["op"]): - raise RuntimeError(f"Data given is not a {op_type} operation.") - - # Populate measurement params - measure_params_dict = dict() - measure_params_dict["reader"] = self.dataset.attributes["warp"]["device_id"] - dataset_op = self.dataset.attributes["instruction"]["operation"] - if self.op_type == "absorbance": - measure_params_dict["wavelength"] = ( - dataset_op["wavelength"].split(":")[0] + "nm" - ) - if self.op_type == "fluorescence": - measure_params_dict["wavelength"] = ( - f"excitation: {dataset_op['excitation'].split(':')[0] + 'nm'} " - f"emission: {dataset_op['emission'].split(':')[0] + 'nm'}" - ) - if self.op_type == "luminescence": - measure_params_dict["wavelength"] = "" - - self.params = measure_params_dict - - # Populate plate field - plate_info_dict = dict() - plate_info_dict["id"] = self.dataset.attributes["container"]["id"] - plate_info_dict["col_count"] = self.dataset.attributes["container"][ - "container_type" - ]["col_count"] - plate_info_dict["well_count"] = self.dataset.attributes["container"][ - "container_type" - ]["well_count"] - self.params["plate"] = plate_info_dict - - # Get dataset and parse into DataFrame - data_dict = get_dataset(self.dataset.attributes["id"]) - self.df = pandas.DataFrame() - well_count = self.dataset.attributes["container"]["container_type"][ - "well_count" - ] - col_count = self.dataset.attributes["container"]["container_type"]["col_count"] - # If no group well list specified, default to including all well data - # values in one group - if not group_wells: - self.df = pandas.DataFrame( - [x[0] for x in list(data_dict.values())], columns=[group_labels[0]] - ) - # If given list of all int, assume one group with all wells in list - elif all(isinstance(i, int) for i in group_wells): - if len(group_wells) > len(data_dict): - raise ValueError("Sum of group lengths exceeds total no. of wells.") - wells = [ - ContainerType.humanize_static(_, well_count, col_count).lower() - for _ in group_wells - ] - if not all(_ in data_dict for _ in wells): - raise ValueError(f"Not all wells {wells} are in dataset {data_dict}.") - - self.df = pandas.DataFrame( - [data_dict[_][0] for _ in wells], columns=[group_labels[0]] - ) - elif all(isinstance(i, list) for i in group_wells): - if group_wells and (sum([len(i) for i in group_wells]) > len(data_dict)): - raise ValueError("Sum of group lengths exceeds total no. of wells.") - for (idx, well_list) in enumerate(group_wells): - wells = [ - ContainerType.humanize_static(_, well_count, col_count).lower() - for _ in well_list - ] - if not all(_ in data_dict for _ in wells): - raise ValueError( - f"Not all wells {wells} are in dataset {data_dict}." - ) - col = pandas.DataFrame( - [data_dict[_][0] for _ in wells], columns=[group_labels[idx]] - ) - # if group_well members are of different lengths, - # concat automatically pads resultant DataFrame with NaN - self.df = pandas.concat([self.df, col], axis=1) - else: - raise ValueError( - "Format Error: Group Well List should be a list of list of \ - wells in robot format" - ) - - # If control absorbance object specified, create df_abj variable by - # subtracting control df from original - if control_reading: - self.df_adj = self.df - control_reading.df - - self.cv = self.df.std() / (self.df.mean() * 100) - - def plot(self, mpl=True, plot_type="box", **plt_kwargs): - """ - Parameters - ---------- - mpl : boolean, optional - Set to True to render a matplotlib plot, otherwise a Plotly plot - is rendered - plot_type : {"box", "bar", "line", "hist"}, optional - Type of plot to render - plot_kwargs : dict, optional - Optional dictionary of specifications for your plot type of choice - """ - py.offline.init_notebook_mode() - - mpl_fig, ax = plt.subplots() - nl = "\n" if mpl else "
" - ax.set_ylabel(self.op_type + nl + self.params["wavelength"]) - self.df.plot(kind=plot_type, ax=ax) - labels = [item.label.get_text() for item in ax.xaxis.get_major_ticks()] - if mpl: - return None - else: - if not plt_kwargs: - plt_kwargs = { - "layout": { - "xaxis": { - "tickmode": "array", - "ticktext": labels, - "tickvals": list(range(1, len(labels) + 1)), - "tickangle": 0, - "tickfont": {"size": 10}, - } - } - } - pyfig = tls.mpl_to_plotly(mpl_fig) - pyfig.update(plt_kwargs) - return py.offline.iplot(pyfig) - - -class Absorbance(_PlateRead): - - """ - An Absorbance object parses a dataset object and provides functions for - easy statistical analysis and visualization. - - Parameters - ---------- - - dataset: dataset - Single dataset selected from datasets object - group_labels: list[str] - Labels for each of the respective groups - group_wells: list[list[int]] - List of list of wells (robot form) belonging to each group in order. - E.g. [[1,3,5],[2,4,6]] - control_abs: Absorbance object, optional - Absorbance object of water/control blank. If specified, will create - adjusted dataframe df_adj by subtracting from existing df - name: str, optional - Name of absorbance object. Used in plotting functions - - """ - - def __init__( - self, dataset, group_labels, group_wells=None, control_abs=None, name=None - ): - _PlateRead.__init__( - self, "absorbance", dataset, group_labels, group_wells, control_abs, name - ) - - def beers_law(self, conc_list=None, use_adj=True, **kwargs): - """ " - Apply Beer-Lambert's law to a series of absorbance readings and get - an estimation of the linearity between the absorbance and concentration - values. - - Parameters - ---------- - - conc_list: list[double], optional - List of concentrations of dye used - use_adj: Boolean, optional - Boolean option which determines if the adjusted absorbance readings - are used - kwargs : dict - Optional dictionary of specifications for your plot type of choice - """ - if "title" not in kwargs: - if self.name: - kwargs["title"] = f"Beer's Law ({self.name})" - else: - kwargs["title"] = "Beer's Law" - if "yerr" not in kwargs: - kwargs["yerr"] = self.df.std() - - # Use df_adj for beer's law if control abs object was given - if use_adj and self.control_reading: - dataf = self.df_adj - else: - dataf = self.df - # Use default labels if concentration not provided - if not conc_list: - if "xlim" not in kwargs: - kwargs["xlim"] = (-1, len(dataf.mean())) - dataf.mean().plot(**kwargs) - else: - plot_obj = pandas.DataFrame( - {"values": dataf.mean(), "conc": np.asarray(conc_list)} - ) - result = np.polyfit(plot_obj["conc"], plot_obj["values"], 1, full=True) - gradient, intercept = result[0] - mpl_fig, ax = plt.subplots() - plot_obj.plot(x="conc", y="values", kind="scatter", ax=ax, **kwargs) - plt.plot(plot_obj["conc"], gradient * plot_obj["conc"] + intercept, "-") - ax.set_ylabel("Absorbance " + self.params["wavelength"]) - - # Calculate R^2 from residuals - ss_res = result[1] - ss_tot = np.sum(np.square((plot_obj["values"] - plot_obj["values"].mean()))) - print( - f"{self.name if self.name is not None else ''} R^2: {1 - ss_res // ss_tot}" - ) - - -class Fluorescence(_PlateRead): - - """ - An Fluorescence object parses a dataset object and provides functions for - easy statistical analysis and visualization. - - Parameters - ---------- - - dataset: dataset - Single dataset selected from datasets object - group_labels: list[str] - Labels for each of the respective groups - group_wells: list[int] - List of list of wells (robot form) belonging to each group in order. - E.g. [[1,3,5],[2,4,6]] - control_fluor: Fluorescence object, optional - Fluorescence object of water/control blank. If specified, will create - adjusted dataframe df_adj by subtracting from existing df - name: str, optional - Name of fluorescence object. Used in plotting functions - - """ - - def __init__( - self, dataset, group_labels, group_wells=None, control_fluor=None, name=None - ): - _PlateRead.__init__( - self, - "fluorescence", - dataset, - group_labels, - group_wells, - control_fluor, - name, - ) - - -class Luminescence(_PlateRead): - - """ - An Luminescence object parses a dataset object and provides functions for - easy statistical analysis and visualization. - - Parameters - ---------- - - dataset: dataset - Single dataset selected from datasets object - group_labels: list[str] - Labels for each of the respective groups - group_wells: list[int] - List of list of wells (robot form) belonging to each group in order. - E.g. [[1,3,5],[2,4,6]] - control_lumi: Luminescence object, optional - Luminescence object of water/control blank. If specified, will create - adjusted dataframe df_adj by subtracting from existing df - name: str, optional - Name of luminescence object. Used in plotting functions - - """ - - def __init__( - self, dataset, group_labels, group_wells=None, control_lumi=None, name=None - ): - _PlateRead.__init__( - self, "luminescence", dataset, group_labels, group_wells, control_lumi, name - ) - - -def compare_standards(pr_obj, std_pr_obj): - """ - Compare a sample plate read object with a standard plate read object to get - measures such as the Root-Mean-Square-Error (RMSE) and - Coefficient-of-Variation (CV). - - - Parameters - ---------- - - pr_obj: _PlateRead - Sample plate read object - std_pr_obj: _PlateRead - Standard plate read object - """ - # Compare against mean of standard absorbance - # Check to ensure CVs are at least 2 apart - for indx in range(len(pr_obj.cv)): - cv_ratio = pr_obj.cv.iloc[indx] // std_pr_obj.cv.iloc[indx] - if cv_ratio < 2: - print( - f"Warning for {pr_obj.cv.index[indx]}: Sample CV is only " - f"{cv_ratio} times that of Standard CV. RMSE may be inaccurate." - ) - # RMSE (normalized wrt to standard mean) - RMSE = ( - np.sqrt(np.square(pr_obj.df - std_pr_obj.df.mean()).mean()) - / std_pr_obj.df.mean() - * 100 - ) - RMSE = pandas.DataFrame(RMSE, columns=["RMSE % (normalized to standard mean)"]) - - sampleVariance = pandas.DataFrame(pr_obj.df.var(), columns=["Sample Variance"]) - sampleCV = pandas.DataFrame(pr_obj.cv, columns=["Sample (%) CV"]) - - try: - # pylint: disable=import-error - from IPython.display import HTML, display - - if pr_obj.name: - display(HTML(f"Standards Comparison ({pr_obj.name})")) - display(sampleVariance) - display(sampleCV) - display(RMSE) - except: - # If IPython module is not present or unable to show, print results - print(sampleVariance) - print(sampleCV) - print(RMSE) diff --git a/transcriptic/auth.py b/transcriptic/auth.py deleted file mode 100644 index 3da5ef09..00000000 --- a/transcriptic/auth.py +++ /dev/null @@ -1,98 +0,0 @@ -import base64 - -from abc import ABC -from email.utils import formatdate -from urllib.parse import urlparse - -from Crypto.Hash import SHA256 -from httpsig.requests_auth import HTTPSignatureAuth -from httpsig.utils import HttpSigException -from requests import Session -from requests.auth import AuthBase - - -class AuthSession(Session): - """Custom Session to handle any auth specific behaviors""" - - def rebuild_auth(self, prepared_request, response): - """ - Monkey-patches original rebuild_auth method which handles auth building - for redirects. In cases where we're using any of our StrateosAuthBase - classes, we want to always apply our own internal auth logic handlers. - """ - if isinstance(self.auth, StrateosAuthBase): - prepared_request.prepare_auth(self.auth) - else: - super().rebuild_auth(prepared_request, response) - - -class StrateosAuthBase(AuthBase, ABC): - def __init__(self, api_root): - self.api_root = api_root - - def is_internal_request(self, request): - return urlparse(request.url).netloc == urlparse(self.api_root).netloc - - -class StrateosSign(StrateosAuthBase): - """Signs requests""" - - def __init__(self, email, secret, api_root): - super().__init__(api_root) - self.email = email - self.secret = secret - - headers = ["(request-target)", "Date", "Host"] - body_headers = ["Digest", "Content-Length"] - try: - self.auth = HTTPSignatureAuth( - key_id=self.email, - algorithm="rsa-sha256", - headers=headers, - secret=self.secret, - ) - self.body_auth = HTTPSignatureAuth( - key_id=self.email, - algorithm="rsa-sha256", - headers=headers + body_headers, - secret=self.secret, - ) - except HttpSigException: - raise ValueError( - "Could not parse the specified RSA Key, ensure it " - "is a PRIVATE key in PEM format" - ) - - def __call__(self, request): - if not self.is_internal_request(request): - return request - - if "Date" not in request.headers: - request.headers["Date"] = formatdate( - timeval=None, localtime=False, usegmt=True - ) - - if request.method.upper() in ("PUT", "POST", "PATCH"): - encoded_body = ( - request.body - if (isinstance(request.body, bytes) or request.body is None) - else request.body.encode() - ) - digest = SHA256.new(encoded_body).digest() - sha = base64.b64encode(digest).decode("ascii") - request.headers["Digest"] = f"SHA-256={sha}" - return self.body_auth(request) - - return self.auth(request) - - -class StrateosBearerAuth(StrateosAuthBase): - def __init__(self, token, api_root): - super().__init__(api_root) - self.token = token - - def __call__(self, request): - if self.is_internal_request(request): - request.headers["authorization"] = self.token - - return request diff --git a/transcriptic/cli.py b/transcriptic/cli.py deleted file mode 100755 index d90b37a7..00000000 --- a/transcriptic/cli.py +++ /dev/null @@ -1,764 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import sys - -import click -import requests - -from transcriptic import commands -from transcriptic.config import Connection - - -class FeatureGroup(click.Group): - """Custom group to handle hiding of commands based on the `feature` tag - TODO: Deprecate once Click 7 lands and use `hidden` parameter in commands - """ - - def __init__(self, **attrs): - click.Group.__init__(self, **attrs) - - def format_commands(self, ctx, formatter): - """Custom formatter to control whether a command is displayed - Note: This is only called when formatting the help message. - """ - ctx.obj = ContextObject() - try: - ctx.obj.api = Connection.from_file("~/.transcriptic") - except (FileNotFoundError, OSError): - # This defaults to feature_groups = [] - ctx.obj.api = Connection() - - rows = [] - for subcommand in self.list_commands(ctx): - cmd = self.get_command(ctx, subcommand) - if cmd is None: - continue - try: - if ( - cmd.feature is not None - and cmd.feature in ctx.obj.api.feature_groups - ): - help = cmd.short_help or "" - rows.append((subcommand, help)) - else: - continue - except AttributeError: - help = cmd.short_help or "" - rows.append((subcommand, help)) - - if rows: - with formatter.section("Commands"): - formatter.write_dl(rows) - - -class FeatureCommand(click.Command): - """Extend off Command to add `feature` attribute - TODO: Deprecate once Click 7 lands and use `hidden` parameter in commands - """ - - def __init__(self, feature=None, **attrs): - click.Command.__init__(self, **attrs) - self.feature = feature - - -class HiddenOption(click.Option): - """Monkey patch of click Option to enable hidden options - TODO: Deprecate once Click 7 lands and use `hidden` option instead - """ - - def __init__(self, *param_decls, **attrs): - __hidden__ = attrs.pop("hidden", True) - click.Option.__init__(self, *param_decls, **attrs) - self.__hidden__ = __hidden__ - - def get_help_record(self, ctx): - """This hijacks the help record so that a hidden option does not show - up in the help text - """ - if self.__hidden__: - return - click.Option.get_help_record(self, ctx) - - -class ContextObject(object): - """Object passed along Click context - Note: `ctx` is passed along whenever the @click.pass_context decorator is - present. This object is referenced using `ctx.obj` - """ - - def __init__(self): - self._api = None - - @property - def api(self): - return self._api - - @api.setter - def api(self, value): - self._api = value - - -_CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group(context_settings=_CONTEXT_SETTINGS, cls=FeatureGroup) -@click.option("--api-root", default=None, hidden=True, cls=HiddenOption) -@click.option("--email", default=None, hidden=True, cls=HiddenOption) -@click.option("--token", default=None, hidden=True, cls=HiddenOption) -@click.option("--organization", "-o", default=None, hidden=True, cls=HiddenOption) -@click.option( - "--config", - envvar="TRANSCRIPTIC_CONFIG", - default="~/.transcriptic", - help="Specify a configuration file.", -) -@click.version_option(prog_name="Transcriptic Python Library (TxPy)") -@click.pass_context -def cli(ctx, api_root, email, token, organization, config): - """A command line tool for working with Transcriptic. - - Note: This is the main entry point of the CLI. If specifying credentials, - note that the order of preference is: --flag, environment then config file. - - Example: `transcriptic --organization "my_org" projects` >> - `export USER_ORGANIZATION="my_org"` >> `"organization_id": "my_org" in - ~/.transcriptic - """ - # Initialize ContextObject to be used for storing api object - ctx.obj = ContextObject() - - if ctx.invoked_subcommand in ["compile", "preview", "summarize", "init"]: - # For local commands, initialize empty connection - ctx.obj.api = Connection() - elif ctx.invoked_subcommand == "login": - # Load analytics option from existing dotfile if present, else prompt - try: - api = Connection.from_file(config) - api.api_root = api_root or os.environ.get("BASE_URL", None) or api.api_root - ctx.obj.api = api - except (OSError, IOError): - ctx.obj.api = Connection() - # Echo a warning if other options are defined for login - if organization or email or token: - click.echo( - "Only the `--api-root` option is applicable for the " - "`login` command. All other options are ignored." - ) - else: - try: - api = Connection.from_file(config) - api.api_root = api_root or os.environ.get("BASE_URL", None) or api.api_root - api.organization_id = ( - organization - or os.environ.get("USER_ORGANIZATION", None) - or api.organization_id - ) - api.email = email or os.environ.get("USER_EMAIL", None) or api.email - api.token = token or os.environ.get("USER_TOKEN", None) or api.token - ctx.obj.api = api - except (OSError, IOError): - click.echo( - "Welcome to TxPy! It seems like your `.transcriptic` " - "config file is missing or out of date" - ) - analytics = click.confirm( - "Send TxPy CLI usage information to " - "improve the CLI user " - "experience?", - default=True, - ) - ctx.obj.api = Connection() # Initialize empty connection - ctx.invoke(login_cmd, api_root=api_root, analytics=analytics) - if ctx.obj.api.analytics: - try: - ctx.obj.api._post_analytics( - event_action=ctx.invoked_subcommand, event_category="cli" - ) - except requests.exceptions.RequestException: - pass - - -@cli.command("submit", cls=FeatureCommand, feature="can_submit_autoprotocol") -@click.argument("file", default="-") -@click.option( - "--project", - "-p", - metavar="PROJECT_ID", - required=True, - help=( - "Project id or name to submit the run to. " - "Use `transcriptic projects` command to list existing projects." - ), -) -@click.option("--title", "-t", help="Optional title of your run") -@click.option("--test", help="Submit this run in test mode", is_flag=True) -@click.option( - "--pm", - metavar="PAYMENT_METHOD_ID", - required=False, - help="Payment id to be used for run submission. " - "Use `transcriptic payments` command to list existing " - "payment methods.", -) -@click.pass_context -def submit_cmd(ctx, file, project, title=None, test=None, pm=None): - """Submit your run to the project specified.""" - api = ctx.obj.api - try: - run_url = commands.submit(api, file, project, title=title, test=test, pm=pm) - click.echo(f"Run created: {run_url}") - except RuntimeError as err: - click.echo(f"{err}", err=True) - sys.exit(1) - - -@cli.command("build-release", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("package", required=False, metavar="PACKAGE") -@click.option("--name", "-n", help="Optional name for your zip file") -@click.pass_context -def release_cmd(ctx, name=None, package=None): - """Compress the contents of the current directory to upload as a release.""" - api = ctx.obj.api - commands.release(api, name=name, package=package) - - -@cli.command("upload-release", cls=FeatureCommand, feature="can_upload_packages") -@click.argument( - "archive", required=True, type=click.Path(exists=True), metavar="ARCHIVE" -) -@click.argument("package", required=True, metavar="PACKAGE") -@click.pass_context -def upload_release_cmd(ctx, archive, package): - """Upload a release archive to a package.""" - api = ctx.obj.api - commands.upload_release(api, archive, package) - - -@cli.command("upload-dataset") -@click.argument("file_path", type=click.Path(exists=True), metavar="FILE") -@click.argument("title", metavar="TITLE") -@click.argument("run_id", metavar="RUN-ID") -@click.option( - "--tool", - "-t", - required=True, - help="Name of analysis tool used for generating the dataset", -) -@click.option( - "--version", - "-v", - required=True, - help="Version of analysis tool used for generating the dataset", -) -@click.pass_context -def upload_dataset_cmd(ctx, file_path, title, run_id, tool, version): - """Uploads specified file as an analysis dataset to the specified run.""" - api = ctx.obj.api - commands.upload_dataset(api, file_path, title, run_id, tool, version) - - -@cli.command("protocols") -@click.pass_context -@click.option( - "--local", - is_flag=True, - required=False, - default=False, - help="Shows available local protocols instead of remote protocols", -) -@click.option("--json", "json_flag", help="print JSON response", is_flag=True) -def protocols_cmd(ctx, local, json_flag): - """List protocols within your manifest or organization.""" - api = ctx.obj.api - commands.protocols(api, local, json_flag) - - -@cli.command("packages") -@click.pass_context -@click.option("-i") -def packages_cmd(ctx, i): - """List packages in your organization.""" - api = ctx.obj.api - commands.packages(api, i) - - -@cli.command("create-package", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("name") -@click.argument("description") -@click.pass_context -def create_package_cmd(ctx, description, name): - """Create a new empty protocol package""" - api = ctx.obj.api - commands.create_package(api, description, name) - - -@cli.command("delete-package", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("name") -@click.option( - "--force", - "-f", - help="force delete a package without being \ - prompted if you're sure", - is_flag=True, -) -@click.pass_context -def delete_package_cmd(ctx, name, force): - """Delete an existing protocol package""" - api = ctx.obj.api - commands.delete_package(api, name, force) - - -@cli.command("generate-protocol") -@click.pass_context -@click.argument("name") -def generate_protocol_cmd(ctx, name): - """Generate a python protocol scaffold""" - commands.generate_protocol(name) - - -@cli.command("projects") -@click.pass_context -@click.option("-i", help="DEPRECATED option. Use `--names` instead.") -@click.option("--json", "json_flag", help="print JSON response", is_flag=True) -@click.option( - "--names", - "names_only", - help="returns a mapping of `project_id`: `project_name`", - is_flag=True, -) -def projects_cmd(ctx, i, json_flag, names_only): - """List the projects in your organization""" - api = ctx.obj.api - try: - response = commands.projects(api, i, json_flag, names_only) - if i or names_only: - click.echo(response) - elif json_flag: - click.echo(json.dumps(response)) - else: - click.echo("\n{:^80}".format("PROJECTS:\n")) - click.echo(f"{'PROJECT NAME':^40}" + "|" + f"{'PROJECT ID':^40}") - click.echo(f"{'':-^80}") - for proj_id, name in list(response.items()): - click.echo(f"{name:<40}" + "|" + f"{proj_id:^40}") - click.echo(f"{'':-^80}") - except RuntimeError: - click.echo( - "There was an error listing the projects in your " - "organization. Make sure your login details are correct.", - err=True, - ) - - -@cli.command("runs") -@click.pass_context -@click.argument("project_name") -@click.option("--json", "json_flag", help="print JSON response", is_flag=True) -def runs_cmd(ctx, project_name, json_flag): - """List the runs that exist in a project""" - api = ctx.obj.api - commands.runs(api, project_name, json_flag) - - -@cli.command("create-project") -@click.argument("name", metavar="PROJECT_NAME") -@click.option("--dev", "-d", "-pilot", help="Create a pilot project", is_flag=True) -@click.pass_context -def create_project_cmd(ctx, name, dev): - """Create a new empty project.""" - api = ctx.obj.api - commands.create_project(api, name, dev) - - -@cli.command("delete-project") -@click.argument("name", metavar="PROJECT_NAME") -@click.option( - "--force", - "-f", - help="force delete a project without being \ - prompted if you're sure", - is_flag=True, -) -@click.pass_context -def delete_project_cmd(ctx, name, force): - """Delete an existing project.""" - api = ctx.obj.api - commands.delete_project(api, name, force) - - -@cli.command("resources") -@click.argument("query", default="*") -@click.pass_context -def resources_cmd(ctx, query): - """Search catalog of provisionable resources""" - api = ctx.obj.api - commands.resources(api, query) - - -@cli.command("inventory") -@click.argument("query", default="*") -@click.option( - "--include_aliquots", help="include containers with matching aliquots", is_flag=True -) -@click.option("--show_status", help="show container status", is_flag=True) -@click.option( - "--retrieve_all", help="retrieve all samples, this may take a while", is_flag=True -) -@click.pass_context -def inventory_cmd(ctx, include_aliquots, show_status, retrieve_all, query): - """Search organization for inventory""" - api = ctx.obj.api - commands.inventory(api, include_aliquots, show_status, retrieve_all, query) - - -@cli.command("payments") -@click.pass_context -def payments_cmd(ctx): - """Lists available payment methods""" - api = ctx.obj.api - commands.payments(api) - - -@cli.command("init", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("path", default=".") -def init_cmd(path): - """Initialize a directory with a manifest.json file.""" - commands.init(path) - - -@cli.command("analyze") -@click.argument("file", default="-") -@click.option("--test", help="Analyze this run in test mode", is_flag=True) -@click.pass_context -def analyze_cmd(ctx, file, test): - """Analyze a block of Autoprotocol JSON.""" - api = ctx.obj.api - commands.analyze(api, file, test) - - -@cli.command("preview", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("protocol_name", metavar="PROTOCOL_NAME") -@click.option("--view", is_flag=True) -@click.option("--dye_test", is_flag=True) -@click.pass_context -def preview_cmd(ctx, protocol_name, view, dye_test): - """Preview the Autoprotocol output of protocol in the current package.""" - api = ctx.obj.api - commands.preview(api, protocol_name, view, dye_test) - - -@cli.command("summarize") -@click.argument("file", default="-") -@click.pass_context -@click.option( - "--html", "-x", is_flag=True, help="Generates an html view of the autoprotocol" -) -@click.option( - "--tree", - "-t", - is_flag=True, - help="Prints a job tree with instructions as leaf nodes", -) -@click.option( - "--lookup", - "-l", - is_flag=True, - help="Queries Transcriptic to convert resourceID to string", -) -# time allowance is on order of seconds -@click.option("--runtime", type=click.INT, default=5) -def summarize_cmd(ctx, file, html, tree, lookup, runtime): - """Summarize Autoprotocol as a list of plain English steps, as well as a - visualized Job Tree contingent upon desired runtime allowance (in seconds). - A Job Tree refers to a structure of protocol based on container dependency, - where each node, and its corresponding number, represents an instruction of - the protocol. More specifically, the tree structure contains process branches, - in which the x-axis refers to the dependency depth in a given branch, while - the y-axis refers to the traversal of branches themselves. - - Example usage is as follows: - - python my_script.py | transcriptic summarize --tree - - python my_script.py | transcriptic summarize --tree --runtime 20 - - python my_script.py | transcriptic summarize --html - """ - if lookup or html: - try: - config = "~/.transcriptic" - ctx.obj = ContextObject() - ctx.obj.api = Connection.from_file(config) - except: - click.echo( - "Connection with Transcriptic failed. Summarizing without lookup.", - err=True, - ) - - api = ctx.obj.api - commands.summarize(api, file, html, tree, lookup, runtime) - - -@cli.command("compile", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("protocol_name", metavar="PROTOCOL_NAME") -@click.argument("args", nargs=-1) -def compile_cmd(protocol_name, args): - """Compile a protocol by passing it a config file (without submitting or - analyzing).""" - commands.compile(protocol_name, args) - - -@cli.command("launch") -@click.argument("protocol") -@click.argument( - "params", metavar="PARAMETERS_FILE", type=click.File("r"), required=False -) -@click.option( - "--project", - "-p", - metavar="PROJECT_ID", - required=False, - help="Project id or name context for configuring the protocol. Use " - "`transcriptic projects` command to list existing projects.", -) -@click.option( - "--title", - "-t", - metavar="RUN_TITLE", - required=False, - help="If specified, will apply custom title to run created, default run title" - "will be the DISPLAY-NAME_MM_DD_YYYY of the protocol selected.", -) -@click.option( - "--save_input", - metavar="FILE", - required=False, - help="Save the protocol or parameters input JSON in a file. This is " - "useful for debugging a protocol.", -) -@click.option( - "--local", - is_flag=True, - required=False, - help="If specified, the protocol will launch a local protocol and submit a run.", -) -@click.option( - "--accept_quote", - is_flag=True, - required=False, - help="If specified, the quote will automatically be accepted, and a run " - "will be directly submitted.", -) -@click.option( - "--pm", - metavar="PAYMENT_METHOD_ID", - required=False, - help="Payment id to be used for run submission. " - "Use `transcriptic payments` command to list existing " - "payment methods.", -) -@click.option("--test", help="Submit this run in test mode", is_flag=True) -@click.option( - "--pkg", - metavar="PACKAGE_ID", - required=False, - help="Package ID for discriminating between protocols with identical names", -) -@click.option( - "--save_preview", - "-sp", - is_flag=True, - required=False, - help="Save the protocol preview parameters and refs selected as input and merge into local " - "manifest.json. This is useful for debugging a protocol.", -) -@click.pass_context -def launch_cmd( - ctx, - protocol, - project, - title, - save_input, - local, - accept_quote, - params, - pm=None, - test=None, - pkg=None, - save_preview=False, -): - """Configure and launch a protocol either using the local manifest file or remotely. - If no parameters are specified, uses the webapp to select the inputs.""" - api = ctx.obj.api - commands.launch( - api, - protocol, - project, - title, - save_input, - local, - accept_quote, - params, - pm=None, - test=None, - pkg=None, - save_preview=save_preview, - ) - - -@cli.command("select-org") -@click.argument("organization", metavar="ORGANIZATION_NAME", type=str, required=False) -@click.pass_context -def select_org_cmd(ctx, organization=None): - """Allows you to switch organizations. If the organization argument - is provided, this will directly select the specified organization. - """ - api = ctx.obj.api - config = ctx.parent.params["config"] - commands.select_org(api, config, organization) - - -@cli.command("login") -@click.option( - "--rsa-key", - type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="Path to RSA key used for signing requests", -) -@click.pass_context -def login_cmd(ctx, api_root=None, analytics=True, rsa_key=None): - """Authenticate to your Transcriptic account.""" - api = ctx.obj.api - config = ctx.parent.params["config"] - commands.login(api, config, api_root, analytics, rsa_key) - - -@cli.command("format", cls=FeatureCommand, feature="can_upload_packages") -@click.argument("manifest", default="manifest.json") -def format_cmd(manifest): - """Check Autoprotocol format of manifest.json.""" - commands.format(manifest) - - -@cli.command("exec") -@click.argument("autoprotocol", type=click.File("r"), default=sys.stdin) -@click.option( - "--api", - "-a", - help="The api endpoint of your test dashboard, or the scle test workcell instance (if used with --no-redirect).", - required=True, -) -@click.option( - "--no-redirect", - help="If set, the api endpoint given is the scle test workcell instance.", - is_flag=True, -) -@click.option( - "--workcell-id", - "-w", - help="The workcell id to use for the device set (wc4-mcx1, tst-01-mcx-01, etc.). This is not permitted along with the `device-set` or `session-id` option.", -) -@click.option( - "--device-set", - "-d", - type=click.File("r"), - help="A DeviceSet json file to use for scheduling. This is not permitted along with the `workcell-id` or `session-id` options.", -) -@click.option( - "--session-id", - "-s", - help="The session id of the session that should be used for scheduling this run. This is not permitted along with the `workcell-id` or `device-set` options.", -) -@click.option( - "--time-limit", - "-t", - type=click.INT, - default=30, - help="The maximum time in seconds to spend scheduling. The scheduler will use all the time until an optimal solution is found.", -) -@click.option( - "--schedule-at", - default=None, - help="The absolute time at which the given protocol should start (at the earliest). Absolute time format YYYY-MM-DDThh:mm in the local time of the target workcell (if year, month, or day is missing, it will be auto filled with the current values).", -) -@click.option( - "--schedule-delay", - type=click.INT, - default=None, - help="Delay in minutes at which the given protocol should start (at the earliest).", -) -@click.option( - "--time-constraints-are-suggestion", - "-tc-suggestion", - help="If set, the time constraints will be considered only a suggestion.", - is_flag=True, -) -@click.option( - "--exclude", - "-e", - metavar="FILTER", - help="Remove those instructions from the payload. e.g.: 0, 0-5, x_human, op:povision, x_human!:false", - multiple=True, -) -@click.option( - "--include", - "-i", - metavar="FILTER", - help="Add those instructions to the payload after the --exclude has been applied.", - multiple=True, -) -@click.option( - "--partition-group-size", - type=click.INT, - default=None, - help="The number of x_partition groups to be scheduled together.", -) -@click.option( - "--partition-horizon", - type=click.INT, - default=None, - help="The time in seconds to overlap partitions by.", -) -@click.option( - "--partitioning-swap-device-id", - default=None, - help="The device id to use as a swap space when partitioning.", -) -@click.pass_context -def execute( - ctx, - autoprotocol, - api, - no_redirect, - workcell_id, - device_set, - session_id, - time_limit, - schedule_at, - schedule_delay, - time_constraints_are_suggestion, - exclude, - include, - partition_group_size, - partition_horizon, - partitioning_swap_device_id, -): - """Send autoprotocol to a test workcell (no hardware) for scheduling.""" - commands.execute( - autoprotocol, - api, - no_redirect, - workcell_id, - device_set, - session_id, - time_limit, - schedule_at, - schedule_delay, - time_constraints_are_suggestion, - exclude, - include, - partition_group_size, - partition_horizon, - partitioning_swap_device_id, - ctx.obj.api.email, - ) diff --git a/transcriptic/commands.py b/transcriptic/commands.py deleted file mode 100644 index fdd3fe7d..00000000 --- a/transcriptic/commands.py +++ /dev/null @@ -1,1751 +0,0 @@ -#!/usr/bin/env python3 -""" -Contains abstracted functions which is primarily used by the CLI. However, they can -be separately imported and used in other contexts. - -There is a mix of functions which directly call `click.echo` vs returning responses to -the caller (e.g. CLI). We should move towards the latter pattern. -""" - -import json -import locale -import os -import re -import sys -import time -import warnings -import zipfile - -from collections import OrderedDict -from contextlib import contextmanager -from os.path import abspath, expanduser, isfile - -import click -import requests - -from click.exceptions import BadParameter -from jinja2 import Environment, PackageLoader -from transcriptic import routes -from transcriptic.auth import StrateosSign -from transcriptic.config import AnalysisException, Connection -from transcriptic.english import AutoprotocolParser -from transcriptic.util import ( - PreviewParameters, - ascii_encode, - flatmap, - iter_json, - makedirs, -) - - -def submit( - api: Connection, - file: str, - project: str, - title: str = None, - test: bool = None, - pm: str = None, -): - """ - Submit your run to the project specified. - If successful, returns the formatted url link to the created run. - - Parameters - ---------- - api: Connection - API context used for making base calls - file: str - Name of file to read from. Use `-` if reading from standard input. - project: str - `ProjectId` to submit this json to. - title: str, optional - If specified, Title of the created run. - test: bool, optional - If true, submit as a test run. - pm: str, optional - If specified, `PaymentId` to be used. - """ - if pm is not None and not is_valid_payment_method(api, pm): - raise RuntimeError( - "Payment method is invalid. Please specify a payment " - "method from `transcriptic payments` or not specify the " - "`--payment` flag to use the default payment method." - ) - valid_project_id = get_project_id(api, project) - if not valid_project_id: - raise RuntimeError(f"Invalid project {project} specified") - with click.open_file(file, "r") as f: - try: - protocol = json.loads(f.read()) - except ValueError: - raise RuntimeError( - "Error: Could not submit since your manifest.json " - "file is improperly formatted." - ) - - try: - req_json = api.submit_run( - protocol, - project_id=valid_project_id, - title=title, - test_mode=test, - payment_method_id=pm, - ) - run_id = req_json["id"] - formatted_url = api.url(f"{valid_project_id}/runs/{run_id}") - return formatted_url - except AnalysisException as e: - raise RuntimeError(e.message) - - -def release(api, name=None, package=None): - deflated = zipfile.ZIP_DEFLATED - if name: - filename = f"release_{name}" - else: - filename = "release" - if os.path.isfile(filename + ".zip"): - new = click.prompt( - f"You already have a release named {filename} in " - f"this directory, make another one? [y/n]", - default="y", - ) - if new == "y": - num_existing = sum([1 for x in os.listdir(".") if filename in x]) - filename = filename + "_" + str(num_existing) - else: - return - click.echo("Compressing all files in this directory...") - zf = zipfile.ZipFile(filename + ".zip", "w", deflated) - for (path, dirs, files) in os.walk("."): - for f in files: - if ".zip" not in f: - zf.write(os.path.join(path, f)) - zf.close() - click.echo("Archive %s created." % (filename + ".zip")) - if package: - package_id = get_package_id(api, package) or get_package_name(api, package) - upload_release(api, filename + ".zip", package_id) - - -def upload_release(api, archive, package): - """Upload a release archive to a package.""" - try: - package_id = get_package_id(api, package.lower()) or get_package_name( - api, package.lower() - ) - package_name = get_package_name(api, package_id.lower()) or get_package_id( - api, package_id.lower() - ) - click.echo(f"Uploading {archive} to {package_name}") - except AttributeError: - click.echo("Error: Invalid package id or name.") - return - - with click.progressbar( - None, - 100, - "Upload Progress", - show_eta=False, - width=70, - fill_char="|", - empty_char="-", - ) as bar: - bar.update(40) - file = open(os.path.basename(archive), "rb") - upload_id = api.upload_to_uri( - file, "application/zip, application/octet-stream", archive, archive - ) - bar.update(20) - try: - up = api.post_release( - data=json.dumps( - {"release": {"upload_id": upload_id, "user_id": api.user_id}} - ), - package_id=package_id, - ) - re = up["id"] - except (ValueError, PermissionError) as err: - if type(err) == ValueError: - click.echo( - "\nError: There was a problem uploading your release." - "\nVerify that your manifest.json file is properly " - "formatted and that all previews in your manifest " - "produce valid Autoprotocol by using the " - "`transcriptic preview` and/or `transcriptic analyze` " - "commands." - ) - elif type(err) == PermissionError: - click.echo("\n" + str(err)) - return - - bar.update(20) - time.sleep(10) - status = api.get_release_status( - package_id=package_id, release_id=re, timestamp=int(time.time()) - ) - errors = status["validation_errors"] - bar.update(30) - if errors: - click.echo( - "\nPackage upload to %s unsuccessful. " - "The following error(s) was returned: \n%s" - % ( - get_package_name(api, package_id), - ("\n").join(e.get("message", "[Unknown]") for e in errors), - ) - ) - else: - click.echo( - "\nPackage uploaded successfully! \n" - "Visit %s to publish." % api.url("packages/%s" % package_id) - ) - - -def protocols(api, local, json_flag): - """List protocols within your manifest or organization.""" - if not local: - protocol_objs = api.get_protocols() - else: - manifest = load_manifest() - if "protocols" not in list(manifest.keys()) or not manifest["protocols"]: - click.echo( - "Your manifest.json file doesn't contain any protocols or" - " is improperly formatted." - ) - return - protocol_objs = manifest["protocols"] - if json_flag: - click.echo(json.dumps(protocol_objs)) - else: - click.echo( - "\n{:^60}".format( - "Protocols within this {}:".format( - "organization" if not local else "manifest" - ) - ) - ) - click.echo(f"{'':-^60}") - for p in protocol_objs: - if p.get("display_name"): - display_str = f"{p['name']} ({p.get('display_name')})" - else: - display_str = p["name"] - click.echo(f"{display_str}\n{'':-^60}") - - -def packages(api, i): - """List packages in your organization.""" - response = api.packages() - # there's probably a better way to do this - package_names = OrderedDict( - sorted(list({"yours": {}, "theirs": {}}.items()), key=lambda t: len(t[0])) - ) - - for pack in response: - n = str(pack["name"]).lower().replace(f"com.{api.organization_id}.", "") - - latest = str(pack["latest_version"]) if pack["latest_version"] else "-" - - if pack.get("owner") and pack["owner"]["email"] == api.email: - package_names["yours"][n] = {} - package_names["yours"][n]["id"] = str(pack["id"]) - package_names["yours"][n]["latest"] = latest - else: - package_names["theirs"][n] = {} - package_names["theirs"][n]["id"] = str(pack["id"]) - package_names["theirs"][n]["latest"] = latest - if i: - return dict( - list(package_names["yours"].items()) + list(package_names["theirs"].items()) - ) - else: - for category, packages in list(package_names.items()): - if category == "yours": - click.echo("\n{:^90}".format("YOUR PACKAGES:\n")) - click.echo( - f"{'PACKAGE NAME':^30}" - + "|" - + f"{'PACKAGE ID':^30}" - + "|" - + f"{'LATEST PUBLISHED RELEASE':^30}" - ) - click.echo(f"{'':-^90}") - elif category == "theirs" and list(packages.values()): - click.echo("\n{:^90}".format("OTHER PACKAGES IN YOUR ORG:\n")) - click.echo( - f"{'PACKAGE NAME':^30}" - + "|" - + f"{'PACKAGE ID':^30}" - + "|" - + f"{'LATEST PUBLISHED RELEASE':^30}" - ) - click.echo(f"{'':-^90}") - for name, p in list(packages.items()): - click.echo( - f"{name:<30}" + "|" + f"{p['id']:^30}" + "|" + f"{p['latest']:^30}" - ) - click.echo(f"{'':-^90}") - - -def create_package(api, description, name): - """Create a new empty protocol package""" - existing = api.packages() - for p in existing: - if name == p["name"].split(".")[-1]: - click.echo( - f"You already have an existing package with the name " - f'"{name}". Please choose a different package name.' - ) - return - try: - new_pack = api.create_package(name, description) - if new_pack: - click.echo( - "New package '%s' created with id %s \n" - "View it at %s" - % (name, new_pack["id"], api.url("packages/%s" % new_pack["id"])) - ) - else: - click.echo("There was an error creating this package.") - except Exception as err: - click.echo("\n" + str(err)) - - -def delete_package(api, name, force): - """Delete an existing protocol package""" - package_id = get_package_id(api, name) - if package_id: - try: - if not force: - click.confirm( - "Are you sure you want to permanently delete the package " - "'%s'? All releases within will be lost." - % get_package_name(api, package_id), - default=False, - abort=True, - ) - click.confirm("Are you really really sure?", default=True) - del_pack = api.delete_package(package_id=package_id) - if del_pack: - click.echo("Package deleted.") - else: - click.echo("There was a problem deleting this package.") - except Exception as err: - click.echo("\n" + str(err)) - - -def generate_protocol(name): - """Generate a python protocol scaffold""" - # TODO we should update click to be like rails - # transcriptic generate protocol FOO - # transcriptic generate other_type FOO - env = Environment(loader=PackageLoader("transcriptic", "templates")) - template_infos = [ - {"template_name": "manifest.json.jinja", "file_name": "manifest.json"}, - {"template_name": "protocol.py.jinja", "file_name": f"{name}.py"}, - {"template_name": "requirements.txt.jinja", "file_name": "requirements.txt"}, - ] - - # make directory for protocol - dirname = name - makedirs(dirname, exist_ok=True) - - # write __init__ package file - open(f"{dirname}/{'__init__.py'}", "w").write("") - - for template_info in template_infos: - template_name = template_info["template_name"] - file_name = template_info["file_name"] - template = env.get_template(template_name) - file = open(f"{dirname}/{file_name}", "w") - output = template.render(name=name) - file.write(output) - - click.echo(f"Successfully generated protocol '{name}'!") - click.echo("Testing the protocol is as easy as:") - click.echo("") - click.echo(f"\tcd {name}") - click.echo("\tpip install -r requirements.txt") - click.echo(f"\ttranscriptic preview {name}") - click.echo(f"\ttranscriptic launch --local {name} -p SOME_PROJECT_ID") - - -def upload_dataset(api, file_path, title, run_id, tool, version): - """Uploads specified file as an analysis dataset to the specified run.""" - resp = api.upload_dataset_from_filepath( - file_path=file_path, - title=title, - run_id=run_id, - analysis_tool=tool, - analysis_tool_version=version, - ) - try: - data_id = resp["data"]["id"] - run_route = api.url(f"/api/runs/{run_id}?fields[runs]=project_id") - run_resp = api.get(run_route) - project = run_resp["data"]["attributes"]["project_id"] - datasets_route = api.get_route("datasets", project_id=project, run_id=run_id) - data_url = f"{datasets_route}/analysis/{data_id}" - click.echo(f"Dataset uploaded to {data_url}") - except KeyError: - click.echo("An unexpected response was returned from the server. ") - - -def projects( - api: Connection, - i: any = None, - json_flag: bool = False, - names_only: bool = False, -): - """ - List the projects in your organization. - - When no options are specified, returns a summarized format. - - Parameters - ---------- - api: Connection - API context used for making base calls - i: any, optional - DEPRECATED option. See `names_only`. - json_flag: bool, optional - Returns the full response which is json formatted. - names_only: bool, optional - Returns a `project_id: project_name` mapping. - """ - if i: - warnings.warn( - "`i` will be deprecated in the future. Please use `names_only` instead.", - FutureWarning, - ) - names_only = True - - response = api.projects() - - proj_id_names = {} - all_proj = {} - for proj in response: - status = " (archived)" if proj["archived_at"] else "" - proj_id_names[proj["id"]] = proj["name"] - all_proj[proj["id"]] = proj["name"] + status - - if names_only: - return proj_id_names - elif json_flag: - return response - else: - return all_proj - - -def runs(api, project_name, json_flag): - """List the runs that exist in a project""" - project_id = get_project_id(api, project_name) - run_list = [] - if project_id: - req = api.runs(project_id=project_id) - if not req: - click.echo(f"Project '{project_name}' is empty.") - return - for r in req: - run_list.append( - [ - r["title"] or "(Untitled)", - r["id"], - r["completed_at"].split("T")[0] - if r["completed_at"] - else r["created_at"].split("T")[0], - r["status"].replace("_", " "), - ] - ) - if json_flag: - extraction = map( - lambda x: { - "title": x["title"] or "(Untitled)", - "id": x["id"], - "completed_at": x["completed_at"] if x["completed_at"] else None, - "created_at": x["created_at"], - "status": x["status"], - }, - req, - ) - - return click.echo(json.dumps(extraction)) - else: - click.echo( - "\n{:^120}".format( - "Runs in Project '%s':\n" % get_project_name(api, project_id) - ) - ) - click.echo( - f"{'RUN TITLE':^30}" - + "|" - + f"{'RUN ID':^30}" - + "|" - + f"{'RUN DATE':^30}" - + "|" - + f"{'RUN STATUS':^30}" - ) - click.echo(f"{'':-^120}") - for run in run_list: - click.echo( - f"{run[0]:^30}" - + "|" - + f"{run[1]:^30}" - + "|" - + f"{run[2]:^30}" - + "|" - + f"{run[3]:^30}" - ) - click.echo(f"{'':-^120}") - - -def create_project(api, name, dev): - """Create a new empty project.""" - existing = api.projects() - for p in existing: - if name == p["name"].split(".")[-1]: - click.confirm( - f"You already have an existing project with the name '{name}'. " - f"Are you sure you want to create another one?", - default=False, - abort=True, - ) - break - try: - new_proj = api.create_project(name) - click.echo( - "New%s project '%s' created with id %s \nView it at %s" - % ( - " pilot" if dev else "", - name, - new_proj["id"], - api.url("%s" % (new_proj["id"])), - ) - ) - except RuntimeError: - click.echo("There was an error creating this project.") - - -def delete_project(api, name, force): - """Delete an existing project.""" - project_id = get_project_id(api, name) - if project_id: - if not force: - click.confirm( - "Are you sure you want to permanently delete '%s'?" - % get_project_name(api, project_id), - default=False, - abort=True, - ) - if api.delete_project(project_id=str(project_id)): - click.echo("Project deleted.") - else: - click.confirm( - "Could not delete project. This may be because it contains \ - runs. Try archiving it instead?", - default=False, - abort=True, - ) - if api.archive_project(project_id=str(project_id)): - click.echo("Project archived.") - else: - click.echo("Could not archive project!") - - -def resources(api, query): - """Search catalog of provisionable resources""" - resource_req = api.resources(query) - if resource_req["results"]: - kit_req = api.kits(query) - if not kit_req["results"]: - common_name = resource_req["results"][0]["name"] - kit_req = api.kits(common_name) - flat_items = list( - flatmap( - lambda x: [ - { - "name": y["resource"]["name"], - "id": y["resource"]["id"], - "vendor": x["vendor"]["name"] - if "vendor" in list(x.keys()) - else "", - } - for y in x["kit_items"] - if (y["provisionable"] and not y["reservable"]) - ], - kit_req["results"], - ) - ) - rs_id_list = [rs["id"] for rs in resource_req["results"]] - - matched_resources = [] - for item in flat_items: - if item["id"] in rs_id_list and item not in matched_resources: - matched_resources.append(item) - - if matched_resources: - click.echo(f"Results for '{query}':") - click.echo( - f"{'Resource Name':^40}" - + "|" - + f"{'Vendor':^40}" - + "|" - + f"{'Resource ID':^40}" - ) - click.echo(f"{'':-^120}") - for resource in matched_resources: - click.echo( - f"{ascii_encode(resource['name']):^40}" - + "|" - + f"{ascii_encode(resource['vendor']):^40}" - + "|" - + f"{ascii_encode(resource['id']):^40}" - ) - click.echo(f"{'':-^120}") - else: - click.echo(f"No usable resource for '{query}'.") - else: - click.echo(f"No results for '{query}'.") - - -def inventory(api, include_aliquots, show_status, retrieve_all, query): - """Search organization for inventory""" - inventory_req = api.inventory(query) - num_pages = inventory_req["num_pages"] - per_page = inventory_req["per_page"] - results = inventory_req["results"] - max_results_bound = num_pages * per_page - - num_prefiltered = len(results) - - if retrieve_all: - for i in range(1, num_pages): - click.echo( - f"Retrieved {i * per_page} records out of " - f"{max_results_bound} total for '{query}'...\r", - nl=False, - ) - inventory_req = api.inventory(query, page=i) - results.extend(inventory_req["results"]) - click.echo() - - if include_aliquots: - results = [c if "label" in c else c["container"] for c in results] - else: - results = [c for c in results if "label" in c] - results = [i for n, i in enumerate(results) if i not in results[n + 1 :]] - - if results: - - def truncate_time(d, k): - old_time = d[k] - d[k] = old_time.split("T")[0] - return d - - results = [truncate_time(c, "created_at") for c in results] - barcode_present = any(c["barcode"] for c in results) - keys = ["label", "id", "container_type_id", "storage_condition", "created_at"] - if barcode_present: - keys.insert(2, "barcode") - if show_status: - keys.append("status") - friendly_keys = {k: k.split("_")[0] for k in keys} - spacing = { - k: max(len(friendly_keys[k]), max([len(str(c[k])) for c in results])) - for k in keys - } - spacing = {k: (v // 2 + 1) * 2 + 1 for k, v in spacing.items()} - sum_spacing = sum(spacing.values()) + (len(keys) - 1) * 3 + 1 - spacing = {k: "{:^%s}" % v for k, v in spacing.items()} - sum_spacing = "{:-^%s}" % sum_spacing - click.echo(f"Results for '{query}':") - click.echo(" | ".join([spacing[k].format(friendly_keys[k]) for k in keys])) - click.echo(sum_spacing.format("")) - for c in results: - click.echo( - " | ".join([spacing[k].format(ascii_encode(c[k])) for k in keys]) - ) - click.echo(sum_spacing.format("")) - if not retrieve_all: - if num_pages > 1: - click.echo( - f"Retrieved {num_prefiltered} records out of " - f"{max_results_bound} total (use the --retrieve_all " - f"flag to request all records)." - ) - else: - if retrieve_all: - click.echo(f"No results for '{query}'.") - else: - if num_pages > 1: - click.echo( - f"Retrieved {num_prefiltered} records out of " - f"{max_results_bound} total but all were filtered " - f"out. Use the --retrieve_all flag to request all " - f"records." - ) - else: - click.echo( - "All records were filtered out. Use flags to modify your search" - ) - - -def payments(api): - """Lists available payment methods""" - methods = api.payment_methods() - click.echo(f"{'Method':^50}" + "|" + f"{'Expiry':^20}" + "|" + f"{'Id':^20}") - click.echo(f"{'':-^90}") - if len(methods) == 0: - print_stderr("No payment methods found.") - return - for method in methods: - if method["type"] == "CreditCard": - description = ( - f"{method['credit_card_type']} ending with " - f"{method['credit_card_last_4']}" - ) - elif method["type"] == "PurchaseOrder": - description = f"Purchase Order \"{method['description']}\"" - else: - description = method["description"] - if method["is_default?"]: - description += " (Default)" - if not method["is_valid"]: - description += " (Invalid)" - click.echo( - f"{ascii_encode(description):^50}" - + "|" - + f"{ascii_encode(method['expiry']):^20}" - + "|" - + f"{ascii_encode(method['id']):^20}" - ) - - -def init(path): - """Initialize a directory with a manifest.json file.""" - manifest_data = OrderedDict( - format="python", - license="MIT", - protocols=[ - { - "name": "SampleProtocol", - "version": "0.0.1", - "display_name": "Sample Protocol", - "description": "This is a protocol.", - "command_string": "python sample_protocol.py", - "inputs": {}, - "preview": {"refs": {}, "parameters": {}}, - } - ], - ) - try: - os.makedirs(path) - except OSError: - click.echo("Specified directory already exists.") - if isfile(f"{path}/manifest.json"): - click.confirm( - "This directory already contains a manifest.json file, " - "would you like to overwrite it with an empty one? ", - default=False, - abort=True, - ) - with open(f"{path}/manifest.json", "w+") as f: - click.echo("Creating empty manifest.json...") - f.write(json.dumps(dict(manifest_data), indent=2)) - click.echo("manifest.json created") - - -def analyze(api, file, test): - """Analyze a block of Autoprotocol JSON.""" - with click.open_file(file, "r") as f: - try: - protocol = json.loads(f.read()) - except ValueError: - click.echo( - "Error: The Autoprotocol you're trying to analyze is " - "not properly formatted. \n" - "Check that your manifest.json file is " - "valid JSON and/or your script " - "doesn't print anything other than pure Autoprotocol " - "to standard out." - ) - return - - try: - analysis = api.analyze_run(protocol, test_mode=test) - click.echo("\u2713 Protocol analyzed") - format_analysis(analysis) - except Exception as err: - click.echo("\n" + str(err)) - - -def preview(api, protocol_name, view, dye_test): - """Preview the Autoprotocol output of protocol in the current package.""" - manifest, protocol = load_manifest_and_protocol(protocol_name) - - try: - inputs = protocol["preview"] - except KeyError: - click.echo( - "Error: The manifest.json you're trying to preview doesn't " - 'contain a "preview" section' - ) - return - - run_protocol(api, manifest, protocol, inputs, view, dye_test) - - -def summarize(api, file, html, tree, lookup, runtime): - with click.open_file(file, "r") as f: - try: - protocol = json.loads(f.read()) - except ValueError: - click.echo("The autoprotocol you're trying to summarize is invalid.") - return - - if html: - url = ProtocolPreview(protocol, api).preview_url - click.echo(f"View your protocol here {url}") - return - - parser = AutoprotocolParser(protocol, api=api) - - if tree: - import multiprocessing - - print("\nGenerating Job Tree...") - p = multiprocessing.Process(target=parser.job_tree) - p.start() - - # Wait for seconds or until process finishes - p.join(runtime) - - # If thread is still active - if p.is_alive(): - print("Still running... Aborting tree construction.") - print( - "Please allow for more runtime allowance, or opt for no tree construction.\n" - ) - - # Terminate - p.terminate() - p.join() - else: - print("\nYour Job Tree is complete!\n") - - -def compile(protocol_name, args): - """Compile a protocol by passing it a config file (without submitting or - analyzing).""" - manifest, protocol = load_manifest_and_protocol(protocol_name) - - try: - command = protocol["command_string"] - except KeyError: - click.echo( - 'Error: Your manifest.json file does not have a "command_string" \ - key.' - ) - return - from subprocess import call - - call(["bash", "-c", command + " " + " ".join(args)]) - - -def launch( - api: Connection, - protocol: str, - project: str, - title: str, - save_input: bool, - local: bool, - accept_quote: bool, - params: str, - pm: str = None, - test: bool = None, - pkg: str = None, - predecessor_id: str = None, - save_preview: bool = False, -): - """Configure and launch a protocol either using the local manifest file or remotely. - If no parameters are specified, uses the webapp to select the inputs.""" - # Validate payment method - if pm is not None and not is_valid_payment_method(api, pm): - print_stderr( - "Payment method is invalid. Please specify a payment " - "method from `transcriptic payments` or not specify the " - "`--payment` flag to use the default payment method." - ) - return - - # Project is required for quick launch - if not project: - click.echo( - "Project field is required if parameters file is not specified and is required for Run submission." - ) - return - else: - project = get_project_id(api, project) - if not project: - return - - # Load protocol from local file if not remote and load from listed protocols otherwise - if not local: - print_stderr( - f"Searching for {protocol} in organization {api.organization_id}..." - ) - matched_protocols = [ - p - for p in api.get_protocols() - if (p["name"] == protocol and (pkg is None or p["package_id"] == pkg)) - ] - - if len(matched_protocols) == 0: - print_stderr( - f"Protocol {protocol} in " - f"{f'package {pkg}' if pkg else 'unspecified package'} " - f"was not found." - ) - return - elif len(matched_protocols) > 1: - print_stderr("More than one match found. Using the first match.") - else: - print_stderr("Protocol found.") - protocol_obj = matched_protocols[0] - else: - manifest, protocol_obj = load_manifest_and_protocol(protocol) - - # For remote execution, use input params file if specified, else use quick_launch inputs - if not params: - # If parameters are not specified, use quick launch to get inputs - # Creates web browser and generates inputs for quick_launch - quick_launch = _get_quick_launch(api, protocol_obj, project) - params = dict(parameters=quick_launch["raw_inputs"]) - else: - try: - params = json.loads(params.read()) - except ValueError: - print_stderr( - "Unable to load parameters given. " - "File is probably incorrectly formatted." - ) - return - - # Save parameters to file if specified - if save_input: - try: - with click.open_file(save_input, "w") as f: - f.write(json.dumps(params, indent=2)) - except Exception as e: - print_stderr("\nUnable to save inputs: %s" % str(e)) - - if save_preview: - pp = PreviewParameters(api, params["parameters"], protocol_obj) - # Read manifest.json and write updated manifest to working dir - try: - pp.merge(load_manifest()) - with click.open_file("manifest.json", "w") as f: - f.write(json.dumps(pp.merged_manifest, indent=2)) - f.close() - except Exception as e: - print_stderr( - f"\nUnable to save preview inputs due to not being" - f" able to process: {type(e)} {str(e)}" - ) - - if not local: - req_id, launch_protocol = _get_launch_request(api, params, protocol_obj, test) - - # Check for generation errors - generation_errs = launch_protocol["generation_errors"] - - if len(generation_errs) > 0: - for errors in generation_errs: - click.echo("\n\n" + str(errors["message"])) - if errors.get("info"): - errors_info = errors.get("info") - indexes = [ - idx - for idx in range(len(errors_info)) - if errors_info.startswith("Error", idx) - or errors_info.startswith("error", idx) - ] - # 100 length should give enough information - errors_info_msgs = [ - str(errors_info[idx : idx + 100]) for idx in indexes - ] - for info_msg in errors_info_msgs: - click.echo("\n" + info_msg) - - click.echo("\nPlease fix the above errors and try again.") - return - - # Confirm proceeding with purchase - if not accept_quote: - click.echo("\n\nCost Breakdown") - resp = api.analyze_launch_request(req_id, test_mode=test) - click.echo(price(resp)) - confirmed = click.confirm( - "Would you like to continue with launching the protocol", - prompt_suffix="? ", - default=False, - ) - if not confirmed: - return - - from time import gmtime, strftime - - if title: - run_title = title - else: - run_title = f"{protocol}_{strftime('%b_%d_%Y', gmtime())}" - - try: - req_json = api.submit_launch_request( - req_id, - protocol_id=protocol_obj["id"], - project_id=project, - title=run_title, - test_mode=test, - payment_method_id=pm, - predecessor_id=predecessor_id, - ) - run_id = req_json["id"] - formatted_url = api.url(f"{project}/runs/{run_id}") - click.echo(f"\nRun created: {formatted_url}") - return formatted_url - except Exception as err: - click.echo("\n" + str(err)) - else: - print_stderr("\nGenerating Autoprotocol....\n") - if not params: - run_protocol(api, manifest, protocol_obj, quick_launch["inputs"]) - else: - """ - In the case of a local `launch`, we need to generate `inputs` from - `raw_inputs`, since the `run_protocol` function takes in JSON which - is `inputs`-formatted. - `inputs` is basically an extended version of `raw_inputs`, where - we populate properties such as aliquot information for specified - containerIds. - In order to generate these `inputs`, we can create a new quick - launch - """ - # This is the input format required by resolve_inputs - formatted_inputs = dict(inputs=params["parameters"]) - - quick_launch = api.create_quick_launch( - data=json.dumps({"manifest": protocol_obj}), project_id=project - ) - quick_launch_obj = api.resolve_quick_launch_inputs( - formatted_inputs, project_id=project, quick_launch_id=quick_launch["id"] - ) - inputs = quick_launch_obj["inputs"] - run_protocol(api, manifest, protocol_obj, inputs) - - -def select_org(api, config, organization=None): - """Allows you to switch organizations. If the organization argument - is provided, this will directly select the specified organization. - """ - org_list = [ - {"name": org["name"], "subdomain": org["subdomain"]} - for org in api.organizations() - ] - if organization is None: - organization = org_prompt(org_list) - - r = api.get_organization(org_id=organization) - if r.status_code != 200: - click.echo(f"Error accessing organization: {r.text}") - sys.exit(1) - - api.organization_id = organization - api.save(config) - click.echo(f"Logged in with organization: {organization}") - - -def login(api, config, api_root=None, analytics=True, rsa_key=None): - """Authenticate to your Transcriptic account.""" - if api_root is None: - # Always default to the pre-defined api-root if possible, else use - # the secure.transcriptic.com domain - try: - api_root = api.api_root - except ValueError: - api_root = "https://secure.strateos.com" - - rsa_auth = None - rsa_key_path = None - if rsa_key is not None: - try: - rsa_key_path = abspath(expanduser(rsa_key)) - with open(rsa_key_path, "rb") as key_file: - rsa_secret = key_file.read() - except Exception: - click.echo( - f"Error loading RSA key. Please check that the file " - f"{rsa_key} is accessible", - err=True, - ) - sys.exit(1) - - # Try making an auth handler with a dummy email so that the command - # fails early - try: - rsa_auth = StrateosSign("foo@bar.com", rsa_secret, api_root) - except Exception as e: - click.echo(f"Error loading RSA key: {e}", err=True) - sys.exit(1) - - email = click.prompt("Email") - password = click.prompt("Password", hide_input=True) - - # replace the dummy rsa_auth with a handler using the given email - if rsa_auth is not None: - rsa_auth = StrateosSign(email, rsa_auth.secret, api_root) - - try: - r = api.post( - routes.login(api_root=api_root), - data=json.dumps( - { - "user": { - "email": email, - "password": password, - }, - } - ), - headers={ - "Accept": "application/json", - "Content-Type": "application/json", - }, - status_response={ - "200": lambda resp: resp, - "401": lambda resp: resp, - "default": lambda resp: resp, - }, - auth=rsa_auth, - ) - - except requests.exceptions.RequestException: - click.echo( - f"Error logging into specified host: {api_root}. " - f"Please check your internet connection and host name" - ) - sys.exit(1) - - except Exception as e: - click.echo(f"Error connecting to host: {e}") - sys.exit(1) - - if r.status_code != 200: - click.echo(f"Error logging into Transcriptic: {r.json()['error']}") - sys.exit(1) - user = r.json() - token = user.get("authentication_token") or user["test_mode_authentication_token"] - user_id = user.get("id") - feature_groups = user.get("feature_groups") - organization = org_prompt(user["organizations"]) - - try: - r = api.get( - routes.get_organization(api_root=api_root, org_id=organization), - headers={ - "X-User-Email": email, - "X-User-Token": token, - "Accept": "application/json", - }, - auth=rsa_auth, - status_response={"200": lambda resp: resp, "default": lambda resp: resp}, - ) - except PermissionError as e: - click.echo(e) - if rsa_key is not None: - click.echo("Are you sure you require the `--rsa-key` option?") - sys.exit(1) - - if r.status_code != 200: - click.echo(f"Error accessing organization: {r.text}") - sys.exit(1) - api = Connection( - email=email, - token=token, - organization_id=organization, - api_root=api_root, - user_id=user_id, - analytics=analytics, - feature_groups=feature_groups, - rsa_key=rsa_key_path, - ) - api.save(config) - click.echo(f"Logged in as {user['email']} ({organization})") - - -def format(manifest): - """Check Autoprotocol format of manifest.json.""" - manifest = parse_json(manifest) - try: - iter_json(manifest) - click.echo("No manifest formatting errors found.") - except RuntimeError: - pass - - -###################### -# UTIL -###################### - - -def is_valid_payment_method(api, id): - """Determines if payment is valid""" - methods = api.payment_methods() - return any([id == method["id"] and method["is_valid"] for method in methods]) - - -def format_analysis(response): - def count(thing, things, num): - click.echo(f" {num} {thing if num == 1 else things}") - - count("instruction", "instructions", len(response["instructions"])) - count("container", "containers", len(response["refs"])) - price(response) - for w in response["warnings"]: - message = w["message"] - if "instruction" in w["context"]: - context = f"instruction {w['context']['instruction']}" - else: - context = json.dumps(w["context"]) - click.echo(f"WARNING ({context}): {message}") - - -def price(response): - """Prints out price based on response""" - - # quote won't appear in response if user is missing permissions. - if "quote" not in response or "items" not in response["quote"]: - return - - locale.setlocale(locale.LC_ALL, "en_US.UTF-8") - separator_len = 24 - - for quote_item in response["quote"]["items"]: - locale_cost = locale.currency(float(quote_item["cost"]), grouping=True) - quote_str = f" {quote_item['title']}: {locale_cost}" - click.echo(quote_str) - separator_len = max(separator_len, len(quote_str)) - - click.echo("-" * separator_len) - - click.echo( - " Total Cost: %s" - % locale.currency(float(response["total_cost"]), grouping=True) - ) - - -def _create_launch_request(params, bsl=1, test_mode=False): - """Creates launch_request from input params""" - params_dict = dict() - params_dict["launch_request"] = params - params_dict["launch_request"]["bsl"] = bsl - params_dict["launch_request"]["test_mode"] = test_mode - return json.dumps(params_dict) - - -def _get_launch_request(api, params, protocol, test_mode): - """Launches protocol from parameters""" - launch_request = _create_launch_request(params, test_mode=test_mode) - - launch_protocol = api.launch_protocol(launch_request, protocol_id=protocol["id"]) - launch_request_id = launch_protocol["id"] - - # Wait until launch request is updated (max 5 minutes) - count = 1 - while count <= 150 and launch_protocol["progress"] != 100: - sys.stderr.write( - "\rWaiting for launch request to be configured%s" % ("." * count) - ) - sys.stderr.flush() - time.sleep(2) - launch_protocol = api.get_launch_request( - protocol_id=protocol["id"], launch_request_id=launch_request_id - ) - count += 1 - - return launch_request_id, launch_protocol - - -def _get_quick_launch(api, protocol, project): - """Creates quick launch object and opens it in a new tab""" - quick_launch = api.create_quick_launch( - data=json.dumps({"manifest": protocol}), project_id=project - ) - quick_launch_mtime = quick_launch["updated_at"] - - format_str = "\nOpening %s" - url = api.get_route( - "get_quick_launch", project_id=project, quick_launch_id=quick_launch["id"] - ) - print_stderr(format_str % url) - - """ - Open the URL in the webbrowser. We have to temporarily suppress stdout/ - stderr because the webbrowser module dumps some garbage which gets into - out stdout and corrupts the generated autoprotocol - """ - import webbrowser - - with stdchannel_redirected(sys.stderr, os.devnull): - with stdchannel_redirected(sys.stdout, os.devnull): - webbrowser.open_new_tab(url) - - # Wait until the quick launch inputs are updated (max 15 minutes) - count = 1 - while ( - count <= 180 - and quick_launch["inputs"] is None - or quick_launch_mtime >= quick_launch["updated_at"] - ): - sys.stderr.write("\rWaiting for inputs to be configured%s" % ("." * count)) - sys.stderr.flush() - time.sleep(5) - - quick_launch = api.get_quick_launch( - project_id=project, quick_launch_id=quick_launch["id"] - ) - count += 1 - return quick_launch - - -def org_prompt(org_list): - """Organization prompt for helping with selecting organization""" - if len(org_list) < 1: - click.echo( - f"Error: You don't appear to belong to any organizations. \n" - f"Visit {'https://secure.transcriptic.com'} and create an " - f"organization." - ) - sys.exit(1) - if len(org_list) == 1: - organization = org_list[0]["subdomain"] - else: - click.echo("You belong to %s organizations:" % len(org_list)) - for indx, o in enumerate(org_list): - click.echo(f"{indx + 1}. {o['name']} ({o['subdomain']})") - - def parse_valid_org(indx): - try: - org_indx = int(indx) - 1 - if org_indx < 0 or org_indx >= len(org_list): - raise ValueError("Value out of range") - return org_list[org_indx]["subdomain"] - except: - raise BadParameter( - "Please enter an integer between 1 and %s" % (len(org_list)) - ) - - organization = click.prompt( - "Which organization would you like to log in as", - default=1, - prompt_suffix="? ", - type=int, - value_proc=lambda x: parse_valid_org(x), - ) - # Catch since `value_proc` doesn't properly parse default - if organization == 1: - organization = org_list[0]["subdomain"] - return organization - - -def get_project_id(api, name): - project_id_name_mapping = projects(api, names_only=True) - if name in project_id_name_mapping: - return name - else: - project_ids = [k for k, v in project_id_name_mapping.items() if v == name] - if not project_ids: - click.echo(f"The project '{name}' was not found in your organization.") - return - elif len(project_ids) > 1: - click.echo(f"Found multiple projects: {project_ids} that match '{name}'.") - # TODO: Add project selector with dates and number of runs - return - else: - return project_ids[0] - - -def get_project_name(api, id): - project_id_name_mapping = projects(api, names_only=True) - name = project_id_name_mapping.get(id) - if not name: - name = id if id in project_id_name_mapping.values() else None - if not name: - click.echo(f"The project '{name}' was not found in your organization.") - return - return name - - -def get_package_id(api, name): - package_names = packages(api, True) - package_names = {k.lower(): v["id"] for k, v in list(package_names.items())} - package_id = package_names.get(name) - if not package_id: - package_id = name if name in list(package_names.values()) else None - if not package_id: - click.echo(f"The package '{name}' does not exist in your organization.") - return - return package_id - - -def get_package_name(api, package_id): - package_names_all = packages(api, True) - package_names = {v["id"]: k for k, v in list(package_names_all.items())} - package_name = package_names.get(package_id) - if not package_name: - package_name = ( - package_id if package_id in list(package_names.values()) else None - ) - if not package_name: - click.echo( - f"The id '{package_id}' does not match any package in your " - f"organization." - ) - return - return package_name - - -def load_manifest(): - try: - with click.open_file("manifest.json", "r") as f: - manifest = json.loads(f.read(), object_pairs_hook=OrderedDict) - except IOError: - click.echo("The current directory does not contain a manifest.json file.") - sys.exit(1) - except ValueError: - click.echo( - "Error: Your manifest.json file is improperly formatted. " - "Please double check your brackets and commas!" - ) - sys.exit(1) - return manifest - - -def load_protocol(manifest, protocol_name): - try: - p = next(p for p in manifest["protocols"] if p["name"] == protocol_name) - except KeyError: - click.echo( - 'Error: Your manifest.json file does not have a "protocols" \ - key.' - ) - sys.exit(1) - except StopIteration: - click.echo( - f"Error: The protocol name '{protocol_name}' does not match " - f"any protocols that can be previewed from within this " - f"directory. \n" - f"Check either your protocol's spelling or your " - f"manifest.json file and try again." - ) - sys.exit(1) - return p - - -def load_manifest_and_protocol(protocol_name): - manifest = load_manifest() - protocol = load_protocol(manifest, protocol_name) - return (manifest, protocol) - - -def run_protocol(api, manifest, protocol, inputs, view=False, dye_test=False): - try: - command = protocol["command_string"] - except KeyError: - click.echo( - 'Error: Your manifest.json file does not have a "command_string" \ - key.' - ) - return - - import tempfile - - from subprocess import CalledProcessError, check_output - - with tempfile.NamedTemporaryFile() as fp: - fp.write(bytes(json.dumps(inputs), "UTF-8")) - fp.flush() - try: - if dye_test: - protocol = check_output( - ["bash", "-c", command + " " + fp.name + " --dye_test"] - ) - else: - protocol = check_output(["bash", "-c", command + " " + fp.name]) - click.echo(protocol) - if view: - click.echo( - f"View your protocol's raw JSON above or see the " - f"instructions rendered at the following link: \n" - f"{ProtocolPreview(protocol, api).preview_url}" - ) - except CalledProcessError as e: - click.echo(e.output) - return - - -def validate_filter(filters, number_of_instructions): - invalid_filters = set() - - for arg in filters: - if arg.isdigit(): - idx = int(arg) - if 0 > idx or idx >= number_of_instructions: - invalid_filters.add(arg) - elif re.match(r"\d+-\d+", arg): - [s, e] = [int(v) for v in arg.split("-")] - if s > e or number_of_instructions <= e: - invalid_filters.add(arg) - elif ":" in arg: - tokens = arg.split(":") - if len(tokens) != 2: - invalid_filters.add(arg) - else: - invalid_filters.add(arg) - - return invalid_filters - - -def execute( - autoprotocol, - api, - no_redirect, - workcell_id, - device_set, - session_id, - time_limit, - schedule_at, - schedule_delay, - time_constraints_are_suggestion, - exclude, - include, - partition_group_size, - partition_horizon, - partitioning_swap_device_id, - email, -): - # Clean api end point - if api.startswith("http://"): - clean_api = api[7:] - elif api.startswith("https://"): - click.echo("HTTPS endpoint is not supported, falling back to HTTP.") - clean_api = api[8:] - else: - clean_api = api - if clean_api[-1] == "/": - clean_api = clean_api[0:-1] # remove trailing slash - - # Define the initial payload - payload = {"timeLimit": f"{time_limit}:second"} - - if schedule_delay is not None and schedule_at is not None: - click.echo( - "Error: '--schedule-delay' and '--schedule-at' are mutually exclusive.", - err=True, - ) - sys.exit(1) - - # Get the requested scheduling time - if schedule_delay is not None: - # round up to the next minute! - payload["delay"] = schedule_delay - elif schedule_at is not None: # absolute time - payload["scheduleAt"] = schedule_at - - # Get the autoprotocol - try: - autoprotocol_json = json.loads(autoprotocol.read()) - payload["autoprotocol"] = autoprotocol_json - except json.decoder.JSONDecodeError as err: - click.echo(f"Error decoding autoprotocol json: {err}", err=True) - sys.exit(1) - - # validate filters - num_instructions = len(autoprotocol_json["instructions"]) - invalid_exclude = validate_filter(exclude, num_instructions) - invalid_include = validate_filter(include, num_instructions) - if len(invalid_exclude) + len(invalid_include) > 0: - click.echo( - f"Error: invalid filters: {','.join(invalid_exclude.union(invalid_include))} (number of instructions: {num_instructions})", - err=True, - ) - sys.exit(1) - - # update the payload - payload["exclude"] = exclude - payload["include"] = include - - # device set resolution - in_use = [] - if device_set: - device_str = device_set.read() - try: - device_json = json.loads(device_str) - payload["deviceSet"] = device_json - except json.decoder.JSONDecodeError as err: - click.echo(f"Error decoding device set json: {err}", err=True) - sys.exit(1) - in_use.append("--device-set") - - if workcell_id: - if "." in workcell_id: - raise BadParameter(f"Workcell id can't have '.' but was {workcell_id}") - payload["workcellIdForDeviceSet"] = workcell_id - in_use.append("--workcell-id") - - if session_id is not None: - payload["sessionId"] = session_id - in_use.append("--session-id") - - if len(in_use) > 1: - click.echo(f"Error: {', '.join(in_use)} are mutually exclusive.", err=True) - sys.exit(1) - - if len(in_use) == 0: - payload["workcellIdForDeviceSet"] = "wctest-mcx1" - - # partition parameters - if partition_group_size is not None: - payload["partitionGroupSize"] = partition_group_size - - if partition_horizon is not None: - payload["partitionHorizon"] = f"{partition_horizon}:second" - - if partitioning_swap_device_id is not None: - payload["partitioningSwapDeviceId"] = partitioning_swap_device_id - - payload["timeConstraintsAreSuggestion"] = time_constraints_are_suggestion - - if no_redirect: - frontend_node_address = clean_api - else: - # Validate api - path_tokens = clean_api.split("/") - if len(path_tokens) != 3: - click.echo( - f"Error: Invalid api target, expects base-url/facility/workcell.", - err=True, - ) - sys.exit(1) - - clean_api = f"http://{clean_api}" - path_base = f"http://{path_tokens[0]}" - path_lab = path_tokens[1] - path_workcell = path_tokens[2] - - # get the scle test workcell endpoint - res = requests.get(f"{path_base}/app-config") - try: - res_json = json.loads(res.text) - if ( - res_json["hostManifest"] - and res_json["hostManifest"][path_lab] - and res_json["hostManifest"][path_lab][path_workcell] - ): - frontend_node_address = res_json["hostManifest"][path_lab][ - path_workcell - ]["url"] - else: - click.echo( - f"Error when getting frontend node address: {res_json}", err=True - ) - sys.exit(1) - except json.decoder.JSONDecodeError: - click.echo( - f"Error when getting frontend node address: {res.text}", err=True - ) - sys.exit(1) - - # add sentBy - if email is not None: - payload["sentBy"] = email.split("@")[0] - - # POST to workcell - test_run_endpoint = f"http://{frontend_node_address}/testRun" - click.echo(f"Sending request to {test_run_endpoint}") - res = requests.post(test_run_endpoint, json=payload) - try: - res_json = json.loads(res.text) - if res_json["success"]: - click.echo( - f"Success. View {clean_api}/dashboard to see the scheduling outcome." - ) - if "message" in res_json: - click.echo(res_json["message"]) - else: - click.echo(f"Error: {res_json['message']}", err=True) - if "sessionId" in res_json: - click.echo( - f"Dashboard can be seen at: {clean_api}/dashboard", - err=True, - ) - sys.exit(1) - except json.decoder.JSONDecodeError: - click.echo(f"Error: {res.text}", err=True) - sys.exit(1) - - -def parse_json(json_file): - try: - return json.load(open(json_file)) - except ValueError as e: - click.echo(f"Invalid json: {e}") - return None - - -def print_stderr(msg): - sys.stderr.write(msg + "\n") - sys.stderr.flush() - - -@contextmanager -def stdchannel_redirected(stdchannel, dest_filename): - """ - A context manager to temporarily redirect stdout or stderr - - e.g.: - - - with stdchannel_redirected(sys.stderr, os.devnull): - if compiler.has_function('clock_gettime', libraries=['rt']): - libraries.append('rt') - """ - try: - oldstdchannel = os.dup(stdchannel.fileno()) - dest_file = open(dest_filename, "w") - os.dup2(dest_file.fileno(), stdchannel.fileno()) - - yield - finally: - if oldstdchannel is not None: - os.dup2(oldstdchannel, stdchannel.fileno()) - if dest_file is not None: - dest_file.close() - - -# Placing this here since this is exclusively used by the CLI currently -class ProtocolPreview(object): - """ - An object for previewing protocols - """ - - def __init__(self, protocol, api): - self.protocol = protocol - preview_id = api.preview_protocol(protocol) - self.preview_url = api.get_route( - "preview_protocol_embed", preview_id=preview_id - ) - - def _repr_html_(self): - return f"""""" diff --git a/transcriptic/config.py b/transcriptic/config.py deleted file mode 100644 index a86ba887..00000000 --- a/transcriptic/config.py +++ /dev/null @@ -1,1231 +0,0 @@ -import http.client as http_client -import inspect -import io -import json -import logging -import os -import platform -import time -import warnings -import zipfile - -import requests -import transcriptic - -from Crypto.PublicKey import RSA - -from . import routes -from .auth import AuthSession, StrateosBearerAuth, StrateosSign -from .util import is_valid_jwt_token -from .version import __version__ - - -try: - import magic -except ImportError: - warnings.warn( - "`python-magic` is recommended. You may be missing some system-level " - "dependencies if you have already pip-installed it.\n" - "Please refer to https://github.com/ahupp/python-magic#installation " - "for more installation instructions." - ) - - -def initialize_default_session(): - """ - Initialize a default `requests.Session()` object which can be used for - requests into the tx web api. - """ - session = AuthSession() - session.headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": f"txpy/{__version__} " - f"({platform.python_implementation()}/" - f"{platform.python_version()}; " - f"{platform.system()}/{platform.release()}; " - f"{platform.machine()}; {platform.architecture()[0]})", - } - return session - - -class Connection(object): - """ - A Connection object is the object used for communicating with Transcriptic. - - Local usage: This is most easily instantiated by using the `from_file` - function after calling `transcriptic login` from the command line. - - .. code-block:: shell - :caption: shell - - $ transcriptic login - Email: me@example.com - Password: - Logged in as me@example.com (example-lab) - - .. code-block:: python - :caption: python - - from transcriptic.config import Connection - api = Connection.from_file("~/.transcriptic") - - For those using Jupyter notebooks on secure.strateos.com (beta), a - Connection object is automatically instantiated as api. - - .. code-block:: python - :caption: python - - from transcriptic import api - - The `api` object can then be used for making any api calls. It is - recommended to use the objects in `transcriptic.objects` since that wraps - the response in a more friendly format. - - Example Usage: - - .. code-block:: python - :caption: python - - api.projects() - api.runs(project_id="p123456789") - - If you have multiple organizations and would like to switch to a specific - organization, or if you would like to auto-load certain projects, you can - set it directly by assigning to the corresponding variable. - - Example Usage: - - .. code-block:: python - :caption: python - - api.organization_id = "my_other_org" - api.project_id = "p123" - - """ - - def __init__( - self, - email=None, - token=None, - organization_id=None, - api_root="https://secure.strateos.com", - cookie=None, - verbose=False, - analytics=True, - user_id="default", - feature_groups=[], - rsa_key=None, - session=None, - bearer_token=None, - ): - # Initialize environment args used for computing routes - self.env_args = dict() - self.api_root = api_root - - # Initialize session headers - if session is None: - session = initialize_default_session() - self.session = session - - self._bearer_token = None - - # Initialize RSA props - self._rsa_key = None - self._rsa_key_path = None - self._rsa_secret = None - # Set/Load rsa_key from argument as string or Path - self.rsa_key = rsa_key - - # NB: These many setattr calls update self.session.headers - # cookie authentication is mutually exclusive from token authentication - self.organization_id = organization_id - if cookie: - if email is not None or token is not None: - warnings.warn( - "Cookie and token authentication is mutually " - "exclusive. Ignoring email and token" - ) - self.session.headers["X-User-Email"] = None - self.session.headers["X-User-Token"] = None - self.cookie = cookie - self.update_session_auth(use_signature=False) - else: - if cookie is not None: - warnings.warn( - "Cookie and token authentication is mutually " - "exclusive. Ignoring cookie" - ) - self.session.headers["Cookie"] = None - self.email = email - if bearer_token is not None: - if token is not None: - warnings.warn( - "User token and bearer token authentication" - "is mutually exclusive. Ignoring user token" - ) - self.bearer_token = bearer_token - elif token is not None: - self.token = token - self.update_session_auth() - - # Initialize feature groups - self.feature_groups = feature_groups - - # Initialize CLI parameters - self.verbose = verbose - self.analytics = analytics - self.user_id = user_id - - transcriptic.api = self - - @staticmethod - def from_file(path): - """Loads connection from file""" - config_path = os.path.expanduser(path) - with open(config_path) as f: - cfg = json.load(f) - - expected_keys = set( - ("email", "token", "organization_id", "api_root", "analytics", "user_id") - ) - keys = set(cfg.keys()) - - if not keys.issuperset(expected_keys): - raise OSError( - f"Key(s) not found in configuration file ({config_path}) " - f"Missing {repr(expected_keys - keys)}" - ) - return Connection(**cfg) - - @staticmethod - def from_default_config(): - """ - Load the default configuration file from the home directory of the - current user and return a Connection instance that is costructed from it. - """ - return Connection.from_file("~/.transcriptic") - - @property - def api_root(self): - try: - return self.env_args["api_root"] - except (NameError, KeyError): - raise ValueError("api_root is not set.") - - @api_root.setter - def api_root(self, value): - self.update_environment(api_root=value) - - @property - def organization_id(self): - try: - return self.session.headers["X-Organization-Id"] - except (NameError, KeyError): - raise ValueError("organization_id is not set.") - - @organization_id.setter - def organization_id(self, value): - self.update_headers(**{"X-Organization-Id": value}) - self.update_environment(org_id=value) - - @property - def project_id(self): - try: - return self.env_args["project_id"] - except (NameError, KeyError): - raise ValueError("project_id is not set.") - - @project_id.setter - def project_id(self, value): - self.update_environment(project_id=value) - - @property - def email(self): - try: - return self.session.headers["X-User-Email"] - except (NameError, KeyError): - raise ValueError("email is not set.") - - @email.setter - def email(self, value): - if self.cookie is not None: - warnings.warn( - "Cookie and token authentication is mutually " - "exclusive. Clearing cookie from headers" - ) - self.update_headers(**{"Cookie": None}) - self.update_headers(**{"X-User-Email": value}) - self.update_session_auth() - - @property - def bearer_token(self): - return self._bearer_token - - @bearer_token.setter - def bearer_token(self, value): - if is_valid_jwt_token(value): - self._bearer_token = value - else: - raise ValueError("Malformed JWT Bearer Token") - - @property - def token(self): - try: - return self.session.headers["X-User-Token"] - except (NameError, KeyError): - raise ValueError("token is not set.") - - @token.setter - def token(self, value): - if self.cookie is not None: - warnings.warn( - "Cookie and token authentication is mutually " - "exclusive. Clearing cookie from headers" - ) - self.update_headers(**{"Cookie": None}) - self.update_headers(**{"X-User-Token": value}) - - @property - def cookie(self): - try: - return self.session.headers["Cookie"] - except (NameError, KeyError): - return ValueError("cookie is not set.") - - @cookie.setter - def cookie(self, value): - if self.email is not None or self.token is not None: - warnings.warn( - "Cookie and token authentication is mutually " - "exclusive. Clearing email and token from headers" - ) - self.update_headers(**{"X-User-Email": None, "X-User-Token": None}) - self.update_headers(**{"Cookie": value}) - - @property - def rsa_key(self): - return self._rsa_key - - @rsa_key.setter - def rsa_key(self, value): - self._rsa_key = value - self.load_rsa_secret() - - def get_container(self, container_id): - route = self.get_route( - "get_container", org_id=self.organization_id, container_id=container_id - ) - return self.get(route) - - def save(self, path): - """Saves current connection into specified file, used for CLI""" - with open(os.path.expanduser(path), "w") as f: - config_params = { - "email": self.email, - "token": self.token, - "organization_id": self.organization_id, - "api_root": self.api_root, - "analytics": self.analytics, - "user_id": self.user_id, - "feature_groups": self.feature_groups, - } - # We dont want to save a string key, only a path - if self._rsa_key is not None: - config_params["rsa_key"] = self._rsa_key_path - - f.write(json.dumps(config_params, indent=2)) - - def load_rsa_secret(self): - key_string_or_path = self._rsa_key - - if key_string_or_path is None: - self._rsa_key_path = None - self._rsa_secret = None - else: - try: # First try loading it as a key - key = RSA.import_key(key_string_or_path) - self._rsa_key_path = None - self._rsa_secret = key.export_key() - - except ValueError: # Then try as a Path - key_path = os.path.abspath(os.path.expanduser(key_string_or_path)) - with open(key_path, "rb") as key_file: - key = RSA.import_key(key_file.read()) - self._rsa_key_path = str(key_path) - self._rsa_secret = key.export_key() - - self.update_session_auth() - - def update_session_auth(self, use_signature=True): - if ( - use_signature - and self._rsa_secret - and "X-User-Email" in self.session.headers - ): - self.session.auth = StrateosSign( - self.email, self._rsa_secret, self.api_root - ) - elif self.bearer_token: - self.session.auth = StrateosBearerAuth(self.bearer_token, self.api_root) - else: - self.session.auth = None - - def update_environment(self, **kwargs): - """ - Updates environment variables used for computing routes. - To remove an existing variable, set value to None. - """ - self.env_args = dict(self.env_args, **kwargs) - - def update_headers(self, **kwargs): - """ - Updates session headers - To remove an existing variable, set value to None. - """ - self.session.headers = dict(self.session.headers, **kwargs) - - def url(self, path): - """url format helper""" - if path.startswith("/"): - return f"{self.api_root}{path}" - else: - return f"{self.api_root}/{self.organization_id}/{path}" - - def preview_protocol(self, protocol): - """Post protocol preview""" - route = self.get_route("preview_protocol") - protocol = _parse_protocol(protocol) - err_default = "Unable to preview protocol" - return self.post( - route, - json={"protocol": protocol}, - allow_redirects=False, - status_response={ - "200": lambda resp: resp.json()["key"], - "default": lambda resp: RuntimeError(err_default), - }, - ) - - def organizations(self): - """Get list of organizations""" - route = self.get_route("get_organizations") - return self.get(route) - - def get_organization(self, org_id=None): - """Get particular organization""" - route = self.get_route("get_organization", org_id=org_id) - err_404 = f"There was an error fetching the organization {org_id}" - resp = self.get( - route, - status_response={ - "200": lambda resp: resp, - "404": lambda resp: RuntimeError(err_404), - "default": lambda resp: resp, - }, - ) - return resp - - def projects(self): - """Get list of projects in organization""" - route = self.get_route("get_projects") - err_default = ( - "There was an error listing the projects in your " - "organization. Make sure your login details are correct." - ) - return self.get( - route, - status_response={ - "200": lambda resp: resp.json()["projects"], - "default": lambda resp: RuntimeError(err_default), - }, - ) - - def project(self, project_id=None): - """Get particular project""" - route = self.get_route("get_project", project_id=project_id) - err_default = f"There was an error fetching project {project_id}" - return self.get( - route, status_response={"default": lambda resp: RuntimeError(err_default)} - ) - - def runs(self, project_id=None): - """Get list of runs in project""" - route = self.get_route("get_project_runs", project_id=project_id) - err_default = ( - f"There was an error fetching the runs in project " f"{project_id}" - ) - results = [] - while True: - response = self.get( - route, - status_response={ - "200": lambda resp: resp.json(), - "default": lambda resp: RuntimeError(err_default), - }, - ) - results.extend( - [{**data["attributes"], "id": data["id"]} for data in response["data"]] - ) - if "next" in response["links"]: - route = response["links"]["next"] - else: - break - return results - - def create_project(self, title): - """Create project with given title""" - route = self.get_route("create_project") - return self.post(route, data=json.dumps({"name": title})) - - def delete_project(self, project_id=None): - """Delete project with given project_id""" - route = self.get_route("delete_project", project_id=project_id) - return self.delete(route, status_response={"200": lambda resp: True}) - - def archive_project(self, project_id=None): - """Archive project with given project_id""" - route = self.get_route("archive_project", project_id=project_id) - return self.put( - route, - data=json.dumps({"project": {"archived": True}}), - status_response={"200": lambda resp: True}, - ) - - def modify_aliquot_properties( - self, aliquot_id, set_properties={}, delete_properties=[] - ): - """ - Modify the properties of an alquot: - - If specified set_properties must be dict mapping {str: str} - these properties will be set on the aliquot specified. - - If specified delete_properties must be a list of string - properties which will be deleted on the aliquot. - """ - route = self.get_route("modify_aliquot_properties", aliquot_id=aliquot_id) - return self.put( - route, - json={ - "id": aliquot_id, - "data": {"set": set_properties, "delete": delete_properties}, - }, - ) - - def packages(self): - """Get list of packages in organization""" - route = self.get_route("get_packages") - return self.get(route) - - def package(self, package_id=None): - """Get package with given package_id""" - route = self.get_route("get_package", package_id=package_id) - return self.get(route) - - def create_package(self, name, description): - """Create package with given name and description""" - route = self.get_route("create_package") - return self.post( - route, - data=json.dumps( - { - "name": "%s%s" % ("com.%s." % self.organization_id, name), - "description": description, - } - ), - ) - - def delete_package(self, package_id=None): - """Delete package with given package_id""" - route = self.get_route("delete_package", package_id=package_id) - return self.delete(route, status_response={"200": lambda resp: True}) - - def post_release(self, data, package_id=None): - """Create release with given data and package_id""" - route = self.get_route("post_release", package_id=package_id) - return self.post(route, data=data) - - def get_release_status(self, package_id=None, release_id=None, timestamp=None): - """Get status of current release upload""" - route = self.get_route( - "get_release_status", - package_id=package_id, - release_id=release_id, - timestamp=timestamp, - ) - return self.get(route) - - def get_quick_launch(self, project_id=None, quick_launch_id=None): - """Get quick launch object""" - route = self.get_route( - "get_quick_launch", project_id=project_id, quick_launch_id=quick_launch_id - ) - return self.get(route) - - def create_quick_launch(self, data, project_id=None): - """Create quick launch object""" - route = self.get_route("create_quick_launch", project_id=project_id) - return self.post(route, data=data) - - def launch_protocol(self, params, protocol_id=None): - """Launch protocol-id with params""" - route = self.get_route("launch_protocol", protocol_id=protocol_id) - return self.post(route, data=params) - - def get_launch_request(self, protocol_id=None, launch_request_id=None): - """Get launch request id""" - route = self.get_route( - "get_launch_request", - protocol_id=protocol_id, - launch_request_id=launch_request_id, - ) - return self.get(route) - - def resolve_quick_launch_inputs( - self, raw_inputs, project_id=None, quick_launch_id=None - ): - """Resolves `raw_inputs` to `inputs` for quick_launch""" - route = self.get_route( - "resolve_quick_launch_inputs", - project_id=project_id, - quick_launch_id=quick_launch_id, - ) - return self.post(route, json=raw_inputs) - - def get_protocols(self): - """Get list of available protocols""" - route = self.get_route("get_protocols") - return self.get(route) - - def resources(self, query): - """Get resources""" - route = self.get_route("query_resources", query=query) - return self.get(route) - - def inventory(self, query, timeout=30.0, page=0): - """Get inventory""" - route = self.get_route("query_inventory", query=query, page=page) - return self.get(route, timeout=timeout) - - def kits(self, query): - """Get kits""" - route = self.get_route("query_kits", query=query) - return self.get(route) - - def payment_methods(self): - route = self.get_route("get_payment_methods") - return self.get(route) - - def monitoring_data( - self, data_type, instruction_id, grouping=None, start_time=None, end_time=None - ): - """Get monitoring_data""" - route = self.get_route( - "monitoring_data", - data_type=data_type, - instruction_id=instruction_id, - grouping=grouping, - start_time=start_time, - end_time=end_time, - ) - return self.get(route) - - def raw_image_data(self, data_id=None): - """Get raw image data""" - route = self.get_route("view_raw_image", data_id=data_id) - return self.get(route, status_response={"200": lambda resp: resp}, stream=True) - - def _get_object(self, obj_id, obj_type=None): - """Helper function for loading objects""" - # TODO: Migrate away from deref routes for other object types - if obj_type == "dataset": - route = self.get_route("dataset_short", data_id=obj_id) - else: - route = self.get_route("deref_route", obj_id=obj_id) - err_404 = f"[404] No object found for ID {obj_id}" - return self.get(route, status_response={"404": lambda resp: Exception(err_404)}) - - def analyze_run(self, protocol, test_mode=False): - """Analyze given protocol""" - protocol = _parse_protocol(protocol) - if "errors" in protocol: - raise AnalysisException( - ( - "Error%s in protocol:\n%s" - % ( - ("s" if len(protocol["errors"]) > 1 else ""), - "".join( - ["- " + e["message"] + "\n" for e in protocol["errors"]] - ), - ) - ) - ) - - def error_string(r): - return AnalysisException( - "Error%s in protocol:\n%s" - % ( - ("s" if len(r.json()["protocol"]) > 1 else ""), - "".join(["- " + e["message"] + "\n" for e in r.json()["protocol"]]), - ) - ) - - return self.post( - self.get_route("analyze_run"), - data=json.dumps({"protocol": protocol, "test_mode": test_mode}), - status_response={"422": lambda resp: error_string(resp)}, - ) - - def submit_run( - self, - protocol, - project_id=None, - title=None, - test_mode=False, - payment_method_id=None, - ): - """Submit given protocol""" - protocol = _parse_protocol(protocol) - payload = { - "title": title, - "protocol": protocol, - "test_mode": test_mode, - "payment_method_id": payment_method_id, - } - data = {k: v for k, v in payload.items() if v is not None} - route = self.get_route("submit_run", project_id=project_id) - err_404 = ( - f"Error: Couldn't create run (404).\n Are you sure the " - f"project {self.url(project_id)} exists, and that you have " - f"access to it?" - ) - - def err_422(resp): - f"Error creating run: {resp.text}" - - return self.post( - route, - data=json.dumps(data), - status_response={ - "404": lambda resp: AnalysisException(err_404), - "422": lambda resp: AnalysisException(err_422), - }, - ) - - def analyze_launch_request(self, launch_request_id, test_mode=False): - return self.post( - self.get_route("analyze_launch_request"), - data=json.dumps( - {"launch_request_id": launch_request_id, "test_mode": test_mode} - ), - ) - - def submit_launch_request( - self, - launch_request_id, - project_id=None, - protocol_id=None, - title=None, - test_mode=False, - payment_method_id=None, - predecessor_id=None, - ): - """Submit specified launch request""" - payload = { - "title": title, - "launch_request_id": launch_request_id, - "protocol_id": protocol_id, - "test_mode": test_mode, - "payment_method_id": payment_method_id, - "predecessor_id": predecessor_id, - } - data = {k: v for k, v in payload.items() if v is not None} - return self.post( - self.get_route("submit_launch_request", project_id=project_id), - data=json.dumps(data), - status_response={ - "404": lambda resp: AnalysisException( - "Error: Couldn't create run (404). \n" - "Are you sure the project %s " - "exists, and that you have access to it?" % self.url(project_id) - ), - "422": lambda resp: AnalysisException( - f"Error creating run: {resp.text}" - ), - }, - ) - - def dataset(self, data_id, key="*"): - """Get dataset with given data_id""" - route = self.get_route("dataset", data_id=data_id, key=key) - return self.get(route) - - def _get_uploads_from_key(self, key): - """Fetches uploads for a data upload key - - Parameters - ---------- - key : str - data upload key - - Returns - ------- - requests.Response - """ - return self.get( - route=self.get_route(method="get_uploads", key=key), - status_response={"200": lambda resp: resp}, - stream=True, - ) - - def attachments(self, data_id): - """Fetches all attachments for a given dataset id - - Parameters - ---------- - data_id : str - dataset id - - Returns - ------- - dict(str, bytes) - a dict of attachment names and contents - - """ - dataset_route = self.get_route("dataset_short", data_id=data_id) - dataset_attachments = self.get(dataset_route).get("attachments") - attachment_names = [ - os.path.basename(_.get("name", "")) for _ in dataset_attachments - ] - attachment_contents = [ - self._get_uploads_from_key(_.get("key")).content - for _ in dataset_attachments - ] - return dict(zip(attachment_names, attachment_contents)) - - def datasets(self, project_id=None, run_id=None, timeout=30.0): - """Get datasets belonging to run""" - route = self.get_route("datasets", project_id=project_id, run_id=run_id) - err_404 = ( - f"[404] No run found for ID {run_id}. Please ensure you " - f"have the right permissions." - ) - return self.get( - route, - status_response={"404": lambda resp: Exception(err_404)}, - timeout=timeout, - ) - - def data_object(self, id): - """Fetches a data object by id - - Parameters - ---------- - id: str - data_object id - - Returns - ------- - dict - attributes dict - """ - route = self.get_route("data_object", id=id) - attributes = self.get(route).get("data").get("attributes") - attributes["id"] = id - - return attributes - - def data_objects(self, dataset_id): - """Fetches all data objects given a dataset id - - Parameters - ---------- - dataset_id: str - dataset id - - Returns - ------- - arr[dict] - array of attributes dict - """ - route_base = self.get_route("data_objects", dataset_id=dataset_id) - page = 0 - limit = 50 - has_more = True - - results = [] - - while has_more: - route = f"{route_base}&page[limit]={limit}&page[offset]=" f"{page * limit}" - response = self.get(route) - entities = response.get("data") - - for entity in entities: - attributes = entity["attributes"] - attributes["id"] = entity["id"] - results.append(attributes) - - page += 1 - - if len(entities) != limit: - has_more = False - - return results - - def upload_dataset_from_filepath( - self, file_path, title, run_id, analysis_tool, analysis_tool_version - ): - """ - Helper for uploading a file as a dataset to the specified run. - - Uses `upload_dataset`. - - .. code-block:: python - - api.upload_dataset_from_filepath( - "my_file.txt", - title="my cool dataset", - run_id="r123", - analysis_tool="cool script", - analysis_tool_version="v1.0.0" - ) - - Parameters - ---------- - file: str - Path to file to be uploaded - title: str - Name of dataset - run_id: str - Run-id - analysis_tool: str, optional - Name of tool used for analysis - analysis_tool_version: str, optional - Version of tool used - - Returns - ------- - response: dict - JSON-formatted response - """ - try: - file_path = os.path.expanduser(file_path) - file_handle = open(file_path, "rb") - name = os.path.basename(file_handle.name) - except (AttributeError, FileNotFoundError): - raise ValueError("'file' has to be a valid filepath") - - try: - content_type = magic.from_file(file_path, mime=True) - except NameError: - # Handle issues with magic import by not decoding content_type - content_type = None - return self.upload_dataset( - file_handle, - name, - title, - run_id, - analysis_tool, - analysis_tool_version, - content_type, - ) - - def upload_dataset( - self, - file_handle, - name, - title, - run_id, - analysis_tool, - analysis_tool_version, - content_type=None, - ): - """ - Uploads a file_handle as a dataset to the specified run. - - .. code-block:: python - - # Uploading a data_frame via file_handle, using Py3 - import io - - temp_buffer = io.StringIO() - my_df.to_csv(temp_buffer) - - api.upload_dataset( - temp_buffer, - name="my_df", - title="my cool dataset", - run_id="r123", - analysis_tool="cool script", - analysis_tool_version="v1.0.0" - ) - - Parameters - ---------- - file_handle: file_handle - File handle to be uploaded - name: str - Dataset filename - title: str - Name of dataset - run_id: str - Run-id - analysis_tool: str, optional - Name of tool used for analysis - analysis_tool_version: str, optional - Version of tool used - content_type: str - Type of content uploaded - - Returns - ------- - response: dict - JSON-formatted response - """ - upload_id = self.upload_to_uri(file_handle, content_type, title, name) - upload_datasets_route = self.get_route("upload_datasets") - upload_resp = self.post( - upload_datasets_route, - json={ - "upload_id": upload_id, - "file_name": name, - "title": title, - "run_id": run_id, - "analysis_tool": analysis_tool, - "analysis_tool_version": analysis_tool_version, - }, - status_response={ - "404": lambda resp: "[404] Please double-check your parameters" - " and ensure they are valid." - }, - ) - - return upload_resp - - def upload_to_uri(self, file_handle, content_type, title, name): - """ - Helper for uploading files via the `upload` route - - Parameters - ---------- - file_handle: file_handle - File handle to be uploaded - content_type: str - Type of content uploaded - title: str - Title of content to be uploaded - name: str - Name of file to be uploaded - - Returns - ------- - key: str - s3 key - """ - # NOTE(meawoppl) title argument is unused? - - # TODO: - # Currently, we are passing `0` for file_size as it doesn't really - # matter for non multipart uploads, though it would be better to - # supply the correct value. - data = { - "attributes": { - "file_name": name, - "file_size": 0, - "last_modified": int(time.time()), - "is_multipart": False, - } - } - - uri_route = self.get_route("upload") - uri_resp = self.post(uri_route, data=json.dumps({"data": data})) - - try: - upload_id = uri_resp["data"]["id"] - upload_uri = uri_resp["data"]["attributes"]["upload_url"] - except KeyError: - raise RuntimeError("Unexpected payload returned for upload_dataset") - - if isinstance(file_handle, io.StringIO): - try: - # io.StringIO instances must be converted to bytes - file_handle = io.BytesIO(bytes(file_handle.getvalue(), "utf-8")) - except AttributeError: - raise ValueError("Unable to convert read buffer to bytes") - - headers = { - "Content-Disposition": f"attachment; filename={name}", - "Content-Type": content_type, - } - headers = {k: v for k, v in headers.items() if v} - self.put( - upload_uri, - data=file_handle, - headers=headers, - status_response={"200": lambda resp: resp}, - ) - return upload_id - - def get_zip(self, data_id, file_path=None): - """ - Get zip file with given data_id. Downloads to memory and returns a - Python ZipFile by default. - When dealing with larger files where it may not be desired to load the - entire file into memory, specifying `file_path` will enable the file to - be downloaded locally. - - Example Usage: - - .. code-block:: python - - small_zip_id = 'd12345' - small_zip = api.get_zip(small_zip_id) - - my_big_zip_id = 'd99999' - api.get_zip(my_big_zip_id, file_path='big_file.zip') - - Parameters - ---------- - data_id: data_id - Data id of file to download - file_path: Optional[str] - Path to file which to save the response to. If specified, will not - return ZipFile explicitly. - - Returns - ---------- - zip: zipfile.ZipFile - A Python ZipFile is returned unless `file_path` is specified - - """ - route = self.get_route("get_data_zip", data_id=data_id) - req = self.get(route, status_response={"200": lambda resp: resp}, stream=True) - - if file_path: - with open(file_path, "wb") as f: - # Buffer download of data into memory with smaller chunk sizes - chunk_sz = 1024 # 1kb chunks - for chunk in req.iter_content(chunk_sz): - if chunk: - f.write(chunk) - print(f"Zip file downloaded locally to {file_path}.") - else: - return zipfile.ZipFile(io.BytesIO(req.content)) - - def get_route(self, method, **kwargs): - """ - Helper function to automatically match and supply required arguments - """ - route_method = getattr(routes, method) - route_method_args, _, _, route_defaults = inspect.getargspec(route_method) - if route_defaults: - route_method_args = route_method_args[: -len(route_defaults)] - # Update loaded argument dict with new arguments which are not None - new_args = {k: v for k, v in list(kwargs.items()) if v is not None} - arg_dict = dict(self.env_args, **new_args) - input_args = [] - for arg in route_method_args: - if arg_dict[arg]: - input_args.append(arg_dict[arg]) - else: - raise Exception( - f"For route: {method}, argument {arg} needs to be provided." - ) - return route_method( # pylint: disable=no-value-for-parameter - *tuple(input_args) - ) - - def get(self, route, **kwargs): - return self._call("get", route, **kwargs) - - def put(self, route, **kwargs): - return self._call("put", route, **kwargs) - - def post(self, route, **kwargs): - return self._call("post", route, **kwargs) - - def delete(self, route, **kwargs): - return self._call("delete", route, **kwargs) - - def _req_call(self, method, route, **kwargs): - return getattr(self.session, method)(route, **kwargs) - - def _call(self, method, route, status_response={}, **kwargs): - """Base function for handling all requests""" - if self.verbose: - print(f"{method.upper()}: {route}") - - return self._handle_response( - self._req_call(method, route, **kwargs), **status_response - ) - - def _handle_response(self, response, **kwargs): - unauthorized_resp = ( - "You are not authorized to execute this command. " - "For more information on access " - "permissions see the package documentation." - ) - internal_error_resp = ( - "An internal server error has occurred. " - "Please contact support for assistance." - ) - default_status_response = { - "200": lambda resp: resp.json(), - "201": lambda resp: resp.json(), - "401": lambda resp: PermissionError( - "[%d] %s" % (resp.status_code, unauthorized_resp) - ), - "403": lambda resp: PermissionError( - "[%d] %s" % (resp.status_code, unauthorized_resp) - ), - "500": lambda resp: Exception( - "[%d] %s" % (resp.status_code, internal_error_resp) - ), - "default": lambda resp: Exception( - "[%d] %s" % (resp.status_code, resp.text) - ), - } - status_response = dict(default_status_response, **kwargs) - - return_val = status_response.get( - str(response.status_code), status_response["default"] - ) - - if isinstance(return_val(response), Exception): - raise return_val(response) - else: - return return_val(response) - - # NOTE(meawoppl) This is only called externally - def _post_analytics(self, client_id=None, event_action=None, event_category="cli"): - route = "https://www.google-analytics.com/collect" - if not client_id: - client_id = self.user_id - packet = ( - f"v=1&tid=UA-28937242-7&cid={client_id}&t=event&" - f"ea={event_action}&ec={event_category}" - ) - requests.post(route, packet) - - -class AnalysisException(Exception): - def __init__(self, message): - self.message = message - - def __str__(self): - return self.message - - -def _parse_protocol(protocol): - if isinstance(protocol, dict): - return protocol - try: - from autoprotocol import Protocol - except ImportError: - raise RuntimeError( - "Please install `autoprotocol-python` in order " - "to work with Protocol objects" - ) - if isinstance(protocol, Protocol): - return protocol.as_dict() diff --git a/transcriptic/english.py b/transcriptic/english.py deleted file mode 100644 index 0c3f0a13..00000000 --- a/transcriptic/english.py +++ /dev/null @@ -1,883 +0,0 @@ -import ast -import json -import re - -from collections import OrderedDict - - -PLURAL_UNITS = [ - "microliter", - "nanoliter", - "milliliter", - "second", - "minute", - "hour", - "g", - "nanometer", -] - -TEMP_DICT = { - "cold_20": "-20 degrees celsius", - "cold_80": "-80 degrees celsius", - "warm_37": "37 degrees celsius", - "cold_4": "4 degrees celsius", - "warm_30": "30 degrees celsius", - "ambient": "room temperature", -} - - -class AutoprotocolParser(object): - def __init__(self, protocol_obj, api=None, parsed_output=None): - self.api = api - self.resource = dict() - self.parse(protocol_obj) - - def parse(self, obj): - self.object_list = [] - self.instructions = obj["instructions"] - - parsed_output = [] - for i in self.instructions: - try: - output = getattr(self, i["op"])(i) - parsed_output.extend(output) if isinstance( - output, list - ) else parsed_output.append(output) - except AttributeError: - parsed_output.append("[Unknown instruction]") - - self.parsed_output = parsed_output - for i, p in enumerate(parsed_output): - print("%d. %s" % (i + 1, p)) - - def job_tree(self): - """ - A Job Tree visualizes the instructions of a protocol in a hierarchical - structure based on container dependency to help human readers with manual - execution. Its construction utilizes the algorithm below, as well as the - Node object class (to store relational information) at the bottom of this - script. - - Example Usage: - .. code-block:: python - - p = Protocol() - - bacterial_sample = p.ref("bacteria", None, "micro-1.5", discard=True) - test_plate = p.ref("test_plate", None, "96-flat", storage="cold_4") - - p.dispense_full_plate(test_plate, "lb-broth-noAB", "50:microliter") - w = 0 - amt = 1 - while amt < 20: - p.transfer(bacterial_sample.well( - 0), test_plate.well(w), "%d:microliter" % amt) - amt += 2 - w += 1 - - pjsonString = json.dumps(p.as_dict(), indent=2) - pjson = json.loads(pjsonString) - parser_instance = english.AutoprotocolParser(pjson) - parser_instance.job_tree() - - Output: - 1 - +---2 - 3 - +---4 - 5 - +---6 - 7 - +---8 - 9 - +---10 - 11 - +---12 - - - Variables - --------- - steps: list - deep list of objects per instruction/step; - is primary information job tree is built from - nodes: list - list of node objects - proto_forest: list - list of lists grouped by connected nodes - forest: list - list of nested dictionaries, depicting parent-children relations - forest_list: list - list of nested lists, depticting parent-children relations - """ - - # 1. Enforce depth of 1 for steps - def depth_one(steps): - depth_one = [] - for step in steps: - if isinstance(step, list): - if isinstance(step[0], list): - depth_one.append(step[0]) - else: - depth_one.append(step) - else: - depth_one.append([step]) - return depth_one - - # 2. Convert steps to list of node objects (0,1,2,3...) - def assign_nodes(steps): - nodes = [i for i in range(len(steps))] - objects = list(set([elem for sublist in steps for elem in sublist])) - - # checks for multiple src and dst objects -- added when looking for - # mutiples - split_objects = [] - for obj in objects: - if len(obj) > 1: - new_objs = obj.split(", ") - split_objects.extend(new_objs) - else: - split_objects.append(obj) - objects = split_objects - del split_objects - - # populate with leafless trees (Node objects, no edges) - for node in nodes: - nodes[node] = Node(str(node)) - - # search for leafy trees - for obj in objects: - - # accounts for multiple drc/dst objects - leaves = [] - for i, sublist in enumerate(steps): - for string in sublist: - if string.count(",") > 0: - if obj in string: - leaves.append(i) - else: - if obj in sublist: - leaves.append(i) - leaves = sorted(list(set(leaves))) - - if len(leaves) > 1: - viable_edges = [] - - # compute cross-product - for leaf1 in leaves: - for leaf2 in leaves: - if ( - str(leaf1) != str(leaf2) - and sorted((leaf1, leaf2)) not in viable_edges - ): - viable_edges.append(sorted((leaf1, leaf2))) - - # form edge networks - for edge in viable_edges: - n1, n2 = nodes[edge[0]], nodes[edge[1]] - n1.add_edge(n2) - n2.add_edge(n1) - nodes[int(n1.name)], nodes[int(n2.name)] = n1, n2 - return nodes - - # 3. Determine number of trees and regroup by connected nodes - def connected_nodes(nodes): - proto_trees = [] - nodes = set(nodes) - - while nodes: - n = nodes.pop() - group = {n} - queue = [n] - while queue: - n = queue.pop(0) - neighbors = n.edges - neighbors.difference_update(group) - nodes.difference_update(neighbors) - group.update(neighbors) - queue.extend(neighbors) - proto_trees.append(group) - return proto_trees - - # 4. Convert nodes to nested dictionary of parent-children relations - # i.e. adding depth -- also deals with tree-node sorting and path - # optimization - def build_tree_dict(trees, steps): - # node sorting in trees - sorted_trees = [] - for tree in trees: - sorted_trees.append(sorted(tree, key=lambda x: int(x.name))) - - # retrieve values of the nodes (the protocol's containers) - # for each tree ... may want to use dictionary eventually - all_values = [] - for tree in sorted_trees: - values = [steps[int(node.name)] for node in tree] - all_values.append(values) - - # create relational tuples: - all_digs = [] - singles = [] - dst_potentials = [] - for tree_idx in range(len(sorted_trees)): - edge_flag = False - tree_digs = [] - for node_idx in range(len(sorted_trees[tree_idx])): - - # digs: directed graph vectors - digs = [] - dst_nodes = [] - node_values = all_values[tree_idx][node_idx] - src_node = str(sorted_trees[tree_idx][node_idx].name) - - # ACTION ON MULTIPLE OBJECTS (E.G. TRANSFER FROM SRC -> DST - # WELLS) - # Outcome space: {1-1, 1-many, many-1, many-many} - if len(node_values) == 2: - # single destination (x-1) - if node_values[1].count(",") == 0: - dst_nodes = [ - i - for i, sublist in enumerate(steps) - if node_values[1] == sublist[0] - ] - # multiple destinations (x-many) - elif node_values[1].count(",") > 0: - dst_nodes = [] - for dst in node_values[1].replace(", ", ""): - for i, sublist in enumerate(steps): - if i not in dst_nodes and dst == sublist[0]: - dst_nodes.append(i) - - # ACTION ON A SINGLE OBJECT - elif len(node_values) == 1: - dst_nodes = [ - i - for i, sublist in enumerate(steps) - if node_values[0] == sublist[0] - ] - - # Constructing tuples in (child, parent) format - for dst_node in dst_nodes: - dig = (int(dst_node), int(src_node)) - digs.append(dig) - - # else: an edge-case for dictionaries constructed with no edges - # initiates tree separation via flag - if digs != []: - edge_flag = False - tree_digs.append(digs) - else: - edge_flag = True - digs = [(int(src_node), int(src_node))] - tree_digs.append(digs) - - # digraph cycle detection: avoids cycles by overlooking set - # repeats - true_tree_digs = [] - for digs in tree_digs: - for dig in digs: - if tuple(sorted(dig, reverse=True)) not in true_tree_digs: - true_tree_digs.append(tuple(sorted(dig, reverse=True))) - - # edge-case for dictionaries constructed with no edges - if true_tree_digs != [] and edge_flag == False: - all_digs.append(true_tree_digs) - elif edge_flag == True: - all_digs.extend(tree_digs) - - # Enforces forest ordering - all_digs = sorted(all_digs, key=lambda x: x[0]) - - # job tree traversal to find all paths: - forest = [] - for digs_set in all_digs: - - # pass 1: initialize nodes dictionary - nodes = OrderedDict() - for tup in digs_set: - id, parent_id = tup - # ensure all nodes accounted for - nodes[id] = OrderedDict({"id": id}) - nodes[parent_id] = OrderedDict({"id": parent_id}) - - # pass 2: create trees and parent-child relations - for tup in digs_set: - id, parent_id = tup - node = nodes[id] - # links node to its parent - if id != parent_id: - # add new_node as child to parent - parent = nodes[parent_id] - if not "children" in parent: - # ensure parent has a 'children' field - parent["children"] = [] - children = parent["children"] - children.append(node) - - desired_tree_idx = sorted(list(nodes.keys()))[0] - forest.append(nodes[desired_tree_idx]) - return forest - - # 5. Convert dictionary-stored nodes to unflattened, nested list of - # parent-children relations - def dict_to_list(forest): - forest_list = [] - for tree in forest: - tString = str(json.dumps(tree)) - tString = ( - tString.replace('"id": ', "") - .replace('"children": ', "") - .replace("[{", "[") - .replace("}]", "]") - .replace("{", "[") - .replace("}", "]") - ) - - # find largest repeated branch (if applicable) - # maybe think about using prefix trees or SIMD extensions for better - # efficiency - x, y, length, match = 0, 0, 0, "" - for y in range(len(tString)): - for x in range(len(tString)): - substring = tString[y:x] - if ( - len(list(re.finditer(re.escape(substring), tString))) > 1 - and len(substring) > length - ): - match = substring - length = len(substring) - - # checking for legitimate branch repeat - if "[" in match and "]" in match: - hits = [] - index = 0 - if len(tString) > 3: - while index < len(tString): - index = tString.find(str(match), index) - if index == -1: - break - hits.append(index) - index += len(match) - - # find all locations of repeated branch and remove - if len(hits) > 1: - for start_loc in hits[1:]: - tString = tString[:start_loc] + tString[start_loc:].replace( - match, "]", 1 - ) - - # increment all numbers in string to match the protocol - newString = "" - numString = "" - for el in tString: - if el.isdigit(): # build number - numString += el - else: - if ( - numString != "" - ): # convert it to int and reinstantaite numString - numString = str(int(numString) + 1) - newString += numString - newString += el - numString = "" - tString = newString - del newString - - forest_list.append(ast.literal_eval(tString)) - return forest_list - - # 6. Print job tree(s) - def print_tree(lst, level=0): - print(" " * (level - 1) + "+---" * (level > 0) + str(lst[0])) - for l in lst[1:]: - if isinstance(l, list): - print_tree(l, level + 1) - else: - print(" " * level + "+---" + l) - - # 1 - steps = depth_one(self.object_list) - # 2 - nodes = assign_nodes(steps) - # 3 - proto_forest = connected_nodes(nodes) - # 4 - forest = build_tree_dict(proto_forest, steps) - # 5 - self.forest_list = dict_to_list(forest) - # 6 - print("\n" + "A suggested Job Tree based on container dependency: \n") - for tree_list in self.forest_list: - print_tree(tree_list) - - def absorbance(self, opts): - self.object_list.append([opts["object"]]) - return ( - f"Measure absorbance at {self.unit(opts['wavelength'])} for " - f"{self.well_list(opts['wells'])} of plate {opts['object']}" - ) - - def acoustic_transfer(self, opts): - transfers = [] - for t in opts["groups"][0]["transfer"]: - transfers.append( - f"Acoustic transfer {self.unit(t['volume'])} " - f"from {t['from']} to {t['to']}" - ) - self.object_list.append([t["from"], t["to"]]) - return transfers - - def autopick(self, opts): - picks = [] - for i, g in enumerate(opts["groups"]): - picks.extend( - [ - "Pick %s colonies from %s %s: %s to %s, %s" - % ( - len(g["to"]), - len(g["from"]), - ("well" if len(g["from"]) == 1 else "wells"), - self.well_list(g["from"]), - self.well_list(g["to"]), - ( - f"data saved at '{opts['dataref']}'" - if i == 0 - else "analyzed with previous" - ), - ) - ] - ) - self.object_list.append([g["from"], g["to"]]) - return picks - - def cover(self, opts): - self.object_list.append([opts["object"]]) - return f"Cover {opts['object']} with a {opts['lid']} lid" - - def dispense(self, opts): - self.object_list.append([opts["object"]]) - unique_vol = [] - for col in opts["columns"]: - vol = self.unit(col["volume"]) - if vol not in unique_vol: - unique_vol.append(vol) - if "reagent" in opts: - reagent = opts["reagent"] - elif "resource_id" in opts: - resource_id = opts["resource_id"] - if resource_id in self.resource: - reagent = self.resource[resource_id] - elif self.api: - resource = self.api.resources(resource_id) - if resource["results"]: - reagent = resource["results"][0]["name"].lower() - self.resource[resource_id] = reagent - else: - reagent = f"resource with resource ID {resource_id}" - else: - reagent = f"resource with resource ID {resource_id}" - else: - reagent = "unknown" - - if len(opts["columns"]) == 12 and len(unique_vol) == 1: - return ( - f"Dispense {unique_vol[0]} of {reagent} to the full plate " - f"of {opts['object']}" - ) - else: - return "Dispense corresponding amounts of %s to %d column(s) of %s" % ( - reagent, - len(opts["columns"]), - opts["object"], - ) - - def flash_freeze(self, opts): - self.object_list.append([opts["object"]]) - return f"Flash freeze {opts['object']} for {self.unit(opts['duration'])}" - - def fluorescence(self, opts): - self.object_list.append([opts["object"]]) - return ( - f"Read fluorescence of {self.well_list(opts['wells'])} of " - f"plate {opts['object']} at excitation wavelength " - f"{self.unit(opts['excitation'])} and emission wavelength " - f"{self.unit(opts['emission'])}" - ) - - def gel_separate(self, opts): - self.object_list.append([opts["matrix"]]) - return ( - f"Perform gel electrophoresis using a " - f"{opts['matrix'].split(',')[1][:-1]} agarose gel for " - f"{self.unit(opts['duration'])}" - ) - - def gel_purify(self, opts): - self.object_list.append([opts["matrix"]]) - unique_bl = [] - for ext in opts["extract"]: - bl = ext["band_size_range"] - if bl not in unique_bl: - unique_bl.append(bl) - for i in range(len(unique_bl)): - unique_bl[i] = ( - str(unique_bl[i]["min_bp"]) + "-" + str(unique_bl[i]["max_bp"]) - ) - - if len(unique_bl) <= 3: - return ( - f"Perform gel purification on the " - f"{opts['matrix'].split(',')[1][:-1]} agarose gel with " - f"band range(s) {', '.join(unique_bl)}" - ) - else: - return ( - f"Perform gel purification on the " - f"{opts['matrix'].split(',')[1][:-1]} agarose gel with " - f"{len(unique_bl)} band ranges" - ) - - def incubate(self, opts): - self.object_list.append([opts["object"]]) - shaking = " (shaking)" if opts["shaking"] else "" - return ( - f"Incubate {opts['object']} at " - f"{TEMP_DICT[opts['where']]} for " - f"{self.unit(opts['duration'])}{shaking}" - ) - - def image_plate(self, opts): - self.object_list.append([opts["object"]]) - return f"Take an image of {opts['object']}" - - def luminescence(self, opts): - self.object_list.append([opts["object"]]) - return ( - f"Read luminescence of {self.well_list(opts['wells'])} of " - f"plate {opts['object']}" - ) - - def oligosynthesize(self, opts): - self.object_list.append([o["destination"] for o in opts["oligos"]]) - return [ - f"Oligosynthesize sequence '{o['sequence']}' into " f"'{o['destination']}'" - for o in opts["oligos"] - ] - - def provision(self, opts): - self.object_list.append([self.platename(t["well"]) for t in opts["to"]]) - resource_id = opts["resource_id"] - if resource_id in self.resource: - reagent = self.resource[resource_id] - elif self.api: - resource = self.api.resources(resource_id) - if resource["results"]: - reagent = resource["results"][0]["name"].lower() - self.resource[resource_id] = reagent - else: - reagent = f"resource with resource ID {resource_id}" - else: - reagent = f"resource with resource ID {resource_id}" - provisions = [] - for t in opts["to"]: - provisions.append( - f"Provision {self.unit(t['volume'])} of " - f"{reagent} to well {self.well(t['well'])} of " - f"container {self.platename(t['well'])}" - ) - return provisions - - def sanger_sequence(self, opts): - self.object_list.append([opts["object"]]) - seq = ( - f"Sanger sequence {self.well_list(opts['wells'])} of plate " - f"{opts['object']}" - ) - if opts["type"] == "standard": - return seq - elif opts["type"] == "rca": - return seq + " with %s" % self.platename(opts["primer"]) - - def illumina_sequence(self, opts): - unique_wells = self.get_unique_wells(opts["lanes"]) - unique_plates = self.get_unique_plates(unique_wells) - self.object_list.append(unique_plates) - - if len(unique_plates) == 1 and len(unique_wells) <= 3: - seq = "Illumina sequence wells %s" % (", ".join(unique_wells)) - elif len(unique_plates) > 1 and len(unique_plates) <= 3: - seq = "Illumina sequence the corresponding wells of plates %s" % ", ".join( - unique_plates[0] - ) - else: - seq = "Illumina sequence the corresponding wells of %s plates" % len( - unique_wells - ) - - return seq + f" with library size {opts['library_size']}" - - def flow_analyze(self, opts): - wells = [] - for sample in opts["samples"]: - if sample["well"] not in wells: - wells.append(sample["well"]) - self.object_list.append([self.platename(w) for w in wells]) - - return ( - "Perform flow cytometry on %s with the respective FSC and SSC channel parameters" - % ", ".join(wells) - ) - - def seal(self, opts): - self.object_list.append([opts["object"]]) - return f"Seal {opts['object']} ({opts['type']})" - - def spin(self, opts): - self.object_list.append([opts["object"]]) - return ( - f"Spin {opts['object']} for {self.unit(opts['duration'])} at " - f"{self.unit(opts['acceleration'])}" - ) - - def spread(self, opts): - self.object_list.append([self.well(opts["from"]), self.well(opts["to"])]) - return [ - f"Spread {opts['volume']} of bacteria from well " - f"{self.well(opts['from'])} of {self.platename(opts['from'])} " - f"to well {self.well(opts['to'])} of agar plate " - f"{self.platename(opts['to'])}" - ] - - def stamp(self, opts): - stamps = [] - for g in opts["groups"]: - for pip in g: - if pip == "transfer": - stamps.extend( - [ - "Stamp %s from source origin %s " - "to destination origin %s %s (%s)" - % ( - self.unit(p["volume"]), - p["from"], - p["to"], - ( - "with the same set of tips as previous" - if (len(g[pip]) > 1 and i > 0) - else "" - ), - ( - "%s rows x %s columns" - % (g["shape"]["rows"], g["shape"]["columns"]) - ), - ) - for i, p in enumerate(g[pip]) - ] - ) - from_objs = str( - [self.platename(p["from"]) for i, p in enumerate(g[pip])] - ) - to_objs = str( - [self.platename(p["to"]) for i, p in enumerate(g[pip])] - ) - self.object_list.append([from_objs, to_objs]) - return stamps - - def thermocycle(self, opts): - self.object_list.append([opts["object"]]) - return f"Thermocycle {opts['object']}" - - def pipette(self, opts): - pipettes = [] - for g in opts["groups"]: - for pip in g: - if pip == "mix": - for m in g[pip]: - pipettes.append( - "Mix well %s of plate %s %d times " - "with a volume of %s" - % ( - self.well(m["well"]), - self.platename(m["well"]), - m["repetitions"], - self.unit(m["volume"]), - ) - ) - self.object_list.append(self.platename(m["well"])) - elif pip == "transfer": - pipettes.extend( - [ - f"Transfer {self.unit(p['volume'])} from {p['from']} to {p['to']} {'with the same tip as previous' if len(g[pip]) > 1 and i > 0 else ''}" - for i, p in enumerate(g[pip]) - ] - ) - - from_objs = str( - [self.platename(p["from"]) for i, p in enumerate(g[pip])] - ) - to_objs = str( - [self.platename(p["to"]) for i, p in enumerate(g[pip])] - ) - self.object_list.append([from_objs, to_objs]) - elif pip == "distribute": - pipettes.append( - f"Distribute from {g[pip]['from']} into " - f"{self.well_list([d['well'] for d in g[pip]['to']], 20)}" - ) - self.object_list.append([g[pip]["from"], g[pip]["to"][0]["well"]]) - elif pip == "consolidate": - pipettes.append( - f"Consolidate " - f"{self.well_list([c['well'] for c in g[pip]['from']], 20)} " - f"into {g[pip]['to']}" - ) - self.object_list.append([g[pip]["from"][0]["well"], g[pip]["to"]]) - return pipettes - - def magnetic_transfer(self, opts): - specific_op = list(opts["groups"][0][0].keys())[0] - specs_dict = opts["groups"][0][0][specific_op] - self.object_list.append([specs_dict["object"]]) - seq = f"Magnetically {specific_op} {specs_dict['object']}" - - if specific_op == "dry": - return seq + " for %s" % self.unit(specs_dict["duration"]) - elif specific_op == "incubate": - return ( - seq + f" for {self.unit(specs_dict['duration'])} with a " - f"tip position of {specs_dict['tip_position']}" - ) - elif specific_op == "collect": - return ( - seq + f" beads for {specs_dict['cycles']} cycles with a " - f"pause duration of {self.unit(specs_dict['pause_duration'])}" - ) - elif specific_op == "release" or "mix": - return ( - seq + f" beads for {self.unit(specs_dict['duration'])} at " - f"an amplitude of {specs_dict['amplitude']}" - ) - - def measure_volume(self, opts): - unique_plates = self.get_unique_plates(opts["object"]) - self.object_list.append(unique_plates) - - if len(unique_plates) <= 3: - return f"Measure volume of {len(opts['object'])} wells from {', '.join(unique_plates)}" - else: - return f"Measure volume of {len(opts['object'])} wells from the {len(unique_plates)} plates" - - def measure_mass(self, opts): - unique_plates = self.get_unique_plates(opts["object"]) - self.object_list.append(unique_plates) - return "Measure mass of %s" % ", ".join(opts["object"]) - - def measure_concentration(self, opts): - unique_plates = self.get_unique_plates(opts["object"]) - self.object_list.append(unique_plates) - return ( - f"Measure concentration of {self.unit(opts['volume'])} " - f"{opts['measurement']} source aliquots of {self.platename(opts['object'][0])}" - ) - - def uncover(self, opts): - self.object_list.append([opts["object"]]) - return f"Uncover {opts['object']}" - - def unseal(self, opts): - self.object_list.append([opts["object"]]) - return f"Unseal {opts['object']}" - - @staticmethod - def platename(ref): - return ref.split("/")[0] - - @staticmethod - def well(ref): - return ref.split("/")[1] - - @staticmethod - def get_unique_wells(list_of_wells): - unique_wells = [] - for well in list_of_wells: - w = well["object"] - if w not in unique_wells: - unique_wells.append(w) - return unique_wells - - @staticmethod - def get_unique_plates(list_of_wells): - unique_plates = [] - for well in list_of_wells: - loc = well.find("/") - if loc == -1: - plate = well - else: - plate = well[:loc] - - if plate not in unique_plates: - unique_plates.append(plate) - return unique_plates - - @staticmethod - def well_list(wells, max_len=10): - well_list = "wells " + (", ").join(str(x) for x in wells) - if len(wells) > max_len: - well_list = str(len(wells)) + " wells" - return well_list - - @staticmethod - def unit(u): - value = u.split(":")[0] - unit = u.split(":")[1] - return ( - f"{value} " - f"{unit + 's' if float(value) > 1 and unit in PLURAL_UNITS else unit}" - ) - - -class Node(object): - """ - A Node represents a Job Tree element that fulfils a broader child-parent - relational structure. It contains relevant information on its relationships - in the form of edges. The job_tree algorithm above then constructs the - actual hierachy of the aforementioned relational structure. - - Example Usage: - .. code-block:: python - - a = Node("a") - b = Node("b") - c = Node("c") - d = Node("d") - e = Node("e") - f = Node("f") structure: - a.add_edge(b) a - a.add_edge(c) / \ - b.add_edge(d) b c - c.add_edge(e) / / \ - c.add_edge(f) d e f - - Attributes - ---------- - name: str - Name the Node object - edges: set - Set of edges that each node owns - """ - - def __init__(self, name): - self.__name = name - self.__links = set() - - @property - def name(self): - return self.__name - - @property - def edges(self): - return set(self.__links) - - def add_edge(self, other): - self.__links.add(other) - other.__links.add(self) diff --git a/transcriptic/jupyter/__init__.py b/transcriptic/jupyter/__init__.py deleted file mode 100644 index be976224..00000000 --- a/transcriptic/jupyter/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .container import Container -from .dataobject import DataObject -from .dataset import Dataset -from .instruction import Instruction -from .project import Project -from .run import Run diff --git a/transcriptic/jupyter/common.py b/transcriptic/jupyter/common.py deleted file mode 100644 index f255f694..00000000 --- a/transcriptic/jupyter/common.py +++ /dev/null @@ -1,69 +0,0 @@ -try: - import pandas as pd -except ImportError: - raise ImportError( - "Please run `pip install transcriptic[jupyter] if you " - "would like to use Transcriptic objects." - ) - - -def _check_api(obj_type): - from transcriptic import api - - if not api: - raise RuntimeError( - f"You have to be logged in to be able to create {obj_type} objects" - ) - return api - - -class _BaseObject(object): - """Base object which other objects inherit from""" - - # TODO: Inherit more stuff from here. Need to ensure web has unified fields for - # jupyter - def __init__(self, obj_type, obj_id, attributes, connection=None): - # If attributes and connection are explicitly provided, just return and not do - # any smart parsing - if attributes and connection: - self.connection = connection - self.attributes = attributes - else: - if not connection: - self.connection = _check_api(obj_type) - else: - self.connection = connection - (self.id, self.name) = self.load_object(obj_type, obj_id) - if not attributes: - self.attributes = self.connection._get_object(self.id, obj_type) - else: - self.attributes = attributes - - def load_object(self, obj_type, obj_id): - """Find and match object by name""" - # TODO: Remove the try/except statement and properly handle cases where objects - # are not found - # TODO: Fix `datasets` route since that only returns non-analysis objects - try: - objects = getattr(self.connection, obj_type + "s")() - except Exception: - return obj_id, str(obj_id) - matched_objects = [] - for obj in objects: - # Special case here since we use both 'name' and 'title' for object names - if "name" in obj: - if obj_id == obj["name"] or obj_id == obj["id"]: - matched_objects.append((obj["id"], obj["name"])) - if "title" in obj: - if obj_id == obj["title"] or obj_id == obj["id"]: - matched_objects.append((obj["id"], obj["title"])) - if len(matched_objects) == 0: - raise TypeError(f"{obj_id} is not found in your {obj_type}s.") - elif len(matched_objects) == 1: - return matched_objects[0] - else: - print( - f"More than 1 match found. Defaulting to the first match: " - f"{matched_objects[0]}" - ) - return matched_objects[0] diff --git a/transcriptic/jupyter/container.py b/transcriptic/jupyter/container.py deleted file mode 100644 index e05c7dc3..00000000 --- a/transcriptic/jupyter/container.py +++ /dev/null @@ -1,173 +0,0 @@ -import warnings - -from operator import itemgetter - -import pandas as pd - -from .common import _BaseObject - - -class Container(_BaseObject): - """ - A Container object represents a container from the Transcriptic LIMS and - contains relevant information on the container type as well as the - aliquots present in the container. - - Example Usage: - .. code-block:: python - - my_container = container("ct186apgz6a374") - my_container.well_map - my_container.aliquots - - my_container.container_type.col_count - my_container.container_type.robotize("B1") - my_container.container_type.humanize(12) - - Attributes - ---------- - name: str - Name of container - well_map: dict - Well mapping with well indices for keys and well names as values - aliquots: DataFrame - DataFrame of aliquots present in the container. DataFrame index - now corresponds to the Well Index. - container_type: autoprotocol.container_type.ContainerType - Autoprotocol ContainerType object with many useful container type - information and functions. - cover: str - Cover type of container - storage: str - Storage condition of container - - Example Usage: - - .. code-block:: python - - my_container = container("ct186apgz6a374") - - my_container.well_map - - my_container.container_type.col_count - my_container.container_type.robotize("B1") - my_container.container_type.humanize(12) - - - """ - - def __init__(self, container_id, attributes=None, connection=None): - """ - Initialize a Container by providing a container name/id. The attributes and - connection parameters are generally not specified unless one wants to manually - initialize the object. - - Parameters - ---------- - container_id: str - Container name or id in string form - attributes: Optional[dict] - Attributes of the container - connection: Optional[transcriptic.config.Connection] - Connection context. The default context object will be used unless - explicitly provided - """ - super(Container, self).__init__( - "container", container_id, attributes, connection - ) - # TODO: Unify container "label" with name, add Containers route - self.id = container_id - self.cover = self.attributes["cover"] - self.name = self.attributes["label"] - self.storage = self.attributes["storage_condition"] - self.well_map = { - aliquot["well_idx"]: aliquot["name"] - for aliquot in self.attributes["aliquots"] - } - self.container_type = self._parse_container_type() - self._aliquots = pd.DataFrame() - - def _parse_container_type(self): - """Helper function for parsing container string into container object""" - - container_type = self.attributes["container_type"] - - # Return the corresponding AP-Py container object for now. In the future, - # consider merging the current and future dictionary when instantiating - # container_type - try: - from autoprotocol.container_type import _CONTAINER_TYPES - - return _CONTAINER_TYPES[container_type["shortname"]] - except ImportError: - warnings.warn( - "Please install `autoprotocol-python` in order to get container types" - ) - return None - except KeyError: - warnings.warn("ContainerType given is not supported yet in AP-Py") - return None - - @property - def aliquots(self): - """ - Return a DataFrame of aliquots in the container, along with aliquot - name, volume, and properties. Row index for the DataFrame corresponds - to the well index of the aliquot. - - """ - if self._aliquots.empty: - aliquot_list = self.attributes["aliquots"] - try: - from autoprotocol import Unit - - self._aliquots = pd.DataFrame( - sorted( - [ - dict( - { - "Well Index": x["well_idx"], - "Name": x["name"], - "Id": x["id"], - "Volume": Unit(float(x["volume_ul"]), "microliter"), - }, - **x["properties"], - ) - for x in aliquot_list - ], - key=itemgetter("Well Index"), - ) - ) - except ImportError: - warnings.warn( - "Volume is not cast into Unit-type. Please install " - "`autoprotocol-python` in order to have automatic Unit casting" - ) - self._aliquots = pd.DataFrame( - sorted( - [ - dict( - { - "Well Index": x["well_idx"], - "Name": x["name"], - "Id": x["id"], - "Volume": float(x["volume_ul"]), - }, - **x["properties"], - ) - for x in aliquot_list - ], - key=itemgetter("Well Index"), - ) - ) - indices = self._aliquots.pop("Well Index") - self._aliquots.set_index(indices, inplace=True) - return self._aliquots - - def __repr__(self): - """ - Return a string representation of a Container using the specified name. - (ex. Container('my_plate')) - - """ - return "Container(%s)" % (str(self.name)) diff --git a/transcriptic/jupyter/dataobject.py b/transcriptic/jupyter/dataobject.py deleted file mode 100644 index 0460e2f5..00000000 --- a/transcriptic/jupyter/dataobject.py +++ /dev/null @@ -1,154 +0,0 @@ -import json - -from io import StringIO - -import pandas as pd -import requests - -from .common import _check_api -from .container import Container - - -class DataObject(object): - """ - A DataObject holds a reference to the raw data, stored in S3, along with format and - validation information - - Attributes - ---------- - id : str - DataObject id - dataset_id : str - Dataset id - data : bytes - Bytes fetched from the url - name: str - Dataset name - content_type: str - content type - format: str - format - size: int - size in bytes - status: Enum("valid", "invalid", "unverified") - valid vs invalid - url: str - download url which expires every 1hr. Call `refresh` to renew - validation_errors: list(str) - validation errors - container: Container - Container object that was used for this data object - attributes: dict - Master attributes dictionary - """ - - def __init__(self, data_object_id=None): - attributes = {} - - # Fetch dataobject from server if id supplied - if data_object_id is not None: - attributes = DataObject.fetch_attributes(data_object_id) - - self.__init_attrs(attributes) - - # cached values - self._container = None - self._data = None - self._json = None - - def __init_attrs(self, attributes): - self.attributes = attributes - - self.id = attributes.get("id") - self.dataset_id = attributes.get("dataset_id") - self.content_type = attributes.get("content_type") - self.format = attributes.get("format") - self.name = attributes.get("name") - self.size = attributes.get("size") - self.status = attributes.get("status") - self.url = attributes.get("url") - self.validation_errors = attributes.get("validation_errors") - - @staticmethod - def fetch_attributes(data_object_id): - connection = _check_api("data_objects") - return connection.data_object(data_object_id) - - @staticmethod - def init_from_attributes(attributes): - data_object = DataObject() - data_object.__init_attrs(attributes) - - return data_object - - @staticmethod - def init_from_id(data_object_id): - return DataObject(data_object_id) - - @staticmethod - def init_from_dataset_id(data_object_id): - connection = _check_api("data_objects") - - # array of attributes - attributes_arr = connection.data_objects(data_object_id) - - return [DataObject.init_from_attributes(a) for a in attributes_arr] - - @property - def container(self): - container_id = self.attributes["container_id"] - - if container_id is None: - return None - - if not self._container: - self._container = Container(container_id) - - return self._container - - @property - def data(self): - if self._data: - return self._data - - self._data = requests.get(self.url).content - - return self._data - - @property - def data_str(self): - return self.data.decode("utf-8") - - @property - def json(self): - if self._json: - return self._json - - self._json = json.loads(self.data) - - return self._json - - def dataframe(self): - """Creates a simple Pandas Dataframe""" - if self.format == "csv" or self.content_type == "text/csv": - return pd.read_csv(StringIO(self.data_str)) - else: - return pd.DataFrame(self.json) - - def save_data(self, filepath, chunk_size=1024): - """Save DataObject data to a file. Useful for large files""" - with open(filepath, "wb") as f: - if self._data: - f.write(self._data) - return - - r = requests.get(self.url, stream=True) - - for chunk in r.iter_content(chunk_size=chunk_size): - if chunk: - f.write(chunk) - - def refresh(self): - """Refresh DataObject as the url will expire after 1 hour""" - clone = DataObject.init_from_id(self.id) - self.__init_attrs(clone.attributes) diff --git a/transcriptic/jupyter/dataset.py b/transcriptic/jupyter/dataset.py deleted file mode 100644 index e5ee582f..00000000 --- a/transcriptic/jupyter/dataset.py +++ /dev/null @@ -1,161 +0,0 @@ -import warnings - -from copy import deepcopy - -import pandas as pd - -from .common import _BaseObject -from .container import Container -from .dataobject import DataObject - - -class Dataset(_BaseObject): - """ - A Dataset object contains helper methods for accessing data related information - - Attributes - ---------- - id : str - Dataset id - name: str - Dataset name - data : DataFrame - DataFrame of well-indexed data values. Note that associated metadata is found in - attributes dictionary - data_objects : list(DataObject) - List of DataObject type - attachments : dict(str, bytes) - names and data of all attachments for the dataset - container: Container - Container object that was used for this dataset - operation: str - Operation used for generating the dataset - data_type: str - Data type of this dataset - attributes: dict - Master attributes dictionary - connection: transcriptic.config.Connection - Transcriptic Connection object associated with this specific object - - """ - - def __init__(self, data_id, attributes=None, connection=None): - """ - Initialize a Dataset by providing a data name/id. The attributes and connection - parameters are generally not specified unless one wants to manually initialize - the object. - - Parameters - ---------- - data_id: str - Dataset name or id in string form - attributes: Optional[dict] - Attributes of the dataset - connection: Optional[transcriptic.config.Connection] - Connection context. The default context object will be used unless - explicitly provided - """ - super(Dataset, self).__init__("dataset", data_id, attributes, connection) - # TODO: Get BaseObject to handle dataset name - self.name = self.attributes["title"] - self.id = data_id - - # TODO: Consider more formally distinguishing between dataset types - try: - self.operation = self.attributes["instruction"]["operation"]["op"] - except KeyError: - self.operation = None - try: - self.container = Container( - self.attributes["container"]["id"], - attributes=self.attributes["container"], - connection=connection, - ) - except KeyError as e: - if "instruction" in self.attributes: - warnings.warn(f"Missing key {e} when initializing dataset") - self.container = None - - self.analysis_tool = self.attributes["analysis_tool"] - self.analysis_tool_version = self.attributes["analysis_tool_version"] - self.data_type = self.attributes["data_type"] - self._raw_data = None - self._data = pd.DataFrame() - self._attachments = None - self._data_objects = None - - @property - def attachments(self): - if not self._attachments: - self._attachments = self.connection.attachments(data_id=self.id) - return self._attachments - - @property - def raw_data(self): - if not self._raw_data: - # Get all raw data - self._raw_data = self.connection.dataset(data_id=self.id, key="*") - return self._raw_data - - @property - def data(self, key="*"): - if self._data.empty: - # Get all data initially (think about lazy loading in the future) - try: - self._data = pd.DataFrame(self.raw_data) - except: - raise RuntimeError( - "Failed to cast data as DataFrame. Try using raw_data property " - "instead." - ) - self._data.columns = [x.upper() for x in self._data.columns] - if key == "*": - return self._data - else: - return self._data[key] - - def data_objects(self): - if not self._data_objects: - self._data_objects = DataObject.init_from_dataset_id(self.id) - return self._data_objects - - def cross_ref_aliquots(self): - # Use the container.aliquots DataFrame as the base - aliquot_data = deepcopy(self.container.aliquots) - data_column = [] - indices_without_data = [] - # Print a warning if new column will overwrite existing column - if "Aliquot Data" in aliquot_data.columns.values.tolist(): - warnings.warn( - "Column 'Aliquot Data' will be overwritten with data pulled from " - "Dataset." - ) - # Look up data for every well index - for index in aliquot_data.index: - # Get humanized index - humanized_index = self.container.container_type.humanize(int(index)) - if humanized_index in self.data: - # Use humanized index to get data for that well - data_point = self.data.loc[0, humanized_index] - else: - # If no data for that well, use None instead - data_point = None - indices_without_data.append(humanized_index) - # Append data point to list - data_column.append(data_point) - # Print a list of well indices that do not have corresponding data keys - if len(indices_without_data) > 0: - warnings.warn( - "The following indices were not found as data keys: %s" - % ", ".join(indices_without_data) - ) - # Add these data as a column to the DataFrame - aliquot_data["Aliquot Data"] = data_column - - return aliquot_data - - def _repr_html_(self): - return """""" % self.connection.get_route( - "view_data", data_id=self.id - ) diff --git a/transcriptic/jupyter/instruction.py b/transcriptic/jupyter/instruction.py deleted file mode 100644 index 8c0bace0..00000000 --- a/transcriptic/jupyter/instruction.py +++ /dev/null @@ -1,159 +0,0 @@ -import warnings - -import pandas as pd - - -class Instruction(object): - """ - An Instruction object contains information related to the current instruction such - as the start, completed time as well as warps associated with the instruction. - - Note that Instruction objects are usually created as part of a run and not created - explicitly. - - Additionally, if diagnostic information is available, one can click on the - `Show Diagnostics Data` button to view relevant diagnostic information. - - Example Usage: - - .. code-block:: python - - myRun = Run('r12345') - myRun.instructions - - # Access instruction object - myRun.Instructions[1] - myRun.Instructions[1].warps - - - Attributes - ---------- - id : str - Instruction id - name: str - Instruction name - warps : DataFrame - DataFrame of warps in the instruction - started_at : str - Time where instruction begun - completed_at : str - Time where instruction ended - device_id: str - Id of device which instruction was executed on - attributes: dict - Master attributes dictionary - connection: transcriptic.config.Connection - Transcriptic Connection object associated with this specific object - """ - - def __init__(self, attributes, connection=None): - """ - Parameters - ---------- - attributes : dict - Instruction attributes - connection: Optional[transcriptic.config.Connection] - Connection context. The default context object will be used unless - explicitly provided - """ - self.connection = connection - self.attributes = attributes - self.id = attributes["id"] - self.name = attributes["operation"]["op"] - self.started_at = attributes["started_at"] - self.completed_at = attributes["completed_at"] - self.generated_containers = attributes["generated_containers"] - if len(attributes["warps"]) > 0: - device_id_set = set( - [warp["device_id"] for warp in self.attributes["warps"]] - ) - self.device_id = device_id_set.pop() - if len(device_id_set) > 1: - warnings.warn( - "There is more than one device involved in this instruction. Please" - " contact Transcriptic for assistance." - ) - - else: - self.device_id = None - self._warps = pd.DataFrame() - self._warp_events = pd.DataFrame() - - @property - def warps(self): - if self._warps.empty: - warp_list = self.attributes["warps"] - if len(warp_list) != 0: - self._warps = pd.DataFrame(x["command"] for x in warp_list) - self._warps.columns = [x.title() for x in self._warps.columns.tolist()] - # Rearrange columns to start with `Name` - if "Name" in self._warps.columns: - col_names = ["Name"] + [ - col for col in self._warps.columns if col != "Name" - ] - self._warps = self._warps[col_names] - self._warps.insert(1, "WarpId", [x["id"] for x in warp_list]) - self._warps.insert( - 2, "Completed", [x["reported_completed_at"] for x in warp_list] - ) - self._warps.insert( - 3, "Started", [x["reported_started_at"] for x in warp_list] - ) - else: - warnings.warn( - "There are no warps associated with this instruction. Please " - "contact Transcriptic for assistance." - ) - return self._warps - - @property - def warp_events(self): - """ - Warp events include discrete monitoring events such as liquid sensing - events for a particular instruction. - """ - # Note: We may consider adding special classes for specific warp - # events, with more specific annotations/fields. - if self._warp_events.empty: - self._warp_events = self.monitoring(data_type="events") - return self._warp_events - - def monitoring(self, data_type="pressure", grouping=None): - """ - View monitoring data of a given instruction - - Parameters - ---------- - data_type: Optional[str] - Monitoring data type, defaults to 'pressure' - grouping: Optional[str] - Determines whether the values will be grouped, defaults to None. E.g. "5:ms" - - Returns - ------- - DataFrame - Returns a pandas dataframe of the monitoring data if present. - Returns an empty dataframe if no data can be found due to errors. - """ - response = self.connection.monitoring_data( - instruction_id=self.id, data_type=data_type, grouping=grouping - ) - # Handle errors by returning empty dataframe - if "error" in response: - warnings.warn(response["error"]) - return pd.DataFrame() - res = pd.DataFrame(response["results"]) - # re-order so that "name" column is always leading - if "name" in res.columns: - rearr_cols = ["name"] + res.columns[res.columns != "name"].tolist() - return res[rearr_cols] - return res - - def _repr_html_(self): - return """""" % self.connection.get_route( - "view_instruction", - run_id=self.attributes["run_id"], - project_id=self.attributes["project_id"], - instruction_id=self.id, - ) diff --git a/transcriptic/jupyter/project.py b/transcriptic/jupyter/project.py deleted file mode 100644 index 6137c2ad..00000000 --- a/transcriptic/jupyter/project.py +++ /dev/null @@ -1,100 +0,0 @@ -import pandas as pd - -from .common import _BaseObject -from .run import Run - - -class Project(_BaseObject): - """ - A Project object contains helper methods for managing your runs. You can view the - runs associated with this project as well as submit runs to the project. - - Example Usage: - - .. code-block:: python - - myProject = Project("My Project") - projectRuns = myProject.runs() - myRunId = projectRuns.query("title == 'myRun'").id.item() - myRun = Run(myRunId) - - Attributes - ---------- - id : str - Project id - name: str - Project name - attributes: dict - Master attributes dictionary - connection: transcriptic.config.Connection - Transcriptic Connection object associated with this specific object - - """ - - def __init__(self, project_id, attributes=None, connection=None): - """ - Initialize a Project by providing a project name/id. The attributes and - connection parameters are generally not specified unless one wants to manually - initialize the object. - - Parameters - ---------- - project_id: str - Project name or id in string form - attributes: Optional[dict] - Attributes of the project - connection: Optional[transcriptic.config.Connection] - Connection context. The default context object will be used unless - explicitly provided - """ - super(Project, self).__init__("project", project_id, attributes, connection) - self._runs = pd.DataFrame() - - def runs(self, use_cache=True): - """ - Get the list of runs belonging to the project - - Parameters - ---------- - use_cache: Boolean - Determines whether the cached list of runs is returned - - Returns - ------- - DataFrame - Returns a DataFrame of runs, with the id and title as columns - """ - if self._runs.empty and use_cache: - temp = self.connection.env_args - self.connection.update_environment(project_id=self.id) - project_runs = self.connection.runs() - self._runs = pd.DataFrame([[pr["id"], pr["title"]] for pr in project_runs]) - self._runs.columns = ["id", "Name"] - self.connection.env_args = temp - return self._runs - - def submit(self, protocol, title, test_mode=False): - """ - Submit a run to this project - - Parameters - ---------- - protocol: dict - Autoprotocol Protocol in dictionary form, can be generated using - Protocol.as_dict() - title: Optional[str] - Title of run. Run-id will automatically be used as name if field is not - provided - test_mode: Optional[boolean] - Determines if run will be submitted will be treated as a test run or a run - that is meant for execution - - Returns - ------- - Run - Returns a run object if run is successfully submitted - """ - response = self.connection.submit_run( - protocol, project_id=self.id, title=title, test_mode=test_mode - ) - return Run(response["id"], response) diff --git a/transcriptic/jupyter/run.py b/transcriptic/jupyter/run.py deleted file mode 100644 index 9dd68684..00000000 --- a/transcriptic/jupyter/run.py +++ /dev/null @@ -1,275 +0,0 @@ -import pandas as pd - -from requests.exceptions import ReadTimeout - -from .common import _BaseObject -from .container import Container -from .dataset import Dataset -from .instruction import Instruction - - -class Run(_BaseObject): - """ - A Run object contains helper methods for accessing Run-related information such as - Instructions, Datasets and monitoring data. - - Example Usage: - - .. code-block:: python - - myRun = Run('r12345') - myRun.data - myRun.instructions - myRun.containers - myRun.Instructions[0] - - Attributes - ---------- - id : str - Run id - name: str - Run name - data: DataFrame - DataFrame summary of all datasets which belong to this run - instructions: DataFrame - DataFrame summary of all Instruction objects which belong to this run - containers: DataFrame - DataFrame summary of all Container objects which belong to this run - project_id : str - Project id which run belongs to - attributes: dict - Master attributes dictionary - connection: transcriptic.config.Connection - Transcriptic Connection object associated with this specific object - - """ - - def __init__(self, run_id, attributes=None, connection=None, timeout=30.0): - """ - Initialize a Run by providing a run name/id. The attributes and connection - parameters are generally not specified unless one wants to manually initialize - the object. - - Parameters - ---------- - run_id: str - Run name or id in string form - attributes: Optional[dict] - Attributes of the run - connection: Optional[transcriptic.config.Connection] - Connection context. The default context object will be used unless - explicitly provided - timeout: Optional[float] - Timeout in seconds (defaults to 30.0). This will be used when making API - calls to fetch data associated with the run. - """ - super(Run, self).__init__("run", run_id, attributes, connection) - self.project_id = self.attributes["project"]["id"] - self.timeout = timeout - self._data_ids = pd.DataFrame() - self._instructions = pd.DataFrame() - self._containers = pd.DataFrame() - self._data = pd.DataFrame() - - @property - def data_ids(self): - """ - Find and generate a list of datarefs and data_ids associated with this run. - - Returns - ------- - DataFrame - Returns a DataFrame of data ids, with datarefs and data_ids as columns - - """ - if self._data_ids.empty: - datasets = [] - for dataset in self.attributes["datasets"]: - inst_id = dataset["instruction_id"] - if inst_id: - titles = [ - inst.attributes["operation"]["dataref"] - for inst in self.instructions["Instructions"] - if inst.attributes["id"] == inst_id - ] - if len(titles) == 0: - title = "unknown" - elif len(titles) == 1: - title = titles[0] - else: - # This should never happen since instruction_ids are unique - raise ValueError("No unique instruction id found") - else: - title = dataset["title"] - datasets.append( - { - "Name": title, - "DataType": dataset["data_type"], - "Id": dataset["id"], - } - ) - if len(datasets) > 0: - data_ids = pd.DataFrame(datasets) - self._data_ids = data_ids[["Name", "DataType", "Id"]] - return self._data_ids - - @property - def instructions(self): - if self._instructions.empty: - instruction_list = [ - Instruction( - dict(x, **{"project_id": self.project_id, "run_id": self.id}), - connection=self.connection, - ) - for x in self.attributes["instructions"] - ] - self._instructions = pd.DataFrame(instruction_list) - self._instructions.columns = ["Instructions"] - self._instructions.insert( - 0, "Name", [inst.name for inst in self._instructions.Instructions] - ) - self._instructions.insert( - 1, "Id", [inst.id for inst in self._instructions.Instructions] - ) - self._instructions.insert( - 2, - "Started", - [inst.started_at for inst in self._instructions.Instructions], - ) - self._instructions.insert( - 3, - "Completed", - [inst.completed_at for inst in self._instructions.Instructions], - ) - return self._instructions - - @property - def Instructions(self): - """ - Helper for allowing direct access of `Instruction` objects - - Returns - ------- - Series - Returns a Series of `Instruction` objects - - """ - return self.instructions.Instructions - - @property - def containers(self): - if self._containers.empty: - container_list = [] - for ref in Run(self.id).attributes["refs"]: - container_list.append(Container(ref["container"]["id"])) - self._containers = pd.DataFrame(container_list) - self._containers.columns = ["Containers"] - self._containers.insert( - 0, "Name", [container.name for container in self._containers.Containers] - ) - self._containers.insert( - 1, - "ContainerId", - [container.id for container in self._containers.Containers], - ) - self._containers.insert( - 2, - "Type", - [ - container.container_type.shortname - for container in self._containers.Containers - ], - ) - self._containers.insert( - 3, - "Status", - [ - container.attributes["status"] - for container in self._containers.Containers - ], - ) - self._containers.insert( - 4, - "Storage Condition", - [container.storage for container in self._containers.Containers], - ) - return self._containers - - @property - def Containers(self): - """ - Helper for allowing direct access of `Container` objects - - Returns - ------- - Series - Returns a Series of `Container` objects - """ - return self.containers.Containers - - @property - def data(self): - """ - Find and generate a list of Dataset objects which are associated with this run - - Returns - ------- - DataFrame - Returns a DataFrame of datasets, with Name, Dataset and DataType as columns - - """ - if self._data.empty: - num_datasets = len(self.data_ids) - if num_datasets == 0: - print("No datasets were found.") - else: - print(f"Attempting to fetch ${num_datasets} datasets...") - try: - data_list = [] - for name, data_type, data_id in self.data_ids.values: - dataset = Dataset(data_id) - data_list.append( - { - "Name": name, - "DataType": data_type, - "Operation": dataset.operation, - "AnalysisTool": dataset.analysis_tool, - "Datasets": dataset, - } - ) - data_frame = pd.DataFrame(data_list) - - # Rearrange columns - self._data = data_frame[ - ["Name", "DataType", "Operation", "AnalysisTool", "Datasets"] - ] - except ReadTimeout: - print( - f"Operation timed out after {self.timeout} seconds. Returning " - "data_ids instead of Datasets.\nTo try again, increase value " - "of self.timeout and resubmit request." - ) - return self.data_ids - return self._data - - @property - def Datasets(self): - """ - Helper for allowing direct access of `Dataset` objects - - Returns - ------- - Series - Returns a Series of `Dataset` objects - """ - try: - return self.data.Datasets - except Exception: - print("Unable to load Datasets successfully. Returning empty series.") - return pd.Series() - - def _repr_html_(self): - return """""" % self.connection.get_route( - "view_run", project_id=self.project_id, run_id=self.id - ) diff --git a/transcriptic/routes.py b/transcriptic/routes.py deleted file mode 100644 index a4d20c44..00000000 --- a/transcriptic/routes.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -Big dumb file of routes, please do not add any logic into this file -Note: {api_root} and {org_id} are automatically supplied in the Connection.get_route and do not need to be specified -""" - - -def get_container(api_root, org_id, container_id): - return "{api_root}/{org_id}/samples/{container_id}".format(**locals()) - - -def create_project(api_root, org_id): - return "{api_root}/{org_id}".format(**locals()) - - -def delete_project(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}".format(**locals()) - - -def archive_project(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}".format(**locals()) - - -def get_project(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}".format(**locals()) - - -def get_projects(api_root, org_id): - return "{api_root}/{org_id}/?q=&per_page=500".format(**locals()) - - -def get_project_runs(api_root, org_id, project_id): - return "{api_root}/api/runs?filter[project_id]={project_id}&fields[runs]=id,title,status,created_at,completed_at&page[limit]=100".format( - **locals() - ) - - -def create_package(api_root, org_id): - return "{api_root}/{org_id}/packages".format(**locals()) - - -def delete_package(api_root, org_id, package_id): - return "{api_root}/{org_id}/packages/{package_id}".format(**locals()) - - -def get_package(api_root, org_id, package_id): - return "{api_root}/{org_id}/packages/{package_id}".format(**locals()) - - -def get_packages(api_root, org_id): - return "{api_root}/{org_id}/packages/".format(**locals()) - - -def get_protocols(api_root, org_id): - return "{api_root}/{org_id}/protocols".format(**locals()) - - -def launch_protocol(api_root, org_id, protocol_id): - return "{api_root}/{org_id}/protocols/{protocol_id}/launch".format(**locals()) - - -def get_launch_request(api_root, org_id, protocol_id, launch_request_id): - return ( - "{api_root}/{org_id}/protocols/{protocol_id}/launch/{launch_request_id}".format( - **locals() - ) - ) - - -def post_release(api_root, org_id, package_id): - return "{api_root}/{org_id}/packages/{package_id}/releases/".format(**locals()) - - -def get_release_status(api_root, org_id, package_id, release_id, timestamp): - return "{api_root}/{org_id}/packages/{package_id}/releases/{release_id}?_={timestamp}".format( - **locals() - ) - - -def query_kits(api_root, query): - return "{api_root}/_commercial/kits?q={query}&per_page=1000&full_json=true".format( - **locals() - ) - - -def query_resources(api_root, query): - return "{api_root}/_commercial/resources?q={query}&per_page=1000".format(**locals()) - - -def query_inventory(api_root, org_id, query, page=0): - return "{api_root}/{org_id}/inventory/samples?q={query}&per_page=75&page={page}".format( - **locals() - ) - - -def get_quick_launch(api_root, org_id, project_id, quick_launch_id): - return ( - "{api_root}/{org_id}/{project_id}/runs/quick_launch/{quick_launch_id}".format( - **locals() - ) - ) - - -def create_quick_launch(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}/runs/quick_launch".format(**locals()) - - -def resolve_quick_launch_inputs(api_root, org_id, project_id, quick_launch_id): - return ( - "{api_root}/{org_id}/{project_id}/runs/quick_launch/" - "{quick_launch_id}/resolve_inputs".format(**locals()) - ) - - -def login(api_root): - return "{api_root}/users/sign_in".format(**locals()) - - -def get_organizations(api_root): - return "{api_root}/organizations".format(**locals()) - - -def get_organization(api_root, org_id): - return "{api_root}/{org_id}".format(**locals()) - - -def deref_route(api_root, obj_id): - return "{api_root}/-/{obj_id}".format(**locals()) - - -def analyze_run(api_root, org_id): - return "{api_root}/{org_id}/analyze_run".format(**locals()) - - -def analyze_launch_request(api_root, org_id): - return "{api_root}/{org_id}/analyze_run".format(**locals()) - - -def submit_run(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}/runs".format(**locals()) - - -def submit_launch_request(api_root, org_id, project_id): - return "{api_root}/{org_id}/{project_id}/runs".format(**locals()) - - -def dataset_short(api_root, data_id): - return "{api_root}/datasets/{data_id}.json".format(**locals()) - - -def dataset(api_root, data_id, key): - return "{api_root}/datasets/{data_id}.json?key={key}".format(**locals()) - - -def datasets(api_root, org_id, project_id, run_id): - return "{api_root}/{org_id}/{project_id}/runs/{run_id}/data".format(**locals()) - - -def data_object(api_root, id): - return "{api_root}/api/data_objects/{id}".format(**locals()) - - -def data_objects(api_root, dataset_id): - return "{api_root}/api/data_objects?filter[dataset_id]={dataset_id}".format( - **locals() - ) - - -def get_uploads(api_root, key): - return "{api_root}/upload/url_for?key={key}".format(**locals()) - - -def upload(api_root): - return "{api_root}/api/uploads".format(**locals()) - - -def upload_datasets(api_root): - return "{api_root}/api/datasets".format(**locals()) - - -def modify_aliquot_properties(api_root, aliquot_id): - return "{api_root}/api/aliquots/{aliquot_id}/modify_properties".format(**locals()) - - -def preview_protocol(api_root): - return "{api_root}/runs/preview".format(**locals()) - - -def preview_protocol_embed(api_root, preview_id): - return "{api_root}/runs/preview/{preview_id}.embed".format(**locals()) - - -def view_data(api_root, data_id): - return "{api_root}/datasets/{data_id}.embed".format(**locals()) - - -def view_run(api_root, org_id, project_id, run_id): - return "{api_root}/{org_id}/{project_id}/runs/{run_id}.embed".format(**locals()) - - -def view_instruction(api_root, org_id, project_id, run_id, instruction_id): - return "{api_root}/{org_id}/{project_id}/runs/{run_id}/instructions/{instruction_id}.embed".format( - **locals() - ) - - -def view_raw_image(api_root, data_id): - return "{api_root}/-/{data_id}.raw".format(**locals()) - - -def get_data_zip(api_root, data_id): - return "{api_root}/-/{data_id}.zip".format(**locals()) - - -def monitoring_data( - api_root, data_type, instruction_id, grouping=None, start_time=None, end_time=None -): - base_route = ( - "{api_root}/sensor_data/{data_type}?instruction_id={instruction_id}".format( - **locals() - ) - ) - if grouping: - base_route += "&grouping={grouping}".format(**locals()) - if start_time: - base_route += "&start_time={start_time}".format(**locals()) - if end_time: - base_route += "&end_time={end_time}".format(**locals()) - return base_route - - -def get_payment_methods(api_root, org_id): - return "{api_root}/{org_id}/payment_methods".format(**locals()) diff --git a/transcriptic/sampledata/__init__.py b/transcriptic/sampledata/__init__.py deleted file mode 100644 index eea71446..00000000 --- a/transcriptic/sampledata/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .container import load_sample_container -from .project import load_sample_project -from .run import load_sample_run diff --git a/transcriptic/sampledata/_data/ct123.json b/transcriptic/sampledata/_data/ct123.json deleted file mode 100644 index 0ee8a750..00000000 --- a/transcriptic/sampledata/_data/ct123.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"ct123","container_type_id":"384-echo","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.261-07:00","accessions":0,"slot":null,"cover":null,"test_mode":false,"label":"Echo Source Plate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"properties":{},"will_be_destroyed_at":null,"generated_by_run":{"id":"r123","project":{"id":"p123"}},"aliquots":[{"id":"aq1egnpw5q5ythw","container_id":"ct123","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"99.0","name":null,"properties":{},"lot_no":null}],"container_type":{"id":"384-echo","name":"384-Well Echo Qualified Polypropylene Microplate 2.0","well_count":384,"well_depth_mm":"11.5","well_volume_ul":"135.0","capabilities":["spin","incubate","seal","image_plate","stamp","echo_dest","echo_source","dispense-destination","envision"],"shortname":"384-echo","col_count":24,"is_tube":false,"acceptable_lids":["universal","foil","ultra-clear"],"height_mm":"14.4","vendor":"Labcyte","catalog_number":"PP-0200","retired_at":null,"sale_price":"0.0"},"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null,"device_events":[]},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org","test_account":true}} diff --git a/transcriptic/sampledata/_data/ct124.json b/transcriptic/sampledata/_data/ct124.json deleted file mode 100644 index d22efe0a..00000000 --- a/transcriptic/sampledata/_data/ct124.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","accessions":0,"slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"properties":{},"will_be_destroyed_at":null,"generated_by_run":{"id":"r123","project":{"id":"p123"}},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null,"device_events":[]},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org","test_account":true}} diff --git a/transcriptic/sampledata/_data/d123-raw.json b/transcriptic/sampledata/_data/d123-raw.json deleted file mode 100644 index e94e063f..00000000 --- a/transcriptic/sampledata/_data/d123-raw.json +++ /dev/null @@ -1 +0,0 @@ -{"a1":[0.05],"a2":[0.04],"a3":[0.06],"b1":[1.21],"b2":[1.13],"b3":[1.32],"c1":[2.22],"c2":[2.15],"c3":[2.37]} diff --git a/transcriptic/sampledata/_data/d123.json b/transcriptic/sampledata/_data/d123.json deleted file mode 100644 index bb894456..00000000 --- a/transcriptic/sampledata/_data/d123.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"d123","warp_id":"w124","created_at":"2020-06-01T15:40:50.824-07:00","data_type":"platereader","instruction_id":"i124","parameters":{},"attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"supported_formats":["csv","json"],"metadata":{},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"warp":{"id":"w124","device_id":"wc1-infinite1","command":{"wavelength":"600.0:nanometer","numFlashes":25,"name":"PlateReader.ReadAbsorbance","cType":"96-well-v-bottom","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T15:40:50.824-07:00","completed_at":"2020-06-01T15:40:55.044-07:00"},"instruction":{"id":"i124","operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600"},"completed_at":"2020-06-01T15:40:55.049-07:00","executed_at":"2020-06-01T15:40:54.989-07:00","run":{"id":"r123","status":"complete","title":"Sample Run"}}} diff --git a/transcriptic/sampledata/_data/d124-raw.json b/transcriptic/sampledata/_data/d124-raw.json deleted file mode 100644 index 73d9783d..00000000 --- a/transcriptic/sampledata/_data/d124-raw.json +++ /dev/null @@ -1 +0,0 @@ -{"a1":[0.045],"a2":[0.034],"a3":[0.058],"b1":[1.397],"b2":[1.481],"b3":[1.342],"c1":[2.41],"c2":[2.23],"c3":[2.381]} diff --git a/transcriptic/sampledata/_data/d124.json b/transcriptic/sampledata/_data/d124.json deleted file mode 100644 index d05a2dd2..00000000 --- a/transcriptic/sampledata/_data/d124.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"d124","warp_id":"w124","created_at":"2020-06-01T16:40:50.824-07:00","data_type":"platereader","instruction_id":"i124","parameters":{},"attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"supported_formats":["csv","json"],"metadata":{},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"warp":{"id":"w124","device_id":"wc1-infinite1","command":{"wavelength":"600.0:nanometer","numFlashes":25,"name":"PlateReader.ReadAbsorbance","cType":"96-well-v-bottom","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T16:40:50.824-07:00","completed_at":"2020-06-01T16:40:55.044-07:00"},"instruction":{"id":"i124","operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600_1"},"completed_at":"2020-06-01T16:40:55.049-07:00","executed_at":"2020-06-01T16:40:54.989-07:00","run":{"id":"r123","status":"complete","title":"Sample Run"}}} diff --git a/transcriptic/sampledata/_data/d125-raw.json b/transcriptic/sampledata/_data/d125-raw.json deleted file mode 100644 index c3667d60..00000000 --- a/transcriptic/sampledata/_data/d125-raw.json +++ /dev/null @@ -1 +0,0 @@ -{"a1":[0.044],"a2":[0.049],"a3":[0.068],"b1":[1.563],"b2":[1.872],"b3":[1.77],"c1":[2.574],"c2":[2.434],"c3":[2.574]} diff --git a/transcriptic/sampledata/_data/d125.json b/transcriptic/sampledata/_data/d125.json deleted file mode 100644 index 7a69bc58..00000000 --- a/transcriptic/sampledata/_data/d125.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"d125","warp_id":"w124","created_at":"2020-06-01T17:40:50.824-07:00","data_type":"platereader","instruction_id":"i124","parameters":{},"attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"supported_formats":["csv","json"],"metadata":{},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"warp":{"id":"w124","device_id":"wc1-infinite1","command":{"wavelength":"600.0:nanometer","numFlashes":25,"name":"PlateReader.ReadAbsorbance","cType":"96-well-v-bottom","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T17:40:50.824-07:00","completed_at":"2020-06-01T17:40:55.044-07:00"},"instruction":{"id":"i124","operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600_2"},"completed_at":"2020-06-01T17:40:55.049-07:00","executed_at":"2020-06-01T17:40:54.989-07:00","run":{"id":"r123","status":"complete","title":"Sample Run"}}} diff --git a/transcriptic/sampledata/_data/d126-raw.json b/transcriptic/sampledata/_data/d126-raw.json deleted file mode 100644 index 5105f33c..00000000 --- a/transcriptic/sampledata/_data/d126-raw.json +++ /dev/null @@ -1 +0,0 @@ -{"a1":[0.052],"a2":[0.054],"a3":[0.051],"b1":[2.304],"b2":[2.405],"b3":[2.287],"c1":[2.679],"c2":[2.861],"c3":[2.746]} diff --git a/transcriptic/sampledata/_data/d126.json b/transcriptic/sampledata/_data/d126.json deleted file mode 100644 index fe37cbeb..00000000 --- a/transcriptic/sampledata/_data/d126.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"d126","warp_id":"w124","created_at":"2020-06-01T18:40:50.824-07:00","data_type":"platereader","instruction_id":"i124","parameters":{},"attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"supported_formats":["csv","json"],"metadata":{},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"warp":{"id":"w124","device_id":"wc1-infinite1","command":{"wavelength":"600.0:nanometer","numFlashes":25,"name":"PlateReader.ReadAbsorbance","cType":"96-well-v-bottom","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T18:40:50.824-07:00","completed_at":"2020-06-01T18:40:55.044-07:00"},"instruction":{"id":"i124","operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600_3"},"completed_at":"2020-06-01T18:40:55.049-07:00","executed_at":"2020-06-01T18:40:54.989-07:00","run":{"id":"r123","status":"complete","title":"Sample Run"}}} diff --git a/transcriptic/sampledata/_data/d127-raw.json b/transcriptic/sampledata/_data/d127-raw.json deleted file mode 100644 index 0a5de03e..00000000 --- a/transcriptic/sampledata/_data/d127-raw.json +++ /dev/null @@ -1 +0,0 @@ -{"a1":[0.038],"a2":[0.034],"a3":[0.059],"b1":[2.775],"b2":[3.041],"b3":[3.111],"c1":[2.893],"c2":[3.052],"c3":[2.986]} diff --git a/transcriptic/sampledata/_data/d127.json b/transcriptic/sampledata/_data/d127.json deleted file mode 100644 index 79490265..00000000 --- a/transcriptic/sampledata/_data/d127.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"d127","warp_id":"w124","created_at":"2020-06-01T19:40:50.824-07:00","data_type":"platereader","instruction_id":"i124","parameters":{},"attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"supported_formats":["csv","json"],"metadata":{},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"aliquots":[{"id":"aq123","container_id":"ct124","well_idx":0,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_0","properties":{},"lot_no":null},{"id":"aq124","container_id":"ct124","well_idx":1,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_1","properties":{},"lot_no":null},{"id":"aq125","container_id":"ct124","well_idx":2,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"control_2","properties":{},"lot_no":null},{"id":"aq126","container_id":"ct124","well_idx":12,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_0","properties":{},"lot_no":null},{"id":"aq127","container_id":"ct124","well_idx":13,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq128","container_id":"ct124","well_idx":14,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample1_1","properties":{},"lot_no":null},{"id":"aq129","container_id":"ct124","well_idx":24,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_0","properties":{},"lot_no":null},{"id":"aq130","container_id":"ct124","well_idx":25,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_1","properties":{},"lot_no":null},{"id":"aq131","container_id":"ct124","well_idx":26,"created_at":"2020-06-01T15:39:55.064-07:00","volume_ul":"1.0","name":"sample2_2","properties":{},"lot_no":null}],"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"warp":{"id":"w124","device_id":"wc1-infinite1","command":{"wavelength":"600.0:nanometer","numFlashes":25,"name":"PlateReader.ReadAbsorbance","cType":"96-well-v-bottom","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T19:40:50.824-07:00","completed_at":"2020-06-01T19:40:55.044-07:00"},"instruction":{"id":"i124","operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600_4"},"completed_at":"2020-06-01T19:40:55.049-07:00","executed_at":"2020-06-01T19:40:54.989-07:00","run":{"id":"r123","status":"complete","title":"Sample Run"}}} diff --git a/transcriptic/sampledata/_data/p123-runs.json b/transcriptic/sampledata/_data/p123-runs.json deleted file mode 100644 index f0522a2b..00000000 --- a/transcriptic/sampledata/_data/p123-runs.json +++ /dev/null @@ -1 +0,0 @@ -{"data":[{"id":"r123","type":"runs","links":{"self":""},"attributes":{"completed_at":null,"created_at":"2014-08-05T20:59:38.435-07:00","status":"canceled","title":"Sample Run"}}],"meta":{"record_count":1},"links":{"first":"","last":""}} diff --git a/transcriptic/sampledata/_data/p123.json b/transcriptic/sampledata/_data/p123.json deleted file mode 100644 index 4cf87a80..00000000 --- a/transcriptic/sampledata/_data/p123.json +++ /dev/null @@ -1 +0,0 @@ -{"id": "p123", "name": "sample project", "created_at": "2020-10-01T00:00:00.000-07:00", "updated_at": "2020-10-01T00:59:59.100-07:00", "archived_at": null, "event_stream_settings": {}, "payment_method_id": "pm123", "webhook_url": null, "bsl": 1, "visibility_in_words": "Organization-wide", "users": [], "payment_method": {"id": "pm123", "organization_id": "sample-org", "created_at": "2020-10-01T00:00:00.000-07:00", "type": "PurchaseOrder", "po_reference_number": "foo", "po_limit": "123123.0", "po_attachment_url": "uploads/", "po_approved_at": "2020-10-01T15:07:12.834-07:00", "po_invoice_address": null, "expiry": "2025-10-01", "is_valid": true, "description": "foo", "is_removable": true, "is_default?": true, "can_make_default": true, "limit": "123123.0", "address": {"id": "addr123", "attention": "Foo", "street": "123 Foo", "street_2": "Suite Foo", "city": "Menlo Park", "state": "CA", "zipcode": "94025", "country": "US"}}} diff --git a/transcriptic/sampledata/_data/r123.json b/transcriptic/sampledata/_data/r123.json deleted file mode 100644 index db9e491a..00000000 --- a/transcriptic/sampledata/_data/r123.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"r123","status":"complete","title":"Sample Run","created_at":"2020-05-20T14:57:29.230-07:00","updated_at":"2020-06-01T15:39:55.132-07:00","completed_at":"2020-06-01T15:39:55.116-07:00","conversation_id":"conv123","quote":{"items":[{"cost":"96.09","quantity":1,"title":"Workcell Time","run_id":"r123","run_credit_applicable":true},{"cost":"7.35","quantity":1,"title":"Reagents & Consumables","run_id":"r123","run_credit_applicable":false}],"ppp":false},"results":{},"test_mode":false,"accepted_at":"2020-05-20T14:57:29.229-07:00","started_at":"2020-06-01T15:39:50.860-07:00","canceled_at":null,"aborted_at":null,"draft_quote":null,"progress":100,"protocol_id":null,"request_type":"protocol","launch_request_id":null,"flagged":false,"scheduled_to_start_at":null,"properties":{},"internal_run":true,"bsl":1,"success":null,"success_notes":null,"total_cost":"103.44","has_quote?":true,"can_cancel?":false,"warnings":[],"pending_shipment_ids":[],"billing_valid?":true,"unrealized_input_containers":[],"dependents":[],"datasets":[{"id":"d123","warp_id":"w124","created_at":"2020-06-01T15:40:55.044-07:00","data_type":"platereader","instruction_id":"i124","attachments":[],"title":null,"deleted_at":null,"deletion_requested":false,"run_id":null,"uploaded_by":null,"analysis_tool":null,"analysis_tool_version":null,"is_analysis":false}],"refs":[{"name":"Echo Source Plate","container_id":"ct123","new_container_type":"384-echo","destiny":{"store":{"where":"cold_4","shaking":false}},"container_type":{"id":"384-echo","name":"384-Well Echo Qualified Polypropylene Microplate 2.0","well_count":384,"well_depth_mm":"11.5","well_volume_ul":"135.0","capabilities":["spin","incubate","seal","image_plate","stamp","echo_dest","echo_source","dispense-destination","envision"],"shortname":"384-echo","col_count":24,"is_tube":false,"acceptable_lids":["universal","foil","ultra-clear"],"height_mm":"14.4","vendor":"Labcyte","catalog_number":"PP-0200","retired_at":null,"sale_price":"0.0"},"container":{"id":"ct123","container_type_id":"384-echo","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.261-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"Echo Source Plate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":1,"container_type":{"id":"384-echo","name":"384-Well Echo Qualified Polypropylene Microplate 2.0","well_count":384,"well_depth_mm":"11.5","well_volume_ul":"135.0","capabilities":["spin","incubate","seal","image_plate","stamp","echo_dest","echo_source","dispense-destination","envision"],"shortname":"384-echo","col_count":24,"is_tube":false,"acceptable_lids":["universal","foil","ultra-clear"],"height_mm":"14.4","vendor":"Labcyte","catalog_number":"PP-0200","retired_at":null,"sale_price":"0.0"},"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}}},{"name":"VbottomPlate","container_id":"ct124","new_container_type":"96-well-v-bottom","destiny":{"store":{"where":"cold_4","shaking":false}},"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"container":{"id":"ct124","container_type_id":"96-well-v-bottom","barcode":null,"deleted_at":null,"created_at":"2020-05-20T14:57:29.241-07:00","organization_id":"org123","slot":null,"cover":null,"test_mode":false,"label":"VbottomPlate","location_id":"loc1bv7bn3sc37v","shipment_id":null,"kit_request_id":null,"storage_condition":"cold_4","shipment_code":null,"status":"available","expires_at":null,"aliquot_count":9,"container_type":{"id":"96-well-v-bottom","name":"96-well cell culture multiple well plate, V bottom","well_count":96,"well_depth_mm":"10.668","well_volume_ul":"200.0","capabilities":["dispense","spin","seal","unseal","liquid_handle","cover","echo_dest","spectrophotometry","image_plate","incubate","uncover","dispense-destination","envision","absorbance","fluorescence"],"shortname":"96-well-v-bottom","col_count":12,"is_tube":false,"acceptable_lids":["standard","universal","low_evaporation","ultra-clear","foil"],"height_mm":"3.25","vendor":"Corning","catalog_number":"3894","retired_at":null,"sale_price":"0.0"},"device":{"id":"wc7-handoff1","model":null,"manufacturer":null,"name":"wc7-handoff1","device_class":null,"configuration":{},"location_id":"loc1bv7bn3sc37v","serial_number":null,"purchased_at":null,"manufactured_at":null},"location":{"id":"loc1bv7bn3sc37v","created_at":"2018-08-23T17:12:04.841-07:00","updated_at":"2018-08-23T17:12:04.841-07:00","parent_id":"loc1bv7b4c5u2rb","name":"wc7-handoff1","position":null,"properties":{},"parent_path":["loc1bv7b4c5u2rb"],"merged_properties":{},"row":null,"col":null,"location_type":{"id":"loctyp1959vuy4482f","name":"Unknown","category":"Unknown","capacity":null,"created_at":"2016-06-21T16:30:16.096-07:00","updated_at":"2016-07-18T19:41:00.006-07:00","location_type_categories":[]},"ancestors":[{"id":"loc1bv7b4c5u2rb","parent_id":null,"name":"wc7-frontend1","position":null,"human_path":"wc7-frontend1"}]},"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}}}],"owner":{"id":"u123","email":"sampler@sample-org.com","name":"Peter Peterson","profile_img_url":""},"project":{"id":"p123","name":"vbottoms","archived_at":null,"bsl":1,"organization":{"id":"org123","name":"Sample Org","subdomain":"sample-org"}},"successors":[],"instructions":[{"id":"i123","sequence_no":0,"operation":{"op":"acoustic_transfer","groups":[{"transfer":[{"from":"Echo Source Plate/0","to":"VbottomPlate/0","volume":"1000:nanoliter"}]}],"droplet_size":"25:nanoliter"},"generated_containers": [{"id": "ct123","label": "container_generated"}],"completed_at":"2020-06-01T15:39:55.049-07:00","data_name":null,"started_at":"2020-06-01T15:39:50.873-07:00","warps":[{"id":"w123","device_id":"wc1-echo1","command":{"name":"AcousticLiquidHandler.Transfer","params":{"sourceContainer":{"id":"ct123","cType":"384-echo"},"destContainer":{"id":"ct124","cType":"96-well-v-bottom"},"transfers":[{"from":"0","to":"0","volume":"1000.0:nanoliter"}],"dataRef":"Echo Source Plate to VbottomPlate","dropletSize":"25.0:nanoliter"}},"state":"Completed","created_at":"2020-06-01T15:39:50.824-07:00","completed_at":"2020-06-01T15:39:55.044-07:00","nominal_duration":"00:00:05.31","min_duration":"00:00:00.81","max_duration":"00:00:11.31","reported_started_at":"2020-06-01T15:39:50.791-07:00","reported_completed_at":"2020-06-01T15:39:54.989-07:00","instruction_id":"i123","run_id":"r123"}]},{"id":"i124","sequence_no":1,"operation":{"op":"absorbance","object":"VbottomPlate","wells":["0","1","2","12","13","14","24","25","26"],"wavelength":"600:nanometer","num_flashes":25,"dataref":"OD600"},"generated_containers": [],"completed_at":"2020-06-01T15:40:55.049-07:00","data_name":null,"started_at":"2020-06-01T15:40:50.873-07:00","warps":[{"id":"w124","device_id":"wc1-infinite1","command":{"name":"PlateReader.ReadAbsorbance","wavelength":"600:nanometer","numFlashes":25,"cType":"96-flat","wells":[0,1,2,12,13,14,24,25,26],"settleTime":"0.0:millisecond"},"state":"Completed","created_at":"2020-06-01T15:40:50.824-07:00","completed_at":"2020-06-01T15:40:55.044-07:00","nominal_duration":"00:00:05.31","min_duration":"00:00:00.81","max_duration":"00:00:11.31","reported_started_at":"2020-06-01T15:40:50.791-07:00","reported_completed_at":"2020-06-01T15:40:54.989-07:00","instruction_id":"i124","run_id":"r123"}]}]} diff --git a/transcriptic/sampledata/_data/sample-org-projects.json b/transcriptic/sampledata/_data/sample-org-projects.json deleted file mode 100644 index b07c45ef..00000000 --- a/transcriptic/sampledata/_data/sample-org-projects.json +++ /dev/null @@ -1 +0,0 @@ -{"projects":[{"id": "p123", "name": "sample project", "created_at": "2020-10-01T00:00:00.000-07:00", "updated_at": "2020-10-01T00:59:59.100-07:00"}]} diff --git a/transcriptic/sampledata/connection.py b/transcriptic/sampledata/connection.py deleted file mode 100644 index dfc5e0f4..00000000 --- a/transcriptic/sampledata/connection.py +++ /dev/null @@ -1,101 +0,0 @@ -import responses - -from requests.exceptions import ConnectionError -from transcriptic.config import Connection -from transcriptic.sampledata.project import sample_project_attr -from transcriptic.sampledata.run import sample_run_attr -from transcriptic.util import load_sampledata_json - - -class MockConnection(Connection): - """ - MockConnection object used for previewing Juypter objects without establishing a - connection. - - Example Usage: - - .. code-block:: python - - mock_connection = MockConnection() - mock_connection.projects() - myRun = Run('r123') - """ - - def __init__(self, *args, organization_id="sample-org", **kwargs): - super().__init__(*args, organization_id=organization_id, **kwargs) - - @responses.activate - def _req_call(self, method, route, **kwargs): - self._register_mocked_responses() - try: - return super()._req_call(method, route, **kwargs) - except ConnectionError: - # Default raised exception lists all routes which is very verbose - if self.verbose: - raise - else: - raise ConnectionError(f"Mocked route not implemented: {route}") - - def _register_mocked_responses(self): - # TODO: Everything is hardcoded right now. Move to Jinja - # Register Project routes - responses.add( - responses.GET, - self.get_route("get_project", project_id="p123"), - json=sample_project_attr, - status=200, - ) - responses.add( - responses.GET, - self.get_route("deref_route", obj_id="p123"), - json=sample_project_attr, - status=200, - ) - responses.add( - responses.GET, - self.get_route("get_projects", org_id="sample-org"), - json=load_sampledata_json("sample-org-projects.json"), - status=200, - ) - responses.add( - responses.GET, - self.get_route("get_project_runs", org_id="sample-org", project_id="p123"), - json=load_sampledata_json("p123-runs.json"), - ) - # Register Run routes - responses.add( - responses.GET, - self.get_route("deref_route", obj_id="r123"), - json=sample_run_attr, - status=200, - ) - # Register Container routes - responses.add( - responses.GET, - self.get_route("deref_route", obj_id="ct123"), - json=load_sampledata_json("ct123.json"), - status=200, - ) - responses.add( - responses.GET, - self.get_route("deref_route", obj_id="ct124"), - json=load_sampledata_json("ct124.json"), - status=200, - ) - # Register Dataset routes - for data_id in ["d123", "d124", "d125", "d126", "d127"]: - # Note: `match_querystring` is important for correct resolution - responses.add( - responses.GET, - self.get_route("dataset_short", data_id=data_id), - json=load_sampledata_json(f"{data_id}.json"), - status=200, - match_querystring=True, - ) - responses.add( - responses.GET, - self.get_route("dataset", data_id=data_id, key="*"), - json=load_sampledata_json(f"{data_id}-raw.json"), - status=200, - match_querystring=True, - ) diff --git a/transcriptic/sampledata/container.py b/transcriptic/sampledata/container.py deleted file mode 100644 index 9a4e44c2..00000000 --- a/transcriptic/sampledata/container.py +++ /dev/null @@ -1,44 +0,0 @@ -from transcriptic.jupyter import Container -from transcriptic.util import load_sampledata_json - - -def load_container_from_attributes( - container_id: str, - attributes: dict, -) -> Container: - """ - Helper function for constructing an object from specified attributes - """ - return Container( - container_id=container_id, - attributes=attributes, - ) - - -sample_container_attr = load_sampledata_json("ct123.json") - - -def load_sample_container(container_id: str = "ct123") -> Container: - """ - Loads sample container from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_container = load_sample_container() - my_container.name - - Parameters - ---------- - container_id: str - ContainerId of registered object mock to load. - - Returns - ------- - Container - Returns a Container object with some mocked data - """ - return load_container_from_attributes( - container_id, load_sampledata_json(f"{container_id}.json") - ) diff --git a/transcriptic/sampledata/dataset.py b/transcriptic/sampledata/dataset.py deleted file mode 100644 index d81cca51..00000000 --- a/transcriptic/sampledata/dataset.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import List - -from transcriptic.jupyter import Dataset -from transcriptic.util import load_sampledata_json - - -def load_dataset_from_attributes( - dataset_id: str, - attributes: dict, -) -> Dataset: - """ - Helper function for constructing an object from specified attributes - """ - return Dataset( - data_id=dataset_id, - attributes=attributes, - ) - - -sample_dataset_attr = load_sampledata_json("d123.json") - - -def load_sample_dataset(dataset_id="d123") -> Dataset: - """ - Loads sample dataset from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_dataset = load_sample_dataset() - my_dataset.data_type - - Parameters - ---------- - dataset_id: str - DatasetId of registered object mock to load. - - Returns - ------- - Dataset - Returns a Dataset object with some mocked data - """ - return load_dataset_from_attributes( - dataset_id, load_sampledata_json(f"{dataset_id}.json") - ) - - -# Manually annotate which datasets correspond to absorbance datasets for now -ABSORBANCE_DATASETS = ["d123", "d124", "d125", "d126", "d127"] - - -def load_sample_absorbance_dataset(dataset_id="d123") -> Dataset: - """ - Loads sample absorbance dataset from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_dataset = load_sample_absorbance_dataset() - my_dataset.data_type - Absorbance(my_dataset, ["some label"]) - - Returns - ------- - Dataset - Returns an Absorbance Dataset object with some mocked data - - """ - assert dataset_id in ABSORBANCE_DATASETS - return load_sample_dataset(dataset_id) - - -def load_sample_kinetics_datasets(dataset_ids=None) -> List[Dataset]: - """ - Loads sample kinetics dataset from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_datasets = load_sample_kinetics_datasets() - print([dataset.data_type for dataset in my_datasets]) - kinetics.Spectrophotometry(my_datasets) - - Returns - ------- - List[Dataset] - Returns a list of Absorbance Dataset objects with some mocked data - - """ - if dataset_ids is None: - dataset_ids = ABSORBANCE_DATASETS - return [load_sample_dataset(dataset_id) for dataset_id in dataset_ids] diff --git a/transcriptic/sampledata/project.py b/transcriptic/sampledata/project.py deleted file mode 100644 index ae64668d..00000000 --- a/transcriptic/sampledata/project.py +++ /dev/null @@ -1,41 +0,0 @@ -from transcriptic.jupyter import Project -from transcriptic.util import load_sampledata_json - - -def load_project_from_attributes(project_id: str, attributes: dict) -> Project: - """ - Helper function for constructing an object from specified attributes - """ - return Project( - project_id, - attributes=attributes, - ) - - -sample_project_attr = load_sampledata_json("p123.json") - - -def load_sample_project(project_id="p123") -> Project: - """ - Loads sample project from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_project = load_sample_project() - my_project.name - - Parameters - ---------- - project_id: str - ProjectId of registered object mock to load. - - Returns - ------- - Project - Returns a Project object with some mocked data - """ - return load_project_from_attributes( - project_id, load_sampledata_json(f"{project_id}.json") - ) diff --git a/transcriptic/sampledata/run.py b/transcriptic/sampledata/run.py deleted file mode 100644 index 87729252..00000000 --- a/transcriptic/sampledata/run.py +++ /dev/null @@ -1,42 +0,0 @@ -from transcriptic.jupyter import Run -from transcriptic.util import load_sampledata_json - - -def load_run_from_attributes( - run_id: str, - attributes: dict, -) -> Run: - """ - Helper function for constructing an object from specified attributes - """ - return Run( - run_id=run_id, - attributes=attributes, - ) - - -sample_run_attr = load_sampledata_json("r123.json") - - -def load_sample_run(run_id="r123") -> Run: - """ - Loads sample run from registered mocked data. - - Example Usage: - - .. code-block:: python - - my_run = load_sample_run() - my_run.name - - Parameters - ---------- - run_id: str - RunId of registered object mock to load. - - Returns - ------- - Run - Returns a Run object with some mocked data - """ - return load_run_from_attributes(run_id, load_sampledata_json(f"{run_id}.json")) diff --git a/transcriptic/templates/manifest.json.jinja b/transcriptic/templates/manifest.json.jinja deleted file mode 100644 index b843738f..00000000 --- a/transcriptic/templates/manifest.json.jinja +++ /dev/null @@ -1,64 +0,0 @@ -{ - "license": "MIT", - "format": "python", - "protocols": [ - { - "name": "{{ name }}", - "display_name": "{{ name }} example protocol", - "categories": [ - "Example Protocol" - ], - "description": "This is an example description for a protocol.", - "version": "0.0.1", - "command_string": "python -m {{ name }}", - "inputs": { - "source_aliquot": { - "type": "aliquot", - "label": "Source aliquot", - "description": "Source Aliquot to transfer volume from" - }, - "volume": { - "type": "volume", - "label": "Transfer volume per well", - "default": "20:microliter", - "description": "Volume to transfer from source well to every well of the destination container." - }, - "dest_ctype": { - "label": "Destination Container type", - "type": "choice", - "default": "96-pcr", - "options": [ - { "value": "96-pcr" }, - { "value": "96-flat" }, - { "value": "96-deep" }, - { "value": "384-pcr" }, - { "value": "384-flat" }, - { "value": "384-echo" } - ] - } - }, - "preview": { - "refs": { - "fake_source_container": { - "label": "Fake Source Container", - "type": "micro-2.0", - "store": "cold_20", - "cover": null, - "aliquots": { - "0": { - "name": "Fake Source Aliquot", - "volume": "1800.0:microliter", - "properties": {} - } - } - } - }, - "parameters": { - "source_aliquot": "fake_source_container/0", - "volume": "20:microliter", - "dest_ctype": "96-pcr" - } - } - } - ] -} diff --git a/transcriptic/templates/protocol.py.jinja b/transcriptic/templates/protocol.py.jinja deleted file mode 100644 index bbbd5f5d..00000000 --- a/transcriptic/templates/protocol.py.jinja +++ /dev/null @@ -1,41 +0,0 @@ -################################################################################################ -# This example protocol will create a new destination container and -# transfer `volume` amount of `source_aliquot` into each well. -# -# Test locally with some like the following, though will need to supply a valid project id. -# You must be in the same directory as the manifest and the protocol for the command to succeed. -# -# transcriptic launch --local {{ name }} -p SOME_PROJECT_ID -# -# For more information about autoprotocol-python check out the documentation. -# -# http://autoprotocol-python.readthedocs.io/en/latest/ -# -# For more information about editing the manifest.json check out the documentation. -# -# https://developers.transcriptic.com/v1.0/docs/input-types -################################################################################################ - - -def {{ name }}(protocol, params): - # These arguments and their types are specified in the manifest.json - # source_aliquot is of type Well - # volume is of type string and will be in the format similar to '100:microliter' - # dest_ctype is of type string and will be one of the options specified in the manifest. - source_well = params["source_aliquot"] - volume = params["volume"] - dest_ctype = params["dest_ctype"] - - # Create a ref for a the destination container - dest_container = protocol.ref("destination_container", - cont_type=dest_ctype, - storage="ambient") - - for dest_well in dest_container.wells_from(0, 12): - # add an instruction to the protocol - protocol.transfer(source_well, dest_well, volume) - - -if __name__ == '__main__': - from autoprotocol.harness import run - run({{ name }}, "{{ name }}") diff --git a/transcriptic/templates/requirements.txt.jinja b/transcriptic/templates/requirements.txt.jinja deleted file mode 100644 index 3524bc30..00000000 --- a/transcriptic/templates/requirements.txt.jinja +++ /dev/null @@ -1,2 +0,0 @@ -autoprotocol>=3.9.0,<4.0 -autoprotocol_utilities>=2.1.7,<3.0 diff --git a/transcriptic/util.py b/transcriptic/util.py deleted file mode 100644 index 915cd60e..00000000 --- a/transcriptic/util.py +++ /dev/null @@ -1,368 +0,0 @@ -import itertools -import json -import re - -from collections import OrderedDict, defaultdict -from os.path import abspath, dirname, join - -import click - - -def natural_sort(l): - convert = lambda text: int(text) if text.isdigit() else text.lower() - alphanum_key = lambda key: [convert(c) for c in re.split("([0-9]+)", key)] - return sorted(l, key=alphanum_key) - - -def flatmap(func, items): - return itertools.chain.from_iterable(map(func, items)) - - -def ascii_encode(non_compatible_string): - """Primarily used for ensuring terminal display compatibility""" - if non_compatible_string: - return non_compatible_string.encode("ascii", errors="ignore").decode("ascii") - else: - return "" - - -def pull(nested_dict): - if "type" in nested_dict and "inputs" not in nested_dict: - return nested_dict - else: - inputs = {} - if "type" in nested_dict and "inputs" in nested_dict: - for param, input in list(nested_dict["inputs"].items()): - inputs[str(param)] = pull(input) - return inputs - else: - return nested_dict - - -def regex_manifest(protocol, input): - """Special input types, gets updated as more input types are added""" - if "type" in input and input["type"] == "choice": - if "options" in input: - pattern = r"\[(.*?)\]" - match = re.search(pattern, str(input["options"])) - if not match: - click.echo( - 'Error in %s: input type "choice" options must ' - 'be in the form of: \n[\n {\n "value": ' - ', \n "label": \n ' - "},\n ...\n]" % protocol["name"] - ) - raise RuntimeError - else: - click.echo( - f"Must have options for 'choice' input type. Error in: {protocol['name']}" - ) - raise RuntimeError - - -def iter_json(manifest): - all_types = {} - try: - protocol = manifest["protocols"] - except TypeError: - raise RuntimeError( - "Error: Your manifest.json file doesn't contain " - "valid JSON and cannot be formatted." - ) - for protocol in manifest["protocols"]: - types = {} - for param, input in list(protocol["inputs"].items()): - types[param] = pull(input) - if isinstance(input, dict): - if input["type"] == "group" or input["type"] == "group+": - for i, j in list(input.items()): - if isinstance(j, dict): - for k, l in list(j.items()): - regex_manifest(protocol, l) - else: - regex_manifest(protocol, input) - all_types[protocol["name"]] = types - return all_types - - -def by_well(datasets, well): - return [ - datasets[reading].props["data"][well][0] for reading in list(datasets.keys()) - ] - - -def makedirs(name, mode=None, exist_ok=False): - """Forward ports `exist_ok` flag for Py2 makedirs. Retains mode defaults""" - from os import makedirs - - mode = mode if mode is not None else 0o777 - makedirs(name, mode, exist_ok) - - -def is_valid_jwt_token(token: str): - regex = r"Bearer ([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)" - return re.fullmatch(regex, token) is not None - - -def load_sampledata_json(filename: str) -> dict: - with open(sampledata_path(filename)) as fh: - return json.load(fh) - - -def sampledata_path(filename: str) -> str: - return join(sampledata_dir(), filename) - - -def sampledata_dir() -> str: - return abspath(join(dirname(__file__), "sampledata", "_data")) - - -class PreviewParameters: - """ - A PreviewParameters object modifies web browser quick launch parameters and - modifies them for application protocol testing and debugging. - - Attributes - ------ - api : object - the Connection object to provide session for using api endpoints - - quick_launch_params: dict - web browser generated inputs for quick launch - - selected_samples: defaultdict - all aliquots selected through the web quick launch manifest - - modified_params: dict - the modified quick launch launch parameters, converts quick launch - aliquot objects into strings for debugging - - refs: dict - all unique refs seen in the quick launch parameters - - preview: dict - the combination of refs and modified_params for scientific - application debugging - - protocol_obj: dict - the protocol object from the manifest - - """ - - def __init__(self, api, quick_launch_params, protocol_obj): - """ - Initialize TestParameter by providing a web generated params dict. - - Parameters - ---------- - quick_launch_params: dict - web browser generated inputs for quick launch - """ - self.api = api - self.protocol_obj = protocol_obj - self.container_cache = {} - self.selected_samples = {} - self.csv_templates = {} - self.quick_launch_params = quick_launch_params - self.preview = self.build_preview() - - def build_preview(self): - """Builds preview parameters""" - self.modify_preview_parameters() - self.refs = self.generate_refs() - preview = defaultdict(lambda: defaultdict(dict)) - preview["preview"]["parameters"].update(self.modified_params) - preview["preview"].update(self.refs) - return preview - - def adjust_csv_table_input_type(self): - """ - Traverses the protocol object from the manifest to find any csv-table - input types. If it finds one it creates the headers and modifies the - modified_params that eventually will be the preview parameters for - autoprotocol testing. - """ - self.traverse_protocol_obj(self.protocol_obj["inputs"]) - - def modify_preview_parameters(self): - """ - This method will traverse the quick launch 'raw_inputs' and modify - container ids and aliquot dicts into a preview parameter container - string for autoprotocol generation debugging. - """ - self.modified_params = self.traverse_quick_launch( - obj=self.quick_launch_params, callback=self.create_preview_string - ) - self.adjust_csv_table_input_type() - - def generate_refs(self): - """ - This method takes the aggregated containers and aliquots to produce - the refs aliquot values - """ - ref_dict = defaultdict(lambda: defaultdict(dict)) - ref_dict["refs"] = {} - for cid, index_arr in self.selected_samples.items(): - container = self.container_cache.get(cid) - cont_name = PreviewParameters.format_container_name(container) - ref_dict["refs"][cont_name] = { - "label": cont_name, - "type": container.get("container_type").get("id"), - "store": container.get("storage_condition"), - "cover": container.get("cover", None), - "properties": container.get("properties"), - "aliquots": {}, - } - - if None not in index_arr: - ref_dict["refs"][cont_name]["aliquots"] = self.get_selected_aliquots( - container, index_arr - ) - elif container.get("aliquots", None): - for ali in container.get("aliquots"): - ref_dict["refs"][cont_name]["aliquots"][ali["well_idx"]] = { - "name": ali["name"], - "volume": ali["volume_ul"] + ":microliter", - "properties": ali["properties"], - } - - return ref_dict - - def traverse_quick_launch(self, obj, callback=None): - """ - Will traverse quick launch object and send value to a callback - action method. - """ - if isinstance(obj, dict): - # If object has 'containerId' and 'wellIndex', then it is an aliquot - if "containerId" and "wellIndex" in obj.keys(): - return self.create_string_from_aliquot(value=obj) - else: - value = { - k: self.traverse_quick_launch(v, callback) for k, v in obj.items() - } - elif isinstance(obj, list): - return [self.traverse_quick_launch(elem, callback) for elem in obj] - else: - value = obj - - if callback is None: - return value - else: - return callback(value) - - def add_to_cache(self, container_id): - """Adds requested container to cache for later use""" - if container_id in self.container_cache: - container = self.container_cache[container_id] - else: - container = self.api.get_container(container_id) - self.container_cache[container_id] = container - return container - - def create_string_from_aliquot(self, value): - """Creates preview aliquot representation""" - well_idx = value.get("wellIndex") - container_id = value.get("containerId") - container = self.add_to_cache(container_id) - cont_name = PreviewParameters.format_container_name(container) - self.add_to_selected(container_id, well_idx) - return "{}/{}".format(cont_name, well_idx) - - def create_preview_string(self, value): - """Creates preview parameters string representation""" - if isinstance(value, str): - if value[:2] == "ct": - container_id = value - container = self.add_to_cache(container_id) - cont_name = PreviewParameters.format_container_name(container) - self.add_to_selected(container_id) - return cont_name - else: - return value - else: - return value - - def add_to_selected(self, container_id, well_idx=None): - """Saves which containers were selected.""" - if container_id in self.selected_samples: - self.selected_samples[container_id].append(well_idx) - else: - self.selected_samples[container_id] = [well_idx] - - def get_selected_aliquots(self, container, index_arr): - """Grabs the properties from the selected aliquots""" - ref_aliquots = dict() - container_aliquots = { - ali.get("well_idx"): ali for ali in container.get("aliquots") - } - for i in index_arr: - ali = container_aliquots.get(i, container) - ref_aliquots[i] = { - "name": ali.get("name"), - "volume": "{}:microliter".format(ali.get("volume_ul", 10)), - "properties": ali.get("properties"), - } - return ref_aliquots - - def update_nested(self, in_dict, key, value): - for k, v in in_dict.items(): - if key == k: - in_dict[k] = [value, v] - elif isinstance(v, dict): - self.update_nested(v, key, value) - elif isinstance(v, list): - for o in v: - if isinstance(o, dict): - self.update_nested(o, key, value) - - def traverse_protocol_obj(self, obj, parentkey=None): - if isinstance(obj, dict): - if obj.get("type") == "csv-table": - t = obj.get("template") - headers = {k: c for k, c in zip(t.get("keys"), t.get("col_type"))} - self.update_nested(self.modified_params, parentkey, headers) - return obj - else: - value = { - pkey: self.traverse_protocol_obj(v, pkey) for pkey, v in obj.items() - } - elif isinstance(obj, list): - return [self.traverse_protocol_obj(elem, parentkey) for elem in obj] - else: - value = obj - return value - - def merge(self, manifest): - # Get selected protocol - selected_protocol = next( - p - for p in manifest["protocols"] - if p["name"] == self.protocol_obj.get("name") - ) - - # Get the index of the protocol in the protocols list - protocol_idx = manifest["protocols"].index(selected_protocol) - updated_protocol = OrderedDict() - # Ensure that the merged protocol object has the same key order - updated_protocol["name"] = self.protocol_obj["name"] - updated_protocol["display_name"] = self.protocol_obj["display_name"] - updated_protocol["categories"] = self.protocol_obj.get("categories", []) - updated_protocol["description"] = self.protocol_obj["description"] - updated_protocol["version"] = self.protocol_obj["version"] - updated_protocol["command_string"] = self.protocol_obj["command_string"] - updated_protocol["inputs"] = self.protocol_obj["inputs"] - updated_protocol["preview"] = self.preview.get("preview") - - # Place modified protocol in the appropriate index - manifest["protocols"][protocol_idx] = updated_protocol - - # Ensure that manifest has correct order - self.merged_manifest = OrderedDict() - self.merged_manifest["format"] = "python" - self.merged_manifest["license"] = "MIT" - self.merged_manifest["protocols"] = manifest["protocols"] - - @classmethod - def format_container_name(cls, container): - return container.get("label").replace(" ", "_") diff --git a/transcriptic/version.py b/transcriptic/version.py deleted file mode 100644 index 6960ea03..00000000 --- a/transcriptic/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "9.6.3"