{ "info": { "author": "Martijn Faassen", "author_email": "faassen@startifact.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Zope3", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP" ], "description": "Version Control Synchronization\n===============================\n\nThis package contains code that helps with handling synchronization of\npersistent content with a version control system. \n\nThis can be useful in software that needs to be able to work\noffline. The web application runs on a user's laptop that may be away\nfrom an internet connection. When connected again, the user syncs with\na version control server, receiving updates that may have been made by\nothers, and committing their own changes.\n\nAnother advantage is that the version control system always contains a\nhistory of how content developed over time. The version-control based\ncontent can also be used for other purposes independent of the\napplication.\n\nWhile this package has been written with other version control systems\nin mind, it has only been developed to work with SVN so far. Examples\nbelow are all given with SVN.\n\nThe synchronization sequence is as follows:\n \n1) save persistent state (IState) to svn checkout (ICheckout) on the\n same machine as the Zope application.\n\n2) ``svn up``. Subversion merges in changed made by others users that\n were checked into the svn server.\n\n3) Any svn conflicts are automatically resolved.\n\n4) reload changes in svn checkout into persistent Python objects\n\n5) ``svn commit``.\n\nThis is all happening in a single step. It can happen over and over\nagain in a reasonably safe manner, as after the synchronization has\nconcluded, the state of the persistent objects and that of the local\nSVN checkout will always be in sync.\n\nDuring synchronisation, the system tries to take care only to\nsynchronize those objects and files that have changed. That is, in\nstep 1) only applies those objects that have been modified, added or\nremoved will have an effect on the checkout. In step 4) only those\nfiles that have been changed, added or removed on the filesystem due\nto the ``up`` action will change the persistent object state.\n\nState\n-----\n \nContent to synchronize is represented by an object that provides\n``IState``. A state represents a container object, which should\ncontain a ``data`` object (a container that contains the actual data\nto be synchronized) and a ``found`` object (a container that contains\nobjects that would otherwise be lost during conflict resolution).\n\nThe following methods need to be implemented:\n\n* ``get_revision_nr()``: return the last revision number that the\n application was synchronized with. The state typically stores this\n the application object.\n\n* ``set_revision_nr(nr)``: store the last revision number that the\n application was synchronized with.\n\n* ``objects(revision_nr)``: any object that has been modified (or\n added) since the synchronization for ``revision_nr``. Returning 'too\n many' objects (objects that weren't modified) is safe, though less\n efficient as they will then be re-exported.\n\n Typically in your application this would be implemented by doing\n a catalog search, so that they can be looked up quickly.\n\n* ``removed(revision_nr)``: any path that has had an object removed\n from it since revision_nr. It is safe to return paths that have\n been removed and have since been replaced by a different object with\n the same name. It is also safe to return 'too many' paths, though\n less efficient as the objects in these paths may be re-exported\n unnecessarily.\n\n Typically in your application you would maintain a list of removed\n objects by hooking into ``IObjectMovedEvent`` and\n ``IObjectRemovedEvent`` and recording the paths of all objects that\n were moved or removed. After an export it is safe to purge this\n list.\n\nIn this example, we will use a simpler, less efficient, implementation\nthat goes through a content to find changes. It tracks the\nrevision number as a special attribute of the root object::\n\n >>> from z3c.vcsync.tests import TestState\n\nThe content\n-----------\n\nNow that we have something that can synchronize a tree of content in\ncontainers, let's actually build ourselves a tree of content.\n\nAn item contains some payload data, and maintains the SVN revision\nafter which it was changed. In a real application you would typically\nmaintain the revision number of objects by using an annotation and\nlistening to ``IObjectModifiedEvent``, but we will use a property\nhere::\n\n >>> from z3c.vcsync.tests import Item\n\nThis code needs a ``get_revision_nr`` method available to get access\nto the revision number of last synchronization. For now we'll just define\nthis to return 0, but we will change this later::\n\n >>> def get_revision_nr(self):\n ... return 0\n >>> Item.get_revision_nr = get_revision_nr\n\nBesides the ``Item`` class, we also have a ``Container`` class::\n\n >>> from z3c.vcsync.tests import Container\n\nIt is a class that implements enough of the dictionary API and\nimplements the ``IContainer`` interface. A normal Zope 3 folder or\nGrok container will also work. \n\nLet's create a container now::\n\n >>> root = Container()\n >>> root.__name__ = 'root'\n\nThe container has two subcontainers (``data`` and ``found``).\n\n >>> root['data'] = data = Container()\n >>> root['found'] = Container()\n >>> data['foo'] = Item(payload=1)\n >>> data['bar'] = Item(payload=2)\n >>> data['sub'] = Container()\n >>> data['sub']['qux'] = Item(payload=3)\n\nAs part of the synchronization procedure we need the ability to export\npersistent python objects to the version control checkout directory in\nthe form of files and directories.\n\nNow that we have an implementation of ``IState`` that works for our\nstate, let's create our ``state`` object::\n\n >>> state = TestState(root)\n\nReading from and writing to the filesystem\n------------------------------------------\n\nTo integrate with the synchronization machinery, we need a way to dump\na Python object to the filesystem (to an SVN working copy), and to\nparse it back to an object again.\n\nLet's grok this package first, as it provides some of the required\ninfrastructure::\n\n >>> import grokcore.component as grok\n >>> grok.testing.grok('z3c.vcsync')\n \nWe need to provide a serializer for the Item class that takes an item\nand writes it to the filesystem to a file with a particular extension\n(``.test``)::\n\n >>> from z3c.vcsync.tests import ItemSerializer\n\nWe also need to provide a parser to load an object from the filesystem\nback into Python, overwriting the previously existing object::\n\n >>> from z3c.vcsync.tests import ItemParser\n\nSometimes there is no previously existing object in the Python tree,\nand we need to add it. To do this we implement a factory (where we use\nthe parser for the real work)::\n\n >>> from z3c.vcsync.tests import ItemFactory\n\nBoth parser and factory are registered per extension, in this case\n``.test``. This is the name of the utility.\n\nWe register these components::\n\n >>> grok.testing.grok_component('ItemSerializer', ItemSerializer)\n True\n >>> grok.testing.grok_component('ItemParser', ItemParser)\n True\n >>> grok.testing.grok_component('ItemFactory', ItemFactory)\n True\n\nWe also need a parser and factory for containers, registered for the\nempty extension (thus no special utility name). These can be very\nsimple::\n\n >>> from z3c.vcsync.tests import ContainerParser, ContainerFactory\n >>> grok.testing.grok_component('ContainerParser', ContainerParser)\n True\n >>> grok.testing.grok_component('ContainerFactory', ContainerFactory)\n True\n\nSetting up the SVN repository\n-----------------------------\n\nNow we need an SVN repository to synchronize with. We create a test\nSVN repository now and create a svn path to a checkout::\n\n >>> from z3c.vcsync.tests import svn_repo_wc\n >>> repo, wc = svn_repo_wc()\n\nWe can now initialize the ``SvnCheckout`` object with the SVN path to\nthe checkout we just created::\n\n >>> from z3c.vcsync.svn import SvnCheckout\n >>> checkout = SvnCheckout(wc)\n\nThe root directory of the working copy will be synchronized with the\nroot container of the state. The checkout will therefore contain\n``data`` and a ``found`` sub-directories.\n\nConstructing the synchronizer\n-----------------------------\n\nNow that we have the checkout and the state, we can set up a synchronizer::\n\n >>> from z3c.vcsync import Synchronizer\n >>> s = Synchronizer(checkout, state)\n\nLet's make ``s`` the current synchronizer as well. We need this in\nthis example to get back to the last revision number::\n\n >>> current_synchronizer = s\n\nIt's now time to set up our ``get_revision_nr`` method a bit better,\nmaking use of the information in the current synchronizer. In actual\napplications we'd probably get the revision number directly from the\ncontent, and there would be no need to get back to the synchronizer\n(it doesn't need to be persistent but can be constructed on demand)::\n\n >>> def get_revision_nr(self):\n ... return current_synchronizer.state.get_revision_nr()\n >>> Item.get_revision_nr = get_revision_nr\n\nSynchronization\n---------------\n\nWe'll synchronize for the first time now::\n\n >>> info = s.sync(\"synchronize\")\n\nWe will now examine the SVN checkout to see whether the\nsynchronization was successful.\n\nTo do this we'll introduce some helper functions that help us present\nthe paths in a more readable form, relative to the base of the\ncheckout::\n\n >>> def pretty_path(path):\n ... return path.relto(wc)\n >>> def pretty_paths(paths):\n ... return sorted([pretty_path(path) for path in paths])\n\nWe see that the Python object structure of containers and items has\nbeen translated to the same structure of directories and ``.test``\nfiles on the filesystem::\n\n >>> pretty_paths(wc.listdir())\n ['data']\n >>> pretty_paths(wc.join('data').listdir())\n ['data/bar.test', 'data/foo.test', 'data/sub']\n >>> pretty_paths(wc.join('data').join('sub').listdir())\n ['data/sub/qux.test']\n\nThe ``.test`` files have the payload data we expect::\n \n >>> print wc.join('data').join('foo.test').read()\n 1\n >>> print wc.join('data').join('bar.test').read()\n 2\n >>> print wc.join('data').join('sub').join('qux.test').read()\n 3\n\nSynchronization back into objects\n---------------------------------\n\nLet's now try the reverse: we will change the SVN content from another\ncheckout, and synchronize the changes back into the object tree.\n\nWe have a second, empty tree that we will load objects into::\n\n >>> root2 = Container()\n >>> root2.__name__ = 'root'\n >>> state2 = TestState(root2)\n\nWe make another checkout of the repository::\n\n >>> import py\n >>> wc2 = py.test.ensuretemp('wc2')\n >>> wc2 = py.path.svnwc(wc2)\n >>> wc2.checkout(repo)\n >>> checkout2 = SvnCheckout(wc2)\n\nLet's make a synchronizer for this new checkout and state::\n\n >>> s2 = Synchronizer(checkout2, state2)\n\nThis is now the current synchronizer (so that our ``get_revision_nr``\nworks properly)::\n\n >>> current_synchronizer = s2\n\nNow we'll synchronize::\n\n >>> info = s2.sync(\"synchronize\")\n\nThe state of objects in the tree must now mirror that of the original state::\n\n >>> sorted(root2.keys()) \n ['data']\n\n >>> sorted(root2['data'].keys())\n ['bar', 'foo', 'sub']\n\nNow we will change some of these objects, and synchronize again::\n\n >>> root2['data']['bar'].payload = 20\n >>> root2['data']['sub']['qux'].payload = 30\n >>> info2 = s2.sync(\"synchronize\")\n\nWe can now synchronize the original tree again::\n\n >>> current_synchronizer = s\n >>> info = s.sync(\"synchronize\")\n\nWe should see the changes reflected into the original tree::\n\n >>> root2['data']['bar'].payload\n 20\n >>> root2['data']['sub']['qux'].payload\n 30\n\nMore information\n----------------\n\nTo learn more about the APIs you can use and need to implement, see\n``interfaces.py``.\n\nTo learn about using ``z3c.vcsync`` to import and export content, see\n``importexport.txt``.\n\nMore low-level information may be gleaned from ``conflicts.txt`` and\n``internal.txt``.\n\n\nz3c.vcsync changes\n==================\n\n0.17 (2009-06-05)\n-----------------\n\n* Depend only on ``grokcore.component`` and ``zope.app.container``\n directly, not on ``grok`` itself. We want instead to depend only \n on ``zope.container``, but we cannot do this yet as we need\n compatibility with Grok 0.14.\n\n* Use a somewhat less verbose way to set up the tests.\n\n* An issue existed when a common filesystem representation was used\n that could be translated (by a single factory) into a number of\n different classes. If an object was added, synchronized, then\n removed and an object of the name but different class was added, an\n error would occur when synchronizing this. This bug has been fixed\n at the (small) cost of a few-reparses here and there.\n\n0.16 (2009-06-02)\n-----------------\n\n* Change the method by which zip files are created for export to a\n less filesystem-intensive method; files are directly added to the\n zip file. This hopefully brings performance benefits on platforms\n where accessing many small files is slow.\n\n0.15 (2008-08-19)\n-----------------\n\n* Fix a bug where the SVN \"R\" status was not recognized by the py lib.\n Monkey-patch py for now, though a fix should be released with py\n 0.9.2 eventually. Strictly depend on py 0.9.1 for now to make sure\n monkey-patch applies cleanly.\n\n* A bit of refactoring of duplicated code in retrieving the objects\n modified and objects removed lists. Still not happy that this gets\n called twice per synchronization, but it was already doing this so\n doesn't get worse either.\n\n0.14 (2008-07-04)\n-----------------\n\n* Fixed a bug where too many path fragments were returned in case of\n conflicts. Now only paths that have in fact changed should be\n returned. Added some tests for this.\n\n* There was a case where two users would add a file with the same name\n to their own states independently. This used to result in an SVN\n error, but now also generates a conflict. This conflict is a bit\n different in its behavior unfortunately, as it prefers the version\n already in SVN as opposed to the one last added.\n\n0.13 (2008-06-02)\n-----------------\n\n* The root directory of the checkout is now truly equivalent to the\n root object of the state. This means that if the SVN checkout\n content is to remain the same, the state root to synchronize should\n be one level higher (the parent of the current state root).\n\n* Conflict resolution has been cleaned up. When a conflict occurs, the\n other half of the conflict (the one not resolved) is moved into the\n ``found`` directory, which is created in the checkout root. This is\n also represented in the state container object and is synchronized\n like any other content.\n\n0.12 (2008-05-16)\n-----------------\n\nFeatures added\n~~~~~~~~~~~~~~\n\n* The API has been cleaned up and revised. This will break code that\n uses this library, but so far I don't think that is many people\n yet. :)\n\n* A major refactoring of the tests, including real SVN tests. This\n requires SVN to be installed on the system where the tests are being\n run, including the ``svn-admin`` command.\n\n* ``IState`` objects now need to implement methods to access and\n maintain the revision number of the last revision.\n\n* The developer must now also implement ``IParser`` utilities for\n files that can be synchronized (besides ``IFactory``, which used to\n be called ``IVcFactory``. The ``IParser`` utility overwrites the\n existing object instead of creating a new one. This allows\n synchronization to be a bit nicer and not remove and recreate\n objects unnecessarily, which makes it harder to implement things\n like references between objects.\n\n* Add a facility to pass a special function along that is called for\n all objects created or modified during the synchronization (or\n during import). This function is called at the end when all objects\n that are going to exist already exist, so can be used in situations\n where the state of an object relies on the existence of another one.\n\n0.11 (2008-03-10)\n-----------------\n\nBugs fixed\n~~~~~~~~~~\n\n* Do not try to remove non-existent files during synchronization. A\n file might have been removed in SVN and there is no more need to\n re-remove it if it was also removed locally.\n\n* There was an off-by one error during the \"up\" phase of\n synchronization with SVN, and as a result a log entry that was\n already processed could be re-processed during this next\n synchronisation. This could in some cases revive folders as unknown\n directories on the filesystem, leading to errors and\n inconsistencies.\n\n0.10 (2008-01-08)\n-----------------\n\nFeatures added\n~~~~~~~~~~~~~~\n\n* The ``.sync()`` method now does not return the revision number, but\n an ``ISynchronizationInfo`` object. This has a ``revision_nr``\n attribute and also contains some information on what happened during\n the synchronization process.\n\nBugs fixed\n~~~~~~~~~~\n\n* revision number after synchronization was not always updated\n properly to the latest number of the repository. Now retrieve this\n number from ``commit()`` where possible.\n\n0.9.1 (2007-11-29)\n------------------\n\nBugs fixed\n~~~~~~~~~~\n\n* When resolving objects in the ZODB, a path was generated that has\n separators that are actually dependent on the operating system in\n use (``/`` for Unices, but ``\\`` for windows). This caused\n synchronization to fail on Windows, completely flattening\n hierarchies. Now use os.path.sep to be platform-independent.\n\n0.9 (2007-11-25)\n----------------\n\nFeatures added\n~~~~~~~~~~~~~~\n\n* The importing logic now allows the user to import new content over\n existing content. In this case any existing content is left alone,\n but new objects are added. Any attempt to overwrite existing content\n is ignored.\n\nBugs fixed\n~~~~~~~~~~\n\n* In some cases a containing directory is referenced which does not\n exist anymore when removing files. In this case we do not need to\n remove the file anymore, as the directory itself is gone.\n\n* SVN doesn't actually remove directories, just mark them for\n removal. This could confuse the system during synchronization:\n removed directories might reappear again as they were still on the\n filesystem during loading. Make sure now that any directories marked\n for removal are also properly removed in the filesystem before load\n starts, but after up (as rm-ing a directory marked for removal\n before svn up will actually re-add this directory!).\n\nRestructuring\n~~~~~~~~~~~~~\n\n* Previously the datetime of last synchronization was used to\n determine what to synchronize both in the ZODB as well as in the\n checkout. This has a significant drawback if the datetime setting of\n the computer the synchronization code is running on is ahead of the\n datetime setting of the version control server: updates could be\n lost. \n\n Changed the code to use a revision_nr instead. This is a number that\n increments with each synchronization, and the number can be used to\n determine both what changes have been made since last\n synchronization in the ZODB as well as in the version control\n system. This is a more robust approach.\n\n0.8.1 (2007-11-07)\n------------------\n\nBugs fixed\n~~~~~~~~~~\n\n* Fix a bug in conversion of SVN timestamps to datetimes. Previous\n code worked in DST, but not during winter time. The new code might\n of course break under DST - the mysterious of datetime conversion\n are legion.\n\n* A cleaner way to cache the files listing from SVN.\n\n* Work around a bug in the Py library. The Py library doesn't support\n the R status code from SVN and raises a NotImplementedError when it\n encounters it. Evilly catch these NotImplementedErrors for now. The\n bug has been reported upstream and should be fixed in the next\n release of Py.", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "UNKNOWN", "keywords": "zope3 grok web svn synchronization", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "z3c.vcsync", "package_url": "https://pypi.org/project/z3c.vcsync/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/z3c.vcsync/", "project_urls": { "Download": "UNKNOWN", "Homepage": "UNKNOWN" }, "release_url": "https://pypi.org/project/z3c.vcsync/0.17/", "requires_dist": null, "requires_python": null, "summary": "Synchronize object data with a version control system", "version": "0.17" }, "last_serial": 802122, "releases": { "0.12": [ { "comment_text": "", "digests": { "md5": "94f059332740ceb62e4a37ec007b24dd", "sha256": "6a05e6a1ba035533fcc7fc97ef2b9e7c485fc7fb9849a6781a037dba388413d4" }, "downloads": -1, "filename": "z3c.vcsync-0.12.tar.gz", "has_sig": false, "md5_digest": "94f059332740ceb62e4a37ec007b24dd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32528, "upload_time": "2008-05-16T20:10:39", "url": "https://files.pythonhosted.org/packages/0f/d4/c13bda1603bc8c22a77cd041d68eb3056604371800aa401d50a9c86cb1f4/z3c.vcsync-0.12.tar.gz" } ], "0.13": [ { "comment_text": "", "digests": { "md5": "8a53f39991b2afa2e72cc01cb5bd0d07", "sha256": "35bc93ea4c348484d61c232c1305a25160dcf898e0a22698b0aa060f5009bf40" }, "downloads": -1, "filename": "z3c.vcsync-0.13.tar.gz", "has_sig": false, "md5_digest": "8a53f39991b2afa2e72cc01cb5bd0d07", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33242, "upload_time": "2008-06-02T14:55:07", "url": "https://files.pythonhosted.org/packages/bf/8f/418362e8ea895209bf8698a1429e6f7fc29e7782a930f973149ad156eec1/z3c.vcsync-0.13.tar.gz" } ], "0.14": [ { "comment_text": "", "digests": { "md5": "7a875a7cca65423304b94625556f98bd", "sha256": "3263616938bdd1b8d626163173b5a84b4495be789c0205e34bd7e533c410c248" }, "downloads": -1, "filename": "z3c.vcsync-0.14.tar.gz", "has_sig": false, "md5_digest": "7a875a7cca65423304b94625556f98bd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34783, "upload_time": "2008-07-04T18:39:10", "url": "https://files.pythonhosted.org/packages/f8/7b/23ba4d064a73a2a5bd6b496d14481663069e6da792af738a8a22e81a83b3/z3c.vcsync-0.14.tar.gz" } ], "0.15": [ { "comment_text": "", "digests": { "md5": "a51c3f37f83d2f5d1bb1e3f1bd53de76", "sha256": "ef5621bd7f8d9e58d98591a7c9378049b7b993740350c93b07b01189e269dcb6" }, "downloads": -1, "filename": "z3c.vcsync-0.15.tar.gz", "has_sig": false, "md5_digest": "a51c3f37f83d2f5d1bb1e3f1bd53de76", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 37023, "upload_time": "2008-08-19T17:36:07", "url": "https://files.pythonhosted.org/packages/ad/63/c8e845626b5e4d3bc6ad3175cb2b8b8c597503513bbd2b06be12b866611e/z3c.vcsync-0.15.tar.gz" } ], "0.16": [ { "comment_text": "", "digests": { "md5": "ae257aeb3b83fa071a0f6eae8156c2d5", "sha256": "7c7da3d60f56f876ec4a06ef3509ece85c9757971030ee72b6da31f39b2a6510" }, "downloads": -1, "filename": "z3c.vcsync-0.16.tar.gz", "has_sig": false, "md5_digest": "ae257aeb3b83fa071a0f6eae8156c2d5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 37343, "upload_time": "2009-06-02T16:38:43", "url": "https://files.pythonhosted.org/packages/bc/7a/22a1ea66a336494df2a6252c3afb6000ce71884fe96230107f1870d27ea9/z3c.vcsync-0.16.tar.gz" } ], "0.17": [ { "comment_text": "", "digests": { "md5": "5bf8e71ec76d6cd97844c9127738f857", "sha256": "d2808ee40bc211a72be4992fbeceec96e40c93628d23d35ac84f4157e5b4a92d" }, "downloads": -1, "filename": "z3c.vcsync-0.17.tar.gz", "has_sig": false, "md5_digest": "5bf8e71ec76d6cd97844c9127738f857", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 41895, "upload_time": "2009-06-05T17:51:02", "url": "https://files.pythonhosted.org/packages/e7/4b/fe0263094d6eb634e9d105f08dc3779cbf4b6827bbe981260d493b60d923/z3c.vcsync-0.17.tar.gz" } ], "0.9": [ { "comment_text": "", "digests": { "md5": "8eb5c5d8de53f0f4f4cf9e9806a47770", "sha256": "b6dbd375690a4b9a7486911d4811d71008f689a1915b77753cdf50dba494138f" }, "downloads": -1, "filename": "z3c.vcsync-0.9.tar.gz", "has_sig": false, "md5_digest": "8eb5c5d8de53f0f4f4cf9e9806a47770", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14493, "upload_time": "2007-11-25T22:23:33", "url": "https://files.pythonhosted.org/packages/3a/21/2e3e22181b5465b575817a510befb3ba1a8d5209a249617371090645bf81/z3c.vcsync-0.9.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "5bf8e71ec76d6cd97844c9127738f857", "sha256": "d2808ee40bc211a72be4992fbeceec96e40c93628d23d35ac84f4157e5b4a92d" }, "downloads": -1, "filename": "z3c.vcsync-0.17.tar.gz", "has_sig": false, "md5_digest": "5bf8e71ec76d6cd97844c9127738f857", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 41895, "upload_time": "2009-06-05T17:51:02", "url": "https://files.pythonhosted.org/packages/e7/4b/fe0263094d6eb634e9d105f08dc3779cbf4b6827bbe981260d493b60d923/z3c.vcsync-0.17.tar.gz" } ] }