Skip to main content

No project description provided

Project description

unitofwork

A lightweight, database-agnostic implementation of the Unit of Work pattern for Python applications. Designed for clean architecture, type safety, and atomic transactions across mixed repository types.

Features

  • Atomic Transactions: Ensure all operations succeed or fail together
  • Mixed Repository Support: Works with SQL, in-memory, file-based, and custom repositories
  • Type Safety: Full mypy support with generics and protocols
  • Simple API: Intuitive context manager interface
  • No Dependencies: Pure Python implementation
  • Comprehensive Testing: 100% test coverage with extensive test suite
  • Rollback Support: Automatic rollback for in-memory repositories
  • Protocol-Based: Uses structural typing with SupportsRollback interface

Installation

$ pip install unitofwork

Quick Start

import copy
from dataclasses import dataclass
from uuid import UUID, uuid4

from unitofwork import UnitOfWork

# Your repositories must implement the SupportsRollback interface
class InMemoryUserRepository:
    def __init__(self):
        self._users: dict[UUID, User] = {}
        self._snapshots: list[dict[UUID, User]] = []

    def checkpoint(self) -> dict[UUID, User]:
        snapshot = copy.deepcopy(self._users)
        self._snapshots.append(snapshot)
        return snapshot

    def restore(self, snapshot: dict[UUID, User]) -> None:
        self._users = snapshot

    def commit(self) -> None:
        self._snapshots.clear()

    def add(self, user: User) -> None:
        self._users[user.id] = user

@dataclass
class User:
    id: UUID
    name: str
    email: str

# Create repository that follows SupportsRollback interface
user_repo = InMemoryUserRepository()

# Atomic transaction
with UnitOfWork(user_repo) as uow:
    user = User(uuid4(), "Alice", "alice@example.com")
    uow.register_operation(lambda: user_repo.add(user))

# Operation is committed automatically on successful exit

SupportsRollback interface

All repositories must implement three essential methods:

from typing import Any, Protocol

class SupportsRollback(Protocol):
    """Protocol that all repositories must follow to work with UnitOfWork"""
    
    def checkpoint(self) -> Any:
        """Return a snapshot of the current state for potential rollback"""
        ...
    
    def restore(self, snapshot: Any) -> None:
        """Restore state from a previously taken snapshot"""
        ...
    
    def commit(self) -> None:
        """Finalize the transaction after successful operations"""
        ...

Implementing the interface

In-memory repository example

For in-memory repo we simply make a deep copy of the repo content before the transaction (checkpoint). On failure we can restore using this deep copy of the repo content as shown below:

class InMemoryRepository:
    def __init__(self):
        self._data = {}
        self._snapshots = []
    
    def checkpoint(self) -> dict:
        return copy.deepcopy(self._data)
    
    def restore(self, snapshot: dict) -> None:
        self._data = snapshot
    
    def commit(self) -> None:
        self._snapshots.clear()

SQL repository example

In case of an SQL repository, we can use SAVEPOINT to save the transaction checkpoint. It allows us to ROLLBACK to SAVEPOINT if the transaction fails.

from sqlalchemy.engine import Connection

# Example: SQLAlchemy repository
class SQLUserRepository:
    def __init__(self, connection: Connection):
        self.conn = connection
        self._savepoint = None
    
    def checkpoint(self) -> str:
        """Create a database savepoint"""
        if not self.conn.in_transaction():
            self.conn.begin()
        self._savepoint = f"savepoint_{id(self)}"
        self.conn.execute(f"SAVEPOINT {self._savepoint}")
        return self._savepoint
    
    def restore(self, savepoint: str) -> None:
        """Rollback to savepoint"""
        self.conn.execute(f"ROLLBACK TO SAVEPOINT {savepoint}")
    
    def commit(self) -> None:
        """Commit the transaction"""
        if self.conn.in_transaction():
            self.conn.commit()

Why Unit of Work?

The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.

Without Unit of Work

# Risk: Partial failures
user_repo.add(user)        # Success
product_repo.add(product)  # Failure - database error
# Now user exists but product doesn't - inconsistent state, not OK!

With Unit of Work

# Safe: Atomic operations
with UnitOfWork(user_repo, product_repo) as uow:
    uow.register_operation(lambda: user_repo.add(user))
    uow.register_operation(lambda: product_repo.add(product))
# Both succeed or both fail - guaranteed consistency, now it's OK!

Usage Guide

Basic Usage

from unitofwork import UnitOfWork

# Create repository with custom ID field
class Product:
    def __init__(self, sku: str, name: str, price: float):
        self.sku = sku
        self.name = name
        self.price = price

product_repo = InMemoryRepository[str, Product](id_field="sku")

# Simple transaction
with UnitOfWork(product_repo) as uow:
    uow.register_operation(
        lambda: product_repo.add(
            Product("laptop-123", "Premium Laptop", 1299.99),
        ),
    )

Mixed Repository Types

Provided you have multiple repositories implementing SupportsRollback interface, the same UnitOfWork pattern can be applied to all of them.

from unitofwork import UnitOfWork
from sqlalchemy.orm import Session
from your_app.repositories import SQLUserRepository, FileLogRepository

# Mix different repository types
sql_user_repo = SQLUserRepository(session)
in_memory_cache = InMemoryRepository[str, CachedData](id_field="key")
file_log_repo = FileLogRepository("/path/to/logs")

with UnitOfWork(sql_user_repo, in_memory_cache, file_log_repo) as uow:
    uow.register_operation(lambda: sql_user_repo.add_user(new_user))
    uow.register_operation(lambda: in_memory_cache.add(cached_data))
    uow.register_operation(lambda: file_log_repo.log_operation("user_created"))

Custom Repositories

from typing import Dict, Any
from unitofwork import SupportsRollback

class CustomRepository(SupportsRollback[str, str]):
    def __init__(self):
        self._data: Dict[str, str] = {}
    
    def checkpoint(self) -> Dict[str, str]:
        return self._data.copy()
    
    def restore(self, snapshot: Dict[str, str]) -> None:
        self._data = snapshot
    
    def add(self, key: str, value: str) -> None:
        self._data[key] = value
    
    def get(self, key: str) -> str:
        return self._data[key]

# Use your custom repository
custom_repo = CustomRepository()
with UnitOfWork(custom_repo) as uow:
    uow.register_operation(lambda: custom_repo.add("test_key", "test_value"))

Error handling

try:
    with UnitOfWork(user_repo, order_repo) as uow:
        uow.register_operation(lambda: user_repo.add(user))
        uow.register_operation(lambda: order_repo.add(order))
        
        # Simulate business rule violation
        if not user.can_purchase():
            raise ValueError("User cannot make purchase")
            
except ValueError as e:
    print(f"Transaction failed: {e}")
    # Both user_repo and order_repo are automatically rolled back!

Architecture

Core Components

  • UnitOfWork: Main coordinator class managing transactions
  • SupportsRollback: Protocol defining repository interface
  • UnitOfWorkError: Base exception for UnitOfWork errors
  • RollbackError: Exception raised when rollback fails partially

Design Principles

  • Database Agnostic: Works with any persistence mechanism
  • Type Safe: Full static type checking support
  • Minimal API: Simple, intuitive interface
  • Extensible: Easy to adapt existing repositories
  • Thread Safe: Designed for concurrent usage

Key Principle: Structural Typing

Your repositories do not need to inherit from SupportsRollback --- they just need to implement:

checkpoint() -> Any
restore(snapshot: Any) -> None
commit() -> None

This enables seamless integration with any persistence mechanism, provided it allows for creating a checkpoint and makes it technically possible to revert to this checkpoint.

Advanced Usage

Manual Rollback

with UnitOfWork(user_repo, product_repo) as uow:
    # These will commit if no exception
    uow.register_operation(lambda: user_repo.add(user))
    uow.register_operation(lambda: product_repo.add(product))
    
    # Manual rollback if needed
    if some_condition:
        uow.rollback()
        # Additional cleanup...

Acknowledgements

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unitofwork-0.1.1.tar.gz (13.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unitofwork-0.1.1-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

File details

Details for the file unitofwork-0.1.1.tar.gz.

File metadata

  • Download URL: unitofwork-0.1.1.tar.gz
  • Upload date:
  • Size: 13.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for unitofwork-0.1.1.tar.gz
Algorithm Hash digest
SHA256 116b9be732b6c18c6b8e05dc86822887f8da4c341ee3cdeeb2fccecb25e48bc2
MD5 f823bbf16dc7a04f633e07a849e07846
BLAKE2b-256 86325bd7aa220c0bf17a9ef2e3f28604966472b376434668bc041ef4c93757a6

See more details on using hashes here.

File details

Details for the file unitofwork-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: unitofwork-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 8.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for unitofwork-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b3bb50454724985b6339a447d6bd3a3c84b871a97ba1aca04415c9b1b98e94ac
MD5 aa36fc9d478f5732c145668c802ed4d0
BLAKE2b-256 dba67a6063b50806b73311c0f1dd980782cb3b90d3d8e02cd1a4922d27858172

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page