Skip to content
This repository was archived by the owner on Jul 13, 2022. It is now read-only.

Commit 313dc15

Browse files
ssantana-nsns-circle-cijdrake
authored
Move to pynocular (#2)
* Update code to ref pynocular and do readme * finished readme * Version bumped to 0.3.0 * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * Update README.md Co-authored-by: Jonathan Drake <[email protected]> * PR comments * test issues * run pre-commit on readme * fix precommit hook * Update pynocular/__init__.py Co-authored-by: Jonathan Drake <[email protected]> * update license * update year in license Co-authored-by: ns-circle-ci <[email protected]> Co-authored-by: Jonathan Drake <[email protected]>
1 parent 6309297 commit 313dc15

File tree

15 files changed

+232
-65
lines changed

15 files changed

+232
-65
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ignore = E203,E501,E731,W503,W605
88
import-order-style = google
99
# Packages added in this list should be added to the setup.cfg file as well
1010
application-import-names =
11-
ns_sql_utils
11+
pynocular
1212
exclude =
1313
*vendor*
1414
.venv

LICENSE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
Copyright (c) 2021, Narrative Science
22

33
All rights reserved.
4-
Redistribution and use in source and binary forms, with or without
5-
modification, are NOT permitted.
4+
5+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6+
7+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9+
Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10+
11+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
# ns_sql_utils
1+
# pynocular
2+
3+
[![](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)
4+
5+
Pynocular is a lightweight ORM that lets you query your database using Pydantic models and asyncio.
6+
7+
With Pynocular you can decorate your existing Pydantic models to sync them with the corresponding table in your
8+
database, allowing you to persist changes without ever having to think about the database. Transaction management is
9+
automatically 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.
210

3-
Utilities for interacting with SQL databases
411

512
Features:
613

7-
- <!-- list of features -->
14+
- Fully supports asyncio to write to SQL databases
15+
- Provides simple methods for basic SQLAlchemy support (create, delete, update, read)
16+
- Contains access to more advanced functionality such as custom SQLAlchemy selects
17+
- Contains helper functions for creating new database tables
18+
- Advanced transaction management system allows you to conditionally put requests in transactions
819

920
Table of Contents:
1021

@@ -14,21 +25,179 @@ Table of Contents:
1425

1526
## Installation
1627

17-
ns_sql_utils requires Python 3.6 or above.
28+
pynocular requires Python 3.6 or above.
1829

1930
```bash
20-
pip install ns_sql_utils
31+
pip install pynocular
2132
# or
22-
poetry add ns_sql_utils
33+
poetry add pynocular
2334
```
2435

2536
## Guide
2637

27-
<!-- Subsections explaining how to use the package -->
38+
### Basic Usage
39+
Pynocular works by decorating your base Pydantic model with the function `database_model`. Once decorated
40+
with the proper information, you can proceed to use that model to interface with your specified database table.
41+
42+
The first step is to define a `DBInfo` object. This will contain the connection information to your database.
43+
44+
```python
45+
from pynocular.engines import DatabaseType, DBInfo
46+
47+
48+
# Example below shows how to connect to a locally-running Postgres database
49+
connection_string = f"postgresql://{db_user_name}:{db_user_password}@localhost:5432/{db_name}?sslmode=disable"
50+
)
51+
db_info = DBInfo(DatabaseType.aiopg_engine, connection_string)
52+
```
53+
54+
Pynocular supports connecting to your database through two different asyncio engines; aiopg and asyncpgsa.
55+
You can pick which one you want to use by passing the correct `DatabaseType` enum value into `DBInfo`.
56+
57+
Once you define a `db_info` object, you are ready to decorate your Pydantic models and interact with your database!
58+
59+
```python
60+
from pydantic import BaseModel, Field
61+
from pynocular.database_model import database_model, UUID_STR
62+
63+
64+
@database_model("organizations", db_info)
65+
class Org(BaseModel):
66+
67+
id: Optional[UUID_STR] = Field(primary_key=True, fetch_on_create=True)
68+
serial_id: Optional[int]
69+
name: str = Field(max_length=45)
70+
slug: str = Field(max_length=45)
71+
tag: Optional[str] = Field(max_length=100)
72+
73+
created_at: Optional[datetime] = Field(fetch_on_create=True)
74+
updated_at: Optional[datetime] = Field(fetch_on_update=True)
75+
76+
77+
# Create a new Org via `create`
78+
org = await Org.create("new org", "new-org")
79+
80+
81+
# Create a new Org via `save`
82+
org2 = Org("new org2", "new-org2")
83+
await org2.save()
84+
85+
86+
# Update an org
87+
org.name = "renamed org"
88+
await org.save()
89+
90+
91+
# Delete org
92+
await org.delete()
93+
94+
95+
# Fetch org
96+
org3 = await Org.get(org2.id)
97+
assert org3 == org2
98+
99+
# Fetch a list of orgs
100+
orgs = await Org.get_list()
101+
102+
# Fetch a filtered list of orgs
103+
orgs = await Org.get_list(tag="green")
104+
105+
# Fetch orgs that have several different tags
106+
orgs = await Org.get_list(tag=["green", "blue", "red"])
107+
```
108+
109+
With Pynocular you can set fields to be optional and set by the database. This is useful
110+
if you want to let the database autogenerate your primary key or `created_at` and `updated_at` fields
111+
on your table. To do this you must:
112+
* Wrap the typehint in `Optional`
113+
* Provide keyword arguments of `fetch_on_create=True` or `fetch_on_update=True` to the `Field` class
114+
115+
### Advanced Usage
116+
For most use cases, the basic usage defined above should suffice. However, there are certain situations
117+
where you don't necessarily want to fetch each object or you need to do more complex queries that
118+
are not exposed by the `DatabaseModel` interface. Below are some examples of how those situations can
119+
be addressed using Pynocular.
120+
121+
#### Batch operations on tables
122+
Sometimes you want to insert a bunch of records into a database and you don't want to do an insert for each one.
123+
This can be handled by the `create_list` function.
124+
125+
```python
126+
org_list = [
127+
Org(name="org1", slug="org-slug1"),
128+
Org(name="org2", slug="org-slug2"),
129+
Org(name="org3", slug="org-slug3"),
130+
]
131+
await Org.create_list(org_list)
132+
```
133+
This function will insert all records into your database table in one batch.
134+
135+
136+
If you have a use case that requires deleting a bunch of records based on some field value, you can use `delete_records`:
137+
138+
```python
139+
# Delete all records with the tag "green"
140+
await Org.delete_records(tag="green")
141+
142+
# Delete all records with if their tag has any of the following: "green", "blue", "red"
143+
await Org.delete_records(tag=["green", "blue", "red"])
144+
```
145+
146+
Sometimes 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
147+
the `update_record` function:
148+
149+
```python
150+
await Org.update_record(
151+
id="05c0060c-ceb8-40f0-8faa-dfb91266a6cf",
152+
tag="blue"
153+
)
154+
org = await Org.get("05c0060c-ceb8-40f0-8faa-dfb91266a6cf")
155+
assert org.tag == "blue"
156+
```
157+
158+
#### Complex queries
159+
Sometimes your application will require performing complex queries, such as getting the count of each unique field value for all records in the table.
160+
Because Pynocular is backed by SQLAlchemy, we can access table columns directly to write pure SQLAlchemy queries as well!
161+
162+
```python
163+
from sqlalchemy import func, select
164+
from pynocular.engines import DBEngine
165+
async def generate_org_stats():
166+
query = (
167+
select([func.count(Org.column.id), Org.column.tag])
168+
.group_by(Org.column.tag)
169+
.order_by(func.count().desc())
170+
)
171+
async with await DBEngine.transaction(Org._database_info, is_conditional=False) as conn:
172+
result = await conn.execute(query)
173+
return [dict(row) async for row in result]
174+
```
175+
NOTE: `DBengine.transaction` is used to create a connection to the database using the credentials passed in.
176+
If `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
177+
in 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
178+
a new one and any subsequent calls underneath that context manager will be added to the new transaction.
179+
180+
If `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.
181+
182+
183+
### Creating database tables
184+
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
185+
(although accessing private variables is not recommended). Using this, along with Pynocular's `create_tracked_table` function, allows you to create tables
186+
in your database based off of Pydantic models!
187+
188+
```python
189+
from pynocular.db_utils import create_tracked_table
190+
191+
from my_package import Org
192+
193+
# Creates the table "organizations" in the database defined by db_info
194+
await create_tracked_table(Org._database_info, Org._table)
195+
196+
```
28197

29198
## Development
30199

31-
To develop ns_sql_utils, install dependencies and enable the pre-commit hook:
200+
To develop pynocular, install dependencies and enable the pre-commit hook:
32201

33202
```bash
34203
pip install pre-commit poetry

ns_sql_utils/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

ns_sql_utils/config.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

pynocular/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Lightweight ORM that lets you query your database using Pydantic models and asyncio"""
2+
3+
__version__ = "0.3.0"
4+
5+
from pynocular.engines import DatabaseType, DBInfo
File renamed without changes.

pynocular/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Configuration for engines and models"""
2+
import os
3+
4+
POOL_RECYCLE = int(os.environ.get("POOL_RECYCLE", 300))
5+
DB_POOL_MIN_SIZE = int(os.environ.get("DB_POOL_MIN_SIZE", 2))
6+
DB_POOL_MAX_SIZE = int(os.environ.get("DB_POOL_MAX_SIZE", 10))

ns_sql_utils/database_model.py renamed to pynocular/database_model.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from sqlalchemy.sql.base import ImmutableColumnCollection
2626
from sqlalchemy.sql.elements import BinaryExpression, UnaryExpression
2727

28-
from ns_sql_utils.engines import DBEngine, DBInfo
29-
from ns_sql_utils.exceptions import (
28+
from pynocular.engines import DBEngine, DBInfo
29+
from pynocular.exceptions import (
3030
DatabaseModelMisconfigured,
3131
DatabaseModelMissingField,
3232
DatabaseRecordNotFound,
@@ -53,8 +53,6 @@ def is_valid_uuid(string: str) -> bool:
5353
return False
5454

5555

56-
# To be swapped once ns_data_structures exists
57-
# from ns_data_structures.pydantic_types import UUID_STR
5856
class UUID_STR(str):
5957
"""A string that represents a UUID4 value"""
6058

ns_sql_utils/db_util.py renamed to pynocular/db_util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import sqlalchemy as sa
88
from sqlalchemy.sql.ddl import CreateTable
99

10-
from ns_sql_utils.engines import DatabaseType, DBEngine, DBInfo
11-
from ns_sql_utils.exceptions import InvalidSqlIdentifierErr
10+
from pynocular.engines import DatabaseType, DBEngine, DBInfo
11+
from pynocular.exceptions import InvalidSqlIdentifierErr
1212

1313
logger = logging.getLogger()
1414

0 commit comments

Comments
 (0)