├── .gitignore ├── requirements.txt ├── exception ├── __init__.py ├── entity_not_found.py └── unexpected_entity.py ├── base_adaptor.py ├── __init__.py ├── README.md └── sqlalchemy_adaptor.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | SQLAlchemy==1.2.8 2 | -------------------------------------------------------------------------------- /exception/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /exception/entity_not_found.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | class EntityNotFoundException(BaseException): 4 | """ 5 | This should be thrown when the entity is not found in the repository 6 | """ 7 | pass 8 | 9 | -------------------------------------------------------------------------------- /exception/unexpected_entity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | class UnexpectedEntityException(BaseException): 4 | """ 5 | This should be thrown when an attempt is made to persist an entity using the 6 | wrong repository. 7 | """ 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /base_adaptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | class BaseAdaptor(object): 4 | """ 5 | Interface for entity storage services 6 | """ 7 | 8 | entity = NotImplementedError 9 | 10 | def get_by_id(self, entity_id): 11 | """ 12 | Get entity by id 13 | """ 14 | raise NotImplementedError 15 | 16 | def get_by_id_or_fail(self, entity_id): 17 | """ 18 | Get entity by id or raise EntityNotFoundException 19 | """ 20 | raise NotImplementedError 21 | 22 | def save(self, entity): 23 | """ 24 | Persist entity to storage 25 | """ 26 | raise NotImplementedError 27 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from functools import wraps 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import sessionmaker, scoped_session 6 | from sqlalchemy.ext.declarative import declarative_base 7 | 8 | engine = create_engine('sqlite:///db.sqlite') 9 | session_factory = sessionmaker(bind=engine) 10 | Session = scoped_session(session_factory) 11 | 12 | 13 | def get_session(): 14 | """ 15 | :return: Session 16 | """ 17 | return Session() 18 | 19 | 20 | def transaction(function): 21 | @wraps(function) 22 | def get_session_for_transaction(*args, **kwargs): 23 | session = get_session() 24 | try: 25 | result = function(*args, **kwargs) 26 | session.commit() 27 | return result 28 | except Exception as e: 29 | session.rollback() 30 | raise e 31 | finally: 32 | session.close() 33 | 34 | return get_session_for_transaction 35 | 36 | 37 | Base = declarative_base() 38 | 39 | 40 | def create_all(): 41 | """ 42 | Create database tables. 43 | """ 44 | Base.metadata.create_all(engine) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | A Python implementation of the DDD Repository pattern. An SQLAlchemy adaptor is provided. 3 | 4 | ## Usage 5 | Define models using the `declarative_base` provided in the module: 6 | ``` 7 | #!/usr/bin/env python3 8 | from sqlalchemy import Column, Integer, String 9 | 10 | import repository 11 | 12 | 13 | class User(repository.Base): 14 | __tablename__ = 'users' 15 | 16 | id = Column(Integer, primary_key=True, autoincrement=True) 17 | name = Column(String) 18 | fullname = Column(String) 19 | nickname = Column(String) 20 | 21 | def __repr__(self): 22 | return "" % ( 23 | self.name, self.fullname, self.nickname) 24 | 25 | def greet(self): 26 | return 'Hello, I am {}'.format(self.name) 27 | ``` 28 | 29 | Create a repository for that model by subclassing the SQLAlchemy Adaptor: 30 | ``` 31 | #!/usr/bin/env python3 32 | 33 | from domain.models.User import User 34 | from repository.SqlAlchemyAdaptor import SqlAlchemyAdaptor 35 | 36 | 37 | class UserRepository(SqlAlchemyAdaptor): 38 | 39 | entity = User 40 | 41 | def get_by_nickname(self, nickname): 42 | return self.session.query(self.entity).filter_by(nickname=nickname).first() 43 | 44 | ``` 45 | 46 | Use the model and repository in your application: 47 | ``` 48 | #!/usr/bin/env python3 49 | 50 | import repository 51 | 52 | from domain.models.User import User 53 | from domain.services.UserRepository import UserRepository 54 | 55 | repository.create_all() 56 | 57 | user_repository = UserRepository() 58 | 59 | user = User(name='Pseudo', fullname='Pseudo Nym', nickname='Nym') 60 | user_repository.save(user) 61 | user_again = user_repository.get_by_nickname('Nym') 62 | 63 | different_user = User(name='An', fullname='An Other User', nickname='Number 2') 64 | user_repository.save(different_user) 65 | different_user_again = user_repository.get_by_nickname('Number 2') 66 | 67 | 68 | print(user) 69 | print(user_again) 70 | print(user.greet(), ', ', different_user.greet()) 71 | 72 | print(different_user) 73 | print(different_user_again) 74 | print(user_again.greet(), ', ', different_user_again.greet()) 75 | ``` 76 | -------------------------------------------------------------------------------- /sqlalchemy_adaptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .base_adaptor import BaseAdaptor 4 | 5 | from .exception.entity_not_found import EntityNotFoundException 6 | from .exception.unexpected_entity import UnexpectedEntityException 7 | 8 | from . import get_session 9 | 10 | 11 | class SqlAlchemyAdaptor(BaseAdaptor): 12 | """ 13 | Persist entities using SQLAlchemy. 14 | 15 | Subclass this class, define an entity on the subclass, using the 16 | declarative_base provided in the module interface, and add your own methods 17 | for retrieval or specialised persistence. 18 | """ 19 | 20 | entity = NotImplementedError 21 | 22 | def __init__(self): 23 | """ 24 | Initialise the adaptor with a session 25 | """ 26 | self.session = get_session() 27 | 28 | def get_by_id(self, entity_id): 29 | """ 30 | Get the entity by id 31 | 32 | :param entity_id: int|str 33 | :return: entity 34 | """ 35 | return self.session.query(self.entity).get(entity_id) 36 | 37 | def get_by_id_or_fail(self, entity_id): 38 | """ 39 | Get the entity by id or throw exception 40 | 41 | :param entity_id: int|str 42 | :return: entity 43 | """ 44 | entity = self.get_by_id(entity_id) 45 | if not entity: 46 | raise EntityNotFoundException( 47 | '{} with id {} was not found.'.format( 48 | self.entity.__name__, 49 | entity_id 50 | ) 51 | ) 52 | 53 | return entity 54 | 55 | def get_all(self): 56 | return self.session.query(self.entity).all() 57 | 58 | def save(self, entity): 59 | """ 60 | Add the entity to the session, and commit. This follows the Aggregate 61 | pattern. 62 | 63 | :param entity: entity 64 | """ 65 | if not isinstance(entity, self.entity): 66 | raise UnexpectedEntityException( 67 | '{} is not a {}'.format( 68 | entity.__class__.__name__, 69 | self.entity.__name__ 70 | ) 71 | ) 72 | 73 | self.session.add(entity) 74 | 75 | def remove(self, entity): 76 | """ 77 | Delete a specific entity. 78 | 79 | :param entity: 80 | :return: 81 | """ 82 | self.session.delete(entity) 83 | --------------------------------------------------------------------------------