lica-sqa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lica_sqa/__init__.py +30 -0
- lica_sqa/adapters/__init__.py +0 -0
- lica_sqa/adapters/asyncio/__init__.py +0 -0
- lica_sqa/adapters/asyncio/decouple.py +55 -0
- lica_sqa/adapters/decouple.py +46 -0
- lica_sqa/metadata/__init__.py +30 -0
- lica_sqa/metadata/asyncio.py +28 -0
- lica_sqa/ports/__init__.py +20 -0
- lica_sqa/ports/asyncio.py +20 -0
- lica_sqa/view.py +63 -0
- lica_sqa-0.1.0.dist-info/METADATA +261 -0
- lica_sqa-0.1.0.dist-info/RECORD +14 -0
- lica_sqa-0.1.0.dist-info/WHEEL +4 -0
- lica_sqa-0.1.0.dist-info/licenses/LICENSE +21 -0
lica_sqa/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ----------------
|
|
7
|
+
# standard library
|
|
8
|
+
# ----------------
|
|
9
|
+
import logging
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("lica-sqa")
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
__version__ = "0.0.0+dev"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .ports import Database
|
|
19
|
+
from .metadata import Model, metadata
|
|
20
|
+
from .view import view
|
|
21
|
+
|
|
22
|
+
def sqa_logging(debug: bool) -> None:
|
|
23
|
+
if debug:
|
|
24
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
|
25
|
+
# This is needed, otherwise aiosqlite msgs will jump in
|
|
26
|
+
logging.getLogger("aiosqlite").setLevel(logging.INFO)
|
|
27
|
+
else:
|
|
28
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
29
|
+
|
|
30
|
+
__all__ = ["sqa_logging","Database", "Model", "metadata", "view"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ----------------
|
|
7
|
+
# standard library
|
|
8
|
+
# ----------------
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
# ---------------------
|
|
12
|
+
# Third party libraries
|
|
13
|
+
# ---------------------
|
|
14
|
+
import decouple
|
|
15
|
+
from sqlalchemy.ext.asyncio import (
|
|
16
|
+
AsyncEngine,
|
|
17
|
+
AsyncSession,
|
|
18
|
+
async_sessionmaker,
|
|
19
|
+
create_async_engine,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# -----------------
|
|
23
|
+
# Local dependecies
|
|
24
|
+
# -----------------
|
|
25
|
+
from ...ports.asyncio import AsyncDatabase
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncDcplDatabase(AsyncDatabase):
|
|
29
|
+
"""main entry point instatinates this class"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, env_var: str = "DATABASE_URL") -> None:
|
|
32
|
+
self._env_var = env_var
|
|
33
|
+
|
|
34
|
+
def init(self) -> None:
|
|
35
|
+
url: str = decouple.config(self._env_var)
|
|
36
|
+
self._engine = create_async_engine(
|
|
37
|
+
url,
|
|
38
|
+
pool_pre_ping=True,
|
|
39
|
+
connect_args={"check_same_thread": False},
|
|
40
|
+
)
|
|
41
|
+
self._session_factory = async_sessionmaker(
|
|
42
|
+
self._engine, autoflush=False, expire_on_commit=False
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@asynccontextmanager
|
|
46
|
+
async def session(self) -> AsyncGenerator[AsyncSession]:
|
|
47
|
+
async with self._session_factory() as _session:
|
|
48
|
+
yield _session
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def engine(self) -> AsyncEngine:
|
|
52
|
+
return self._engine
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["DcplDatabase", "AsyncDcplDatabase"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ----------------
|
|
7
|
+
# standard library
|
|
8
|
+
# ----------------
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
|
|
12
|
+
import decouple # pyright: ignore[reportMissingImports]
|
|
13
|
+
from sqlalchemy import create_engine
|
|
14
|
+
from sqlalchemy.engine.base import Engine
|
|
15
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
16
|
+
|
|
17
|
+
# ---------------
|
|
18
|
+
# Own dependecies
|
|
19
|
+
# ---------------
|
|
20
|
+
from ..ports import Database
|
|
21
|
+
|
|
22
|
+
class DcplDatabase(Database):
|
|
23
|
+
"""main entry point instatinates this class"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, env_var: str = "DATABASE_URL") -> None:
|
|
26
|
+
self._env_var = env_var
|
|
27
|
+
|
|
28
|
+
def init(self) -> None:
|
|
29
|
+
url: str = decouple.config(self._env_var)
|
|
30
|
+
self._engine = create_engine(
|
|
31
|
+
url,
|
|
32
|
+
pool_pre_ping=True,
|
|
33
|
+
connect_args={"check_same_thread": False},
|
|
34
|
+
)
|
|
35
|
+
self._session_factory = sessionmaker(self._engine, autoflush=False, expire_on_commit=False)
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def session(self) -> Generator[Session]:
|
|
39
|
+
with self._session_factory() as _session:
|
|
40
|
+
yield _session
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def engine(self) -> Engine:
|
|
44
|
+
return self._engine
|
|
45
|
+
|
|
46
|
+
__all__ = ["DcplDatabase"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ---------------------
|
|
7
|
+
# Third party libraries
|
|
8
|
+
# ---------------------
|
|
9
|
+
from sqlalchemy import MetaData
|
|
10
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
11
|
+
|
|
12
|
+
metadata = MetaData(
|
|
13
|
+
# For the different artifacts (indexes, constraints, etc.)
|
|
14
|
+
naming_convention={
|
|
15
|
+
"ix": "ix_%(column_0_label)s",
|
|
16
|
+
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
17
|
+
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
18
|
+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
19
|
+
"pk": "pk_%(table_name)s",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Model(DeclarativeBase):
|
|
25
|
+
"""The Base class all of our models must derive"""
|
|
26
|
+
|
|
27
|
+
metadata = metadata
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["metadata", "Model"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ---------------------
|
|
7
|
+
# Third party libraries
|
|
8
|
+
# ---------------------
|
|
9
|
+
from sqlalchemy import MetaData
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
11
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
12
|
+
|
|
13
|
+
metadata = MetaData(
|
|
14
|
+
# For the different artifacts (indexes, constraints, etc.)
|
|
15
|
+
naming_convention={
|
|
16
|
+
"ix": "ix_%(column_0_label)s",
|
|
17
|
+
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
18
|
+
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
19
|
+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
20
|
+
"pk": "pk_%(table_name)s",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncModel(AsyncAttrs, DeclarativeBase):
|
|
26
|
+
"""The Base class all of our async models must derive for lazy model loading"""
|
|
27
|
+
|
|
28
|
+
metadata = metadata
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ----------------
|
|
7
|
+
# standard library
|
|
8
|
+
# ----------------
|
|
9
|
+
from typing import ContextManager, Protocol
|
|
10
|
+
|
|
11
|
+
# ---------------------
|
|
12
|
+
# Third party libraries
|
|
13
|
+
# ---------------------
|
|
14
|
+
from sqlalchemy.orm import Session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Database(Protocol):
|
|
18
|
+
"""application software use this interface as the dependency"""
|
|
19
|
+
|
|
20
|
+
def session(self) -> ContextManager[Session]: ...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2026 Rafael Gonzalez.
|
|
3
|
+
#
|
|
4
|
+
# See the LICENSE file for details
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
# ----------------
|
|
7
|
+
# standard library
|
|
8
|
+
# ----------------
|
|
9
|
+
from typing import AsyncContextManager, Protocol
|
|
10
|
+
|
|
11
|
+
# ---------------------
|
|
12
|
+
# Third party libraries
|
|
13
|
+
# ---------------------
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncDatabase(Protocol):
|
|
18
|
+
"""application software use this interface as the dependency"""
|
|
19
|
+
|
|
20
|
+
def session(self) -> AsyncContextManager[AsyncSession]: ...
|
lica_sqa/view.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# --------------------------------------------------------------------
|
|
2
|
+
# View recipe from https://github.com/sqlalchemy/sqlalchemy/wiki/Views
|
|
3
|
+
# (updated for 2.0.38)
|
|
4
|
+
# --------------------------------------------------------------------
|
|
5
|
+
# ---------------------
|
|
6
|
+
# Third party libraries
|
|
7
|
+
# ---------------------
|
|
8
|
+
import sqlalchemy
|
|
9
|
+
from sqlalchemy.ext import compiler
|
|
10
|
+
from sqlalchemy.schema import DDLElement
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CreateView(DDLElement):
|
|
14
|
+
def __init__(self, name: str, selectable):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.selectable = selectable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DropView(DDLElement):
|
|
20
|
+
def __init__(self, name: str):
|
|
21
|
+
self.name = name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@compiler.compiles(CreateView)
|
|
25
|
+
def _create_view(element, compiler, **kw):
|
|
26
|
+
return "CREATE VIEW %s AS %s" % (
|
|
27
|
+
element.name,
|
|
28
|
+
compiler.sql_compiler.process(element.selectable, literal_binds=True),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@compiler.compiles(DropView)
|
|
33
|
+
def _drop_view(element, compiler, **kw):
|
|
34
|
+
return "DROP VIEW %s" % (element.name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def view_exists(ddl, target, connection, **kw):
|
|
38
|
+
return ddl.name in sqlalchemy.inspect(connection).get_view_names()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def view_doesnt_exist(ddl, target, connection, **kw):
|
|
42
|
+
return not view_exists(ddl, target, connection, **kw)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def view(name, metadata, selectable):
|
|
46
|
+
t = sqlalchemy.table(
|
|
47
|
+
name,
|
|
48
|
+
*(
|
|
49
|
+
sqlalchemy.Column(c.name, c.type, primary_key=c.primary_key)
|
|
50
|
+
for c in selectable.selected_columns
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
t.primary_key.update(c for c in t.c if c.primary_key)
|
|
54
|
+
|
|
55
|
+
sqlalchemy.event.listen(
|
|
56
|
+
metadata,
|
|
57
|
+
"after_create",
|
|
58
|
+
CreateView(name, selectable).execute_if(callable_=view_doesnt_exist),
|
|
59
|
+
)
|
|
60
|
+
sqlalchemy.event.listen(
|
|
61
|
+
metadata, "before_drop", DropView(name).execute_if(callable_=view_exists)
|
|
62
|
+
)
|
|
63
|
+
return t
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lica-sqa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LICA utilities for SQLAlchemy
|
|
5
|
+
Author: Rafael González
|
|
6
|
+
Author-email: Rafael González <rafael08@ucm.es>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
14
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
15
|
+
Requires-Dist: alembic>=1.20
|
|
16
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0 ; extra == 'asyncio'
|
|
17
|
+
Requires-Dist: python-decouple ; extra == 'decouple'
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Provides-Extra: asyncio
|
|
20
|
+
Provides-Extra: decouple
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# lica-sqa
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
`lica-sqa` is a lightweight SQLAlchemy utility layer for day-to-day LICA Python applications. It defines minimal synchronous and asynchronous database interfaces, provides shared declarative-model metadata, and includes adapters that create SQLAlchemy engines and session factories from a configuration value.
|
|
28
|
+
|
|
29
|
+
The package supports conventional SQLAlchemy ORM models, asynchronous models based on `AsyncAttrs`, consistent database-object naming conventions, and a small helper for creating and dropping SQL views. Application code depends only on the `Database` or `AsyncDatabase` protocol, while the concrete database behaviour is supplied by an adapter.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install `lica-sqa` directly from the `lica-ng` Git repository:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install lica-sqa
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or add it to your `pyproject.toml`:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[project]
|
|
43
|
+
dependencies = [
|
|
44
|
+
"lica-sqa",
|
|
45
|
+
]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The included adapters require SQLAlchemy and `python-decouple`:
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[project]
|
|
52
|
+
dependencies = [
|
|
53
|
+
"lica-sqa",
|
|
54
|
+
"sqlalchemy",
|
|
55
|
+
"python-decouple",
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For asynchronous applications using SQLite, add an async driver such as `aiosqlite`:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[project]
|
|
63
|
+
dependencies = [
|
|
64
|
+
"lica-sqa",
|
|
65
|
+
"sqlalchemy",
|
|
66
|
+
"python-decouple",
|
|
67
|
+
"aiosqlite",
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Interfaces
|
|
72
|
+
|
|
73
|
+
`lica-sqa` defines its database contracts using `typing.Protocol`. Adapters do not need to inherit from these protocols explicitly; they only need to provide the required methods.
|
|
74
|
+
|
|
75
|
+
### Synchronous database
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from contextlib import AbstractContextManager
|
|
79
|
+
from typing import Protocol
|
|
80
|
+
|
|
81
|
+
from sqlalchemy.orm import Session
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Database(Protocol):
|
|
85
|
+
"""Application software uses this interface as the dependency."""
|
|
86
|
+
|
|
87
|
+
def session(self) -> AbstractContextManager[Session]: ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Asynchronous database
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from contextlib import AbstractAsyncContextManager
|
|
94
|
+
from typing import Protocol
|
|
95
|
+
|
|
96
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AsyncDatabase(Protocol):
|
|
100
|
+
"""Application software uses this interface as the dependency."""
|
|
101
|
+
|
|
102
|
+
def session(self) -> AbstractAsyncContextManager[AsyncSession]: ...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Adapters
|
|
106
|
+
|
|
107
|
+
| Adapter | Module | Extra | Description |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `DcplDatabase` | `lica_sqa.adapters.decouple` | `sqlalchemy`, `python-decouple` | Creates a synchronous SQLAlchemy engine and session factory from a `python-decouple` configuration value. |
|
|
110
|
+
| `AsyncDcplDatabase` | `lica_sqa.adapters.asyncio.decouple` | `sqlalchemy`, `python-decouple` | Creates an asynchronous SQLAlchemy engine and session factory from a `python-decouple` configuration value. |
|
|
111
|
+
|
|
112
|
+
Both adapters accept the name of the environment variable or configuration key containing the database URL:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
DcplDatabase(env_var: str = "DATABASE_URL") -> None
|
|
116
|
+
AsyncDcplDatabase(env_var: str = "DATABASE_URL") -> None
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Call `init()` before using the adapter. It reads the database URL, creates the engine with `pool_pre_ping=True`, and creates a session factory with `autoflush=False` and `expire_on_commit=False`.
|
|
120
|
+
|
|
121
|
+
The expected configuration value is a standard SQLAlchemy database URL, for example:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
DATABASE_URL=sqlite:///app.db
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For an asynchronous SQLite database:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
DATABASE_URL=sqlite+aiosqlite:///app.db
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Session usage
|
|
134
|
+
|
|
135
|
+
The `session()` method returns a context manager that yields a SQLAlchemy session and closes it automatically on exit.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
with database.session() as session:
|
|
139
|
+
session.add(obj)
|
|
140
|
+
session.commit()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For asynchronous adapters:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
async with database.session() as session:
|
|
147
|
+
session.add(obj)
|
|
148
|
+
await session.commit()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Models and metadata
|
|
152
|
+
|
|
153
|
+
`lica-sqa` provides shared SQLAlchemy `MetaData` objects with a consistent naming convention for indexes, unique constraints, check constraints, foreign keys, and primary keys.
|
|
154
|
+
|
|
155
|
+
### Synchronous models
|
|
156
|
+
|
|
157
|
+
Derive application models from `Model`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
161
|
+
|
|
162
|
+
from lica_sqa import Model
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class User(Model):
|
|
166
|
+
__tablename__ = "user"
|
|
167
|
+
|
|
168
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
169
|
+
name: Mapped[str]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Asynchronous models
|
|
173
|
+
|
|
174
|
+
Derive asynchronous application models from `AsyncModel`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
178
|
+
|
|
179
|
+
from lica_sqa.metadata.asyncio import AsyncModel
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class User(AsyncModel):
|
|
183
|
+
__tablename__ = "user"
|
|
184
|
+
|
|
185
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
186
|
+
name: Mapped[str]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`AsyncModel` combines SQLAlchemy’s `AsyncAttrs` and `DeclarativeBase`, enabling lazy attribute loading in asynchronous applications.
|
|
190
|
+
|
|
191
|
+
## SQL views
|
|
192
|
+
|
|
193
|
+
The `view()` helper creates a SQLAlchemy table-like object associated with a SQL view. It registers `CREATE VIEW` and `DROP VIEW` events on the supplied `MetaData`, creating the view only if it does not already exist and dropping it only if it exists.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from sqlalchemy import select
|
|
197
|
+
|
|
198
|
+
from lica_sqa import metadata, view
|
|
199
|
+
from myapp.models import User
|
|
200
|
+
|
|
201
|
+
user_view = view(
|
|
202
|
+
"user_names",
|
|
203
|
+
metadata,
|
|
204
|
+
select(User.id, User.name),
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The helper is based on the SQLAlchemy views recipe and has been updated for SQLAlchemy 2.0.
|
|
209
|
+
|
|
210
|
+
## Logging
|
|
211
|
+
|
|
212
|
+
Use `sqa_logging()` to configure the verbosity of SQLAlchemy engine logging:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from lica_sqa import sqa_logging
|
|
216
|
+
|
|
217
|
+
sqa_logging(debug=True)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
When `debug=True`, the `sqlalchemy.engine` and `aiosqlite` loggers are set to `INFO`. Otherwise, `sqlalchemy.engine` is set to `WARNING`.
|
|
221
|
+
|
|
222
|
+
## Usage example, with lica-cli package
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import sys
|
|
226
|
+
from argparse import ArgumentParser, Namespace
|
|
227
|
+
|
|
228
|
+
from lica_cli import parse_args, run_cli
|
|
229
|
+
from lica_sqa import Database
|
|
230
|
+
from lica_sqa.adapters.decouple import DcplDatabase
|
|
231
|
+
|
|
232
|
+
from myapp.models import User
|
|
233
|
+
from myprog import __version__
|
|
234
|
+
|
|
235
|
+
DESCRIPTION = "My program short description"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def add_args(parser: ArgumentParser) -> None:
|
|
239
|
+
"""Add extra CLI arguments here"""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cli_main(args: Namespace, database: Database) -> None:
|
|
244
|
+
with database.session() as session:
|
|
245
|
+
session.add(User(name="Alice"))
|
|
246
|
+
session.commit()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main():
|
|
250
|
+
args = parse_args(add_args, __name__, __version__, DESCRIPTION)
|
|
251
|
+
|
|
252
|
+
database: Database = DcplDatabase()
|
|
253
|
+
database.init()
|
|
254
|
+
|
|
255
|
+
return_code = run_cli(cli_main, args, __name__, __version__, database=database)
|
|
256
|
+
sys.exit(return_code)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
main()
|
|
261
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
lica_sqa/__init__.py,sha256=-kPUbsM1qgauX8Txm-XhwdroBlWlaHkptjC_1Si9DMQ,947
|
|
2
|
+
lica_sqa/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
lica_sqa/adapters/asyncio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
lica_sqa/adapters/asyncio/decouple.py,sha256=Gx0CvBAQItMzomqZntX4T8ATtch3qS_Hm0QHKRTZ8Bk,1533
|
|
5
|
+
lica_sqa/adapters/decouple.py,sha256=4ooytjzcL41jPI_kexMfCgV74AWxb5J3lCyKSFzwH8M,1379
|
|
6
|
+
lica_sqa/metadata/__init__.py,sha256=041505Mtqw0MC5iZIOuzmTXQeWpbMJx_HU3s0uZgljI,890
|
|
7
|
+
lica_sqa/metadata/asyncio.py,sha256=qXEDlNGkmv8_nD4awp6bX1TMUX2GV_toMFNWF98k0tY,948
|
|
8
|
+
lica_sqa/ports/__init__.py,sha256=FAGUrwUnT4VeWsKwZLdF5zS30pmujeisdZ0u9rdZeCY,581
|
|
9
|
+
lica_sqa/ports/asyncio.py,sha256=GlpU6SDPCpJkh1kq0S5iRuB5D0ihBNhVR20mgo6gHww,614
|
|
10
|
+
lica_sqa/view.py,sha256=9OGV0LV2LhILJOY6szMBf5SKJieOzmRh5iJ166gH-Xg,1779
|
|
11
|
+
lica_sqa-0.1.0.dist-info/licenses/LICENSE,sha256=AAxFY4JFsUgATHGcfHy_aFXU7KbvycCxGfszmPiUCjA,1066
|
|
12
|
+
lica_sqa-0.1.0.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
|
|
13
|
+
lica_sqa-0.1.0.dist-info/METADATA,sha256=RPdNLUJn8DTtrNF7asMl6WEWESGVR86DJdhNZRyyLlY,7072
|
|
14
|
+
lica_sqa-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guaix UCM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|