Metadata-Version: 2.1
Name: wexample-orm
Version: 1.3.3
Summary: Provides SQLAlchemy ORM abstractions — base entity, repository, session factory, and repository manager — with PostgreSQL-first defaults via psycopg
Author-Email: weeger <contact@wexample.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Project-URL: homepage, https://github.com/wexample/python-orm
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: psycopg[binary]>=3.2
Requires-Dist: sqlalchemy<3,>=2
Requires-Dist: wexample-helpers>=20.0.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Description-Content-Type: text/markdown

# orm

Version: 1.3.3

`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.

## Table of Contents

- [Installation](#installation)
- [Quickstart](#quickstart)
- [Tests](#tests)
- [Architecture](#architecture)
- [Integration in the Suite](#integration-in-the-suite)
- [Dependencies](#dependencies)
- [Versioning & Compatibility Policy](#versioning--compatibility-policy)
- [License](#license)
- [About us](#about-us)
- [Known Limitations & Roadmap](#known-limitations--roadmap)
- [Status & Compatibility](#status--compatibility)
- [Useful Links](#useful-links)
- [Migration Notes](#migration-notes)

## Installation

```bash
pip install wexample-orm
```

Requires Python >=3.10.

## Quickstart

Install from PyPI:

```bash
pip install wexample-orm
```

### Define an entity and a repository

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:

```python
from sqlalchemy.orm import Mapped, mapped_column
from wexample_orm.entity.abstract_entity import AbstractEntity
from wexample_orm.repository.abstract_repository import AbstractRepository

class User(AbstractEntity):
    name: Mapped[str] = mapped_column()

class UserRepository(AbstractRepository):
    @classmethod
    def get_entity_type(cls) -> type[AbstractEntity]:
        return User
```

### Connect and persist (PostgreSQL)

`AbstractSessionFactory` wraps `create_engine` and `sessionmaker`. Pass a SQLAlchemy URL and call `create_session()`:

```python
from wexample_orm.session.abstract_session_factory import AbstractSessionFactory

factory = AbstractSessionFactory(dsn="postgresql+psycopg://user:password@localhost/mydb")
User.metadata.create_all(factory.get_engine())

session = factory.create_session()
repo = UserRepository(session=session)

user = repo.save(User(name="alice"))
print(user.id)    # 1 — assigned by the database

found = repo.find(user.id)
print(found.name) # alice
```

`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.

### In-memory session for local development and tests

`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:

```python
from wexample_orm.testing import in_memory_session

with in_memory_session(AbstractEntity) as session:
    repo = UserRepository(session=session)
    repo.save(User(name="alice"))

    found = repo.find(1)
    print(found.name)  # alice
```

## Tests

This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.

### Installation

First, install the required testing dependencies:
```bash
.venv/bin/python -m pip install pytest pytest-cov
```

### Basic Usage

Run all tests with coverage:
```bash
.venv/bin/python -m pytest --cov --cov-report=html
```

### Common Commands
```bash
# Run tests with coverage for a specific module
.venv/bin/python -m pytest --cov=your_module

# Show which lines are not covered
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing

# Generate an HTML coverage report
.venv/bin/python -m pytest --cov=your_module --cov-report=html

# Combine terminal and HTML reports
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html

# Run specific test file with coverage
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
```

### Viewing HTML Reports

After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.

### Coverage Threshold

To enforce a minimum coverage percentage:
```bash
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
```

This will cause the test suite to fail if coverage drops below 80%.

## Architecture

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.

### Entity

src/wexample_orm/entity/abstract_entity.py

`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.

Two conventions are fixed here and nowhere else:

- **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.
- **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`).

### Repository

src/wexample_orm/repository/abstract_repository.py

`AbstractRepository` is the data-access object. It holds one `session` field (default `None`) and exposes:

- `get_entity_type()` — abstract classmethod every concrete repository must implement; returns the `AbstractEntity` subclass it owns.
- `find(id)` — delegates to `session.get(EntityType, id)`; returns `None` when no session is set.
- `find_one_by(filter_clause)` — `query().filter(clause).first()`.
- `query()` — returns a raw `session.query(EntityType)`; calls `_require_session()` first.
- `save(entity, flush=True, refresh=False)` — `session.add` then `session.commit`; optionally refreshes from DB.
- `_require_session()` — raises `RepositorySessionMissingException` when `session` is `None`; called by any method that cannot function without it.

A repository can be constructed standalone (session-less `find` returns `None`, everything else raises) or injected with a session directly.

### Session factory

src/wexample_orm/session/abstract_session_factory.py

`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()`.

Public API:

- `dsn` — SQLAlchemy URL, e.g. `postgresql+psycopg://user:pwd@host/db` or `sqlite:///:memory:`.
- `engine_options` / `session_options` — dicts forwarded verbatim to `create_engine()` and `sessionmaker()`.
- `create_session()` — returns a fresh `Session` from the cached maker.
- `get_engine()` — returns the cached engine (creates it on first call).
- `dispose()` — calls `engine.dispose()` and resets both caches; intended for test teardown.
- `_engine_kwargs()` / `_session_kwargs()` — overridable hooks; by default they just copy the option dicts.

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.

### Repositories manager

src/wexample_orm/common/abstract_repositories_manager.py

`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.

Fields:

- `repository_classes` — the list of repository classes the manager may instantiate.
- `session` — the SQLAlchemy session shared across every repository the manager creates.
- `_instances` — private cache (`entity_type → repository instance`); a repository is built once and reused.

Methods:

- `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.
- `get_by_table_name(table_name)` — same lookup but keyed on `entity_type.get_entity_name()` (the snake-case table name).
- `get_classes()` — returns a copy of `repository_classes`.

### Call path

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.

A write follows the same path through `repo.save(entity)`, which calls `session.add` then `session.commit`.

### Exceptions

src/wexample_orm/exception/unknown_repository_exception.py

`UnknownRepositoryException` (error code `ORM_UNKNOWN_REPOSITORY`) is raised by the manager when `get()` receives an entity type for which no repository class is registered.

src/wexample_orm/exception/repository_session_missing_exception.py

`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`.

Both extend `UndefinedException` from `wexample-helpers` and carry a structured `data` dict alongside the human-readable message.

### Testing helper

src/wexample_orm/testing/sqlite.py

`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.

```python
from wexample_orm.testing import in_memory_session
from wexample_orm.entity.abstract_entity import AbstractEntity

with in_memory_session(AbstractEntity) as session:
    repo = UserRepository(session=session)
    repo.save(User(name="alice"))
    assert repo.find(1).name == "alice"
```

The helper is excluded from the published wheel (`tool.setuptools.packages.find` omits `wexample_orm.testing*`) and is only available in a development install.

### Source layout

```
src/wexample_orm/
    __init__.py                        # re-exports the three main abstractions
    entity/abstract_entity.py          # declarative base + PK convention
    repository/abstract_repository.py  # data-access object
    session/abstract_session_factory.py# engine + sessionmaker cache
    common/abstract_repositories_manager.py  # repository registry
    exception/
        unknown_repository_exception.py
        repository_session_missing_exception.py
    testing/sqlite.py                  # in-memory SQLite helper
```

## Integration in the Suite

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.

### Related Packages

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.

Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.

## Dependencies

- attrs: >=23.1.0
- psycopg: >=3.2
- sqlalchemy: <3,>=2
- wexample-helpers: >=20.0.0

## Versioning & Compatibility Policy

Wexample packages follow **Semantic Versioning** (SemVer):

- **MAJOR**: Breaking changes
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible

We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

Free to use in both personal and commercial projects.

## About us

[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.

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.

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.

## Known Limitations & Roadmap

Current limitations and planned features are tracked in the GitHub issues.

See the [project roadmap](https://github.com/wexample/python-orm/issues) for upcoming features and improvements.

## Status & Compatibility

**Maturity**: Production-ready

**Python Support**: >=3.10

**OS Support**: Linux, macOS, Windows

**Status**: Actively maintained

## Useful Links

- **Homepage**: https://github.com/wexample/python-orm
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
- **Issue Tracker**: https://github.com/wexample/python-orm/issues
- **Discussions**: https://github.com/wexample/python-orm/discussions
- **PyPI**: [pypi.org/project/wexample-orm](https://pypi.org/project/wexample-orm/)

## Migration Notes

When upgrading between major versions, refer to the migration guides in the documentation.

Breaking changes are clearly documented with upgrade paths and examples.
