{ "info": { "author": "Shoobx Team", "author_email": "dev@shoobx.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "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", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "=================================\nPostGreSQL/JSONB Data Persistence\n=================================\n\nThis document outlines the general capabilities of the ``pjpersist``\npackage. ``pjpersist`` is a PostGreSQL/JSONB storage implementation for\npersistent Python objects. It is *not* a storage for the ZODB.\n\nThe goal of ``pjpersist`` is to provide a data manager that serializes\nobjects to JSONB blobs at transaction boundaries. The PJ 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 ``psycopg.Connection`` instance. In this case\nour tests use the ``pjpersist_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(2014, 5, 14, 12, 30)\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 PJ:\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 Tables\n-------------------------\n\nBy default, persistent objects are stored in a table having the escaped\nPython path of the class:\n\n >>> from pjpersist import serialize\n >>> person_cn = serialize.get_dotted_name(Person, True)\n >>> person_cn\n 'u__main___dot_Person'\n\n >>> transaction.commit()\n >>> dumpTable(person_cn)\n [{'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan',\n u'phone': None,\n u'today': {u'_py_type': u'datetime.datetime',\n u'value': u'2014-05-14T12:30:00.000000'},\n u'visited': []},\n 'id': u'0001020304050607080a0b0c0'}]\n\n\nAs you can see, the stored document for the person looks very much like a\nnatural JSON document. But oh no, I forgot to specify the full name for\nStephan. Let's do that:\n\n >>> dm.root['stephan'].name = u'Stephan Richter'\n >>> dm.root['stephan']._p_changed\n True\n\nThis time, the data is not automatically saved:\n\n >>> fetchone(person_cn)['data']['name']\n u'Stephan'\n\nSo we have to commit the transaction first:\n\n >>> dm.root['stephan']._p_changed\n True\n >>> transaction.commit()\n >>> dm.root['stephan']._p_changed\n >>> fetchone(person_cn)['data']['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_pj_table = '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\npjpersist supports a special attribute called ``_p_pj_table``,\nwhich allows you to specify a custom table 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 >>> dumpTable('address', isolate=True)\n relation \"address\" does not exist\n ...\n\nBut once we commit the transaction, everything is available:\n\n >>> transaction.commit()\n >>> dumpTable('address')\n [{'data': {u'_py_persistent_type': u'__main__.Address',\n u'city': u'Maynard',\n u'zip': u'01754'},\n 'id': u'0001020304050607080a0b0c0'}]\n\n >>> dumpTable(person_cn)\n [{'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'address'},\n u'birthday': None,\n u'friends': {},\n u'name': u'Stephan Richter',\n u'phone': None,\n u'today': {u'_py_type': u'datetime.datetime',\n u'value': u'2014-05-14T12:30:00.000000'},\n u'visited': []},\n 'id': u'0001020304050607080a0b0c0'}]\n\n >>> dm.root['stephan'].address\n
\n\n\nNon-Persistent Objects\n----------------------\n\nAs you can see, even the reference looks nice and all components are easily\nvisible. 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 JSONB document again:\n\n >>> transaction.commit()\n >>> dm.root['stephan'].phone\n \n\n >>> dumpTable(person_cn)\n [{'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'address'},\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': {u'_py_type': u'datetime.datetime',\n u'value': u'2014-05-14T12:30:00.000000'},\n u'visited': []},\n 'id': u'0001020304050607080a0b0c0'}]\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 JSON does not have two sequence types.\n\n >>> import pprint\n >>> pprint.pprint(dict(\n ... fetchone(person_cn, \"\"\"data @> '{\"name\": \"Stephan Richter\"}'\"\"\")))\n {'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'address'},\n u'birthday': {u'_py_type': u'datetime.date',\n u'value': u'1980-01-25'},\n u'friends': {u'roy': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'u__main___dot_Person'}},\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': {u'_py_type': u'datetime.datetime',\n u'value': u'2014-05-14T12:30:00.000000'},\n u'visited': [u'Germany', u'USA']},\n 'id': u'0001020304050607080a0b0c0'}\n\n\nCustom Serializers\n------------------\n\n(A patch to demonstrate)\n\n >>> dm.root['stephan'].birthday = datetime.date(1981, 1, 25)\n >>> transaction.commit()\n\n >>> pprint.pprint(\n ... fetchone(person_cn,\n ... \"\"\"data @> '{\"name\": \"Stephan Richter\"}'\"\"\")['data']['birthday'])\n {u'_py_type': u'datetime.date', u'value': u'1981-01-25'}\n\nAs you can see, the serialization of the birthay is an ISO string. 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') == 'custom_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': 'custom_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(1981, 1, 25)\n\n >>> pprint.pprint(dict(\n ... fetchone(person_cn, \"\"\"data @> '{\"name\": \"Stephan Richter\"}'\"\"\")))\n {'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'address'},\n u'birthday': {u'_py_type': u'custom_date', u'ordinal': 723205},\n u'friends': {u'roy': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'u__main___dot_Person'}},\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': {u'_py_type': u'custom_date', u'ordinal': 735367},\n u'visited': [u'Germany', u'USA']},\n 'id': u'0001020304050607080a0b0c0'}\n\nMuch better!\n\n >>> del serialize.SERIALIZERS[:]\n\n\nPersistent Objects as Sub-Documents\n-----------------------------------\n\nIn order to give more control over which objects receive their own tables\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_pj_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_pj_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(dict(\n ... fetchone(person_cn, \"\"\"data @> '{\"name\": \"Stephan Richter\"}'\"\"\")))\n {'data': {u'_py_persistent_type': u'__main__.Person',\n u'address': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'address'},\n u'birthday': {u'_py_type': u'datetime.date',\n u'value': u'1981-01-25'},\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': {u'_py_type': u'DBREF',\n u'database': u'pjpersist_test',\n u'id': u'0001020304050607080a0b0c0',\n u'table': u'u__main___dot_Person'}},\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': {u'_py_type': u'datetime.date', u'value': u'2014-05-14'},\n u'visited': [u'Germany', u'USA']},\n 'id': u'0001020304050607080a0b0c0'}\n\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\nTable Sharing\n-------------\n\nSince PostGreSQL/JSONB is so flexible, it sometimes makes sense to store\nmultiple types of (similar) objects in the same table. In those cases you\ninstruct the object 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 table sharing, you simply create another class\nthat has the same ``_p_pj_table`` string as another (sub-classing will\nensure that).\n\nSo let's give Stephan two extended addresses now.\n\n >>> dm.root['stephan'].address2 = ExtendedAddress(\n ... 'Tettau', '01945', 'Germany')\n >>> dm.root['stephan'].address2\n \n\n >>> dm.root['stephan'].address3 = ExtendedAddress(\n ... 'Arnsdorf', '01945', 'Germany')\n >>> dm.root['stephan'].address3\n \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 >>> dm.root['stephan'].address3\n \n\n\nPersistent Serialization Hooks\n------------------------------\n\nWhen persistent components implement the ``IPersistentSerializationHooks``, it\nis possible for the object to conduct some custom storage function.\n\n\n >>> from pjpersist.persistent import PersistentSerializationHooks\n >>> class Usernames(PersistentSerializationHooks):\n ... _p_pj_table = 'usernames'\n ... format = 'email'\n ...\n ... def _pj_after_store_hook(self, conn):\n ... print('After Store Hook')\n ...\n ... def _pj_after_load_hook(self, conn):\n ... print('After Load Hook')\n\nWhen we store the object, the hook is called:\n(actually twice, because this is a new object)\n\n >>> dm.root['stephan'].usernames = Usernames()\n >>> transaction.commit()\n After Store Hook\n After Store Hook\n\nWhen loading, the same happens:\n\n >>> dm.root['stephan'].usernames.format\n After Load Hook\n 'email'\n\nThe store hook fires just once if the object is not new:\n\n >>> dm.root['stephan'].usernames.format = 'snailmail'\n >>> transaction.commit()\n After Store Hook\n\n\nColumn Serialization\n--------------------\n\npjpersist also allows for the object to specify values, usually attributes or\nproperties, to be stored as columns on the object's storage table.\n\nNote that we support only a one-way transformation, because object state\nwill be always deserialized from the ``data`` jsonb field.\n\n >>> import zope.schema\n >>> class IPerson(zope.interface.Interface):\n ...\n ... name = zope.schema.TextLine(title=u'Name')\n ... address = zope.schema.TextLine(title=u'Address')\n ... visited = zope.schema.Datetime(title=u'Visited')\n ... phone = zope.schema.TextLine(title=u'Phone')\n\nInitially, we are storing only the name in a column:\n\n >>> from pjpersist.persistent import SimpleColumnSerialization, select_fields\n >>> @zope.interface.implementer(IPerson)\n ... class ColumnPerson(SimpleColumnSerialization, Person):\n ... _p_pj_table = 'cperson'\n ... _pj_column_fields = select_fields(IPerson, 'name')\n\nSo once I create such a person and commit the transaction, the person table is\nextended to store the attribute and the person is added to the table:\n\n >>> dm.root['anton'] = anton = ColumnPerson(u'Anton')\n >>> transaction.commit()\n\n >>> dumpTable('cperson')\n [{'data': {u'_py_persistent_type': u'__main__.ColumnPerson',\n u'address': None,\n u'birthday': None,\n u'friends': {},\n u'name': u'Anton',\n u'phone': None,\n u'today': {u'_py_type': u'datetime.datetime',\n u'value': u'2014-05-14T12:30:00.000000'},\n u'visited': []},\n 'id': u'0001020304050607080a0b0c0',\n 'name': u'Anton'}]\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: <...>\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 Tables\n---------------------\n\nNow that we have talked so much about the gory details on storing one object,\nwhat about mappings that reflect an entire table, for example a\ntable of people.\n\nThere are many approaches that can be taken. The following implementation\ndefines an attribute in the document as the mapping key and names a\ntable:\n\n >>> from pjpersist import mapping\n >>> class People(mapping.PJTableMapping):\n ... __pj_table__ = person_cn\n ... __pj_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\n=======\nCHANGES\n=======\n\n\n1.7.1 (2019-06-19)\n------------------\n\n- Fixed an edge case when the serializer gets a mapping with a key `dict_data`.\n Reading such object failed.\n\n- Fixed an edge case with the serializer, when an object's state living\n in a persistent object became 'empty'. Basically the state was just\n `{'_py_persistent_type': 'SomeClass'}`\n `SomeClass.__setstate__` was not called, thus the object could miss\n attributes. Like a subclass of `UserDict` would miss the `data` attribute.\n\n- Removed checking for 0x00 chars in dict keys. Turns out PostGreSQL just\n can not store 0x00.\n\n1.7.0 (2019-05-29)\n------------------\n\n- Support for sub-second datetime and time resolution during serialization.\n\n- Add `use_cache` argument to `PJContainer._load_one()` to support ignoring\n the cache. (This became handy if a container keeps track of multiple\n versions of an item and you try to load all old revisions.)\n\n\n1.6.0 (2019-05-29)\n------------------\n\n- Make `id` and `data` column name configurable via `_pj_id_column` and\n `_pj_data_column` attributes in `PJContainer`, respectively.\n\n- Auto-assign a name to objects when using `PJContainer`, not just\n `IdNamesPJContainer`.\n\n\n1.5.0 (2018-10-10)\n------------------\n\n- Support for Python 3.7. Removed Python 3.5 testing from tox.\n\n\n1.4.1 (2018-09-13)\n------------------\n\n- No need to log in tpc_finish.\n\n\n1.4.0 (2018-09-13)\n------------------\n\n- Implemented skipping tpc_prepare when DM has no writes.\n We found out that AWS Aurora is dog slow at the moment on tpc_prepare.\n When the DataManager has no writes, there's no need to call tpc_prepare.\n See `CALL_TPC_PREPARE_ON_NO_WRITE_TRANSACTION`, by default True for backwards\n compatibility.\n\n- Added ability to log whether the transaction had writes.\n See `LOG_READ_WRITE_TRANSACTION`, by default False\n\n\n1.3.2 (2018-04-19)\n------------------\n\n- More precise flushing of datamanager to avoid unnecessary database\n writes.\n\n\n1.3.1 (2018-04-11)\n------------------\n\n- Enabled concurrent adds to IdNamesPJContainer by eliminating a query\n that was causing transaction conflicts.\n\n1.3.0 (2018-03-22)\n------------------\n\n- Python 3 compatibility fixes\n- More efficient PJContainer.values() implementation\n\n\n1.2.2 (2017-12-12)\n------------------\n\n- Need to protect all DB calls against `DatabaseDisconnected`\n\n\n1.2.1 (2017-12-12)\n------------------\n\n- `psycopg2.OperationalError` and `psycopg2.InterfaceError` will be caught\n on SQL command execution and reraised as `DatabaseDisconnected`\n\n\n1.2.0 (2017-10-24)\n------------------\n\n- Added a new helper function to link subobject to main doc object. This is\n needed when a custom `__getstate__()` and `__setstate__()` is implemented. A\n detailed example is provided.\n\n- Implemented `flush_hint` argument for `IDataManager.execute()` to allow\n flushing only some objects during query. `flush_hints` is a list table names\n that need to be flushed for the query to return a correct result.\n\n- The Zope-specific containers use the `flush_hint` to only flush objects they\n manage when a query is run on the container.\n\n- While flushing objects, every main document object is now only flushed\n once. Before that fix, any subobject would cause its doc object to be dumped\n again.\n\nNote: These optimizations provide a 15% performance improvements in real-world\napplications.\n\n\n1.1.2 (2017-09-14)\n------------------\n\n- Make sure changed objects aren't `_p_changed` anymore after commit.\n\n\n1.1.1 (2017-07-03)\n------------------\n\n- Nothing changed yet.\n\n\n1.0.0 (2017-03-18)\n------------------\n\n- Initial Public Release\n\n- Project forked from mongopersist to work with PostGreSQL and JSONB data\n type. The main motiviation is the ability to utilize PostGreSQL's great\n transactional support.", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/Shoobx/pjpersist", "keywords": "postgres jsonb persistent", "license": "ZPL 2.1", "maintainer": "", "maintainer_email": "", "name": "pjpersist", "package_url": "https://pypi.org/project/pjpersist/", "platform": "", "project_url": "https://pypi.org/project/pjpersist/", "project_urls": { "Homepage": "https://github.com/Shoobx/pjpersist" }, "release_url": "https://pypi.org/project/pjpersist/1.7.1/", "requires_dist": null, "requires_python": "", "summary": "PostgreSQL/JSONB Persistence Backend", "version": "1.7.1" }, "last_serial": 5420533, "releases": { "1.0.0": [ { "comment_text": "", "digests": { "md5": "caf134a1c906c576f998e90786e5f0c1", "sha256": "7ab8519dcf514f0df4a7b9fe6d4da5d36ae2018dd1e374726a414725d1c4ea23" }, "downloads": -1, "filename": "pjpersist-1.0.0.tar.gz", "has_sig": false, "md5_digest": "caf134a1c906c576f998e90786e5f0c1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 108534, "upload_time": "2017-03-18T15:21:09", "url": "https://files.pythonhosted.org/packages/c0/68/b4072f509a6084afad7b91d1c5a4cd7e9ee01da3af27e491c00fd04ef8f5/pjpersist-1.0.0.tar.gz" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "1c6b44ddb314e7abcf9b1b550887af45", "sha256": "3264fe265fb5e82c3c3a8886bf37bcdaab1bf9737ceea3056c6bc81484e837f7" }, "downloads": -1, "filename": "pjpersist-1.1.0.tar.gz", "has_sig": false, "md5_digest": "1c6b44ddb314e7abcf9b1b550887af45", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110110, "upload_time": "2017-05-22T11:56:17", "url": "https://files.pythonhosted.org/packages/15/50/d00fdcf4f79dad479147a67bae953a53fdd7fd8b924d29601773e73c9944/pjpersist-1.1.0.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "48bb7b85c53fa84763cbd7af93448257", "sha256": "128aa122ebc2f2b3796aa33d7dc0e23efb80ba921d1db9142914752af2dd7b33" }, "downloads": -1, "filename": "pjpersist-1.1.1.tar.gz", "has_sig": false, "md5_digest": "48bb7b85c53fa84763cbd7af93448257", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 108865, "upload_time": "2017-07-03T11:52:06", "url": "https://files.pythonhosted.org/packages/ab/f1/ccaaba96e1f3bbf7975adb82c8d393cbb2601f1a2ed9472c8c20fb996d1c/pjpersist-1.1.1.tar.gz" } ], "1.1.2": [ { "comment_text": "", "digests": { "md5": "5905836f52afb5764125c68033c24d04", "sha256": "8ee616adf36b199fc49bbd9e234f656ca113701354acfc5a9011135981303129" }, "downloads": -1, "filename": "pjpersist-1.1.2.tar.gz", "has_sig": false, "md5_digest": "5905836f52afb5764125c68033c24d04", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110039, "upload_time": "2017-09-14T10:49:44", "url": "https://files.pythonhosted.org/packages/2f/53/3a8b14e88052780d0aa227d39d470505e47f142e116a9adf430eee4ac952/pjpersist-1.1.2.tar.gz" } ], "1.1.2.dev0": [ { "comment_text": "", "digests": { "md5": "61c4613e93d8dd5078c2f02a72222b4c", "sha256": "32a1a07d1126e18159ffcc9e25c7d8961d0dd2aa33f620f22ce3dc6bc8ef65a2" }, "downloads": -1, "filename": "pjpersist-1.1.2.dev0.tar.gz", "has_sig": false, "md5_digest": "61c4613e93d8dd5078c2f02a72222b4c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110063, "upload_time": "2017-09-14T10:48:21", "url": "https://files.pythonhosted.org/packages/03/4e/8994965ae257a7f07f706b844e8de1c3d9afd3a5c8cd37dc67537d8e01ec/pjpersist-1.1.2.dev0.tar.gz" } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "2171cdc297999ba1a6a8b5328b9ab3b5", "sha256": "fc0e65e58029bf98e4996c799e9f12ed79c554cee55212c24829fe70c27e54cd" }, "downloads": -1, "filename": "pjpersist-1.2.0.tar.gz", "has_sig": false, "md5_digest": "2171cdc297999ba1a6a8b5328b9ab3b5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110902, "upload_time": "2017-10-25T00:37:21", "url": "https://files.pythonhosted.org/packages/db/0d/d3c52c6092b25a62516da990f54eea04ecc293177d4b12b52be045116071/pjpersist-1.2.0.tar.gz" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "50594c9b99c367b26f4adf3608cf73c9", "sha256": "2f5be5443dd3ecb34a70a2794fc93eb405e598ff32ba9b194beb7d218424caae" }, "downloads": -1, "filename": "pjpersist-1.2.1.tar.gz", "has_sig": false, "md5_digest": "50594c9b99c367b26f4adf3608cf73c9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 111027, "upload_time": "2017-12-12T10:04:39", "url": "https://files.pythonhosted.org/packages/34/ab/7327da70f35969430cfa4e2600f873ff0032df659b8daa47384d09c2afe8/pjpersist-1.2.1.tar.gz" } ], "1.2.2": [ { "comment_text": "", "digests": { "md5": "dec291768c88c4624a8c234db6909e01", "sha256": "43f2b481c99661d84ad81b8f61346f368b925c50975daa5389b32b905384d13c" }, "downloads": -1, "filename": "pjpersist-1.2.2.tar.gz", "has_sig": false, "md5_digest": "dec291768c88c4624a8c234db6909e01", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 111255, "upload_time": "2017-12-12T11:54:33", "url": "https://files.pythonhosted.org/packages/5a/5c/ca583f92c474dd7a13d83392c977271025bc12fd98322bf277b436992931/pjpersist-1.2.2.tar.gz" } ], "1.3.0": [ { "comment_text": "", "digests": { "md5": "58f551145e930a4dc6087dba1ee91b70", "sha256": "f9431b06fa68da2d2d9a10a9c7636d697100d7deee8bcaf3b57c40b4b393a170" }, "downloads": -1, "filename": "pjpersist-1.3.0.tar.gz", "has_sig": false, "md5_digest": "58f551145e930a4dc6087dba1ee91b70", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 113723, "upload_time": "2018-03-22T15:43:21", "url": "https://files.pythonhosted.org/packages/8c/75/a5d6f4dd51ec3632e1399ab02d065d52cb96a6df37c24001fd6915f73aeb/pjpersist-1.3.0.tar.gz" } ], "1.3.1": [ { "comment_text": "", "digests": { "md5": "1831ed202d448eb2480b86dc9587b239", "sha256": "a85322cba110b3e72223b576a65584edbb6d649e4a0af2cc12904376017e8e7a" }, "downloads": -1, "filename": "pjpersist-1.3.1.tar.gz", "has_sig": false, "md5_digest": "1831ed202d448eb2480b86dc9587b239", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 114118, "upload_time": "2018-04-11T09:23:31", "url": "https://files.pythonhosted.org/packages/1f/47/a4dfb862784447a4c74c67caf7b5f803535b7822064e404be1fb6b1b907f/pjpersist-1.3.1.tar.gz" } ], "1.3.2": [ { "comment_text": "", "digests": { "md5": "e68ea497a81b054d04855195bb3744b7", "sha256": "6ea4ac3981b8b232e3fed58b42aef2003beed91850f133aebf26b100856e4e11" }, "downloads": -1, "filename": "pjpersist-1.3.2.tar.gz", "has_sig": false, "md5_digest": "e68ea497a81b054d04855195bb3744b7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 115111, "upload_time": "2018-04-19T17:36:22", "url": "https://files.pythonhosted.org/packages/db/7e/bf624ccbb5b5e74f1a84750cbc46972a01453100df067925acec034d81a7/pjpersist-1.3.2.tar.gz" } ], "1.3.3.dev0": [ { "comment_text": "", "digests": { "md5": "9e9d4a13b4e91cf0fa3b3c59be205b7a", "sha256": "b48619657e1c4aaa802a0b4e685cba794a322aaee88854e8a2ec0524a3d3cff2" }, "downloads": -1, "filename": "pjpersist-1.3.3.dev0.tar.gz", "has_sig": false, "md5_digest": "9e9d4a13b4e91cf0fa3b3c59be205b7a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 114105, "upload_time": "2018-09-13T06:45:21", "url": "https://files.pythonhosted.org/packages/97/a4/5f2876ee79bb1844c77f03d85252d679e06118626a9582e1e8d235c6ce14/pjpersist-1.3.3.dev0.tar.gz" } ], "1.4.0": [ { "comment_text": "", "digests": { "md5": "011eab51379a1c598713c07777c78858", "sha256": "ce26703e15f9df4dc8ea56f4579c4ad7771e31c4583c03da806ac7756c132f82" }, "downloads": -1, "filename": "pjpersist-1.4.0.tar.gz", "has_sig": false, "md5_digest": "011eab51379a1c598713c07777c78858", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 114851, "upload_time": "2018-09-13T12:03:28", "url": "https://files.pythonhosted.org/packages/86/8e/70a929e07b347b51758ce8fa4fe136d01eb3a8ef1a9d785dbe466b8ac4ec/pjpersist-1.4.0.tar.gz" } ], "1.4.1": [ { "comment_text": "", "digests": { "md5": "b5871de85028b4bfb858f044bb7d1261", "sha256": "3053fcbc0df21c51208f952bb0b5a627d32033fb7d52d13431c1ae56901b1d4b" }, "downloads": -1, "filename": "pjpersist-1.4.1.tar.gz", "has_sig": false, "md5_digest": "b5871de85028b4bfb858f044bb7d1261", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 114896, "upload_time": "2018-09-13T13:22:04", "url": "https://files.pythonhosted.org/packages/1c/49/3ba0e761635cbda3821dca5f0e8e089175749dbd472bbc0f6937c9a31bef/pjpersist-1.4.1.tar.gz" } ], "1.5.0": [ { "comment_text": "", "digests": { "md5": "5e620dfffadf4253971bfa2ec26e8f4d", "sha256": "39a6d1e2c2de4d27eeeb9d8c803169e9cf75496a45ad73c6dc0eea43dc805efb" }, "downloads": -1, "filename": "pjpersist-1.5.0.tar.gz", "has_sig": false, "md5_digest": "5e620dfffadf4253971bfa2ec26e8f4d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 116340, "upload_time": "2018-10-10T05:14:13", "url": "https://files.pythonhosted.org/packages/12/85/d573894c70f050fafa74a03b936023f5c84213647a09dca5b61c3077f8e1/pjpersist-1.5.0.tar.gz" } ], "1.6.0": [ { "comment_text": "", "digests": { "md5": "81c5c1a2f15e99822652ca146a449f58", "sha256": "353157d0d5bef92231c33e7c8701f4cf9608487d2d8a0356a880cc5f5c1558bb" }, "downloads": -1, "filename": "pjpersist-1.6.0.tar.gz", "has_sig": false, "md5_digest": "81c5c1a2f15e99822652ca146a449f58", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 116557, "upload_time": "2019-05-29T14:24:58", "url": "https://files.pythonhosted.org/packages/06/2d/101358d41c639e3e5add7a1bea3b4393a4200abdd58fd1df9708085b586c/pjpersist-1.6.0.tar.gz" } ], "1.7.0": [ { "comment_text": "", "digests": { "md5": "619e50694eb19a81157469fd11cb083a", "sha256": "0351b481b75e5aa3769fa1afe02f66715a7c3032698007c2c8819a33eb1e97cc" }, "downloads": -1, "filename": "pjpersist-1.7.0.tar.gz", "has_sig": false, "md5_digest": "619e50694eb19a81157469fd11cb083a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 117560, "upload_time": "2019-05-29T20:38:57", "url": "https://files.pythonhosted.org/packages/eb/37/c518a2e0def174bfa2c72a24dadec6763eb74580e8b19dd9466aaa00a86e/pjpersist-1.7.0.tar.gz" } ], "1.7.1": [ { "comment_text": "", "digests": { "md5": "daee8686f77f4abb946fa362a29afdd4", "sha256": "e910f719bdb213891e5693991ed87d5f7c75c355078689c9e96f8a21f6e5ae45" }, "downloads": -1, "filename": "pjpersist-1.7.1.tar.gz", "has_sig": false, "md5_digest": "daee8686f77f4abb946fa362a29afdd4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 119061, "upload_time": "2019-06-19T14:27:32", "url": "https://files.pythonhosted.org/packages/c4/2c/396c021d683978891873663ab6d6bae0dcf31e6a0fa6537d632182c52558/pjpersist-1.7.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "daee8686f77f4abb946fa362a29afdd4", "sha256": "e910f719bdb213891e5693991ed87d5f7c75c355078689c9e96f8a21f6e5ae45" }, "downloads": -1, "filename": "pjpersist-1.7.1.tar.gz", "has_sig": false, "md5_digest": "daee8686f77f4abb946fa362a29afdd4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 119061, "upload_time": "2019-06-19T14:27:32", "url": "https://files.pythonhosted.org/packages/c4/2c/396c021d683978891873663ab6d6bae0dcf31e6a0fa6537d632182c52558/pjpersist-1.7.1.tar.gz" } ] }