Skip to content

Commit

Permalink
✨ Add async support starting with aiopg (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
zschumacher committed Feb 24, 2022
1 parent 7fae9e6 commit 6eaa4dc
Show file tree
Hide file tree
Showing 77 changed files with 2,442 additions and 559 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:

- name: install dependencies
if: steps.cache-poetry-deps.outputs.cache-hit != 'true'
run: poetry install --extras "psycopg2 pymssql mysql-connector-python cx_Oracle"
run: poetry install --extras "psycopg2 pymssql mysql-connector-python cx_Oracle aiopg"

- name: test and coverage
env:
Expand Down
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,31 @@ pip install pydapper[psycopg2]
poetry add pydapper -E psycopg2
```

## A Simple Example
## Never write this again...
```python
from psycopg2 import connect

@dataclass
class Task:
id: int
description: str
due_date: datetime.date

with connect("postgresql://pydapper:pydapper@localhost/pydapper") as conn:
with conn.cursor() as cursor:
cursor.execute("select id, description, due_date from task")
headers = [i[0] for i in cursor.description]
data = cursor.fetchall()

list_data = [Task(**dict(zip(headers, row))) for row in data]
```

## Instead, write...
```python
from dataclasses import dataclass
import datetime

from pydapper import connect
import pydapper


@dataclass
Expand All @@ -43,10 +62,7 @@ class Task:
due_date: datetime.date


with connect("postgresql+psycopg2://pydapper:pydapper@locahost/pydapper") as commands:
with pydapper.connect("postgresql+psycopg2://pydapper:pydapper@locahost/pydapper") as commands:
tasks = commands.query("select id, description, due_date from task;", model=Task)

print(tasks)
# [Task(id=1, description='Add a README!', due_date=datetime.date(2022, 1, 16))]
```
(This script is complete, it should run "as is")
File renamed without changes.
60 changes: 60 additions & 0 deletions docs/async_methods/execute_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

`execute_async` can execute a command one or multiple times and return the number of affected rows. This method is usually used
to execute insert, update or delete operations.

## Parameters
| name | type | description | optional | default |
|-------|----------------------------|-----------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ListParamType, ParamType` | params to substitute in the query | :thumbsup: | `None` |

## Example - Execute Insert
### Single
Execute the INSERT statement a single time.

```python
{!docs/../docs_src/async_methods/execute/insert_single.py!}
```
(*This script is complete, it should run "as is"*)

### Multiple
Execute the INSERT statement multiple times, one for each object in the param list.

```python
{!docs/../docs_src/async_methods/execute/insert_multiple.py!}
```
(*This script is complete, it should run "as is"*)

## Example - Execute Update
### Single
Execute the UPDATE statement a single time.

```python
{!docs/../docs_src/async_methods/execute/update_single.py!}
```
(*This script is complete, it should run "as is"*)

### Multiple
Execute the UPDATE statement multiple times, one for each object in the param list.

```python
{!docs/../docs_src/async_methods/execute/update_multiple.py!}
```
(*This script is complete, it should run "as is"*)

## Example - Execute Delete
### Single
Execute the DELETE statement a single time.

```python
{!docs/../docs_src/async_methods/execute/delete_single.py!}
```
(*This script is complete, it should run "as is"*)

### Multiple
Execute the DELETE statement multiple times, one for each object in the param list.

```python
{!docs/../docs_src/async_methods/execute/delete_multiple.py!}
```
(*This script is complete, it should run "as is"*)
18 changes: 18 additions & 0 deletions docs/async_methods/execute_scalar_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

`execute_scalar_async` executes the query, and returns the first column of the first row in the result set returned by
the query. The additional columns or rows are ignored.


## Parameters
| name | type | description | optional | default |
|-------|-------------|-----------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |

## Example
Get the name of the first task owner in the database.

```python
{!docs/../docs_src/async_methods/execute_scalar/example.py!}
```
(*This script is complete, it should run "as is"*)
35 changes: 35 additions & 0 deletions docs/async_methods/query_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

`query_async` can execute a query and serialize the results to a model.

## Parameters
| name | type | description | optional | default |
|----------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |
| buffered | `bool` | whether to buffer reading the results of the query | :thumbsup: | `True` |

## Example - Serialize to a dataclass
The raw sql query can be executed using the `query_async` method and map the results to a list of dataclasses.
```python
{!docs/../docs_src/async_methods/query/basic_query.py!}
```
(*This script is complete, it should run "as is"*)

### Example - Serialize a one to one relationship
You can get creative with what you pass in to the model kwarg of `query`
```python
{!docs/../docs_src/async_methods/query/one_to_one_query.py!}
```
(This script is complete, it should run "as is")


### Example - Buffering queries
By default, `query` fetches all results and stores them in a list (buffered). By setting `buffered=False`, you can
instead have `query` act as a generator function, fetching one record from the result set at a time. This may be useful
if querying a large amount of data that would not fit into memory, but note that this keeps both the connection and
cursor open while you're retrieving results.
```python
{!docs/../docs_src/async_methods/query/query_unbuffered.py!}
```
(This script is complete, it should run "as is")
19 changes: 19 additions & 0 deletions docs/async_methods/query_first_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

`query_first_async` can execute a query and map the first result.

## Parameters
| name | type | description | optional | default |
|-------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |

## First, Single and Default
{!docs/.first_single_default.md!}

## Example
Execute a query and map the first result to a dataclass.
```python
{!docs/../docs_src/async_methods/query_first/example.py!}
```
(This script is complete, it should run "as is")
22 changes: 22 additions & 0 deletions docs/async_methods/query_first_or_default_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

`query_first_or_default_async` can execute a query and serialize the first result, or return a default value if the result
set contains no records.

## Parameters
| name | type | description | optional | default |
|---------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| default | `Any` | any object to return if the result set is empty | :thumbsdown: |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |


## First, Single and Default
{!docs/.first_single_default.md!}

## Example
Execute a query and map the first result to a dataclass.
```python
{!docs/../docs_src/async_methods/query_first_or_default/example.py!}
```
(This script is complete, it should run "as is")
17 changes: 17 additions & 0 deletions docs/async_methods/query_multiple_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

`query_multiple_async` can execute multiple queries with the same cursor and serialize the results. This method
will throw a `ValueError` if you don't supply the same number of queries and models.

## Parameters
| name | type | description | optional | default |
|-------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |

## Example
Query two tables and return the serialized results.
```python
{!docs/../docs_src/async_methods/query_multiple/example.py!}
```
(This script is complete, it should run "as is")
20 changes: 20 additions & 0 deletions docs/async_methods/query_single_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

`query_single_async` can execute a query and serialize the first result. It throws an exception if there is not exactly one
record in the result set.

## Parameters
| name | type | description | optional | default |
|-------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |

## First, Single and Default
{!docs/.first_single_default.md!}

## Example
Execute a query and map the first result to a dataclass.
```python
{!docs/../docs_src/async_methods/query_single/example.py!}
```
(This script is complete, it should run "as is")
22 changes: 22 additions & 0 deletions docs/async_methods/query_single_or_default_async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

`query_single_or_default_async` can execute a query and serialize the first result, or return a default value if the result
set is empty; this method throws an exception if there is more than one element in the result set.

## Parameters
| name | type | description | optional | default |
|---------|-------------|-----------------------------------------------------------------------------------------------|--------------|---------|
| sql | `str` | the sql query str to execute | :thumbsdown: | |
| default | `Any` | any object to return if the result set is empty | :thumbsdown: |
| param | `ParamType` | params to substitute in the query | :thumbsup: | `None` |
| model | `Any` | the callable to serialize the model; callable must be able to accept column names as kwargs. | :thumbsup: | `dict` |


## First, Single and Default
{!docs/.first_single_default.md!}

## Example
Execute a query and map the result to a dataclass.
```python
{!docs/../docs_src/async_methods/query_single_or_default/example.py!}
```
(This script is complete, it should run "as is")
29 changes: 28 additions & 1 deletion docs/database_support/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,25 @@ Below is a generic example of using *pydapper* to connect to `sqlite`.
```python
import pydapper

with pydapper.connect("some.db") as commands:
with pydapper.connect() as commands:
# do stuff
```

### `connect_async`
*connect_async* will manage an asynchronous connection for you when using a dsn of a supported async dbapi. The api
is almost identical to that of the sync api.

```python
import pydapper
import asyncio

async def main():
async with pydapper.connect_async() as commands:
# do stuff

asyncio.run(main())
```

### `using`
You should use the `using` method when you want to use your own connection. A use case
for this could be if you have a custom connection pool in your application and you don't want a framework
Expand All @@ -69,3 +84,15 @@ What's going on here?
* grab the actual dbapi connection object, which is stored in the `connection` property of the Django
connection proxy
* pass the dbapi connection object into `pydapper.using` and get a pydapper `Commands` instance back

### `using_async`
You should use the `using_async` method when you want to use your own connection. The api is almost identical to that
of the sync api.

```python
import pydapper

some_pool = ConnectionPool()
conn = await some_pool.acquire()
commands = pydapper.using_async(conn)
```
7 changes: 4 additions & 3 deletions docs/database_support/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Supported drivers:
| [mysql-connector-python](https://dev.mysql.com/doc/connector-python/en/) | :thumbsup: | `mysql+mysql` | `mysql.connector.connection_cext.CMySQLConnection` |

## mysql-connector-python
`mysql-connector-python` is the default dbapi driver for MySQL in *pydapper*.
`mysql-connector-python` is the default dbapi driver for MySQL in *pydapper*. It is actually registered as `mysql`
because that is the name of the actual package that is installed.

!!! note
Because of the build in behavior of `mysql-connector-python`, it is currently required to run `cursor.fetchall()`
Expand All @@ -28,12 +29,12 @@ Supported drivers:
### DSN format
=== "Template"
```python
dsn = f"mysql+mysql-connector-python://{user}:{password}@{host}:{port}/{dbname}"
dsn = f"mysql+mysql://{user}:{password}@{host}:{port}/{dbname}"
```

=== "Example"
```python
dsn = "mysql+mysql-connector-python://myuser:mypassword:3306@localhost/mydb"
dsn = "mysql+mysql://myuser:mypassword:3306@localhost/mydb"
```

=== "Example (Default Driver)"
Expand Down
41 changes: 41 additions & 0 deletions docs/database_support/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,44 @@ Use *pydapper* with a `psycopg2` connection pool.
```python
{!docs/../docs_src/connections/psycopg2_using.py!}
```

## aiopg

### Installation
=== "pip"
```console
pip install pydapper[aiopg]
```

=== "poetry"
```console
poetry add pydapper -E aiopg
```

### DSN format
=== "Template"
```python
dsn = f"postgresql+aiopg://{user}:{password}@{host}:{port}/{dbname}"
```

=== "Example"
```python
dsn = "postgresql+aiopg://myuser:mypassword:1521@localhost/myservicename"
```

### Example - `connect_async`
Please see the [aiopg docs](https://aiopg.readthedocs.io/en/stable/) for a full description of the
context manager behavior.
```python
{!docs/../docs_src/connections/aiopg_connect.py!}
```

!!! note
`aiopg` always runs in [autocommit mode](https://aiopg.readthedocs.io/en/stable/core.html#aiopg.Connection.autocommit).


### Example - `using_async`
Use *pydapper* with a `aiopg` connection pool.
```python
{!docs/../docs_src/connections/aiopg_using.py!}
```
Loading

0 comments on commit 6eaa4dc

Please sign in to comment.