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.
Files changed (55) hide show
  1. duo_orm-0.1.4/PKG-INFO +173 -0
  2. duo_orm-0.1.4/README.md +116 -0
  3. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/__init__.py +4 -0
  4. duo_orm-0.1.4/duo_orm/_payloads.py +46 -0
  5. duo_orm-0.1.4/duo_orm/basemodel.py +431 -0
  6. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/db.py +63 -32
  7. duo_orm-0.1.4/duo_orm/executor.py +545 -0
  8. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/cli.py +14 -7
  9. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/query.py +271 -5
  10. duo_orm-0.1.4/duo_orm.egg-info/PKG-INFO +173 -0
  11. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/SOURCES.txt +8 -0
  12. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/requires.txt +1 -0
  13. {duo_orm-0.1.2 → duo_orm-0.1.4}/pyproject.toml +2 -1
  14. duo_orm-0.1.4/tests/test_basemodel_core.py +117 -0
  15. duo_orm-0.1.4/tests/test_basemodel_extras.py +51 -0
  16. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_concurrency.py +4 -22
  17. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_config_errors.py +10 -20
  18. duo_orm-0.1.4/tests/test_crud_api.py +356 -0
  19. duo_orm-0.1.4/tests/test_datatypes.py +39 -0
  20. duo_orm-0.1.4/tests/test_db_helpers.py +82 -0
  21. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_db_isolation.py +0 -12
  22. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_db_urls.py +3 -3
  23. duo_orm-0.1.4/tests/test_enhancements.py +188 -0
  24. duo_orm-0.1.4/tests/test_executor_bulk_hooks.py +136 -0
  25. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_orm_core.py +181 -208
  26. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_patch.py +10 -17
  27. duo_orm-0.1.4/tests/test_payloads.py +28 -0
  28. duo_orm-0.1.4/tests/test_pydantic_integration.py +91 -0
  29. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_queries.py +8 -36
  30. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_query_deep.py +10 -18
  31. duo_orm-0.1.4/tests/test_query_helpers_extra.py +117 -0
  32. duo_orm-0.1.4/tests/test_related_paths.py +78 -0
  33. duo_orm-0.1.2/PKG-INFO +0 -135
  34. duo_orm-0.1.2/README.md +0 -79
  35. duo_orm-0.1.2/duo_orm/basemodel.py +0 -238
  36. duo_orm-0.1.2/duo_orm/executor.py +0 -285
  37. duo_orm-0.1.2/duo_orm.egg-info/PKG-INFO +0 -135
  38. duo_orm-0.1.2/tests/test_basemodel_extras.py +0 -46
  39. duo_orm-0.1.2/tests/test_datatypes.py +0 -74
  40. duo_orm-0.1.2/tests/test_query_helpers_extra.py +0 -142
  41. duo_orm-0.1.2/tests/test_related_paths.py +0 -117
  42. {duo_orm-0.1.2 → duo_orm-0.1.4}/LICENSE +0 -0
  43. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/exceptions.py +0 -0
  44. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/__init__.py +0 -0
  45. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/migrations/config.py +0 -0
  46. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/patch.py +0 -0
  47. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm/session.py +0 -0
  48. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/dependency_links.txt +0 -0
  49. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/entry_points.txt +0 -0
  50. {duo_orm-0.1.2 → duo_orm-0.1.4}/duo_orm.egg-info/top_level.txt +0 -0
  51. {duo_orm-0.1.2 → duo_orm-0.1.4}/setup.cfg +0 -0
  52. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_executor.py +0 -0
  53. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_exports.py +0 -0
  54. {duo_orm-0.1.2 → duo_orm-0.1.4}/tests/test_migrations.py +0 -0
  55. {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.
@@ -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}