wexample-orm 1.4.5__tar.gz → 2.0.1__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 (42) hide show
  1. wexample_orm-2.0.1/PKG-INFO +117 -0
  2. wexample_orm-2.0.1/README.md +96 -0
  3. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/pyproject.toml +3 -2
  4. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/const/__init__.py +0 -0
  5. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/const/generator.py +0 -0
  6. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/const/import_export.py +0 -0
  7. wexample_orm-2.0.1/src/wexample_orm/entity/abstract_entity.py +40 -0
  8. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/exception/import_reference_not_found_exception.py +0 -0
  9. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/exception/import_unknown_entity_exception.py +0 -0
  10. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/generator/__init__.py +0 -0
  11. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/generator/entity_generator.py +3 -2
  12. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/generator/entity_schema.py +0 -0
  13. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/generator/repository_generator.py +0 -0
  14. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/helper/__init__.py +0 -0
  15. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/helper/doctrine.py +2 -0
  16. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/import_export/__init__.py +0 -0
  17. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/import_export/entity_exporter.py +0 -0
  18. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/import_export/entity_importer.py +4 -2
  19. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/import_export/import_references_bag.py +0 -0
  20. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/import_export/normalized_entity_hydrator.py +0 -0
  21. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/py.typed +0 -0
  22. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/repository/abstract_repository.py +6 -1
  23. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/tests/.gitkeep +0 -0
  24. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/tests/test_abstract.py +19 -12
  25. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/tests/test_entity_generator.py +0 -0
  26. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/tests/test_entity_importer.py +9 -7
  27. wexample_orm-1.4.5/PKG-INFO +0 -347
  28. wexample_orm-1.4.5/README.md +0 -327
  29. wexample_orm-1.4.5/src/wexample_orm/entity/abstract_entity.py +0 -41
  30. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/__init__.py +0 -0
  31. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/common/__init__.py +0 -0
  32. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/common/abstract_repositories_manager.py +0 -0
  33. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/entity/__init__.py +0 -0
  34. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/exception/__init__.py +0 -0
  35. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/exception/repository_session_missing_exception.py +0 -0
  36. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/exception/unknown_repository_exception.py +0 -0
  37. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/repository/__init__.py +0 -0
  38. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/session/__init__.py +0 -0
  39. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/session/abstract_session_factory.py +0 -0
  40. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/testing/__init__.py +0 -0
  41. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/src/wexample_orm/testing/sqlite.py +0 -0
  42. {wexample_orm-1.4.5 → wexample_orm-2.0.1}/tests/__init__.py +0 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.1
2
+ Name: wexample-orm
3
+ Version: 2.0.1
4
+ Summary: Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg
5
+ Author-Email: weeger <contact@wexample.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Project-URL: homepage, https://github.com/wexample/python-orm
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: attrs>=23.1.0
13
+ Requires-Dist: psycopg[binary]>=3.2
14
+ Requires-Dist: sqlalchemy<3,>=2
15
+ Requires-Dist: uuid-utils>=0.11
16
+ Requires-Dist: wexample-helpers>=20.6.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-cov; extra == "dev"
20
+ Description-Content-Type: text/markdown
21
+
22
+ # orm
23
+
24
+ Version: 2.0.1
25
+
26
+ Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg
27
+
28
+ ## Table of Contents
29
+
30
+ - [Introduction](#introduction)
31
+ - [Install](#install)
32
+ - [Tests](#tests)
33
+ - [Roadmap](#roadmap)
34
+ - [Status Compatibility](#status-compatibility)
35
+ - [Useful Links](#useful-links)
36
+
37
+ Python Doctrine inspired ORM
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install wexample-orm
43
+ ```
44
+
45
+ Requires Python >=3.10.
46
+
47
+ ## Tests
48
+
49
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
50
+
51
+ ### Installation
52
+
53
+ First, install the required testing dependencies:
54
+ ```bash
55
+ .venv/bin/python -m pip install pytest pytest-cov
56
+ ```
57
+
58
+ ### Basic Usage
59
+
60
+ Run all tests with coverage:
61
+ ```bash
62
+ .venv/bin/python -m pytest --cov --cov-report=html
63
+ ```
64
+
65
+ ### Common Commands
66
+ ```bash
67
+ # Run tests with coverage for a specific module
68
+ .venv/bin/python -m pytest --cov=your_module
69
+
70
+ # Show which lines are not covered
71
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
72
+
73
+ # Generate an HTML coverage report
74
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
75
+
76
+ # Combine terminal and HTML reports
77
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
78
+
79
+ # Run specific test file with coverage
80
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
81
+ ```
82
+
83
+ ### Viewing HTML Reports
84
+
85
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
86
+
87
+ ### Coverage Threshold
88
+
89
+ To enforce a minimum coverage percentage:
90
+ ```bash
91
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
92
+ ```
93
+
94
+ This will cause the test suite to fail if coverage drops below 80%.
95
+
96
+ ## Known Limitations & Roadmap
97
+
98
+ Current limitations and planned features are tracked in the GitHub issues.
99
+
100
+ See the [project roadmap](https://github.com/wexample/python-orm/issues) for upcoming features and improvements.
101
+
102
+ ## Status & Compatibility
103
+
104
+ **Maturity**: Production-ready
105
+
106
+ **Python Support**: >=3.10
107
+
108
+ **OS Support**: Linux, macOS, Windows
109
+
110
+ **Status**: Actively maintained
111
+
112
+ ## Useful Links
113
+
114
+ - **Homepage**: https://github.com/wexample/python-orm
115
+ - **Issue Tracker**: https://github.com/wexample/python-orm/issues
116
+ - **Discussions**: https://github.com/wexample/python-orm/discussions
117
+ - **PyPI**: [pypi.org/project/wexample-orm](https://pypi.org/project/wexample-orm/)
@@ -0,0 +1,96 @@
1
+ # orm
2
+
3
+ Version: 2.0.1
4
+
5
+ Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg
6
+
7
+ ## Table of Contents
8
+
9
+ - [Introduction](#introduction)
10
+ - [Install](#install)
11
+ - [Tests](#tests)
12
+ - [Roadmap](#roadmap)
13
+ - [Status Compatibility](#status-compatibility)
14
+ - [Useful Links](#useful-links)
15
+
16
+ Python Doctrine inspired ORM
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install wexample-orm
22
+ ```
23
+
24
+ Requires Python >=3.10.
25
+
26
+ ## Tests
27
+
28
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
29
+
30
+ ### Installation
31
+
32
+ First, install the required testing dependencies:
33
+ ```bash
34
+ .venv/bin/python -m pip install pytest pytest-cov
35
+ ```
36
+
37
+ ### Basic Usage
38
+
39
+ Run all tests with coverage:
40
+ ```bash
41
+ .venv/bin/python -m pytest --cov --cov-report=html
42
+ ```
43
+
44
+ ### Common Commands
45
+ ```bash
46
+ # Run tests with coverage for a specific module
47
+ .venv/bin/python -m pytest --cov=your_module
48
+
49
+ # Show which lines are not covered
50
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
51
+
52
+ # Generate an HTML coverage report
53
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
54
+
55
+ # Combine terminal and HTML reports
56
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
57
+
58
+ # Run specific test file with coverage
59
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
60
+ ```
61
+
62
+ ### Viewing HTML Reports
63
+
64
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
65
+
66
+ ### Coverage Threshold
67
+
68
+ To enforce a minimum coverage percentage:
69
+ ```bash
70
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
71
+ ```
72
+
73
+ This will cause the test suite to fail if coverage drops below 80%.
74
+
75
+ ## Known Limitations & Roadmap
76
+
77
+ Current limitations and planned features are tracked in the GitHub issues.
78
+
79
+ See the [project roadmap](https://github.com/wexample/python-orm/issues) for upcoming features and improvements.
80
+
81
+ ## Status & Compatibility
82
+
83
+ **Maturity**: Production-ready
84
+
85
+ **Python Support**: >=3.10
86
+
87
+ **OS Support**: Linux, macOS, Windows
88
+
89
+ **Status**: Actively maintained
90
+
91
+ ## Useful Links
92
+
93
+ - **Homepage**: https://github.com/wexample/python-orm
94
+ - **Issue Tracker**: https://github.com/wexample/python-orm/issues
95
+ - **Discussions**: https://github.com/wexample/python-orm/discussions
96
+ - **PyPI**: [pypi.org/project/wexample-orm](https://pypi.org/project/wexample-orm/)
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-orm"
9
- version = "1.4.5"
9
+ version = "2.0.1"
10
10
  description = "Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg"
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -21,7 +21,8 @@ dependencies = [
21
21
  "attrs>=23.1.0",
22
22
  "psycopg[binary]>=3.2",
23
23
  "sqlalchemy>=2,<3",
24
- "wexample-helpers>=20.5.0",
24
+ "uuid-utils>=0.11",
25
+ "wexample-helpers>=20.6.0",
25
26
  ]
26
27
 
27
28
  [project.readme]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from uuid import UUID
4
+
5
+ from sqlalchemy import Uuid
6
+ from sqlalchemy.orm import Mapped, as_declarative, declared_attr, mapped_column
7
+ from wexample_helpers.helper.string import string_to_snake_case
8
+
9
+
10
+ @as_declarative()
11
+ class AbstractEntity:
12
+ """Common ancestor for every wex-managed SQLAlchemy entity.
13
+
14
+ Imposes a single convention for the primary key across all projects: a
15
+ UUIDv7, as the PHP side gives every entity (symfony-helpers
16
+ ``AbstractEntity``), so the same table is read and written from PHP and
17
+ Python alike.
18
+
19
+ The id is generated here, not by the database: an entity written from
20
+ Python has its id before the flush, and its timestamp prefix keeps the
21
+ creation order. ``Uuid`` is a native ``UUID`` on PostgreSQL and a
22
+ ``CHAR(32)`` on SQLite, so in-memory sessions used by
23
+ ``wexample_orm.testing.in_memory_session`` keep working as they are.
24
+ """
25
+
26
+ __abstract__ = True
27
+
28
+ @declared_attr
29
+ def __tablename__(cls) -> str:
30
+ return cls.get_entity_name()
31
+
32
+ @classmethod
33
+ def get_entity_name(cls) -> str:
34
+ return string_to_snake_case(cls.__name__)
35
+
36
+ @declared_attr
37
+ def id(cls) -> Mapped[UUID]:
38
+ from uuid_utils.compat import uuid7
39
+
40
+ return mapped_column(Uuid, primary_key=True, default=uuid7)
@@ -124,10 +124,11 @@ class EntityGenerator(BaseClass):
124
124
  self._attribute(
125
125
  foreign_key,
126
126
  "object",
127
- f"mapped_column(Integer, {constraint}nullable={nullable})",
127
+ f"mapped_column(Uuid, {constraint}nullable={nullable})",
128
128
  )
129
129
  )
130
- imports.add("Integer")
130
+ # Every id is a UUID (AbstractEntity), whatever entity it points to.
131
+ imports.add("Uuid")
131
132
  field_names.add(foreign_key)
132
133
 
133
134
  if target is not None:
@@ -21,6 +21,8 @@ def doctrine_type_to_sqlalchemy(orm: dict[str, Any]) -> tuple[str, set[str]]:
21
21
  the names it needs imported from `sqlalchemy`."""
22
22
  doctrine_type = str(orm.get("type") or "string").lower()
23
23
 
24
+ if doctrine_type == "uuid":
25
+ return "Uuid", {"Uuid"}
24
26
  if doctrine_type in {"integer", "int", "smallint", "bigint"}:
25
27
  return "Integer", {"Integer"}
26
28
  if doctrine_type in {"string", "guid"}:
@@ -35,7 +35,7 @@ class EntityImporter(BaseClass):
35
35
  """
36
36
 
37
37
  assign_ids: bool = public_field(
38
- description="Give every entity a sequential id, for graphs that never reach a database.",
38
+ description="Give every entity its UUID at once, for graphs that never reach a database.",
39
39
  default=False,
40
40
  )
41
41
  entity_types: list[type[AbstractEntity]] = public_field(
@@ -159,7 +159,9 @@ class EntityImporter(BaseClass):
159
159
  entity = entity_type()
160
160
  self._entities.append(entity)
161
161
  if self.assign_ids:
162
- entity.id = len(self._entities)
162
+ from uuid_utils.compat import uuid7
163
+
164
+ entity.id = uuid7()
163
165
  return entity
164
166
 
165
167
  def _find_column_attribute(self, mapper: Mapper, name: str) -> str | None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
+ from uuid import UUID
4
5
 
5
6
  from wexample_helpers.classes.base_class import BaseClass
6
7
  from wexample_helpers.classes.field import public_field
@@ -23,9 +24,13 @@ class AbstractRepository(BaseClass):
23
24
  def get_entity_type(cls) -> type[AbstractEntity]:
24
25
  cls._raise_not_implemented_error()
25
26
 
26
- def find(self, entity_id: int) -> AbstractEntity | None:
27
+ def find(self, entity_id: UUID | str) -> AbstractEntity | None:
27
28
  if self.session is None:
28
29
  return None
30
+ # Ids arrive as text as often as not (message bodies, command lines,
31
+ # API payloads); the Uuid column only binds UUID objects.
32
+ if not isinstance(entity_id, UUID):
33
+ entity_id = UUID(str(entity_id))
29
34
  return self.session.get(self.get_entity_type(), entity_id)
30
35
 
31
36
  def find_one_by(self, filter_clause: Any) -> AbstractEntity | None:
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from uuid import UUID
4
+
3
5
  import pytest
4
- from sqlalchemy import BigInteger
6
+ from sqlalchemy import Uuid
5
7
  from sqlalchemy.orm import Mapped, mapped_column
6
8
 
7
9
  from wexample_orm.common.abstract_repositories_manager import (
@@ -28,18 +30,20 @@ def test_entity_name_is_snake_case_of_class_name() -> None:
28
30
  # ---------------------------------------------------------------------------
29
31
 
30
32
 
31
- def test_id_is_bigint_identity_style() -> None:
32
- """Every entity gets a BIGINT autoincrement PK with no Sequence.
33
-
34
- Production schema (PG) uses ``GENERATED BY DEFAULT AS IDENTITY`` — the
35
- DB owns id allocation, SQLAlchemy doesn't carry a Sequence default.
36
- """
33
+ def test_id_is_a_uuid_v7_given_by_python() -> None:
34
+ """Every entity gets a UUID primary key, generated on the Python side: the
35
+ same identity the PHP entities have, available before the flush."""
37
36
  id_col = User.__table__.c.id
38
- assert isinstance(id_col.type, BigInteger)
39
- assert id_col.default is None # no SQLAlchemy-managed Sequence
40
- assert id_col.autoincrement is True
37
+ assert isinstance(id_col.type, Uuid)
41
38
  assert id_col.primary_key is True
42
39
 
40
+ with in_memory_session(AbstractEntity) as session:
41
+ user = User(name="alice")
42
+ session.add(user)
43
+ session.flush()
44
+ assert isinstance(user.id, UUID)
45
+ assert user.id.version == 7
46
+
43
47
 
44
48
  def test_manager_caches_repository_instances() -> None:
45
49
  with in_memory_session(AbstractEntity) as session:
@@ -86,12 +90,15 @@ def test_repository_without_session_raises_on_query() -> None:
86
90
  def test_save_and_find_roundtrip() -> None:
87
91
  with in_memory_session(AbstractEntity) as session:
88
92
  repo = UserRepository(session=session)
89
- repo.save(User(name="alice"))
93
+ alice = User(name="alice")
94
+ repo.save(alice)
90
95
  repo.save(User(name="bob"))
91
96
 
92
- first = repo.find(1)
97
+ first = repo.find(alice.id)
93
98
  assert first is not None
94
99
  assert first.name == "alice"
100
+ # The text form, as ids come from messages and payloads.
101
+ assert repo.find(str(alice.id)) is first
95
102
 
96
103
 
97
104
  class User(AbstractEntity):
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from uuid import UUID
4
+
3
5
  import pytest
4
- from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String
6
+ from sqlalchemy import JSON, DateTime, ForeignKey, String, Uuid
5
7
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
8
 
7
9
  from wexample_orm.entity.abstract_entity import AbstractEntity
@@ -157,9 +159,9 @@ class ImpFolder(AbstractEntity):
157
159
  "ImpFile", foreign_keys="ImpFile.folder_id", back_populates="folder"
158
160
  )
159
161
  owner = relationship("ImpOwner", foreign_keys="ImpFolder.owner_id")
160
- owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("imp_owner.id"), nullable=True)
162
+ owner_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("imp_owner.id"), nullable=True)
161
163
  pinned = relationship("ImpFile", foreign_keys="ImpFolder.pinned_id")
162
- pinned_id: Mapped[int] = mapped_column(Integer, ForeignKey("imp_file.id"), nullable=True)
164
+ pinned_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("imp_file.id"), nullable=True)
163
165
  title: Mapped[str] = mapped_column(String(64), nullable=True)
164
166
 
165
167
 
@@ -169,9 +171,9 @@ class ImpFile(AbstractEntity):
169
171
  folder = relationship(
170
172
  "ImpFolder", foreign_keys="ImpFile.folder_id", back_populates="files"
171
173
  )
172
- folder_id: Mapped[int] = mapped_column(Integer, ForeignKey("imp_folder.id"), nullable=True)
174
+ folder_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("imp_folder.id"), nullable=True)
173
175
  name: Mapped[str] = mapped_column(String(64), nullable=True)
174
- target: Mapped[int] = mapped_column(Integer, nullable=True)
176
+ target: Mapped[UUID] = mapped_column(Uuid, nullable=True)
175
177
 
176
178
 
177
179
  class ImpTag(AbstractEntity):
@@ -180,9 +182,9 @@ class ImpTag(AbstractEntity):
180
182
 
181
183
  class ImpFolderTag(AbstractEntity):
182
184
  folder = relationship("ImpFolder", foreign_keys="ImpFolderTag.folder_id")
183
- folder_id: Mapped[int] = mapped_column(Integer, ForeignKey("imp_folder.id"), nullable=True)
185
+ folder_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("imp_folder.id"), nullable=True)
184
186
  tag = relationship("ImpTag", foreign_keys="ImpFolderTag.tag_id")
185
- tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("imp_tag.id"), nullable=True)
187
+ tag_id: Mapped[UUID] = mapped_column(Uuid, ForeignKey("imp_tag.id"), nullable=True)
186
188
 
187
189
 
188
190
  ENTITY_TYPES = [ImpOwner, ImpFolder, ImpFile, ImpTag, ImpFolderTag]
@@ -1,347 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: wexample-orm
3
- Version: 1.4.5
4
- Summary: Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg
5
- Author-Email: weeger <contact@wexample.com>
6
- License: MIT
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Project-URL: homepage, https://github.com/wexample/python-orm
11
- Requires-Python: >=3.10
12
- Requires-Dist: attrs>=23.1.0
13
- Requires-Dist: psycopg[binary]>=3.2
14
- Requires-Dist: sqlalchemy<3,>=2
15
- Requires-Dist: wexample-helpers>=20.5.0
16
- Provides-Extra: dev
17
- Requires-Dist: pytest; extra == "dev"
18
- Requires-Dist: pytest-cov; extra == "dev"
19
- Description-Content-Type: text/markdown
20
-
21
- # orm
22
-
23
- Version: 1.4.5
24
-
25
- `wexample-orm` provides four SQLAlchemy base classes — `AbstractEntity`, `AbstractRepository`, `AbstractSessionFactory`, and `AbstractRepositoriesManager` — that impose a Doctrine-style repository pattern on top of SQLAlchemy 2. It targets Python developers in the Wexample stack who connect to PostgreSQL via psycopg and want a consistent, opinionated foundation without rewriting engine, session, and repository boilerplate for every project.
26
-
27
- ## Table of Contents
28
-
29
- - [Installation](#installation)
30
- - [Quickstart](#quickstart)
31
- - [Tests](#tests)
32
- - [Architecture](#architecture)
33
- - [Integration in the Suite](#integration-in-the-suite)
34
- - [Dependencies](#dependencies)
35
- - [Versioning & Compatibility Policy](#versioning--compatibility-policy)
36
- - [License](#license)
37
- - [About us](#about-us)
38
- - [Known Limitations & Roadmap](#known-limitations--roadmap)
39
- - [Status & Compatibility](#status--compatibility)
40
- - [Useful Links](#useful-links)
41
- - [Migration Notes](#migration-notes)
42
-
43
- ## Installation
44
-
45
- ```bash
46
- pip install wexample-orm
47
- ```
48
-
49
- Requires Python >=3.10.
50
-
51
- ## Quickstart
52
-
53
- Install from PyPI:
54
-
55
- ```bash
56
- pip install wexample-orm
57
- ```
58
-
59
- ### Define an entity and a repository
60
-
61
- Subclass `AbstractEntity` to get a BIGINT primary key and a table name derived automatically from the class name (`User` → `user`). Subclass `AbstractRepository` and declare which entity it covers:
62
-
63
- ```python
64
- from sqlalchemy.orm import Mapped, mapped_column
65
- from wexample_orm.entity.abstract_entity import AbstractEntity
66
- from wexample_orm.repository.abstract_repository import AbstractRepository
67
-
68
- class User(AbstractEntity):
69
- name: Mapped[str] = mapped_column()
70
-
71
- class UserRepository(AbstractRepository):
72
- @classmethod
73
- def get_entity_type(cls) -> type[AbstractEntity]:
74
- return User
75
- ```
76
-
77
- ### Connect and persist (PostgreSQL)
78
-
79
- `AbstractSessionFactory` wraps `create_engine` and `sessionmaker`. Pass a SQLAlchemy URL and call `create_session()`:
80
-
81
- ```python
82
- from wexample_orm.session.abstract_session_factory import AbstractSessionFactory
83
-
84
- factory = AbstractSessionFactory(dsn="postgresql+psycopg://user:password@localhost/mydb")
85
- User.metadata.create_all(factory.get_engine())
86
-
87
- session = factory.create_session()
88
- repo = UserRepository(session=session)
89
-
90
- user = repo.save(User(name="alice"))
91
- print(user.id) # 1 — assigned by the database
92
-
93
- found = repo.find(user.id)
94
- print(found.name) # alice
95
- ```
96
-
97
- `save()` calls `session.commit()` by default. Pass `flush=False` to add the row without committing, or `refresh=True` to reload the instance from the database after commit.
98
-
99
- ### In-memory session for local development and tests
100
-
101
- `wexample_orm.testing.in_memory_session` creates a disposable SQLite engine — no PostgreSQL required. It creates all tables on entry and disposes the engine on exit:
102
-
103
- ```python
104
- from wexample_orm.testing import in_memory_session
105
-
106
- with in_memory_session(AbstractEntity) as session:
107
- repo = UserRepository(session=session)
108
- repo.save(User(name="alice"))
109
-
110
- found = repo.find(1)
111
- print(found.name) # alice
112
- ```
113
-
114
- ## Tests
115
-
116
- This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
117
-
118
- ### Installation
119
-
120
- First, install the required testing dependencies:
121
- ```bash
122
- .venv/bin/python -m pip install pytest pytest-cov
123
- ```
124
-
125
- ### Basic Usage
126
-
127
- Run all tests with coverage:
128
- ```bash
129
- .venv/bin/python -m pytest --cov --cov-report=html
130
- ```
131
-
132
- ### Common Commands
133
- ```bash
134
- # Run tests with coverage for a specific module
135
- .venv/bin/python -m pytest --cov=your_module
136
-
137
- # Show which lines are not covered
138
- .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
139
-
140
- # Generate an HTML coverage report
141
- .venv/bin/python -m pytest --cov=your_module --cov-report=html
142
-
143
- # Combine terminal and HTML reports
144
- .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
145
-
146
- # Run specific test file with coverage
147
- .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
148
- ```
149
-
150
- ### Viewing HTML Reports
151
-
152
- After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
153
-
154
- ### Coverage Threshold
155
-
156
- To enforce a minimum coverage percentage:
157
- ```bash
158
- .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
159
- ```
160
-
161
- This will cause the test suite to fail if coverage drops below 80%.
162
-
163
- ## Architecture
164
-
165
- The package (`wexample-orm`) is a thin layer on top of SQLAlchemy 2.x. It defines four abstract base classes that projects subclass, two domain exceptions, and a test helper. Nothing is concrete out of the box: the library ships patterns, not implementations.
166
-
167
- ### Entity
168
-
169
- src/wexample_orm/entity/abstract_entity.py
170
-
171
- `AbstractEntity` is the SQLAlchemy declarative base for the whole stack. It is decorated with `@as_declarative()`, so every subclass automatically participates in the same `MetaData` registry.
172
-
173
- Two conventions are fixed here and nowhere else:
174
-
175
- - **Table name** — `__tablename__` is set by `get_entity_name()`, which calls `string_to_snake_case(cls.__name__)`. A class named `UserAccount` maps to the table `user_account` with no annotation needed.
176
- - **Primary key** — `id` is a `BigInteger` with `autoincrement=True`. A `with_variant(Integer(), "sqlite")` is bolted on so that the same declaration works in the in-memory SQLite sessions used during testing (SQLite only auto-increments a column typed `INTEGER PRIMARY KEY`, not `BIGINT`).
177
-
178
- ### Repository
179
-
180
- src/wexample_orm/repository/abstract_repository.py
181
-
182
- `AbstractRepository` is the data-access object. It holds one `session` field (default `None`) and exposes:
183
-
184
- - `get_entity_type()` — abstract classmethod every concrete repository must implement; returns the `AbstractEntity` subclass it owns.
185
- - `find(id)` — delegates to `session.get(EntityType, id)`; returns `None` when no session is set.
186
- - `find_one_by(filter_clause)` — `query().filter(clause).first()`.
187
- - `query()` — returns a raw `session.query(EntityType)`; calls `_require_session()` first.
188
- - `save(entity, flush=True, refresh=False)` — `session.add` then `session.commit`; optionally refreshes from DB.
189
- - `_require_session()` — raises `RepositorySessionMissingException` when `session` is `None`; called by any method that cannot function without it.
190
-
191
- A repository can be constructed standalone (session-less `find` returns `None`, everything else raises) or injected with a session directly.
192
-
193
- ### Session factory
194
-
195
- src/wexample_orm/session/abstract_session_factory.py
196
-
197
- `AbstractSessionFactory` builds and caches a SQLAlchemy engine and sessionmaker from a DSN string. The engine is created on first call to `get_engine()` and reused thereafter; the sessionmaker is created on first call to `create_session()`.
198
-
199
- Public API:
200
-
201
- - `dsn` — SQLAlchemy URL, e.g. `postgresql+psycopg://user:pwd@host/db` or `sqlite:///:memory:`.
202
- - `engine_options` / `session_options` — dicts forwarded verbatim to `create_engine()` and `sessionmaker()`.
203
- - `create_session()` — returns a fresh `Session` from the cached maker.
204
- - `get_engine()` — returns the cached engine (creates it on first call).
205
- - `dispose()` — calls `engine.dispose()` and resets both caches; intended for test teardown.
206
- - `_engine_kwargs()` / `_session_kwargs()` — overridable hooks; by default they just copy the option dicts.
207
-
208
- The session factory is decoupled from the rest: nothing in the repository or manager imports it. Callers create a session through it and pass it in manually.
209
-
210
- ### Repositories manager
211
-
212
- src/wexample_orm/common/abstract_repositories_manager.py
213
-
214
- `AbstractRepositoriesManager` is a registry that maps entity types to repository instances. It is the single entry point when a project has multiple repositories and wants to avoid constructing them by hand.
215
-
216
- Fields:
217
-
218
- - `repository_classes` — the list of repository classes the manager may instantiate.
219
- - `session` — the SQLAlchemy session shared across every repository the manager creates.
220
- - `_instances` — private cache (`entity_type → repository instance`); a repository is built once and reused.
221
-
222
- Methods:
223
-
224
- - `get(entity_type)` — looks up the cache; on miss, walks `repository_classes` looking for the class whose `get_entity_type()` matches, instantiates it with `session=self.session`, stores it, and returns it. Raises `UnknownRepositoryException` if nothing matches.
225
- - `get_by_table_name(table_name)` — same lookup but keyed on `entity_type.get_entity_name()` (the snake-case table name).
226
- - `get_classes()` — returns a copy of `repository_classes`.
227
-
228
- ### Call path
229
-
230
- A typical read goes: caller asks the manager for a repository (`manager.get(User)`) → manager instantiates `UserRepository(session=session)` if not cached → caller calls `repo.find(42)` → repository calls `session.get(User, 42)` → SQLAlchemy issues the SQL against the engine that the session factory built.
231
-
232
- A write follows the same path through `repo.save(entity)`, which calls `session.add` then `session.commit`.
233
-
234
- ### Exceptions
235
-
236
- src/wexample_orm/exception/unknown_repository_exception.py
237
-
238
- `UnknownRepositoryException` (error code `ORM_UNKNOWN_REPOSITORY`) is raised by the manager when `get()` receives an entity type for which no repository class is registered.
239
-
240
- src/wexample_orm/exception/repository_session_missing_exception.py
241
-
242
- `RepositorySessionMissingException` (error code `ORM_REPOSITORY_SESSION_MISSING`) is raised by `AbstractRepository._require_session()` when a session-dependent method is called on a repository that has `session=None`.
243
-
244
- Both extend `UndefinedException` from `wexample-helpers` and carry a structured `data` dict alongside the human-readable message.
245
-
246
- ### Testing helper
247
-
248
- src/wexample_orm/testing/sqlite.py
249
-
250
- `in_memory_session(declarative_base)` is a context manager that creates a fresh SQLite in-memory engine, runs `metadata.create_all()` on it, yields a session, then closes the session and disposes the engine. The `declarative_base` argument must be the `@as_declarative()`-decorated class whose `MetaData` holds the tables to create — in practice `AbstractEntity` or a project subclass of it.
251
-
252
- ```python
253
- from wexample_orm.testing import in_memory_session
254
- from wexample_orm.entity.abstract_entity import AbstractEntity
255
-
256
- with in_memory_session(AbstractEntity) as session:
257
- repo = UserRepository(session=session)
258
- repo.save(User(name="alice"))
259
- assert repo.find(1).name == "alice"
260
- ```
261
-
262
- The helper is excluded from the published wheel (`tool.setuptools.packages.find` omits `wexample_orm.testing*`) and is only available in a development install.
263
-
264
- ### Source layout
265
-
266
- ```
267
- src/wexample_orm/
268
- __init__.py # re-exports the three main abstractions
269
- entity/abstract_entity.py # declarative base + PK convention
270
- repository/abstract_repository.py # data-access object
271
- session/abstract_session_factory.py# engine + sessionmaker cache
272
- common/abstract_repositories_manager.py # repository registry
273
- exception/
274
- unknown_repository_exception.py
275
- repository_session_missing_exception.py
276
- testing/sqlite.py # in-memory SQLite helper
277
- ```
278
-
279
- ## Integration in the Suite
280
-
281
- This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.
282
-
283
- ### Related Packages
284
-
285
- The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
286
-
287
- ## Dependencies
288
-
289
- - attrs: >=23.1.0
290
- - psycopg: >=3.2
291
- - sqlalchemy: <3,>=2
292
- - wexample-helpers: >=20.5.0
293
-
294
- ## Versioning & Compatibility Policy
295
-
296
- Wexample packages follow **Semantic Versioning** (SemVer):
297
-
298
- - **MAJOR**: Breaking changes
299
- - **MINOR**: New features, backward compatible
300
- - **PATCH**: Bug fixes, backward compatible
301
-
302
- We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
303
-
304
- ## License
305
-
306
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
307
-
308
- Free to use in both personal and commercial projects.
309
-
310
- ## About us
311
-
312
- [Wexample](https://wexample.com) stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
313
-
314
- This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
315
-
316
- Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
317
-
318
- Built and maintained by [Romain Weeger](https://weeger.io).
319
-
320
- ## Known Limitations & Roadmap
321
-
322
- Current limitations and planned features are tracked in the GitHub issues.
323
-
324
- See the [project roadmap](https://github.com/wexample/python-orm/issues) for upcoming features and improvements.
325
-
326
- ## Status & Compatibility
327
-
328
- **Maturity**: Production-ready
329
-
330
- **Python Support**: >=3.10
331
-
332
- **OS Support**: Linux, macOS, Windows
333
-
334
- **Status**: Actively maintained
335
-
336
- ## Useful Links
337
-
338
- - **Homepage**: https://github.com/wexample/python-orm
339
- - **Issue Tracker**: https://github.com/wexample/python-orm/issues
340
- - **Discussions**: https://github.com/wexample/python-orm/discussions
341
- - **PyPI**: [pypi.org/project/wexample-orm](https://pypi.org/project/wexample-orm/)
342
-
343
- ## Migration Notes
344
-
345
- When upgrading between major versions, refer to the migration guides in the documentation.
346
-
347
- Breaking changes are clearly documented with upgrade paths and examples.
@@ -1,327 +0,0 @@
1
- # orm
2
-
3
- Version: 1.4.5
4
-
5
- `wexample-orm` provides four SQLAlchemy base classes — `AbstractEntity`, `AbstractRepository`, `AbstractSessionFactory`, and `AbstractRepositoriesManager` — that impose a Doctrine-style repository pattern on top of SQLAlchemy 2. It targets Python developers in the Wexample stack who connect to PostgreSQL via psycopg and want a consistent, opinionated foundation without rewriting engine, session, and repository boilerplate for every project.
6
-
7
- ## Table of Contents
8
-
9
- - [Installation](#installation)
10
- - [Quickstart](#quickstart)
11
- - [Tests](#tests)
12
- - [Architecture](#architecture)
13
- - [Integration in the Suite](#integration-in-the-suite)
14
- - [Dependencies](#dependencies)
15
- - [Versioning & Compatibility Policy](#versioning--compatibility-policy)
16
- - [License](#license)
17
- - [About us](#about-us)
18
- - [Known Limitations & Roadmap](#known-limitations--roadmap)
19
- - [Status & Compatibility](#status--compatibility)
20
- - [Useful Links](#useful-links)
21
- - [Migration Notes](#migration-notes)
22
-
23
- ## Installation
24
-
25
- ```bash
26
- pip install wexample-orm
27
- ```
28
-
29
- Requires Python >=3.10.
30
-
31
- ## Quickstart
32
-
33
- Install from PyPI:
34
-
35
- ```bash
36
- pip install wexample-orm
37
- ```
38
-
39
- ### Define an entity and a repository
40
-
41
- Subclass `AbstractEntity` to get a BIGINT primary key and a table name derived automatically from the class name (`User` → `user`). Subclass `AbstractRepository` and declare which entity it covers:
42
-
43
- ```python
44
- from sqlalchemy.orm import Mapped, mapped_column
45
- from wexample_orm.entity.abstract_entity import AbstractEntity
46
- from wexample_orm.repository.abstract_repository import AbstractRepository
47
-
48
- class User(AbstractEntity):
49
- name: Mapped[str] = mapped_column()
50
-
51
- class UserRepository(AbstractRepository):
52
- @classmethod
53
- def get_entity_type(cls) -> type[AbstractEntity]:
54
- return User
55
- ```
56
-
57
- ### Connect and persist (PostgreSQL)
58
-
59
- `AbstractSessionFactory` wraps `create_engine` and `sessionmaker`. Pass a SQLAlchemy URL and call `create_session()`:
60
-
61
- ```python
62
- from wexample_orm.session.abstract_session_factory import AbstractSessionFactory
63
-
64
- factory = AbstractSessionFactory(dsn="postgresql+psycopg://user:password@localhost/mydb")
65
- User.metadata.create_all(factory.get_engine())
66
-
67
- session = factory.create_session()
68
- repo = UserRepository(session=session)
69
-
70
- user = repo.save(User(name="alice"))
71
- print(user.id) # 1 — assigned by the database
72
-
73
- found = repo.find(user.id)
74
- print(found.name) # alice
75
- ```
76
-
77
- `save()` calls `session.commit()` by default. Pass `flush=False` to add the row without committing, or `refresh=True` to reload the instance from the database after commit.
78
-
79
- ### In-memory session for local development and tests
80
-
81
- `wexample_orm.testing.in_memory_session` creates a disposable SQLite engine — no PostgreSQL required. It creates all tables on entry and disposes the engine on exit:
82
-
83
- ```python
84
- from wexample_orm.testing import in_memory_session
85
-
86
- with in_memory_session(AbstractEntity) as session:
87
- repo = UserRepository(session=session)
88
- repo.save(User(name="alice"))
89
-
90
- found = repo.find(1)
91
- print(found.name) # alice
92
- ```
93
-
94
- ## Tests
95
-
96
- This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
97
-
98
- ### Installation
99
-
100
- First, install the required testing dependencies:
101
- ```bash
102
- .venv/bin/python -m pip install pytest pytest-cov
103
- ```
104
-
105
- ### Basic Usage
106
-
107
- Run all tests with coverage:
108
- ```bash
109
- .venv/bin/python -m pytest --cov --cov-report=html
110
- ```
111
-
112
- ### Common Commands
113
- ```bash
114
- # Run tests with coverage for a specific module
115
- .venv/bin/python -m pytest --cov=your_module
116
-
117
- # Show which lines are not covered
118
- .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
119
-
120
- # Generate an HTML coverage report
121
- .venv/bin/python -m pytest --cov=your_module --cov-report=html
122
-
123
- # Combine terminal and HTML reports
124
- .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
125
-
126
- # Run specific test file with coverage
127
- .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
128
- ```
129
-
130
- ### Viewing HTML Reports
131
-
132
- After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
133
-
134
- ### Coverage Threshold
135
-
136
- To enforce a minimum coverage percentage:
137
- ```bash
138
- .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
139
- ```
140
-
141
- This will cause the test suite to fail if coverage drops below 80%.
142
-
143
- ## Architecture
144
-
145
- The package (`wexample-orm`) is a thin layer on top of SQLAlchemy 2.x. It defines four abstract base classes that projects subclass, two domain exceptions, and a test helper. Nothing is concrete out of the box: the library ships patterns, not implementations.
146
-
147
- ### Entity
148
-
149
- src/wexample_orm/entity/abstract_entity.py
150
-
151
- `AbstractEntity` is the SQLAlchemy declarative base for the whole stack. It is decorated with `@as_declarative()`, so every subclass automatically participates in the same `MetaData` registry.
152
-
153
- Two conventions are fixed here and nowhere else:
154
-
155
- - **Table name** — `__tablename__` is set by `get_entity_name()`, which calls `string_to_snake_case(cls.__name__)`. A class named `UserAccount` maps to the table `user_account` with no annotation needed.
156
- - **Primary key** — `id` is a `BigInteger` with `autoincrement=True`. A `with_variant(Integer(), "sqlite")` is bolted on so that the same declaration works in the in-memory SQLite sessions used during testing (SQLite only auto-increments a column typed `INTEGER PRIMARY KEY`, not `BIGINT`).
157
-
158
- ### Repository
159
-
160
- src/wexample_orm/repository/abstract_repository.py
161
-
162
- `AbstractRepository` is the data-access object. It holds one `session` field (default `None`) and exposes:
163
-
164
- - `get_entity_type()` — abstract classmethod every concrete repository must implement; returns the `AbstractEntity` subclass it owns.
165
- - `find(id)` — delegates to `session.get(EntityType, id)`; returns `None` when no session is set.
166
- - `find_one_by(filter_clause)` — `query().filter(clause).first()`.
167
- - `query()` — returns a raw `session.query(EntityType)`; calls `_require_session()` first.
168
- - `save(entity, flush=True, refresh=False)` — `session.add` then `session.commit`; optionally refreshes from DB.
169
- - `_require_session()` — raises `RepositorySessionMissingException` when `session` is `None`; called by any method that cannot function without it.
170
-
171
- A repository can be constructed standalone (session-less `find` returns `None`, everything else raises) or injected with a session directly.
172
-
173
- ### Session factory
174
-
175
- src/wexample_orm/session/abstract_session_factory.py
176
-
177
- `AbstractSessionFactory` builds and caches a SQLAlchemy engine and sessionmaker from a DSN string. The engine is created on first call to `get_engine()` and reused thereafter; the sessionmaker is created on first call to `create_session()`.
178
-
179
- Public API:
180
-
181
- - `dsn` — SQLAlchemy URL, e.g. `postgresql+psycopg://user:pwd@host/db` or `sqlite:///:memory:`.
182
- - `engine_options` / `session_options` — dicts forwarded verbatim to `create_engine()` and `sessionmaker()`.
183
- - `create_session()` — returns a fresh `Session` from the cached maker.
184
- - `get_engine()` — returns the cached engine (creates it on first call).
185
- - `dispose()` — calls `engine.dispose()` and resets both caches; intended for test teardown.
186
- - `_engine_kwargs()` / `_session_kwargs()` — overridable hooks; by default they just copy the option dicts.
187
-
188
- The session factory is decoupled from the rest: nothing in the repository or manager imports it. Callers create a session through it and pass it in manually.
189
-
190
- ### Repositories manager
191
-
192
- src/wexample_orm/common/abstract_repositories_manager.py
193
-
194
- `AbstractRepositoriesManager` is a registry that maps entity types to repository instances. It is the single entry point when a project has multiple repositories and wants to avoid constructing them by hand.
195
-
196
- Fields:
197
-
198
- - `repository_classes` — the list of repository classes the manager may instantiate.
199
- - `session` — the SQLAlchemy session shared across every repository the manager creates.
200
- - `_instances` — private cache (`entity_type → repository instance`); a repository is built once and reused.
201
-
202
- Methods:
203
-
204
- - `get(entity_type)` — looks up the cache; on miss, walks `repository_classes` looking for the class whose `get_entity_type()` matches, instantiates it with `session=self.session`, stores it, and returns it. Raises `UnknownRepositoryException` if nothing matches.
205
- - `get_by_table_name(table_name)` — same lookup but keyed on `entity_type.get_entity_name()` (the snake-case table name).
206
- - `get_classes()` — returns a copy of `repository_classes`.
207
-
208
- ### Call path
209
-
210
- A typical read goes: caller asks the manager for a repository (`manager.get(User)`) → manager instantiates `UserRepository(session=session)` if not cached → caller calls `repo.find(42)` → repository calls `session.get(User, 42)` → SQLAlchemy issues the SQL against the engine that the session factory built.
211
-
212
- A write follows the same path through `repo.save(entity)`, which calls `session.add` then `session.commit`.
213
-
214
- ### Exceptions
215
-
216
- src/wexample_orm/exception/unknown_repository_exception.py
217
-
218
- `UnknownRepositoryException` (error code `ORM_UNKNOWN_REPOSITORY`) is raised by the manager when `get()` receives an entity type for which no repository class is registered.
219
-
220
- src/wexample_orm/exception/repository_session_missing_exception.py
221
-
222
- `RepositorySessionMissingException` (error code `ORM_REPOSITORY_SESSION_MISSING`) is raised by `AbstractRepository._require_session()` when a session-dependent method is called on a repository that has `session=None`.
223
-
224
- Both extend `UndefinedException` from `wexample-helpers` and carry a structured `data` dict alongside the human-readable message.
225
-
226
- ### Testing helper
227
-
228
- src/wexample_orm/testing/sqlite.py
229
-
230
- `in_memory_session(declarative_base)` is a context manager that creates a fresh SQLite in-memory engine, runs `metadata.create_all()` on it, yields a session, then closes the session and disposes the engine. The `declarative_base` argument must be the `@as_declarative()`-decorated class whose `MetaData` holds the tables to create — in practice `AbstractEntity` or a project subclass of it.
231
-
232
- ```python
233
- from wexample_orm.testing import in_memory_session
234
- from wexample_orm.entity.abstract_entity import AbstractEntity
235
-
236
- with in_memory_session(AbstractEntity) as session:
237
- repo = UserRepository(session=session)
238
- repo.save(User(name="alice"))
239
- assert repo.find(1).name == "alice"
240
- ```
241
-
242
- The helper is excluded from the published wheel (`tool.setuptools.packages.find` omits `wexample_orm.testing*`) and is only available in a development install.
243
-
244
- ### Source layout
245
-
246
- ```
247
- src/wexample_orm/
248
- __init__.py # re-exports the three main abstractions
249
- entity/abstract_entity.py # declarative base + PK convention
250
- repository/abstract_repository.py # data-access object
251
- session/abstract_session_factory.py# engine + sessionmaker cache
252
- common/abstract_repositories_manager.py # repository registry
253
- exception/
254
- unknown_repository_exception.py
255
- repository_session_missing_exception.py
256
- testing/sqlite.py # in-memory SQLite helper
257
- ```
258
-
259
- ## Integration in the Suite
260
-
261
- This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.
262
-
263
- ### Related Packages
264
-
265
- The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
266
-
267
- ## Dependencies
268
-
269
- - attrs: >=23.1.0
270
- - psycopg: >=3.2
271
- - sqlalchemy: <3,>=2
272
- - wexample-helpers: >=20.5.0
273
-
274
- ## Versioning & Compatibility Policy
275
-
276
- Wexample packages follow **Semantic Versioning** (SemVer):
277
-
278
- - **MAJOR**: Breaking changes
279
- - **MINOR**: New features, backward compatible
280
- - **PATCH**: Bug fixes, backward compatible
281
-
282
- We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
283
-
284
- ## License
285
-
286
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
287
-
288
- Free to use in both personal and commercial projects.
289
-
290
- ## About us
291
-
292
- [Wexample](https://wexample.com) stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
293
-
294
- This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
295
-
296
- Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
297
-
298
- Built and maintained by [Romain Weeger](https://weeger.io).
299
-
300
- ## Known Limitations & Roadmap
301
-
302
- Current limitations and planned features are tracked in the GitHub issues.
303
-
304
- See the [project roadmap](https://github.com/wexample/python-orm/issues) for upcoming features and improvements.
305
-
306
- ## Status & Compatibility
307
-
308
- **Maturity**: Production-ready
309
-
310
- **Python Support**: >=3.10
311
-
312
- **OS Support**: Linux, macOS, Windows
313
-
314
- **Status**: Actively maintained
315
-
316
- ## Useful Links
317
-
318
- - **Homepage**: https://github.com/wexample/python-orm
319
- - **Issue Tracker**: https://github.com/wexample/python-orm/issues
320
- - **Discussions**: https://github.com/wexample/python-orm/discussions
321
- - **PyPI**: [pypi.org/project/wexample-orm](https://pypi.org/project/wexample-orm/)
322
-
323
- ## Migration Notes
324
-
325
- When upgrading between major versions, refer to the migration guides in the documentation.
326
-
327
- Breaking changes are clearly documented with upgrade paths and examples.
@@ -1,41 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from sqlalchemy import BigInteger, Integer
4
- from sqlalchemy.orm import Mapped, as_declarative, declared_attr, mapped_column
5
- from wexample_helpers.helper.string import string_to_snake_case
6
-
7
-
8
- @as_declarative()
9
- class AbstractEntity:
10
- """Common ancestor for every wex-managed SQLAlchemy entity.
11
-
12
- Imposes a single convention for the primary key across all projects:
13
-
14
- ``id BIGINT GENERATED BY DEFAULT AS IDENTITY`` on PostgreSQL — modern
15
- SQL standard (PG ≥ 10) and Doctrine's default when it owns the
16
- schema, so the same table works seamlessly from PHP and Python.
17
-
18
- A ``with_variant(Integer, "sqlite")`` is bolted on for tests: SQLite
19
- only auto-increments a column declared as ``INTEGER PRIMARY KEY``
20
- (ROWID alias), not ``BIGINT``. Production stays BIGINT, in-memory
21
- sessions used by ``wexample_orm.testing.in_memory_session`` keep
22
- working without per-project gymnastics.
23
- """
24
-
25
- __abstract__ = True
26
-
27
- @declared_attr
28
- def __tablename__(cls) -> str:
29
- return cls.get_entity_name()
30
-
31
- @classmethod
32
- def get_entity_name(cls) -> str:
33
- return string_to_snake_case(cls.__name__)
34
-
35
- @declared_attr
36
- def id(cls) -> Mapped[int]:
37
- return mapped_column(
38
- BigInteger().with_variant(Integer(), "sqlite"),
39
- primary_key=True,
40
- autoincrement=True,
41
- )