{ "info": { "author": "Jeremy Lewis", "author_email": "jlewis@nerdwallet.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "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", "Programming Language :: SQL", "Topic :: Database", "Topic :: Database :: Front-Ends", "Topic :: Software Development", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "# savage\n[![Build Status](https://travis-ci.org/NerdWalletOSS/savage.svg?branch=master)](https://travis-ci.org/NerdWalletOSS/savage)\n[![PyPI](https://img.shields.io/pypi/v/savage.svg)](https://pypi.org/project/savage/)\n\nA library built on top of the SQLAlchemy ORM for versioning row changes to PostgreSQL tables.\n\nBased on [versionalchemy](https://github.com/NerdWalletOSS/versionalchemy)\n\nAuthor: [Jeremy Lewis](https://www.github.com/luislew/)\n\n## Why not use versionalchemy?\n\n`versionalchemy` executes four SQL statements for every versioned row that is inserted/updated/deleted:\n\n 1. `INSERT`|`UPDATE`|`DELETE`: Insert/update/delete of the versioned row\n\n 2. `SELECT max(va_version) ...`: Selects the current max `va_version` from archive table based on row\n\n 3. `INSERT ...`: Inserts a new row into the archive table, with `va_version` incremented from previous result\n\n 4. `UPDATE ... SET va_id = ...`: Update versioned row with `va_id`, returned after the previous result executes\n\nPostgreSQL has a couple of features that allow for a simpler implementation:\n\n * `RETURNING`: PostgreSQL allows you to return server generated column values on `INSERT`/`UPDATE`\n\n * `txid_current()`: System function that returns a monotonically increasing 64-bit int ID for current transaction\n\nUtilizing these two features allows for a much simpler implementation. Instead of storing `va_id` on the archived\ntable, we store `version_id` (generated server-side using `txid_current()`) on both the archived and archive tables.\nAs a result, we don't need to select the max version (b/c it's handled server-side), and we don't need to update\nthe archive row with `archive_id`.\n\n## Getting Started\n\nSample Usage\n\n```python\nimport sqlalchemy as sa\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.schema import UniqueConstraint\n\nfrom savage import init\nfrom savage.models import SavageLogMixin, SavageModelMixin\n\nPOSTGRESQL_URL = ''\nengine = create_engine(POSTGRESQL_URL)\nBase = declarative_base(bind=engine)\n\n\nclass Example(Base, SavageModelMixin):\n __tablename__ = 'example'\n version_columns = ['id']\n id = sa.Column(sa.Integer, primary_key=True)\n value = sa.Column(sa.String(128))\n\n\nclass ExampleArchive(Base, SavageLogMixin):\n __tablename__ = 'example_archive'\n __table_args__ = (\n UniqueConstraint('id', 'version_id'),\n )\n id = sa.Column(sa.Integer)\n user_id = sa.Column(sa.Integer)\n\n\ninit() # Only call this once\nExample.register(ExampleArchive, engine) # Call this once per engine, AFTER init()\n```\n\n## Latency\n\nWe compared the results of [benchmark.py](https://gist.github.com/akshaynanavati/f1e816596d100a33e4b4a9c48099a8b7) to\na comparable [benchmark.py](https://github.com/NerdWalletOSS/savage/blob/master/benchmark.py) written for Savage. It times the performance of inserts using SQLAlchemy core, ORM\nwith and without version tracking, and (for Savage only) bulk inserts with versioning.\n\nThe below stats were generated for 100,000 records using local Docker containers with MySQL and Postgres (average of 3 runs).\n\n| | Core Inserts | ORM Inserts | Versioned ORM | Bulk Versioning\n|--------|:------------:|:-----------:|:-------------:|:---------------:\n| VersionAlchemy/MySQL 5.6 | 135 s. | 203 s. | 489 s. | _unsupported_\n| Savage/Postgres 9.6 | 154 s. (**-12%**) | 177 s. (**+15%**) | 283 s. (**+73%**) | 17.7 s. (**+2,658%**)\n\n* VersionAlchemy: ~5 ms./record\n* Savage: ~3 ms./record\n* Bulk insert/archive with Savage: ~180 \u00b5s./record (!!)\n\n\n## Caveats\n\n`txid_current()` depends on executing within a single transaction context.\n\n```python\nfrom models import db, Example\n\nexample = Example(value='foo')\nwith db.session.begin():\n db.session.add(example)\n db.session.commit()\n\n example.value = 'bar'\n db.session.add(example)\n db.session.commit() # This will raise an IntegrityError because `txid_current()` hasn't changed\n```\n\nNote that this is only an issue if you try to commit the same archived row multiple times within a single transaction.\n\nThe following would work just fine:\n\n```python\nfrom models import db, Example\n\nexample = Example(value='foo')\ndb.session.add(example)\ndb.session.commit()\n\nexample.value = 'bar'\ndb.session.add(example)\ndb.session.commit()\n```\n\n## Why is it called Savage?\n\n**S**QL**A**lchemy**V**ersion**A**lchemyPost**g**r**e**s\n\n## Style\n\n- Follow PEP8 with a line length of 100 characters\n- Prefer parenthesis to `\\` for line breaks\n\n## License\n\n[MIT License](https://github.com/NerdWalletOSS/savage/blob/master/LICENSE)", "description_content_type": "text/markdown", "docs_url": null, "download_url": "https://github.com/NerdWalletOSS/savage/archive/v1.0.0.tar.gz", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/NerdWalletOSS/savage", "keywords": "", "license": "MIT License", "maintainer": "Jeremy Lewis", "maintainer_email": "jlewis@nerdwallet.com", "name": "savage", "package_url": "https://pypi.org/project/savage/", "platform": "", "project_url": "https://pypi.org/project/savage/", "project_urls": { "Download": "https://github.com/NerdWalletOSS/savage/archive/v1.0.0.tar.gz", "Homepage": "https://github.com/NerdWalletOSS/savage" }, "release_url": "https://pypi.org/project/savage/1.0.0/", "requires_dist": null, "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4", "summary": "Automatic version tracking for SQLAlchemy + PostgreSQL (based on versionalchemy)", "version": "1.0.0" }, "last_serial": 5702560, "releases": { "0.0.4": [ { "comment_text": "", "digests": { "md5": "5ff6f383a977f1a1ca6917695153a8a3", "sha256": "82d239b033bca388436d0777afeccbcbdf95fc5ebf1be8496f64dc0f34d028e3" }, "downloads": -1, "filename": "savage-0.0.4.tar.gz", "has_sig": false, "md5_digest": "5ff6f383a977f1a1ca6917695153a8a3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11028, "upload_time": "2018-04-13T22:58:17", "url": "https://files.pythonhosted.org/packages/b4/90/dc1efbee824542611e2603a9f56d5b75c6d4abc8d5656ab373a7a69b4427/savage-0.0.4.tar.gz" } ], "0.0.6": [ { "comment_text": "", "digests": { "md5": "efd0b13e49c946ffab5838851efa3baf", "sha256": "4b8c20fb74005960e1ca8e9a0992c7d4864828795233d13a075a6da1f8bd5549" }, "downloads": -1, "filename": "savage-0.0.6.tar.gz", "has_sig": false, "md5_digest": "efd0b13e49c946ffab5838851efa3baf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11028, "upload_time": "2018-04-14T05:12:13", "url": "https://files.pythonhosted.org/packages/b8/06/8679e6d9604e0a757b53b9f1daa50e7a214b154e494623fa0c806cd56646/savage-0.0.6.tar.gz" } ], "0.0.8": [ { "comment_text": "", "digests": { "md5": "3bf6ef326bca5d91e2a64d2a1ac267d2", "sha256": "e98403000cbd10f32069c6f5f5757bf5d28f355e2ceed982bdff7fd23ecc791c" }, "downloads": -1, "filename": "savage-0.0.8.tar.gz", "has_sig": false, "md5_digest": "3bf6ef326bca5d91e2a64d2a1ac267d2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11033, "upload_time": "2018-04-14T06:20:10", "url": "https://files.pythonhosted.org/packages/d8/8f/650d966240d7c99df896adc5d899dd7e5f09fa4ae372dbd5b2a22ba36c61/savage-0.0.8.tar.gz" } ], "0.1.0": [ { "comment_text": "", "digests": { "md5": "6a18f7c82b54f54276c018d68809f651", "sha256": "4a8000b50260a018159bda91791b70bb7cbb4c18eaeba68032d29570f3ec7dd2" }, "downloads": -1, "filename": "savage-0.1.0.tar.gz", "has_sig": false, "md5_digest": "6a18f7c82b54f54276c018d68809f651", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11834, "upload_time": "2018-04-14T07:40:29", "url": "https://files.pythonhosted.org/packages/8c/55/743195078aaef80ec1c8bf5d4f26af3293351b2cd0d19f56964f3204c69c/savage-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "7c66cafd9fcd654ea870b800fa14dba5", "sha256": "f5420b59cdc6d0d710bec8df24c6781739aa609d8ba28cfff2c239f4dd86d4c4" }, "downloads": -1, "filename": "savage-0.1.1.tar.gz", "has_sig": false, "md5_digest": "7c66cafd9fcd654ea870b800fa14dba5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11866, "upload_time": "2018-04-14T08:26:44", "url": "https://files.pythonhosted.org/packages/22/0d/bdea38c2fe4c282c08e841dfe547302d3cfc67c46341b6d02a07deebd74b/savage-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "a1c01bce27491be24d7544529a608194", "sha256": "9fe31dce4f013726d389ffaca32921443c5de8c7aaa7ae3c656fcc35fc1f537b" }, "downloads": -1, "filename": "savage-0.1.2.tar.gz", "has_sig": false, "md5_digest": "a1c01bce27491be24d7544529a608194", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11884, "upload_time": "2018-04-15T20:37:46", "url": "https://files.pythonhosted.org/packages/00/ca/c1fd1c6aef92379cb881cf254a503b4bf0db3ff64ebb184eed4405e6197c/savage-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "45d7d5d1054f7e7696b0ae1ac3c273cc", "sha256": "5b1e74cc0fd2932a38330f374c31c86c848e0e572601428accc90314821e59e4" }, "downloads": -1, "filename": "savage-0.1.3.tar.gz", "has_sig": false, "md5_digest": "45d7d5d1054f7e7696b0ae1ac3c273cc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11896, "upload_time": "2018-05-16T05:32:14", "url": "https://files.pythonhosted.org/packages/36/b0/898366c1b32987465d54603213604bc115a12194fe36bb6b933bd15659af/savage-0.1.3.tar.gz" } ], "0.1.4": [ { "comment_text": "", "digests": { "md5": "0734b51c1b5c9b2e1bfbc2c21a1a1f42", "sha256": "0e12b9ae130eab1981524e0b3a652a06a24e53a930c552a5dbe30004961f2587" }, "downloads": -1, "filename": "savage-0.1.4.tar.gz", "has_sig": false, "md5_digest": "0734b51c1b5c9b2e1bfbc2c21a1a1f42", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11912, "upload_time": "2018-05-16T22:02:44", "url": "https://files.pythonhosted.org/packages/d0/d0/1d4083836423f6531d9c5b987fda93d4f85b192aa9480f2e6be84f9f9806/savage-0.1.4.tar.gz" } ], "0.1.5": [ { "comment_text": "", "digests": { "md5": "90e0a052648d03d4683094e5d4a06d15", "sha256": "7a96023691c99856f0ff39d44c6f116779bb46a1a8ba1ff5aa599c58ed2af2a1" }, "downloads": -1, "filename": "savage-0.1.5.tar.gz", "has_sig": false, "md5_digest": "90e0a052648d03d4683094e5d4a06d15", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12618, "upload_time": "2018-05-16T22:19:48", "url": "https://files.pythonhosted.org/packages/55/3f/071025c642114f8b598e2fd5fa641c8145f59ac30c1e310d3a3dee952650/savage-0.1.5.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "fee242c0a399585f215970851b029ae7", "sha256": "ca2070ca1fd4c7a652d10a61ba5cc7f011094dc1f9abb6db40a4ed30ee30f2ac" }, "downloads": -1, "filename": "savage-0.2.0.tar.gz", "has_sig": false, "md5_digest": "fee242c0a399585f215970851b029ae7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12761, "upload_time": "2018-06-04T23:47:30", "url": "https://files.pythonhosted.org/packages/6b/72/522324d6f8dbd5beec25fe3c80ace1638094f30be597afdf16ef7ab7886b/savage-0.2.0.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "5c6dd8b21c9442d3bd0981e28be5ce53", "sha256": "809c8f428c9f6bd1ff482b2c6b7fd34450c61c0862e897f7aa7c375c37e54c2c" }, "downloads": -1, "filename": "savage-0.2.1.tar.gz", "has_sig": false, "md5_digest": "5c6dd8b21c9442d3bd0981e28be5ce53", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12868, "upload_time": "2018-06-05T23:07:27", "url": "https://files.pythonhosted.org/packages/a1/a7/8f9ba78e5aec510f40f1a33b92bd8c7b036f0582d5dae2009445fffc3852/savage-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "07aee220c850642f230ac89cedec09f9", "sha256": "0545357336b49a4f6efa52643c35fa3d29edd3a76b1afebcc762bf83bf253bae" }, "downloads": -1, "filename": "savage-0.2.2.tar.gz", "has_sig": false, "md5_digest": "07aee220c850642f230ac89cedec09f9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12872, "upload_time": "2018-06-06T23:33:28", "url": "https://files.pythonhosted.org/packages/b4/b8/224c2d1a8f3a0b93f01d916bfa0b57ee2a85d9d41c352c823c2718224494/savage-0.2.2.tar.gz" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "0b5761cf9e6e2f6e5eadd1a5388fcbc4", "sha256": "d04c52d421eee427908c0a0f9f04624f515c1d88c5685904c0f771f5202d8f00" }, "downloads": -1, "filename": "savage-0.2.3.tar.gz", "has_sig": false, "md5_digest": "0b5761cf9e6e2f6e5eadd1a5388fcbc4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 13215, "upload_time": "2018-06-07T06:16:51", "url": "https://files.pythonhosted.org/packages/70/bf/9dd99bdde5b9ae5b6aadb08204703139d40a817c57a415ee82c7cede507e/savage-0.2.3.tar.gz" } ], "1.0.0": [ { "comment_text": "", "digests": { "md5": "4a6bd68e75c0c23fb09401a40994058a", "sha256": "bec074380a2fd5e646803b40474b52e96ee9ecf9b913fabaeab6c9522699a321" }, "downloads": -1, "filename": "savage-1.0.0.tar.gz", "has_sig": false, "md5_digest": "4a6bd68e75c0c23fb09401a40994058a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4", "size": 15756, "upload_time": "2019-08-20T10:23:04", "url": "https://files.pythonhosted.org/packages/78/56/e796e9358caeb3a6ac8797820d7b5d096acecf419745f42a9b51bd6de2aa/savage-1.0.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "4a6bd68e75c0c23fb09401a40994058a", "sha256": "bec074380a2fd5e646803b40474b52e96ee9ecf9b913fabaeab6c9522699a321" }, "downloads": -1, "filename": "savage-1.0.0.tar.gz", "has_sig": false, "md5_digest": "4a6bd68e75c0c23fb09401a40994058a", "packagetype": "sdist", "python_version": "source", "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4", "size": 15756, "upload_time": "2019-08-20T10:23:04", "url": "https://files.pythonhosted.org/packages/78/56/e796e9358caeb3a6ac8797820d7b5d096acecf419745f42a9b51bd6de2aa/savage-1.0.0.tar.gz" } ] }