belgie-alchemy 0.1.0a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.3
2
+ Name: belgie-alchemy
3
+ Version: 0.1.0a4
4
+ Summary: SQLAlchemy building blocks for Belgie
5
+ Author: Matt LeMay
6
+ Author-email: Matt LeMay <mplemay@users.noreply.github.com>
7
+ Requires-Dist: belgie-proto
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: pydantic-settings>=2.0
10
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ # belgie-alchemy
15
+
16
+ SQLAlchemy 2.0 building blocks for database models.
17
+
18
+ ## Overview
19
+
20
+ `belgie-alchemy` provides opinionated defaults and utilities for SQLAlchemy:
21
+
22
+ - **Base**: Declarative base with dataclass mapping and sensible defaults
23
+ - **Mixins**: `PrimaryKeyMixin` (UUID), `TimestampMixin` (created/updated/deleted timestamps)
24
+ - **Types**: `DateTimeUTC` (timezone-aware datetimes), `Scopes` (dialect-specific array/JSON storage)
25
+
26
+ This module provides **building blocks only** - you define your own models.
27
+
28
+ ## Quick Start
29
+
30
+ ```python
31
+ from datetime import datetime
32
+ from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC
33
+ from sqlalchemy.orm import Mapped, mapped_column
34
+
35
+ class Article(Base, PrimaryKeyMixin, TimestampMixin):
36
+ __tablename__ = "articles"
37
+
38
+ title: Mapped[str]
39
+ published_at: Mapped[datetime] = mapped_column(DateTimeUTC)
40
+ ```
41
+
42
+ This gives you:
43
+
44
+ - UUID primary key with server-side generation
45
+ - Automatic `created_at`, `updated_at`, `deleted_at` timestamps
46
+ - Timezone-aware datetime handling
47
+ - Dataclass-style `__init__`, `__repr__`, `__eq__`
48
+
49
+ ## Building Blocks
50
+
51
+ ### Base
52
+
53
+ Declarative base with dataclass mapping enabled:
54
+
55
+ ```python
56
+ from belgie_alchemy import Base
57
+ from sqlalchemy.orm import Mapped, mapped_column
58
+
59
+ class MyModel(Base):
60
+ __tablename__ = "my_models"
61
+
62
+ id: Mapped[int] = mapped_column(primary_key=True)
63
+ name: Mapped[str]
64
+
65
+ # Dataclass-style instantiation
66
+ model = MyModel(id=1, name="example")
67
+ ```
68
+
69
+ Features:
70
+
71
+ - Consistent naming conventions for constraints
72
+ - Automatic type annotation mapping (`datetime` → `DateTimeUTC`)
73
+ - Dataclass mapping with `kw_only=True`, `repr=True`, `eq=True`
74
+
75
+ ### Mixins
76
+
77
+ #### PrimaryKeyMixin
78
+
79
+ Adds a UUID primary key with server-side generation:
80
+
81
+ ```python
82
+ from belgie_alchemy import Base, PrimaryKeyMixin
83
+
84
+ class MyModel(Base, PrimaryKeyMixin):
85
+ __tablename__ = "my_models"
86
+ # Automatically includes: id: Mapped[UUID]
87
+ ```
88
+
89
+ The `id` field:
90
+
91
+ - Type: `UUID`
92
+ - Server-generated using `gen_random_uuid()`
93
+ - Indexed and unique
94
+ - Primary key
95
+
96
+ #### TimestampMixin
97
+
98
+ Adds automatic timestamp tracking:
99
+
100
+ ```python
101
+ from belgie_alchemy import Base, TimestampMixin
102
+
103
+ class MyModel(Base, TimestampMixin):
104
+ __tablename__ = "my_models"
105
+ # Automatically includes:
106
+ # - created_at: Mapped[datetime]
107
+ # - updated_at: Mapped[datetime] (auto-updates on changes)
108
+ # - deleted_at: Mapped[datetime | None]
109
+ ```
110
+
111
+ Features:
112
+
113
+ - `created_at` set automatically on insert
114
+ - `updated_at` auto-updates on row changes
115
+ - `deleted_at` for soft deletion
116
+ - `mark_deleted()` method to set `deleted_at`
117
+
118
+ ### Types
119
+
120
+ #### DateTimeUTC
121
+
122
+ Timezone-aware datetime storage:
123
+
124
+ ```python
125
+ from datetime import datetime
126
+ from belgie_alchemy import Base, DateTimeUTC
127
+ from sqlalchemy.orm import Mapped, mapped_column
128
+
129
+ class Event(Base):
130
+ __tablename__ = "events"
131
+
132
+ id: Mapped[int] = mapped_column(primary_key=True)
133
+ happened_at: Mapped[datetime] = mapped_column(DateTimeUTC)
134
+ ```
135
+
136
+ Features:
137
+
138
+ - Automatically converts naive datetimes to UTC
139
+ - Preserves timezone-aware datetimes
140
+ - Always returns UTC-aware datetimes from database
141
+ - Works with PostgreSQL, SQLite, MySQL
142
+
143
+ #### Scopes
144
+
145
+ Dialect-specific array storage for permission scopes:
146
+
147
+ ```python
148
+ from enum import StrEnum
149
+ from belgie_alchemy import Base, Scopes
150
+ from sqlalchemy.orm import Mapped, mapped_column
151
+
152
+ class User(Base):
153
+ __tablename__ = "users"
154
+
155
+ id: Mapped[int] = mapped_column(primary_key=True)
156
+ # Option 1: Simple string array (works everywhere)
157
+ scopes: Mapped[list[str] | None] = mapped_column(Scopes, default=None)
158
+ ```
159
+
160
+ Features:
161
+
162
+ - PostgreSQL: Uses native `ARRAY(String)` type
163
+ - SQLite/MySQL: Uses JSON storage
164
+ - Automatically converts StrEnum values to strings
165
+ - Handles `None` values correctly
166
+
167
+ For PostgreSQL with application-specific enum types, you can override:
168
+
169
+ ```python
170
+ from enum import StrEnum
171
+ from sqlalchemy import ARRAY
172
+ from sqlalchemy.dialects.postgresql import ENUM
173
+
174
+ class AppScope(StrEnum):
175
+ READ = "resource:read"
176
+ WRITE = "resource:write"
177
+ ADMIN = "admin"
178
+
179
+ class User(Base):
180
+ __tablename__ = "users"
181
+
182
+ id: Mapped[int] = mapped_column(primary_key=True)
183
+ # Option 2: PostgreSQL native ENUM array (type-safe)
184
+ scopes: Mapped[list[AppScope] | None] = mapped_column(
185
+ ARRAY(ENUM(AppScope, name="app_scope", create_type=True)),
186
+ default=None,
187
+ )
188
+ ```
189
+
190
+ ## Complete Example: Auth Models
191
+
192
+ See `examples/alchemy/auth_models.py` for a complete reference implementation of authentication models:
193
+
194
+ - `User` - with email, verification, and scopes
195
+ - `Account` - OAuth provider linkage
196
+ - `Session` - user session management
197
+ - `OAuthState` - OAuth flow state
198
+
199
+ **These are templates** - copy them to your project and customize as needed.
200
+
201
+ Example structure:
202
+
203
+ ```python
204
+ from datetime import datetime
205
+ from uuid import UUID
206
+ from sqlalchemy import ForeignKey
207
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
208
+ from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC, Scopes
209
+
210
+ class User(Base, PrimaryKeyMixin, TimestampMixin):
211
+ __tablename__ = "users"
212
+
213
+ email: Mapped[str] = mapped_column(unique=True, index=True)
214
+ email_verified: Mapped[bool] = mapped_column(default=False)
215
+ scopes: Mapped[list[str] | None] = mapped_column(Scopes, default=None)
216
+
217
+ accounts: Mapped[list["Account"]] = relationship(
218
+ back_populates="user",
219
+ cascade="all, delete-orphan",
220
+ init=False,
221
+ )
222
+
223
+ class Account(Base, PrimaryKeyMixin, TimestampMixin):
224
+ __tablename__ = "accounts"
225
+
226
+ user_id: Mapped[UUID] = mapped_column(
227
+ ForeignKey("users.id", ondelete="cascade"),
228
+ nullable=False,
229
+ )
230
+ provider: Mapped[str]
231
+ provider_account_id: Mapped[str]
232
+
233
+ user: Mapped[User] = relationship(
234
+ back_populates="accounts",
235
+ lazy="selectin",
236
+ init=False,
237
+ )
238
+ ```
239
+
240
+ ## Design Principles
241
+
242
+ 1. **Building blocks, not frameworks** - You own your models completely
243
+ 2. **Sensible defaults** - UTC datetimes, UUIDs, timestamps by default
244
+ 3. **Dataclass-friendly** - Clean instantiation and repr
245
+ 4. **Dialect-aware** - Use the best type for each database
246
+ 5. **Minimal magic** - Clear, explicit behavior
247
+
248
+ ## Migration from impl/auth.py
249
+
250
+ If you previously imported models from `belgie_alchemy.impl.auth`:
251
+
252
+ **Before:**
253
+
254
+ ```python
255
+ from belgie_alchemy.impl.auth import User, Account, Session, OAuthState
256
+ ```
257
+
258
+ **After:**
259
+
260
+ ```python
261
+ # Copy models from examples/alchemy/auth_models.py to your project
262
+ # Then import from your own code:
263
+ from myapp.models import User, Account, Session, OAuthState
264
+ ```
265
+
266
+ This gives you full control to customize the models for your application.
@@ -0,0 +1,28 @@
1
+ belgie_alchemy/__init__.py,sha256=zK7g-06wUYg1ThS389aPFnAOK38osQQwtw0ZizUajDc,1070
2
+ belgie_alchemy/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ belgie_alchemy/__tests__/adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ belgie_alchemy/__tests__/adapter/test_adapter.py,sha256=-tlLCqihEkJXJRloeLVRtbPdIfpKMQ3axsfs7xZRRTk,16064
5
+ belgie_alchemy/__tests__/auth_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ belgie_alchemy/__tests__/auth_models/test_auth_models.py,sha256=RQNaGfmvGNIrndgokueKMxvR9uMnOoj9N7FKVTAkM9o,2741
7
+ belgie_alchemy/__tests__/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ belgie_alchemy/__tests__/base/test_base.py,sha256=XZVy2eoutSOpDdufvZkEc7uJmmVOyxOh9HJkjsCcimM,3298
9
+ belgie_alchemy/__tests__/conftest.py,sha256=S_c1HqCF3fviRnAcox0ZwKPcKiAPOcfEDMguJRNqqNE,1190
10
+ belgie_alchemy/__tests__/fixtures/__init__.py,sha256=Ldyp56D1LrPVvJHcXfyvxtskqbeqXZqme-FNnNBb-oE,347
11
+ belgie_alchemy/__tests__/fixtures/database.py,sha256=yKYkRQcdbZ9wGDNWhn6tDhY-rF2QQz0oibEb2LSoFdY,1129
12
+ belgie_alchemy/__tests__/fixtures/models.py,sha256=ftWLG-MkqmUa1Z_Yp7pon6m84qLbtJo9XmCYiYYlsTo,3822
13
+ belgie_alchemy/__tests__/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ belgie_alchemy/__tests__/mixins/test_mixins.py,sha256=0OQWWTc0fs_CN_4lrvVJgxrXji5mfn0wgRyRVJd_4gU,2419
15
+ belgie_alchemy/__tests__/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ belgie_alchemy/__tests__/settings/test_settings.py,sha256=jW7ZtTDfkBSX9AJYUKe5HSm2AjGSd-D51BFoRRVCIAA,11263
17
+ belgie_alchemy/__tests__/settings/test_settings_integration.py,sha256=StkOTvfssTG8lvyAa_r-pdyyO1e6vC8IeRQJMuRr7hc,15480
18
+ belgie_alchemy/__tests__/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ belgie_alchemy/__tests__/types/test_types.py,sha256=bTMlfmyCJRLKsZ0Hj-4gor9xlbPTfteR0S0pqzaWR-8,5300
20
+ belgie_alchemy/adapter.py,sha256=30ePooceD1O40WpODfEycxc4KaOKx_71GiCJqqF1rRE,10305
21
+ belgie_alchemy/base.py,sha256=x-AddtIS4aBzA8p7exFDK-AYpMZ9HdhuwzmcdpPZ74g,843
22
+ belgie_alchemy/mixins.py,sha256=rn3zXiU-UdyEcYOBNpy8p5JFkJRVzr6QaL_kk2VfoV8,3090
23
+ belgie_alchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ belgie_alchemy/settings.py,sha256=0zK-GDEVzmI3efEceWGV7H3jieW63gqZHVJ4vgJVlMM,4991
25
+ belgie_alchemy/types.py,sha256=BJ_rL5Ce5TyVV-C3I20VukA8A3Qifxzch-0OY1lqAcQ,1177
26
+ belgie_alchemy-0.1.0a4.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
27
+ belgie_alchemy-0.1.0a4.dist-info/METADATA,sha256=N0hl6JhUuzWWNqZeZft17_pz1Qr725fmpEulnILq-p0,7005
28
+ belgie_alchemy-0.1.0a4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.28
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any