{ "info": { "author": "CodeLV", "author_email": "frmdstryr@gmail.com", "bugtrack_url": null, "classifiers": [], "description": "[![Build Status](https://travis-ci.org/codelv/atom-db.svg?branch=master)](https://travis-ci.org/codelv/atom-db)\n[![codecov](https://codecov.io/gh/codelv/atom-db/branch/master/graph/badge.svg)](https://codecov.io/gh/codelv/atom-db)\n\natom-db is a database abstraction layer for the\n[atom](https://github.com/nucleic/atom) framework. This package provides api's for\nseamlessly saving and restoring atom objects from json based document databases\nand SQL databases supported by sqlalchemy.\n\n\nThe main reason for building this is to make it easier have database integration\nwith [enaml](https://github.com/nucleic/enaml) applications so a separate\nframework is not needed to define database models.\n\nThis was originally a part of [enaml-web](https://github.com/codelv/enaml-web)\nbut has been pulled out to a separate package.\n\n\n### Overview\n\n- Supports MySQL and Postgres\n- Uses django like queries or raw sqlalchemy queries\n- Works with alembic database migrations\n- Supports MongoDB using motor\n\nIt's still in development....\n\n\n### Structure\n\nThe design is based somewhat on django.\n\nThere is a \"manager\" called `Model.objects` to do queries on the database table\ncreated for each subclass.\n\nSerialization and deserialization is done with `Model.serializer`\n\nEach `Model` has async `save`, `delete`, and `restore` methods to interact with\nthe database. The pickle protocol methods `__getstate__` and `__setstate__` are\nre-implemented (setstate is async).\n\n\n# MySQL and Postgres support\n\nYou can use atom-db to save and restore atom subclasses to MySQL and Postgres.\n\nJust define models using atom members, but subclass the SQLModel and atom-db\nwill convert the builtin atom members of your model to sqlalchemy table columns\nand create a `sqlalchemy.Table` for your model.\n\n\n### Customizing table creation\n\nTo customize how table columns are created you can tag members with information\nneeded for sqlalchemy columns, ex `Str().tag(length=40)` will make a `sa.String(40)`.\nSee https://docs.sqlalchemy.org/en/latest/core/type_basics.html. Tagging any\nmember with `store=False` will make the member be excluded from the db.\n\natomdb will attempt to determine the proper column type, but if you need more\ncontrol, you can tag the member to specify the column type with\n`type=sa.` or specify the full column definition with\n`column=sa.Column(...)`.\n\nIf you have a custom member, you can define a `def get_column(self, model)`\nor `def get_column_type(self, model)` method to create the table column for the\ngiven model.\n\n\n##### Primary keys\n\nYou can tag a member with `primary_key=True` to make it the pk. If no member\nis tagged with `primary_key` it will create and use `_id` as the primary key.\nThe`_id` member will be always alias to the actual primary key. Use the `__pk__`\nattribute of the class to get the name of the primary key member.\n\n##### Table metadata\n\nLike in Django a nested `Meta` class can be added to specify the `db_name`\nand `unique_together` constraints. If no `db_name` is specified on a Meta class,\nthe table name defaults the what is set in the `__model__` member. This defaults\nto the qualname of the class, eg `myapp.SomeModel`.\n\n\n```python\n\nclass SomeModel(SQLModel):\n # ...\n\n class Meta:\n db_table = 'custom_table_name'\n\n```\n\n##### Table creation / dropping\n\nOnce your tables are defined as atom models, create and drop tables using\n `create_table` and `drop_table` of `Model.objects` respectively For example:\n\n```python\n\nfrom atomdb.sql import SQLModel, SQLModelManager\n\n# Call create_tables to create sqlalchemy tables. This does NOT write them to\n# the db but ensures that all ForeignKey relations are created\nmgr = SQLModelManager.instance()\nmgr.create_tables()\n\n# Now actually drop/create for each of your models\n\n# Drop the table for this model (will raise sqlalchemy's error if it doesn't exist)\nawait User.objects.drop_table()\n\n# Create the user table\nawait User.objects.create_table()\n\n\n```\n\nThe `mgr.create_tables()` method will create the sqlalchemy tables for each\nimported SQLModel subclass (anything in the manager's `registry` dict). This\nshould be called after all of your models are imported so sqlalchemy can\nproperly setup any foreign key relations.\n\nThe manager also has a `metadata` member which holds the `sqlalchemy.MetaData`\nneeded for migrations.\n\nOnce the tables are created, they are accessible via `Model.objects.table`.\n\n> Note: The sqlachemy table is also assigned to the `__table__` attribute of\neach model class, however this will not be defined until the manager has\ncreated it.\n\n\n#### Database setup\n\nBefore accessing the DB you must assign a \"database engine\" to the manager's\n`database` member.\n\n```python\nimport os\nimport re\nfrom aiomysql.sa import create_engine\nfrom atomdb.sql import SQLModelManager\n\nDATABASE_URL = os.environ.get('MYSQL_URL')\n\n# Parse the DB url\nm = re.match(r'mysql://(.+):(.*)@(.+):(\\d+)/(.+)', DATABASE_URL)\nuser, pwd, host, port, db = m.groups()\n\n# Create the engine\nengine = await create_engine(\n db=db, user=user, password=pwd, host=host, port=port)\n\n# Assign it to the manager\nmgr = SQLModelManager.instance()\nmgr.database = engine\n\n\n```\n\nThis engine will then be used by the manager to execute queries. You can\nretrieve the database engine from any Model by using `Model.objects.engine`.\n\n\n#### Django style queries\n\nOnly very basic ORM style queries are implemented for common use cases. These\nare `get`, `get_or_create`, `filter`, and `all`. These all accept\n\"django style\" queries using `=` or `__=`.\n\nFor example:\n\n```python\n\njohn, created = await User.objects.get_or_create(\n name=\"John Doe\", email=\"jon@example.com\", age=21, active=True)\nassert created\n\njane, created = await User.objects.get_or_create(\n name=\"Jane Doe\", email=\"jane@example.com\", age=48, active=False,\n rating=10.0)\nassert created\n\n# Startswith\nu = await User.objects.get(name__startswith=\"John\")\nassert u.name == john.name\n\n# In query\nusers = await User.objects.filter(name__in=[john.name, jane.name])\nassert len(users) == 2\n\n# Is query\nusers = await User.objects.filter(active__is=False)\nassert len(users) == 1 and users[0].active == False\n\n```\n\nSee [sqlachemy's ColumnElement](https://docs.sqlalchemy.org/en/latest/core/sqlelement.html?highlight=column#sqlalchemy.sql.expression.ColumnElement)\nfor which queries can be used in this way. Also the tests check that these\nactually work as intended.\n\n#### Advanced / raw sqlalchemy queries\n\nFor more advanced queries using joins, etc.. you must build the query with\nsqlalchemy then execute it. The `sa.Table` for an atom model can be retrieved\nusing `Model.objects.table` on which you can use select, where, etc... to build\nup whatever query you need.\n\nThen use `fetchall`, `fetchone`, `fetchmany`, or `execute` to do these queries.\n\nThese methods do NOT return an object but the row from the database so they\nmust manually be restored.\n\nWhen joining you'll usually want to pass `use_labels=True`. For example:\n\n```python\n\nq = Job.objects.table.join(JobRole.objects.table).select(use_labels=True)\n\nfor row in await Job.objects.fetchall(q):\n # Restore each manually, it handles pulling out the fields that are it's own\n job = await Job.restore(row)\n role = await JobRole.restore(row)\n\n```\n\nDepending on the relationships, you may need to then post-process these so they\ncan be accessed in a more pythonic way. This is trade off between complexity\nand ease of use.\n\n\n### Connections and Transactions\n\nA connection can be retrieved using `Model.objects.connection()` and used\nlike normal aiomysql / aiopg connection. A transaction is done in the same way\nas defined in the docs for those libraries eg.\n\n```python\n\nasync with Job.objects.connection() as conn:\n trans = await conn.begin()\n try:\n # Do your queries here and pass the `connection` to each\n job, created = await Job.objects.get_or_create(connection=conn, **state)\n except:\n await trans.rollback()\n raise\n else:\n await trans.commit()\n\n```\n\nWhen using a transaction you need to pass the active connection to\neach call or it will use a different connection outside of the transaction!\n\nThe connection argument is removed from the filters/state. If your model happens\nto have a member named `connection` you can rename the connection argument by \nwith `Model.object.connection_kwarg = 'connection_'` or whatever name you like.\n\n### Migrations\n\nMigrations work using [alembic](https://alembic.sqlalchemy.org/en/latest/autogenerate.html). The metadata needed\nto autogenerate migrations can be retrieved from `SQLModelManager.instance().metadata` so add the following\nin your alembic's env.py:\n\n```python\n# Import your db models first\nfrom myapp.models import *\n\nfrom atomdb.sql import SQLModelManager\nmanager = SQLModelManager.instance()\nmanager.create_tables() # Create sa tables\ntarget_metadata = manager.metadata\n\n```\n\nThe rest is handled by alembic.\n\n\n# NoSQL support\n\nYou can also use atom-db to save and restore atom subclasses to MongoDB.\n\nThe NoSQL version is very basic as mongo is much more relaxed. No restriction\nis imposed on what type of manager is used, leaving that to whichever database\nlibrary is preferred but it's tested (and currently used) with [motor](https://motor.readthedocs.io/en/stable/)\nand [tornado](https://www.tornadoweb.org/en/stable/index.html).\n\nJust define models using atom members, but subclass the `NoSQLModel`.\n\n```python\n\nfrom atom.api import Unicode, Int, Instance, List\nfrom atomdb.nosql import NoSQLModel, NoSQLModelManager\nfrom motor.motor_asyncio import AsyncIOMotorClient\n\n# Set DB\nclient = AsyncIOMotorClient()\nmgr = NoSQLModelManager.instance()\nmgr.database = client.test_db\n\n\nclass Group(NoSQLModel):\n name = Unicode()\n\nclass User(NoSQLModel):\n name = Unicode()\n age = Int()\n groups = List(Group)\n\n\n```\n\nThen we can create an instance and save it. It will perform an upsert or replace\nthe existing entry.\n\n```python\n\nadmins = Group(name=\"Admins\")\nawait admins.save()\n\n# It will save admins using it's ObjectID\nbob = User(name=\"Bob\", age=32, groups=[admins])\nawait bob.save()\n\ntom = User(name=\"Tom\", age=34, groups=[admins])\nawait tom.save()\n\n```\n\nTo fetch from the DB each model has a `ModelManager` called `objects` that will\nsimply return the collection for the model type. For example.\n\n```python\n\n# Fetch from db, you can use any MongoDB queries here\nstate = await User.objects.find_one({'name': \"James\"})\nif state:\n james = await User.restore(state)\n\n# etc...\n```\n\nRestoring is async because it will automatically fetch any related objects\n(ex the groups in this case). It saves objects using the ObjectID when present.\n\nAnd finally you can either delete using queries on the manager directly or\ncall on the object.\n\n```python\nawait tom.delete()\nassert not await User.objects.find_one({'name': \"Tom\"})\n\n```\n\nYou can exclude members from being saved to the DB by tagging them\nwith `.tag(store=False)`.\n\n\n\n## Contributing\n\nThis is currently used in a few projects but not considered mature by\nany means.\n\nPull requests and feature requests are welcome!\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/codelv/atom-db", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "atom-db", "package_url": "https://pypi.org/project/atom-db/", "platform": "", "project_url": "https://pypi.org/project/atom-db/", "project_urls": { "Homepage": "https://github.com/codelv/atom-db" }, "release_url": "https://pypi.org/project/atom-db/0.3.2/", "requires_dist": [ "atom" ], "requires_python": "", "summary": "Database abstraction layer for atom objects", "version": "0.3.2" }, "last_serial": 5997640, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "f962d85468ec0ac8c65c56f7888dfd6f", "sha256": "ea9e9cc8ebd67e7ada603103c7712f1ac26b58ca4717ae3561ec5c7bd9c794c8" }, "downloads": -1, "filename": "atom_db-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "f962d85468ec0ac8c65c56f7888dfd6f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 15137, "upload_time": "2019-03-19T17:40:51", "url": "https://files.pythonhosted.org/packages/36/00/d432688fcfdc8b16373d6e8b1256c7c783a5e2d97e8326c7266395dd27f1/atom_db-0.1.0-py3-none-any.whl" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "8eb5dfc4b635ae7b93c93a304c618246", "sha256": "c4ab0b2307124a1f4e4fb8a8607ceb19237c7bf90cb482bce4d4f87e7f78bb7e" }, "downloads": -1, "filename": "atom_db-0.2.3-py3-none-any.whl", "has_sig": false, "md5_digest": "8eb5dfc4b635ae7b93c93a304c618246", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 19183, "upload_time": "2019-09-10T19:30:22", "url": "https://files.pythonhosted.org/packages/78/6e/ce1a46ea0495ce6f0a41cd72f5888b277c644b284f20e0b72ebeea96d657/atom_db-0.2.3-py3-none-any.whl" } ], "0.2.4": [ { "comment_text": "", "digests": { "md5": "9717147ec571de03be4d4e904b2d2734", "sha256": "8313d9794415e8a82295ac3d61e9c36ba8ff041373af5f84dd89f840bd67002a" }, "downloads": -1, "filename": "atom_db-0.2.4-py3-none-any.whl", "has_sig": false, "md5_digest": "9717147ec571de03be4d4e904b2d2734", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 19381, "upload_time": "2019-09-30T18:39:31", "url": "https://files.pythonhosted.org/packages/51/ee/e8e9fd24ecddf70f1065566e793ee4ef184ed5ff13ed29d80bac1ff14667/atom_db-0.2.4-py3-none-any.whl" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "478618535e2833e18e66644b1a6c6dd1", "sha256": "18bf10e3a55b9f3b530eb1e4086d992ebe25132af1a83062d69f5dc968122896" }, "downloads": -1, "filename": "atom_db-0.3.0-py3-none-any.whl", "has_sig": false, "md5_digest": "478618535e2833e18e66644b1a6c6dd1", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 19916, "upload_time": "2019-10-11T16:15:50", "url": "https://files.pythonhosted.org/packages/dc/06/4a580bd72358cba8725d58ac77fd9166b6829908b4349a7d268e26b112ca/atom_db-0.3.0-py3-none-any.whl" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "e010fd1e040a44efa68cfa23073a0c56", "sha256": "0407baf6cc8918ececce69fdd969bf1298f74fabd06f41d46fb296f85c2aa210" }, "downloads": -1, "filename": "atom_db-0.3.1-py3-none-any.whl", "has_sig": false, "md5_digest": "e010fd1e040a44efa68cfa23073a0c56", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 19975, "upload_time": "2019-10-11T20:23:40", "url": "https://files.pythonhosted.org/packages/dc/92/786d2d437e6b6a3dad8c9c8fe294374fb61fbbf69d8341f53e742d4ebaaf/atom_db-0.3.1-py3-none-any.whl" } ], "0.3.2": [ { "comment_text": "", "digests": { "md5": "826a6fca8725a94dadedec06f4dc33f0", "sha256": "30c1c65c13db6fedc83a5359b5b42131697662b2d8f1d6883cacdf922164a2b0" }, "downloads": -1, "filename": "atom_db-0.3.2-py3-none-any.whl", "has_sig": false, "md5_digest": "826a6fca8725a94dadedec06f4dc33f0", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 21457, "upload_time": "2019-10-18T21:28:18", "url": "https://files.pythonhosted.org/packages/d6/10/dd0af2eecd45b1e84a3a0a5bf4f95fc501dde3677cb51ffca559daf1895e/atom_db-0.3.2-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "826a6fca8725a94dadedec06f4dc33f0", "sha256": "30c1c65c13db6fedc83a5359b5b42131697662b2d8f1d6883cacdf922164a2b0" }, "downloads": -1, "filename": "atom_db-0.3.2-py3-none-any.whl", "has_sig": false, "md5_digest": "826a6fca8725a94dadedec06f4dc33f0", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 21457, "upload_time": "2019-10-18T21:28:18", "url": "https://files.pythonhosted.org/packages/d6/10/dd0af2eecd45b1e84a3a0a5bf4f95fc501dde3677cb51ffca559daf1895e/atom_db-0.3.2-py3-none-any.whl" } ] }