decorates 3.1.1__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.
- decorates-3.1.1/LICENSE +6 -0
- decorates-3.1.1/PKG-INFO +279 -0
- decorates-3.1.1/README.md +252 -0
- decorates-3.1.1/pyproject.toml +58 -0
- decorates-3.1.1/setup.cfg +4 -0
- decorates-3.1.1/src/decorates/cli/__init__.py +56 -0
- decorates-3.1.1/src/decorates/cli/container.py +71 -0
- decorates-3.1.1/src/decorates/cli/dispatcher.py +119 -0
- decorates-3.1.1/src/decorates/cli/exceptions.py +58 -0
- decorates-3.1.1/src/decorates/cli/middleware.py +84 -0
- decorates-3.1.1/src/decorates/cli/parser.py +186 -0
- decorates-3.1.1/src/decorates/cli/plugins.py +76 -0
- decorates-3.1.1/src/decorates/cli/registry.py +271 -0
- decorates-3.1.1/src/decorates/cli/utils/reflection.py +65 -0
- decorates-3.1.1/src/decorates/cli/utils/typing.py +62 -0
- decorates-3.1.1/src/decorates/db/__init__.py +91 -0
- decorates-3.1.1/src/decorates/db/decorators.py +314 -0
- decorates-3.1.1/src/decorates/db/engine.py +125 -0
- decorates-3.1.1/src/decorates/db/exceptions.py +113 -0
- decorates-3.1.1/src/decorates/db/fields.py +21 -0
- decorates-3.1.1/src/decorates/db/metadata.py +97 -0
- decorates-3.1.1/src/decorates/db/operators.py +80 -0
- decorates-3.1.1/src/decorates/db/registry.py +1081 -0
- decorates-3.1.1/src/decorates/db/relations.py +297 -0
- decorates-3.1.1/src/decorates/db/schema.py +263 -0
- decorates-3.1.1/src/decorates/db/security.py +61 -0
- decorates-3.1.1/src/decorates/db/typing_utils.py +138 -0
- decorates-3.1.1/src/decorates/py.typed +1 -0
- decorates-3.1.1/src/decorates.egg-info/PKG-INFO +279 -0
- decorates-3.1.1/src/decorates.egg-info/SOURCES.txt +38 -0
- decorates-3.1.1/src/decorates.egg-info/dependency_links.txt +1 -0
- decorates-3.1.1/src/decorates.egg-info/requires.txt +9 -0
- decorates-3.1.1/src/decorates.egg-info/top_level.txt +1 -0
- decorates-3.1.1/tests/test_cli_registry.py +321 -0
- decorates-3.1.1/tests/test_cli_registry_edge_cases.py +396 -0
- decorates-3.1.1/tests/test_db_registry.py +947 -0
- decorates-3.1.1/tests/test_db_registry_edge_cases.py +268 -0
- decorates-3.1.1/tests/test_db_registry_fastapi_integration.py +214 -0
- decorates-3.1.1/tests/test_db_registry_migration.py +75 -0
- decorates-3.1.1/tests/test_db_registry_spec_features.py +177 -0
decorates-3.1.1/LICENSE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## LICENSE
|
|
2
|
+
Copyright (c) [2026] [Charles DeFreese]
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
6
|
+
|
decorates-3.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: decorates
|
|
3
|
+
Version: 3.1.1
|
|
4
|
+
Summary: Decorator-driven persistence registry for Pydantic models and CLI tooling
|
|
5
|
+
Author: Charles DeFreese
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nexustech101/decorates
|
|
8
|
+
Project-URL: Repository, https://github.com/nexustech101/decorates
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
24
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == "dev"
|
|
25
|
+
Requires-Dist: pymysql>=1.1; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Framework
|
|
29
|
+
|
|
30
|
+
Decorator-driven tooling for Python:
|
|
31
|
+
|
|
32
|
+
- `decorates.cli` for ergonomic command-line apps
|
|
33
|
+
- `decorates.db` for Pydantic + SQLAlchemy persistence
|
|
34
|
+
|
|
35
|
+
The philosophy is simple: minimal setup, predictable behavior, and a fast path to shipping.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install decorates
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start Guide
|
|
44
|
+
|
|
45
|
+
1. Build one CLI command with a decorator.
|
|
46
|
+
2. Build one DB model with a decorator.
|
|
47
|
+
3. Use `Model.objects` for CRUD.
|
|
48
|
+
|
|
49
|
+
### CLI in 60 seconds
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from decorates.cli import CommandRegistry
|
|
53
|
+
|
|
54
|
+
cli = CommandRegistry()
|
|
55
|
+
|
|
56
|
+
# ── built-in help alias ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
@cli.register(
|
|
59
|
+
options=["-g", "--greet"],
|
|
60
|
+
name="greet",
|
|
61
|
+
description="Greet someone",
|
|
62
|
+
)
|
|
63
|
+
def greet(name: str) -> str:
|
|
64
|
+
return f"Hello, {name}!"
|
|
65
|
+
|
|
66
|
+
@cli.register(
|
|
67
|
+
options=["-h", "--help"],
|
|
68
|
+
name="help",
|
|
69
|
+
description="List all registered commands",
|
|
70
|
+
)
|
|
71
|
+
def list_clis() -> None:
|
|
72
|
+
cli.list_clis()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
cli.run()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python app.py greet Alice
|
|
81
|
+
python app.py --greet Alice
|
|
82
|
+
python app.py g Alice
|
|
83
|
+
|
|
84
|
+
python app.py help
|
|
85
|
+
python app.py --help
|
|
86
|
+
python app.py h
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
Hello, Alice!
|
|
91
|
+
|
|
92
|
+
Available commands:
|
|
93
|
+
greet [-g, --greet]: Greet someone
|
|
94
|
+
help [-h, --help]: List all registered commands
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Database + FastAPI in 5 minutes
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from contextlib import asynccontextmanager
|
|
101
|
+
from fastapi import FastAPI, HTTPException
|
|
102
|
+
from pydantic import BaseModel
|
|
103
|
+
from decorates.db import (
|
|
104
|
+
RecordNotFoundError,
|
|
105
|
+
UniqueConstraintError,
|
|
106
|
+
database_registry,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
DB_URL = "sqlite:///shop.db"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@database_registry(DB_URL, table_name="customers", unique_fields=["email"])
|
|
113
|
+
class Customer(BaseModel):
|
|
114
|
+
id: int | None = None
|
|
115
|
+
name: str
|
|
116
|
+
email: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@database_registry(DB_URL, table_name="products")
|
|
120
|
+
class Product(BaseModel):
|
|
121
|
+
id: int | None = None
|
|
122
|
+
name: str
|
|
123
|
+
price: float
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@database_registry(DB_URL, table_name="orders")
|
|
127
|
+
class Order(BaseModel):
|
|
128
|
+
id: int | None = None
|
|
129
|
+
customer_id: int
|
|
130
|
+
product_id: int
|
|
131
|
+
quantity: int
|
|
132
|
+
total: float
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CreateCustomer(BaseModel):
|
|
136
|
+
name: str
|
|
137
|
+
email: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CreateProduct(BaseModel):
|
|
141
|
+
name: str
|
|
142
|
+
price: float
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class CreateOrder(BaseModel):
|
|
146
|
+
customer_id: int
|
|
147
|
+
product_id: int
|
|
148
|
+
quantity: int
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@asynccontextmanager
|
|
152
|
+
async def lifespan(app: FastAPI):
|
|
153
|
+
for model in (Customer, Product, Order):
|
|
154
|
+
model.create_schema()
|
|
155
|
+
yield
|
|
156
|
+
for model in (Customer, Product, Order):
|
|
157
|
+
model.objects.dispose()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
app = FastAPI(lifespan=lifespan)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.post("/customers", response_model=Customer, status_code=201)
|
|
164
|
+
def create_customer(payload: CreateCustomer):
|
|
165
|
+
try:
|
|
166
|
+
return Customer.objects.create(**payload.model_dump())
|
|
167
|
+
except UniqueConstraintError:
|
|
168
|
+
raise HTTPException(status_code=409, detail="Email already exists")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.get("/customers/{customer_id}", response_model=Customer)
|
|
172
|
+
def get_customer(customer_id: int):
|
|
173
|
+
try:
|
|
174
|
+
return Customer.objects.require(customer_id)
|
|
175
|
+
except RecordNotFoundError:
|
|
176
|
+
raise HTTPException(status_code=404, detail="Customer not found")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.post("/products", response_model=Product, status_code=201)
|
|
180
|
+
def create_product(payload: CreateProduct):
|
|
181
|
+
return Product.objects.create(**payload.model_dump())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.post("/orders", response_model=Order, status_code=201)
|
|
185
|
+
def create_order(payload: CreateOrder):
|
|
186
|
+
customer = Customer.objects.get(payload.customer_id)
|
|
187
|
+
if customer is None:
|
|
188
|
+
raise HTTPException(status_code=404, detail="Customer not found")
|
|
189
|
+
|
|
190
|
+
product = Product.objects.get(payload.product_id)
|
|
191
|
+
if product is None:
|
|
192
|
+
raise HTTPException(status_code=404, detail="Product not found")
|
|
193
|
+
|
|
194
|
+
return Order.objects.create(
|
|
195
|
+
customer_id=customer.id,
|
|
196
|
+
product_id=product.id,
|
|
197
|
+
quantity=payload.quantity,
|
|
198
|
+
total=product.price * payload.quantity,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.get("/orders/desc", response_model=list[Order])
|
|
203
|
+
def list_orders_desc(limit: int = 20, offset: int = 0): # Filter by oldest (1, 2, 3...n)
|
|
204
|
+
return Order.objects.filter(order_by="id", limit=limit, offset=offset)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.get("/orders/asc", response_model=list[Order])
|
|
208
|
+
def list_orders_asc(limit: int = 20, offset: int = 0): # Filter by newest (n...3, 2, 1)
|
|
209
|
+
return Order.objects.filter(order_by="-id", limit=limit, offset=offset)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Core Concepts
|
|
213
|
+
|
|
214
|
+
### `decorates.cli`
|
|
215
|
+
|
|
216
|
+
- Register functions as commands with `@cli.register(...)`.
|
|
217
|
+
- Type annotations drive argument parsing.
|
|
218
|
+
- Optional command aliases with `options=["-x", "--long"]`.
|
|
219
|
+
- Optional DI (`DIContainer`) and middleware (`MiddlewareChain`).
|
|
220
|
+
- `CommandRegistry.run()` preserves decorates exceptions and wraps unexpected handler crashes as `CommandExecutionError` (with original exception chaining).
|
|
221
|
+
- Operational logs use standard Python logging namespaces under `decorates.cli.*`.
|
|
222
|
+
|
|
223
|
+
### `decorates.db`
|
|
224
|
+
|
|
225
|
+
- Register `BaseModel` classes with `@database_registry(...)`.
|
|
226
|
+
- Access all persistence through `Model.objects`.
|
|
227
|
+
- `id: int | None = None` gives database-managed autoincrement IDs.
|
|
228
|
+
- Schema helpers are available as class methods: `create_schema`, `drop_schema`, `schema_exists`, `truncate`.
|
|
229
|
+
- Unexpected SQLAlchemy runtime failures are normalized into `SchemaError` for cleaner, predictable error handling.
|
|
230
|
+
- Operational logs use standard Python logging namespaces under `decorates.db.*`.
|
|
231
|
+
- DB exceptions provide structured metadata (`exc.context`, `exc.to_dict()`) for production diagnostics.
|
|
232
|
+
|
|
233
|
+
## `decorates.db` Usage Snapshot
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
# Filtering operators
|
|
237
|
+
Order.objects.filter(total__gte=100)
|
|
238
|
+
Customer.objects.filter(email__ilike="%@example.com")
|
|
239
|
+
Order.objects.filter(quantity__in=[1, 2, 3])
|
|
240
|
+
|
|
241
|
+
# Sorting and pagination
|
|
242
|
+
Order.objects.filter(order_by="-id", limit=20, offset=0)
|
|
243
|
+
|
|
244
|
+
# Bulk writes
|
|
245
|
+
Product.objects.bulk_create([...])
|
|
246
|
+
Product.objects.bulk_upsert([...])
|
|
247
|
+
|
|
248
|
+
# Additive migration helpers
|
|
249
|
+
Customer.objects.ensure_column("phone", str | None, nullable=True)
|
|
250
|
+
Customer.objects.rename_table("customers_archive")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
After `rename_table(...)` succeeds, the same `Model.objects` manager and
|
|
254
|
+
schema helpers are immediately bound to the new table name.
|
|
255
|
+
|
|
256
|
+
If your model contains a field named `password`, password values are automatically hashed on write, and instances receive `verify_password(...)`.
|
|
257
|
+
|
|
258
|
+
## Documentation
|
|
259
|
+
|
|
260
|
+
- DB guide: `src/decorates/db/USAGE.md`
|
|
261
|
+
- CLI source API: `src/decorates/cli`
|
|
262
|
+
- DB source API: `src/decorates/db`
|
|
263
|
+
|
|
264
|
+
## Requirements
|
|
265
|
+
|
|
266
|
+
- Python 3.10+
|
|
267
|
+
- `pydantic>=2.0`
|
|
268
|
+
- `sqlalchemy>=2.0`
|
|
269
|
+
|
|
270
|
+
## Testing
|
|
271
|
+
|
|
272
|
+
- Default `pytest` includes SQLite plus PostgreSQL/MySQL rename-state integration tests.
|
|
273
|
+
- Start Docker Desktop (or another Docker engine) before running tests so
|
|
274
|
+
`docker-compose.test-db.yml` services can boot.
|
|
275
|
+
- The decorates is backed by a rigorous, production-focused test suite (170+ tests) that covers unit, edge-case, and multi-dialect integration behavior.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Framework
|
|
2
|
+
|
|
3
|
+
Decorator-driven tooling for Python:
|
|
4
|
+
|
|
5
|
+
- `decorates.cli` for ergonomic command-line apps
|
|
6
|
+
- `decorates.db` for Pydantic + SQLAlchemy persistence
|
|
7
|
+
|
|
8
|
+
The philosophy is simple: minimal setup, predictable behavior, and a fast path to shipping.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install decorates
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start Guide
|
|
17
|
+
|
|
18
|
+
1. Build one CLI command with a decorator.
|
|
19
|
+
2. Build one DB model with a decorator.
|
|
20
|
+
3. Use `Model.objects` for CRUD.
|
|
21
|
+
|
|
22
|
+
### CLI in 60 seconds
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from decorates.cli import CommandRegistry
|
|
26
|
+
|
|
27
|
+
cli = CommandRegistry()
|
|
28
|
+
|
|
29
|
+
# ── built-in help alias ────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
@cli.register(
|
|
32
|
+
options=["-g", "--greet"],
|
|
33
|
+
name="greet",
|
|
34
|
+
description="Greet someone",
|
|
35
|
+
)
|
|
36
|
+
def greet(name: str) -> str:
|
|
37
|
+
return f"Hello, {name}!"
|
|
38
|
+
|
|
39
|
+
@cli.register(
|
|
40
|
+
options=["-h", "--help"],
|
|
41
|
+
name="help",
|
|
42
|
+
description="List all registered commands",
|
|
43
|
+
)
|
|
44
|
+
def list_clis() -> None:
|
|
45
|
+
cli.list_clis()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
cli.run()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python app.py greet Alice
|
|
54
|
+
python app.py --greet Alice
|
|
55
|
+
python app.py g Alice
|
|
56
|
+
|
|
57
|
+
python app.py help
|
|
58
|
+
python app.py --help
|
|
59
|
+
python app.py h
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
Hello, Alice!
|
|
64
|
+
|
|
65
|
+
Available commands:
|
|
66
|
+
greet [-g, --greet]: Greet someone
|
|
67
|
+
help [-h, --help]: List all registered commands
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Database + FastAPI in 5 minutes
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from contextlib import asynccontextmanager
|
|
74
|
+
from fastapi import FastAPI, HTTPException
|
|
75
|
+
from pydantic import BaseModel
|
|
76
|
+
from decorates.db import (
|
|
77
|
+
RecordNotFoundError,
|
|
78
|
+
UniqueConstraintError,
|
|
79
|
+
database_registry,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
DB_URL = "sqlite:///shop.db"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@database_registry(DB_URL, table_name="customers", unique_fields=["email"])
|
|
86
|
+
class Customer(BaseModel):
|
|
87
|
+
id: int | None = None
|
|
88
|
+
name: str
|
|
89
|
+
email: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@database_registry(DB_URL, table_name="products")
|
|
93
|
+
class Product(BaseModel):
|
|
94
|
+
id: int | None = None
|
|
95
|
+
name: str
|
|
96
|
+
price: float
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@database_registry(DB_URL, table_name="orders")
|
|
100
|
+
class Order(BaseModel):
|
|
101
|
+
id: int | None = None
|
|
102
|
+
customer_id: int
|
|
103
|
+
product_id: int
|
|
104
|
+
quantity: int
|
|
105
|
+
total: float
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CreateCustomer(BaseModel):
|
|
109
|
+
name: str
|
|
110
|
+
email: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CreateProduct(BaseModel):
|
|
114
|
+
name: str
|
|
115
|
+
price: float
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CreateOrder(BaseModel):
|
|
119
|
+
customer_id: int
|
|
120
|
+
product_id: int
|
|
121
|
+
quantity: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@asynccontextmanager
|
|
125
|
+
async def lifespan(app: FastAPI):
|
|
126
|
+
for model in (Customer, Product, Order):
|
|
127
|
+
model.create_schema()
|
|
128
|
+
yield
|
|
129
|
+
for model in (Customer, Product, Order):
|
|
130
|
+
model.objects.dispose()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
app = FastAPI(lifespan=lifespan)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.post("/customers", response_model=Customer, status_code=201)
|
|
137
|
+
def create_customer(payload: CreateCustomer):
|
|
138
|
+
try:
|
|
139
|
+
return Customer.objects.create(**payload.model_dump())
|
|
140
|
+
except UniqueConstraintError:
|
|
141
|
+
raise HTTPException(status_code=409, detail="Email already exists")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.get("/customers/{customer_id}", response_model=Customer)
|
|
145
|
+
def get_customer(customer_id: int):
|
|
146
|
+
try:
|
|
147
|
+
return Customer.objects.require(customer_id)
|
|
148
|
+
except RecordNotFoundError:
|
|
149
|
+
raise HTTPException(status_code=404, detail="Customer not found")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.post("/products", response_model=Product, status_code=201)
|
|
153
|
+
def create_product(payload: CreateProduct):
|
|
154
|
+
return Product.objects.create(**payload.model_dump())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.post("/orders", response_model=Order, status_code=201)
|
|
158
|
+
def create_order(payload: CreateOrder):
|
|
159
|
+
customer = Customer.objects.get(payload.customer_id)
|
|
160
|
+
if customer is None:
|
|
161
|
+
raise HTTPException(status_code=404, detail="Customer not found")
|
|
162
|
+
|
|
163
|
+
product = Product.objects.get(payload.product_id)
|
|
164
|
+
if product is None:
|
|
165
|
+
raise HTTPException(status_code=404, detail="Product not found")
|
|
166
|
+
|
|
167
|
+
return Order.objects.create(
|
|
168
|
+
customer_id=customer.id,
|
|
169
|
+
product_id=product.id,
|
|
170
|
+
quantity=payload.quantity,
|
|
171
|
+
total=product.price * payload.quantity,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.get("/orders/desc", response_model=list[Order])
|
|
176
|
+
def list_orders_desc(limit: int = 20, offset: int = 0): # Filter by oldest (1, 2, 3...n)
|
|
177
|
+
return Order.objects.filter(order_by="id", limit=limit, offset=offset)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.get("/orders/asc", response_model=list[Order])
|
|
181
|
+
def list_orders_asc(limit: int = 20, offset: int = 0): # Filter by newest (n...3, 2, 1)
|
|
182
|
+
return Order.objects.filter(order_by="-id", limit=limit, offset=offset)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Core Concepts
|
|
186
|
+
|
|
187
|
+
### `decorates.cli`
|
|
188
|
+
|
|
189
|
+
- Register functions as commands with `@cli.register(...)`.
|
|
190
|
+
- Type annotations drive argument parsing.
|
|
191
|
+
- Optional command aliases with `options=["-x", "--long"]`.
|
|
192
|
+
- Optional DI (`DIContainer`) and middleware (`MiddlewareChain`).
|
|
193
|
+
- `CommandRegistry.run()` preserves decorates exceptions and wraps unexpected handler crashes as `CommandExecutionError` (with original exception chaining).
|
|
194
|
+
- Operational logs use standard Python logging namespaces under `decorates.cli.*`.
|
|
195
|
+
|
|
196
|
+
### `decorates.db`
|
|
197
|
+
|
|
198
|
+
- Register `BaseModel` classes with `@database_registry(...)`.
|
|
199
|
+
- Access all persistence through `Model.objects`.
|
|
200
|
+
- `id: int | None = None` gives database-managed autoincrement IDs.
|
|
201
|
+
- Schema helpers are available as class methods: `create_schema`, `drop_schema`, `schema_exists`, `truncate`.
|
|
202
|
+
- Unexpected SQLAlchemy runtime failures are normalized into `SchemaError` for cleaner, predictable error handling.
|
|
203
|
+
- Operational logs use standard Python logging namespaces under `decorates.db.*`.
|
|
204
|
+
- DB exceptions provide structured metadata (`exc.context`, `exc.to_dict()`) for production diagnostics.
|
|
205
|
+
|
|
206
|
+
## `decorates.db` Usage Snapshot
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Filtering operators
|
|
210
|
+
Order.objects.filter(total__gte=100)
|
|
211
|
+
Customer.objects.filter(email__ilike="%@example.com")
|
|
212
|
+
Order.objects.filter(quantity__in=[1, 2, 3])
|
|
213
|
+
|
|
214
|
+
# Sorting and pagination
|
|
215
|
+
Order.objects.filter(order_by="-id", limit=20, offset=0)
|
|
216
|
+
|
|
217
|
+
# Bulk writes
|
|
218
|
+
Product.objects.bulk_create([...])
|
|
219
|
+
Product.objects.bulk_upsert([...])
|
|
220
|
+
|
|
221
|
+
# Additive migration helpers
|
|
222
|
+
Customer.objects.ensure_column("phone", str | None, nullable=True)
|
|
223
|
+
Customer.objects.rename_table("customers_archive")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
After `rename_table(...)` succeeds, the same `Model.objects` manager and
|
|
227
|
+
schema helpers are immediately bound to the new table name.
|
|
228
|
+
|
|
229
|
+
If your model contains a field named `password`, password values are automatically hashed on write, and instances receive `verify_password(...)`.
|
|
230
|
+
|
|
231
|
+
## Documentation
|
|
232
|
+
|
|
233
|
+
- DB guide: `src/decorates/db/USAGE.md`
|
|
234
|
+
- CLI source API: `src/decorates/cli`
|
|
235
|
+
- DB source API: `src/decorates/db`
|
|
236
|
+
|
|
237
|
+
## Requirements
|
|
238
|
+
|
|
239
|
+
- Python 3.10+
|
|
240
|
+
- `pydantic>=2.0`
|
|
241
|
+
- `sqlalchemy>=2.0`
|
|
242
|
+
|
|
243
|
+
## Testing
|
|
244
|
+
|
|
245
|
+
- Default `pytest` includes SQLite plus PostgreSQL/MySQL rename-state integration tests.
|
|
246
|
+
- Start Docker Desktop (or another Docker engine) before running tests so
|
|
247
|
+
`docker-compose.test-db.yml` services can boot.
|
|
248
|
+
- The decorates is backed by a rigorous, production-focused test suite (170+ tests) that covers unit, edge-case, and multi-dialect integration behavior.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "decorates"
|
|
7
|
+
version = "3.1.1"
|
|
8
|
+
description = "Decorator-driven persistence registry for Pydantic models and CLI tooling"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Charles DeFreese" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic>=2.0",
|
|
28
|
+
"sqlalchemy>=2.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7.4",
|
|
34
|
+
"pytest-cov>=4.1",
|
|
35
|
+
"pytest-asyncio>=0.23",
|
|
36
|
+
"psycopg[binary]>=3.2",
|
|
37
|
+
"pymysql>=1.1",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/nexustech101/decorates"
|
|
42
|
+
Repository = "https://github.com/nexustech101/decorates"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "-v --tb=short"
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
include = ["decorates*"]
|
|
52
|
+
exclude = ["*.tests*", "*.tests.*"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools]
|
|
55
|
+
package-dir = {"" = "src"} # Container for package modules
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
decorates = ["py.typed"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A lightweight, decorates-based CLI decorates.
|
|
3
|
+
|
|
4
|
+
Public API surface::
|
|
5
|
+
|
|
6
|
+
from decorates.cli import (
|
|
7
|
+
CommandRegistry,
|
|
8
|
+
DIContainer,
|
|
9
|
+
MiddlewareChain,
|
|
10
|
+
Dispatcher,
|
|
11
|
+
CommandExecutionError,
|
|
12
|
+
build_parser,
|
|
13
|
+
load_plugins,
|
|
14
|
+
logging_middleware_pre,
|
|
15
|
+
logging_middleware_post,
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from decorates.cli.dispatcher import Dispatcher
|
|
20
|
+
from decorates.cli.middleware import (
|
|
21
|
+
MiddlewareChain,
|
|
22
|
+
logging_middleware_post,
|
|
23
|
+
logging_middleware_pre,
|
|
24
|
+
)
|
|
25
|
+
from decorates.cli.parser import build_parser
|
|
26
|
+
from decorates.cli.container import DIContainer
|
|
27
|
+
from decorates.cli.exceptions import (
|
|
28
|
+
CommandExecutionError,
|
|
29
|
+
DependencyNotFoundError,
|
|
30
|
+
DuplicateCommandError,
|
|
31
|
+
FrameworkError,
|
|
32
|
+
PluginLoadError,
|
|
33
|
+
UnknownCommandError,
|
|
34
|
+
)
|
|
35
|
+
from decorates.cli.registry import CommandRegistry
|
|
36
|
+
from decorates.cli.plugins import load_plugins
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Core decorates
|
|
40
|
+
"CommandRegistry",
|
|
41
|
+
"DIContainer",
|
|
42
|
+
"Dispatcher",
|
|
43
|
+
"MiddlewareChain",
|
|
44
|
+
"build_parser",
|
|
45
|
+
"load_plugins",
|
|
46
|
+
"logging_middleware_pre",
|
|
47
|
+
"logging_middleware_post",
|
|
48
|
+
|
|
49
|
+
# Exceptions
|
|
50
|
+
"CommandExecutionError",
|
|
51
|
+
"DependencyNotFoundError",
|
|
52
|
+
"DuplicateCommandError",
|
|
53
|
+
"FrameworkError",
|
|
54
|
+
"PluginLoadError",
|
|
55
|
+
"UnknownCommandError",
|
|
56
|
+
]
|