{ "info": { "author": "Zope Corporation and Contributors", "author_email": "zope3-dev@zope.org", "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": "===================\nzope.fssync Package\n===================\n\nThis package provides filesystem synchronization utilities for Zope\n3. It is used by the zope.app.fssync package.\n\n\n==========================\nFilesystem Synchronization\n==========================\n\nThis package provides an API for the synchronization of Python objects\nwith a serialized filesystem representation. This API does not address\nsecurity issues. (See zope.app.fssync for a protected web-based API).\nThis API is Zope and ZODB independent.\n\nThe main use cases are\n\n - data export / import (e.g. moving data from one place to another)\n\n - content management (e.g. managing a wiki or other collections of\n documents offline)\n\nThe target representation depends on your use case. In the use case of\ndata export/import, for instance, it is crucial that all data are\nexported as completely as possible. Since the data need not be read\nby humans in most circumstances a pickle format may be the most\ncomplete and easy one to use.\nIn the use case of content management it may be more important that\nall metadata are readable by humans. In this case another format,\ne.g. RDFa, may be more appropriate.\n\nMain components\n===============\n\nA synchronizer serializes content objects and stores the serialized\ndata in a repository in an application specific format. It uses\ndeserializers to read the object back into the content space.\nThe serialization format must be rich enough to preserve various forms\nof references which should be reestablished on deserialization.\n\nAll these components should be replaceable. Application may use\ndifferent serialization formats with different references for\ndifferent purposes (e.g. backup vs. content management) and different\ntarget systems (e.g. a zip archive vs. a svn repository).\n\nThe main components are:\n\n - ISyncTasks like Checkout, Check, and Commit which synchronize\n a content space with a repository. These tasks uses serializers\n to produce serialized data for a repository in an application\n specific format. They use deserializers to read the data back.\n The default implementation uses xmlpickle for python objects,\n data streams for file contents, and special directories for\n extras and metadata. Alternative implementations may use\n standard pickles, a human readable format like RDFa, or\n application specific formats.\n\n - ISynchronizer: Synchronizers produce serialized pieces of a\n Python object (the ISerializer part of a synchronizer) and\n consume serialized data to (re-)create Python objects (the\n IDeserializer part of a synchronizer).\n\n - IPickler: An adapter that determines the pickle format.\n\n - IRepository: represents a target system that can be used\n to read and write serialized data.\n\n\nLet's take some samples:\n\n >>> from StringIO import StringIO\n >>> from zope import interface\n >>> from zope import component\n >>> from zope.fssync import interfaces\n >>> from zope.fssync import task\n >>> from zope.fssync import synchronizer\n >>> from zope.fssync import repository\n >>> from zope.fssync import pickle\n\n >>> class A(object):\n ... data = 'data of a'\n >>> class B(A):\n ... pass\n >>> a = A()\n >>> b = B()\n >>> b.data = 'data of b'\n >>> b.extra = 'extra of b'\n >>> root = dict(a=a, b=b)\n\n\nPersistent References\n=====================\n\nMany applications use more than one system of persistent references.\nZope, for instance, uses p_oids, int ids, key references,\ntraversal paths, dotted names, named utilities, etc.\n\nOther systems might use generic reference systems like global unique\nids or primary keys together with domain specific references, like\nemails, URI, postal addresses, code numbers, etc.\nAll these references are candidates for exportable references as long\nas they can be resolved on import or reimport.\n\nIn our example we use simple integer ids:\n\n >>> class GlobalIds(object):\n ... ids = dict()\n ... count = 0\n ... def getId(self, obj):\n ... for k, v in self.ids.iteritems():\n ... if obj == v:\n ... return k\n ... def register(self, obj):\n ... uid = self.getId(obj)\n ... if uid is not None:\n ... return uid\n ... self.count += 1\n ... self.ids[self.count] = obj\n ... return self.count\n ... def resolve(self, uid):\n ... return self.ids.get(int(uid), None)\n\n >>> globalIds = GlobalIds()\n >>> globalIds.register(a)\n 1\n >>> globalIds.register(b)\n 2\n >>> globalIds.register(root)\n 3\n\nIn our example we use the int ids as a substitute for the default path\nreferences which are the most common references in Zope.\n\nIn our examples we use a SnarfRepository which can easily be examined:\n\n>>> snarf = repository.SnarfRepository(StringIO())\n>>> checkout = task.Checkout(synchronizer.getSynchronizer, snarf)\n\nSnarf is a Zope3 specific archive format where the key\nneed is for simple software. The format is dead simple: each file\nis represented by the string\n\n ' \\n'\n\nfollowed by exactly bytes. Directories are not represented\nexplicitly.\n\n\nEntry Ids\n=========\n\nPersistent ids are also used in the metadata files of fssync.\nThe references are generated by an IEntryId adapter which must\nhave a string representation in order to be saveable in a text file.\nTypically these object ids correspond to the persistent pickle ids, but\nthis is not necessarily the case.\n\nSince we do not have paths we use our integer ids:\n\n >>> @component.adapter(interface.Interface)\n ... @interface.implementer(interfaces.IEntryId)\n ... def entryId(obj):\n ... global globalIds\n ... return globalIds.getId(obj)\n >>> component.provideAdapter(entryId)\n\n\nSynchronizer\n============\n\nIn the use case of data export / import it is crucial that fssync is\nable to serialize \"all\" object data. Note that it isn't always obvious\nwhat data is intrinsic to an object. Therefore we must provide\nspecial serialization / de-serialization tools which take care of\nwriting and reading \"all\" data.\n\nAn obvious solution would be to use inheriting synchronization\nadapters. But this solution bears a risk. If someone created a subclass\nand forgot to create an adapter, then their data would be serialized\nincompletely. To give an example: What happens if someone has a\nserialization adapter for class Person which serializes every aspect of\nPerson instances and defines a subclass Employee(Person) later on?\nIf the Employee class has some extra aspects (for example additional\nattributes like insurance id, wage, etc.) these would never be serialized\nas long as there is no special serialization adapter for Employees\nwhich handles this extra aspects. The behavior is different if the\nadapters are looked up by their dotted class name (i.e. the most specific\nclass) and not their class or interface (which might led to adapters\nwritten for super classes). If no specific adapter exists a default\nserializer (e.g a xmlpickler) can serialize the object completely. So\neven if you forget to provide special serializers for all your classes\nyou can be sure that your data are complete.\n\nSince the component architecture doesn't support adapters that work\none class only (not their subclasses), we register the adapter classes\nas named ISynchronizerFactory utilities and use the dotted name of the\nclass as lookup key. The default synchronizer is registered as a\nunnamed ISynchronizerFactory utility. This synchronizer ensures that\nall data are pickled to the target repository.\n\n >>> component.provideUtility(synchronizer.DefaultSynchronizer,\n ... provides=interfaces.ISynchronizerFactory)\n\nAll special synchronizers are registered for a specific content class and\nnot an abstract interface. The class is represented by the dotted class\nname in the factory registration:\n\n >>> class AFileSynchronizer(synchronizer.Synchronizer):\n ... interface.implements(interfaces.IFileSynchronizer)\n ... def dump(self, writeable):\n ... writeable.write(self.context.data)\n ... def load(self, readable):\n ... self.context.data = readable.read()\n\n >>> component.provideUtility(AFileSynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(A))\n\nThe lookup of the utilities by the dotted class name is handled\nby the getSynchronizer function, which first tries to find\na named utility. The IDefaultSynchronizer utility is used as a fallback:\n\n >>> synchronizer.getSynchronizer(a)\n \n\nIf no named adapter is registered it returns the registered unnamed default\nadapter (as long as the permissions allow this):\n\n >>> synchronizer.getSynchronizer(b)\n \n\nThis default serializer typically uses a pickle format, which is determined\nby the IPickler adapter. Here we use Zope's xmlpickle.\n\n >>> component.provideAdapter(pickle.XMLPickler)\n >>> component.provideAdapter(pickle.XMLUnpickler)\n\nFor container like objects we must provide an adapter that maps the\ncontainer to a directory. In our example we use the buildin dict class:\n\n >>> component.provideUtility(synchronizer.DirectorySynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(dict))\n\n\nNow we can export the object to the snarf archive:\n\n >>> checkout.perform(root, 'test')\n >>> print snarf.stream.getvalue()\n 00000213 @@Zope/Entries.xml\n \n \n \n \n 00000339 test/@@Zope/Entries.xml\n \n \n \n \n \n 00000009 test/a\n data of a00000370 test/b\n \n \n \n \n \n \n \n \n data of b\n \n \n extra of b\n \n \n \n \n \n\nAfter the registration of the necessary generators we can reimport the\nserialized data from the repository:\n\n >>> component.provideUtility(synchronizer.FileGenerator(),\n ... provides=interfaces.IFileGenerator)\n\n >>> target = {}\n >>> commit = task.Commit(synchronizer.getSynchronizer, snarf)\n >>> commit.perform(target, 'root', 'test')\n >>> sorted(target.keys())\n ['root']\n >>> sorted(target['root'].keys())\n ['a', 'b']\n\n >>> target['root']['a'].data\n 'data of a'\n\n >>> target['root']['b'].extra\n 'extra of b'\n\nIf we want to commit the data back into the original place we must check\nwhether the repository is still consistent with the original content.\nWe modify the objects in place to see what happens:\n\n >>> check = task.Check(synchronizer.getSynchronizer, snarf)\n >>> check.check(root, '', 'test')\n >>> check.errors()\n []\n\n >>> root['a'].data = 'overwritten'\n >>> root['b'].extra = 'overwritten'\n\n >>> check = task.Check(synchronizer.getSynchronizer, snarf)\n >>> check.check(root, '', 'test')\n >>> check.errors()\n ['test/a', 'test/b']\n\n >>> commit.perform(root, '', 'test')\n >>> sorted(root.keys())\n ['a', 'b']\n >>> root['a'].data\n 'data of a'\n >>> root['b'].extra\n 'extra of b'\n\n >>> del root['a']\n >>> commit.perform(root, '', 'test')\n >>> sorted(root.keys())\n ['a', 'b']\n\n >>> del root['b']\n >>> commit.perform(root, '', 'test')\n >>> sorted(root.keys())\n ['a', 'b']\n\n >>> del root['a']\n >>> del root['b']\n >>> commit.perform(root, '', 'test')\n >>> sorted(root.keys())\n ['a', 'b']\n\n\nPickling\n========\n\nIn many data structures, large, complex objects are composed of\nsmaller objects. These objects are typically stored in one of two\nways:\n\n 1. The smaller objects are stored inside the larger object.\n\n 2. The smaller objects are allocated in their own location,\n and the larger object stores references to them.\n\nIn case 1 the object is self-contained and can be pickled\ncompletely. This is the default behavior of the fssync pickler:\n\n >>> pickler = interfaces.IPickler([42])\n >>> pickler\n \n >>> print pickler.dumps()\n \n \n \n 42\n \n \n \n\nCase 2 is more complex since the pickler has to take persistent\nreferences into account.\n\n >>> class Complex(object):\n ... def __init__(self, part1, part2):\n ... self.part1 = part1\n ... self.part2 = part2\n\nEverthing here depends on the definition of what we consider to be an intrinsic\nreference. In the examples above we simply considered all objects as intrinsic.\n\n >>> from zope.fssync import pickle\n >>> c = root['c'] = Complex(a, b)\n >>> stream = StringIO()\n >>> print interfaces.IPickler(c).dumps()\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n data of a\n \n \n \n \n \n \n \n \n \n \n \n data of b\n \n \n overwritten\n \n \n \n \n \n \n \n \n \n\nIn order to use persistent references we must define a\nPersistentIdGenerator for our pickler, which determines whether\nan object should be pickled completely or only by reference:\n\n >>> class PersistentIdGenerator(object):\n ... interface.implements(interfaces.IPersistentIdGenerator)\n ... component.adapts(interfaces.IPickler)\n ... def __init__(self, pickler):\n ... self.pickler = pickler\n ... def id(self, obj):\n ... if isinstance(obj, Complex):\n ... return None\n ... return globalIds.getId(obj)\n\n >>> component.provideAdapter(PersistentIdGenerator)\n\n >>> globalIds.register(a)\n 1\n >>> globalIds.register(b)\n 2\n >>> globalIds.register(root)\n 3\n\n >>> xml = interfaces.IPickler(c).dumps()\n >>> print xml\n \n \n \n \n \n \n \n \n 1 \n \n \n 2 \n \n \n \n \n \n\nThe persistent ids can be loaded if we define and register\na IPersistentIdLoader adapter first:\n\n >>> class PersistentIdLoader(object):\n ... interface.implements(interfaces.IPersistentIdLoader)\n ... component.adapts(interfaces.IUnpickler)\n ... def __init__(self, unpickler):\n ... self.unpickler = unpickler\n ... def load(self, id):\n ... global globalIds\n ... return globalIds.resolve(id)\n\n >>> component.provideAdapter(PersistentIdLoader)\n >>> c2 = interfaces.IUnpickler(None).loads(xml)\n >>> c2.part1 == a\n True\n\n\nAnnotations, Extras, and Metadata\n=================================\n\nComplex objects often combine metadata and content data in various ways.\nThe fssync package allows to distinguish between file content, extras,\nannotations, and fssync specific metadata:\n\n - The file content or body is directly stored in a corresponding\n file.\n - The extras are object attributes which are part of the object but not\n part of the file content. They are typically store in extra files.\n - Annotations are content related metadata which can be stored as\n attribute annotations or outside the object itself. They are typically\n stored in seperate pickles for each annotation namespace.\n - Metadata directly related to fssync are stored in Entries.xml\n files.\n\nWhere exactly these aspects are stored is defined in the\nsynchronization format. The default format uses a @@Zope directory with\nsubdirectories for object extras and annotations. These @@Zope directories\nalso contain an Entries.xml metadata file which defines the following\nattributes:\n\n - id: the system id of the object, in Zope typically a traversal path\n - name: the filename of the serialized object\n - factory: the factory of the object, typically a dotted name of a class\n - type: a type identifier for pickled objects without factory\n - provides: directly provided interfaces of the object\n - key: the original name in the content space which is used\n in cases where the repository is not able to store this key\n unambigously\n - binary: a flag that prevents merging of binary data\n - flag: a status flag with the values 'added' or 'removed'\n\nIn part the metadata have to be delivered by the synchronizer. The base\nsynchronizer, for instance, returns the directly provided interfaces\nof an object as part of it's metadata:\n\n >>> class IMarkerInterface(interface.Interface):\n ... pass\n >>> interface.directlyProvides(a, IMarkerInterface)\n >>> pprint(synchronizer.Synchronizer(a).metadata())\n {'factory': 'zope.fssync.doctest.A',\n 'provides': 'zope.fssync.doctest.IMarkerInterface'}\n\nThe setmetadata method can be used to write metadata\nback to an object. Which metadata are consumed is up to the\nsynchronizer:\n\n >>> metadata = {'provides': 'zope.fssync.doctest.IMarkerInterface'}\n >>> synchronizer.Synchronizer(b).setmetadata(metadata)\n >>> [x for x in interface.directlyProvidedBy(b)]\n []\n\nIn order to serialize annotations we must first provide a\nISynchronizableAnnotations adapter:\n\n >>> snarf = repository.SnarfRepository(StringIO())\n >>> checkout = task.Checkout(synchronizer.getSynchronizer, snarf)\n\n >>> from zope import annotation\n >>> from zope.annotation.attribute import AttributeAnnotations\n >>> component.provideAdapter(AttributeAnnotations)\n >>> class IAnnotatableSample(interface.Interface):\n ... pass\n >>> class AnnotatableSample(object):\n ... interface.implements(IAnnotatableSample,\n ... annotation.interfaces.IAttributeAnnotatable)\n ... data = 'Main file content'\n ... extra = None\n >>> sample = AnnotatableSample()\n\n >>> class ITestAnnotations(interface.Interface):\n ... a = interface.Attribute('A')\n ... b = interface.Attribute('B')\n >>> import persistent\n >>> class TestAnnotations(persistent.Persistent):\n ... interface.implements(ITestAnnotations,\n ... annotation.interfaces.IAnnotations)\n ... component.adapts(IAnnotatableSample)\n ... def __init__(self):\n ... self.a = None\n ... self.b = None\n\n >>> component.provideAdapter(synchronizer.SynchronizableAnnotations)\n\n\n\n >>> from zope.annotation.factory import factory\n >>> component.provideAdapter(factory(TestAnnotations))\n >>> ITestAnnotations(sample).a = 'annotation a'\n >>> ITestAnnotations(sample).a\n 'annotation a'\n >>> sample.extra = 'extra'\n\nWithout a special serializer the annotations are pickled since\nthe annotations are stored in the __annotions__ attribute:\n\n >>> root = dict()\n >>> root['test'] = sample\n >>> checkout.perform(root, 'test')\n >>> print snarf.stream.getvalue()\n 00000197 @@Zope/Entries.xml\n \n \n \n \n 00000182 test/@@Zope/Entries.xml\n \n \n \n \n 00001929 test/test\n \n \n \n \n \n \n ...\n \n \n \n \n\nIf we provide a directory serializer for annotations and extras we get a\nfile for each extra attribute and annotation namespace.\n\n >>> component.provideUtility(\n ... synchronizer.DirectorySynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(synchronizer.Extras))\n\n >>> component.provideUtility(\n ... synchronizer.DirectorySynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(\n ... synchronizer.SynchronizableAnnotations))\n\nSince the annotations are already handled by the Synchronizer base class\nwe only need to specify the extra attribute here:\n\n >>> class SampleFileSynchronizer(synchronizer.Synchronizer):\n ... interface.implements(interfaces.IFileSynchronizer)\n ... def dump(self, writeable):\n ... writeable.write(self.context.data)\n ... def extras(self):\n ... return synchronizer.Extras(extra=self.context.extra)\n ... def load(self, readable):\n ... self.context.data = readable.read()\n >>> component.provideUtility(SampleFileSynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(AnnotatableSample))\n\n >>> interface.directlyProvides(sample, IMarkerInterface)\n >>> root['test'] = sample\n >>> checkout.perform(root, 'test')\n >>> print snarf.stream.getvalue()\n 00000197 @@Zope/Entries.xml\n \n \n \n \n 00000182 test/@@Zope/Entries.xml\n \n \n \n \n 00001929 test/test\n \n \n \n \n \n \n \n \n ...\n \n \n extra\n \n \n \n \n 00000197 @@Zope/Entries.xml\n \n \n \n \n 00000296 test/@@Zope/Entries.xml\n \n \n \n \n 00000211 test/@@Zope/Annotations/test/@@Zope/Entries.xml\n \n \n \n \n 00000617 test/@@Zope/Annotations/test/zope.fssync.doctest.TestAnnotations\n \n \n ...\n \n 00000161 test/@@Zope/Extra/test/@@Zope/Entries.xml\n \n \n \n \n 00000082 test/@@Zope/Extra/test/extra\n \n extra \n 00000017 test/test\n Main file content\n\nThe annotations and extras can of course also be deserialized. The default\ndeserializer handles both cases:\n\n >>> target = {}\n >>> commit = task.Commit(synchronizer.getSynchronizer, snarf)\n >>> commit.perform(target, 'root', 'test')\n >>> result = target['root']['test']\n >>> result.extra\n 'extra'\n >>> ITestAnnotations(result).a\n 'annotation a'\n\nSince we use an IDirectorySynchronizer each extra attribute and\nannotation namespace get's it's own file:\n\n >>> for path in sorted(snarf.iterPaths()):\n ... print path\n @@Zope/Entries.xml\n test/@@Zope/Annotations/test/@@Zope/Entries.xml\n test/@@Zope/Annotations/test/zope.fssync.doctest.TestAnnotations\n test/@@Zope/Entries.xml\n test/@@Zope/Extra/test/@@Zope/Entries.xml\n test/@@Zope/Extra/test/extra\n test/test\n\nThe number of files can be reduced if we provide the default synchronizer\nwhich uses a single file for all annotations and a single file for\nall extras:\n\n >>> component.provideUtility(\n ... synchronizer.DefaultSynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(synchronizer.Extras))\n\n >>> component.provideUtility(\n ... synchronizer.DefaultSynchronizer,\n ... interfaces.ISynchronizerFactory,\n ... name=synchronizer.dottedname(\n ... synchronizer.SynchronizableAnnotations))\n\n >>> root['test'] = sample\n >>> snarf = repository.SnarfRepository(StringIO())\n >>> checkout.repository = snarf\n >>> checkout.perform(root, 'test')\n >>> for path in sorted(snarf.iterPaths()):\n ... print path\n @@Zope/Entries.xml\n test/@@Zope/Annotations/test\n test/@@Zope/Entries.xml\n test/@@Zope/Extra/test\n test/test\n\nThe annotations and extras can of course also be deserialized. The default\ndeserializer handles both\n\n >>> target = {}\n >>> commit = task.Commit(synchronizer.getSynchronizer, snarf)\n >>> commit.perform(target, 'root', 'test')\n >>> result = target['root']['test']\n >>> result.extra\n 'extra'\n >>> ITestAnnotations(result).a\n 'annotation a'\n >>> [x for x in interface.directlyProvidedBy(result)]\n []\n\nIf we encounter an error, or multiple errors, while commiting we'll\nsee them in the traceback.\n\n >>> def bad_sync(container, key, fspath, add_callback):\n ... raise ValueError('1','2','3')\n\n >>> target = {}\n >>> commit = task.Commit(synchronizer.getSynchronizer, snarf)\n >>> old_sync_new = commit.synchNew\n >>> commit.synchNew = bad_sync\n >>> commit.perform(target, 'root', 'test')\n Traceback (most recent call last):\n ...\n Exception: test: '1', '2', '3'\n\nNotice that if we encounter multiple exceptions we print them all\nout at the end.\n\n >>> old_sync_old = commit.synchOld\n >>> commit.synchOld = bad_sync\n >>> commit.perform(target, 'root', 'test')\n Traceback (most recent call last):\n ...\n Exceptions:\n test: '1', '2', '3'\n test: '1', '2', '3'\n\n >>> commit.synchNew = old_sync_new\n >>> commit.synchOld = old_sync_old\n\n\nChanges\n=======\n\n3.6.1 (2013-05-02)\n------------------\n\n- Fixed exception raising on unpickling errors when checking in.\n\n- Improved reporting of unpickling errors.\n\n\n3.6.0 (2012-03-15)\n------------------\n\n- Commit task will collect errors and send them all back rather\n than stopping on the first error encountered.\n\n\n3.5.2 (2010-10-18)\n------------------\n\n- Fix tests; zope.location no longer exports TLocation.\n\n- Raise the right error in zope.fssync.synchronizer when the configured\n synchronizer does not exist.\n\n- Update dependency information.\n\n- Minor code cleanups.\n\n\n3.5.1 (2009-07-24)\n------------------\n\n- Properly setup tests, so that they will work in a release as well.\n\n- Removed slugs.\n\n3.5 (????)\n----------\n\n- Added the support for empty directories in snarf format. Now\n directories can be explicitly described by snarf.\n\n- Synchronizers can now return callbacks from the load\n method. This allows for fix ups to be run later. This is useful\n when adding multiple objects at the same time that depend on\n each other. Callbacks can in turn return callbacks.\n\n- Add support to FSMerger to allow locally modified files to be\n overwritten by files returned from the server. The purpose of\n this is to avoid conflicts after commit on files that are\n formatted differently on the server from local versions.\n\n3.4.0b1 (????)\n--------------\n\nRefactoring of zope.fssync and zope.app.fssync into two clearly\nseparated packages:\n\n- zope.fssync contains now a Python API that has no critical dependencies\n on Zope, the ZODB, and the security machinery.\n\n- zope.app.fssync contains a protected web-based API and special\n synchronizers for zope.app content types.\n\nOther major changes are\n\n- synchronizers (i.e. serialization/de-serialization adapters) are created\n by named utilities which use dotted class names as lookup keys\n\n- added doctests\n\n- support for large files\n\n- adapters for pickler, unpickler and handling of persistent pickle ids\n\n- binaries are no longer merged\n\n- case-insensitive filesystems and repositories use disambiguated names on\n export and the original names on import\n\n- export and import of directly provided interfaces\n\n- direct export to archives/direct import from archives\n\n- addressed encoding problems on Mac OSX", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://pypi.python.org/pypi/zope.fssync", "keywords": "zope3 serialization synchronization", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "zope.fssync", "package_url": "https://pypi.org/project/zope.fssync/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/zope.fssync/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://pypi.python.org/pypi/zope.fssync" }, "release_url": "https://pypi.org/project/zope.fssync/3.6.1/", "requires_dist": null, "requires_python": null, "summary": "Filesystem synchronization utility for Zope 3.", "version": "3.6.1" }, "last_serial": 718563, "releases": { "3.5": [ { "comment_text": "", "digests": { "md5": "211ee31e96c112fe65e24c55808ebefa", "sha256": "29a8e59c7c91aecbcbe846a11e4e0f9db0119b5a8cd774312f4af748ef403d11" }, "downloads": -1, "filename": "zope.fssync-3.5.tar.gz", "has_sig": false, "md5_digest": "211ee31e96c112fe65e24c55808ebefa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 62661, "upload_time": "2009-03-13T15:07:38", "url": "https://files.pythonhosted.org/packages/3e/a2/93ad89e2deb9af749ccac21800b2593b8d0b0f9c6798e3b54260485479d8/zope.fssync-3.5.tar.gz" } ], "3.5.1": [ { "comment_text": "", "digests": { "md5": "8c5a12f701694c0577312cbad4e77eef", "sha256": "05db6f532c37a405f7569c3de11c180d5d78992073a82c605940d70b3e06230e" }, "downloads": -1, "filename": "zope.fssync-3.5.1.tar.gz", "has_sig": false, "md5_digest": "8c5a12f701694c0577312cbad4e77eef", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 74258, "upload_time": "2009-07-24T19:15:12", "url": "https://files.pythonhosted.org/packages/d3/01/a2ba1f4593e373c408bd4fae38207fab3ad3c1555b205559a0932b9e6e1c/zope.fssync-3.5.1.tar.gz" } ], "3.5.2": [ { "comment_text": "", "digests": { "md5": "014745b4405685649ddfa547ba6569e7", "sha256": "9ab0bdc5b0c2395a4f2f19255df75ee34604c1d19f0bc9c5373eec6dd994c8da" }, "downloads": -1, "filename": "zope.fssync-3.5.2.tar.gz", "has_sig": false, "md5_digest": "014745b4405685649ddfa547ba6569e7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 75029, "upload_time": "2010-10-18T20:21:59", "url": "https://files.pythonhosted.org/packages/76/55/ca0224b1dd13f6761a30ba01ed7c0737bca951477affdc288c1e0e4a34e9/zope.fssync-3.5.2.tar.gz" } ], "3.6.0": [ { "comment_text": "", "digests": { "md5": "1754941b2ef894d5d8620f68282aa190", "sha256": "bcb50a2501e60c3ea2cc64e75a60e109468fd51df98264165777d4a18a5af74e" }, "downloads": -1, "filename": "zope.fssync-3.6.0.tar.gz", "has_sig": false, "md5_digest": "1754941b2ef894d5d8620f68282aa190", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 75968, "upload_time": "2012-03-15T16:37:44", "url": "https://files.pythonhosted.org/packages/40/46/4642db82e453c77df48e32083e9c8974459b34e1aab085b20e6126457f54/zope.fssync-3.6.0.tar.gz" } ], "3.6.1": [ { "comment_text": "", "digests": { "md5": "f62812c50707178c95dc6c4c22b8572b", "sha256": "a23b56644a894405a270a1195076e7c79de68bbc4066ec61ef8c17781bc9208b" }, "downloads": -1, "filename": "zope.fssync-3.6.1.zip", "has_sig": false, "md5_digest": "f62812c50707178c95dc6c4c22b8572b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 100526, "upload_time": "2013-05-02T10:48:24", "url": "https://files.pythonhosted.org/packages/2c/9c/7c738647ac8d8bf25de9ce2f52854fc9006a6dcca7cabf45bd62b606b184/zope.fssync-3.6.1.zip" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "f62812c50707178c95dc6c4c22b8572b", "sha256": "a23b56644a894405a270a1195076e7c79de68bbc4066ec61ef8c17781bc9208b" }, "downloads": -1, "filename": "zope.fssync-3.6.1.zip", "has_sig": false, "md5_digest": "f62812c50707178c95dc6c4c22b8572b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 100526, "upload_time": "2013-05-02T10:48:24", "url": "https://files.pythonhosted.org/packages/2c/9c/7c738647ac8d8bf25de9ce2f52854fc9006a6dcca7cabf45bd62b606b184/zope.fssync-3.6.1.zip" } ] }