{ "info": { "author": "Axiros GmbH", "author_email": "gk@axiros.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Pre-processors" ], "description": "# DevApps - DevOps Ready Applications\n\n[![Build Status][cisvg]][ci] [![Coverage Status][covsvg]][cov] [![PyPI version][pypisvg]][pypi] [![Code style: black][blacksvg]][black]\n\n[cisvg]: https://travis-ci.org/axiros/DevApps.svg?branch=master\n[ci]: https://travis-ci.org/axiros/DevApps\n[covsvg]: https://codecov.io/github/axiros/DevApps/branch/master/graph/badge.svg\n[cov]: https://codecov.io/github/axiros/DevApps\n[pypisvg]: https://img.shields.io/pypi/v/devapps.svg\n[pypi]: https://badge.fury.io/py/DevApps\n[blacksvg]: https://img.shields.io/badge/code%20style-black-000000.svg\n[black]: https://github.com/ambv/black\n\n\n\n\n\n\n[TOC]\n\n# `devapp`: Command Line Function Configurator and Runner\n\nGiven you have a setup like this:\n\n```python\nimport operator\n\n\nclass Calc:\n \"\"\"Calculator Demo\"\"\"\n\n oper_func = 'add'\n\n # One (PY2 compatible) way to provide type hints (optional, for less imperative code):\n def do_run(self, a=int, b=int):\n \"\"\"Runs operator function on the arguments\"\"\"\n return getattr(operator, self.oper_func, self.not_found)(a, b)\n\n def not_found(self, *a, **kw):\n raise Exception('not supported:', self.oper_func)\n```\n\nVia the `devapp` command, you can provide config on the CLI (in addition to\nconfig file and environ) and run this right away:\n\n```bash\n$ devapp ./calc.py 41 1\n42\n\n$ devapp ./calc.py b=1 41\n42\n\n$ devapp ./calc.py oper_func=mul a=2 b=100\n200\n\n$ devapp ./calc.py of=mul a=2 b=150\n300\n\n$ export Calc_oper_func=mul Calc_run='{\"a\":100}'; devapp ./calc.py b=4\n400\n\n$ devapp ./calc.py a=1 b=foo # b has wrong type\n0.02285 [error ] Cannot cast expected_type=int for_param=b got=foo\n\n$ devapp ./calc.py # missing params\n0.02001 [error ] Value required param=a type=int\n0.02025 [error ] Value required param=b type=int\n\n$ devapp ./calc.py of=mul -h # help output\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | mul | C | add | | |\n\n## Actions\n\n### run\n\nRuns operator function on the arguments\n\n> do_run(a=, b=)\n```\n\n\n# `@app`: Converts Class Trees\n\nThe decorator variant allows to run the application standalone:\n\n```python\n#!/usr/bin/env python\n\nimport operator\nfrom devapps.app import app # one way of usage is via this decorator\n\n\n@app # apply the decorator to your app holding class\nclass Calc:\n \"\"\"Calculator Demo\"\"\"\n\n oper_func = 'add'\n\n def do_run(calc, a=int, b=int):\n \"\"\"Runs operator function on the arguments\"\"\"\n return getattr(operator, calc.oper_func, calc.not_found)(a, b)\n\n def not_found(calc, *a, **kw):\n raise Exception('not supported:', calc.oper_func)\n```\n\nWe\n- added a hashbang and made the file executable.\n- decorated the toplevel class.\n\nWe can run the thing now from the CLI, directly:\n\n\n```bash\n$ ./calc1.py a=2 b=4\n6\n\n$ ./calc1.py b=2 a=4 # mappable so supported\n6\n\n$ ./calc1.py 2 4.1 # positionally given, rounded\n6\n\n$ ./calc1.py a=2 4.1 # mapping found\n6\n\n$ ./calc1.py oper_func=mul a=2 b=4\n8\n\n$ ./calc1.py of=mul a=2 b=4 # short form for oper_func\n8\n```\n\n## More Actions\n\nA class may have more action functions (by default prefixed with `do_`.\n`do_run` was just the default action - run if no other is configured.\n\nLets add another one:\n\n```python\n#!/usr/bin/env python\n\nimport operator\nfrom devapps.app import app # one way of usage is via this decorator\n\n\n@app # apply the decorator to your app holding class\nclass Calc:\n \"\"\"Calculator Demo\"\"\"\n\n oper_func = 'add'\n\n op = lambda calc: getattr(operator, calc.oper_func, calc.not_found)\n\n def do_run(calc, a=int, b=int):\n \"\"\"Runs operator function on the arguments\"\"\"\n return calc.op()(a, b)\n\n def do_list_sum(calc, args=[0]):\n \"\"\"Sums up all numbers given\"\"\"\n return sum(args)\n\n def not_found(calc, *a, **kw):\n raise Exception('not supported:', calc.oper_func)\n```\n\nand run it:\n```bash\n$ ./calc2.py list_sum \"1,2,3\"\n6\n\n$ ./calc2.py ls \"1, 2, 3\" # short form for function supported\n6\n```\n\n## Help Output\n\n`-h` delivers a markdown formatted help output:\n```markdown\n$ ./calc2.py -h\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | add | | | | |\n\n## Actions\n\n### list_sum\n\nSums up all numbers given\n\n> do_list_sum(args=[0])\n\n### run\n\nRuns operator function on the arguments\n\n> do_run(a=, b=)\n```\n\n### Markdown?\n\nBecause this allows to add a lot of structuring information - which we can use to nicely colorize the output, provide TOCs, put into README's and so on.\n\n`-hc` shows the implementation:\n```markdown\n$ ./calc2.py -hc\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | add | | | | |\n\n## Actions\n\n### list_sum\n\nSums up all numbers given\n\n``python=\ndef do_list_sum(calc, args=[0]):\n return sum(args)\n``\n\n### run\n\nRuns operator function on the arguments\n\n``python=\ndef do_run(calc, a=int, b=int):\n return calc.op()(a, b)\n``\n```\n\n> If the terminal width is not wide enough for the parameter tables we render the parameters vertically.\n> `-hu` (classic unix) forces this always.\n\n### Defaults Are Configurable\nLets check `-h` output when arguments are supplied:\n```markdown\n$ ./calc2.py of=multiply 1 -h | head -n 10\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | -------- | - | ---- | ----- | ---- |\n| oper_func | multiply | C | add | | |\n```\n\nAs you can see our value from the CLI made it into the documentation. \nThe `F` (From) column shows where the value was comming from.\n\n# Providers\n\nChanging the defaults of an app makes more sense to do via other means\nthan the CLI.\n\nBuilt in we do have two more so called \"providers\", i.e. deliverers of config:\n\n`0.` [Programmed Defaults]\n`1.` Config File\n`2.` Environ\n`3.` CLI\n\noverriding each other's values in the given order. That order can be changed.\n\n## File\n\nLets create a config file, changing the default operator to `mul` and also the default of the first *function parameter* `a`:\n\n```python\n$ python -c \"if 1:\n cfg = {'oper_func': 'mul', 'run': {'a': 10.3}}\n\n # write to user config dir:\n from appdirs import user_config_dir as ucd\n from json import dumps\n with open(ucd() + '/Calc.json', 'w') as fd:\n fd.write(dumps(cfg))\"\n```\nNow we can run the app *without* supplying `a` and get a multiplication:\n```bash\n$ ./calc1.py b=42\n420\n```\n> Positionally you could overwrite `a` still on the CLI, so we do not map one value only to `b`\n```bash\n$ ./calc1.py 5 42\n210\n```\n```bash\n$ ./calc1.py 5\n0.03074 [error ] Value required param=b type=int\n```\nHere is the output of `-h`:\n```markdown\n$ ./calc1.py -h\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | mul | F | add | | |\n\n## Actions\n\n### run\n\nRuns operator function on the arguments\n:::warning\nDefaults modified (by File):\n- a: 10 (was )\n:::\n\n> do_run(a=10, b=)\n```\nAgain the app was reconfigured - this time by the config file (F)\n\nObserve the int value - it was converted from the float, since that is what the function explicitly asked for.\n> Yes, we did mutate inplace the defaults of the `Calc.do_run` function - i.e. process wide!\n> Keep this in mind when using that feature - reading the source code is then misleading.\n> Help output shows modifications and origin rather prominently as you can see.\n\nWe delete the file for now.\n\n## Environ\n\nSupported as well - but you have to provide structed values in lit.eval form:\n```bash\n$ export Calc_oper_func=mul Calc_run='{\"a\":4.2}'; ./calc1.py b=3\n12\n```\n\n> By default we do NOT support short forms at the environ provider and also we are case sensitive.\n> Note that the overridden defaults still had been casted to the original types of the function signature.\n\n\nHelp output, as expected:\n```markdown\n$ export Calc_oper_func=xxx Calc_run='{\"b\":4.2}';./calc1.py a=2.1 -h\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | xxx | E | add | | |\n\n## Actions\n\n### run\n\nRuns operator function on the arguments\n:::warning\nDefaults modified (by Env):\n- b: 4 (was )\n:::\n\n> do_run(a=, b=4)\n```\n\nUp to now there is no indication within the app regarding allowed values for the operator function.\nThat is why we accepted the bogus value, when configuring the app.\n\n# Nesting Functional Blocks\n\nWhen the app gets more complex you can recursively nest/compose functional blocks into each other\n\n```python\n#!/usr/bin/env python\n\nimport operator\nfrom devapps.app import app\n\n\nclass Log:\n \"\"\"Print Logger\"\"\"\n\n level = 10\n\n def do_testmsg(log, ev='hello'):\n log.msg(30, ev)\n return ''\n\n def msg(log, level, ev, **kw):\n if level >= log.level:\n print('[%s] %s %s' % (level, ev, kw))\n\n\n@app\nclass Calc:\n \"\"\"Calculator Demo\"\"\"\n\n oper_func = 'add'\n log = Log\n\n def do_run(calc, a=int, b=int):\n \"\"\"Runs operator function on the arguments\"\"\"\n of = calc.oper_func\n calc.log.msg(10, 'Calculating', operation=of, a=a, b=b)\n res = getattr(operator, of, calc.not_found)(a, b)\n calc.log.msg(20, 'Returning', result=res)\n return res\n\n def not_found(calc, *a, **kw):\n raise Exception('not supported:', calc.oper_func)\n```\n```bash\n$ ./calc_tree.py 1 299\n[10] Calculating {'operation': 'add', 'a': 1, 'b': 299}\n[20] Returning {'result': 300}\n300\n\n$ ./calc_tree.py log.level=20 of=mul 100 3\n[20] Returning {'result': 300}\n300\n\n$ ./calc_tree.py l.l=20 of=mul 100 3 # shorthand notation for nested blocks\n[20] Returning {'result': 300}\n300\n\n$ ./calc_tree.py l.t \"hi there\" # calling nested functions\n[30] hi there {}\n```\n\n> Of course you could have defined the inner class directly within the main app class as well\n\nHelp output (again with overridden defaults):\n\n```bash\n$ ./calc_tree.py l.l=20 l.t.ev=hi of=mul -h\n\n# Calc\n\nCalculator Demo\n\n## Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| --------- | --- | - | ---- | ----- | ---- |\n| oper_func | mul | C | add | | |\n\n## Actions\n\n### run\n\nRuns operator function on the arguments\n\n> do_run(a=, b=)\n\n---\n## log\n\nPrint Logger\n\n### Parameters\n\n| Name | Val | F | Dflt | Descr | Expl |\n| ----- | --- | - | ---- | ----- | ---- |\n| level | 20 | C | 10 | | |\n\n### Actions\n\n#### testmsg\n\n:::warning\nDefaults modified (by CLI):\n- ev: hi (was hello)\n:::\n\n> do_testmsg(ev=hi)\n```\n\n## Tree Navigation\n\nThe arrangement of nested classes can be navigated during runtime like so:\n\n```python\n#!/usr/bin/env python\nfrom __future__ import print_function # for Python2\n\nimport operator\nimport attr\nfrom devapps import root, parent, as_dict\nfrom devapps import app\n\n\nclass Inner:\n inner_var = 1\n\n def do_nav_demo(inner):\n return root(inner).do_run(inner.inner_var, inner.Deep.deep_var)\n\n class Deep:\n deep_var = 2\n\n def do_nav_demo(deep):\n print(root(deep).app_var, parent(deep).do_nav_demo())\n return ''\n\n\n@app.app\nclass App:\n inner = Inner\n app_var = 0\n\n def do_run(app, a=1, b=2):\n return a, b\n\n def do_dump(app, asdict=False):\n print(app if not asdict else as_dict(app))\n return ''\n```\n\nCalling `App.inner.Deep.do_nav_demo()` on a configured tree:\n```bash\n$ ./calc_tree_nav.py av=100 i.iv=200 i.D.dv=300 i.D.nd\n100 (200, 300)\n```\n\n# Serializing / PrettyPrint\n\nConfigurative state can be pretty printed and dict-dumped:\n```bash\n$ ./calc_tree_nav.py av=1 i.D.dv=42 du # du matched to dump\nApp(app_var=1, inner=Inner(Deep=Inner.Deep(deep_var=42), inner_var=1))\n\n\n$ ./calc_tree_nav.py app_var=2 inner.Deep.deep_var=42 dump asdict=true\n{'app_var': 2, 'inner': {'Deep': {'deep_var': 42}, 'inner_var': 1}}\n```\n\nThe dict format can be piped as is into a config file for subsequent runs.\n> Currently we do not serialize function parameter changes.\n\n\n\n# Credits\n\n[Hynek Schlawack](https://hynek.me/):\n\n- [structlog][structlog]\n- [attrs][attrs]\n\nTesting/CI:\n\n- [pytest](https://github.com/pytest-dev/pytest/graphs/contributors)\n- [codecov][cov]\n- [travis][ci]\n\n[structlog]: https://github.com/hynek/structlog\n[attrs]: https://github.com/python-attrs/attrs\n\n\n\n# Alternatives\n\nThere are already tons of options to get the CLI parsed:\n\n- [click](https://pypi.org/project/click/)\n- [docopt](https://github.com/docopt/docopt)\n- [argparse](https://docs.python.org/3/library/argparse.html) Stdlib\n- [argh](https://github.com/neithere/argh/) \n\nFurther great libs, as from the argh docs:\n\n- [argdeclare](http://code.activestate.com/recipes/576935-argdeclare-declarative-interface-to-argparse/)\n- [argparse-cli](http://code.google.com/p/argparse-cli/)\n- [django-boss](https://github.com/zacharyvoase/django-boss/tree/master/src/)\n seems to lack support for nested commands and is strictly Django-specific.\n- [entrypoint](http://pypi.python.org/pypi/entrypoint/) is lightweight\n- [opster](http://pypi.python.org/pypi/opster/) and\n [finaloption](http://pypi.python.org/pypi/finaloption/) support\n nested commands but are based on the outdated optparse library and\n therefore reimplement some features available in argparse.\n- [simpleopt](http://pypi.python.org/pypi/simpleopt/)\n- [opterator](https://github.com/buchuki/opterator/) is based on the\n outdated optparse and does not support nested commands.\n- [clap](http://pypi.python.org/pypi/Clap/)\n- [plac](http://micheles.googlecode.com/hg/plac/doc/plac.html) is a\n very powerful alternative to argparse.\n- [baker](http://pypi.python.org/pypi/Baker/)\n- [plumbum](http://plumbum.readthedocs.org/en/latest/cli.html)\n- [docopt](http://docopt.org)\n- [aaargh](http://pypi.python.org/pypi/aaargh)\n- [cliff](http://pypi.python.org/pypi/cliff)\n- [cement](http://builtoncement.com/2.0/)\n\nSeems like *every man should plant a tree, raise a son and write a command line parser.*\n\n\n\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": "http://github.com/axiros/DevApps", "keywords": "app,config,source code", "license": "", "maintainer": "", "maintainer_email": "", "name": "devapps", "package_url": "https://pypi.org/project/devapps/", "platform": "", "project_url": "https://pypi.org/project/devapps/", "project_urls": { "Homepage": "http://github.com/axiros/DevApps" }, "release_url": "https://pypi.org/project/devapps/20181022/", "requires_dist": [ "attrs", "structlog", "appdirs", "colorama", "coverage; extra == 'tests'", "pytest-to-md; extra == 'tests'", "pytest (>=3.3.0); extra == 'tests'" ], "requires_python": "", "summary": "Application Config", "version": "20181022" }, "last_serial": 4433459, "releases": { "20181011": [ { "comment_text": "", "digests": { "md5": "f2161c908d6b4dfc8e9aac1219492209", "sha256": "8708714b41935f7db52b15051889e6d9b8cccfc8a0be2b2bbcfb835688c76820" }, "downloads": -1, "filename": "devapps-20181011-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "f2161c908d6b4dfc8e9aac1219492209", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 23663, "upload_time": "2018-10-28T23:06:37", "url": "https://files.pythonhosted.org/packages/1e/71/b0d1d6ac01ef7ead4cb2ce1087696496202e84b146daa2aa72ec25747fb8/devapps-20181011-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "47faadac606d07c2180c1b25b9ebc7a2", "sha256": "a3edd837c2a6a8b01392998274cd66b476c0089b31658ae8b43cb08b7840be9a" }, "downloads": -1, "filename": "devapps-20181011.tar.gz", "has_sig": false, "md5_digest": "47faadac606d07c2180c1b25b9ebc7a2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27509, "upload_time": "2018-10-28T23:06:39", "url": "https://files.pythonhosted.org/packages/86/f2/175ffe05d4860a996382bc12c9c5469e17dbee81181f89d85bd5d5ef51b5/devapps-20181011.tar.gz" } ], "20181020": [ { "comment_text": "", "digests": { "md5": "0b3727aeb11818a5dafaf5305983b856", "sha256": "12669530a91b02e081f5b43cf7bccf85ff217e2db899e4b1ea712b2d45d5803d" }, "downloads": -1, "filename": "devapps-20181020-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "0b3727aeb11818a5dafaf5305983b856", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 23832, "upload_time": "2018-10-29T13:18:09", "url": "https://files.pythonhosted.org/packages/fe/2d/edcf6d5995eff3850a5342dd78c0dcb2721b6be71cb91f751edd71d91898/devapps-20181020-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "eb4532bb0d2da331794f2ae075301c60", "sha256": "73fabeeb923d4ffcf6ff4b042a63543ce5fe6671f0360301003674dba9ec07ee" }, "downloads": -1, "filename": "devapps-20181020.tar.gz", "has_sig": false, "md5_digest": "eb4532bb0d2da331794f2ae075301c60", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27717, "upload_time": "2018-10-29T13:18:11", "url": "https://files.pythonhosted.org/packages/d1/2c/b5f942d0bcbb69428696ba7c1c5a4ccf67d0f130484aae04b751d01a1f01/devapps-20181020.tar.gz" } ], "20181021": [ { "comment_text": "", "digests": { "md5": "ff4285ef25cc4563e8d4a689dda256ec", "sha256": "35d80a54932ae60004804f70872998c67c7b842c6343d9c2470b2f0a18031038" }, "downloads": -1, "filename": "devapps-20181021-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "ff4285ef25cc4563e8d4a689dda256ec", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 23832, "upload_time": "2018-10-30T18:59:42", "url": "https://files.pythonhosted.org/packages/a3/32/a12b63f5fed9f9ae92c5a735cc95a344065606709d56113688efc5ceea1c/devapps-20181021-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "4d12c35839420d61a7b2bbac44b3c58e", "sha256": "6a681ad68cbcdbc37af068cf7c900abc892553b12902162d6659e02f1bd2259a" }, "downloads": -1, "filename": "devapps-20181021.tar.gz", "has_sig": false, "md5_digest": "4d12c35839420d61a7b2bbac44b3c58e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27723, "upload_time": "2018-10-30T18:59:43", "url": "https://files.pythonhosted.org/packages/07/1a/5556df2ae632b5c6894485225b45a43853af5c491b53b09e4ea001a652ac/devapps-20181021.tar.gz" } ], "20181022": [ { "comment_text": "", "digests": { "md5": "43742f877cbc85b7e649892b07df5641", "sha256": "0ccfb6c5a7a9481a9d83d258b3af7915d008613226fcec717914b765d3190919" }, "downloads": -1, "filename": "devapps-20181022-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "43742f877cbc85b7e649892b07df5641", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 23889, "upload_time": "2018-10-30T22:15:24", "url": "https://files.pythonhosted.org/packages/98/11/7321bceac313e78491497687a3931836e7cff7f2ea22e9a47a191832366a/devapps-20181022-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9a96fe5a4b928699d3f6894d1a158328", "sha256": "b2dc1d1dde88c393be4f89b73444771fcf5bfaa468b6f093ecf3fcdb24154918" }, "downloads": -1, "filename": "devapps-20181022.tar.gz", "has_sig": false, "md5_digest": "9a96fe5a4b928699d3f6894d1a158328", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27822, "upload_time": "2018-10-30T22:15:27", "url": "https://files.pythonhosted.org/packages/35/59/609d5dd9010bef20effae04594eb61ff020e754d3bb803e1a50462b9ca3f/devapps-20181022.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "43742f877cbc85b7e649892b07df5641", "sha256": "0ccfb6c5a7a9481a9d83d258b3af7915d008613226fcec717914b765d3190919" }, "downloads": -1, "filename": "devapps-20181022-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "43742f877cbc85b7e649892b07df5641", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 23889, "upload_time": "2018-10-30T22:15:24", "url": "https://files.pythonhosted.org/packages/98/11/7321bceac313e78491497687a3931836e7cff7f2ea22e9a47a191832366a/devapps-20181022-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9a96fe5a4b928699d3f6894d1a158328", "sha256": "b2dc1d1dde88c393be4f89b73444771fcf5bfaa468b6f093ecf3fcdb24154918" }, "downloads": -1, "filename": "devapps-20181022.tar.gz", "has_sig": false, "md5_digest": "9a96fe5a4b928699d3f6894d1a158328", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27822, "upload_time": "2018-10-30T22:15:27", "url": "https://files.pythonhosted.org/packages/35/59/609d5dd9010bef20effae04594eb61ff020e754d3bb803e1a50462b9ca3f/devapps-20181022.tar.gz" } ] }