duo-orm 0.1.2__tar.gz → 0.1.4__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.
- duo_orm-0.1.4/PKG-INFO +173 -0
- duo_orm-0.1.4/README.md +116 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/__init__.py +4 -0
- duo_orm-0.1.4/duo_orm/_payloads.py +46 -0
- duo_orm-0.1.4/duo_orm/basemodel.py +431 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/db.py +63 -32
- duo_orm-0.1.4/duo_orm/executor.py +545 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/cli.py +14 -7
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/query.py +271 -5
- duo_orm-0.1.4/duo_orm.egg-info/PKG-INFO +173 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/SOURCES.txt +8 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/requires.txt +1 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/pyproject.toml +2 -1
- duo_orm-0.1.4/tests/test_basemodel_core.py +117 -0
- duo_orm-0.1.4/tests/test_basemodel_extras.py +51 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_concurrency.py +4 -22
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_config_errors.py +10 -20
- duo_orm-0.1.4/tests/test_crud_api.py +356 -0
- duo_orm-0.1.4/tests/test_datatypes.py +39 -0
- duo_orm-0.1.4/tests/test_db_helpers.py +82 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_db_isolation.py +0 -12
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_db_urls.py +3 -3
- duo_orm-0.1.4/tests/test_enhancements.py +188 -0
- duo_orm-0.1.4/tests/test_executor_bulk_hooks.py +136 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_orm_core.py +181 -208
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_patch.py +10 -17
- duo_orm-0.1.4/tests/test_payloads.py +28 -0
- duo_orm-0.1.4/tests/test_pydantic_integration.py +91 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_queries.py +8 -36
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_query_deep.py +10 -18
- duo_orm-0.1.4/tests/test_query_helpers_extra.py +117 -0
- duo_orm-0.1.4/tests/test_related_paths.py +78 -0
- duo_orm-0.1.2/PKG-INFO +0 -135
- duo_orm-0.1.2/README.md +0 -79
- duo_orm-0.1.2/duo_orm/basemodel.py +0 -238
- duo_orm-0.1.2/duo_orm/executor.py +0 -285
- duo_orm-0.1.2/duo_orm.egg-info/PKG-INFO +0 -135
- duo_orm-0.1.2/tests/test_basemodel_extras.py +0 -46
- duo_orm-0.1.2/tests/test_datatypes.py +0 -74
- duo_orm-0.1.2/tests/test_query_helpers_extra.py +0 -142
- duo_orm-0.1.2/tests/test_related_paths.py +0 -117
- {duo_orm-0.1.2 → duo_orm-0.1.4}/LICENSE +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/exceptions.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/__init__.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/config.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/patch.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/session.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/dependency_links.txt +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/entry_points.txt +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/top_level.txt +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/setup.cfg +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_executor.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_exports.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_migrations.py +0 -0
- {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_session.py +0 -0
duo_orm-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: duo-orm
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: An opinionated, modern ORM for Python combining the power of SQLAlchemy 2.0 with a clean, symmetrical API for sync and async operations.
|
|
5
|
+
Author-email: Siddhanth Bhimakari <siddhanth.n.b@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/SiddhanthNB/duo-orm
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/SiddhanthNB/duo-orm/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
22
|
+
Requires-Dist: alembic>=1.8
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: toml>=0.10
|
|
25
|
+
Requires-Dist: aiosqlite>=0.17
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
|
|
29
|
+
Requires-Dist: oracledb>=2.0; extra == "all"
|
|
30
|
+
Requires-Dist: pymysql>=1.1; extra == "all"
|
|
31
|
+
Requires-Dist: asyncmy>=0.2; extra == "all"
|
|
32
|
+
Requires-Dist: pyodbc>=5.0; extra == "all"
|
|
33
|
+
Requires-Dist: aioodbc>=0.5; extra == "all"
|
|
34
|
+
Requires-Dist: aiosqlite>=0.20; extra == "all"
|
|
35
|
+
Provides-Extra: postgresql
|
|
36
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "postgresql"
|
|
37
|
+
Provides-Extra: oracle
|
|
38
|
+
Requires-Dist: oracledb>=2.0; extra == "oracle"
|
|
39
|
+
Provides-Extra: mysql
|
|
40
|
+
Requires-Dist: pymysql>=1.1; extra == "mysql"
|
|
41
|
+
Requires-Dist: asyncmy>=0.2; extra == "mysql"
|
|
42
|
+
Provides-Extra: mssql
|
|
43
|
+
Requires-Dist: pyodbc>=5.0; extra == "mssql"
|
|
44
|
+
Requires-Dist: aioodbc>=0.5; extra == "mssql"
|
|
45
|
+
Provides-Extra: sqlite
|
|
46
|
+
Requires-Dist: aiosqlite>=0.20; extra == "sqlite"
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.20; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
51
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
52
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
53
|
+
Requires-Dist: mkdocs>=1.5; extra == "dev"
|
|
54
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "dev"
|
|
55
|
+
Requires-Dist: mkdocstrings-python>=1.0; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
<p align="center">
|
|
59
|
+
<a href="https://duo-orm.readthedocs.io/">
|
|
60
|
+
<img src="https://raw.githubusercontent.com/SiddhanthNB/duo-orm/main/docs/assets/logo.svg" width="200" alt="DuoORM Logo">
|
|
61
|
+
</a>
|
|
62
|
+
</p>
|
|
63
|
+
<p align="center">
|
|
64
|
+
<a href="https://pypi.org/project/duo-orm/">
|
|
65
|
+
<img src="https://img.shields.io/pypi/v/duo-orm.svg?cacheSeconds=300" alt="PyPI version">
|
|
66
|
+
</a>
|
|
67
|
+
<a href="https://pypi.org/project/duo-orm/">
|
|
68
|
+
<img src="https://img.shields.io/pypi/pyversions/duo-orm.svg?cacheSeconds=300" alt="Python versions">
|
|
69
|
+
</a>
|
|
70
|
+
<a href="https://duo-orm.readthedocs.io/">
|
|
71
|
+
<img src="https://img.shields.io/badge/docs-readthedocs-blue.svg" alt="Docs">
|
|
72
|
+
</a>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
# DuoORM
|
|
76
|
+
|
|
77
|
+
DuoORM is a modern ORM built on SQLAlchemy 2.0 that empowers your data models, turning them into a powerful and expressive query interface. It's designed for developers who love clean, symmetrical sync/async APIs and explicit control over their unit of work, without sacrificing the power of the underlying SQLAlchemy Core.
|
|
78
|
+
|
|
79
|
+
## Key Features
|
|
80
|
+
|
|
81
|
+
- **Symmetrical Sync & Async API:** Write your queries once. Use `await` in an async context or call directly in a sync context. The API is identical.
|
|
82
|
+
- **Fluent, Model-Centric API:** Chainable methods like `.where()`, `.order_by()`, and `.limit()` flow directly from your models. CRUD is intuitive with methods like `Model.create()` and `instance.save()`.
|
|
83
|
+
- **Explicit Unit of Work:** By default, every operation is a single, isolated statement. For complex workflows, use the `db.transaction()` context manager to share a single session and guarantee atomicity.
|
|
84
|
+
- **Automated Driver Management:** Use clean, driverless database URLs (e.g., `postgresql://...`). DuoORM automatically injects the correct, high-performance sync and async drivers for you.
|
|
85
|
+
- **First-Class Pydantic Integration:** Use Pydantic models for validated data creation and updates right out of the box, or use plain dictionaries. The choice is yours.
|
|
86
|
+
- **Powerful Escape Hatch:** Never get blocked. Drop down to raw SQLAlchemy at any time by calling `.alchemize()` on any query to get the underlying `Select` object for advanced SQL needs.
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Core library with SQLite support
|
|
92
|
+
pip install duo-orm
|
|
93
|
+
|
|
94
|
+
# Or install with a specific database driver
|
|
95
|
+
pip install "duo-orm[postgresql]"
|
|
96
|
+
pip install "duo-orm[mysql]"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
Getting started with DuoORM is a simple four-step process.
|
|
102
|
+
|
|
103
|
+
### 0. Initialize your Project
|
|
104
|
+
|
|
105
|
+
DuoORM includes a handy CLI to set up a recommended project structure for your database code.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# This creates a `db/` directory with models, schemas, and migrations.
|
|
109
|
+
$ duo-orm init
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 1. Define your Models
|
|
113
|
+
|
|
114
|
+
First, configure your database and define your ORM models in a Python file (e.g., `db/models.py`).
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from duo_orm import Database, Mapped, mapped_column
|
|
118
|
+
|
|
119
|
+
# Configure the database with a clean, driverless URL
|
|
120
|
+
db = Database("sqlite:///app.db")
|
|
121
|
+
|
|
122
|
+
# Define an ORM model
|
|
123
|
+
class User(db.Model):
|
|
124
|
+
__tablename__ = "users"
|
|
125
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
126
|
+
name: Mapped[str]
|
|
127
|
+
age: Mapped[int]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2. Create Database Tables
|
|
131
|
+
|
|
132
|
+
Next, use the `duo-orm` CLI in your terminal to create the database tables from your models. This provides a safe, version-controlled schema for your application.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# First, generate a migration script from your models
|
|
136
|
+
$ duo-orm migration create "Initial user schema"
|
|
137
|
+
|
|
138
|
+
# Then, apply the migration to the database
|
|
139
|
+
$ duo-orm migration upgrade
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 3. Query your Data
|
|
143
|
+
|
|
144
|
+
With your tables created, you can now use DuoORM's fluent API to interact with your database in any Python script.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import asyncio
|
|
148
|
+
from db.models import User # Import your model from the file you created
|
|
149
|
+
|
|
150
|
+
async def query_users():
|
|
151
|
+
# Create a user (in an async context)
|
|
152
|
+
ada = await User.create({"name": "Ada Lovelace", "age": 35})
|
|
153
|
+
print(f"Created: {ada.name}")
|
|
154
|
+
|
|
155
|
+
# Find a user (the API is the same in sync contexts, just without `await`)
|
|
156
|
+
found_user = await User.where(User.name == "Ada Lovelace").first()
|
|
157
|
+
print(f"Found: {found_user.name}")
|
|
158
|
+
|
|
159
|
+
# Run the async function
|
|
160
|
+
asyncio.run(query_users())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Full Documentation
|
|
164
|
+
|
|
165
|
+
For detailed guides on all features, including transactions, Pydantic integration, database migrations, and advanced queries, please see the full documentation on **[ReadTheDocs](https://duo-orm.readthedocs.io/)**.
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome! Please feel free to open an issue or submit a pull request.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
This project is licensed under the MIT License.
|
duo_orm-0.1.4/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://duo-orm.readthedocs.io/">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/SiddhanthNB/duo-orm/main/docs/assets/logo.svg" width="200" alt="DuoORM Logo">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://pypi.org/project/duo-orm/">
|
|
8
|
+
<img src="https://img.shields.io/pypi/v/duo-orm.svg?cacheSeconds=300" alt="PyPI version">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://pypi.org/project/duo-orm/">
|
|
11
|
+
<img src="https://img.shields.io/pypi/pyversions/duo-orm.svg?cacheSeconds=300" alt="Python versions">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://duo-orm.readthedocs.io/">
|
|
14
|
+
<img src="https://img.shields.io/badge/docs-readthedocs-blue.svg" alt="Docs">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
# DuoORM
|
|
19
|
+
|
|
20
|
+
DuoORM is a modern ORM built on SQLAlchemy 2.0 that empowers your data models, turning them into a powerful and expressive query interface. It's designed for developers who love clean, symmetrical sync/async APIs and explicit control over their unit of work, without sacrificing the power of the underlying SQLAlchemy Core.
|
|
21
|
+
|
|
22
|
+
## Key Features
|
|
23
|
+
|
|
24
|
+
- **Symmetrical Sync & Async API:** Write your queries once. Use `await` in an async context or call directly in a sync context. The API is identical.
|
|
25
|
+
- **Fluent, Model-Centric API:** Chainable methods like `.where()`, `.order_by()`, and `.limit()` flow directly from your models. CRUD is intuitive with methods like `Model.create()` and `instance.save()`.
|
|
26
|
+
- **Explicit Unit of Work:** By default, every operation is a single, isolated statement. For complex workflows, use the `db.transaction()` context manager to share a single session and guarantee atomicity.
|
|
27
|
+
- **Automated Driver Management:** Use clean, driverless database URLs (e.g., `postgresql://...`). DuoORM automatically injects the correct, high-performance sync and async drivers for you.
|
|
28
|
+
- **First-Class Pydantic Integration:** Use Pydantic models for validated data creation and updates right out of the box, or use plain dictionaries. The choice is yours.
|
|
29
|
+
- **Powerful Escape Hatch:** Never get blocked. Drop down to raw SQLAlchemy at any time by calling `.alchemize()` on any query to get the underlying `Select` object for advanced SQL needs.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Core library with SQLite support
|
|
35
|
+
pip install duo-orm
|
|
36
|
+
|
|
37
|
+
# Or install with a specific database driver
|
|
38
|
+
pip install "duo-orm[postgresql]"
|
|
39
|
+
pip install "duo-orm[mysql]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
Getting started with DuoORM is a simple four-step process.
|
|
45
|
+
|
|
46
|
+
### 0. Initialize your Project
|
|
47
|
+
|
|
48
|
+
DuoORM includes a handy CLI to set up a recommended project structure for your database code.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# This creates a `db/` directory with models, schemas, and migrations.
|
|
52
|
+
$ duo-orm init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 1. Define your Models
|
|
56
|
+
|
|
57
|
+
First, configure your database and define your ORM models in a Python file (e.g., `db/models.py`).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from duo_orm import Database, Mapped, mapped_column
|
|
61
|
+
|
|
62
|
+
# Configure the database with a clean, driverless URL
|
|
63
|
+
db = Database("sqlite:///app.db")
|
|
64
|
+
|
|
65
|
+
# Define an ORM model
|
|
66
|
+
class User(db.Model):
|
|
67
|
+
__tablename__ = "users"
|
|
68
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
69
|
+
name: Mapped[str]
|
|
70
|
+
age: Mapped[int]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Create Database Tables
|
|
74
|
+
|
|
75
|
+
Next, use the `duo-orm` CLI in your terminal to create the database tables from your models. This provides a safe, version-controlled schema for your application.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# First, generate a migration script from your models
|
|
79
|
+
$ duo-orm migration create "Initial user schema"
|
|
80
|
+
|
|
81
|
+
# Then, apply the migration to the database
|
|
82
|
+
$ duo-orm migration upgrade
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Query your Data
|
|
86
|
+
|
|
87
|
+
With your tables created, you can now use DuoORM's fluent API to interact with your database in any Python script.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import asyncio
|
|
91
|
+
from db.models import User # Import your model from the file you created
|
|
92
|
+
|
|
93
|
+
async def query_users():
|
|
94
|
+
# Create a user (in an async context)
|
|
95
|
+
ada = await User.create({"name": "Ada Lovelace", "age": 35})
|
|
96
|
+
print(f"Created: {ada.name}")
|
|
97
|
+
|
|
98
|
+
# Find a user (the API is the same in sync contexts, just without `await`)
|
|
99
|
+
found_user = await User.where(User.name == "Ada Lovelace").first()
|
|
100
|
+
print(f"Found: {found_user.name}")
|
|
101
|
+
|
|
102
|
+
# Run the async function
|
|
103
|
+
asyncio.run(query_users())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Full Documentation
|
|
107
|
+
|
|
108
|
+
For detailed guides on all features, including transactions, Pydantic integration, database migrations, and advanced queries, please see the full documentation on **[ReadTheDocs](https://duo-orm.readthedocs.io/)**.
|
|
109
|
+
|
|
110
|
+
## Contributing
|
|
111
|
+
|
|
112
|
+
Contributions are welcome! Please feel free to open an issue or submit a pull request.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
This project is licensed under the MIT License.
|
|
@@ -26,6 +26,7 @@ from sqlalchemy import Table, ForeignKey, UniqueConstraint, CheckConstraint, Ind
|
|
|
26
26
|
# 4. The Types Kit
|
|
27
27
|
from sqlalchemy import (
|
|
28
28
|
types,
|
|
29
|
+
ARRAY,
|
|
29
30
|
Integer,
|
|
30
31
|
BigInteger,
|
|
31
32
|
SmallInteger,
|
|
@@ -40,6 +41,7 @@ from sqlalchemy import (
|
|
|
40
41
|
UUID,
|
|
41
42
|
JSON,
|
|
42
43
|
LargeBinary,
|
|
44
|
+
Identity,
|
|
43
45
|
)
|
|
44
46
|
from sqlalchemy.dialects.postgresql import ARRAY as PG_ARRAY
|
|
45
47
|
|
|
@@ -79,6 +81,7 @@ __all__ = [
|
|
|
79
81
|
"not_",
|
|
80
82
|
# Types Kit
|
|
81
83
|
"types",
|
|
84
|
+
"ARRAY",
|
|
82
85
|
"Integer",
|
|
83
86
|
"BigInteger",
|
|
84
87
|
"SmallInteger",
|
|
@@ -93,6 +96,7 @@ __all__ = [
|
|
|
93
96
|
"UUID",
|
|
94
97
|
"JSON",
|
|
95
98
|
"LargeBinary",
|
|
99
|
+
"Identity",
|
|
96
100
|
"PG_ARRAY",
|
|
97
101
|
# SQL Functions & Helpers Kit
|
|
98
102
|
"func",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Type
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import inspect as sa_inspect
|
|
6
|
+
|
|
7
|
+
from .exceptions import ValidationError
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ValidationError as PydanticValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def coerce_payload(obj: Any, *, partial: bool, model_cls: Type) -> Dict[str, Any]:
|
|
13
|
+
"""Normalize incoming data (dict or Pydantic model) to a plain dict of column keys.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
obj: dict or pydantic BaseModel (or None treated as empty dict).
|
|
17
|
+
partial: if True, drop unset/None fields (for updates); if False, include all.
|
|
18
|
+
model_cls: model class used to filter to mapped columns.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict of column-key -> value suitable for constructing or updating the model.
|
|
22
|
+
"""
|
|
23
|
+
if obj is None:
|
|
24
|
+
data: Dict[str, Any] = {}
|
|
25
|
+
elif isinstance(obj, BaseModel):
|
|
26
|
+
try:
|
|
27
|
+
validated = type(obj).model_validate(obj)
|
|
28
|
+
data = validated.model_dump(exclude_none=partial, exclude_unset=partial)
|
|
29
|
+
except PydanticValidationError as exc:
|
|
30
|
+
raise ValidationError(str(exc)) from exc
|
|
31
|
+
elif isinstance(obj, dict):
|
|
32
|
+
data = dict(obj)
|
|
33
|
+
else:
|
|
34
|
+
raise ValidationError("Expected a dict or Pydantic BaseModel as payload.")
|
|
35
|
+
|
|
36
|
+
mapper = sa_inspect(model_cls)
|
|
37
|
+
column_keys = {col.key for col in mapper.columns}
|
|
38
|
+
guarded_fields = set(getattr(model_cls, "__guarded__", ()))
|
|
39
|
+
pk_keys = {col.key for col in mapper.primary_key}
|
|
40
|
+
guarded_fields.update(pk_keys)
|
|
41
|
+
|
|
42
|
+
# Allow PKs on create (partial=False) so required keys can be provided,
|
|
43
|
+
# but still block __guarded__ fields always.
|
|
44
|
+
effective_guarded = guarded_fields if partial else (guarded_fields - pk_keys)
|
|
45
|
+
|
|
46
|
+
return {k: v for k, v in data.items() if k in column_keys and k not in effective_guarded}
|