Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add async support starting with aiopg #22

Merged
merged 7 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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