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.
Files changed (28) hide show
  1. belgie_alchemy-0.1.0/PKG-INFO +266 -0
  2. belgie_alchemy-0.1.0/README.md +253 -0
  3. belgie_alchemy-0.1.0/pyproject.toml +19 -0
  4. belgie_alchemy-0.1.0/src/belgie_alchemy/__init__.py +33 -0
  5. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/__init__.py +0 -0
  6. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/adapter/__init__.py +0 -0
  7. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/adapter/test_adapter.py +493 -0
  8. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/auth_models/__init__.py +0 -0
  9. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/auth_models/test_auth_models.py +91 -0
  10. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/base/__init__.py +0 -0
  11. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/base/test_base.py +90 -0
  12. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/conftest.py +39 -0
  13. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/__init__.py +12 -0
  14. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/database.py +38 -0
  15. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/fixtures/models.py +119 -0
  16. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/mixins/__init__.py +0 -0
  17. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/mixins/test_mixins.py +80 -0
  18. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/__init__.py +0 -0
  19. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/test_settings.py +342 -0
  20. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/settings/test_settings_integration.py +416 -0
  21. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/types/__init__.py +0 -0
  22. belgie_alchemy-0.1.0/src/belgie_alchemy/__tests__/types/test_types.py +155 -0
  23. belgie_alchemy-0.1.0/src/belgie_alchemy/adapter.py +323 -0
  24. belgie_alchemy-0.1.0/src/belgie_alchemy/base.py +25 -0
  25. belgie_alchemy-0.1.0/src/belgie_alchemy/mixins.py +83 -0
  26. belgie_alchemy-0.1.0/src/belgie_alchemy/py.typed +0 -0
  27. belgie_alchemy-0.1.0/src/belgie_alchemy/settings.py +146 -0
  28. 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
+ ]