metaorm 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.
metaorm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: metaorm
3
+ Version: 0.1.0
4
+ Summary: Async repository layer over SQLModel
5
+ Author-email: Author Name <author@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/username/metaorm
8
+ Project-URL: Repository, https://github.com/username/metaorm
9
+ Project-URL: Issues, https://github.com/username/metaorm/issues
10
+ Keywords: async,database,orm,repository,sqlmodel
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: pydantic-filters>=0.3.5
24
+ Requires-Dist: sqlalchemy>=2.0
25
+ Requires-Dist: sqlmodel>=0.0.22
File without changes
@@ -0,0 +1,26 @@
1
+ from .container import RepositoriesContainer
2
+ from .exceptions import (
3
+ AlreadyExistsError,
4
+ DatabaseException,
5
+ HaveNoSessionError,
6
+ NotFoundError,
7
+ )
8
+ from .repositories import BaseRepository
9
+ from .settings import DatabaseSettings
10
+ from .tables import BaseTable
11
+
12
+ __all__ = (
13
+ # container
14
+ "RepositoriesContainer",
15
+ # exceptions
16
+ "AlreadyExistsError",
17
+ "DatabaseException",
18
+ "HaveNoSessionError",
19
+ "NotFoundError",
20
+ # repositories
21
+ "BaseRepository",
22
+ # settings
23
+ "DatabaseSettings",
24
+ # tables
25
+ "BaseTable",
26
+ )
@@ -0,0 +1,60 @@
1
+ from collections.abc import AsyncGenerator
2
+ from contextlib import asynccontextmanager
3
+ from typing import TypeVar
4
+
5
+ import contextvars
6
+
7
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
8
+ from sqlmodel.ext.asyncio.session import AsyncSession
9
+
10
+ from .settings import DatabaseSettings
11
+
12
+
13
+ RepositoryType = TypeVar("RepositoryType", bound="BaseRepository")
14
+
15
+
16
+ class RepositoriesContainer:
17
+ def __init__(self, settings: DatabaseSettings):
18
+ engine_parameters = {
19
+ "url": settings.dsn,
20
+ "pool_recycle": settings.pool_recycle,
21
+ }
22
+ if not settings.dsn.startswith("sqlite"):
23
+ engine_parameters["pool_timeout"] = settings.pool_timeout
24
+ engine_parameters["pool_size"] = settings.pool_size
25
+
26
+ self._engine = create_async_engine(**engine_parameters)
27
+ self._session_context = contextvars.ContextVar(
28
+ "session_context",
29
+ default=None,
30
+ )
31
+
32
+ @property
33
+ def engine(self) -> AsyncEngine:
34
+ return self._engine
35
+
36
+ @property
37
+ def session(self) -> AsyncSession | None:
38
+ return self._session_context.get()
39
+
40
+ @asynccontextmanager
41
+ async def transaction(self) -> AsyncGenerator[AsyncSession, None]:
42
+ existing_session = self._session_context.get()
43
+ if existing_session is not None:
44
+ yield existing_session
45
+ return
46
+
47
+ session_parameters = {
48
+ "bind": self._engine,
49
+ "expire_on_commit": False,
50
+ }
51
+ async with AsyncSession(**session_parameters) as session:
52
+ token = self._session_context.set(session)
53
+ try:
54
+ async with session.begin():
55
+ yield session
56
+ finally:
57
+ self._session_context.reset(token)
58
+
59
+ def get_repository(self, repository_class: type[RepositoryType]) -> RepositoryType:
60
+ return repository_class(container=self)
@@ -0,0 +1,17 @@
1
+ class DatabaseException(Exception):
2
+ pass
3
+
4
+
5
+ class NotFoundError(DatabaseException):
6
+ def __init__(self, detail: str = "Not found"):
7
+ super().__init__(detail)
8
+
9
+
10
+ class HaveNoSessionError(DatabaseException):
11
+ def __init__(self):
12
+ super().__init__("Have no actual session")
13
+
14
+
15
+ class AlreadyExistsError(DatabaseException):
16
+ def __init__(self, detail: str = "Record already exists"):
17
+ super().__init__(detail)
@@ -0,0 +1,150 @@
1
+ from collections.abc import AsyncGenerator, Sequence
2
+ from contextlib import asynccontextmanager
3
+ from typing import Any, Generic, TypeVar
4
+
5
+ from pydantic_filters import BasePagination, BaseSort
6
+ from pydantic_filters.drivers.sqlalchemy import append_to_statement
7
+ from sqlalchemy import func
8
+ from sqlalchemy.exc import IntegrityError
9
+ from sqlmodel import delete, insert, select, update
10
+ from sqlmodel.ext.asyncio.session import AsyncSession
11
+
12
+ from .container import RepositoriesContainer
13
+ from .exceptions import AlreadyExistsError, DatabaseException, HaveNoSessionError
14
+ from .tables import BaseTable
15
+
16
+
17
+ DTOType = TypeVar("DTOType")
18
+ FilterType = TypeVar("FilterType")
19
+
20
+
21
+ class BaseRepository(Generic[DTOType, FilterType]):
22
+ def __init__(self, container: RepositoriesContainer):
23
+ self._container = container
24
+
25
+ @property
26
+ def session(self) -> AsyncSession:
27
+ session = self._container.session
28
+ if session is None:
29
+ raise HaveNoSessionError()
30
+ return session
31
+
32
+ @asynccontextmanager
33
+ async def transaction(self) -> AsyncGenerator[None, None]:
34
+ existing_session = self._container.session
35
+ if existing_session is not None:
36
+ yield
37
+ return
38
+
39
+ async with self._container.transaction():
40
+ yield
41
+
42
+ def get_db_table(self) -> type[BaseTable]:
43
+ raise NotImplementedError
44
+
45
+ async def create_tables(self) -> None:
46
+ table = self.get_db_table()
47
+ async with self._container.engine.begin() as connection:
48
+ await connection.run_sync(
49
+ table.metadata.create_all,
50
+ tables=[table.__table__],
51
+ )
52
+
53
+ async def get_items_count(
54
+ self,
55
+ filter_: FilterType | None = None,
56
+ ) -> int:
57
+ table = self.get_db_table()
58
+ statement = select(func.count()).select_from(table)
59
+ statement = append_to_statement(
60
+ statement=statement,
61
+ model=table,
62
+ filter_=filter_,
63
+ )
64
+
65
+ async with self.transaction():
66
+ result = await self.session.exec(statement)
67
+ items_count = result.first()
68
+
69
+ return items_count
70
+
71
+ async def get_items(
72
+ self,
73
+ filter_: FilterType | None = None,
74
+ pagination: BasePagination | None = None,
75
+ sort: BaseSort | None = None,
76
+ options: Sequence[Any] | None = None,
77
+ ) -> AsyncGenerator[DTOType]:
78
+ table = self.get_db_table()
79
+ statement = select(table)
80
+ statement = append_to_statement(
81
+ statement=statement,
82
+ model=table,
83
+ filter_=filter_,
84
+ pagination=pagination,
85
+ sort=sort,
86
+ )
87
+ if options:
88
+ statement = statement.options(*options)
89
+
90
+ async with self.transaction():
91
+ result = await self.session.execute(statement)
92
+ result = result.yield_per(100)
93
+ for db_item in result.scalars():
94
+ yield db_item.to_item()
95
+
96
+ async def create_item(self, item: DTOType) -> DTOType:
97
+ table = self.get_db_table()
98
+ values = table.from_item(item=item).to_values()
99
+ statement = insert(table).values(values).returning(table)
100
+
101
+ async with self.transaction():
102
+ try:
103
+ result = await self.session.exec(statement)
104
+ except IntegrityError as error:
105
+ error_message = str(error.orig).lower()
106
+ if "unique" in error_message or "duplicate" in error_message:
107
+ raise AlreadyExistsError(
108
+ f"Record for model '{table.__name__}' already exists",
109
+ ) from error
110
+ raise DatabaseException(
111
+ f"Integrity error for model '{table.__name__}'",
112
+ ) from error
113
+ db_item = result.scalar_one()
114
+
115
+ return db_item.to_item()
116
+
117
+ async def update_items(
118
+ self,
119
+ filter_: FilterType | None = None,
120
+ **values,
121
+ ) -> AsyncGenerator[DTOType]:
122
+ table = self.get_db_table()
123
+ statement = update(table)
124
+ statement = append_to_statement(
125
+ statement=statement,
126
+ model=table,
127
+ filter_=filter_,
128
+ )
129
+ statement = statement.values(**values).returning(table)
130
+
131
+ async with self.transaction():
132
+ result = await self.session.exec(statement)
133
+ result = result.yield_per(100)
134
+ for db_item in result.scalars():
135
+ yield db_item.to_item()
136
+
137
+ async def delete_items(
138
+ self,
139
+ filter_: FilterType | None = None,
140
+ ) -> None:
141
+ table = self.get_db_table()
142
+ statement = delete(table)
143
+ statement = append_to_statement(
144
+ statement=statement,
145
+ model=table,
146
+ filter_=filter_,
147
+ )
148
+
149
+ async with self.transaction():
150
+ await self.session.exec(statement)
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class DatabaseSettings(BaseModel):
5
+ dsn: str = Field(default="sqlite+aiosqlite:///db.sqlite3", pattern=r"^.+://")
6
+ pool_size: int = Field(default=5, ge=1)
7
+ pool_recycle: int = Field(default=60, ge=1) # in seconds: 1 minute
8
+ pool_timeout: int = Field(default=60, ge=1) # in seconds: 1 minute
@@ -0,0 +1,21 @@
1
+ from typing import Any, Generic, Self, TypeVar
2
+
3
+ from pydantic import BaseModel
4
+ from sqlmodel import SQLModel
5
+
6
+
7
+ ItemType = TypeVar("ItemType", bound=BaseModel)
8
+
9
+
10
+ class BaseTable(SQLModel, Generic[ItemType]):
11
+ @classmethod
12
+ def from_item(cls, item: ItemType) -> Self:
13
+ raise NotImplementedError
14
+
15
+ def to_item(self) -> ItemType:
16
+ raise NotImplementedError
17
+
18
+ def to_values(self) -> dict[str, Any]:
19
+ return {
20
+ column.name: getattr(self, column.name) for column in self.__table__.columns
21
+ }
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: metaorm
3
+ Version: 0.1.0
4
+ Summary: Async repository layer over SQLModel
5
+ Author-email: Author Name <author@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/username/metaorm
8
+ Project-URL: Repository, https://github.com/username/metaorm
9
+ Project-URL: Issues, https://github.com/username/metaorm/issues
10
+ Keywords: async,database,orm,repository,sqlmodel
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: pydantic-filters>=0.3.5
24
+ Requires-Dist: sqlalchemy>=2.0
25
+ Requires-Dist: sqlmodel>=0.0.22
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ metaorm/__init__.py
4
+ metaorm/container.py
5
+ metaorm/exceptions.py
6
+ metaorm/repositories.py
7
+ metaorm/settings.py
8
+ metaorm/tables.py
9
+ metaorm.egg-info/PKG-INFO
10
+ metaorm.egg-info/SOURCES.txt
11
+ metaorm.egg-info/dependency_links.txt
12
+ metaorm.egg-info/requires.txt
13
+ metaorm.egg-info/top_level.txt
14
+ tests/test_container.py
15
+ tests/test_exceptions.py
16
+ tests/test_repositories.py
17
+ tests/test_settings.py
18
+ tests/test_tables.py
@@ -0,0 +1,4 @@
1
+ pydantic>=2.0
2
+ pydantic-filters>=0.3.5
3
+ sqlalchemy>=2.0
4
+ sqlmodel>=0.0.22
@@ -0,0 +1 @@
1
+ metaorm
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "metaorm"
7
+ version = "0.1.0"
8
+ description = "Async repository layer over SQLModel"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ {name = "Author Name", email = "author@example.com"},
14
+ ]
15
+ keywords = [
16
+ "async",
17
+ "database",
18
+ "orm",
19
+ "repository",
20
+ "sqlmodel",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Database",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ ]
33
+ dependencies = [
34
+ "pydantic>=2.0",
35
+ "pydantic-filters>=0.3.5",
36
+ "sqlalchemy>=2.0",
37
+ "sqlmodel>=0.0.22",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/username/metaorm"
42
+ Repository = "https://github.com/username/metaorm"
43
+ Issues = "https://github.com/username/metaorm/issues"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "aiosqlite>=0.22.0",
48
+ "pytest>=9.1.1",
49
+ "pytest-asyncio>=1.4.0",
50
+ "pytest-cov>=7.1.0",
51
+ "ruff>=0.16.2",
52
+ ]
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["."]
56
+ include = ["metaorm*"]
57
+
58
+ [tool.pytest.ini_options]
59
+ asyncio_mode = "auto"
60
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,47 @@
1
+ import pytest
2
+ from sqlalchemy.ext.asyncio import AsyncEngine
3
+
4
+ from metaorm import HaveNoSessionError, RepositoriesContainer
5
+ from tests.models import UserRepository
6
+
7
+
8
+ class TestRepositoriesContainer:
9
+ async def test_engine_property_returns_async_engine(
10
+ self,
11
+ repositories_container: RepositoriesContainer,
12
+ ) -> None:
13
+ assert isinstance(repositories_container.engine, AsyncEngine)
14
+
15
+ async def test_session_is_none_without_transaction(
16
+ self,
17
+ repositories_container: RepositoriesContainer,
18
+ ) -> None:
19
+ assert repositories_container.session is None
20
+
21
+ async def test_transaction_creates_session(
22
+ self,
23
+ repositories_container: RepositoriesContainer,
24
+ ) -> None:
25
+ assert repositories_container.session is None
26
+
27
+ async with repositories_container.transaction() as session:
28
+ assert session is not None
29
+ assert repositories_container.session is session
30
+
31
+ assert repositories_container.session is None
32
+
33
+ async def test_nested_transaction_yields_same_session(
34
+ self,
35
+ repositories_container: RepositoriesContainer,
36
+ ) -> None:
37
+ async with repositories_container.transaction() as outer_session:
38
+ async with repositories_container.transaction() as inner_session:
39
+ assert inner_session is outer_session
40
+
41
+ async def test_get_repository_returns_repository_instance(
42
+ self,
43
+ repositories_container: RepositoriesContainer,
44
+ ) -> None:
45
+ repository = repositories_container.get_repository(UserRepository)
46
+
47
+ assert isinstance(repository, UserRepository)
@@ -0,0 +1,56 @@
1
+ import pytest
2
+
3
+ from metaorm import (
4
+ AlreadyExistsError,
5
+ DatabaseException,
6
+ HaveNoSessionError,
7
+ NotFoundError,
8
+ )
9
+
10
+
11
+ class TestExceptions:
12
+ def test_database_exception_is_base_class(self) -> None:
13
+ assert issubclass(NotFoundError, DatabaseException)
14
+ assert issubclass(HaveNoSessionError, DatabaseException)
15
+ assert issubclass(AlreadyExistsError, DatabaseException)
16
+
17
+ def test_not_found_error_default_message(self) -> None:
18
+ error = NotFoundError()
19
+
20
+ assert str(error) == "Not found"
21
+
22
+ def test_not_found_error_custom_message(self) -> None:
23
+ error = NotFoundError(detail="User not found")
24
+
25
+ assert str(error) == "User not found"
26
+
27
+ def test_have_no_session_error_message(self) -> None:
28
+ error = HaveNoSessionError()
29
+
30
+ assert str(error) == "Have no actual session"
31
+
32
+ def test_already_exists_error_default_message(self) -> None:
33
+ error = AlreadyExistsError()
34
+
35
+ assert str(error) == "Record already exists"
36
+
37
+ def test_already_exists_error_custom_message(self) -> None:
38
+ error = AlreadyExistsError(detail="User already exists")
39
+
40
+ assert str(error) == "User already exists"
41
+
42
+ @pytest.mark.parametrize(
43
+ "exception_class",
44
+ [
45
+ DatabaseException,
46
+ NotFoundError,
47
+ HaveNoSessionError,
48
+ AlreadyExistsError,
49
+ ],
50
+ )
51
+ def test_all_exceptions_are_catchable_as_database_exception(
52
+ self,
53
+ exception_class: type[Exception],
54
+ ) -> None:
55
+ with pytest.raises(DatabaseException):
56
+ raise exception_class()
@@ -0,0 +1,129 @@
1
+ import pytest
2
+ from pydantic_filters import BaseSort, OffsetPagination
3
+
4
+ from metaorm import AlreadyExistsError, HaveNoSessionError
5
+ from tests.models import User, UserRepository
6
+
7
+
8
+ class TestBaseRepository:
9
+ async def test_session_raises_error_without_transaction(
10
+ self,
11
+ user_repository: UserRepository,
12
+ ) -> None:
13
+ with pytest.raises(HaveNoSessionError):
14
+ _ = user_repository.session
15
+
16
+ async def test_create_item(self, user_repository: UserRepository) -> None:
17
+ user = User(name="Alice", email="alice@example.com")
18
+
19
+ created = await user_repository.create_item(user)
20
+
21
+ assert created.id is not None
22
+ assert created.name == "Alice"
23
+ assert created.email == "alice@example.com"
24
+
25
+ async def test_get_items(self, user_repository: UserRepository) -> None:
26
+ user1 = User(name="Alice", email="alice@example.com")
27
+ user2 = User(name="Bob", email="bob@example.com")
28
+ await user_repository.create_item(user1)
29
+ await user_repository.create_item(user2)
30
+
31
+ items = [item async for item in user_repository.get_items()]
32
+
33
+ assert len(items) == 2
34
+ assert {item.name for item in items} == {"Alice", "Bob"}
35
+
36
+ async def test_get_items_count(self, user_repository: UserRepository) -> None:
37
+ await user_repository.create_item(
38
+ User(name="Alice", email="alice@example.com"),
39
+ )
40
+ await user_repository.create_item(
41
+ User(name="Bob", email="bob@example.com"),
42
+ )
43
+
44
+ count = await user_repository.get_items_count()
45
+
46
+ assert count == 2
47
+
48
+ async def test_update_items(self, user_repository: UserRepository) -> None:
49
+ await user_repository.create_item(
50
+ User(name="Alice", email="alice@example.com"),
51
+ )
52
+ await user_repository.create_item(
53
+ User(name="Bob", email="bob@example.com"),
54
+ )
55
+
56
+ updated = [
57
+ item
58
+ async for item in user_repository.update_items(name="Updated")
59
+ ]
60
+
61
+ assert len(updated) == 2
62
+ assert all(item.name == "Updated" for item in updated)
63
+
64
+ async def test_delete_items(self, user_repository: UserRepository) -> None:
65
+ await user_repository.create_item(
66
+ User(name="Alice", email="alice@example.com"),
67
+ )
68
+ await user_repository.create_item(
69
+ User(name="Bob", email="bob@example.com"),
70
+ )
71
+
72
+ await user_repository.delete_items()
73
+
74
+ count = await user_repository.get_items_count()
75
+
76
+ assert count == 0
77
+
78
+ async def test_create_item_raises_already_exists_on_duplicate(
79
+ self,
80
+ user_repository: UserRepository,
81
+ ) -> None:
82
+ user = User(name="Alice", email="alice@example.com")
83
+ await user_repository.create_item(user)
84
+
85
+ with pytest.raises(AlreadyExistsError):
86
+ await user_repository.create_item(user)
87
+
88
+ async def test_transaction_reuses_existing_session(
89
+ self,
90
+ user_repository: UserRepository,
91
+ ) -> None:
92
+ async with user_repository.transaction():
93
+ _ = user_repository.session
94
+
95
+ async def test_get_items_with_pagination(
96
+ self,
97
+ user_repository: UserRepository,
98
+ ) -> None:
99
+ for index in range(5):
100
+ await user_repository.create_item(
101
+ User(name=f"User{index}", email=f"user{index}@example.com"),
102
+ )
103
+
104
+ pagination = OffsetPagination(offset=1, limit=2)
105
+ items = [
106
+ item
107
+ async for item in user_repository.get_items(pagination=pagination)
108
+ ]
109
+
110
+ assert len(items) == 2
111
+
112
+ async def test_get_items_with_sort(
113
+ self,
114
+ user_repository: UserRepository,
115
+ ) -> None:
116
+ await user_repository.create_item(
117
+ User(name="Charlie", email="c@example.com"),
118
+ )
119
+ await user_repository.create_item(
120
+ User(name="Alice", email="a@example.com"),
121
+ )
122
+ await user_repository.create_item(
123
+ User(name="Bob", email="b@example.com"),
124
+ )
125
+
126
+ sort = BaseSort(sort_by="name", sort_by_order="asc")
127
+ items = [item async for item in user_repository.get_items(sort=sort)]
128
+
129
+ assert [item.name for item in items] == ["Alice", "Bob", "Charlie"]
@@ -0,0 +1,47 @@
1
+ import pytest
2
+ from pydantic import ValidationError
3
+
4
+ from metaorm import DatabaseSettings
5
+
6
+
7
+ class TestDatabaseSettings:
8
+ def test_default_values(self) -> None:
9
+ settings = DatabaseSettings()
10
+
11
+ assert settings.dsn == "sqlite+aiosqlite:///db.sqlite3"
12
+ assert settings.pool_size == 5
13
+ assert settings.pool_recycle == 60
14
+ assert settings.pool_timeout == 60
15
+
16
+ def test_custom_values(self) -> None:
17
+ settings = DatabaseSettings(
18
+ dsn="postgresql+asyncpg://user:pass@localhost/db",
19
+ pool_size=10,
20
+ pool_recycle=120,
21
+ pool_timeout=30,
22
+ )
23
+
24
+ assert settings.dsn == "postgresql+asyncpg://user:pass@localhost/db"
25
+ assert settings.pool_size == 10
26
+ assert settings.pool_recycle == 120
27
+ assert settings.pool_timeout == 30
28
+
29
+ def test_dsn_must_match_pattern(self) -> None:
30
+ with pytest.raises(ValidationError):
31
+ DatabaseSettings(dsn="invalid_dsn")
32
+
33
+ @pytest.mark.parametrize(
34
+ "field_name,invalid_value",
35
+ [
36
+ ("pool_size", 0),
37
+ ("pool_recycle", 0),
38
+ ("pool_timeout", 0),
39
+ ],
40
+ )
41
+ def test_integer_fields_must_be_greater_or_equal_one(
42
+ self,
43
+ field_name: str,
44
+ invalid_value: int,
45
+ ) -> None:
46
+ with pytest.raises(ValidationError):
47
+ DatabaseSettings(**{field_name: invalid_value})
@@ -0,0 +1,42 @@
1
+ from typing import Optional
2
+
3
+ import pytest
4
+ from sqlmodel import Field, SQLModel
5
+
6
+ from metaorm import BaseTable
7
+ from tests.models import User
8
+
9
+
10
+ class TestBaseTable:
11
+ def test_to_values_returns_column_data(self) -> None:
12
+ from tests.models import UserTable
13
+
14
+ user_table = UserTable(id=1, name="Alice", email="alice@example.com")
15
+
16
+ values = user_table.to_values()
17
+
18
+ assert values == {
19
+ "id": 1,
20
+ "name": "Alice",
21
+ "email": "alice@example.com",
22
+ }
23
+
24
+ def test_from_item_not_implemented_in_base_class(self) -> None:
25
+ class DummyFromItemTable(BaseTable[User], table=True):
26
+ __tablename__ = "dummy_from_item"
27
+ id: Optional[int] = Field(default=None, primary_key=True)
28
+
29
+ with pytest.raises(NotImplementedError):
30
+ DummyFromItemTable.from_item(
31
+ User(name="Alice", email="alice@example.com"),
32
+ )
33
+
34
+ def test_to_item_not_implemented_in_base_class(self) -> None:
35
+ class DummyToItemTable(BaseTable[User], table=True):
36
+ __tablename__ = "dummy_to_item"
37
+ id: Optional[int] = Field(default=None, primary_key=True)
38
+
39
+ dummy = DummyToItemTable(id=1, name="Alice", email="alice@example.com")
40
+
41
+ with pytest.raises(NotImplementedError):
42
+ dummy.to_item()