belgie-alchemy 0.1.0__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.
- belgie_alchemy/__init__.py +33 -0
- belgie_alchemy/__tests__/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/test_adapter.py +493 -0
- belgie_alchemy/__tests__/auth_models/__init__.py +0 -0
- belgie_alchemy/__tests__/auth_models/test_auth_models.py +91 -0
- belgie_alchemy/__tests__/base/__init__.py +0 -0
- belgie_alchemy/__tests__/base/test_base.py +90 -0
- belgie_alchemy/__tests__/conftest.py +39 -0
- belgie_alchemy/__tests__/fixtures/__init__.py +12 -0
- belgie_alchemy/__tests__/fixtures/database.py +38 -0
- belgie_alchemy/__tests__/fixtures/models.py +119 -0
- belgie_alchemy/__tests__/mixins/__init__.py +0 -0
- belgie_alchemy/__tests__/mixins/test_mixins.py +80 -0
- belgie_alchemy/__tests__/settings/__init__.py +0 -0
- belgie_alchemy/__tests__/settings/test_settings.py +342 -0
- belgie_alchemy/__tests__/settings/test_settings_integration.py +416 -0
- belgie_alchemy/__tests__/types/__init__.py +0 -0
- belgie_alchemy/__tests__/types/test_types.py +155 -0
- belgie_alchemy/adapter.py +323 -0
- belgie_alchemy/base.py +25 -0
- belgie_alchemy/mixins.py +83 -0
- belgie_alchemy/py.typed +0 -0
- belgie_alchemy/settings.py +146 -0
- belgie_alchemy/types.py +32 -0
- belgie_alchemy-0.1.0.dist-info/METADATA +266 -0
- belgie_alchemy-0.1.0.dist-info/RECORD +28 -0
- belgie_alchemy-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from importlib.util import find_spec
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from sqlalchemy import text
|
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
|
|
10
|
+
from belgie_alchemy.settings import DatabaseSettings
|
|
11
|
+
|
|
12
|
+
ASYNC_PG_AVAILABLE = find_spec("asyncpg") is not None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_sqlite_engine_fk_enabled() -> None:
|
|
17
|
+
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:", "enable_foreign_keys": True})
|
|
18
|
+
|
|
19
|
+
async with db.engine.connect() as conn:
|
|
20
|
+
result = await conn.execute(text("PRAGMA foreign_keys"))
|
|
21
|
+
value = result.scalar_one()
|
|
22
|
+
|
|
23
|
+
assert value == 1
|
|
24
|
+
await db.engine.dispose()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_session_maker_expire_disabled() -> None:
|
|
29
|
+
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
30
|
+
|
|
31
|
+
session_factory = db.session_maker
|
|
32
|
+
assert session_factory.kw["expire_on_commit"] is False
|
|
33
|
+
|
|
34
|
+
async with session_factory() as session:
|
|
35
|
+
assert isinstance(session, AsyncSession)
|
|
36
|
+
|
|
37
|
+
await db.engine.dispose()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_postgres_url_creation() -> None:
|
|
41
|
+
if not ASYNC_PG_AVAILABLE:
|
|
42
|
+
pytest.skip("asyncpg not installed; skip postgres engine test")
|
|
43
|
+
|
|
44
|
+
db = DatabaseSettings(
|
|
45
|
+
dialect={
|
|
46
|
+
"type": "postgres",
|
|
47
|
+
"host": "localhost",
|
|
48
|
+
"port": 5432,
|
|
49
|
+
"database": "belgie",
|
|
50
|
+
"username": "user",
|
|
51
|
+
"password": "secret",
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert db.engine.url.get_backend_name() == "postgresql"
|
|
56
|
+
assert db.engine.url.get_driver_name() == "asyncpg"
|
|
57
|
+
assert db.engine.url.username == "user"
|
|
58
|
+
assert db.engine.url.host == "localhost"
|
|
59
|
+
assert db.engine.url.database == "belgie"
|
|
60
|
+
assert db.engine.url.port == 5432
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_sqlite_fk_violations_raise_error() -> None:
|
|
65
|
+
"""Test that foreign key constraints are enforced when enabled."""
|
|
66
|
+
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:", "enable_foreign_keys": True})
|
|
67
|
+
|
|
68
|
+
# Try to insert a child record with invalid FK - should raise IntegrityError
|
|
69
|
+
async with db.engine.begin() as conn:
|
|
70
|
+
# Create simple test tables
|
|
71
|
+
await conn.execute(text("CREATE TABLE test_parents (id INTEGER PRIMARY KEY, name TEXT)"))
|
|
72
|
+
await conn.execute(
|
|
73
|
+
text(
|
|
74
|
+
"CREATE TABLE test_children "
|
|
75
|
+
"(id INTEGER PRIMARY KEY, parent_id INTEGER NOT NULL REFERENCES test_parents(id), name TEXT)",
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async with db.session_maker() as session:
|
|
80
|
+
# Try to insert child without parent - should raise IntegrityError
|
|
81
|
+
with pytest.raises(IntegrityError): # noqa: PT012
|
|
82
|
+
await session.execute(text("INSERT INTO test_children (id, parent_id, name) VALUES (1, 999, 'orphan')"))
|
|
83
|
+
await session.commit()
|
|
84
|
+
|
|
85
|
+
await db.engine.dispose()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_dependency_yields_different_sessions() -> None:
|
|
90
|
+
"""Test that dependency yields different session instances."""
|
|
91
|
+
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
92
|
+
|
|
93
|
+
sessions = []
|
|
94
|
+
async for session1 in db.dependency():
|
|
95
|
+
sessions.append(session1)
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
async for session2 in db.dependency():
|
|
99
|
+
sessions.append(session2)
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
assert len(sessions) == 2
|
|
103
|
+
assert sessions[0] is not sessions[1]
|
|
104
|
+
await db.engine.dispose()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
async def test_dependency_handles_exceptions() -> None:
|
|
109
|
+
"""Test that dependency properly handles exceptions."""
|
|
110
|
+
db = DatabaseSettings(dialect={"type": "sqlite", "database": ":memory:"})
|
|
111
|
+
|
|
112
|
+
# Simulate exception during request handling
|
|
113
|
+
simulated_error = "Simulated error"
|
|
114
|
+
try:
|
|
115
|
+
async for _session in db.dependency():
|
|
116
|
+
# Force an error
|
|
117
|
+
raise ValueError(simulated_error) # noqa: TRY301
|
|
118
|
+
except ValueError:
|
|
119
|
+
pass # Expected
|
|
120
|
+
|
|
121
|
+
# Verify we can still get new sessions
|
|
122
|
+
async for session in db.dependency():
|
|
123
|
+
assert isinstance(session, AsyncSession)
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
await db.engine.dispose()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_postgres_settings_validation() -> None:
|
|
130
|
+
"""Test that PostgreSQL settings validates all required fields."""
|
|
131
|
+
# Test with valid data
|
|
132
|
+
db = DatabaseSettings(
|
|
133
|
+
dialect={
|
|
134
|
+
"type": "postgres",
|
|
135
|
+
"host": "db.example.com",
|
|
136
|
+
"port": 5433,
|
|
137
|
+
"database": "testdb",
|
|
138
|
+
"username": "testuser",
|
|
139
|
+
"password": "testpass",
|
|
140
|
+
"pool_size": 10,
|
|
141
|
+
"max_overflow": 20,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert db.dialect.type == "postgres"
|
|
146
|
+
assert db.dialect.host == "db.example.com"
|
|
147
|
+
assert db.dialect.port == 5433
|
|
148
|
+
assert db.dialect.database == "testdb"
|
|
149
|
+
assert db.dialect.username == "testuser"
|
|
150
|
+
assert db.dialect.password.get_secret_value() == "testpass"
|
|
151
|
+
assert db.dialect.pool_size == 10
|
|
152
|
+
assert db.dialect.max_overflow == 20
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_sqlite_settings_validation() -> None:
|
|
156
|
+
"""Test that SQLite settings validates correctly."""
|
|
157
|
+
db = DatabaseSettings(
|
|
158
|
+
dialect={
|
|
159
|
+
"type": "sqlite",
|
|
160
|
+
"database": "/tmp/test.db", # noqa: S108
|
|
161
|
+
"enable_foreign_keys": False,
|
|
162
|
+
"echo": True,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assert db.dialect.type == "sqlite"
|
|
167
|
+
assert db.dialect.database == "/tmp/test.db" # noqa: S108
|
|
168
|
+
assert db.dialect.enable_foreign_keys is False
|
|
169
|
+
assert db.dialect.echo is True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ==================== PostgreSQL Integration Tests ====================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.asyncio
|
|
176
|
+
@pytest.mark.integration
|
|
177
|
+
async def test_postgres_engine_connection() -> None:
|
|
178
|
+
"""Test actual PostgreSQL connection and session creation.
|
|
179
|
+
|
|
180
|
+
This test requires a PostgreSQL instance to be available.
|
|
181
|
+
Set POSTGRES_TEST_URL environment variable or skip.
|
|
182
|
+
"""
|
|
183
|
+
if not ASYNC_PG_AVAILABLE:
|
|
184
|
+
pytest.skip("asyncpg not installed")
|
|
185
|
+
|
|
186
|
+
# Allow configuring test database via environment
|
|
187
|
+
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
188
|
+
if not test_url:
|
|
189
|
+
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
190
|
+
|
|
191
|
+
# Parse URL components (format: postgresql://user:pass@host:port/db)
|
|
192
|
+
try:
|
|
193
|
+
parsed = urlparse(test_url)
|
|
194
|
+
db = DatabaseSettings(
|
|
195
|
+
dialect={
|
|
196
|
+
"type": "postgres",
|
|
197
|
+
"host": parsed.hostname or "localhost",
|
|
198
|
+
"port": parsed.port or 5432,
|
|
199
|
+
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
200
|
+
"username": parsed.username or "postgres",
|
|
201
|
+
"password": parsed.password or "",
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Test basic connection
|
|
206
|
+
async with db.engine.connect() as conn:
|
|
207
|
+
result = await conn.execute(text("SELECT 1 as test"))
|
|
208
|
+
value = result.scalar_one()
|
|
209
|
+
assert value == 1
|
|
210
|
+
|
|
211
|
+
await db.engine.dispose()
|
|
212
|
+
|
|
213
|
+
except (OSError, IntegrityError) as e:
|
|
214
|
+
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@pytest.mark.asyncio
|
|
218
|
+
@pytest.mark.integration
|
|
219
|
+
async def test_postgres_session_creation() -> None:
|
|
220
|
+
"""Test that PostgreSQL session factory works correctly."""
|
|
221
|
+
if not ASYNC_PG_AVAILABLE:
|
|
222
|
+
pytest.skip("asyncpg not installed")
|
|
223
|
+
|
|
224
|
+
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
225
|
+
if not test_url:
|
|
226
|
+
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
parsed = urlparse(test_url)
|
|
230
|
+
db = DatabaseSettings(
|
|
231
|
+
dialect={
|
|
232
|
+
"type": "postgres",
|
|
233
|
+
"host": parsed.hostname or "localhost",
|
|
234
|
+
"port": parsed.port or 5432,
|
|
235
|
+
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
236
|
+
"username": parsed.username or "postgres",
|
|
237
|
+
"password": parsed.password or "",
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Test session creation
|
|
242
|
+
async with db.session_maker() as session:
|
|
243
|
+
assert isinstance(session, AsyncSession)
|
|
244
|
+
result = await session.execute(text("SELECT version()"))
|
|
245
|
+
version = result.scalar_one()
|
|
246
|
+
assert "PostgreSQL" in version
|
|
247
|
+
|
|
248
|
+
await db.engine.dispose()
|
|
249
|
+
|
|
250
|
+
except (OSError, IntegrityError) as e:
|
|
251
|
+
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
@pytest.mark.integration
|
|
256
|
+
async def test_postgres_dependency_yields_sessions() -> None:
|
|
257
|
+
"""Test that PostgreSQL dependency generator works correctly."""
|
|
258
|
+
if not ASYNC_PG_AVAILABLE:
|
|
259
|
+
pytest.skip("asyncpg not installed")
|
|
260
|
+
|
|
261
|
+
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
262
|
+
if not test_url:
|
|
263
|
+
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
parsed = urlparse(test_url)
|
|
267
|
+
db = DatabaseSettings(
|
|
268
|
+
dialect={
|
|
269
|
+
"type": "postgres",
|
|
270
|
+
"host": parsed.hostname or "localhost",
|
|
271
|
+
"port": parsed.port or 5432,
|
|
272
|
+
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
273
|
+
"username": parsed.username or "postgres",
|
|
274
|
+
"password": parsed.password or "",
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Test dependency yields working sessions
|
|
279
|
+
sessions = []
|
|
280
|
+
async for session in db.dependency():
|
|
281
|
+
sessions.append(session)
|
|
282
|
+
# Verify session works
|
|
283
|
+
result = await session.execute(text("SELECT 1"))
|
|
284
|
+
assert result.scalar_one() == 1
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
async for session in db.dependency():
|
|
288
|
+
sessions.append(session)
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
# Verify different session instances
|
|
292
|
+
assert len(sessions) == 2
|
|
293
|
+
assert sessions[0] is not sessions[1]
|
|
294
|
+
|
|
295
|
+
await db.engine.dispose()
|
|
296
|
+
|
|
297
|
+
except (OSError, IntegrityError) as e:
|
|
298
|
+
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@pytest.mark.asyncio
|
|
302
|
+
@pytest.mark.integration
|
|
303
|
+
async def test_postgres_connection_pooling() -> None:
|
|
304
|
+
"""Test that PostgreSQL connection pooling is configured correctly."""
|
|
305
|
+
if not ASYNC_PG_AVAILABLE:
|
|
306
|
+
pytest.skip("asyncpg not installed")
|
|
307
|
+
|
|
308
|
+
test_url = os.getenv("POSTGRES_TEST_URL")
|
|
309
|
+
if not test_url:
|
|
310
|
+
pytest.skip("POSTGRES_TEST_URL not set - skipping integration test")
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
parsed = urlparse(test_url)
|
|
314
|
+
db = DatabaseSettings(
|
|
315
|
+
dialect={
|
|
316
|
+
"type": "postgres",
|
|
317
|
+
"host": parsed.hostname or "localhost",
|
|
318
|
+
"port": parsed.port or 5432,
|
|
319
|
+
"database": parsed.path.lstrip("/") if parsed.path else "postgres",
|
|
320
|
+
"username": parsed.username or "postgres",
|
|
321
|
+
"password": parsed.password or "",
|
|
322
|
+
"pool_size": 5,
|
|
323
|
+
"max_overflow": 10,
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Verify pool settings
|
|
328
|
+
assert db.dialect.pool_size == 5
|
|
329
|
+
assert db.dialect.max_overflow == 10
|
|
330
|
+
|
|
331
|
+
# Create multiple sessions to test pooling
|
|
332
|
+
sessions = []
|
|
333
|
+
for _ in range(3):
|
|
334
|
+
async with db.session_maker() as session:
|
|
335
|
+
sessions.append(session)
|
|
336
|
+
result = await session.execute(text("SELECT 1"))
|
|
337
|
+
assert result.scalar_one() == 1
|
|
338
|
+
|
|
339
|
+
await db.engine.dispose()
|
|
340
|
+
|
|
341
|
+
except (OSError, IntegrityError) as e:
|
|
342
|
+
pytest.skip(f"Could not connect to PostgreSQL: {e}")
|