{ "info": { "author": "ELIXIR Cloud & AAI", "author_email": "alexander.kanitz@alumni.ethz.ch", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Environment :: Console", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Intended Audience :: Healthcare Industry", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Medical Science Apps.", "Topic :: System :: Systems Administration", "Topic :: Utilities", "Typing :: Typed" ], "description": "# TEStribute\n\nTask distribution for GA4GH TES instances.\n\n## Synopsis\n\nProof of concept implementation of a **task distribution** logic for a federated\nnetwork of [GA4GH][1] [Task Execution Service] (TES) instances.\n\n![TEStribute_working]\n\n## Usage\n\nYou can use `TEStribute` in three ways:\n\n- Call the **HTTP API service**, e.g. with `curl`:\n\n```bash\ncurl -X POST SERVICE_URI -H \"Content-Type: application/json\" -d PAYLOAD\n```\n\n- Call the **console script**:\n\n```bash\ntestribute [-h] --tes-uri URI --cpu-cores INT --ram-gb FLOAT --disk-gb FLOAT\n --execution-time-sec INT [--jwt TOKEN] [--object-id ID] [--drs-uri\n URI] [-m MODE] [-v]\n```\n\n- Call the **main function** directly, from within your Python code:\n\n```py\nfrom TEStribute import rank_services\n\nrank_services(...)\n```\n\n## Implementation details\n\nGiven a set of available [GA4GH][1] [Task Execution Service] (TES) instances, a\ntask's compute resource requirements, the [Data Repository Service] (DRS)\nobject identifiers of all task inputs (if any), and a list of DRS instances\nwhere these objects might be obtained from, TEStribute returns a list of\ncombinations of TES instances and input object locations, rank-ordered according\nto either increasing *estimated total costs*, increasing *estimated total\nprocessing times*, or a weighting factor that balances both of these\nproperties.\n\nThe application currently relies on [modifications] to the TES specifications\nand assumes that DRS object identifiers are globally unique (i.e., a given\nidentifier will point to the same exact file on any DRS instance), which is\nnot warranted by current DRS specs. More detailed information on these\nrequirements is available at [mock-TES] and [mock-DRS], mockup services which\nimplement these modifications/assumptions. The corresponding clients [TES-cli]\nand [DRS-cli] are used within `TEStribute` to interact with these services.\n\n## Installation\n\n### Deploying the API service\n\nEnsure you have the following software installed:\n\n- [Docker](https://www.docker.com/) (18.06.1-ce, build e68fc7a)\n- [docker-compose](https://docs.docker.com/compose/) (1.23.1, build b02f1306)\n- [Git](https://git-scm.com/) (tested with version 2.17.1)\n\nClone repository and start Docker service\n\n```bash\ngit clone https://github.com/elixir-europe/TEStribute.git app\ncd app\ndocker-compose up --build --detach\n```\n\nYou can explore the HTTP API via the Swagger UI:\n\n```bash\nfirefox http://localhost:7979/ui/\n```\n\n### CLI usage & import\n\nEnsure you have the following software installed:\n\n- [Python](https://www.python.org) (tested with version 3.6.8)\n- [pip](https://pip.pypa.io/en/stable/) (tested with version 19.2.2)\n\nInstall package and `testribute` console script:\n\n```bash\npip install TEStribute\n```\n\n## Extended usage\n\n### Options\n\nThe following properties/options are available when running TEStribute,\nregardless of whether the software is run as an HTTP API service, as a console\nscript or directly from within your Python code. The CLI option is indicated\nin parentheses in those cases where it differs from API / import usage:\n\n- `object_ids` (`object-id`): DRS IDs of objects required by the task. When\n using the console script, indicate the option multiple times to pass multiple\n arguments.\n- `drs_uris` (`drs-id`): URIs of DRS instances that objects may be read from or\n written to. When using the console script, indicate the option multiple times\n to pass multiple arguments.\n- `mode`: Defines how service combinations are ranked, either by 'cost', 'time'\n or both. For the latter, specify a number between 0 and 1, with the boundaries\n representing weights at which services are ranked entirely by cost and time,\n respectively. It is also possible to randomize rankings (specify 'random' or\n -1).\n- `resource_requirements` (not available as CLI option, use properties\n directly): Map of resources required for the task:\n - `cpu_cores` (`cpu-cores`): Requested number of CPUs.\n - `disk_gb` (`disk-gb`): Requested disk size in gigabytes (GB).\n - `execution_time_sec` (`execution-time-sec`): Requested execution time in\n seconds (s).\n - `preemptible` (not available as CLI option): Is the task allowed to run on\n preemptible compute instances (e.g. AWS Spot)? Currently not used.\n - `ram_gb`: Requested RAM required in gigabytes (GB).\n - `zones` (not available as CLI option): Request that the task be run in\n these compute zones. Currently not used.\n- `tes_uris` (`tes-uri`): URIs of known TES instances that the task may be\n computed on. When using the console script, indicate the option multiple times\n to pass multiple arguments.\n- `jwt`: JSON Web Token (JWT) bearer token that is attached as an\n `Authorization` request header, following the keyword `Bearer`, to any\n outgoing service call, if provided, in order to ascertain whether the user has\n permissions to access resources and/or whether user-specific policies or\n contstraints apply (e.g., custom prices, discounts, quotas). Note that when\n using TEStribute through the HTTP API, this property is not available.\n Instead, the token value itself needs to be passed as an `Authorization`\n request header, also following the `Bearer` keyword.\n\n> For more details, including typing information, explore the [API definition],\n> which also forms the basis for validating CLI arguments and the inputs to the\n> `rank_services()` function.\n\n### Example calls\n\nThe following are equivalent calls for either of the TEStribute entry points\ndefined above. Note that the provided TES and DRS URIs point to test instances\nof the services which may or may not be up and running at any given time.\nTherefore, the success of the calls cannot be guaranteed.\n\n#### API service call payload (JSON)\n\n```json\n{\n \"object_ids\": [\n \"a001\",\n \"a002\"\n ],\n \"drs_uris\": [\n \"http://131.152.229.71/ga4gh/drs/v1/\",\n \"http://193.166.24.114/ga4gh/drs/v1/\"\n ],\n \"mode\": 0.5,\n \"resource_requirements\": {\n \"cpu_cores\": 1,\n \"disk_gb\": 1,\n \"execution_time_sec\": 1800,\n \"ram_gb\": 1\n },\n \"tes_uris\": [\n \"http://131.152.229.70/ga4gh/tes/v1/\",\n \"http://193.166.24.111/ga4gh/tes/v1/\"\n ]\n}\n```\n\n#### Console script call\n\n```bash\ntestribute \\\n --tes-uri=\"http://131.152.229.70/ga4gh/tes/v1/\" \\\n --tes-uri=\"http://193.166.24.111/ga4gh/tes/v1/\" \\\n --cpu-cores=1 \\\n --ram-gb=1 \\\n --disk-gb=1 \\\n --execution-time-sec=1800 \\\n --object-id=\"a001\" \\\n --object-id=\"a002\" \\\n --drs-id=\"http://131.152.229.71/ga4gh/drs/v1/\" \\\n --drs_id=\"http://193.166.24.114/ga4gh/drs/v1/\" \\\n --mode=0.5\n```\n\n#### Function call\n\n```py\nfrom TEStribute import rank_services\n\nrank_services(\n object_ids=[\n \"a001\",\n \"a002\"\n ],\n resource_requirements={\n \"cpu_cores\": 1,\n \"ram_gb\": 1,\n \"disk_gb\": 1,\n \"execution_time_sec\": 1800\n },\n tes_uris=[\n \"http://131.152.229.70/ga4gh/tes/v1/\",\n \"http://193.166.24.111/ga4gh/tes/v1/\"\n ],\n drs_uris=[\n \"http://131.152.229.71/ga4gh/drs/v1/\",\n \"http://193.166.24.114/ga4gh/drs/v1/\"\n ],\n mode=0.5,\n jwt=None\n)\n```\n\n### Return types\n\n#### Success\n\nUpon success, the API service returns a JSON object such as this:\n\n```json\n{\n \"service_combinations\": [\n {\n \"access_uris\": {\n \"a001\": \"ftp://ftp.ensembl.org/pub/release-96/fasta/homo_sapiens/dna//Homo_sapiens.GRCh38.dna.chromosome.19.fa.gz\",\n \"a002\": \"ftp://ftp.ensembl.org/pub/release-81/bed/ensembl-compara/11_teleost_fish.gerp_constrained_elements/gerp_constrained_elements.tetraodon_nigroviridis.bed.gz\",\n \"tes_uri\": \"http://193.166.24.111/ga4gh/tes/v1/\"\n },\n \"cost_estimate\": {\n \"amount\": 294727.1443451331,\n \"currency\": \"EUR\"\n },\n \"rank\": 1,\n \"time_estimate\": 2514\n },\n {\n \"access_uris\": {\n \"a001\": \"ftp://ftp.ensembl.org/pub/release-96/fasta/homo_sapiens/dna//Homo_sapiens.GRCh38.dna.chromosome.19.fa.gz\",\n \"a002\": \"ftp://ftp.ensembl.org/pub/release-81/bed/ensembl-compara/11_teleost_fish.gerp_constrained_elements/gerp_constrained_elements.tetraodon_nigroviridis.bed.gz\",\n \"tes_uri\": \"http://131.152.229.70/ga4gh/tes/v1/\"\n },\n \"cost_estimate\": {\n \"amount\": 294697.1938522269,\n \"currency\": \"EUR\"\n },\n \"rank\": 2,\n \"time_estimate\": 3298\n }\n ],\n \"warnings\": []\n}\n```\n\nYou can check out the `Response` model in the [API definition] for more details.\nFor the other entry points, the general response upon success is the same, but\nprovided in different ways. When calling `rank_services()` directly from within\nPython code, the response is an instance of Python class `Response`, which is\nbased on the corresponding model in the [API definition] and defined in module\n[TEStribute.models.response]. It can be converted to dictionary form with:\n\n```py\nresponse = rank_service(...)\nresponse.to_dict()\n```\n\nIt can be further converted to JSON with:\n\n```py\nimport json\n\njson.dumps(response.to_dict())\n```\n\nWhen using the `testribute` console script, the JSONified response is printed to\n`STDOUT`.\n\n#### Failure\n\nIn case of failure, the API service returns a JSON object of the following form:\n\n```json\n{\n \"code\": 400,\n \"errors\": [\n {\n \"reason\": \"werkzeug.exceptions.BadRequest\",\n \"message\": [\n \"Services cannot be ranked. None of the specified TES instances provided any task info.\"\n ]\n }\n ],\n \"message\": \"The request caused an error.\"\n}\n```\n\nWhen using the console script `testribute`, an error will lead to the script\nexiting with a non-zero return code. In addition, warnings and errors are\nwritten to the log which is streamed to `STDERR`, e.g.:\n\n```console\n[WARNING] TES unavailable: the provided URI 'http://i.do.not.exist/' could not be resolved.\n[ERROR] ResourceUnavailableError: Services cannot be ranked. None of the specified TES instances provided any task info.\n```\n\nWhen calling `rank_services()` directly from within Python code, traceback\ninformation for any error is provided, too. For\nexample:\n\n```console\n[WARNING] TES unavailable: the provided URI 'http://i.do.not.exist/' could not be resolved.\nTraceback (most recent call last):\n File \"\", line 21, in \n File \"/home/uniqueg/Dropbox/repos/TEStribute/TEStribute/__init__.py\", line 129, in rank_services\n target_currency=models.Currency[config[\"target_currency\"]],\n File \"/home/uniqueg/Dropbox/repos/TEStribute/TEStribute/models/response.py\", line 55, in __init__\n timeout=self.timeout,\n File \"/home/uniqueg/Dropbox/repos/TEStribute/TEStribute/utils/service_calls.py\", line 311, in fetch_tes_task_info\n \"Services cannot be ranked. None of the specified TES instances \" \\\nTEStribute.errors.ResourceUnavailableError: Services cannot be ranked. None of the specified TES instances provided any task info.\n```\n\n### Configuration\n\nIt is possible to configure some settings of the app, e.g., how JWTs are parsed,\nprocessed and forwarded or in which prices costs are reported, by modifying the\nthe [config file](TEStribute/config/config.yaml) before starting the service /\nrunning TEStribute.\n\n## Testing\n\nUnit and integration tests can be run with the following command:\n\n```bash\npytest\n```\n\n> Note that test coverage is currently sparse and tests are unstable.\n\n## Contributing\n\nThis project is a community effort and lives off your contributions, be it in\nthe form of bug reports, feature requests, discussions, or fixes and other code\nchanges. Please read the [contributing guidelines] if you want to contribute.\nAnd please mind the [code of conduct] for all interactions with the community.\n\n## Versioning\n\nDevelopment of the app is currently still in alpha stage, and current versioning\nis for internal use only. In the future, we are aiming to adopt [semantic\nversioning] that is synchronized to the versioning of [mock-TES], [TES-cli],\n[mock-DRS], and [DRS-cli] in order to ensure that these apps will be compatible\nas long as both their major and minor versions match.\n\n## License\n\nThis project is covered by the [Apache License 2.0] also available [shipped\nwith this repository](LICENSE).\n\n## Contact\n\nPlease contact the [project leader](mailto:alexander.kanitz@sib.swiss) for\ninquiries, proposals, questions etc. that are not covered by the\n[Contributing](#Contributing) section.\n\n## Acknowledgments\n\nThe project is a collaborative effort under the umbrella of the [ELIXIR Cloud\nand AAI] group. It was started during the [2019 Google Summer of Code] as part\nof the [Global Alliance for Genomics and Health][1] [organization].\n\n![logo banner]\n\n[1]: \n[2019 Google Summer of Code]: \n[Apache License 2.0]: \n[API definition]: TEStribute/specs/schema.TEStribute.openapi.yaml\n[code of conduct]: CODE_OF_CONDUCT.md\n[config]: TEStribute/config/config.yaml\n[contributing guidelines]: CONTRIBUTING.md\n[Data Repository Service]: \n[DRS-cli]: \n[ELIXIR Cloud and AAI]: \n[Git]: \n[logo banner]: images/logo-banner.png\n[mock-TES]: \n[modififications]: \n[mock-DRS]: \n[`mode.Mode`]: TEStribute/models.py\n[organization]: \n[OpenAPI]: \n[pip]: \n[Python]: \n[semantic versioning]: \n[Task Execution Service]: \n[TES-cli]: \n[TEStribute.models.response]: TEStribute/models/response.py\n[virtualenv]: \n[TEStribute_working]:images/schema.png\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/elixir-europe/TEStribute", "keywords": "ga4gh tes elixir rest api app openapi python task distribution", "license": "", "maintainer": "Alexander Kanitz", "maintainer_email": "alexander.kanitz@alumnni.ethz.ch", "name": "TEStribute", "package_url": "https://pypi.org/project/TEStribute/", "platform": "", "project_url": "https://pypi.org/project/TEStribute/", "project_urls": { "ELIXIR Cloud & AAI": "https://elixir-europe.github.io/cloud/", "Homepage": "https://github.com/elixir-europe/TEStribute", "Repository": "https://github.com/elixir-europe/TEStribute", "Tracker": "https://github.com/elixir-europe/TEStribute/issues" }, "release_url": "https://pypi.org/project/TEStribute/0.2.1/", "requires_dist": [ "addict (==2.2.0)", "asn1crypto (==0.24.0)", "astroid (==2.2.5)", "atomicwrites (==1.3.0)", "attrs (==19.1.0)", "Automat (==0.7.0)", "autopep8 (==1.4.4)", "bleach (==3.1.0)", "bravado (==10.4.1)", "bravado-core (==5.13.1)", "certifi (==2019.6.16)", "cffi (==1.12.3)", "chardet (==3.0.4)", "Click (==7.0)", "clickclick (==1.2.2)", "connexion (==2.3.0)", "constantly (==15.1.0)", "crochet (==1.10.0)", "cryptography (==2.7)", "cssselect (==1.0.3)", "decorator (==4.4.0)", "dicttoxml (==1.7.4)", "docutils (==0.15.2)", "drs-client (==0.2.0)", "fido (==4.2.2)", "Flask (==1.1.1)", "forex-python (==1.5)", "future (==0.17.1)", "geocoder (==1.38.1)", "geographiclib (==1.49)", "geoip2 (==2.9.0)", "geopy (==1.20.0)", "hyperlink (==19.0.0)", "idna (==2.8)", "importlib-metadata (==0.19)", "incremental (==17.5.0)", "inflection (==0.3.1)", "ip2geotools (==0.1.5)", "IP2Location (==8.1.0)", "isort (==4.3.21)", "itsdangerous (==1.1.0)", "Jinja2 (==2.10.1)", "jsonpointer (==2.0)", "jsonref (==0.2)", "jsonschema (==2.6.0)", "lazy-object-proxy (==1.4.1)", "lxml (==4.3.4)", "MarkupSafe (==1.1.1)", "maxminddb (==1.4.1)", "mccabe (==0.6.1)", "monotonic (==1.5)", "more-itertools (==7.2.0)", "msgpack-python (==0.5.6)", "numpy (==1.17.2)", "openapi-spec-validator (==0.2.8)", "packaging (==19.0)", "pip-review (==1.0)", "pkginfo (==1.5.0.1)", "pluggy (==0.12.0)", "py (==1.8.0)", "pycodestyle (==2.5.0)", "pycparser (==2.19)", "Pygments (==2.4.2)", "PyHamcrest (==1.9.0)", "PyJWT (==1.6.4)", "pylint (==2.3.1)", "pyparsing (==2.4.1.1)", "pyquery (==1.4.0)", "pyrsistent (==0.15.3)", "pytest (==5.0.1)", "python-dateutil (==2.8.0)", "pytz (==2019.1)", "PyYAML (==5.1.1)", "ratelim (==0.1.6)", "readme-renderer (==24.0)", "requests (==2.22.0)", "requests-toolbelt (==0.9.1)", "rfc3987 (==1.3.8)", "selenium (==3.141.0)", "setuptools-git (==1.2)", "simplejson (==3.16.0)", "six (==1.12.0)", "strict-rfc3339 (==0.7)", "swagger-spec-validator (==2.4.3)", "swagger-ui-bundle (==0.0.5)", "tes-client (==0.2.1)", "tqdm (==4.32.2)", "twine (==1.13.0)", "Twisted (==19.7.0)", "typed-ast (==1.4.0)", "typing (==3.7.4)", "typing-extensions (==3.7.4)", "urllib3 (==1.25.3)", "wcwidth (==0.1.7)", "webcolors (==1.9.1)", "webencodings (==0.5.1)", "Werkzeug (==0.15.6)", "wrapt (==1.11.2)", "yelp-bytes (==0.3.0)", "yelp-encodings (==0.1.3)", "zipp (==0.5.2)", "zope.interface (==4.6.0)" ], "requires_python": "~=3.6", "summary": "Task distribution for GA4GH TES instances", "version": "0.2.1", "yanked": false, "yanked_reason": null }, "last_serial": 6133875, "releases": { "0.2.0": [ { "comment_text": "", "digests": { "md5": "09383d119ded8682ffa7a5741a17c3f6", "sha256": "b599143811e1e9bb8b70af38994d21c997e292cc58e0b4c4b28b98219c9dc559" }, "downloads": -1, "filename": "TEStribute-0.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "09383d119ded8682ffa7a5741a17c3f6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": "~=3.6", "size": 40961, "upload_time": "2019-10-21T13:22:05", "upload_time_iso_8601": "2019-10-21T13:22:05.438705Z", "url": "https://files.pythonhosted.org/packages/80/ad/67a4a8f0ca2d1098b70fdcfd0a28176d8134b0c7904aa6d5114cbcc112b4/TEStribute-0.2.0-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "138d310d5ef9624ba7f4e1e8bb632008", "sha256": "04ceeb432854f1a2d1593d8f74b9dd9967cedce5380c758321da5c41c26fb918" }, "downloads": -1, "filename": "TEStribute-0.2.0.tar.gz", "has_sig": false, "md5_digest": "138d310d5ef9624ba7f4e1e8bb632008", "packagetype": "sdist", "python_version": "source", "requires_python": "~=3.6", "size": 108689, "upload_time": "2019-10-21T13:22:10", "upload_time_iso_8601": "2019-10-21T13:22:10.646780Z", "url": "https://files.pythonhosted.org/packages/52/53/356cbaec9a09813c9d99765901d365e1217d003e1a05c91f5fbab7bff939/TEStribute-0.2.0.tar.gz", "yanked": false, "yanked_reason": null } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "1cfb0fd765c40a7c5736a1d875f40155", "sha256": "ba5cbfb847c566e789b50c89f075ce04e9b8428a60ceb0e1096fb532d37c4d00" }, "downloads": -1, "filename": "TEStribute-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "1cfb0fd765c40a7c5736a1d875f40155", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": "~=3.6", "size": 40907, "upload_time": "2019-11-14T01:05:56", "upload_time_iso_8601": "2019-11-14T01:05:56.976559Z", "url": "https://files.pythonhosted.org/packages/c1/9e/2b7be93e0f8508cde37e335829398c698d1200c7cbf7df8d41824d9ed62b/TEStribute-0.2.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ff3533fbc3eadc29480866bbccc79c16", "sha256": "2cea880de3ad3db39e10e11d88dfd432a319a658ba8df5e222ce0a0df5a103f0" }, "downloads": -1, "filename": "TEStribute-0.2.1.tar.gz", "has_sig": false, "md5_digest": "ff3533fbc3eadc29480866bbccc79c16", "packagetype": "sdist", "python_version": "source", "requires_python": "~=3.6", "size": 409379, "upload_time": "2019-11-14T01:06:02", "upload_time_iso_8601": "2019-11-14T01:06:02.430302Z", "url": "https://files.pythonhosted.org/packages/23/01/6fd2a1481c7492c36d40c993a3badb21cdb7d3745d4a48a849e8f462ec3b/TEStribute-0.2.1.tar.gz", "yanked": false, "yanked_reason": null } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "1cfb0fd765c40a7c5736a1d875f40155", "sha256": "ba5cbfb847c566e789b50c89f075ce04e9b8428a60ceb0e1096fb532d37c4d00" }, "downloads": -1, "filename": "TEStribute-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "1cfb0fd765c40a7c5736a1d875f40155", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": "~=3.6", "size": 40907, "upload_time": "2019-11-14T01:05:56", "upload_time_iso_8601": "2019-11-14T01:05:56.976559Z", "url": "https://files.pythonhosted.org/packages/c1/9e/2b7be93e0f8508cde37e335829398c698d1200c7cbf7df8d41824d9ed62b/TEStribute-0.2.1-py3-none-any.whl", "yanked": false, "yanked_reason": null }, { "comment_text": "", "digests": { "md5": "ff3533fbc3eadc29480866bbccc79c16", "sha256": "2cea880de3ad3db39e10e11d88dfd432a319a658ba8df5e222ce0a0df5a103f0" }, "downloads": -1, "filename": "TEStribute-0.2.1.tar.gz", "has_sig": false, "md5_digest": "ff3533fbc3eadc29480866bbccc79c16", "packagetype": "sdist", "python_version": "source", "requires_python": "~=3.6", "size": 409379, "upload_time": "2019-11-14T01:06:02", "upload_time_iso_8601": "2019-11-14T01:06:02.430302Z", "url": "https://files.pythonhosted.org/packages/23/01/6fd2a1481c7492c36d40c993a3badb21cdb7d3745d4a48a849e8f462ec3b/TEStribute-0.2.1.tar.gz", "yanked": false, "yanked_reason": null } ], "vulnerabilities": [] }