{ "info": { "author": "Zope Corporation and Contributors", "author_email": "zope-dev@zope.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Zope :: 3", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP" ], "description": "==================\n zope.generations\n==================\n\n.. image:: https://img.shields.io/pypi/v/zope.generations.svg\n :target: https://pypi.org/project/zope.generations/\n :alt: Latest Version\n\n.. image:: https://img.shields.io/pypi/pyversions/zope.generations.svg\n :target: https://pypi.org/project/zope.generations/\n :alt: Supported Python versions\n\n.. image:: https://travis-ci.org/zopefoundation/zope.generations.svg?branch=master\n :target: https://travis-ci.org/zopefoundation/zope.generations\n :alt: Build Status\n\n.. image:: https://coveralls.io/repos/github/zopefoundation/zope.generations/badge.svg\n :target: https://coveralls.io/github/zopefoundation/zope.generations\n :alt: Code Coverage\n\nGenerations are a way of updating objects in the database when the application\nschema changes. An application schema is essentially the structure of data,\nthe structure of classes in the case of ZODB or the table descriptions in the\ncase of a relational database.\n\n\n.. contents::\n\n======================\nDetailed Documentation\n======================\n\n\nGenerations are a way of updating objects in the database when the application\nschema changes. An application schema is essentially the structure of data,\nthe structure of classes in the case of ZODB or the table descriptions in the\ncase of a relational database.\n\nWhen you change your application's data structures, for example,\nyou change the semantic meaning of an existing field in a class, you will\nhave a problem with databases that were created before your change. For a\nmore thorough discussion and possible solutions, see\nhttp://wiki.zope.org/zope3/DatabaseGenerations\n\nWe will be using the component architecture, and we will need a database and a\nconnection:\n\n >>> try:\n ... from html import escape\n ... except ImportError:\n ... from cgi import escape\n >>> from pprint import pprint\n >>> from zope.interface import implementer\n >>> import transaction\n >>> from ZODB.MappingStorage import DB\n >>> db = DB()\n >>> conn = db.open()\n >>> tx = transaction.begin()\n >>> root = conn.root()\n\nImagine that our application is an oracle: you can teach it to react to\nphrases. Let's keep it simple and store the data in a dict:\n\n >>> root['answers'] = {'Hello': 'Hi & how do you do?',\n ... 'Meaning of life?': '42',\n ... 'four < ?': 'four < five'}\n >>> import transaction\n >>> tx.commit()\n\n\n===============\n Initial setup\n===============\n\nHere's some generations-specific code. We will create and register a\nSchemaManager. SchemaManagers are responsible for the actual updates of the\ndatabase. This one will be just a dummy. The point here is to make the\ngenerations module aware that our application supports generations.\n\nThe default implementation of SchemaManager is not suitable for this test\nbecause it uses Python modules to manage generations. For now, it\nwill be just fine, since we don't want it to do anything just yet.\n\n >>> from zope.generations.interfaces import ISchemaManager\n >>> from zope.generations.generations import SchemaManager\n >>> import zope.component\n >>> dummy_manager = SchemaManager(minimum_generation=0, generation=0)\n >>> zope.component.provideUtility(\n ... dummy_manager, ISchemaManager, name='some.app')\n\n'some.app' is a unique identifier. You should use a URI or the dotted name\nof your package.\n\nWhen you start Zope and a database is opened, an event\nIDatabaseOpenedWithRoot is sent. Zope registers\nevolveMinimumSubscriber by default as a handler for this event. Let's\nsimulate this:\n\n >>> class DatabaseOpenedEventStub(object):\n ... def __init__(self, database):\n ... self.database = database\n >>> event = DatabaseOpenedEventStub(db)\n\n >>> from zope.generations.generations import evolveMinimumSubscriber\n >>> evolveMinimumSubscriber(event)\n\nThe consequence of this action is that now the database contains the fact\nthat our current schema number is 0. When we update the schema, Zope3 will\nhave an idea of what the starting point was. Here, see?\n\n >>> from zope.generations.generations import generations_key\n >>> tx = transaction.begin()\n >>> root[generations_key]['some.app']\n 0\n >>> tx.abort()\n\nIn real life you should never have to bother with this key directly,\nbut you should be aware that it exists.\n\n\n==================\n Upgrade scenario\n==================\n\nBack to the story. Some time passes and one of our clients gets hacked because\nwe forgot to escape HTML special characters! The horror! We must fix this\nproblem ASAP without losing any data. We decide to use generations to impress\nour peers.\n\nLet's update the schema manager (drop the old one and install a new custom\none):\n\n >>> from zope.component import globalregistry\n >>> gsm = globalregistry.getGlobalSiteManager()\n >>> gsm.unregisterUtility(provided=ISchemaManager, name='some.app')\n True\n\n >>> @implementer(ISchemaManager)\n ... class MySchemaManager(object):\n ...\n ... minimum_generation = 1\n ... generation = 2\n ...\n ... def evolve(self, context, generation):\n ... root = context.connection.root()\n ... answers = root['answers']\n ... if generation == 1:\n ... for question, answer in list(answers.items()):\n ... answers[question] = escape(answer)\n ... elif generation == 2:\n ... for question, answer in list(answers.items()):\n ... del answers[question]\n ... answers[escape(question)] = answer\n ... else:\n ... raise ValueError(\"Bummer\")\n ... root['answers'] = answers # ping persistence\n\n >>> manager = MySchemaManager()\n >>> zope.component.provideUtility(manager, ISchemaManager, name='some.app')\n\nWe have set `minimum_generation` to 1. That means that our application\nwill refuse to run with a database older than generation 1. The `generation`\nattribute is set to 2, which means that the latest generation that this\nSchemaManager knows about is 2.\n\nevolve() is the workhorse here. Its job is to get the database from\n`generation`-1 to `generation`. It gets a context which has the attribute\n'connection', which is a connection to the ZODB. You can use that to change\nobjects like in this example.\n\nIn this particular implementation generation 1 escapes the answers (say,\ncritical, because they can be entered by anyone!), generation 2 escapes the\nquestions (say, less important, because these can be entered by authorized\npersonell only).\n\nIn fact, you don't really need a custom implementation of ISchemaManager. One\nis available, we have used it for a dummy previously. It uses Python modules\nfor organization of evolver functions. See its docstring for more information.\n\nIn real life you will have much more complex object structures than the one\nhere. To make your life easier, there are two very useful functions available\nin zope.generations.utility: findObjectsMatching() and\nfindObjectsProviding(). They will dig through containers recursively to help\nyou seek out old objects that you wish to update, by interface or by some other\ncriteria. They are easy to understand, check their docstrings.\n\n\n=======================\n Generations in action\n=======================\n\nSo, our furious client downloads our latest code and restarts Zope. The event\nis automatically sent again:\n\n >>> event = DatabaseOpenedEventStub(db)\n >>> evolveMinimumSubscriber(event)\n\nShazam! The client is happy again!\n\n >>> tx = transaction.begin()\n >>> pprint(root['answers'])\n {'Hello': 'Hi & how do you do?',\n 'Meaning of life?': '42',\n 'four < ?': 'four < five'}\n\nBecause evolveMinimumSubscriber is very lazy, it only updates the database just\nenough so that your application can use it (to the `minimum_generation`, that\nis). Indeed, the marker indicates that the database generation has been bumped\nto 1:\n\n >>> root[generations_key]['some.app']\n 1\n >>> tx.abort()\n\nWe see that generations are working, so we decide to take the next step\nand evolve to generation 2. Let's see how this can be done manually:\n\n >>> from zope.generations.generations import evolve\n >>> evolve(db)\n\n >>> tx = transaction.begin()\n >>> pprint(root['answers'])\n {'Hello': 'Hi & how do you do?',\n 'Meaning of life?': '42',\n 'four < ?': 'four < five'}\n >>> root[generations_key]['some.app']\n 2\n >>> tx.abort()\n\nDefault behaviour of `evolve` upgrades to the latest generation provided by\nthe SchemaManager. You can use the `how` argument to evolve() when you want\njust to check if you need to update or if you want to be lazy like the\nsubscriber which we have called previously.\n\n\nOrdering of schema managers\n===========================\n\nFrequently subsystems used to compose an application rely on other\nsubsystems to operate properly. If both subsystems provide schema\nmanagers, it is often helpful to know the order in which the evolvers\nwill be invoked. This allows a framework and it's clients to be able\nto evolve in concert, and the clients can know that the framework will\nbe evolved before or after itself.\n\nThis can be accomplished by controlling the names of the schema\nmanager utilities. The schema managers are run in the order\ndetermined by sorting their names.\n\n >>> manager1 = SchemaManager(minimum_generation=0, generation=0)\n >>> manager2 = SchemaManager(minimum_generation=0, generation=0)\n\n >>> zope.component.provideUtility(\n ... manager1, ISchemaManager, name='another.app')\n >>> zope.component.provideUtility(\n ... manager2, ISchemaManager, name='another.app-extension')\n\nNotice how the name of the first package is used to create a namespace\nfor dependent packages. This is not a requirement of the framework,\nbut a convenient pattern for this usage.\n\nLet's evolve the database to establish these generations:\n\n >>> event = DatabaseOpenedEventStub(db)\n >>> evolveMinimumSubscriber(event)\n\n >>> root[generations_key]['another.app']\n 0\n >>> root[generations_key]['another.app-extension']\n 0\n\nLet's assume that for some reason each of these subsystems needs to\nadd a generation, and that generation 1 of 'another.app-extension'\ndepends on generation 1 of 'another.app'. We'll need to provide\nschema managers for each that record that they've been run so we can\nverify the result:\n\n >>> gsm.unregisterUtility(provided=ISchemaManager, name='another.app')\n True\n >>> gsm.unregisterUtility(\n ... provided=ISchemaManager, name='another.app-extension')\n True\n\n >>> @implementer(ISchemaManager)\n ... class FoundationSchemaManager(object):\n ...\n ... minimum_generation = 1\n ... generation = 1\n ...\n ... def evolve(self, context, generation):\n ... root = context.connection.root()\n ... ordering = root.get('ordering', [])\n ... if generation == 1:\n ... ordering.append('foundation 1')\n ... print('foundation generation 1')\n ... else:\n ... raise ValueError(\"Bummer\")\n ... root['ordering'] = ordering # ping persistence\n\n >>> @implementer(ISchemaManager)\n ... class DependentSchemaManager(object):\n ...\n ... minimum_generation = 1\n ... generation = 1\n ...\n ... def evolve(self, context, generation):\n ... root = context.connection.root()\n ... ordering = root.get('ordering', [])\n ... if generation == 1:\n ... ordering.append('dependent 1')\n ... print('dependent generation 1')\n ... else:\n ... raise ValueError(\"Bummer\")\n ... root['ordering'] = ordering # ping persistence\n\n >>> manager1 = FoundationSchemaManager()\n >>> manager2 = DependentSchemaManager()\n\n >>> zope.component.provideUtility(\n ... manager1, ISchemaManager, name='another.app')\n >>> zope.component.provideUtility(\n ... manager2, ISchemaManager, name='another.app-extension')\n\nEvolving the database now will always run the 'another.app' evolver\nbefore the 'another.app-extension' evolver:\n\n >>> event = DatabaseOpenedEventStub(db)\n >>> evolveMinimumSubscriber(event)\n foundation generation 1\n dependent generation 1\n\n >>> root['ordering']\n ['foundation 1', 'dependent 1']\n\n\n==============\n Installation\n==============\n\nIn the the example above, we manually initialized the answers. We\nshouldn't have to do that manually. The application should be able to\ndo that automatically.\n\nIInstallableSchemaManager extends ISchemaManager, providing an install\nmethod for performing an intial installation of an application. This\nis a better alternative than registering database-opened subscribers.\n\nLet's define a new schema manager that includes installation:\n\n\n >>> gsm.unregisterUtility(provided=ISchemaManager, name='some.app')\n True\n >>> from zope.generations.interfaces import IInstallableSchemaManager\n >>> @implementer(IInstallableSchemaManager)\n ... class MySchemaManager(object):\n ...\n ... minimum_generation = 1\n ... generation = 2\n ...\n ... def install(self, context):\n ... root = context.connection.root()\n ... root['answers'] = {'Hello': 'Hi & how do you do?',\n ... 'Meaning of life?': '42',\n ... 'four < ?': 'four < five'}\n ...\n ... def evolve(self, context, generation):\n ... root = context.connection.root()\n ... answers = root['answers']\n ... if generation == 1:\n ... for question, answer in answers.items():\n ... answers[question] = escape(answer)\n ... elif generation == 2:\n ... for question, answer in answers.items():\n ... del answers[question]\n ... answers[escape(question)] = answer\n ... else:\n ... raise ValueError(\"Bummer\")\n ... root['answers'] = answers # ping persistence\n\n >>> manager = MySchemaManager()\n >>> zope.component.provideUtility(manager, ISchemaManager, name='some.app')\n\nNow, lets open a new database:\n\n >>> conn.close()\n >>> db.close()\n\n >>> db = DB()\n >>> tx = transaction.begin()\n >>> conn = db.open()\n >>> 'answers' in conn.root()\n False\n >>> tx.abort()\n\n >>> event = DatabaseOpenedEventStub(db)\n >>> evolveMinimumSubscriber(event)\n\n >>> tx = transaction.begin()\n >>> root = conn.root()\n\n >>> pprint(root['answers'])\n {'Hello': 'Hi & how do you do?',\n 'Meaning of life?': '42',\n 'four < ?': 'four < five'}\n >>> root[generations_key]['some.app']\n 2\n >>> tx.abort()\n >>> conn.close()\n\nThe ZODB transaction log notes that our install script was executed\n\n >>> [it.description for it in db.storage.iterator()][-1]\n u'some.app: running install generation'\n >>> db.close()\n\n\n=========\n CHANGES\n=========\n\n5.0.0 (2019-09-23)\n==================\n\n- Add support for transaction managers operating in explicit mode.\n Schema managers were previously required not to commit transactions\n in their ``evolve`` or ``install`` methods, but a loophole was open\n to allow them to commit \"if there were no subsequent operations\".\n That loophole is now closed, at least in explicit mode. See `issue 8\n `_.\n\n\n4.1.0 (2018-10-23)\n==================\n\n- Add support for Python 3.7.\n\n\n4.0.0 (2017-07-20)\n==================\n\n- Dropped support for Python 2.6 and 3.3.\n\n- Added support for Python 3.4, 3.5, 3.6, PyPy2 and PyPy3.\n\n\n4.0.0a1 (2013-02-25)\n====================\n\n- Added support for Python 3.3.\n\n- Replaced deprecated ``zope.interface.implements`` usage with equivalent\n ``zope.interface.implementer`` decorator.\n\n- Dropped support for Python 2.4 and 2.5.\n\n\n3.7.1 (2011-12-22)\n==================\n\n- Removed buildout part which was used during development but does not\n compile on Windows.\n\n- Generation scripts add a transaction note.\n\n\n3.7.0 (2010-09-18)\n==================\n\n- Initial release extracted from ``zope.app.generations``.\n\n- Generations key (stored in database root) has been changed from\n ``zope.app.generations`` to ``zope.generations``. Migration is done when\n ``evolve`` is run the first time by coping the exisiting generations data\n over to the new key. So the old and the new key can be used in parallel.\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/zopefoundation/zope.generations", "keywords": "zope zodb schema generation", "license": "ZPL 2.1", "maintainer": "", "maintainer_email": "", "name": "zope.generations", "package_url": "https://pypi.org/project/zope.generations/", "platform": "", "project_url": "https://pypi.org/project/zope.generations/", "project_urls": { "Homepage": "https://github.com/zopefoundation/zope.generations" }, "release_url": "https://pypi.org/project/zope.generations/5.0.0/", "requires_dist": [ "setuptools", "transaction", "zope.component", "zope.interface", "zope.processlifetime", "ZODB ; extra == 'test'", "zope.site ; extra == 'test'", "zope.testing ; extra == 'test'", "zope.testrunner ; extra == 'test'" ], "requires_python": "", "summary": "Zope application schema generations", "version": "5.0.0" }, "last_serial": 5873017, "releases": { "3.7.0": [ { "comment_text": "", "digests": { "md5": "49375cd1a9429690abeb1b5bbe56ed93", "sha256": "f3ce00fb5a795013e182f0020f707e3ec406e5a3ffbe10adf16315d6c3a09789" }, "downloads": -1, "filename": "zope.generations-3.7.0.tar.gz", "has_sig": false, "md5_digest": "49375cd1a9429690abeb1b5bbe56ed93", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16284, "upload_time": "2010-09-18T13:31:30", "url": "https://files.pythonhosted.org/packages/eb/92/7a0459a67aafda4895b043c6534325e51fe72fb99b9c644834ffcc6b2560/zope.generations-3.7.0.tar.gz" } ], "3.7.1": [ { "comment_text": "", "digests": { "md5": "8871f9f475e4aea09903fdf472918f0a", "sha256": "3012fe5c2a0b340605b16aef78337fb182b4d00d8db4c69cbf368217cfd82a21" }, "downloads": -1, "filename": "zope.generations-3.7.1.tar.gz", "has_sig": false, "md5_digest": "8871f9f475e4aea09903fdf472918f0a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23581, "upload_time": "2011-12-22T20:48:36", "url": "https://files.pythonhosted.org/packages/3d/79/8de1820391dd5773b7b86a0cf2894cbe6a71f8d5f25cbfecce6c7b68cdc4/zope.generations-3.7.1.tar.gz" } ], "4.0": [ { "comment_text": "", "digests": { "md5": "314f7c9b713c7c98244cbe82e53f0451", "sha256": "896959a52f8df1228fb32090827b52d20c3f1f3e6194a8f8f724d9e7b6faf8cd" }, "downloads": -1, "filename": "zope.generations-4.0.tar.gz", "has_sig": false, "md5_digest": "314f7c9b713c7c98244cbe82e53f0451", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26347, "upload_time": "2017-07-20T14:44:03", "url": "https://files.pythonhosted.org/packages/b6/94/00f2cb4648825501a0c687360497fc9c6a10abb8468957546ff8e26680c3/zope.generations-4.0.tar.gz" } ], "4.0.0a1": [ { "comment_text": "", "digests": { "md5": "85e3a8600d35aff7ec947b489e7d8df5", "sha256": "bca0f493f2289f2f1aa1603783b5d77a9a9b582d96e2d28a68eeaa320f666360" }, "downloads": -1, "filename": "zope.generations-4.0.0a1.zip", "has_sig": false, "md5_digest": "85e3a8600d35aff7ec947b489e7d8df5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40667, "upload_time": "2013-02-26T01:19:27", "url": "https://files.pythonhosted.org/packages/46/d2/5fe901ba55dccb47ba80688569a701d8e27aa205eb75b66c9bfae7887ea1/zope.generations-4.0.0a1.zip" } ], "4.1.0": [ { "comment_text": "", "digests": { "md5": "38b0d449627c1db0216ccb580562c2ab", "sha256": "a3c2a4083ed5c4a2d4b091620c5c45a3f892bead8d93c54d20041eba37267948" }, "downloads": -1, "filename": "zope.generations-4.1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "38b0d449627c1db0216ccb580562c2ab", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 28587, "upload_time": "2018-10-23T11:47:46", "url": "https://files.pythonhosted.org/packages/cb/7c/3d2ae518de6a22fb0e80daa01a6eb82ef2b17a010caa28b0cb41b23ced04/zope.generations-4.1.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "08b62741243187542fa57fdd15729f08", "sha256": "5d120b65c3081cb6bbad2ca95dab63e898cd39ac6362e1713bb95ac392b1f29a" }, "downloads": -1, "filename": "zope.generations-4.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "08b62741243187542fa57fdd15729f08", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 28584, "upload_time": "2018-10-23T11:46:21", "url": "https://files.pythonhosted.org/packages/d9/44/daa8301c5c89e6fe12054074d2278440a61dccd20221174c97432cdfa6bc/zope.generations-4.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0f334ffa9c94d3f9377ca9d699e0c048", "sha256": "84daf82811e1080d7f48337350266a4cc5f48869f402f2612be89f8230fa1413" }, "downloads": -1, "filename": "zope.generations-4.1.0.tar.gz", "has_sig": false, "md5_digest": "0f334ffa9c94d3f9377ca9d699e0c048", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25767, "upload_time": "2018-10-23T11:46:23", "url": "https://files.pythonhosted.org/packages/31/2c/761e8f755ecd85291b6ca152ba473e667efbef9b05002e13d9ad63564bcb/zope.generations-4.1.0.tar.gz" } ], "5.0.0": [ { "comment_text": "", "digests": { "md5": "d395a0ab664a015f7038c799ee41c36c", "sha256": "5cea98a2ba22f178cc2bb73904cdd94519daa9f30ad391a9a819abfaf0064b35" }, "downloads": -1, "filename": "zope.generations-5.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "d395a0ab664a015f7038c799ee41c36c", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 30324, "upload_time": "2019-09-23T11:18:43", "url": "https://files.pythonhosted.org/packages/fd/45/73f431bd80c23176fea5524f5b46534585e194b85c6751627fea6be77856/zope.generations-5.0.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "903d9c64a54b77772103d212f7d67a5d", "sha256": "daad94d19d5f4a07ffabc34323aa35706b519ecb9d64c0906194d831c044162d" }, "downloads": -1, "filename": "zope.generations-5.0.0.tar.gz", "has_sig": false, "md5_digest": "903d9c64a54b77772103d212f7d67a5d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27800, "upload_time": "2019-09-23T11:18:45", "url": "https://files.pythonhosted.org/packages/00/e0/8a477c4dab6be4c06b3eaf8df2149faa9cff16944d30750a5080ae51ce0e/zope.generations-5.0.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "d395a0ab664a015f7038c799ee41c36c", "sha256": "5cea98a2ba22f178cc2bb73904cdd94519daa9f30ad391a9a819abfaf0064b35" }, "downloads": -1, "filename": "zope.generations-5.0.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "d395a0ab664a015f7038c799ee41c36c", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 30324, "upload_time": "2019-09-23T11:18:43", "url": "https://files.pythonhosted.org/packages/fd/45/73f431bd80c23176fea5524f5b46534585e194b85c6751627fea6be77856/zope.generations-5.0.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "903d9c64a54b77772103d212f7d67a5d", "sha256": "daad94d19d5f4a07ffabc34323aa35706b519ecb9d64c0906194d831c044162d" }, "downloads": -1, "filename": "zope.generations-5.0.0.tar.gz", "has_sig": false, "md5_digest": "903d9c64a54b77772103d212f7d67a5d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27800, "upload_time": "2019-09-23T11:18:45", "url": "https://files.pythonhosted.org/packages/00/e0/8a477c4dab6be4c06b3eaf8df2149faa9cff16944d30750a5080ae51ce0e/zope.generations-5.0.0.tar.gz" } ] }