belgie-alchemy 0.1.0a4__py3-none-any.whl → 0.2.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.
@@ -1,39 +0,0 @@
1
- """Pytest fixtures for alchemy tests."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- import pytest_asyncio
8
-
9
- from belgie_alchemy.__tests__.fixtures.database import get_test_engine, get_test_session_factory
10
-
11
- if TYPE_CHECKING:
12
- from collections.abc import AsyncGenerator
13
-
14
- from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
15
-
16
-
17
- @pytest_asyncio.fixture
18
- async def alchemy_engine() -> AsyncGenerator[AsyncEngine, None]:
19
- """Create an isolated in-memory SQLite engine for testing.
20
-
21
- Each test gets its own in-memory database, so Base.metadata.create_all
22
- is safe even when tests run in parallel - there's no shared state.
23
- """
24
- engine = await get_test_engine()
25
- yield engine
26
- await engine.dispose()
27
-
28
-
29
- @pytest_asyncio.fixture
30
- async def alchemy_session_factory(alchemy_engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
31
- return await get_test_session_factory(alchemy_engine)
32
-
33
-
34
- @pytest_asyncio.fixture
35
- async def alchemy_session(
36
- alchemy_session_factory: async_sessionmaker[AsyncSession],
37
- ) -> AsyncGenerator[AsyncSession, None]:
38
- async with alchemy_session_factory() as session:
39
- yield session
@@ -1,12 +0,0 @@
1
- from belgie_alchemy.__tests__.fixtures.database import get_test_db, get_test_engine, get_test_session_factory
2
- from belgie_alchemy.__tests__.fixtures.models import Account, OAuthState, Session, User
3
-
4
- __all__ = [
5
- "Account",
6
- "OAuthState",
7
- "Session",
8
- "User",
9
- "get_test_db",
10
- "get_test_engine",
11
- "get_test_session_factory",
12
- ]
@@ -1,38 +0,0 @@
1
- from collections.abc import AsyncGenerator
2
-
3
- from sqlalchemy import event
4
- from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
5
-
6
- from belgie_alchemy import Base
7
-
8
- TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
9
-
10
-
11
- async def get_test_engine() -> AsyncEngine:
12
- engine = create_async_engine(
13
- TEST_DATABASE_URL,
14
- echo=False,
15
- )
16
-
17
- @event.listens_for(engine.sync_engine, "connect")
18
- def _enable_foreign_keys(dbapi_conn, _connection_record) -> None:
19
- cursor = dbapi_conn.cursor()
20
- cursor.execute("PRAGMA foreign_keys=ON")
21
- cursor.close()
22
-
23
- async with engine.begin() as conn:
24
- await conn.run_sync(Base.metadata.create_all)
25
- return engine
26
-
27
-
28
- async def get_test_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
29
- return async_sessionmaker(
30
- engine,
31
- class_=AsyncSession,
32
- expire_on_commit=False,
33
- )
34
-
35
-
36
- async def get_test_db(session_factory: async_sessionmaker[AsyncSession]) -> AsyncGenerator[AsyncSession, None]:
37
- async with session_factory() as session:
38
- yield session
@@ -1,119 +0,0 @@
1
- """Test models for alchemy tests.
2
-
3
- Defines concrete auth models for testing. These mirror the examples in
4
- examples/alchemy/auth_models.py and demonstrate how users would define
5
- their own models.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from datetime import datetime # noqa: TC003
11
- from uuid import UUID # noqa: TC003
12
-
13
- from sqlalchemy import JSON, ForeignKey, Text, UniqueConstraint
14
- from sqlalchemy.orm import Mapped, mapped_column, relationship
15
-
16
- from belgie_alchemy import Base, DateTimeUTC, PrimaryKeyMixin, TimestampMixin
17
-
18
-
19
- class User(Base, PrimaryKeyMixin, TimestampMixin):
20
- """Test User model."""
21
-
22
- __tablename__ = "users"
23
-
24
- email: Mapped[str] = mapped_column(unique=True, index=True)
25
- email_verified: Mapped[bool] = mapped_column(default=False)
26
- name: Mapped[str | None] = mapped_column(default=None)
27
- image: Mapped[str | None] = mapped_column(default=None)
28
- scopes: Mapped[list[str] | None] = mapped_column(JSON, default=None)
29
- custom_field: Mapped[str | None] = mapped_column(default=None)
30
-
31
- accounts: Mapped[list[Account]] = relationship(
32
- back_populates="user",
33
- cascade="all, delete-orphan",
34
- init=False,
35
- )
36
- sessions: Mapped[list[Session]] = relationship(
37
- back_populates="user",
38
- cascade="all, delete-orphan",
39
- init=False,
40
- )
41
- oauth_states: Mapped[list[OAuthState]] = relationship(
42
- back_populates="user",
43
- cascade="all, delete-orphan",
44
- init=False,
45
- )
46
-
47
-
48
- class Account(Base, PrimaryKeyMixin, TimestampMixin):
49
- """Test Account model."""
50
-
51
- __tablename__ = "accounts"
52
-
53
- user_id: Mapped[UUID] = mapped_column(
54
- ForeignKey("users.id", ondelete="cascade", onupdate="cascade"),
55
- nullable=False,
56
- )
57
- provider: Mapped[str] = mapped_column(Text)
58
- provider_account_id: Mapped[str] = mapped_column(Text)
59
- access_token: Mapped[str | None] = mapped_column(default=None)
60
- refresh_token: Mapped[str | None] = mapped_column(default=None)
61
- expires_at: Mapped[datetime | None] = mapped_column(DateTimeUTC, default=None)
62
- token_type: Mapped[str | None] = mapped_column(default=None)
63
- scope: Mapped[str | None] = mapped_column(default=None)
64
- id_token: Mapped[str | None] = mapped_column(default=None)
65
-
66
- user: Mapped[User] = relationship(
67
- back_populates="accounts",
68
- lazy="selectin",
69
- init=False,
70
- )
71
-
72
- __table_args__ = (
73
- UniqueConstraint(
74
- "provider",
75
- "provider_account_id",
76
- name="uq_accounts_provider_provider_account_id",
77
- ),
78
- )
79
-
80
-
81
- class Session(Base, PrimaryKeyMixin, TimestampMixin):
82
- """Test Session model."""
83
-
84
- __tablename__ = "sessions"
85
-
86
- user_id: Mapped[UUID] = mapped_column(
87
- ForeignKey("users.id", ondelete="cascade", onupdate="cascade"),
88
- nullable=False,
89
- )
90
- expires_at: Mapped[datetime] = mapped_column(DateTimeUTC)
91
- ip_address: Mapped[str | None] = mapped_column(default=None)
92
- user_agent: Mapped[str | None] = mapped_column(default=None)
93
-
94
- user: Mapped[User] = relationship(
95
- back_populates="sessions",
96
- lazy="selectin",
97
- init=False,
98
- )
99
-
100
-
101
- class OAuthState(Base, PrimaryKeyMixin, TimestampMixin):
102
- """Test OAuthState model."""
103
-
104
- __tablename__ = "oauth_states"
105
-
106
- state: Mapped[str] = mapped_column(unique=True, index=True)
107
- user_id: Mapped[UUID | None] = mapped_column(
108
- ForeignKey("users.id", ondelete="set null", onupdate="cascade"),
109
- nullable=True,
110
- )
111
- expires_at: Mapped[datetime] = mapped_column(DateTimeUTC)
112
- code_verifier: Mapped[str | None] = mapped_column(default=None)
113
- redirect_url: Mapped[str | None] = mapped_column(default=None)
114
-
115
- user: Mapped[User] | None = relationship(
116
- back_populates="oauth_states",
117
- lazy="selectin",
118
- init=False,
119
- )
File without changes
@@ -1,80 +0,0 @@
1
- from uuid import UUID
2
-
3
- import pytest
4
- from sqlalchemy.exc import IntegrityError
5
- from sqlalchemy.ext.asyncio import AsyncSession
6
-
7
- from belgie_alchemy.__tests__.fixtures.models import User
8
-
9
-
10
- def test_primary_key_mixin_defaults() -> None:
11
- id_column = User.__table__.c.id # type: ignore[attr-defined]
12
- assert id_column.primary_key
13
- assert str(id_column.server_default.arg) == "gen_random_uuid()"
14
- assert id_column.index
15
-
16
-
17
- def test_primary_key_client_side_generation() -> None:
18
- """Test that UUID is generated client-side via default_factory."""
19
- user1 = User(email="user1@example.com")
20
- user2 = User(email="user2@example.com")
21
-
22
- # UUIDs should be generated automatically
23
- assert isinstance(user1.id, UUID)
24
- assert isinstance(user2.id, UUID)
25
-
26
- # UUIDs should be unique
27
- assert user1.id != user2.id
28
-
29
-
30
- @pytest.mark.asyncio
31
- async def test_primary_key_persists_client_generated_uuid(alchemy_session: AsyncSession) -> None:
32
- """Test that client-generated UUIDs are persisted correctly."""
33
- user = User(email="persist@example.com")
34
- original_id = user.id
35
-
36
- alchemy_session.add(user)
37
- await alchemy_session.commit()
38
-
39
- # Refresh from database
40
- await alchemy_session.refresh(user)
41
-
42
- # UUID should be unchanged
43
- assert user.id == original_id
44
- assert isinstance(user.id, UUID)
45
-
46
-
47
- @pytest.mark.asyncio
48
- async def test_primary_key_unique_constraint(alchemy_session: AsyncSession) -> None:
49
- """Test that duplicate UUIDs are rejected by unique constraint."""
50
- user1 = User(email="user1@example.com")
51
- alchemy_session.add(user1)
52
- await alchemy_session.commit()
53
-
54
- # Save the ID and expunge user1 to avoid identity conflicts
55
- user1_id = user1.id
56
- alchemy_session.expunge(user1)
57
-
58
- # Try to create another user with the same ID
59
- user2 = User(email="user2@example.com")
60
- user2.id = user1_id # Force same ID
61
-
62
- alchemy_session.add(user2)
63
- with pytest.raises(IntegrityError):
64
- await alchemy_session.commit()
65
-
66
- await alchemy_session.rollback()
67
-
68
-
69
- def test_timestamp_mixin_defaults() -> None:
70
- user = User(email="defaults@example.com")
71
- assert user.created_at is not None
72
- assert user.updated_at is not None
73
- assert user.deleted_at is None
74
-
75
-
76
- def test_mark_deleted_sets_timestamp() -> None:
77
- user = User(email="x@example.com")
78
- assert user.deleted_at is None
79
- user.mark_deleted()
80
- assert user.deleted_at is not None
File without changes
@@ -1,342 +0,0 @@
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}")