{ "info": { "author": "Nicholas Brochu", "author_email": "nicholas@serpent.ai", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6" ], "description": "offshoot\n========\n\nThis one day in the past, you took your first step on your programming\njourney. Some days were tough, some days were great. You made progress.\nYou made mistakes. You learned some best practices and design patterns.\nYou\u2019ve come to idolize low coupling and modularity. Eventually, you\nstarted working on more ambitious projects, always increasing in\ncomplexity; the possibilities endless. After implementing your 7th\nexport format for your latest project, the words of the great Raymond\nHettinger come to mind: *There\u2019s got to be a better way!* After a short\nstint going all out on inheritance and mixins, you turn your attention\nto plugins. You read up on them, get the general idea and start looking\nat what\u2019s available for Python. You are happy to find out there are a\nquite a few on the market. You start trying them out and for the most\npart, they work great, but it always feels like something is missing.\nPerhaps they make you go through crazy code gymnastics, lack features or\nare plain just horrible to look at. This is the moment you discover\n*offshoot*.\n\n***offshoot***:\n\n- Is a modern, elegant and minimalistic plugin system for Python 3.5+\n- Is unintrusive; Stays out of our way. No file copying, no symlinks,\n nada!\n- Provides a clear and simple plugin definition format.\n- Understands your flow: Provides installation callbacks, can maintain\n a configuration and/or a requirements file for your plugins and has\n an optional plugin validation system on install.\n- Can discover and import any plugin of any type anywhere in your code\n with a one-liner. No more complex plugin management.\n- Batteries included. Comes with an executable to install/uninstall\n plugins.\n- Is fully-tested and is under active development.\n- Does not aim to please the *PEP 8* gods and the purists. Some dark\n magic is used unapologetically.\n\nQuick Tour\n----------\n\n**Your Class you\u2019d like to make pluggable**\n\n.. code:: python\n\n class ExportFormat:\n def __init__(self):\n self.name = \"Export Format\"\n\n def export(self, data):\n raise NotImplementedError()\n\n @classmethod\n def is_an_export_format(cls):\n return True\n\n**Your Class made pluggable with *offshoot***\n\n.. code:: python\n\n import offshoot\n\n class ExportFormat(offshoot.Pluggable):\n def __init__(self):\n self.name = \"Export Format\"\n\n @offshoot.expected\n def export(self, data):\n raise NotImplementedError()\n\n @classmethod\n @offshoot.forbidden\n def is_an_export_format(cls):\n return True\n\nYes, that\u2019s it! More about those optional decorators later.\n\n**A sample *offshoot* plugin definition**\n\n.. code:: python\n\n import offshoot\n\n class YAMLExportFormatPlugin(offshoot.Plugin):\n name = \"YAMLExportFormatPlugin\"\n version = \"0.1.0\"\n\n libraries = [\"PyYAML\"]\n\n files = [\n {\"path\": \"export_formats/yaml.py\", \"pluggable\": \"ExportFormat\"}\n ]\n\n config = {\n \"export_options\": {\n \"width\": 80\n }\n }\n\n @classmethod\n def on_install(cls):\n print(\"\\n\\n%s was installed successfully!\" % cls.__name__)\n\n @classmethod\n def on_uninstall(cls):\n print(\"\\n\\n%s was uninstalled successfully!\" % cls.__name__)\n\n if __name__ == \"__main__\":\n offshoot.executable_hook(YAMLExportFormatPlugin)\n\n**A sample *offshoot* plugin file**\n\n.. code:: python\n\n import offshoot\n from export_format import ExportFormat\n\n import yaml\n\n class YAMLExportFormat(ExportFormat):\n def export(self, data):\n return yaml.dump(data)\n\n**Installing an *offshoot* plugin from the command line**\n\n``offshoot install YAMLExportFormatPlugin``\n\n**Automatic *offshoot* plugin discovery and importing**\n\n.. code:: python\n\n import offshoot\n offshoot.discover(\"ExportFormat\", globals())\n\n YAMLExportFormat # Now in scope!\n\n**Verifying if class name string maps to a discovered plugin class**\n\n.. code:: python\n\n import offshoot\n class_mapping = offshoot.discover(\"ExportFormat\") # We omit scope param to get the class mapping\n\n \"YAMLExportFormat\" in class_mapping # True\n\nRequirements\n------------\n\n- PyYAML (On the roadmap to make it optional so the project is 100%\n dependency-free!)\n\nInstallation\n------------\n\n``pip install offshoot``\n\nConfiguration\n-------------\n\nDefault Configuration Values\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. code:: python\n\n {\n \"modules\": [],\n \"file_paths\": {\n \"plugins\": \"plugins\",\n \"config\": \"config/config.plugins.yml\",\n \"libraries\": \"requirements.plugins.txt\"\n },\n \"allow\": {\n \"plugins\": True,\n \"files\": True,\n \"config\": True,\n \"libraries\": True,\n \"callbacks\": True\n },\n \"sandbox_configuration_keys\": True\n }\n\nInitializing offshoot\n~~~~~~~~~~~~~~~~~~~~~\n\nInitializing *offshoot* will save a YAML copy of the default\nconfiguration to *offshoot.yml* which you can then modify to suit your\nneeds. Just run the following in the command line: ``offshoot init``\n\nConfiguration Keys\n~~~~~~~~~~~~~~~~~~\n\n- **modules**: Perhaps the most important key to modify since nothing\n will happen without some valid module paths in there. *offshoot*\n needs to discover pluggable classes in the project at import time. It\n will explore the modules listed here to find classes that extend\n *offshoot.Pluggable*\n- **file_paths**: Directories and file paths to use when *offshoot*\n needs to hit the file system. *plugins* is where *offshoot* will look\n for plugin files. The defaults should suffice, but do make sure they\n exist.\n- **allow**: *offshoot* allows you to enable/disable certain part of\n the plugin installation. It is recommended to leave all values to\n True.\n- **sandbox_configuration_keys**: If you chose to let *offshoot* merge\n configuration keys during plugin installation, it can either merge\n them all at the root level (False) or sandbox them under the plugin\n name (True)\n\nUsage\n-----\n\n.. _initializing-offshoot-1:\n\nInitializing Offshoot\n~~~~~~~~~~~~~~~~~~~~~\n\nThe first thing you will want to do after installing *offshoot* is run\n``offshoot init`` in the command line at the root of your project. This\nwill create a configuration file named *offshoot.yml*. You can leave it\nbe for now but we will go back to it later.\n\nMaking Your Classes Pluggable\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo make a class pluggable with *offshoot* all that technically needs to\nbe done is extend it with *offshoot.Pluggable*\n\nSo you go from this:\n\n.. code:: python\n\n class Shape:\n pass\n\nTo this:\n\n.. code:: python\n\n import offshoot\n\n class Shape(offshoot.Pluggable):\n pass\n\nThen for every class you make pluggable, you append its module path to\n*offshoot.yml* under the modules key. This means that if you make\n``shape.py`` and ``shapes/rectangle.py`` pluggable, your modules value\nwill look like this ``modules: [\"shape\", \"shapes/rectangle\"]``\n\nMagic Validation\n^^^^^^^^^^^^^^^^\n\n*offshoot* comes with an optional validation system for your pluggable\nclasses. You can control which class, instance and static methods are\neither *expected*, *accepted* or *forbidden* in a plugin file. The way\nyou do this couldn\u2019t be any simpler: you wrap them with a decorator. It\nends up looking like the following:\n\n.. code:: python\n\n import offshoot\n\n class PluggableClass(offshoot.Pluggable):\n @offshoot.expected\n def expected_function(self):\n raise NotImplementedError()\n\n @classmethod\n @offshoot.accepted\n def accepted_function(cls):\n raise NotImplementedError()\n\n @staticmethod\n @offshoot.forbidden\n def forbidden_function():\n raise NotImplementedError()\n\nIf a plugin file is missing an *expected* method, or defining a\n*forbidden* method, it will be rejected and the installation will be\nstopped and reverted.\n\nThey are called magic decorator because under the hood, they do\nabsolutely nothing. They are however found using Python\u2019s abstract\nsyntax trees (*ast* in the stdlib) during plugin installation and\nvalidation can be performed.\n\nInstallation Callbacks\n^^^^^^^^^^^^^^^^^^^^^^\n\nIn addition to magic validators, you have the option to add callbacks\nthat will be executed for each file installed/uninstalled by a plugin.\n\nTo leverage these callbacks, simply add these functions to your\npluggable class:\n\n.. code:: python\n\n @classmethod\n def on_file_install(cls, **kwargs):\n pass\n\n @classmethod\n def on_file_uninstall(cls, **kwargs):\n pass\n\nContained in ``kwargs`` are the file path and the name of the pluggable\nclass.\n\nOne common application for these callbacks would be to seed some values\nin a database. If we stick the ExportFormat example, once you install a\nYAMLExportFormat plugin, you may want to add it to a *export_formats*\ntable along with the name of the class. That could then allow list the\navailable export format options in a more logical fashion. Similarly,\nyou\u2019d want that option to be cleaned up when you uninstall the plugin.\n\nAnatomy of an *offshoot* Plugin\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nExpected File Structure\n^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code:: sh\n\n PLUGINS_DIRECTORY (defined in offshoot.yml)\n \u251c\u2500\u2500 ShapesPlugin # Name of the plugin. Matches the plugin class name in plugin.py\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 files # Any file other than the plugin definition goes here\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 helpers.py # Supporting file. Not in plugin definition but can be accessed by plugin files.\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 shapes\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 rectangle.py # Variant of the Shape pluggable class. Included in plugin definition file\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u251c\u2500\u2500 star.py # Variant of the Shape pluggable class. Included in plugin definition file\n \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 triangle.py # Variant of the Shape pluggable class. Included in plugin definition file\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 plugin.py # Plugin definition file\n \u251c\u2500\u2500 __init__.py\n\nYou are free to structure your file hierarchy exactly the way you want\ninside of the *files* directory. You can also add as many supporting\nfiles as needed.\n\n``__init__.py`` files DO need to be peppered everywhere as we want our\nplugin structure to be accessible as a package.\n\nPlugin Definition File (plugin.py)\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe plugin definition file turns out to be a Python file with a class\nthat extends ``offshoot.Plugin``. The name of that class needs to be an\nexact match of the name of the directory containing the plugin.\n\nHere\u2019s what plugin definition file would look like for a plugin using\nthe file structure above. It is annotated to explain what the various\nsections do.\n\n.. code:: python\n\n import offshoot\n\n class ShapesPlugin(offshoot.Plugin): # We extend offshoot.Plugin\n name = \"ShapesPlugin\" # We define a name for the plugin. Matches the class name.\n version = \"0.1.0\" # We define a version number for the plugin.\n\n # A list of plugin dependencies to check for (by name) before installing the plugin.\n # Optional.\n plugins = [\n \"RequiredShapesPlugin\"\n ]\n\n # A list of required PyPI packages for the plugin.\n # Optional. These libraries will be merge to your offshoot requirements.txt during the installation. Set to None if you don't intend to use it.\n libraries = [\n \"requests\",\n \"requests-respectful==0.2.0\"\n ]\n\n # A list of file objects that target pluggable classes in the project.\n # Required. \"path\" is the file path relative to the plugin root. \"pluggable\" is the pluggable class' name.\n files = [\n {\"path\": \"shapes/rectangle.py\", \"pluggable\": \"Shape\"},\n {\"path\": \"shapes/triangle.py\", \"pluggable\": \"Shape\"},\n {\"path\": \"shapes/star.py\", \"pluggable\": \"Shape\"}\n ]\n\n # A Python dict containing configuration keys that can be referenced by your plugin files at runtime.\n # Optional. Any valid Python dict is accepted. Set to None if you don't intend to use it.\n config = {\n \"i_am_a\": {\n \"plugin\": True,\n \"human\": False\n },\n \"urls\": [\"http://serpent.ai\", \"https://github.com/SerpentAI/offshoot\"],\n \"count\": 42,\n }\n\n # Callbacks to be performed once per install\u00a0/ uninstall\n # Optional.\n @classmethod\n def on_install(cls):\n print(\"\\n\\n%s was installed successfully!\" % cls.__name__)\n\n @classmethod\n def on_uninstall(cls):\n print(\"\\n\\n%s was uninstalled successfully!\" % cls.__name__)\n\n # This hook always needs to be present in a plugin definition file.\n # It is used by the installation process. Pass it the class you just defined above.\n if __name__ == \"__main__\":\n offshoot.executable_hook(ShapesPlugin)\n\nPlugin Files Extending the Pluggable Classes\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nEach plugin file needs to define a class that extends one of the classes\nthat were previously made pluggable. If magic validation decorators were\nused when making the class pluggable, the plugin file needs to validate\nagainst that protocol successfully to be installed.\n\nHere is a sample plugin file, following our Shapes plugin theme:\n\n.. code:: python\n\n from shape import Shape\n\n class Rectangle(Shape):\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n\n self.name = \"Rectangle\"\n self.sides = 4\n self.is_polygon = True\n\n @property\n def shape_is_a_polygon(self):\n return \"A Rectangle is a polygon!\"\n\n def area(self):\n raise NotImplementedError()\n\n def draw(self):\n raise NotImplementedError()\n\nYou are free to go way beyond the pluggable class\u2019 protocol. You can\nrequire functions from supporting files bundled with the plugin and make\nuse of your required PyPI packages and/or configuration keys.\n\nThe *offshoot* Manifest\n~~~~~~~~~~~~~~~~~~~~~~~\n\nThe *offshoot* manifest is a critical file that gets created when you\nattempt to install a plugin for the first time. It contains the metadata\nof installed plugins and helps maintain the overall *offshoot* state.\nLook for *offshoot.manifest.json* if you want to take a peek under the\nhood. Be aware that editing or deleting this file will cause issues!\n\nThe *offshoot* Executable\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe executable is rather minimalistic at the moment but it used to\nperform two crucial operations: Installing and uninstalling plugins.\n\nInstalling Plugins\n^^^^^^^^^^^^^^^^^^\n\nThe first step is making sure the plugin has been copied/cloned into the\nplugin directory defined in *offshoot.yml*\n\nAfter that, simply run the following in the command line:\n\n``offshoot install PLUGIN_NAME``\n\n**What happens when a plugin is installed?**\n\n1. The *offshoot* configuration file is consulted to fetch the allow\n flags\n2. If *plugins* are allowed: Every plugin listed as a dependency in the\n plugin definition is verified to be installed before continuing.\n3. If *files* are allowed: Every plugin file in the plugin definition is\n validated against its pluggable class\u2019 protocol. If even one\n validation test fails, the installation fails and is reverted. File\n installation callbacks are executed.\n4. If *config* is allowed: The configuration keys contained in the\n plugin definition file are merged in the configuration file defined\n in *offshoot.yml*.\n5. If *libraries* are allowed: Libraries contained in the plugin\n definition file are merged in the libraries file defined in\n *offshoot.yml*.\n6. If *callbacks* are allowed: The *on_install* callback is executed.\n7. The plugin metadata is appended to the manifest.\n\nThe installation process will not automatically install libraries with\n*pip*. It is assumed the user will permorm the pip installation.\n\nUninstalling Plugins\n^^^^^^^^^^^^^^^^^^^^\n\n``offshoot uninstall PLUGIN_NAME``\n\nDiscovering & Importing Plugins\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nLast but not least, a simple way of getting installed plugins\u2019 classes\ninto scope has been provided.\n\nHere\u2019s how it\u2019s done:\n\n.. code:: python\n\n import offshoot\n offshoot.discover(\"Shape\", globals())\n\n # All installed plugin classes that extend the Shape pluggable class are now into scope!\n\nThis can be done literally anywhere in your application.\n\nTips & Tricks\n~~~~~~~~~~~~~\n\nListing installed plugins\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nA utility method is exposed allowing you to fetch a list of the\ncurrently installed plugins as per the manifest.\n\nSimply run:\n\n.. code:: python\n\n offshoot.installed_plugins()\n\nExample output:\n\n.. code:: python\n\n [\"ShapesPlugin - 0.1.0\"]\n\nMerging the *offshoot* configuration keys with your application configuration at runtime.\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nChances are you already have a YAML configuration file for your\napplication. In some situations, it may become desirable to merge that\nconfiguration dict with *offshoot*\\ \u2019s configuration dict.\n\nHere\u2019s a code snippet to achieve this:\n\n.. code:: python\n\n import yaml\n\n # Application Configuration\n with open(\"config/config.yml\", \"r\") as f:\n config = yaml.safe_load(f)\n\n # Offshoot Configuration\n import offshoot\n\n with open(offshoot.config[\"file_paths\"][\"config\"], \"r\") as f:\n plugin_config = yaml.safe_load(f)\n\n # Merge Configuration. Application Configuration takes priority in the key space.\n config = {**plugin_config, **config}\n\nYou can then import *config* from this file to have the merged\nconfigurations.\n\nTests\n-----\n\nUnit tests for the project can be run with the following command:\n\n``python -m pytest tests --spec``\n\nYou can install the test requirements by refering to\n*requirements.test.txt* in the repository.\n\nExamples\n--------\n\nYou can find full examples in the ``examples`` directory of the\nrepository.\n\nRoadmap / Contribution Ideas\n----------------------------\n\n- Make PyYaml optional. Use it if it\u2019s there, otherwise default on JSON\n or INI\n- Explore supporting the extension of 3rd-party modules.\n- Windows support? Python 2 branch?\n- Clean up tests. A lot of repetition.\n- \u2026 Anything else that makes sense really!\n\n*If you like offshoot, feel free to check out\n`requests-respectful `__,\nalso by `SerpentAI `__*\n", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/SerpentAI/offshoot", "keywords": "", "license": "Apache License v2", "maintainer": "", "maintainer_email": "", "name": "offshoot", "package_url": "https://pypi.org/project/offshoot/", "platform": "", "project_url": "https://pypi.org/project/offshoot/", "project_urls": { "Homepage": "https://github.com/SerpentAI/offshoot" }, "release_url": "https://pypi.org/project/offshoot/0.1.6/", "requires_dist": null, "requires_python": "", "summary": "Modern, elegant, minimalistic but powerful plugin system for Python 3.5+.", "version": "0.1.6" }, "last_serial": 3549249, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "e29c68b284ae050dbe8b69f26388d13f", "sha256": "af9fdbe60a683edea23089336d6149e49d298859f74c96ff7e503b959f1ec8a8" }, "downloads": -1, "filename": "offshoot-0.1.0.tar.gz", "has_sig": false, "md5_digest": "e29c68b284ae050dbe8b69f26388d13f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12062, "upload_time": "2017-01-23T22:24:51", "url": "https://files.pythonhosted.org/packages/6b/c9/406cf8a23fae824461a2e72ec3d67317cdd8f0e5ad2eb3f20ccf4754aeb8/offshoot-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "05e28cfb421586e336a7e7d03a3fc089", "sha256": "b1e27703e78a32b937675466fcd2ccc3e35ab238bc85e37c432aedd2996f9296" }, "downloads": -1, "filename": "offshoot-0.1.1.tar.gz", "has_sig": false, "md5_digest": "05e28cfb421586e336a7e7d03a3fc089", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12126, "upload_time": "2017-03-04T23:09:48", "url": "https://files.pythonhosted.org/packages/f0/c6/6c9b4334d374d1288eb198d3c0e71f3fc85c58b91b1ad1aaaa0928a10722/offshoot-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "42fd3f18bbea8081bc5b3776979261a1", "sha256": "039ea386eeffda48fbe0639f30a3fef279b4b2cdc3b714390ef92fa053580f2f" }, "downloads": -1, "filename": "offshoot-0.1.2.tar.gz", "has_sig": false, "md5_digest": "42fd3f18bbea8081bc5b3776979261a1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12175, "upload_time": "2017-08-29T17:27:28", "url": "https://files.pythonhosted.org/packages/65/0d/42c2d8585bde414a7af3467abb4f869305d4b855e955095964ba92e333a6/offshoot-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "b92587f0db394c53e53566335de2379e", "sha256": "10462ed5c056518e3f054e43f8f21f35e44c077236d86f6b3bec22fc04866041" }, "downloads": -1, "filename": "offshoot-0.1.3.tar.gz", "has_sig": false, "md5_digest": "b92587f0db394c53e53566335de2379e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12425, "upload_time": "2017-09-03T20:48:38", "url": "https://files.pythonhosted.org/packages/9f/a0/56e20f2a6fdb6ea0377e8c49b6a17b194d71dedb36e9101564595397452f/offshoot-0.1.3.tar.gz" } ], "0.1.4": [ { "comment_text": "", "digests": { "md5": "91b887e857cc4a05dd251cb34c1ac512", "sha256": "261c7b718850dc5bf5e125bcdce8265631c9206da42909662034f31b1105897e" }, "downloads": -1, "filename": "offshoot-0.1.4.tar.gz", "has_sig": false, "md5_digest": "91b887e857cc4a05dd251cb34c1ac512", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12426, "upload_time": "2017-09-03T21:10:50", "url": "https://files.pythonhosted.org/packages/d7/11/81525079759ba72adb463988060b94e184177e3d95471f96532493b82783/offshoot-0.1.4.tar.gz" } ], "0.1.5": [ { "comment_text": "", "digests": { "md5": "4e0aebc67818c74f1b39e93bee059ca0", "sha256": "ef8a05b266cbdcec7096619a5838050819b4db69cdec4d205c88050192d400cd" }, "downloads": -1, "filename": "offshoot-0.1.5.tar.gz", "has_sig": false, "md5_digest": "4e0aebc67818c74f1b39e93bee059ca0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12526, "upload_time": "2018-02-03T21:38:11", "url": "https://files.pythonhosted.org/packages/90/17/cbe14771cdb8536a0e3a706c74c5c1f6ce2fadd7ecc6e48f6a035acec506/offshoot-0.1.5.tar.gz" } ], "0.1.6": [ { "comment_text": "", "digests": { "md5": "86e6bd6d14423cfcdd8e440ea99c0e3b", "sha256": "01c693ad4982c8618a29f7b7f438f6150ccad366ecc7b04abc9c72a58158b716" }, "downloads": -1, "filename": "offshoot-0.1.6.tar.gz", "has_sig": false, "md5_digest": "86e6bd6d14423cfcdd8e440ea99c0e3b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12523, "upload_time": "2018-02-03T22:12:25", "url": "https://files.pythonhosted.org/packages/62/53/9da62933a2fecf377eef0d90ab7502964d91d3daf39946e4c6427dd3fc80/offshoot-0.1.6.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "86e6bd6d14423cfcdd8e440ea99c0e3b", "sha256": "01c693ad4982c8618a29f7b7f438f6150ccad366ecc7b04abc9c72a58158b716" }, "downloads": -1, "filename": "offshoot-0.1.6.tar.gz", "has_sig": false, "md5_digest": "86e6bd6d14423cfcdd8e440ea99c0e3b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12523, "upload_time": "2018-02-03T22:12:25", "url": "https://files.pythonhosted.org/packages/62/53/9da62933a2fecf377eef0d90ab7502964d91d3daf39946e4c6427dd3fc80/offshoot-0.1.6.tar.gz" } ] }