{ "info": { "author": "Steinwurf ApS", "author_email": "contact@steinwurf.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Plugins", "Environment :: Web Environment", "Framework :: Sphinx :: Extension", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", "Topic :: Software Development :: Documentation", "Topic :: Text Processing", "Topic :: Utilities" ], "description": ".. image:: https://ci.appveyor.com/api/projects/status/l41u9e7y50r685ep?svg=true&branch=master\n :target: https://ci.appveyor.com/project/SteinwurfApS/wurfapi\n\n.. image:: https://travis-ci.org/steinwurf/wurfapi.svg?branch=master\n :target: https://travis-ci.org/steinwurf/wurfapi\n\nIntroduction\n============\n\nWe wanted to have a configurable and easy to use Sphinx API documentation\ngenerator for our C++ projects. To achieve this we leaned on others for\ninspiration:\n\n* Breathe (https://github.com/michaeljones/breathe): Excellent extension\n and the default choice for many.\n* Gasp (https://github.com/troelsfr/Gasp): Gasp inspired us by allowing\n templates to control the output. Unfortunately development of Gaps\n seems to have stopped.\n\nSo what is ``wurfapi``:\n\n* Essentially we picked up where Gasp let go. We have\n borrowed the idea of templates to make it highly configurable.\n\n* We made it easy to use by automatically running Doxygen to generate the\n initial API documentation.\n\n* We parse the Doxygen XML into an easy to use Python dictionary. Which can\n be consumed in the templates.\n\n* We prepared the extension for other backends (replacing Doxygen) e.g.\n https://github.com/foonathan/standardese once they become ready.\n\n.. contents:: Table of Contents:\n :local:\n\n\nStatus\n======\n\nWe are still very much in the initial development phase - all things are\nsubject to change.\n\n* Parsing Doxygen XML: We do not support everything yet (and probably never\n will). We still are missing some crucial elements like proper parsing of\n the text elements in comments, parameter descriptions etc.\n\nUsage\n=====\n\nWe recommend that you install wurfapi and sphinx in a virtual environment.\nTo use the extension, the following steps are needed:\n\n1. Create a virtual environment::\n\n Follow the https://docs.python.org/3/tutorial/venv.html\n\n2. Install the extension::\n\n pip install sphinx\n pip install wurfapi\n\n3. Generate the initial ``Sphinx`` documentation by running::\n\n mkdir docs\n cd docs\n python sphinx-quickstart\n\n You will need to enter some basic information about your project such\n as the project name etc.\n\n4. Open the ``conf.py`` generated by ``sphinx-quickstart`` and add the\n the following::\n\n # Append or insert 'wurfapi' in the extensions list\n extensions = ['wurfapi']\n\n # wurfapi options - relative to your docs dir\n wurfapi = {\n 'source_paths': ['../src'],\n 'recursive': True,\n 'parser': {'type': 'doxygen', 'download': True, 'warnings_as_error': True}\n }\n\n .. note::\n\n ``source_path``\n If you separate source and build dir in sphinx your 'source_path'\n should be something like '../../src'.\n\n ``recursive``\n Set recursive ``True`` if you want recursively scan the ``source_paths``\n\n ``download``\n If you do not want to automatically download Doxygen, set\n ``download`` to ``False``. In that case ``wurfapi`` will try to invoke\n plain ``doxygen`` without specifying any path or similar. This means\n it ``doxygen`` must be available in the path.\n\n ``warnings_as_error``\n If Doxygen emits many warnings you might want to set warnings_as_error\n to False until they have been fixed.\n\n5. To generate the API documentation for a class open a ``.rst`` file\n e.g. ``index.rst`` if you ran ``sphinx-quickstart``. Say we want to\n generate docs for a class called ``test`` in the namespace ``project``.\n\n To do this we add the following directive to the rst file::\n\n .. wurfapi:: class_synopsis.rst\n :selector: project::coffee::machine\n\n Such that ``index.rst`` becomes something like::\n\n Welcome to Coffee's documentation!\n ===================================\n\n .. toctree::\n :maxdepth: 2\n :caption: Contents:\n\n .. wurfapi:: class_synopsis.rst\n :selector: project::coffee::machine\n\n .. wurfapi:: class_synopsis.rst\n :selector: project::coffee::recipe\n\n\n Indices and tables\n ==================\n\n * :ref:`genindex`\n * :ref:`modindex`\n * :ref:`search`\n\n\n To do this we use the ``class_synopsis.rst`` template.\n\n6. Generate the Documentation\n\n make html\n\nRunning on readthedocs.org\n--------------------------\n\nTo use this on readthedocs.org you need to have the ``wurfapi`` Sphinx\nextension installed. This can be done by adding a ``requirements.txt`` in the\ndocumentation folder. readthedocs.org can be configured to use the\n``requirements.txt`` when building a project. Simply put ``wurfapi`` in to the\n``requirements.txt``.\n\nDoxygen issues\n--------------\n\nNothing is perfect, neither is Doxygen. Sometimes Doxygen gets it wrong e.g. in\nthe following example::\n\n class foo\n {\n private:\n class bar;\n };\n\nDoxygen incorrectly reports that ``bar`` has public scope (also reported here\nhttps://bit.ly/2BWPllZ). To deal with such issues, until a fix lands in\nDoxygen, you can do the following:\n\nAdd a list of *patches* to the API to your ``conf.py`` file. Extending the\nexample from before, we can add the following fix::\n\n wurfapi = {\n 'source_paths': ['../src'],\n 'recursive': True,\n 'parser': {\n 'type': 'doxygen', 'download': True, 'warnings_as_error': True,\n 'patch_api': [\n {'selector': 'foo::bar', 'key': 'access', 'value': 'private'}\n ]\n }\n }\n\nThe ``patch_api`` allows you to reach in to the parsed API information and\nupdate certain values. The ``selector`` is the ``unique-name`` of the\nentity you want to update. Check the \"Dictionary layout\" section further down\nfor more information.\n\nCollapse inline namespaces\n--------------------------\n\nFor symbol versioning you may use ``inline namespaces``, however typically\nyou don't want these to show up in the docs, as these are mostly\ninvisible for your users.\n\nWith ``wurfapi`` you can collapse the inline namespace such that it\nis removed form the scopes etc.\n\nExample::\n\n namespace foo { inline namespace v1_2_3 { struct bar{}; } }\n\nThe scope to bar is ``foo::v1_2_3``. If you collapse the inline namespace it will\njust be ``foo``.\n\nFirst issue you have to deal with is that Doxygen currently does not\nsupport inline namespaces. So we need to patch the API first::\n\n wurfapi = {\n 'source_paths': ['../src'],\n 'recursive': True,\n 'parser': {\n 'type': 'doxygen', 'download': True, 'warnings_as_error': True,\n 'patch_api': [\n {'selector': 'foo::v1_2_3', 'key': 'inline', 'value': True}\n ]\n }\n }\n\nAfter this we can collapse the namespace::\n\n wurfapi = {\n 'source_paths': ['../src'],\n 'recursive': True,\n 'parser': {\n 'type': 'doxygen', 'download': True, 'warnings_as_error': True,\n 'patch_api': [\n {'selector': 'foo::v1_2_3', 'key': 'inline', 'value': True}\n ],\n 'collapse_inline_namespaces': [\n \"foo::v1_2_3\"\n ]\n }\n }\n\n\nNow you will be able to refer to ``bar`` as ``foo::bar``. Note, that\ncollapsing the namespace will affect the selectors you write when\ngenerating the documentation.\n\nCustom templates\n----------------\n\nYou can write you own custom templates for generating the rst output.\nTo to this you simply write a Jinja2 compatible rst template and place\nit in some folder. Adding the ``user_templates`` key to the ``wurfapi``\nconfiguration dictionary in the ``conf.py`` file will make it available.\n\nFor example::\n\n wurfapi = {\n 'source_paths': ['../src', '../examples/header/header.h'],\n 'recursive': True,\n 'user_templates': 'rst_templates',\n 'parser': {\n 'type': 'doxygen', 'download': True, 'warnings_as_error': True\n }\n }\n\n exclude_patterns = ['rst_templates/*.rst']\n\nNow we can use ``*.rst`` files inside the ``rst_templates`` folder e.g. if\nwe had a ``class_list.rst`` template we could use it like this::\n\n .. wurfapi:: class_list.rst\n :selector: project::coffee\n\nRelease new version\n===================\n\n1. Edit ``NEWS.rst``, ``wscript`` and ``src/wurfapi/wurfapi.py`` (set\n correct ``VERSION``)\n\n2. Run ::\n\n ./waf upload\n\n\nSource code\n===========\n\n\nTests\n=====\n\nThe tests will run automatically by passing ``--run_tests`` to waf::\n\n ./waf --run_tests\n\nThis follows what seems to be \"best practice\" advise, namely to install the\npackage in editable mode in a virtualenv.\n\nRecordings\n----------\n\nA bunch of the tests use a class called ``Record``, defined in\n(``test/record.py``). The ``Record`` class is used to store output as\nfiles from different parsing and rendering operations.\n\nE.g. say we want to make sure that a parser function returns a certain\n``dict`` object. Then we can record that ``dict``::\n\n recorder = record.Record(filename='test.json',\n recording_path='/tmp/recording',\n mismatch_path='/tmp/mismatch')\n\n recorder.record(data={'foo': 2, 'bar': 3})\n\nIf ``data`` changes compared to a previous recording a mismatch will be\ndetected. To update a recording simply delete the recording file.\n\nTest directories\n----------------\n\nYou will also notice that a bunch of the tests take a parameter called\n``testdirectory``. The ``testdirectory`` is a pytest fixture, which\nrepresents a temporary directory on the filesystem. When running the tests\nyou will notice these temporary test directories pop up under the\n``pytest_temp`` directory in the project root.\n\nYou can read more about that here:\n\n* https://github.com/steinwurf/pytest-testdirectory\n\nDeveloper Notes\n===============\n\nThe `sphinx` documentation on creating extensions:\nhttp://www.sphinx-doc.org/en/stable/extdev/index.html#dev-extensions\n\n* An extension is a Python module. When an extension loads, Sphinx will import\n it and execute its ``setup()`` function.\n\n* Understanding how to put together docutils nodes seems pretty difficult. One\n suggesting form the mailinglist was to look at the following document:\n https://github.com/docutils-mirror/docutils/blob/master/test/functional/expected/standalone_rst_pseudoxml.txt\n\n* While researching who to do this, there seem to be three potential approaches:\n\n 1. Use the standard Sphinx approach and operate with the doctree.\n 2. Create RST based on jinja templates\n 3. Create HTML based on jinja templates\n\n* Inspiration - Sphinx extensions that were used as inspiration while\n developing this extension.\n\n * Breathe\n * Gasp\n * https://github.com/Robpol86/sphinxcontrib-imgur\n * https://github.com/djungelorm/sphinx-tabs\n\n* Understanding how to write stuff with docutils:\n * http://agateau.com/2015/docutils-snippets/\n\n* Creating custom directive\n * http://www.xavierdupre.fr/blog/2015-06-07_nojs.html\n\n* Nice looking Sphinx extensions\n * https://github.com/bokeh/bokeh/tree/master/bokeh/sphinxext\n\n* This part of the documentation was useful in order to understand the need\n for ViewLists etc. in the directives run(...) function.\n http://www.sphinx-doc.org/en/stable/extdev/markupapi.html\n\n* This link provided inspiration for the text json format: https://github.com/micnews/html-to-article-json\n* More xml->json for the text: https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html\n\nDictionary layout\n-----------------\n\nWe want to support different \"backends\" like Doxygen to parse the source\ncode. To make this possible we define an internal source code description\nformat. We then translate e.g. Doxygen XML to this and use that to render\nthe API documentation.\n\nThis way a different \"backend\" e.g. Doxygen2 could be use used as the source\ncode parser and the API documentation could be generated.\n\n\n``unique-name``\n...............\n\nIn order to be able to reference the different entities in the API we need\nto assign them a name.\n\nWe use a similar approach here as described in standardese_.\n\nThis means that the ``unique-name`` of an entity is the name with all\nscopes e.g. ``foo::bar::baz``.\n\n* For functions you need to specify the signature (parameter types and for\n member functions cv-qualifier and ref-qualifier) e.g. ``foo::bar::baz::func()``\n or ``foo::bar::baz::func(int a, char*) const``. See cppreference_ for more\n information.\n\n* For class template specilizations the unique name includes the specilization\n arguments. For example::\n\n // Here the unique-name is just 'foo'\n template\n class foo {};\n\n // Here the unique name is foo\n template<>\n class foo {};\n\n.. _cppreference: http://en.cppreference.com/w/cpp/language/member_functions\n.. _standardese: https://github.com/foonathan/standardese#linking\n\n\n\nThe API dictionary\n...................\n\nThe internal structure is a dicts with the different API entities. The\n``unique-name`` of the entity is the key and the entity type also a\nPython dictionary is the value e.g::\n\n\n api = {\n 'unique-name': { ... },\n 'unique-name': { ... },\n ...\n }\n\nTo make this a bit more concrete consider the following code::\n\n namespace ns1\n {\n class shape\n {\n void print(int a) const;\n };\n\n namespace ns2\n {\n struct box\n {\n void hello();\n };\n\n void print();\n }\n }\n\nParsing the above code would produce the following API dictionary::\n\n api = {\n 'ns1': { 'kind': 'namespace', ...},\n 'ns1::shape': { 'kind': 'class', ... },\n 'ns1::shape::print(int) const': { kind': function' ... },\n 'ns1::ns2': { 'kind': 'namespace', ... },\n 'ns1::ns2::box': { 'kind': 'struct', ... },\n 'ns1::ns2::box::hello()': { kind': function' ... },\n 'ns1::ns2::print()': { 'kind': 'function', ...}\n }\n\nThe different entity kinds expose different information about the\nAPI. We will document the different kinds in the following.\n\nWe make some keys *optional* this is marked in the following way::\n\n api = {\n 'unique-name': {\n 'some_key': ...\n Optional('an_optional_key'): ...\n },\n ...\n }\n\n``namespace`` Kind\n..................\n\nPython dictionary representing a C++ namespace::\n\n info = {\n 'kind': 'namespace',\n 'name': 'unqualified-name',\n 'scope': 'unique-name' | None,\n 'members: [ 'unique-name', 'unique-name' ],\n 'briefdescription': paragraphs,\n 'detaileddescription': paragraphs,\n 'inline': True | False\n }\n\nNote: Currently Doxygen does not support parsing ``inline namespaces``. So\nyou need to use the patch API to change the value from ``False`` to ``True``\nmanually. Maybe at some point https://github.com/doxygen/doxygen/issues/6741\nit will be supported.\n\n``class`` | ``struct`` Kind\n...........................\n\nPython dictionary representing a C++ class or struct::\n\n info = {\n 'kind': 'class' | 'struct',\n 'name': 'unqualified-name',\n 'location': location,\n 'scope': 'unique-name' | None,\n 'access': 'public' | 'protected' | 'private',\n Optional('template_parameters'): template_parameters,\n 'members: [ 'unique-name', 'unique-name' ],\n 'briefdescription': paragraphs,\n 'detaileddescription': paragraphs\n }\n\n\n``enum`` | ``enum class`` Kind\n..............................\n\nPython dictionary representing a C++ enum or enum class::\n\n info = {\n 'kind': 'enum',\n 'name': 'unqualified-name',\n 'location': location,\n 'scope': 'unique-name' | None,\n 'access': 'public' | 'protected' | 'private',\n 'values: [\n {\n 'name': 'somename',\n 'briefdescription': paragraphs,\n 'detaileddescription': paragraphs,\n Optional('value'): 'some value'\n }\n ],\n 'briefdescription': paragraphs,\n 'detaileddescription': paragraphs\n }\n\n``typedef`` | ``using`` Kind\n............................\n\nPython dictionary representing a C++ using or typedef statement::\n\n info = {\n 'kind': 'typedef' | 'using',\n 'name': 'unqualified-name',\n 'location': location,\n 'scope': 'unique-name' | None,\n 'access': 'public' | 'protected' | 'private',\n 'type': type,\n 'briefdescription': paragraphs,\n 'detaileddescription': paragraphs\n }\n\n``function`` Kind\n.................\n\nPython dictionary representing a C++ function::\n\n info = {\n 'kind': 'function',\n 'name': 'unqualified-name',\n 'location': location,\n 'scope': 'unique-name' | None,\n Optional('return'): {\n 'type': type,\n 'description': paragraphs\n }\n 'signature': 'text',\n Optional('template_parameters'): template_parameters,\n 'is_const': True | False,\n 'is_static': True | False,\n 'is_virtual': True | False,\n 'is_explicit': True | False,\n 'is_inline': True | False,\n 'is_constructor': True | False,\n 'is_destructor': True | False,\n 'access': 'public' | 'protected' | 'private',\n 'briefdescription: paragraphs,\n 'detaileddescription: paragraphs,\n 'parameters': [\n { 'type': type, Optional('name'): 'somename', 'description': paragraphs },\n ...\n ]\n }\n\nThe `return` key is optional if the function is either a constructor or\ndestructor.\n\n``variable`` Kind\n.................\n\nPython dictionary representing a C++ variable::\n\n info = {\n 'kind': 'variable',\n 'name': 'unqualified-name',\n Optional('value'): 'some value',\n 'type': type,\n 'location': location,\n 'is_static': True | False,\n 'is_mutable': True | False,\n 'is_volatile': True | False,\n 'is_const': True | False,\n 'is_constexpr': True | False,\n 'scope': 'unique-name' | None,\n 'access': 'public' | 'protected' | 'private',\n 'briefdescription: paragraphs,\n 'detaileddescription: paragraphs,\n }\n\n``location`` item\n.................\n\nPython dictionary representing a location::\n\n location = {\n Optional('include'): 'some/header.h',\n 'path': 'src/project/header.h',\n 'line-start': 10,\n 'line-end': 12 | None\n }\n\n* The ``include`` will be relative to any ``include_paths`` specified in the\n ``wurfapi`` dictionary in your Sphinx ``conf.py``.\n\n* The ``path`` will be relative to the project root folder.\n\n``type`` item\n.............\n\nPython list representing a C++ type::\n\n type = [\n {\n 'value': 'sometext',\n Optional('link'): link\n }, ...\n ]\n\nHaving the type as a list of items we can create links to nested types e.g.\nsay we have a `std::unique_ptr` and we would like to make `impl` a link.\nThis could look like::\n\n \"type\": [\n {\n \"value\": \"std::unique_ptr<\"\n },\n {\n \"link\": {\"url\": False, \"value\": \"project::impl\"},\n \"value\": \"impl\"\n },\n {\n \"value\": \">\"\n }\n ]\n\n``link`` item\n.............\n\nPython dictionary representing a link::\n\n link = { 'url': True | False, 'value': 'somestring' }\n\nIf `url` is `True` we have a basic extrenal reference otherwise we have a\nlink to an internal type in the API.\n\n``template_parameters`` item\n.............................\n\nPython list of dictionaries representing template parameters::\n\n template_parameters = [{\n 'type': type,\n 'name': 'somestring',\n Optional('default'): type,\n Optional('description'): paragraphs\n }]\n\nText information\n.................\n\nText information is stored in a list of paragraphs::\n\n paragraphs = [\n {\n \"kind\": \"text\" | \"code\" | \"list\",\n ...\n },\n ...\n ]\n\n text = {\n 'kind': 'text',\n 'content': 'hello',\n Optional('link'): link\n }\n\n code = {\n 'kind': 'code',\n 'content': 'void print();',\n 'is_block': true | false\n }\n\n list = {\n 'kind': 'list',\n 'ordered': true | false,\n 'items': paragraphs # Each item is a list of paragraphs\n }\n\n\n\nProblem with ``unique-name`` for functions\n..........................................\n\nIssue equivalent C++ function signatures can be written in a number of\ndifferent ways::\n\n void hello(const int *x); // x is a pointer to const int\n void hello(int const *x); // x is a pointer to const int\n\nWe can also move the asterix (``*``) to the left::\n\n void hello(const int* x); // x is a pointer to const int\n void hello(int const* x); // x is a pointer to const int\n\nSo we need some way to normalize the function signature when transforming it\nto ``unique-name``. We cannot simply rely on sting comparisons.\n\nAccording to the numerous google searches it is hard to write a regex for this.\nInstead we will try to use a parser:\n\n* Python parser: https://github.com/erezsh/lark\n* C++ Grammar: http://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiers\n\nWe only need to parse the function parameter list denoted as the\n``http://www.externsoft.ch/media/swf/cpp11-iso.html#parameters_and_qualifiers``.\n\n\nGenerated output\n----------------\n\nSince we are going to be using Doxygen's XML output as input to the\nextension we need a place to store it. We store it system temporary folder e.g.\nif the project name is \"foobar\" on Linux this would be\n``/tmp/wurfapi-foobar-123456`` where ``123456`` is a hash of the source\ndirectory paths.\n\nThe API in json format can be found in the ``_build/.doctree/wurfapi_api.json``.\n\nPaths and directories\n---------------------\n\n\n* Source directory: In Sphinx the source directory is where our .rst files are\n located. This is what you pass to ``sphinx-build`` when building your\n documentation. We will use this in our extension to find the C++ source code\n and output customization templates.\n\n\nNotes\n=====\n\n* Why use an ``src`` folder (https://hynek.me/articles/testing-packaging/).\n tl;dr you should run your tests in the same environment as your users would\n run your code. So by placing the source files in a non-importable folder you\n avoid accidentally having access to resources not added to the Python\n package your users will install...\n* Python packaging guide: https://packaging.python.org/distributing/\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/steinwurf/", "keywords": "wurfapi", "license": "BSD 3-clause \"New\" or \"Revised\" License", "maintainer": "", "maintainer_email": "", "name": "wurfapi", "package_url": "https://pypi.org/project/wurfapi/", "platform": "", "project_url": "https://pypi.org/project/wurfapi/", "project_urls": { "Homepage": "https://github.com/steinwurf/" }, "release_url": "https://pypi.org/project/wurfapi/3.0.0/", "requires_dist": [ "pyquery", "python-archive", "schema", "six", "pathlib2 ; python_version < \"3.4\"" ], "requires_python": "", "summary": "C++ Documentation generator.", "version": "3.0.0" }, "last_serial": 5569118, "releases": { "1.0.0": [ { "comment_text": "", "digests": { "md5": "5c7327991e5d4b39d43c18109177c818", "sha256": "c3335fe0464b77b2d6efe8401af09c992610add2ac813d3a3c4f42aa00314959" }, "downloads": -1, "filename": "wurfapi-1.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "5c7327991e5d4b39d43c18109177c818", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 20966, "upload_time": "2018-05-16T21:07:21", "url": "https://files.pythonhosted.org/packages/a9/a5/e7008bfb185fa4ef2c79aa05921c0466ab293ebd88efe8c93b700ab328c2/wurfapi-1.0.0-py2.py3-none-any.whl" } ], "2.1.0": [ { "comment_text": "", "digests": { "md5": "63e03c21f5d3f727cd6188a2a6c06734", "sha256": "98073ddf34645f33e1ecc60b53c62998d10f1c04f0af606cde3af4d247c9da25" }, "downloads": -1, "filename": "wurfapi-2.1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "63e03c21f5d3f727cd6188a2a6c06734", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 26086, "upload_time": "2018-07-30T12:30:52", "url": "https://files.pythonhosted.org/packages/91/34/c34b5a07c119166ba63c42fbfece22ccd7f370f76ba1dccefe08ff377c6e/wurfapi-2.1.0-py2.py3-none-any.whl" } ], "2.2.0": [ { "comment_text": "", "digests": { "md5": "021434ca8290c46d6624be0fc43d27cb", "sha256": "455102aa2751b90838ea5d38b71f9428be4b9366aa6a6b125c4c2a334748d6e4" }, "downloads": -1, "filename": "wurfapi-2.2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "021434ca8290c46d6624be0fc43d27cb", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 26201, "upload_time": "2018-08-01T13:20:17", "url": "https://files.pythonhosted.org/packages/61/4c/27c93d2fae22c13e765e8feb767dfbabf24cc71c1f744bc2115ab65409d2/wurfapi-2.2.0-py2.py3-none-any.whl" } ], "3.0.0": [ { "comment_text": "", "digests": { "md5": "394d53d8ed21ad2c9eb0ec9bcaf3733b", "sha256": "80150f309e0bfe999e1194ef679723202214cc69acabb0b1e616064f100ac07e" }, "downloads": -1, "filename": "wurfapi-3.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "394d53d8ed21ad2c9eb0ec9bcaf3733b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 41525, "upload_time": "2019-07-22T21:06:11", "url": "https://files.pythonhosted.org/packages/dd/31/e4f51e6903f2e36ca3fc86724a997c17f8f3314064edd1bea410c0f32c5f/wurfapi-3.0.0-py2.py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "394d53d8ed21ad2c9eb0ec9bcaf3733b", "sha256": "80150f309e0bfe999e1194ef679723202214cc69acabb0b1e616064f100ac07e" }, "downloads": -1, "filename": "wurfapi-3.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "394d53d8ed21ad2c9eb0ec9bcaf3733b", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 41525, "upload_time": "2019-07-22T21:06:11", "url": "https://files.pythonhosted.org/packages/dd/31/e4f51e6903f2e36ca3fc86724a997c17f8f3314064edd1bea410c0f32c5f/wurfapi-3.0.0-py2.py3-none-any.whl" } ] }