{ "info": { "author": "Jordan Hewitt", "author_email": "srcrr@opayq.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 2 - Pre-Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "historical_collection\n=====================\n\n.. image:: https://img.shields.io/pypi/v/historical_collection.svg\n :target: https://pypi.python.org/pypi/historical_collection\n :alt: Latest PyPI version\n\n.. image:: https://travis-ci.org/srcrr/historical_collection.png\n :target: https://travis-ci.org/srcrr/historical_collection\n :alt: Latest Travis CI build status\n\nEasily keep track of changes to Mongo document changes with a collection.\n\nConcepts\n========\n\nHistoricalCollection\n--------------------\n\nAn ``HistoricalCollection`` behaves just like a regular MongoDB\n``Collection``, but adds additional fields and methods to apply\npatching.\n\nPatching\n--------\n\nThis is most likely something you will not need to worry about since the\nlogic is taken care of. However\u2026\n\nA ``Patch`` is a set of changes. A patch is associated with a\n``Document`` of an ``HistoricalCollection``.\n\nRevision\n--------\n\nA ``Revision`` is a state of a document with a patch applied. Only the\nbase ``Revision`` is stored in the document table. Note (for\n``historical_collection`` developers): ``Revision`` is more of a concept\nthan an actual object.\n\nYou won\u2019t find yourself actively creating ``Revision``\\ s (remember,\nyou\u2019re creating ``Patch``\\ es). They\u2019re only retrieved.\n\nChange\n------\n\nJust like a patch, this is most likely something you don\u2019t need to worry\nabout. Changes are calculated for you.\n\nA change consists of a dict of actions. The actions are one of:\n\n- ``INITIAL`` (character key ``I``)\n- ``ADD`` (character key ``A``)\n- ``REMOVE`` (character key ``R``)\n- ``UPDATE`` (character key ``U``)\n\n``INITIAL`` actions do not not have any attributes. The ``Document``\nstored in the associated ``Collection`` is the all the information\nneeded. The ``INITIAL`` is stored as a revision for any metadata\nassociated with the patch.\n\n``ADD`` and ``UPDATE`` actions take a Python ``dict`` of keys mapped to\nvalues. Obviously any of the ``PK_FIELD``\\ s will not be included in\nthis.\n\n``REMOVE`` takes a list of document keys to be removed. Just like\n``ADD`` and ``UPDATE``, no ``PK_FIELD``\\ s are in this list.\n\nRequirements\n============\n\nBasic Requirements\n------------------\n\n- Python 3.6 or higher\n- MongoDB\n- pymongo\n\nOptionally, you may want ``pip`` and to run this in a virtual\nenvironment.\n\nRequirements For Development or Testing\n---------------------------------------\n\n- Faker\n\nInstallation\n============\n\nTo install from PIP\n\n::\n\n user@host~# pip3 install -U historical_collection\n\nOr clone the repostitory and execute:\n\n::\n\n user@host~# python3 setup.py install\n\nUsage\n=====\n\nExtend the Collection\n---------------------\n\nIn order to keep track of changes to a document, extend\nHistoricalCollection.\n\n::\n\n from historical_collection.historical import HistoricalCollection\n from pymongo import MongoClient\n class Users(HistoricalCollection):\n PK_FIELDS = ['username', ] # <<= This is the only requirement\n\nThe only requirement is the ``PK_FIELDS`` attribute that specifies the\nprimary keys of the document. If omitted, Python will complain. This is\nso that any incoming document can be seen as \u201cthe same.\u201d\n\nIt\u2019s recommended to *not* use the ``_id`` field unless you have a valid\nreason, and if you have a mechanism in place to keep track of the\n``_id``. Otherwise, the changes will most likely be ignored.\n\nPerhaps an example of how to patch will make it more clear.\n\nConnect to a Mongo Database Instance\n------------------------------------\n\n::\n\n CLIENT_URL = \"mongodb://localhost:27017/\"\n DATABASE = \"historical_collection_example\"\n mongo = MongoClient(CLIENT_URL)\n db = mongo[DATABASE]\n\n mongo.drop_database(db)\n\n users = Users(database=db)\n\nThere\u2019s a lot going on under the hood with the line\n``users = Users(database=db)``. We\u2019re also creating a ``deltas``\ncollection with the format ``__deltas_User``. Usually you will not need\nto access the deltas collection, but if you do, then you can always\naccess it with ``._deltas_collection``:\n\n::\n\n users._deltas_collection\n\n\n\n\n::\n\n Collection(Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'historical_collection_example'), '__deltas_Users')\n\n\n\nPatching Documents\n------------------\n\nLet\u2019s add an initial user to your document. You\u2019re probably already\nfamiliar with ``Collection.insert_one()`` and\n``Collection.insert_many()``. Well, ``HistoricalCollection`` has 2\nadditional methods for inserting:\n\n- ``HistoricalCollection.patch_one()``\n- ``HistoricalCollection.patch_many()``\n\nThey behave similarly to ``insert_one`` and ``insert_many`` with one\nmajor difference: Only the first ``Document`` is inserted. Additional\ndocuments have deltas generated and stored in the ``_deltas_collection``\ncollection.\n\nLet\u2019s patch our first document.\n\n::\n\n users.patch_one({\"email\": \"darth_later@example2.com\"})\n\n\n::\n\n\n ---------------------------------------------------------------------------\n\n KeyError Traceback (most recent call last)\n\n ~/projects/historical_collection/historical_collection/historical.py in _document_filter(self, document)\n 134 try:\n --> 135 return dict([(k, document[k]) for k in self.PK_FIELDS])\n 136 except KeyError as e:\n\n\n ~/projects/historical_collection/historical_collection/historical.py in (.0)\n 134 try:\n --> 135 return dict([(k, document[k]) for k in self.PK_FIELDS])\n 136 except KeyError as e:\n\n\n KeyError: 'username'\n\n\n During handling of the above exception, another exception occurred:\n\n\n KeyError Traceback (most recent call last)\n\n in \n ----> 1 users.patch_one({\"email\": \"darth_later@example2.com\"})\n\n\n ~/projects/historical_collection/historical_collection/historical.py in patch_one(self, *args, **kwargs)\n 258 doc = args[0]\n 259 metadata = kwargs.pop(\"metadata\", None)\n --> 260 fltr = self._document_filter(doc)\n 261 latest = self.latest(fltr)\n 262 insert_result = None\n\n\n ~/projects/historical_collection/historical_collection/historical.py in _document_filter(self, document)\n 138 raise KeyError(\n 139 \"Perhaps you forgot to include {} in projection?\".format(\n --> 140 self.PK_FIELDS\n 141 )\n 142 )\n\n\n KeyError: \"Perhaps you forgot to include ['username'] in projection?\"\n\n\nWhoopsie! That\u2019s right! We need to include the ``username`` field!\n\n::\n\n users.patch_one({\"username\": \"darth_later\", \"email\": \"darthlater@example.com\"})\n users.find_one({\"username\": \"darth_later\"})\n\n\n\n\n::\n\n {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com'}\n\n\n\nOkay, now let\u2019s patch it! For starters let\u2019s simply add a field.\n\n::\n\n users.patch_one({\"username\": \"darth_later\", \"email\": \"darthlater@example.com\", \"laser_sword_color\": \"red\"})\n\n\n\n\n::\n\n []\n\n\n\n**One Important Thing to Note:** We need to keep everything from the\n*previous* example, in that, we must include the ``username`` field\n(otherwise, the ``Users`` collection will not find ``darth_vader``) and\nthe ``email`` (otherwise, this will be seen as a ``REMOVE``-al).\n\n::\n\n users.find_one({\"username\": \"darth_later\"})\n\n\n\n\n::\n\n {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com'}\n\n\n\nWhat? What happened? We patched ``darth_vader``, didn\u2019t we?\n\nYes, we did. So the first (and only) ``Document`` stored in the\n``Users`` ``Document`` is the first one. But we do have several\nrevisions. These can be retrieved with the ``revisions()`` function.\nThis behaves just like ``find_all()`` for a standard ``Collection``.\n\n::\n\n list(users.revisions({\"username\": \"darth_later\"}))\n\n\n\n\n::\n\n [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com',\n '_revision_metadata': None},\n {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com',\n '_revision_metadata': None,\n 'laser_sword_color': 'red'}]\n\n\n\nThere we go! There\u2019s the revision we were looking for! This may be\nannoying, though to get all revisions when you most likely just want the\nlatest one. That\u2019s why there\u2019s a ``latest()`` method to make it easy.\n\n::\n\n users.latest({\"username\": \"darth_later\"})\n\n\n\n\n::\n\n {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com',\n '_revision_metadata': None,\n 'laser_sword_color': 'red'}\n\n\n\nNote that this assumes one document. If you want the latest revision of\n*several* documents, use ``find_latest()``\n\n::\n\n list(users.find_latest({\"username\": \"darth_later\"}))\n\n\n\n\n::\n\n [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),\n 'username': 'darth_later',\n 'email': 'darthlater@example.com',\n '_revision_metadata': None,\n 'laser_sword_color': 'red'}]\n\n\n\nThose curious may have noticed a ``_revision_metadata`` element in the\ndocument. That\u2019s added by ``HistoricalCollection`` in the\n``_deltas_collection`` for any additional data that you want to\nassociate with the document. Timestamps are an excellent usage case.\n\nLet\u2019s start over with no users to show an example.\n\n::\n\n mongo.drop_database(DATABASE)\n\n::\n\n from datetime import datetime\n from time import sleep\n import random\n\n SWORD_COLORS='red blue orange green transparent'.split(' ')\n\n for i in range(0, 5):\n timestamp = datetime.now()\n laser_sword_color = random.choice(SWORD_COLORS)\n document = {\"username\": \"darth_later\", \"laser_sword_color\": laser_sword_color}\n metadata = {\"timestamp\": timestamp}\n users.patch_one(document, metadata=metadata)\n sleep(1)\n\n list(users.revisions({\"username\": \"darth_later\"}))\n\n\n\n\n::\n\n [{'_id': ObjectId('5d98c3435d8edadaf0bb845e'),\n 'username': 'darth_later',\n 'laser_sword_color': 'green',\n '_revision_metadata': {'timestamp': datetime.datetime(2019, 10, 5, 9, 22, 27, 994000)}},\n {'_id': ObjectId('5d98c3435d8edadaf0bb845e'),\n 'username': 'darth_later',\n 'laser_sword_color': 'orange',\n '_revision_metadata': {'timestamp': datetime.datetime(2019, 10, 5, 9, 22, 29, 26000)}},\n {'_id': ObjectId('5d98c3435d8edadaf0bb845e'),\n 'username': 'darth_later',\n 'laser_sword_color': 'blue',\n '_revision_metadata': {'timestamp': datetime.datetime(2019, 10, 5, 9, 22, 30, 29000)}},\n {'_id': ObjectId('5d98c3435d8edadaf0bb845e'),\n 'username': 'darth_later',\n 'laser_sword_color': 'blue',\n '_revision_metadata': {'timestamp': datetime.datetime(2019, 10, 5, 9, 22, 31, 31000)}},\n {'_id': ObjectId('5d98c3435d8edadaf0bb845e'),\n 'username': 'darth_later',\n 'laser_sword_color': 'green',\n '_revision_metadata': {'timestamp': datetime.datetime(2019, 10, 5, 9, 22, 32, 33000)}}]", "description_content_type": "text/x-rst", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://gitlab.com/srcrr/historical_collection", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "historical-collection", "package_url": "https://pypi.org/project/historical-collection/", "platform": "", "project_url": "https://pypi.org/project/historical-collection/", "project_urls": { "Homepage": "https://gitlab.com/srcrr/historical_collection" }, "release_url": "https://pypi.org/project/historical-collection/0.1.0/", "requires_dist": null, "requires_python": "", "summary": "Easily keep track of changes to Mongo document changes with a collection.", "version": "0.1.0" }, "last_serial": 5932494, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "3dc067eb5f2f28896f31a04934a5c8d0", "sha256": "9f79223bb475371e750bcc777a9f778696588aebed1383056bf414a0bb1250da" }, "downloads": -1, "filename": "historical_collection-0.1.0.tar.gz", "has_sig": false, "md5_digest": "3dc067eb5f2f28896f31a04934a5c8d0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9692, "upload_time": "2019-10-05T16:51:38", "url": "https://files.pythonhosted.org/packages/b3/15/b3053a0e397bde08730e987fcb0106ba09595eb65154edc7fb25538e0832/historical_collection-0.1.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "3dc067eb5f2f28896f31a04934a5c8d0", "sha256": "9f79223bb475371e750bcc777a9f778696588aebed1383056bf414a0bb1250da" }, "downloads": -1, "filename": "historical_collection-0.1.0.tar.gz", "has_sig": false, "md5_digest": "3dc067eb5f2f28896f31a04934a5c8d0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9692, "upload_time": "2019-10-05T16:51:38", "url": "https://files.pythonhosted.org/packages/b3/15/b3053a0e397bde08730e987fcb0106ba09595eb65154edc7fb25538e0832/historical_collection-0.1.0.tar.gz" } ] }