# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['pynocular']

package_data = \
{'': ['*']}

install_requires = \
['aenum>=3.1.0,<4.0.0',
 'aiocontextvars>=0.2.2,<0.3.0',
 'aiopg[sa]>=1.3.1,<2.0.0',
 'asyncpg>=0.24.0,<0.25.0',
 'asyncpgsa>=0.24.0,<0.25.0',
 'backoff>=1.11.1,<2.0.0',
 'pydantic>=1.6,<2.0']

setup_kwargs = {
    'name': 'pynocular',
    'version': '0.11.0',
    'description': 'Lightweight ORM that lets you query your database using Pydantic models and asyncio',
    'long_description': '# pynocular\n\n[![](https://img.shields.io/pypi/v/pynocular.svg)](https://pypi.org/pypi/pynocular/) [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)\n\nPynocular is a lightweight ORM that lets you query your database using Pydantic models and asyncio.\n\nWith Pynocular you can decorate your existing Pydantic models to sync them with the corresponding table in your\ndatabase, allowing you to persist changes without ever having to think about the database. Transaction management is\nautomatically handled for you so you can focus on the important parts of your code. This integrates seamlessly with frameworks that use Pydantic models such as FastAPI.\n\nFeatures:\n\n- Fully supports asyncio to write to SQL databases\n- Provides simple methods for basic SQLAlchemy support (create, delete, update, read)\n- Contains access to more advanced functionality such as custom SQLAlchemy selects\n- Contains helper functions for creating new database tables\n- Advanced transaction management system allows you to conditionally put requests in transactions\n\nTable of Contents:\n\n- [Installation](#installation)\n- [Guide](#guide)\n  - [Basic Usage](#basic-usage)\n  - [Advanced Usage](#advanced-usage)\n  - [Creating database tables](#creating-database-tables)\n- [Development](#development)\n\n## Installation\n\npynocular requires Python 3.6 or above.\n\n```bash\npip install pynocular\n# or\npoetry add pynocular\n```\n\n## Guide\n\n### Basic Usage\n\nPynocular works by decorating your base Pydantic model with the function `database_model`. Once decorated\nwith the proper information, you can proceed to use that model to interface with your specified database table.\n\nThe first step is to define a `DBInfo` object. This will contain the connection information to your database.\n\n```python\nfrom pynocular.engines import DatabaseType, DBInfo\n\n\n# Example below shows how to connect to a locally-running Postgres database\nconnection_string = f"postgresql://{db_user_name}:{db_user_password}@localhost:5432/{db_name}?sslmode=disable"\n)\ndb_info = DBInfo(DatabaseType.aiopg_engine, connection_string)\n```\n\nPynocular supports connecting to your database through two different asyncio engines; aiopg and asyncpgsa.\nYou can pick which one you want to use by passing the correct `DatabaseType` enum value into `DBInfo`.\n\n#### Object Management\n\nOnce you define a `db_info` object, you are ready to decorate your Pydantic models and interact with your database!\n\n```python\nfrom pydantic import BaseModel, Field\nfrom pynocular.database_model import database_model, UUID_STR\n\nfrom my_package import db_info\n\n@database_model("organizations", db_info)\nclass Org(BaseModel):\n\n    id: Optional[UUID_STR] = Field(primary_key=True, fetch_on_create=True)\n    name: str = Field(max_length=45)\n    slug: str = Field(max_length=45)\n    tag: Optional[str] = Field(max_length=100)\n\n    created_at: Optional[datetime] = Field(fetch_on_create=True)\n    updated_at: Optional[datetime] = Field(fetch_on_update=True)\n\n#### Object management\n\n# Create a new Org via `create`\norg = await Org.create(name="new org", slug="new-org")\n\n\n# Create a new Org via `save`\norg2 = Org(name="new org2", slug="new-org2")\nawait org2.save()\n\n\n# Update an org\norg.name = "renamed org"\nawait org.save()\n\n\n# Delete org\nawait org.delete()\n\n\n# Get org\norg3 = await Org.get(org2.id)\nassert org3 == org2\n\n# Get a list of orgs\norgs = await Org.get_list()\n\n# Get a filtered list of orgs\norgs = await Org.get_list(tag="green")\n\n# Get orgs that have several different tags\norgs = await Org.get_list(tag=["green", "blue", "red"])\n\n# Fetch the latest state of a table in the db\norg3.name = "fake name"\nawait org3.fetch()\nassert org3.name == "new org2"\n\n```\n\n#### Serialization\n\nDatabaseModels have their own serialization functions to convert to and from\ndictionaries.\n\n```python\n# Serializing org with `to_dict()`\norg = Org.create(name="org serialize", slug="org-serialize")\norg_dict = org.to_dict()\nexpected_org_dict = {\n    "id": "e64f6c7a-1bd1-4169-b482-189bd3598079",\n    "name": "org serialize",\n    "slug": "org-serialize",\n    "created_at": "2018-01-01 7:03:45",\n    "updated_at": "2018-01-01 9:24:12"\n}\nassert org_dict == expected_org_dict\n\n\n# De-serializing org with `from_dict()`\nnew_org = Org.from_dict(expected_org_dict)\nassert org == new_org\n```\n\n#### Using Nested DatabaseModels\n\nPynocular also supports basic object relationships. If your database tables have a\nforeign key reference you can leverage that in your pydantic models to increase the\naccessibility of those related objects.\n\n```python\nfrom pydantic import BaseModel, Field\nfrom pynocular.database_model import database_model, nested_model, UUID_STR\n\nfrom my_package import db_info\n\n@database_model("users", db_info)\nclass User(BaseModel):\n\n    id: Optional[UUID_STR] = Field(primary_key=True, fetch_on_create=True)\n    username: str = Field(max_length=100)\n\n    created_at: Optional[datetime] = Field(fetch_on_create=True)\n    updated_at: Optional[datetime] = Field(fetch_on_update=True)\n\n@database_model("organizations", db_info)\nclass Org(BaseModel):\n\n    id: Optional[UUID_STR] = Field(primary_key=True, fetch_on_create=True)\n    name: str = Field(max_length=45)\n    slug: str = Field(max_length=45)\n    # `organizations`.`tech_owner_id` is a foreign key to `users`.`id`\n    tech_owner: Optional[nested_model(User, reference_field="tech_owner_id")]\n    # `organizations`.`business_owner_id` is a foreign key to `users`.`id`\n    business_owner: nested_model(User, reference_field="business_owner_id")\n    tag: Optional[str] = Field(max_length=100)\n\n    created_at: Optional[datetime] = Field(fetch_on_create=True)\n    updated_at: Optional[datetime] = Field(fetch_on_update=True)\n\n\ntech_owner = await User.create("tech owner")\nbusiness_owner = await User.create("business owner")\n\n\n# Creating org with only business owner set\norg = await Org.create(\n    name="org name",\n    slug="org-slug",\n    business_owner=business_owner\n)\n\nassert org.business_owner == business_owner\n\n# Add tech owner\norg.tech_owner = tech_owner\nawait org.save()\n\n# Fetch from the db and check ids\norg2 = Org.get(org.id)\nassert org2.tech_owner.id == tech_owner.id\nassert org2.business_owner.id == business_owner.id\n\n# Swap user roles\norg2.tech_owner = business_owner\norg2.business_owner = tech_owner\nawait org2.save()\norg3 = await Org.get(org2.id)\nassert org3.tech_owner.id == business_owner.id\nassert org3.business_owner.id == tech_owner.id\n\n\n# Serialize org\norg_dict = org3.to_dict()\nexpected_org_dict = {\n    "id": org3.id,\n    "name": "org name",\n    "slug": "org-slug",\n    "business_owner_id": tech_owner.id,\n    "tech_owner_id": business_owner.id,\n    "tag": None,\n    "created_at": org3.created_at,\n    "updated_at": org3.updated_at\n}\n\nassert org_dict == expected_org_dict\n\n```\n\nWhen using `DatabaseModel.get(..)`, any foreign references will need to be resolved before any properties besides the primary ID can be accessed. If you try to access a property before calling `fetch()` on the nested model, a `NestedDatabaseModelNotResolved` error will be thrown.\n\n```python\norg_get = await Org.get(org3.id)\norg_get.tech_owner.id # Does not raise `NestedDatabaseModelNotResolved`\norg_get.tech_owner.username # Raises `NestedDatabaseModelNotResolved`\n\norg_get = await Org.get(org3.id)\nawait org_get.tech_owner.fetch()\norg_get.tech_owner.username # Does not raise `NestedDatabaseModelNotResolved`\n```\n\nAlternatively, calling `DatabaseModel.get_with_refs()` instead of `DatabaseModel.get()` will\nautomatically fetch the referenced records and fully resolve those objects for you.\n\n```python\norg_get_with_refs = await Org.get_with_refs(org3.id)\norg_get_with_refs.tech_owner.username # Does not raise `NestedDatabaseModelNotResolved`\n```\n\nThere are some situations where none of the objects have been persisted to the\ndatabase yet. In this situation, you can call `Database.save(include_nested_models=True)`\non the object with the references and it will persist all of them in a transaction.\n\n```python\n# We create the objects but dont persist them\ntech_owner = User("tech owner")\nbusiness_owner = User("business owner")\n\norg = Org(\n    name="org name",\n    slug="org-slug",\n    business_owner=business_owner\n)\n\nawait org.save(include_nested_models=True)\n```\n\n#### Special Type arguments\n\nWith Pynocular you can set fields to be optional and set by the database. This is useful\nif you want to let the database autogenerate your primary key or `created_at` and `updated_at` fields\non your table. To do this you must:\n\n- Wrap the typehint in `Optional`\n- Provide keyword arguments of `fetch_on_create=True` or `fetch_on_update=True` to the `Field` class\n\n### Advanced Usage\n\nFor most use cases, the basic usage defined above should suffice. However, there are certain situations\nwhere you don\'t necessarily want to fetch each object or you need to do more complex queries that\nare not exposed by the `DatabaseModel` interface. Below are some examples of how those situations can\nbe addressed using Pynocular.\n\n#### Tables with compound keys\n\nPynocular supports tables that use multiple fields as its primary key such as join tables.\n\n```python\nfrom pydantic import BaseModel, Field\nfrom pynocular.database_model import database_model, nested_model, UUID_STR\n\nfrom my_package import db_info\n\n@database_model("user_subscriptions", db_info)\nclass UserSubscriptions(BaseModel):\n\n    user_id: UUID_STR = Field(primary_key=True, fetch_on_create=True)\n    subscription_id: UUID_STR = Field(primary_key=True, fetch_on_create=True)\n    name: str\n\n\nuser_sub = await UserSub.create(\n    user_id="4d4254c4-8e99-45f9-8261-82f87991c659",\n    subscription_id="3cc5d476-dbe6-4cc1-9390-49ebd7593a3d",\n    name="User 1\'s subscriptions"\n)\n\n# Get the users subscription and confirm its the same\nuser_sub_get = await UserSub.get(\n    user_id="4d4254c4-8e99-45f9-8261-82f87991c659",\n    subscription_id="3cc5d476-dbe6-4cc1-9390-49ebd7593a3d",\n)\nassert user_sub_get == user_sub\n\n# Change a property value like any other object\nuser_sub_get.name = "change name"\nawait user_sub_get.save()\n```\n\n#### Batch operations on tables\n\nSometimes you want to insert a bunch of records into a database and you don\'t want to do an insert for each one.\nThis can be handled by the `create_list` function.\n\n```python\norg_list = [\n    Org(name="org1", slug="org-slug1"),\n    Org(name="org2", slug="org-slug2"),\n    Org(name="org3", slug="org-slug3"),\n]\nawait Org.create_list(org_list)\n```\n\nThis function will insert all records into your database table in one batch.\n\nIf you have a use case that requires deleting a bunch of records based on some field value, you can use `delete_records`:\n\n```python\n# Delete all records with the tag "green"\nawait Org.delete_records(tag="green")\n\n# Delete all records with if their tag has any of the following: "green", "blue", "red"\nawait Org.delete_records(tag=["green", "blue", "red"])\n```\n\nSometimes you may want to update the value of a record in a database without having to fetch it first. This can be accomplished by using\nthe `update_record` function:\n\n```python\nawait Org.update_record(\n    id="05c0060c-ceb8-40f0-8faa-dfb91266a6cf",\n    tag="blue"\n)\norg = await Org.get("05c0060c-ceb8-40f0-8faa-dfb91266a6cf")\nassert org.tag == "blue"\n```\n\n#### Complex queries\n\nSometimes your application will require performing complex queries, such as getting the count of each unique field value for all records in the table.\nBecause Pynocular is backed by SQLAlchemy, we can access table columns directly to write pure SQLAlchemy queries as well!\n\n```python\nfrom sqlalchemy import func, select\nfrom pynocular.engines import DBEngine\nasync def generate_org_stats():\n    query = (\n        select([func.count(Org.column.id), Org.column.tag])\n        .group_by(Org.column.tag)\n        .order_by(func.count().desc())\n    )\n    async with await DBEngine.transaction(Org._database_info, is_conditional=False) as conn:\n        result = await conn.execute(query)\n        return [dict(row) async for row in result]\n```\n\nNOTE: `DBengine.transaction` is used to create a connection to the database using the credentials passed in.\nIf `is_conditional` is `False`, then it will add the query to any transaction that is opened in the call chain. This allows us to make database calls\nin different functions but still have them all be under the same database transaction. If there is no transaction opened in the call chain it will open\na new one and any subsequent calls underneath that context manager will be added to the new transaction.\n\nIf `is_conditional` is `True` and there is no transaction in the call chain, then the connection will not create a new transaction. Instead, the query will be performed without a transaction.\n\n### Creating database and tables\n\nWith Pynocular you can use simple python code to create new databases and database tables. All you need is a working connection string to the database host, a `DatabaseInfo` object that contains the information of the database you want to create, and a properly decorated pydantic model. When you decorate a pydantic model with Pynocular, it creates a SQLAlchemy table as a private variable. This can be accessed via the `_table` property\n(although accessing private variables is not recommended).\n\n```python\nfrom pynocular.db_util import create_new_database, create_table\n\nfrom my_package import Org, db_info\n\nconnection_string = "postgresql://postgres:XXXX@localhost:5432/postgres?sslmode=disable"\n\n# Creates a new database and "organizations" table in that database\nawait create_new_database(connection_string, db_info)\nawait create_table(db_info, Org._table)\n\n```\n\n### Unit Testing with DatabaseModels\n\nPynocular comes with tooling to write unit tests against your DatabaseModels, giving you\nthe ability to test your business logic without the extra work and latency involved in\nmanaging a database. All you have to do is use the `patch_database_model` context\nmanager provided in pynocular.\n\n```python\nfrom pynocular.patch_models import patch_database_model\n\nfrom my_package import Org, User\n\n\nwith patch_database_model(Org):\n    orgs = [\n        Org(id=str(uuid4()), name="orgus borgus", slug="orgus_borgus"),\n        Org(id=str(uuid4()), name="orgus borgus2", slug="orgus_borgus"),\n    ]\n\n    await Org.create_list(orgs)\n    fetched_orgs = await Org.get_list(name=orgs[0].name)\n    assert orgs[0] == fetched_orgs[0]\n\n# patch_database_model also works with nested models\nusers = [\n    User(id=str(uuid4()), username="Bob"),\n    User(id=str(uuid4()), username="Sally"),\n]\norgs = [\n    Org(\n        id=str(uuid4()),\n        name="orgus borgus",\n        slug="orgus_borgus",\n        tech_owner=users[0],\n        business_owner=users[1],\n    ),\n]\n\nwith patch_database_model(Org, models=orgs), patch_database_model(\n    User, models=users\n):\n    org = await Org.get(orgs[0].id)\n    org.name = "new test name"\n    users[0].username = "bberkley"\n\n    # Save the username update when saving the org model update\n    await org.save(include_nested_models=True)\n\n    # Get the org with the resolved nested model\n    org_get = await Org.get_with_refs(org.id)\n    assert org_get.name == "new test name"\n    assert org_get.tech_owner.username == "bberkley"\n```\n\n## Development\n\nTo develop pynocular, install dependencies and enable the pre-commit hook:\n\n```bash\npip install pre-commit poetry\npoetry install\npre-commit install\n```\n\nTo run tests:\n\n```bash\npoetry run pytest\n```\n',
    'author': 'RJ Santana',
    'author_email': 'ssantana@narrativescience.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/NarrativeScience/pynocular',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.6.5,<4.0.0',
}


setup(**setup_kwargs)
