lica-sqa 0.1.0__tar.gz

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-0.1.0/LICENSE ADDED
@@ -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.
@@ -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,239 @@
1
+ # lica-sqa
2
+
3
+ ## Introduction
4
+
5
+ `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.
6
+
7
+ 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.
8
+
9
+ ## Installation
10
+
11
+ Install `lica-sqa` directly from the `lica-ng` Git repository:
12
+
13
+ ```bash
14
+ pip install lica-sqa
15
+ ```
16
+
17
+ or add it to your `pyproject.toml`:
18
+
19
+ ```toml
20
+ [project]
21
+ dependencies = [
22
+ "lica-sqa",
23
+ ]
24
+ ```
25
+
26
+ The included adapters require SQLAlchemy and `python-decouple`:
27
+
28
+ ```toml
29
+ [project]
30
+ dependencies = [
31
+ "lica-sqa",
32
+ "sqlalchemy",
33
+ "python-decouple",
34
+ ]
35
+ ```
36
+
37
+ For asynchronous applications using SQLite, add an async driver such as `aiosqlite`:
38
+
39
+ ```toml
40
+ [project]
41
+ dependencies = [
42
+ "lica-sqa",
43
+ "sqlalchemy",
44
+ "python-decouple",
45
+ "aiosqlite",
46
+ ]
47
+ ```
48
+
49
+ ## Interfaces
50
+
51
+ `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.
52
+
53
+ ### Synchronous database
54
+
55
+ ```python
56
+ from contextlib import AbstractContextManager
57
+ from typing import Protocol
58
+
59
+ from sqlalchemy.orm import Session
60
+
61
+
62
+ class Database(Protocol):
63
+ """Application software uses this interface as the dependency."""
64
+
65
+ def session(self) -> AbstractContextManager[Session]: ...
66
+ ```
67
+
68
+ ### Asynchronous database
69
+
70
+ ```python
71
+ from contextlib import AbstractAsyncContextManager
72
+ from typing import Protocol
73
+
74
+ from sqlalchemy.ext.asyncio import AsyncSession
75
+
76
+
77
+ class AsyncDatabase(Protocol):
78
+ """Application software uses this interface as the dependency."""
79
+
80
+ def session(self) -> AbstractAsyncContextManager[AsyncSession]: ...
81
+ ```
82
+
83
+ ## Adapters
84
+
85
+ | Adapter | Module | Extra | Description |
86
+ |---|---|---|---|
87
+ | `DcplDatabase` | `lica_sqa.adapters.decouple` | `sqlalchemy`, `python-decouple` | Creates a synchronous SQLAlchemy engine and session factory from a `python-decouple` configuration value. |
88
+ | `AsyncDcplDatabase` | `lica_sqa.adapters.asyncio.decouple` | `sqlalchemy`, `python-decouple` | Creates an asynchronous SQLAlchemy engine and session factory from a `python-decouple` configuration value. |
89
+
90
+ Both adapters accept the name of the environment variable or configuration key containing the database URL:
91
+
92
+ ```python
93
+ DcplDatabase(env_var: str = "DATABASE_URL") -> None
94
+ AsyncDcplDatabase(env_var: str = "DATABASE_URL") -> None
95
+ ```
96
+
97
+ 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`.
98
+
99
+ The expected configuration value is a standard SQLAlchemy database URL, for example:
100
+
101
+ ```bash
102
+ DATABASE_URL=sqlite:///app.db
103
+ ```
104
+
105
+ For an asynchronous SQLite database:
106
+
107
+ ```bash
108
+ DATABASE_URL=sqlite+aiosqlite:///app.db
109
+ ```
110
+
111
+ ### Session usage
112
+
113
+ The `session()` method returns a context manager that yields a SQLAlchemy session and closes it automatically on exit.
114
+
115
+ ```python
116
+ with database.session() as session:
117
+ session.add(obj)
118
+ session.commit()
119
+ ```
120
+
121
+ For asynchronous adapters:
122
+
123
+ ```python
124
+ async with database.session() as session:
125
+ session.add(obj)
126
+ await session.commit()
127
+ ```
128
+
129
+ ## Models and metadata
130
+
131
+ `lica-sqa` provides shared SQLAlchemy `MetaData` objects with a consistent naming convention for indexes, unique constraints, check constraints, foreign keys, and primary keys.
132
+
133
+ ### Synchronous models
134
+
135
+ Derive application models from `Model`:
136
+
137
+ ```python
138
+ from sqlalchemy.orm import Mapped, mapped_column
139
+
140
+ from lica_sqa import Model
141
+
142
+
143
+ class User(Model):
144
+ __tablename__ = "user"
145
+
146
+ id: Mapped[int] = mapped_column(primary_key=True)
147
+ name: Mapped[str]
148
+ ```
149
+
150
+ ### Asynchronous models
151
+
152
+ Derive asynchronous application models from `AsyncModel`:
153
+
154
+ ```python
155
+ from sqlalchemy.orm import Mapped, mapped_column
156
+
157
+ from lica_sqa.metadata.asyncio import AsyncModel
158
+
159
+
160
+ class User(AsyncModel):
161
+ __tablename__ = "user"
162
+
163
+ id: Mapped[int] = mapped_column(primary_key=True)
164
+ name: Mapped[str]
165
+ ```
166
+
167
+ `AsyncModel` combines SQLAlchemy’s `AsyncAttrs` and `DeclarativeBase`, enabling lazy attribute loading in asynchronous applications.
168
+
169
+ ## SQL views
170
+
171
+ 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.
172
+
173
+ ```python
174
+ from sqlalchemy import select
175
+
176
+ from lica_sqa import metadata, view
177
+ from myapp.models import User
178
+
179
+ user_view = view(
180
+ "user_names",
181
+ metadata,
182
+ select(User.id, User.name),
183
+ )
184
+ ```
185
+
186
+ The helper is based on the SQLAlchemy views recipe and has been updated for SQLAlchemy 2.0.
187
+
188
+ ## Logging
189
+
190
+ Use `sqa_logging()` to configure the verbosity of SQLAlchemy engine logging:
191
+
192
+ ```python
193
+ from lica_sqa import sqa_logging
194
+
195
+ sqa_logging(debug=True)
196
+ ```
197
+
198
+ When `debug=True`, the `sqlalchemy.engine` and `aiosqlite` loggers are set to `INFO`. Otherwise, `sqlalchemy.engine` is set to `WARNING`.
199
+
200
+ ## Usage example, with lica-cli package
201
+
202
+ ```python
203
+ import sys
204
+ from argparse import ArgumentParser, Namespace
205
+
206
+ from lica_cli import parse_args, run_cli
207
+ from lica_sqa import Database
208
+ from lica_sqa.adapters.decouple import DcplDatabase
209
+
210
+ from myapp.models import User
211
+ from myprog import __version__
212
+
213
+ DESCRIPTION = "My program short description"
214
+
215
+
216
+ def add_args(parser: ArgumentParser) -> None:
217
+ """Add extra CLI arguments here"""
218
+ pass
219
+
220
+
221
+ def cli_main(args: Namespace, database: Database) -> None:
222
+ with database.session() as session:
223
+ session.add(User(name="Alice"))
224
+ session.commit()
225
+
226
+
227
+ def main():
228
+ args = parse_args(add_args, __name__, __version__, DESCRIPTION)
229
+
230
+ database: Database = DcplDatabase()
231
+ database.init()
232
+
233
+ return_code = run_cli(cli_main, args, __name__, __version__, database=database)
234
+ sys.exit(return_code)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
239
+ ```
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "lica-sqa"
3
+ version = "0.1.0"
4
+ description = "LICA utilities for SQLAlchemy"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license-files = ["LICENSE"]
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3.12",
10
+ "Development Status :: 3 - Alpha",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Science/Research",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Topic :: Scientific/Engineering :: Astronomy",
15
+ ]
16
+ dependencies = [
17
+ "SQLAlchemy>=2.0",
18
+ "alembic >= 1.20",
19
+ ]
20
+
21
+ [[project.authors]]
22
+ name = "Rafael González"
23
+ email = "rafael08@ucm.es"
24
+
25
+ [project.optional-dependencies]
26
+ asyncio = ["sqlalchemy[asyncio] >= 2.0"]
27
+ decouple = ["python-decouple"]
28
+
29
+ [[tool.uv.index]]
30
+ name = "testpypi"
31
+ url = "https://test.pypi.org/simple/"
32
+ publish-url = "https://test.pypi.org/legacy/"
33
+ explicit = true
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.13.1,<0.14"]
40
+ build-backend = "uv_build"
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "lica-sqa"
3
+ version = "0.1.0"
4
+ description = "LICA utilities for SQLAlchemy"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ {name = "Rafael González", email = "rafael08@ucm.es"},
10
+ ]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3.12",
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Science/Research",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Topic :: Scientific/Engineering :: Astronomy",
18
+ ]
19
+
20
+ dependencies = [
21
+ "SQLAlchemy>=2.0",
22
+ "alembic >= 1.20",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ # sqlalchemy.asyncio subpackage need this
27
+ asyncio = [
28
+ "sqlalchemy[asyncio] >= 2.0",
29
+ ]
30
+ decouple = [
31
+ "python-decouple",
32
+ ]
33
+
34
+ [[tool.uv.index]]
35
+ name = "testpypi"
36
+ url = "https://test.pypi.org/simple/"
37
+ publish-url = "https://test.pypi.org/legacy/"
38
+ explicit = true
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.13.1,<0.14"]
42
+ build-backend = "uv_build"
43
+
44
+ # -----------
45
+ # Other stuff
46
+ # -----------
47
+
48
+ [tool.ruff]
49
+ line-length = 100
@@ -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
@@ -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]: ...
@@ -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