{ "info": { "author": "Stephan Richter", "author_email": "stephan.richter@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Framework :: ZODB", "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" ], "description": "======================\nMongo Data Persistence\n======================\n\nThis document outlines the general capabilities of the ``mongopersist``\npackage. ``mongopersist`` is a Mongo storage implementation for persistent\nPython objects. It is *not* a storage for the ZODB.\n\nThe goal of ``mongopersist`` is to provide a data manager that serializes\nobjects to Mongo at transaction boundaries. The mongo data manager is a\npersistent data manager, which handles events at transaction boundaries (see\n``transaction.interfaces.IDataManager``) as well as events from the\npersistency framework (see ``persistent.interfaces.IPersistentDataManager``).\n\nAn instance of a data manager is supposed to have the same life time as the\ntransaction, meaning that it is assumed that you create a new data manager\nwhen creating a new transaction:\n\n >>> import transaction\n\nNote: The ``conn`` object is a ``pymongo.connection.Connection`` instance. In\nthis case our tests use the ``mongopersist_test`` database.\n\nLet's now define a simple persistent object:\n\n >>> import datetime\n >>> import persistent\n\n >>> class Person(persistent.Persistent):\n ...\n ... def __init__(self, name, phone=None, address=None, friends=None,\n ... visited=(), birthday=None):\n ... self.name = name\n ... self.address = address\n ... self.friends = friends or {}\n ... self.visited = visited\n ... self.phone = phone\n ... self.birthday = birthday\n ... self.today = datetime.datetime.now()\n ...\n ... def __str__(self):\n ... return self.name\n ...\n ... def __repr__(self):\n ... return '<%s %s>' %(self.__class__.__name__, self)\n\nWe will fill out the other objects later. But for now, let's create a new\nperson and store it in Mongo:\n\n >>> stephan = Person(u'Stephan')\n >>> stephan\n \n\nThe datamanager provides a ``root`` attribute in which the object tree roots\ncan be stored. It is special in the sense that it immediately writes the data\nto the DB:\n\n >>> dm.root['stephan'] = stephan\n >>> dm.root['stephan']\n \n\nCustom Persistence Collections\n------------------------------\n\nBy default, persistent objects are stored in a collection having the Python\npath of the class:\n\n >>> from mongopersist import serialize\n >>> person_cn = serialize.get_dotted_name(Person)\n >>> person_cn\n '__main__.Person'\n\n >>> import pprint\n >>> pprint.pprint(list(conn[DBNAME][person_cn].find()))\n [{u'_id': ObjectId('4e7ddf12e138237403000000'),\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan',\n u'phone': None,\n u'today': datetime.datetime(2011, 10, 1, 9, 45),\n u'visited': []}]\n\nAs you can see, the stored document for the person looks very Mongo. But oh\nno, I forgot to specify the full name for Stephan. Let's do that:\n\n >>> dm.root['stephan'].name = u'Stephan Richter'\n\nThis time, the data is not automatically saved:\n\n >>> conn[DBNAME][person_cn].find_one()['name']\n u'Stephan'\n\nSo we have to commit the transaction first:\n\n >>> transaction.commit()\n >>> conn[DBNAME][person_cn].find_one()['name']\n u'Stephan Richter'\n\nLet's now add an address for Stephan. Addresses are also persistent objects:\n\n >>> class Address(persistent.Persistent):\n ... _p_mongo_collection = 'address'\n ...\n ... def __init__(self, city, zip):\n ... self.city = city\n ... self.zip = zip\n ...\n ... def __str__(self):\n ... return '%s (%s)' %(self.city, self.zip)\n ...\n ... def __repr__(self):\n ... return '<%s %s>' %(self.__class__.__name__, self)\n\nMongoPersist supports a special attribute called ``_p_mongo_collection``,\nwhich allows you to specify a custom collection to use.\n\n >>> stephan = dm.root['stephan']\n >>> stephan.address = Address('Maynard', '01754')\n >>> stephan.address\n
\n\nNote that the address is not immediately saved in the database:\n\n >>> list(conn[DBNAME]['address'].find())\n []\n\nBut once we commit the transaction, everything is available:\n\n >>> transaction.commit()\n >>> pprint.pprint(list(conn[DBNAME]['address'].find()))\n [{u'_id': ObjectId('4e7de388e1382377f4000003'),\n u'city': u'Maynard',\n u'zip': u'01754'}]\n\n >>> pprint.pprint(list(conn[DBNAME][person_cn].find()))\n [{u'_id': ObjectId('4e7ddf12e138237403000000'),\n u'address': DBRef(u'address',\n ObjectId('4e7ddf12e138237403000000'),\n u'mongopersist_test'),\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan Richter',\n u'phone': None,\n u'today': datetime.datetime(2011, 10, 1, 9, 45)\n u'visited': []}]\n\n >>> dm.root['stephan'].address\n
\n\n\nNon-Persistent Objects\n----------------------\n\nAs you can see, even the reference looks nice and uses the standard Mongo DB\nreference construct. But what about arbitrary non-persistent, but picklable,\nobjects? Well, let's create a phone number object for that:\n\n >>> class Phone(object):\n ...\n ... def __init__(self, country, area, number):\n ... self.country = country\n ... self.area = area\n ... self.number = number\n ...\n ... def __str__(self):\n ... return '%s-%s-%s' %(self.country, self.area, self.number)\n ...\n ... def __repr__(self):\n ... return '<%s %s>' %(self.__class__.__name__, self)\n\n >>> dm.root['stephan'].phone = Phone('+1', '978', '394-5124')\n >>> dm.root['stephan'].phone\n \n\nLet's now commit the transaction and look at the Mongo document again:\n\n >>> transaction.commit()\n >>> dm.root['stephan'].phone\n \n\n >>> pprint.pprint(list(conn[DBNAME][person_cn].find()))\n [{u'_id': ObjectId('4e7ddf12e138237403000000'),\n u'address': DBRef(u'address',\n ObjectId('4e7ddf12e138237403000000'),\n u'mongopersist_test'),\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan Richter',\n u'phone': {u'_py_type': u'__main__.Phone',\n u'area': u'978',\n u'country': u'+1',\n u'number': u'394-5124'},\n u'today': datetime.datetime(2011, 10, 1, 9, 45)\n u'visited': []}]\n\nAs you can see, for arbitrary non-persistent objects we need a small hint in\nthe sub-document, but it is very minimal. If the ``__reduce__`` method returns\na more complex construct, more meta-data is written. We will see that next\nwhen storing a date and other arbitrary data:\n\n >>> dm.root['stephan'].friends = {'roy': Person(u'Roy Mathew')}\n >>> dm.root['stephan'].visited = (u'Germany', u'USA')\n >>> dm.root['stephan'].birthday = datetime.date(1980, 1, 25)\n\n >>> transaction.commit()\n >>> dm.root['stephan'].friends\n {u'roy': }\n >>> dm.root['stephan'].visited\n [u'Germany', u'USA']\n >>> dm.root['stephan'].birthday\n datetime.date(1980, 1, 25)\n\nAs you can see, a dictionary key is always converted to unicode and tuples are\nalways maintained as lists, since BSON does not have two sequence types.\n\n >>> pprint.pprint(conn[DBNAME][person_cn].find_one(\n ... {'name': 'Stephan Richter'}))\n {u'_id': ObjectId('4e7df744e138230a3e000000'),\n u'address': DBRef(u'address',\n ObjectId('4e7df744e138230a3e000003'),\n u'mongopersist_test'),\n u'birthday': {u'_py_factory': u'datetime.date',\n u'_py_factory_args': [Binary('\\x07\\xbc\\x01\\x19', 0)]},\n u'friends': {u'roy': DBRef(u'__main__.Person',\n ObjectId('4e7df745e138230a3e000004'),\n u'mongopersist_test')},\n u'name': u'Stephan Richter',\n u'phone': {u'_py_type': u'__main__.Phone',\n u'area': u'978',\n u'country': u'+1',\n u'number': u'394-5124'},\n u'today': datetime.datetime(2011, 9, 24, 11, 29, 8, 930000),\n u'visited': [u'Germany', u'USA']}\n\n\nCustom Serializers\n------------------\n\nAs you can see, the serialization of the birthay is all but ideal. We can,\nhowever, provide a custom serializer that uses the ordinal to store the data.\n\n >>> class DateSerializer(serialize.ObjectSerializer):\n ...\n ... def can_read(self, state):\n ... return isinstance(state, dict) and \\\n ... state.get('_py_type') == 'datetime.date'\n ...\n ... def read(self, state):\n ... return datetime.date.fromordinal(state['ordinal'])\n ...\n ... def can_write(self, obj):\n ... return isinstance(obj, datetime.date)\n ...\n ... def write(self, obj):\n ... return {'_py_type': 'datetime.date',\n ... 'ordinal': obj.toordinal()}\n\n >>> serialize.SERIALIZERS.append(DateSerializer())\n >>> dm.root['stephan']._p_changed = True\n >>> transaction.commit()\n\nLet's have a look again:\n\n >>> dm.root['stephan'].birthday\n datetime.date(1980, 1, 25)\n\n >>> pprint.pprint(conn[DBNAME][person_cn].find_one(\n ... {'name': 'Stephan Richter'}))\n {u'_id': ObjectId('4e7df803e138230aeb000000'),\n u'address': DBRef(u'address',\n ObjectId('4e7df803e138230aeb000003'),\n u'mongopersist_test'),\n u'birthday': {u'_py_type': u'datetime.date', u'ordinal': 722839},\n u'friends': {u'roy': DBRef(u'__main__.Person',\n ObjectId('4e7df803e138230aeb000004'),\n u'mongopersist_test')},\n u'name': u'Stephan Richter',\n u'phone': {u'_py_type': u'__main__.Phone',\n u'area': u'978',\n u'country': u'+1',\n u'number': u'394-5124'},\n u'today': datetime.datetime(2011, 9, 24, 11, 32, 19, 640000),\n u'visited': [u'Germany', u'USA']}\n\nMuch better!\n\n\nPersistent Objects as Sub-Documents\n-----------------------------------\n\nIn order to give more control over which objects receive their own collections\nand which do not, the developer can provide a special flag marking a\npersistent class so that it becomes part of its parent object's document:\n\n >>> class Car(persistent.Persistent):\n ... _p_mongo_sub_object = True\n ...\n ... def __init__(self, year, make, model):\n ... self.year = year\n ... self.make = make\n ... self.model = model\n ...\n ... def __str__(self):\n ... return '%s %s %s' %(self.year, self.make, self.model)\n ...\n ... def __repr__(self):\n ... return '<%s %s>' %(self.__class__.__name__, self)\n\nThe ``_p_mongo_sub_object`` is used to mark a type of object to be just part\nof another document:\n\n >>> dm.root['stephan'].car = car = Car('2005', 'Ford', 'Explorer')\n >>> transaction.commit()\n\n >>> dm.root['stephan'].car\n \n\n >>> pprint.pprint(conn[DBNAME][person_cn].find_one(\n ... {'name': 'Stephan Richter'}))\n {u'_id': ObjectId('4e7dfac7e138230d3d000000'),\n u'address': DBRef(u'address',\n ObjectId('4e7dfac7e138230d3d000003'),\n u'mongopersist_test'),\n u'birthday': {u'_py_type': u'datetime.date', u'ordinal': 722839},\n u'car': {u'_py_persistent_type': u'__main__.Car',\n u'make': u'Ford',\n u'model': u'Explorer',\n u'year': u'2005'},\n u'friends': {u'roy': DBRef(u'__main__.Person',\n ObjectId('4e7dfac7e138230d3d000004'),\n u'mongopersist_test')},\n u'name': u'Stephan Richter',\n u'phone': {u'_py_type': u'__main__.Phone',\n u'area': u'978',\n u'country': u'+1',\n u'number': u'394-5124'},\n u'today': datetime.datetime(2011, 9, 24, 11, 44, 7, 662000),\n u'visited': [u'Germany', u'USA']}\n\nThe reason we want objects to be persistent is so that they pick up changes\nautomatically:\n\n >>> dm.root['stephan'].car.year = '2004'\n >>> transaction.commit()\n >>> dm.root['stephan'].car\n \n\n\nCollection Sharing\n------------------\n\nSince Mongo is so flexible, it sometimes makes sense to store multiple types\nof (similar) objects in the same collection. In those cases you instruct the\nobject type to store its Python path as part of the document.\n\nWarning: Please note though that this method is less efficient, since the\ndocument must be loaded in order to create a ghost causing more database\naccess.\n\n >>> class ExtendedAddress(Address):\n ...\n ... def __init__(self, city, zip, country):\n ... super(ExtendedAddress, self).__init__(city, zip)\n ... self.country = country\n ...\n ... def __str__(self):\n ... return '%s (%s) in %s' %(self.city, self.zip, self.country)\n\nIn order to accomplish collection sharing, you simply create another class\nthat has the same ``_p_mongo_collection`` string as another (sub-classing will\nensure that).\n\nSo let's give Stephan an extended address now.\n\n >>> dm.root['stephan'].address2 = ExtendedAddress(\n ... 'Tettau', '01945', 'Germany')\n >>> dm.root['stephan'].address2\n \n >>> transaction.commit()\n\nWhen loading the addresses, they should be of the right type:\n\n >>> dm.root['stephan'].address\n
\n >>> dm.root['stephan'].address2\n \n\n\nTricky Cases\n------------\n\nChanges in Basic Mutable Type\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTricky, tricky. How do we make the framework detect changes in mutable\nobjects, such as lists and dictionaries? Answer: We keep track of which\npersistent object they belong to and provide persistent implementations.\n\n >>> type(dm.root['stephan'].friends)\n \n\n >>> dm.root['stephan'].friends[u'roger'] = Person(u'Roger')\n >>> transaction.commit()\n >>> sorted(dm.root['stephan'].friends.keys())\n [u'roger', u'roy']\n\nThe same is true for lists:\n\n >>> type(dm.root['stephan'].visited)\n \n\n >>> dm.root['stephan'].visited.append('France')\n >>> transaction.commit()\n >>> dm.root['stephan'].visited\n [u'Germany', u'USA', u'France']\n\n\nCircular Non-Persistent References\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAny mutable object that is stored in a sub-document, cannot have multiple\nreferences in the object tree, since there is no global referencing. These\ncircular references are detected and reported:\n\n >>> class Top(persistent.Persistent):\n ... foo = None\n\n >>> class Foo(object):\n ... bar = None\n\n >>> class Bar(object):\n ... foo = None\n\n >>> top = Top()\n >>> foo = Foo()\n >>> bar = Bar()\n >>> top.foo = foo\n >>> foo.bar = bar\n >>> bar.foo = foo\n\n >>> dm.root['top'] = top\n Traceback (most recent call last):\n ...\n CircularReferenceError: <__main__.Foo object at 0x7fec75731890>\n\n\nCircular Persistent References\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIn general, circular references among persistent objects are not a problem,\nsince we always only store a link to the object. However, there is a case when\nthe circular dependencies become a problem.\n\nIf you set up an object tree with circular references and then add the tree to\nthe storage at once, it must insert objects during serialization, so that\nreferences can be created. However, care needs to be taken to only create a\nminimal reference object, so that the system does not try to recursively\nreduce the state.\n\n >>> class PFoo(persistent.Persistent):\n ... bar = None\n\n >>> class PBar(persistent.Persistent):\n ... foo = None\n\n >>> top = Top()\n >>> foo = PFoo()\n >>> bar = PBar()\n >>> top.foo = foo\n >>> foo.bar = bar\n >>> bar.foo = foo\n\n >>> dm.root['ptop'] = top\n\n\nContainers and Collections\n--------------------------\n\nNow that we have talked so much about the gory details on storing one object,\nwhat about mappings that reflect an entire collection, for example a\ncollection of people.\n\nThere are many approaches that can be taken. The folowing implementation\ndefines an attribute in the document as the mapping key and names a\ncollection:\n\n >>> from mongopersist import mapping\n >>> class People(mapping.MongoCollectionMapping):\n ... __mongo_collection__ = person_cn\n ... __mongo_mapping_key__ = 'short_name'\n\nThe mapping takes the data manager as an argument. One can easily create a\nsub-class that assigns the data manager automatically. Let's have a look:\n\n >>> People(dm).keys()\n []\n\nThe reason no person is in the list yet, is because no document has the key\nyet or the key is null. Let's change that:\n\n >>> People(dm)['stephan'] = dm.root['stephan']\n >>> transaction.commit()\n\n >>> People(dm).keys()\n [u'stephan']\n >>> People(dm)['stephan']\n \n\nAlso note that setting the \"short-name\" attribute on any other person will add\nit to the mapping:\n\n >>> dm.root['stephan'].friends['roy'].short_name = 'roy'\n >>> transaction.commit()\n >>> sorted(People(dm).keys())\n [u'roy', u'stephan']\n\n\nWrite-Conflict Detection\n------------------------\n\nSince Mongo has no support for MVCC, it does not provide a concept of write\nconflict detection. However, a simple write-conflict detection can be easily\nimplemented using a serial number on the document.\n\nLet's reset the database and create a data manager with enabled conflict\ndetection:\n\n >>> from mongopersist import conflict, datamanager\n >>> conn.drop_database(DBNAME)\n >>> dm2 = datamanager.MongoDataManager(\n ... conn,\n ... default_database=DBNAME,\n ... root_database=DBNAME,\n ... conflict_handler_factory=conflict.SimpleSerialConflictHandler)\n\nNow we add a person and see that the serial got stored.\n\n >>> dm2.root['stephan'] = Person(u'Stephan')\n >>> dm2.root['stephan']._p_serial\n '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01'\n >>> pprint.pprint(dm2._conn[DBNAME][person_cn].find_one())\n {u'_id': ObjectId('4e7fe18de138233a5b000009'),\n u'_py_serial': 1,\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan',\n u'phone': None,\n u'today': datetime.datetime(2011, 9, 25, 22, 21, 1, 656000),\n u'visited': []}\n\nNext we change the person and commit it again:\n\n >>> dm2.root['stephan'].name = u'Stephan '\n >>> transaction.commit()\n >>> pprint.pprint(dm2._conn[DBNAME][person_cn].find_one())\n {u'_id': ObjectId('4e7fe18de138233a5b000009'),\n u'_py_serial': 2,\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan ',\n u'phone': None,\n u'today': datetime.datetime(2011, 9, 25, 22, 21, 1, 656000),\n u'visited': []}\n\nLet's now start a new transaction with some modifications:\n\n >>> dm2.root['stephan'].name = u'Stephan Richter'\n\nHowever, in the mean time another transaction modifies the object. (We will do\nthis here directly via Mongo for simplicity.)\n\n >>> _ = dm2._conn[DBNAME][person_cn].update(\n ... {'name': u'Stephan '},\n ... {'$set': {'name': u'Stephan R.', '_py_serial': 3}})\n >>> pprint.pprint(dm2._conn[DBNAME][person_cn].find_one())\n {u'_id': ObjectId('4e7fe1f4e138233ac4000009'),\n u'_py_serial': 3,\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan R.',\n u'phone': None,\n u'today': datetime.datetime(2011, 9, 25, 22, 22, 44, 343000),\n u'visited': []}\n\nNow our changing transaction tries to commit:\n\n >>> transaction.commit()\n Traceback (most recent call last):\n ...\n ConflictError: database conflict error\n (oid DBRef(u'__main__.Person',\n ObjectId('4e7ddf12e138237403000000'),\n u'mongopersist_test'),\n class Person,\n orig serial 2, cur serial 3, new serial 3)\n\n >>> transaction.abort()\n\n\n=======\nCHANGES\n=======\n\n0.8.4 (2013-06-13)\n------------------\n\n- Fix insert followed by remove in the same transaction. The document was\n not removed from mongo.\n\n- Fix transaction.abort() behaviour for complex objects. _py_type information\n is not lost after transaction abort().\n\n\n0.8.3 (2013-04-09)\n------------------\n\n- Fixed ``MongoContainer`` vs ``IdNamesMongoContainer``\n ``add`` and ``__setitem__`` behavour on ``None`` keys.\n ``ObjectAddedEvent`` or ``ObjectMovedEvent`` were not fired because\n ``zope.container.contained.setitem`` got the just inserted object back.\n\n ``MongoContainer`` always requires ``_m_mapping_key`` and uses that attribute\n of the object to determine the new key.\n\n ``IdNamesMongoContainer`` requires ``_m_mapping_key`` ``None`` and uses\n ``_id`` to determine the new key.\n\n\n\n0.8.2 (2013-04-03)\n------------------\n\n- Fixed ``check_conflict``: make sure we use the same db and collection\n as in the object\n\n\n0.8.1 (2013-03-19)\n------------------\n\n- Fixed ``_p_changed`` setting on object loading which was caused by assigning\n directly to ``__name__``. That caused all objects read from containers to be\n marked changed on load. That wrecked cache performance too.\n\n\n0.8.0 (2013-02-09)\n------------------\n\n- Feature: Added ``find_objects()`` and ``find_one_object()`` to the collection\n wrapper, so that whenever you get a collection from the data manager, you\n can load objects directly through the find API.\n\n- Feature: Added the ability for MongoContained objects to fully reference and\n load their parents. This allows one to query mongo directly and create the\n object from the doc without going through the right container, which you\n might not know easily.\n\n\n0.7.7 (2013-02-08)\n------------------\n\n- Bug: Do not fail if we cannot delete the parent and name attributes.\n\n\n0.7.6 (2013-02-08)\n------------------\n\n- Feature: Switch to ``pymongo.MongoClient``, set default write concern values,\n allow override of write concern values.\n\n\n0.7.5 (2013-02-06)\n------------------\n\n- Tests: Added, cleaned tests\n\n- Bug: Re-release after missing files in 0.7.4\n\n0.7.4 (2013-02-05)\n------------------\n\n- Bug: Due to ``UserDict`` implementing ``dict`` comparison semantics, any\n empty ``MongoContainer`` would equate to another empty one. This behavior\n would cause object changes to not be properly recognzed by the mongo data\n manager. The implemented solution is to implement default object comparison\n behavior for mongo containers.\n\n\n0.7.3 (2013-01-29)\n------------------\n\n- Feature: Update to latest package versions, specifically pymongo 2.4.x. In\n this release, ``pymongo`` does not reexport ``objectid`` and ``dbref``.\n\n0.7.2 (2012-04-19)\n------------------\n\n- Bug: avoid caching MongoDataManager instances in mongo container to avoid\n multiple MongoDataManagers in the single transaction in multithreaded\n environment. Cache IMongoDataManagerProvider instead.\n\n0.7.1 (2012-04-13)\n------------------\n\n- Performance: Improved the profiler a bit by allowing to disable modification\n of records as well.\n\n- Performance: Added caching of ``_m_jar`` lookups in Mongo Containers, since\n the computation turned out to be significantly expensive.\n\n- Performance: Use lazy hash computation for DBRef. Also, disable support for\n arbitrary keyword arguments. This makes roughly a 2-4% difference in object\n loading time.\n\n- Bug: An error occurred when ``_py_serial`` was missing. This was possible\n due to a bug in version 0.6. It also protects against third party software\n which is not aware of our meta-data.\n\n- Performance: Switched to ``repoze.lru`` (from ``lru``), which is much\n faster.\n\n- Performance: To avoid excessive hash computations, we now use the hash of\n the ``DBRef`` references as cache keys.\n\n- Bug: ``ObjectId`` ids are not guaranteed to be unique across\n collections. Thus they are a bad key for global caches. So we use full\n ``DBRef`` references instead.\n\n0.7.0 (2012-04-02)\n------------------\n\n- Feature: A new ``IConflictHandler`` interface now controls all aspects of\n conflict resolution. The following implementations are provided:\n\n * ``NoCheckConflictHandler``: This handler does nothing and when used, the\n system behaves as before when the ``detect_conflicts`` flag was set to\n ``False``.\n\n * ``SimpleSerialConflictHandler``: This handler uses serial numbers on each\n document to keep track of versions and then to detect conflicts. When a\n conflict is detected, a ``ConflictError`` is raised. This handler is\n identical to ``detect_conflicts`` being set to ``True``.\n\n * ``ResolvingSerialConflictHandler``: Another serial handler, but it has the\n ability to resolve a conflict. For this to happen, a persistent object\n must implement ``_p_resolveConflict(orig_state, cur_state, new_state)``,\n which returns the new, merged state. (Experimental)\n\n As a result, the ``detect_conflicts`` flag of the data manager was removed\n and replaced with the ``conflict_handler`` attribute. One can pass in the\n ``conflict_handler_factory`` to the data manager constructor. The factory\n needs to expect on argument, the data manager.\n\n- Feature: The new ``IdNamesMongoContainer`` class uses the natural Mongo\n ObjectId as the name/key for the items in the container. No more messing\n around with coming up or generating a name. Of course, if you specified\n ``None`` as a key in the past, it already used the object id, but it was\n sotred again in the mapping key field. Now the object id is used directly\n everywhere.\n\n- Feature: Whenever ``setattr()`` is called on a persistent object, it is\n marked as changed even if the new value equals the old one. To minimize\n writes to MongoDB, the latest database state is compared to the new state\n and the new state is only written when changes are detected. A flag called\n ``serialize.IGNORE_IDENTICAL_DOCUMENTS`` (default: ``True``) is used to\n control the feature. (Experimental)\n\n- Feature: ``ConflictError`` has now a much more meaningful API. Instead of\n just referencing the object and different serials, it now actual has the\n original, current and new state documents.\n\n- Feature: Conflicts are now detected while aborting a transaction. The\n implemented policy will not reset the document state, if a conflict is\n detected.\n\n- Feature: Provide a flag to turn on MongoDB access logging. The flag is false\n by default, since access logging is very expensive.\n\n- Feature: Added transaction ID to LoggingDecorator.\n\n- Feature: Added a little script to test performance. It is not very\n sophisticated, but it is sufficient for a first round of optimizations.\n\n- Feature: Massively improved performance on all levels. This was mainly\n accomplished by removing unnecessary database accesses, better caching and\n more efficient algorithms. This results in speedups between 4-25 times.\n\n - When resolving the path to a class, the result is now cached. More\n importantly, lookup failures are also cached mapping path ->\n ``None``. This is important, since an optimization the ``resolve()``\n method causes a lot of failing lookups.\n\n - When resolving the dbref to a type, we try to resolve the dbref early\n using the document, if we know that the documents within the collection\n store their type path. This avoids frequent queries of the name map\n collection when it is not needed.\n\n - When getting the object document to read the class path, it will now read\n the entire document and store it in the ``_latest_states`` dictionary, so\n that other code may pick it up and use it. This should avoid superflous\n reads from MongoDB.\n\n - Drastically improved performance for collections that store only one type\n of object and where the documents do not store the type (i.e. it is\n stored in the name map collection).\n\n - The Mongo Container fast load via find() did not work correctly, since\n setstate() did not change the state from ghost to active and thus the\n state was loaded again from MongoDB and set on the object. Now we use the\n new ``_latest_states`` cache to lookup a document when ``setstate()`` is\n called through the proper channels. Now this \"fast load\" method truly\n causes O(1) database lookups.\n\n - Implemented several more mapping methods for the Mongo Container, so that\n all methods getting the full list of items are fast now.\n\n - Whenever the Mongo Object Id is used as a hash key, use the hash of the id\n instead. The ``__cmp__()`` method of the ``ObjectId`` class is way too\n slow.\n\n - Cache collection name lookup from objects in the ``ObjectWriter`` class.\n\n- Bug: We have seen several occasions in production where we suddenly lost\n some state in some documents, which prohibited the objects from being\n loadable again. The cause was that the ``_original_states`` attribute did not\n store the raw MongoDB document, but a modified one. Since those states are\n used during abort to reset the state, however, the modified document got\n stored making the affected objects inaccessible.\n\n- Bug: When a transaction was aborted, the states of all *loaded* objects were\n reset. Now, only *modified* object states are reset. This should drastically\n lower problems (by the ratio of read over modified objects) due to lack of\n full MVCC.\n\n- Bug: When looking for an item by key/name (``find_*()`` methods) , you would\n never get the right object back, but the first one found in the\n database. This was due to clobbering the search filter with more general\n parameters.\n\n\n0.6.1 (2012-03-28)\n------------------\n\n- Feature: Added quite detailed debug logging around collection methods\n\n0.6.0 (2012-03-12)\n------------------\n\n- Feature: Switched to optimisitc data dumping, which approaches transactions\n by dumping early and as the data comes. All changes are undone when the\n transaction fails/aborts. See ``optimistic-data-dumping.txt`` for\n details. Here are some of the new features:\n\n * Data manager keeps track of all original docs before their objects are\n modified, so any change can be done.\n\n * Added an API to data manager (``DataManager.insert(obj)``) to insert an\n object in the database.\n\n * Added an API to data manager (``DataManager.remove(obj)``) to remove an\n object from the database.\n\n * Data can be flushed to Mongo (``DataManager.flush()``) at any point of the\n transaction retaining the ability to completely undo all changes. Flushing\n features the following characteristics:\n\n + During a given transaction, we guarantee that the user will always receive\n the same Python object. This requires that flush does not reset the object\n cache.\n\n + The ``_p_serial`` is increased by one. (Automatically done in object\n writer.)\n\n + The object is removed from the registered objects and the ``_p_changed``\n flag is set to ``False``.\n\n + Before flushing, potential conflicts are detected.\n\n * Implemented a flushing policy: Changes are always flushed before any query\n is made. A simple wrapper for the ``pymongo`` collection\n (``CollectionWrapper``) ensures that flush is called before the correct\n method calls. Two new API methods ``DataManager.get_collection(db_name,\n coll_name)`` and ``DataManager.get_collection_from_object(obj)``\n allows one to quickly get a wrapped collection.\n\n- Feature: Renamed ``processSpec()`` to ``process_spec()`` to adhere to\n package nameing convention.\n\n- Feature: Created a ``ProcessSpecDecorator`` that is used in the\n ``CollectionWrapper`` class to process the specs of the ``find()``,\n ``find_one()`` and ``find_and_modify()`` collection methods.\n\n- Feature: The ``MongoContainer`` class now removes objects from the database\n upon container removal is ``_m_remove_documents`` is ``True``. The default\n is ``True``.\n\n- Feature: When adding an item to ``MongoContainer`` and the key is ``None``,\n then the OID is chosen as the key. Ids are perfect keys, because they are\n guaranteed to be unique within the collection.\n\n- Feature: Since people did not like the setitem with ``None`` key\n implementation, I also added the ``MongoContainer.add(value, key=None)``\n method, which makes specifying the key optional. The default implementation\n is to use the OID, if the key is ``None``.\n\n- Feature: Removed ``fields`` argument from the ``MongoContainer.find(...)``\n and ``MongoContainer.find_one(...)`` methods, since it was not used.\n\n- Feature: If a container has N items, it took N+1 queries to load the list of\n items completely. This was due to one query returning all DBRefs and then\n using one query to load the state for each. Now, the first query loads all\n full states and uses an extension to ``DataManager.setstate(obj, doc=None)``\n to load the state of the object with the previously queried data.\n\n- Feature: Changed ``MongoContainer.get_collection()`` to return a\n ``CollectionWrapper`` instance.\n\n\n0.5.5 (2012-03-09)\n------------------\n\n- Feature: Moved ZODB dependency to test dependency\n\n- Bug: When an object has a SimpleContainer as attribute, then simply loading\n this object would cause it to written at the end of the transaction. The\n culprit was a persistent dictionary containing the SimpleContainer\n state. This dictionary got modified during state load and caused it to be\n registered as a changed object and it was marked as a ``_p_mongo_sub_object``\n and had the original object as ``_p_mongo_doc_object``.\n\n\n0.5.4 (2012-03-05)\n------------------\n\n- Feature: Added a hook via the IMongoSpecProcessor adapter that gets called\n before each find to process/log spec.\n\n0.5.3 (2012/01/16)\n------------------\n\n- Bug: ``MongoContainer`` did not emit any Zope container or lifecycle\n events. This has been fixed by using the ``zope.container.contained``\n helper functions.\n\n0.5.2 (2012-01-13)\n------------------\n\n- Feature: Added an interface for the ``MongoContainer`` class describing the\n additional attributes and methods.\n\n0.5.1 (2011-12-22)\n------------------\n\n- Bug: The ``MongoContainer`` class did not implement the ``IContainer``\n interface.\n\n0.5.0 (2011-11-04)\n------------------\n\n- Initial Release", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/zopefoundation/mongopersist", "keywords": "mongo persistent", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "mongopersist", "package_url": "https://pypi.org/project/mongopersist/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/mongopersist/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/zopefoundation/mongopersist" }, "release_url": "https://pypi.org/project/mongopersist/0.8.4/", "requires_dist": null, "requires_python": null, "summary": "Mongo Persistence Backend", "version": "0.8.4" }, "last_serial": 794956, "releases": { "0.5.0": [ { "comment_text": "", "digests": { "md5": "2bd29959c5f5e3d3f96a3c3bdc0a425c", "sha256": "a3aabd89f259e415e23e0761e94f191628ef7ad979b5f17fead7f6bb11cf450b" }, "downloads": -1, "filename": "mongopersist-0.5.0.tar.gz", "has_sig": false, "md5_digest": "2bd29959c5f5e3d3f96a3c3bdc0a425c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42164, "upload_time": "2011-11-04T18:29:05", "url": "https://files.pythonhosted.org/packages/a0/0b/5465615787b45ec789bcf64d39b4392a656b04645fbd088eedc2514fac61/mongopersist-0.5.0.tar.gz" } ], "0.5.1": [ { "comment_text": "", "digests": { "md5": "416ef63f9ff7e8625e6b5baf9c36e64c", "sha256": "a2e97b817c2df6c57fe030d32362e0d50a9f29c968e8e1b6bb553d20621c7bf0" }, "downloads": -1, "filename": "mongopersist-0.5.1.tar.gz", "has_sig": false, "md5_digest": "416ef63f9ff7e8625e6b5baf9c36e64c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 43645, "upload_time": "2011-12-22T15:02:26", "url": "https://files.pythonhosted.org/packages/b5/08/9df7f19e4f4240094e0ba34cbb34d365722c10128536810eb6232a26cc02/mongopersist-0.5.1.tar.gz" } ], "0.5.2": [ { "comment_text": "", "digests": { "md5": "8f6c2b40bcb51373171da70e7e409660", "sha256": "12df03ca9a1d8b4fd3a2ffd2880b377f76b0f33d40cfb99aadf148658c108d9a" }, "downloads": -1, "filename": "mongopersist-0.5.2.tar.gz", "has_sig": false, "md5_digest": "8f6c2b40bcb51373171da70e7e409660", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 44325, "upload_time": "2012-01-13T21:21:48", "url": "https://files.pythonhosted.org/packages/bf/16/dc8b6c9d7faf9689b18f94101494b9abad49144efd95accc973cab21044f/mongopersist-0.5.2.tar.gz" } ], "0.5.3": [ { "comment_text": "", "digests": { "md5": "e52747409e9e7c389dc9cb5166dccc17", "sha256": "4c0b874e2c4c75108ec19a2a69f4f91203d63e73ca80cc7edafe17790ef01492" }, "downloads": -1, "filename": "mongopersist-0.5.3.tar.gz", "has_sig": false, "md5_digest": "e52747409e9e7c389dc9cb5166dccc17", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 44590, "upload_time": "2012-01-16T17:28:44", "url": "https://files.pythonhosted.org/packages/7d/14/a72fffb4456117f41cc57a74959291138b7f5b0537b364a364ba9d0a6251/mongopersist-0.5.3.tar.gz" } ], "0.5.4": [ { "comment_text": "", "digests": { "md5": "f7297a90801fd1e0be4f1563cc80061c", "sha256": "3d2a5dfb21a4b95a7a091af467708e25a24b67773f778f9870a1d65314a277f9" }, "downloads": -1, "filename": "mongopersist-0.5.4.tar.gz", "has_sig": false, "md5_digest": "f7297a90801fd1e0be4f1563cc80061c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 45115, "upload_time": "2012-03-05T14:59:58", "url": "https://files.pythonhosted.org/packages/c7/09/23133454589c3669be3ce05f764b4d26bb249f1d2021ba9b6f3083090d23/mongopersist-0.5.4.tar.gz" } ], "0.5.5": [ { "comment_text": "", "digests": { "md5": "3f05533f991b912a5a6ee350e55ccbea", "sha256": "bf68c76b64f0dfd240e73338a8bdb77cf807b542da454edc3128bed364f41dbc" }, "downloads": -1, "filename": "mongopersist-0.5.5.tar.gz", "has_sig": false, "md5_digest": "3f05533f991b912a5a6ee350e55ccbea", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 45510, "upload_time": "2012-03-09T22:35:18", "url": "https://files.pythonhosted.org/packages/e6/68/789fe048fb28309b4222fe955c28a28b4862a1965d35e615ab066c931578/mongopersist-0.5.5.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "44295bba0bbe2236f180bb53dd2eefed", "sha256": "009fcb4efda657b0426ee6f6be17169f6ab1d3617859c52d75e114c6f9d80365" }, "downloads": -1, "filename": "mongopersist-0.6.0.tar.gz", "has_sig": false, "md5_digest": "44295bba0bbe2236f180bb53dd2eefed", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 52072, "upload_time": "2012-03-13T01:16:20", "url": "https://files.pythonhosted.org/packages/a8/5d/be2a8d1b7036132666a13276a8da8c9520f6ada702da2397b041b7a6e241/mongopersist-0.6.0.tar.gz" } ], "0.6.1": [ { "comment_text": "", "digests": { "md5": "3b242ef4eb6d50af16b2fe74357e741b", "sha256": "198ae27a9c1f45b7773fe6e5c3df35fb6b87aeac69759fed47d78362306dfc5e" }, "downloads": -1, "filename": "mongopersist-0.6.1.tar.gz", "has_sig": false, "md5_digest": "3b242ef4eb6d50af16b2fe74357e741b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 52710, "upload_time": "2012-03-28T17:02:02", "url": "https://files.pythonhosted.org/packages/d1/d4/c64c13e6475c947b5c3b402e473c5c75000ae09f85a62e7179ba3dbd9ec5/mongopersist-0.6.1.tar.gz" } ], "0.7.0": [ { "comment_text": "", "digests": { "md5": "5d27e54e91204e3976a915889053b6f0", "sha256": "ad49fb9c2edf7cfdccad9829e5f278a92233186d95ab12c35c8673d01fc3b052" }, "downloads": -1, "filename": "mongopersist-0.7.0.tar.gz", "has_sig": false, "md5_digest": "5d27e54e91204e3976a915889053b6f0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 63666, "upload_time": "2012-04-02T17:18:19", "url": "https://files.pythonhosted.org/packages/1b/13/06a0ec8513d2dccb1954d2e2ce733e8bf3ce6f2ff059bd964e4cdb920b36/mongopersist-0.7.0.tar.gz" } ], "0.7.1": [ { "comment_text": "", "digests": { "md5": "9c022673234b57df4842a56f3e2e1f87", "sha256": "8924eadcd43ce53b0ae594aa9fa2bbb0278cff40201eceaaf6ec1c1cae3653a9" }, "downloads": -1, "filename": "mongopersist-0.7.1.tar.gz", "has_sig": false, "md5_digest": "9c022673234b57df4842a56f3e2e1f87", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 70206, "upload_time": "2012-04-13T15:27:55", "url": "https://files.pythonhosted.org/packages/38/a5/29433f68f96a011548463f5741d2dc16b872c7c97e839a96992fa289e1df/mongopersist-0.7.1.tar.gz" } ], "0.7.2": [ { "comment_text": "", "digests": { "md5": "d15224db22b3ad27bc799f5a38f4418a", "sha256": "6dd863d26bdca8bfd1ba358fae9b1b2a72d1e7b9b158fc5653921d75a2deda63" }, "downloads": -1, "filename": "mongopersist-0.7.2.tar.gz", "has_sig": false, "md5_digest": "d15224db22b3ad27bc799f5a38f4418a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 69753, "upload_time": "2012-04-19T13:46:13", "url": "https://files.pythonhosted.org/packages/5f/16/a96d55910e98c4710aa7ab8b22cab243ab27f92c1852fd7a540fc3c315ce/mongopersist-0.7.2.tar.gz" } ], "0.7.3": [ { "comment_text": "", "digests": { "md5": "21f1d0845884e6cad7fc33e81e53b127", "sha256": "213b240608e5317c2a3a46a69f34d21f52c60743894728d36bb71e4f22adc55c" }, "downloads": -1, "filename": "mongopersist-0.7.3.zip", "has_sig": false, "md5_digest": "21f1d0845884e6cad7fc33e81e53b127", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 94092, "upload_time": "2013-01-29T11:33:01", "url": "https://files.pythonhosted.org/packages/11/ab/ffde72869fb754e24db2605409e7bdca89c454f063a201408036cfa1d7f6/mongopersist-0.7.3.zip" } ], "0.7.4": [ { "comment_text": "", "digests": { "md5": "c0b5cf402c106bc845faff92c6fd1350", "sha256": "4b5900e5ed1fa8f3066c43314fbcf716601a152f7d12977690496e7fc64d4271" }, "downloads": -1, "filename": "mongopersist-0.7.4.tar.gz", "has_sig": false, "md5_digest": "c0b5cf402c106bc845faff92c6fd1350", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 58529, "upload_time": "2013-02-06T02:43:25", "url": "https://files.pythonhosted.org/packages/29/06/1be921a26a1d9b0f41bd053df72328bbe708e580a103897a039c1a1f8cdd/mongopersist-0.7.4.tar.gz" } ], "0.7.5": [ { "comment_text": "", "digests": { "md5": "9406336e0fb6a42e5cda7e900aa8dbf9", "sha256": "09df7e39783d07dce8b96c906e21be55a1d3f8198ec08ea6b58be95b2bb84340" }, "downloads": -1, "filename": "mongopersist-0.7.5.zip", "has_sig": false, "md5_digest": "9406336e0fb6a42e5cda7e900aa8dbf9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 95326, "upload_time": "2013-02-06T08:52:21", "url": "https://files.pythonhosted.org/packages/e9/11/c59319884ea52e5f17bb9d3e75de3134b4ca8602875657c30163f85009bd/mongopersist-0.7.5.zip" } ], "0.7.6": [ { "comment_text": "", "digests": { "md5": "2286d867c9f59803476b6b40a916548a", "sha256": "8630bb5f998cd72345074a2965cdf2d0d0d141ed8e06e8ff3b778f7faabfc2b3" }, "downloads": -1, "filename": "mongopersist-0.7.6.zip", "has_sig": false, "md5_digest": "2286d867c9f59803476b6b40a916548a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 95530, "upload_time": "2013-02-08T10:51:41", "url": "https://files.pythonhosted.org/packages/38/ce/d04c585cb8203a28614386f94539b69efaf41d881be8112084ef9f855470/mongopersist-0.7.6.zip" } ], "0.7.7": [ { "comment_text": "", "digests": { "md5": "5400ca44b856b5bc06dc06d9c981cf26", "sha256": "bbd45ca0c434ce1279212b4a243b7f03867214e20032c66f1c5c44089c7f9d0b" }, "downloads": -1, "filename": "mongopersist-0.7.7.zip", "has_sig": false, "md5_digest": "5400ca44b856b5bc06dc06d9c981cf26", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 95894, "upload_time": "2013-02-08T14:33:48", "url": "https://files.pythonhosted.org/packages/92/50/c9cedea8b0d9f57806f6c40ad2fdfd42c3d6850932ea15957f7390d1340f/mongopersist-0.7.7.zip" } ], "0.8.0": [ { "comment_text": "", "digests": { "md5": "5ee427e0e09cd5773cce124260f2339d", "sha256": "e040c5fd4e60b001098b0a1f841977ee6819355b9418070dbceba42b3824058e" }, "downloads": -1, "filename": "mongopersist-0.8.0.tar.gz", "has_sig": false, "md5_digest": "5ee427e0e09cd5773cce124260f2339d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 77401, "upload_time": "2013-02-09T05:29:47", "url": "https://files.pythonhosted.org/packages/88/b0/0c2285f16b6445f5bf5974d9a4c640b4f86d15f8b97230e6df6e11de9c75/mongopersist-0.8.0.tar.gz" } ], "0.8.1": [ { "comment_text": "", "digests": { "md5": "b55532d5f84396fada5217e308063240", "sha256": "2610eb1f9c9e746adc3b857aaf57b5764b1252ad02da3c7e29c96a92ed8beab3" }, "downloads": -1, "filename": "mongopersist-0.8.1.zip", "has_sig": false, "md5_digest": "b55532d5f84396fada5217e308063240", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 99446, "upload_time": "2013-03-19T10:25:51", "url": "https://files.pythonhosted.org/packages/2a/c2/509410c1ee9f94d37bece1b83048314972283be74864e1a8ade964469f29/mongopersist-0.8.1.zip" } ], "0.8.2": [ { "comment_text": "", "digests": { "md5": "9df87fd1bd86f97ce6158f5a1d2e5188", "sha256": "7384dfbb2dd9fa1b029f5048047eb281e2be309cf883e67b7fefa948bd87517e" }, "downloads": -1, "filename": "mongopersist-0.8.2.zip", "has_sig": false, "md5_digest": "9df87fd1bd86f97ce6158f5a1d2e5188", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 99608, "upload_time": "2013-04-03T13:24:36", "url": "https://files.pythonhosted.org/packages/19/7b/349b36fc65a96470ad371aaab997adb3be25038eaec684822c6eae9c983b/mongopersist-0.8.2.zip" } ], "0.8.3": [ { "comment_text": "", "digests": { "md5": "967cbf863ece5592afa67045e959e13f", "sha256": "1b499aa06597d57eacddb7340510f87a53d717e45fdbf893ab83932544c6ecae" }, "downloads": -1, "filename": "mongopersist-0.8.3.zip", "has_sig": false, "md5_digest": "967cbf863ece5592afa67045e959e13f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 100769, "upload_time": "2013-04-09T14:48:32", "url": "https://files.pythonhosted.org/packages/1d/94/3bc31533fa18dc13e029971a9b6aaacd5b301205ba49cee872ba98aa6391/mongopersist-0.8.3.zip" } ], "0.8.4": [ { "comment_text": "", "digests": { "md5": "269243fee2b44802a29b003d0296612b", "sha256": "79ac93173737c45995c594864e4e17acfc05ba717728802df3033611919c9fae" }, "downloads": -1, "filename": "mongopersist-0.8.4.zip", "has_sig": false, "md5_digest": "269243fee2b44802a29b003d0296612b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 102226, "upload_time": "2013-06-13T12:20:04", "url": "https://files.pythonhosted.org/packages/17/6d/40065e52544cc94220b701f87a7280b53a82bdbc9736bc4669e6a3211136/mongopersist-0.8.4.zip" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "269243fee2b44802a29b003d0296612b", "sha256": "79ac93173737c45995c594864e4e17acfc05ba717728802df3033611919c9fae" }, "downloads": -1, "filename": "mongopersist-0.8.4.zip", "has_sig": false, "md5_digest": "269243fee2b44802a29b003d0296612b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 102226, "upload_time": "2013-06-13T12:20:04", "url": "https://files.pythonhosted.org/packages/17/6d/40065e52544cc94220b701f87a7280b53a82bdbc9736bc4669e6a3211136/mongopersist-0.8.4.zip" } ] }