Skip to content

Commit f1e5154

Browse files
committed
Adding signals, updated docs, Transaction now depends on Session and cannot be used independently anymore
1 parent 1220557 commit f1e5154

15 files changed

+464
-102
lines changed

docs/engine.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,18 @@ engine = Engine.from_uri("sqlite://:memory:") # sqlite instead of sqlite3 to us
4242
engine = Engine.from_uri("psycopg://?host=localhost&port=5432") # using the dbapi module name
4343
```
4444

45-
Once initialized, an engine can be used as a context to start sessions.
45+
Once initialized, an engine can be used as a context to [start sessions](sessions-and-transactions.md).
46+
47+
## Connecting
48+
49+
You shouldn't create connection manually when using sqlorm but use sessions.
50+
51+
To create connections use `conn = engine.connect()`.
52+
To close a connection use `engine.disconnect(conn)`.
53+
54+
## Connection pool
4655

4756
Connections are pooled and re-used by default. You can disabled this behavior by using `pool=False` in the engine constructor.
4857
`max_pool_conns` can also be used to define the maximum number of connections to start.
58+
59+
Use `engine.disconnect_all()` to close all connections.

docs/executing.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ with engine as tx:
2828
tx.execute(stmt)
2929
```
3030

31-
[`SQL` utilities](#sql-utilities) to build sql statements can also be used:
31+
[`SQL` utilities](sql-utilities.md) to build sql statements can also be used:
3232

3333
```python
3434
from sqlorm import SQL
@@ -94,7 +94,7 @@ with engine as tx:
9494
> - The process of populating objects with data from the database is called "hydration".
9595
> - When creating objects, `__init__()` will be bypassed as well as any custom `__setattr__`.
9696
>
97-
> Learn more about sqlorm hydration process in the [mapper section](#hydrate-objects)
97+
> Learn more about sqlorm hydration process in the [mapper section](mapper.md#hydrate-objects)
9898
9999
You can update existing objects by passing an object instance to the `obj` argument.
100100
In this case, only the first row will be used to hydrate the object. The cursor is then closed.
@@ -107,7 +107,7 @@ with engine as tx:
107107
assert task.title == "task title"
108108
```
109109

110-
[Models](#models) and [mappers](#mapping-any-class) can be used to customize how objects are mapped.
110+
[Models](models.md) and [mappers](mapper.md) can be used to customize how objects are mapped.
111111
`Mapper` instances can also be provided as models.
112112

113113
## Composite rows
@@ -183,7 +183,7 @@ with engine as tx:
183183
rows = tx.fetchcomposite("SELECT ... FROM posts LEFT JOIN comments ...")
184184
```
185185

186-
Using the [`SQL` utilities](#sql-utilities) makes it easier to build these kind of queries:
186+
Using the [`SQL` utilities](sql-utilities.md) makes it easier to build these kind of queries:
187187

188188
```python
189189
with engine as tx:

docs/index.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ Sqlorm intends to provide a solution where SQL stays front and center and where
1010

1111
## Getting started
1212

13-
Create an [`Engine`](#the-engine) that will manage database connections for you:
13+
Create an [`Engine`](engine.md) that will manage database connections for you:
1414

1515
```python
1616
from sqlorm import Engine
1717

1818
engine = Engine.from_uri("sqlite://:memory:")
1919
```
2020

21-
Use the engine as a [context to connect to the database](#sessions-and-transactions). A transaction object is provided to execute statements.
21+
Use the engine as a [context to connect to the database](sessions-and-transactions.md). A transaction object is provided to execute statements.
2222
If no exception is raised in the context, the transaction will be committed, rollbacked otherwise.
2323

2424
sqlorm's `Transaction` has a similar API than DBAPI's Connection but instead of using a cursor,
25-
methods return [result sets](#fetching-from-the-database) that you can iterate over:
25+
methods return [result sets](executing.md#fetching-from-the-database) that you can iterate over:
2626

2727
```python
2828
with engine as tx:
@@ -46,7 +46,7 @@ with engine as tx:
4646
print(task1.title)
4747
```
4848

49-
To facilitate managing sql statements, you can create ["sql functions"](#sql-functions).
49+
To facilitate managing sql statements, you can create ["sql functions"](sql-functions.md).
5050
These are functions which only have a doc string containing the SQL statement.
5151

5252
```python
@@ -76,7 +76,7 @@ with engine:
7676

7777
Note: these functions must be executed in the context of a database session (or be bound to an engine)
7878

79-
Finally, we can create [model classes](#models) that can provide custom mapping information:
79+
Finally, we can create [model classes](models.md) that can provide custom mapping information:
8080

8181
```python
8282
from sqlorm import Model, PrimaryKey, create_table

docs/mapper.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ with engine as tx:
113113
tx.execute(mapper.delete(task))
114114
```
115115

116-
The mapper column list `mapper.columns` is an [`SQL.Cols`](#manage-list-of-sql-pieces) object which means it can be used to generate sql conditions:
116+
The mapper column list `mapper.columns` is an [`SQL.Cols`](sql-utilities.md#manage-list-of-sql-pieces) object which means it can be used to generate sql conditions:
117117

118118
```python
119119
with engine as tx:

docs/models.md

+32-8
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ Any class can be used as a model. However, subclassing sqlorm's `Model` class pr
77
- Auto fetching lazy attributes and relationships when accessed
88

99
> [!NOTE]
10-
> When using classes that do not subclass `Model`, `Mapper.from_class()` is used to generate a mapping. See [Mapping any class](#mapping-any-class).
10+
> When using classes that do not subclass `Model`, `Mapper.from_class()` is used to generate a mapping. See [Mapping any class](mapper.md).
1111
1212
## Defining models
1313

1414
Models are classes inheriting from `Model`. To define which table they represent, use the `table` class property.
1515

16-
To define column mapping, define properties via annotations. The type used will be [converted to an sql type](#column-types).
16+
To define column mapping, define properties via annotations. The type used will be [converted to an sql type](sql-utilities.md#column-types).
1717
For more control over the mapping, you can use instantiate `Column()` objects:
1818

1919
```python
@@ -29,7 +29,7 @@ class Task(Model):
2929

3030
Once columns are defined via annotations or `Column` properties, they are accessible as class and instance properties.
3131

32-
As class properties, they can be used as a [composable piece of sql](#sql-utilities):
32+
As class properties, they can be used as a [composable piece of sql](sql-utilities.md):
3333

3434
```python
3535
stmt = SQL("SELECT * FROM tasks WHERE", Task.done == False)
@@ -56,7 +56,7 @@ task.title = "title"
5656

5757
## SQL methods on models
5858

59-
Create SQL methods as you would [SQL functions](#sql-functions). Use `@classmethod` and `@staticmethod` decorator when needed.
59+
Create SQL methods as you would [SQL functions](sql-functions.md). Use `@classmethod` and `@staticmethod` decorator when needed.
6060

6161
```python
6262
class Task(Model):
@@ -140,7 +140,7 @@ However, `Model` also exposes easier to use ways to fetch data using without the
140140
When called out of a transaction context, a non commited transaction will automatically be started. If bound to an engine,
141141
these class methods can also be called out of a session context.
142142

143-
- `query()` executes the provided statement using [`fetchhydrated()`](#fetching-composite-objects)
143+
- `query()` executes the provided statement using [`fetchhydrated()`](executing.md#fetching-composite-objects)
144144
- `find_all()` constructs a select statement based on the provided arguments and executes using `query()`
145145
- `find_one()` same as `find_all()` but only returns the first row
146146
- `get()` to find one row by primary key
@@ -160,8 +160,8 @@ with engine:
160160
> [!TIP]
161161
> You can also build select statement with auto populated column list and from clause using `Model.select_from()`.
162162
163-
Mapped columns can easily be used as [pieces of composable sql](#sql-utilities): accessing the class attribute representing
164-
the column returns an [`SQL.Col` object](#manage-list-of-sql-pieces) that can be used with python operators to return
163+
Mapped columns can easily be used as [pieces of composable sql](sql-utilities.md): accessing the class attribute representing
164+
the column returns an [`SQL.Col` object](sql-utilities.md#manage-list-of-sql-pieces) that can be used with python operators to return
165165
sql conditions:
166166

167167
```python
@@ -182,6 +182,8 @@ Manipulate model objects as you would with any python objects. The following met
182182
- `refresh()` executes a select statement (same as `get()`) and updates the object attribute values
183183
- `create()` a class method to create and insert an object in one line
184184

185+
These methods (apart from `create()`) return a boolean indicating if the operation was performed.
186+
185187
> [!NOTE]
186188
> DML (Data Manipulation Language) statements are the statement that modify data in the database (insert, update and delete mostly)
187189
@@ -273,7 +275,7 @@ with engine:
273275
> You should not disable dirty tracking when allowing unknown columns otherwise setting attributes
274276
> will not result in them being used in DML statements unless they are mapped.
275277
> When dirty tracking is disabled and you are using unknown attributes, the only way sqlorm keeps
276-
> track of them is through the [`__hydrated_attrs__` attribute](#dehydrating-objects).
278+
> track of them is through the [`__hydrated_attrs__` attribute](mapper.md#dehydrating-objects).
277279
278280
You can disallow unknown columns
279281
@@ -445,4 +447,26 @@ with engine:
445447
446448
## Column types
447449
450+
Columns can have a type which defines the SQL type and serialization/deserialization functions.
451+
452+
!!! note
453+
The goal is not to re-define sql types in python. Types are optional in your column definitions.
454+
sqlorm relies mainly on the underlying DBAPI driver to do the conversion. Drivers have custom methods to
455+
provide type mapping.
456+
457+
Define types using `SQLType`:
458+
459+
```py
460+
from sqlorm import SQLType
461+
import json
462+
463+
JSON = SQLType("json", json.loads, json.dumps)
464+
465+
class MyModel(Model):
466+
my_json_column = Column(type=JSON)
467+
```
468+
469+
The following types are already defines and importable from the `sqlorm` package:
470+
`Integer`, `Decimal`, `Varchar`, `Text`, `Boolean`, `Date`, `Time`, `DateTime`, `JSON`, `Pickle`.
448471
472+
Python types from annotations will automatically used one of these type when appropriate (see `sqlorm.types.PYTHON_TYPES_MAP`).

docs/schema.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ with engine:
7272
7373
SQL files can contain multiple statements.
7474

75-
In python files, use `get_current_session()` to create a transaction context:
75+
In python files, use `ensure_transaction()` to create a transaction context:
7676

7777
```python
7878
# 002_seed_data.py
79-
from sqlorm import get_current_session
79+
from sqlorm import ensure_transaction
8080

81-
with get_current_session() as tx:
81+
with ensure_transaction() as tx:
8282
tx.execute("...")
8383
```
8484

docs/sessions-and-transactions.md

+5-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ with engine as tx:
1616
# rollback
1717
```
1818

19-
The next section covers [how to use transactions](#executing-queries).
19+
The next section covers [how to use transactions](executing.md).
2020

2121
> [!NOTE]
2222
> You can create transactions inside transactions with no impact (only the top-most transaction will commit)
@@ -44,8 +44,8 @@ with engine.session() as sess:
4444
# rollback
4545
```
4646
47-
> [!IMPORTANT]
48-
> sqlorm does not assume anything regarding thread safety. However, session contexts are scoped to each thread using thread locals.
47+
> [!WARNING]
48+
> Session contexts are scoped to threads using thread locals.
4949
> This means that using drivers which are not thread-safe (like sqlite) is not an issue as long as you are using the engine
5050
> to start sessions. Doing `with engine.session()` and `with engine:` in different threads will start different sessions.
5151
@@ -83,17 +83,14 @@ count_tasks() # raises MissingEngineError
8383
```
8484

8585
> [!TIP]
86-
> `Session` and `Transaction` objects can be created using raw DBAPI connection objects
86+
> `Session` can be created using raw DBAPI connection objects
8787
>
8888
> ```python
89-
> from sqlorm import Session, Transaction
89+
> from sqlorm import Session
9090
> import sqlite3
9191
>
9292
> conn = sqlite3.connect(":memory:")
9393
>
94-
> with Transaction(conn) as tx:
95-
> # ...
96-
>
9794
> with Session(conn) as sess:
9895
> with sess as tx:
9996
> # ...

docs/signals.md

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Signals
2+
3+
Signals allow you to listen and react to events emitted by sqlorm. The [blinker](https://blinker.readthedocs.io) lib is used to implement signals.
4+
5+
## Engine
6+
7+
The following signals exist on the `Engine` class. The sender is always the engine instance.
8+
9+
- `connected`: receive `conn` (connection instance) and `from_pool` (bool)
10+
- `disconnected`: receive `conn` (connection instance) and `close_conn` (bool). This signal may not indicate a connection has been closed but only returned to the pool. Use the `close_conn` kwargs to distinguish.
11+
12+
Example:
13+
14+
```py
15+
from sqlorm import Engine
16+
17+
@Engine.connected.connect
18+
def on_connected(engine, conn, from_pool):
19+
# do something
20+
```
21+
22+
## Session
23+
24+
The following signals exist on the `Session` class. The sender is always the session instance.
25+
26+
- `before_commit`
27+
- `after_commit`
28+
- `before_rollback`
29+
- `after_rollback`
30+
31+
Use `session.conn` to retreive the connection object (may be None if no connection have been established, use `session.connect()` in this case).
32+
33+
Example:
34+
35+
```py
36+
from sqlorm import Session
37+
38+
@Session.before_commit.connect
39+
def on_before_commit(session):
40+
# do something
41+
```
42+
43+
## Transaction
44+
45+
The following signals exist on the `Transaction` class. The sender is always the transaction instance.
46+
47+
- `before_execute`: receive `stmt` and `params`. Returning a cursor will stop sqlorm execute() and return the cursor directly
48+
- `before_executemany`: receive `stmt` and `seq_of_parameters`. Returning false will stop sqlorm executemany()
49+
50+
## Model
51+
52+
The following signals exist on the `Model` class. The sender is always the model class.
53+
54+
- `before_query`: receive `stmt` and `params` args. can override them by returning a tuple `(stmt, params)`
55+
56+
The following signals receive `obj` kwargs containing the model instance.
57+
Returning `False` from any listener will cancel the operation. Returning a value will override the statement.
58+
59+
- `before_refresh`
60+
- `before_insert`
61+
- `before_update`
62+
- `before_delete`
63+
- `before_save` (receive a `is_new` kwargs, can only cancel operations)
64+
65+
The following signals are sent only if an operation is performed. They receive the `obj` kwargs containing the model instance.
66+
67+
- `after_refresh`
68+
- `after_insert`
69+
- `after_update`: receive also `updated` a boolean indicating if an update stmt was actually performed
70+
- `after_save`
71+
- `after_delete`
72+
73+
Examples:
74+
75+
```py
76+
from sqlorm import Model
77+
78+
@Model.before_query.connect
79+
def on_before_query(model_class, stmt, params):
80+
# do something
81+
82+
@Model.before_query.connect_via(MyModel)
83+
def on_my_model_before_query(model_class, stmt, params):
84+
# do something only on before_query of MyModel
85+
86+
@Model.before_insert.connect
87+
def on_before_insert(model_class, obj, stmt):
88+
return False # cancel the insert
89+
90+
@Model.before_insert.connect
91+
def on_before_insert(model_class, obj, stmt):
92+
return SQL.insert(model_class.table, {"col": "value"}) # return custom statement
93+
```

mkdocs.yml

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ nav:
1212
- models.md
1313
- sql-utilities.md
1414
- mapper.md
15+
- signals.md
1516
- schema.md
1617
- drivers.md
1718

@@ -35,6 +36,7 @@ theme:
3536
name: Switch to light mode
3637
features:
3738
- content.action.edit
39+
- content.code.copy
3840
- toc.integrate
3941
icon:
4042
edit: material/pencil
@@ -46,7 +48,4 @@ markdown_extensions:
4648

4749
plugins:
4850
- search
49-
- callouts
50-
- git-committers:
51-
repository: hyperflask/sqlorm
52-
branch: main
51+
- callouts

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ packages = [{include = "sqlorm"}]
1010

1111
[tool.poetry.dependencies]
1212
python = "^3.10"
13+
blinker = "^1.8.2"
1314

1415
[tool.poetry.group.postgresql.dependencies]
1516
psycopg = {extras = ["binary"], version = "^3.1.18"}
@@ -23,7 +24,6 @@ pytest-cov = "^4.1.0"
2324
ruff = "^0.4.3"
2425
mkdocs-material = "^9.5.24"
2526
mkdocs-callouts = "^1.13.2"
26-
mkdocs-git-committers-plugin-2 = "^2.3.0"
2727

2828
[tool.ruff]
2929
include = ["sqlorm/**/*.py"]

0 commit comments

Comments
 (0)