Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 2eb1327
Author: Livio Ribeiro <[email protected]>
Date:   Sun Apr 7 12:18:02 2024 -0300

    bump version to 0.13.0

commit 29d5263
Author: Livio Ribeiro <[email protected]>
Date:   Sun Apr 7 12:17:11 2024 -0300

    change di to not resolve unnamed dependency in place of named ones (#44)

commit 842dd1a
Author: Livio Ribeiro <[email protected]>
Date:   Fri Apr 5 08:51:19 2024 -0300

    bump version to 0.12.0

commit 7710e52
Author: Livio Ribeiro <[email protected]>
Date:   Fri Apr 5 08:50:36 2024 -0300

    Sqlalchemy session rework (#43)

    * rework sqlalchemy session

    * update sqlalchemy documentation

commit a7ff0f9
Author: Livio Ribeiro <[email protected]>
Date:   Wed Apr 3 16:07:36 2024 -0300

    change sqlalchemy sessionmaker_service

    sessionmake_service returns a single async_sessionmaker, bound to all defined engines, instead of having one async_sessionmaker to each engine

commit 40b362d
Author: Livio Ribeiro <[email protected]>
Date:   Wed Apr 3 15:42:31 2024 -0300

    change middleware strategy

commit 75e6720
Author: Livio Ribeiro <[email protected]>
Date:   Fri Mar 29 18:09:30 2024 -0300

    bump version to 0.11.2

commit 09583d1
Author: Livio Ribeiro <[email protected]>
Date:   Fri Mar 29 18:09:07 2024 -0300

    fix issue with dependency resolution that generates loops

commit 5747bbd
Author: Livio Ribeiro <[email protected]>
Date:   Thu Mar 28 13:25:10 2024 -0300

    add python 3.12 trove classifier

commit 1c0cf27
Author: Livio Ribeiro <[email protected]>
Date:   Thu Mar 28 13:24:48 2024 -0300

    change tests to use psycopg

commit c834117
Author: Livio Ribeiro <[email protected]>
Date:   Thu Mar 21 13:19:41 2024 -0300

    bump version to v0.11.1

commit ccec934
Author: Livio Ribeiro <[email protected]>
Date:   Thu Mar 21 10:22:37 2024 -0300

    Refactor/di (#42)

    refactor di

commit 99304dc
Author: Livio Ribeiro <[email protected]>
Date:   Thu Feb 22 08:46:14 2024 -0300

    bump version to 0.11.0

commit 1c1dda6
Author: Livio Ribeiro <[email protected]>
Date:   Thu Feb 22 08:45:24 2024 -0300

    add flag to @service decorator to create and initialize the service on application startup (#41)
  • Loading branch information
livioribeiro committed Apr 7, 2024
1 parent df61ff7 commit 49561e4
Show file tree
Hide file tree
Showing 51 changed files with 1,116 additions and 577 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
--extras memcached
--no-interaction
- env:
POSTGRES_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/test
POSTGRES_URL: postgresql+psycopg://postgres:postgres@postgres:5432/test
REDIS_URL: redis://default:password@redis:6379/0
MEMCACHED_ADDR: memcached:11211
run: poetry run pytest
286 changes: 175 additions & 111 deletions docs/extensions/sqlalchemy.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# SQLAlchemy

The SQLAlchemy extension makes it easy to set up database connections, providing
the classes `AsyncEngine` and `async_sessionmaker` as services in the dependency
injection context.
`AsyncEngine` and `async_sessionmaker` as services in the dependency injection context.

## Usage

Expand All @@ -12,26 +11,28 @@ Install SQLAlchemy extra and a database driver that supports async:
pip install selva[sqlalchemy] aiosqlite asyncpg aiomysql oracledb
```

Define the configuration properties:
With database drivers are installed, we can define the connections in the
configuration file:

```yaml
extensions:
- selva.ext.data.sqlalchemy # (1)

data:
sqlalchemy:
default: # (2)
url: "sqlite+aiosqlite:///var/db.sqlite3"
connections:
default: # (2)
url: "sqlite+aiosqlite:///var/db.sqlite3"

postgres: # (3)
url: "postgresql+asyncpg://user:pass@localhost/dbname"
postgres: # (3)
url: "postgresql+asyncpg://user:pass@localhost/dbname"

mysql: # (4)
url: "mysql+aiomysql://user:pass@localhost/dbname"
mysql: # (4)
url: "mysql+aiomysql://user:pass@localhost/dbname"

oracle: # (5)
url: "oracle+oracledb_async://user:pass@localhost/DBNAME"
# or "oracle+oracledb_async://user:pass@localhost/?service_name=DBNAME"
oracle: # (5)
url: "oracle+oracledb_async://user:pass@localhost/DBNAME"
# or "oracle+oracledb_async://user:pass@localhost/?service_name=DBNAME"
```

1. Activate the sqlalchemy extension
Expand All @@ -40,23 +41,26 @@ data:
4. Connection registered with name "mysql"
5. Connection registered with name "oracle"

And inject the `async_sessionmaker` services:
Once we define the connections, we can inject `AsyncEngine` into our services.
For each connection, an instance of `AsyncEngine` will be registered, the `default`
connection will be registered wihout a name, and the other will be registered with
their respective names:

```python
from typing import Annotated
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncEngine
from selva.di import service, Inject


@service
class MyService:
# default service
sessionmaker: Annotated[async_sessionmaker, Inject]
engine: Annotated[AsyncEngine, Inject]

# named services
sessionmaker_postgres: Annotated[async_sessionmaker, Inject(name="postgres")]
sessionmaker_mysql: Annotated[async_sessionmaker, Inject(name="mysql")]
sessionmaker_oracle: Annotated[async_sessionmaker, Inject(name="oracle")]
engine_postgres: Annotated[AsyncEngine, Inject(name="postgres")]
engine_mysql: Annotated[AsyncEngine, Inject(name="mysql")]
engine_oracle: Annotated[AsyncEngine, Inject(name="oracle")]
```

Database connections can also be defined with username and password separated from
Expand All @@ -65,30 +69,31 @@ the url, or even with individual components:
```yaml
data:
sqlalchemy:
default:
drivername: sqlite+aiosqlite
database: "/var/db.sqlite3"

postgres: # (1)
url: "postgresql+asyncpg://localhost/dbname"
username: user
password: pass

mysql: # (2)
drivername: mysql+aiomysql
host: localhost
port: 3306
database: dbname
username: user
password: pass

oracle: # (3)
drivername: oracle+oracledb_async
host: localhost
port: 1521
database: DBNAME # (4)
username: user
password: pass
connections:
default:
drivername: sqlite+aiosqlite
database: "/var/db.sqlite3"

postgres: # (1)
url: "postgresql+asyncpg://localhost/dbname"
username: user
password: pass

mysql: # (2)
drivername: mysql+aiomysql
host: localhost
port: 3306
database: dbname
username: user
password: pass

oracle: # (3)
drivername: oracle+oracledb_async
host: localhost
port: 1521
database: DBNAME # (4)
username: user
password: pass
```
1. Username and password separated from the database url
Expand All @@ -102,39 +107,79 @@ data:
## Using environment variables
It is a good pratctice to externalize configuration through environment variables.
We can either reference the variables in the configuration or use variables with
the `SELVA__` prefix, for example, `SELVA__DATA__SQLALCHEMY__CONNECTIONS__DEFAULT__URL`.

=== "configuration/settings.yaml"
```yaml
data:
sqlalchemy:
default:
url: "${DATABASE_URL}" # (1)

other: # (2)
url: "${DATABASE_URL}"
username: "${DATABASE_USERNAME}"
password: "${DATABASE_PASSWORD}"
connections:
default:
url: "${DATABASE_URL}" # (1)
other: # (2)
url: "${DATABASE_URL}"
username: "${DATABASE_USERNAME}"
password: "${DATABASE_PASSWORD}"
another: # (3)
drivername: "${DATABASE_DRIVERNAME}"
host: "${DATABASE_HOST}"
port: ${DATABASE_PORT}
database: "${DATABASE_NAME}"
username: "${DATABASE_USERNAME}"
password: "${DATABASE_PASSWORD}"
another: # (3)
drivername: "${DATABASE_DRIVERNAME}"
host: "${DATABASE_HOST}"
port: ${DATABASE_PORT}
database: "${DATABASE_NAME}"
username: "${DATABASE_USERNAME}"
password: "${DATABASE_PASSWORD}"
```

1. Can be define with just the environment variable `SELVA__DATA__SQLALCHEMY__DEFAULT__URL`
2. Can be defined with just the environment variables:
- `SELVA__DATA__SQLALCHEMY__OTHER__URL`
- `SELVA__DATA__SQLALCHEMY__OTHER__USERNAME`
- `SELVA__DATA__SQLALCHEMY__OTHER__PASSWORD`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__OTHER__URL`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__OTHER__USERNAME`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__OTHER__PASSWORD`
3. Can be defined with just the environment variables:
- `SELVA__DATA__SQLALCHEMY__ANOTHER__DRIVERNAME`
- `SELVA__DATA__SQLALCHEMY__ANOTHER__HOST`
- `SELVA__DATA__SQLALCHEMY__ANOTHER__PORT`
- `SELVA__DATA__SQLALCHEMY__ANOTHER__DATABASE`
- `SELVA__DATA__SQLALCHEMY__ANOTHER__USERNAME`
- `SELVA__DATA__SQLALCHEMY__ANOTHER__PASSWORD`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__DRIVERNAME`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__HOST`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__PORT`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__DATABASE`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__USERNAME`
- `SELVA__DATA__SQLALCHEMY__CONNECTIONS__ANOTHER__PASSWORD`

## Working with async_sessionmaker

Different from the `AsyncEngine`, Selva only creates a single `async_sessionmaker`.
We can bind specific subclasses of `DeclarativeBase` through the `data.sqlalchemy.session.binds`
configuration, otherwise it is bound to just the `default` connection.

=== "application/model.py"

```python
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class OtherBase(DeclarativeBase):
pass
```

=== "configuration/settings.yaml"

```yaml
data:
sqlalchemy:
connections:
default:
url: "sqlite+aiosqlite://db1.sqlite3"
other:
url: "sqlite+aiosqlite://db2.sqlite3"
session:
binds:
application.model.Base: default
application.model.OtherBase: other
```

## Example

Expand Down Expand Up @@ -208,59 +253,78 @@ data:
## Configuration options

Selva offers several options to configure SQLAlchemy. If you need more control over
the SQLAlchemy services, you can create your own `engine` and `sessionmaker` outside
of the DI context.
the SQLAlchemy services, you can create your own `AsyncEngine` and `async_sessionmaker`
outside of the DI context.

The available options are shown below:

```yaml
data:
sqlalchemy:
default:
url: ""
session:
options: # (1)
connect_args: # (2)
arg: value
echo: false
echo_pool: false
enable_from_linting: false
hide_parameters: false
insertmanyvalues_page_size: 1
isolation_level: ""
json_deserializer: "json.loads" # dotted path to the json deserialization function
json_serializer: "json.dumps" # dotted path to the json serialization function
label_length: 1
logging_name: ""
max_identifier_length: 1
max_overflow: 1
module: ""
paramstyle: "qmark" # or "numeric", "named", "format", "pyformat"
poolclass: "sqlalchemy.pool.Pool" # dotted path to the pool class
pool_logging_name: ""
pool_pre_ping: false
pool_size: 1
pool_recycle: 3600
pool_reset_on_return: "rollback" # or "commit"
pool_timeout: 1
pool_use_lifo: false
plugins:
- "plugin1"
- "plugin2"
query_cache_size: 1
use_insertmanyvalues: false
execution_options: # (3)
logging_token: ""
isolation_level: ""
no_parameters: false
stream_results: false
max_row_buffer: 1
yield_per: 1
class: sqlalchemy.ext.asyncio.AsyncSession
autoflush: true
expire_on_commit: true
autobegin: true
twophase: false
enable_baked_queries: true
info:
key: value
query_cls: sqlalchemy.orm.query.Query
join_transaction_mode: conditional_savepoint
close_resets_only: null
binds: # (2)
application.model.Base: default
application.model.OtherBase: other
connections:
default:
url: ""
options: # (3)
connect_args: # (4)
arg: value
echo: false
echo_pool: false
enable_from_linting: false
hide_parameters: false
insertmanyvalues_page_size: 1
schema_translate_map:
null: "my_schema"
some_schema: "other_schema"
isolation_level: ""
json_deserializer: "json.loads" # dotted path to the json deserialization function
json_serializer: "json.dumps" # dotted path to the json serialization function
label_length: 1
logging_name: ""
max_identifier_length: 1
max_overflow: 1
module: ""
paramstyle: "qmark" # or "numeric", "named", "format", "pyformat"
poolclass: "sqlalchemy.pool.Pool" # dotted path to the pool class
pool_logging_name: ""
pool_pre_ping: false
pool_size: 1
pool_recycle: 3600
pool_reset_on_return: "rollback" # or "commit"
pool_timeout: 1
pool_use_lifo: false
plugins:
- "plugin1"
- "plugin2"
query_cache_size: 1
use_insertmanyvalues: false
execution_options: # (5)
logging_token: ""
isolation_level: ""
no_parameters: false
stream_results: false
max_row_buffer: 1
yield_per: 1
insertmanyvalues_page_size: 1
schema_translate_map:
null: "my_schema"
some_schema: "other_schema"
```

1. `options` values are described in [`sqlalchemy.create_engine`](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
2. `connect_args` is a map of args to pass to the `connect` function of the underlying driver
3. `execution_options` values are describe in [`Sqlalchemy.engine.Connection.execution_options`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Connection.execution_options)
1. Values are describe in [`sqlalchemy.orm.Session`](https://docs.sqlalchemy.org/orm/session_api.html#sqlalchemy.orm.Session)
2. Binds subclasses of `sqlalchemy.orm.DeclarativeBase` to connection names defined in `connections`
3. Values are described in [`sqlalchemy.create_engine`](https://docs.sqlalchemy.org/core/engines.html#sqlalchemy.create_engine)
4. `connect_args` is a map of args to pass to the `connect` function of the underlying driver
5. `execution_options` values are describe in [`Sqlalchemy.engine.Connection.execution_options`](https://docs.sqlalchemy.org/core/connections.html#sqlalchemy.engine.Connection.execution_options)
Loading

0 comments on commit 49561e4

Please sign in to comment.