belgie-alchemy 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.
- belgie_alchemy-0.1.0/PKG-INFO +266 -0
- belgie_alchemy-0.1.0/README.md +253 -0
- belgie_alchemy-0.1.0/pyproject.toml +19 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__init__.py +33 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/adapter/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/adapter/test_adapter.py +493 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/auth_models/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/auth_models/test_auth_models.py +91 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/base/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/base/test_base.py +90 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/conftest.py +39 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/__init__.py +12 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/database.py +38 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/models.py +119 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/mixins/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/mixins/test_mixins.py +80 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/test_settings.py +342 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/test_settings_integration.py +416 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/types/__init__.py +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/types/test_types.py +155 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/adapter.py +323 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/base.py +25 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/mixins.py +83 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/py.typed +0 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/settings.py +146 -0
- belgie_alchemy-0.1.0/src/belgie_alchemy/types.py +32 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: belgie-alchemy
|
|
3
|
+
Version: 0.1.0
|
|
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,253 @@
|
|
|
1
|
+
# belgie-alchemy
|
|
2
|
+
|
|
3
|
+
SQLAlchemy 2.0 building blocks for database models.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`belgie-alchemy` provides opinionated defaults and utilities for SQLAlchemy:
|
|
8
|
+
|
|
9
|
+
- **Base**: Declarative base with dataclass mapping and sensible defaults
|
|
10
|
+
- **Mixins**: `PrimaryKeyMixin` (UUID), `TimestampMixin` (created/updated/deleted timestamps)
|
|
11
|
+
- **Types**: `DateTimeUTC` (timezone-aware datetimes), `Scopes` (dialect-specific array/JSON storage)
|
|
12
|
+
|
|
13
|
+
This module provides **building blocks only** - you define your own models.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC
|
|
20
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
21
|
+
|
|
22
|
+
class Article(Base, PrimaryKeyMixin, TimestampMixin):
|
|
23
|
+
__tablename__ = "articles"
|
|
24
|
+
|
|
25
|
+
title: Mapped[str]
|
|
26
|
+
published_at: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This gives you:
|
|
30
|
+
|
|
31
|
+
- UUID primary key with server-side generation
|
|
32
|
+
- Automatic `created_at`, `updated_at`, `deleted_at` timestamps
|
|
33
|
+
- Timezone-aware datetime handling
|
|
34
|
+
- Dataclass-style `__init__`, `__repr__`, `__eq__`
|
|
35
|
+
|
|
36
|
+
## Building Blocks
|
|
37
|
+
|
|
38
|
+
### Base
|
|
39
|
+
|
|
40
|
+
Declarative base with dataclass mapping enabled:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from belgie_alchemy import Base
|
|
44
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
45
|
+
|
|
46
|
+
class MyModel(Base):
|
|
47
|
+
__tablename__ = "my_models"
|
|
48
|
+
|
|
49
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
50
|
+
name: Mapped[str]
|
|
51
|
+
|
|
52
|
+
# Dataclass-style instantiation
|
|
53
|
+
model = MyModel(id=1, name="example")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Features:
|
|
57
|
+
|
|
58
|
+
- Consistent naming conventions for constraints
|
|
59
|
+
- Automatic type annotation mapping (`datetime` → `DateTimeUTC`)
|
|
60
|
+
- Dataclass mapping with `kw_only=True`, `repr=True`, `eq=True`
|
|
61
|
+
|
|
62
|
+
### Mixins
|
|
63
|
+
|
|
64
|
+
#### PrimaryKeyMixin
|
|
65
|
+
|
|
66
|
+
Adds a UUID primary key with server-side generation:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from belgie_alchemy import Base, PrimaryKeyMixin
|
|
70
|
+
|
|
71
|
+
class MyModel(Base, PrimaryKeyMixin):
|
|
72
|
+
__tablename__ = "my_models"
|
|
73
|
+
# Automatically includes: id: Mapped[UUID]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `id` field:
|
|
77
|
+
|
|
78
|
+
- Type: `UUID`
|
|
79
|
+
- Server-generated using `gen_random_uuid()`
|
|
80
|
+
- Indexed and unique
|
|
81
|
+
- Primary key
|
|
82
|
+
|
|
83
|
+
#### TimestampMixin
|
|
84
|
+
|
|
85
|
+
Adds automatic timestamp tracking:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from belgie_alchemy import Base, TimestampMixin
|
|
89
|
+
|
|
90
|
+
class MyModel(Base, TimestampMixin):
|
|
91
|
+
__tablename__ = "my_models"
|
|
92
|
+
# Automatically includes:
|
|
93
|
+
# - created_at: Mapped[datetime]
|
|
94
|
+
# - updated_at: Mapped[datetime] (auto-updates on changes)
|
|
95
|
+
# - deleted_at: Mapped[datetime | None]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Features:
|
|
99
|
+
|
|
100
|
+
- `created_at` set automatically on insert
|
|
101
|
+
- `updated_at` auto-updates on row changes
|
|
102
|
+
- `deleted_at` for soft deletion
|
|
103
|
+
- `mark_deleted()` method to set `deleted_at`
|
|
104
|
+
|
|
105
|
+
### Types
|
|
106
|
+
|
|
107
|
+
#### DateTimeUTC
|
|
108
|
+
|
|
109
|
+
Timezone-aware datetime storage:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from datetime import datetime
|
|
113
|
+
from belgie_alchemy import Base, DateTimeUTC
|
|
114
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
115
|
+
|
|
116
|
+
class Event(Base):
|
|
117
|
+
__tablename__ = "events"
|
|
118
|
+
|
|
119
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
120
|
+
happened_at: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Features:
|
|
124
|
+
|
|
125
|
+
- Automatically converts naive datetimes to UTC
|
|
126
|
+
- Preserves timezone-aware datetimes
|
|
127
|
+
- Always returns UTC-aware datetimes from database
|
|
128
|
+
- Works with PostgreSQL, SQLite, MySQL
|
|
129
|
+
|
|
130
|
+
#### Scopes
|
|
131
|
+
|
|
132
|
+
Dialect-specific array storage for permission scopes:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from enum import StrEnum
|
|
136
|
+
from belgie_alchemy import Base, Scopes
|
|
137
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
138
|
+
|
|
139
|
+
class User(Base):
|
|
140
|
+
__tablename__ = "users"
|
|
141
|
+
|
|
142
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
143
|
+
# Option 1: Simple string array (works everywhere)
|
|
144
|
+
scopes: Mapped[list[str] | None] = mapped_column(Scopes, default=None)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Features:
|
|
148
|
+
|
|
149
|
+
- PostgreSQL: Uses native `ARRAY(String)` type
|
|
150
|
+
- SQLite/MySQL: Uses JSON storage
|
|
151
|
+
- Automatically converts StrEnum values to strings
|
|
152
|
+
- Handles `None` values correctly
|
|
153
|
+
|
|
154
|
+
For PostgreSQL with application-specific enum types, you can override:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from enum import StrEnum
|
|
158
|
+
from sqlalchemy import ARRAY
|
|
159
|
+
from sqlalchemy.dialects.postgresql import ENUM
|
|
160
|
+
|
|
161
|
+
class AppScope(StrEnum):
|
|
162
|
+
READ = "resource:read"
|
|
163
|
+
WRITE = "resource:write"
|
|
164
|
+
ADMIN = "admin"
|
|
165
|
+
|
|
166
|
+
class User(Base):
|
|
167
|
+
__tablename__ = "users"
|
|
168
|
+
|
|
169
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
170
|
+
# Option 2: PostgreSQL native ENUM array (type-safe)
|
|
171
|
+
scopes: Mapped[list[AppScope] | None] = mapped_column(
|
|
172
|
+
ARRAY(ENUM(AppScope, name="app_scope", create_type=True)),
|
|
173
|
+
default=None,
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Complete Example: Auth Models
|
|
178
|
+
|
|
179
|
+
See `examples/alchemy/auth_models.py` for a complete reference implementation of authentication models:
|
|
180
|
+
|
|
181
|
+
- `User` - with email, verification, and scopes
|
|
182
|
+
- `Account` - OAuth provider linkage
|
|
183
|
+
- `Session` - user session management
|
|
184
|
+
- `OAuthState` - OAuth flow state
|
|
185
|
+
|
|
186
|
+
**These are templates** - copy them to your project and customize as needed.
|
|
187
|
+
|
|
188
|
+
Example structure:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from datetime import datetime
|
|
192
|
+
from uuid import UUID
|
|
193
|
+
from sqlalchemy import ForeignKey
|
|
194
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
195
|
+
from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC, Scopes
|
|
196
|
+
|
|
197
|
+
class User(Base, PrimaryKeyMixin, TimestampMixin):
|
|
198
|
+
__tablename__ = "users"
|
|
199
|
+
|
|
200
|
+
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
201
|
+
email_verified: Mapped[bool] = mapped_column(default=False)
|
|
202
|
+
scopes: Mapped[list[str] | None] = mapped_column(Scopes, default=None)
|
|
203
|
+
|
|
204
|
+
accounts: Mapped[list["Account"]] = relationship(
|
|
205
|
+
back_populates="user",
|
|
206
|
+
cascade="all, delete-orphan",
|
|
207
|
+
init=False,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
class Account(Base, PrimaryKeyMixin, TimestampMixin):
|
|
211
|
+
__tablename__ = "accounts"
|
|
212
|
+
|
|
213
|
+
user_id: Mapped[UUID] = mapped_column(
|
|
214
|
+
ForeignKey("users.id", ondelete="cascade"),
|
|
215
|
+
nullable=False,
|
|
216
|
+
)
|
|
217
|
+
provider: Mapped[str]
|
|
218
|
+
provider_account_id: Mapped[str]
|
|
219
|
+
|
|
220
|
+
user: Mapped[User] = relationship(
|
|
221
|
+
back_populates="accounts",
|
|
222
|
+
lazy="selectin",
|
|
223
|
+
init=False,
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Design Principles
|
|
228
|
+
|
|
229
|
+
1. **Building blocks, not frameworks** - You own your models completely
|
|
230
|
+
2. **Sensible defaults** - UTC datetimes, UUIDs, timestamps by default
|
|
231
|
+
3. **Dataclass-friendly** - Clean instantiation and repr
|
|
232
|
+
4. **Dialect-aware** - Use the best type for each database
|
|
233
|
+
5. **Minimal magic** - Clear, explicit behavior
|
|
234
|
+
|
|
235
|
+
## Migration from impl/auth.py
|
|
236
|
+
|
|
237
|
+
If you previously imported models from `belgie_alchemy.impl.auth`:
|
|
238
|
+
|
|
239
|
+
**Before:**
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from belgie_alchemy.impl.auth import User, Account, Session, OAuthState
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**After:**
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Copy models from examples/alchemy/auth_models.py to your project
|
|
249
|
+
# Then import from your own code:
|
|
250
|
+
from myapp.models import User, Account, Session, OAuthState
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This gives you full control to customize the models for your application.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "belgie-alchemy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SQLAlchemy building blocks for Belgie"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Matt LeMay", email = "mplemay@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"belgie-proto",
|
|
12
|
+
"pydantic>=2.0",
|
|
13
|
+
"pydantic-settings>=2.0",
|
|
14
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.9.28,<0.10.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""SQLAlchemy 2.0 building blocks for database models.
|
|
2
|
+
|
|
3
|
+
This module provides opinionated defaults and utilities for SQLAlchemy:
|
|
4
|
+
- Base: Declarative base with dataclass mapping and sensible defaults
|
|
5
|
+
- Mixins: PrimaryKeyMixin (UUID), TimestampMixin (created/updated/deleted)
|
|
6
|
+
- Types: DateTimeUTC (timezone-aware datetime storage)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC
|
|
10
|
+
|
|
11
|
+
class MyModel(Base, PrimaryKeyMixin, TimestampMixin):
|
|
12
|
+
__tablename__ = "my_models"
|
|
13
|
+
|
|
14
|
+
name: Mapped[str]
|
|
15
|
+
created_on: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
16
|
+
|
|
17
|
+
For complete auth model examples, see examples/alchemy/auth_models.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from belgie_alchemy.adapter import AlchemyAdapter
|
|
21
|
+
from belgie_alchemy.base import Base
|
|
22
|
+
from belgie_alchemy.mixins import PrimaryKeyMixin, TimestampMixin
|
|
23
|
+
from belgie_alchemy.settings import DatabaseSettings
|
|
24
|
+
from belgie_alchemy.types import DateTimeUTC
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AlchemyAdapter",
|
|
28
|
+
"Base",
|
|
29
|
+
"DatabaseSettings",
|
|
30
|
+
"DateTimeUTC",
|
|
31
|
+
"PrimaryKeyMixin",
|
|
32
|
+
"TimestampMixin",
|
|
33
|
+
]
|
|
File without changes
|
|
File without changes
|