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

packages = \
['pynocular', 'pynocular.backends']

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

install_requires = \
['databases[postgresql]>=0.5.5,<0.6.0',
 'psycopg2-binary>=2.9.3,<3.0.0',
 'pydantic>=1.6,<2.0']

setup_kwargs = {
    'name': 'pynocular',
    'version': '2.0.0rc5',
    '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 annotate 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 through the [databases](https://www.encode.io/databases/) library\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- Supports automatic and nested transactions\n\nTable of Contents:\n\n- [Installation](#installation)\n- [Basic Usage](#basic-usage)\n  - [Defining models](#defining-models)\n  - [Creating a database and setting the backend](#creating-a-database-and-setting-the-backend)\n  - [Creating, reading, updating, and deleting database objects](#creating-reading-updating-and-deleting-database-objects)\n  - [Serialization](#serialization)\n  - [Special type arguments](#special-type-arguments)\n- [Advanced Usage](#advanced-usage)\n  - [Tables with compound keys](#tables-with-compound-keys)\n  - [Batch operations on tables](#batch-operations-on-tables)\n  - [Transactions and asyncio.gather](#transactions-and-asynciogather)\n  - [Complex queries](#complex-queries)\n  - [Creating database and tables](#creating-database-and-tables)\n  - [Unit testing with DatabaseModel](#unit-testing-with-databasemodel)\n- [Development](#development)\n\n## Installation\n\nPynocular requires Python 3.9 or above.\n\n```bash\npip install pynocular\n# or\npoetry add pynocular\n```\n\n## Basic Usage\n\n### Defining models\n\nPynocular works by augmenting Pydantic\'s `BaseModel` through the `DatabaseModel` class. Once you define a class that extends `DatabaseModel`, you can proceed to use that model to interface with your specified database table.\n\n```python\nfrom pydantic import Field\nfrom pynocular import DatabaseModel, UUID_STR\n\nclass Org(DatabaseModel, table_name="organizations"):\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\n### Creating a database and setting the backend\n\nThe first step is to create a database pool and set the Pynocular backend. This will tell the models how to persist data.\n\nUse the [databases](https://www.encode.io/databases/) library to create a database connection using the dialect of your choice and pass the database object to `SQLDatabaseModelBackend`.\n\n```python\nfrom pynocular import Database, set_backend, SQLDatabaseModelBackend\n\nasync def main():\n    # Example below shows how to connect to a locally-running Postgres database\n    connection_string = f"postgresql://{db_user_name}:{db_user_password}@localhost:5432/{db_name}?sslmode=disable"\n    async with Database(connection_string) as db:\n        with set_backend(SQLDatabaseModelBackend(db)):\n            print(await Org.get_list())\n```\n\n### Creating, reading, updating, and deleting database objects\n\nOnce you define a database model and set a backend, you are ready to interact with your database!\n\n```python\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\nDatabase models have their own serialization functions to convert to and from dictionaries.\n\n```python\n# Serializing org with `to_dict()`\norg = await 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# De-serializing org with `from_dict()`\nnew_org = Org.from_dict(expected_org_dict)\nassert org == new_org\n```\n\n### Special type arguments\n\nWith Pynocular you can set fields to be optional and rely on the database server to set its value. 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 Field\nfrom pynocular import DatabaseModel, UUID_STR\n\n\nclass UserSubscriptions(DatabaseModel, table_name="user_subscriptions"):\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 perform a bulk insert of records into a database table.\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### Transactions and asyncio.gather\n\nYou should avoid using `asyncio.gather` within a database transaction. You can use Pynocular\'s `gather` function instead, which has the same interface but executes queries sequentially:\n\n```python\nfrom pynocular import get_backend\nfrom pynocular.util import gather\n\nasync with get_backend().transaction():\n    await gather(\n        Org.create(id="abc", name="foo"),\n        Org.create(id="def", name="bar"),\n    )\n```\n\nThe reason is that concurrent queries can interfere with each other and result in the error:\n\n```txt\nasyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress\n```\n\nSee: https://github.com/encode/databases/issues/125#issuecomment-511720013\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 import get_backend\n\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    # Get the active backend and open a database transaction\n    async with get_backend().transaction():\n        result = await conn.execute(query)\n        return [dict(row) for row in result]\n```\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 and a properly defined `DatabaseModel` class. When you define a class that extends `DatabaseModel`, Pynocular creates a SQLAlchemy table under the hood. This can be accessed via the `table` property.\n\n```python\nfrom pynocular import Database\nfrom pynocular.util import create_new_database, create_table\n\nfrom my_package import Org\n\nasync def main():\n    connection_string = "postgresql://postgres:XXXX@localhost:5432/postgres"\n    await create_new_database(connection_string, "my_new_db")\n\n    connection_string = "postgresql://postgres:XXXX@localhost:5432/my_new_db"\n    async with Database(connection_string) as db:\n        # Creates a new database and "organizations" table in that database\n        await create_table(db, Org.table)\n\n```\n\n### Unit testing with DatabaseModel\n\nPynocular comes with tooling to write unit tests against your database models, 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 set the backend using the `MemoryDatabaseModelBackend` instead of the SQL backend. You don\'t need to change any of your database model definitions.\n\n```python\nfrom pynocular import MemoryDatabaseModelBackend, set_backend\n\nfrom my_package import Org, User\n\nasync def main():\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    with set_backend(MemoryDatabaseModelBackend()):\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    users = [\n        User(id=str(uuid4()), username="Bob"),\n        User(id=str(uuid4()), username="Sally"),\n    ]\n\n    # You can also seed the backend with existing records\n    with MemoryDatabaseModelBackend(\n        records={\n            "orgs": [o.to_dict() for o in orgs],\n            "users": [u.to_dict() for u in users],\n        }\n    ):\n        org = await Org.get(orgs[0].id)\n        org.name = "new test name"\n        await org.save()\n```\n\n## Development\n\nTo develop Pynocular, install dependencies and enable the pre-commit hook. Make sure to install Python 3.9 and activate it in your shell.\n\n```bash\nsudo yum install libffi-devel # Needed for ctypes to install poetry\npyenv install 3.9.12\npyenv shell 3.9.12\n```\n\nInstall dependencies and enable the pre-commit hook.\n\n```bash\npip install pre-commit poetry\npoetry install\npre-commit install\n```\n\nRun tests to confirm everything is installed correctly.\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.9,<4.0',
}


setup(**setup_kwargs)
