├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── main.py ├── orm ├── __init__.py ├── database.py └── repositories.py ├── requirements.txt ├── schemas.py ├── service_layer ├── __init__.py └── uow.py ├── settings.py └── utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/tasks.json 4 | !.vscode/launch.json 5 | !.vscode/extensions.json 6 | !.vscode/*.code-snippets 7 | 8 | # Local History for Visual Studio Code 9 | .history/ 10 | 11 | # Built Visual Studio Code Extensions 12 | *.vsix 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | cover/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # poetry 111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 112 | # This is especially recommended for binary packages to ensure reproducibility, and is more 113 | # commonly ignored for libraries. 114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 115 | #poetry.lock 116 | 117 | # pdm 118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 119 | #pdm.lock 120 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 121 | # in version control. 122 | # https://pdm.fming.dev/#use-with-ide 123 | .pdm.toml 124 | 125 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 126 | __pypackages__/ 127 | 128 | # Celery stuff 129 | celerybeat-schedule 130 | celerybeat.pid 131 | 132 | # SageMath parsed files 133 | *.sage.py 134 | 135 | # Environments 136 | .env 137 | .venv 138 | env/ 139 | venv/ 140 | ENV/ 141 | env.bak/ 142 | venv.bak/ 143 | 144 | # Spyder project settings 145 | .spyderproject 146 | .spyproject 147 | 148 | # Rope project settings 149 | .ropeproject 150 | 151 | # mkdocs documentation 152 | /site 153 | 154 | # mypy 155 | .mypy_cache/ 156 | .dmypy.json 157 | dmypy.json 158 | 159 | # Pyre type checker 160 | .pyre/ 161 | 162 | # pytype static type analyzer 163 | .pytype/ 164 | 165 | # Cython debug symbols 166 | cython_debug/ 167 | 168 | # PyCharm 169 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 170 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 171 | # and can be added to the global gitignore or merged into this file. For a more nuclear 172 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 173 | #.idea/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "main.py", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Manuel Kanetscheider 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLModel Repository pattern 2 | This repository includes a example implementation of the Unit of Work design and Repository design pattern. 3 | 4 | For more details please check out my [blog post](https://dev.to/manukanne/a-python-implementation-of-the-unit-of-work-and-repository-design-pattern-using-sqlmodel-3mb5)! 5 | 6 | ```mermaid 7 | --- 8 | title: Unit of Work + Repository pattern 9 | --- 10 | classDiagram 11 | direction LR 12 | GenericRepository <|--GenericSqlRepository 13 | GenericRepository <|-- HeroReposityBase 14 | GenericRepository <|-- TeamRepositoryBase 15 | 16 | GenericSqlRepository <|-- TeamRepository 17 | TeamRepositoryBase <|-- TeamRepository 18 | GenericSqlRepository <|-- HeroRepository 19 | HeroReposityBase <|-- HeroRepository 20 | 21 | UnitOfWorkBase <|-- UnitOfWork 22 | HeroReposityBase *-- UnitOfWorkBase 23 | TeamRepositoryBase *-- UnitOfWorkBase 24 | 25 | HeroRepository *--UnitOfWork 26 | TeamRepository *--UnitOfWork 27 | 28 | class GenericRepository~SQLModel~{ 29 | +get_by_id(int id) SQLModel 30 | +list(**filters) List~SQLModel~ 31 | +add(T record) SQLModel 32 | +update(T recored) SQLModel 33 | +delete(id int) 34 | } 35 | 36 | class GenericSqlRepository~SQLModel~{ 37 | -_construct_list_stmt(id) 38 | -_construct_list_stmt(**filters) 39 | } 40 | class HeroReposityBase{ 41 | +get_by_name(str name) Hero 42 | } 43 | class HeroRepository{ 44 | 45 | } 46 | class TeamRepositoryBase{ 47 | +get_by_name(str name) Team 48 | } 49 | class TeamRepository{ 50 | } 51 | class UnitOfWorkBase{ 52 | teams: TeamRepositoryBase 53 | heroes: HeroReposityBase 54 | + __enter__() 55 | + __exit__() 56 | + commit() 57 | + rollback() 58 | } 59 | class UnitOfWork{ 60 | teams: TeamRepository 61 | heroes: HeroRepository 62 | } 63 | 64 | ``` 65 | # Getting Started 66 | ## Tech-stack 67 | - Python 3.10 68 | - [SQLModel](https://sqlmodel.tiangolo.com/) 69 | - [Pydantic](https://docs.pydantic.dev/latest/) 70 | - [SQLAlchemy](https://www.sqlalchemy.org/) 71 | 72 | ## Setup 73 | ### 1. Clone the project: 74 | ```bash 75 | git clone https://github.com/manukanne/sqlmodel-repository-pattern.git 76 | ``` 77 | 78 | ### 2. Install the requirements: 79 | ```bash 80 | python -m pip install -r requirements.txt 81 | ``` 82 | 83 | ### 3. Create the .env file: 84 | ``` 85 | DATABASE_CONNECTION_STR=sqlite:// 86 | ``` 87 | > :information_source: For this project an in-memory SQLite database was used, of course another database supported by [SQLAlchemy](https://docs.sqlalchemy.org/en/20/dialects/) can be used. 88 | 89 | # Executing the programm 90 | In order to run the program, simple execute the following command: 91 | ```bash 92 | python main.py 93 | ``` 94 | 95 | # Authors 96 | [Manuel Kanetscheider](https://dev.to/manukanne) 97 | 98 | # License 99 | This project is licensed under the MIT permissive license - see the [Licence file](LICENSE) for details. 100 | 101 | # Acknowledgments 102 | - [Architecture Patterns with Python](https://www.oreilly.com/library/view/architecture-patterns-with/9781492052197/) 103 | - [Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application) 104 | - [SQLModel](https://sqlmodel.tiangolo.com/) 105 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from sqlmodel import SQLModel 2 | from sqlmodel.pool import StaticPool 3 | 4 | from settings import Settings 5 | from orm.database import create_sqlmodel_engine, sqlmodel_session_maker 6 | from service_layer.uow import UnitOfWork 7 | from schemas import Hero, Team 8 | 9 | 10 | settings = Settings() 11 | engine = create_sqlmodel_engine(settings=settings, poolclass=StaticPool) 12 | SQLModel.metadata.create_all(engine) 13 | session_maker = sqlmodel_session_maker(engine) 14 | 15 | 16 | def print_team_members(team: Team): 17 | for member in team.heroes: 18 | print(f"Name {member.name}, Secret name {member.secret_name}") 19 | 20 | 21 | with UnitOfWork(session_factory=session_maker) as uow: 22 | team = Team(name="Dream Team") 23 | team = uow.teams.add(team) 24 | 25 | uow.heroes.add(Hero(name="Toni Stark", secret_name="Iron Man", team_id=team.id)) 26 | uow.heroes.add(Hero(name="Bruce Banner", secret_name="Hulk", team_id=team.id)) 27 | uow.heroes.add(Hero(name="Steve Rogers", secret_name="Captain America", team_id=team.id)) 28 | uow.heroes.add(Hero(name="John Wick", secret_name="John Wick", team_id=team.id)) 29 | 30 | print("-------------------------- Print dream team ----------------------------------") 31 | print_team_members(team) 32 | hero_hulk = uow.heroes.get_by_name("Bruce Banner") 33 | hero_hulk.secret_name = "Incredible Hulk" 34 | hero_hulk = uow.heroes.update(hero_hulk) 35 | 36 | print("-------------------------- Print updated dream team ----------------------------------") 37 | print_team_members(team) 38 | 39 | hero_john = uow.heroes.list(secret_name="John Wick")[0] 40 | hero_john.team_id = None 41 | uow.heroes.update(hero_john) 42 | 43 | print("-------------------------- Remove member from dream team ----------------------------------") 44 | uow.heroes.delete(hero_john.id) 45 | hero_john = uow.heroes.get_by_id(hero_john.id) 46 | if hero_john is None: 47 | print("Deleted John Wick") 48 | uow.commit() 49 | print_team_members(team) 50 | -------------------------------------------------------------------------------- /orm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manukanne/sqlmodel-repository-pattern/fe792b8a39b551696ac8bdc24dac3655f3a12f40/orm/__init__.py -------------------------------------------------------------------------------- /orm/database.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from sqlmodel import create_engine, Session 4 | 5 | from settings import Settings 6 | 7 | 8 | def create_sqlmodel_engine(settings: Settings, **kwargs): 9 | """Creates a SQLModel engine. 10 | 11 | Args: 12 | settings (Settings): Application settings. 13 | **kwargs: Engine parameters. 14 | 15 | Returns: 16 | Engine: SQLModel engine. 17 | """ 18 | return create_engine(settings.database_connection_str, **kwargs) 19 | 20 | 21 | def sqlmodel_session_maker(engine) -> Callable[[], Session]: 22 | """Returns a SQLModel session maker function. 23 | 24 | Args: 25 | engine (_type_): SQLModel engine. 26 | 27 | Returns: 28 | Callable[[], Session]: Session maker function. 29 | """ 30 | return lambda: Session(bind=engine, autocommit=False, autoflush=False) 31 | -------------------------------------------------------------------------------- /orm/repositories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Generic, TypeVar, Type, Optional, List 3 | 4 | from sqlmodel import Session, select, and_ 5 | from sqlmodel.sql.expression import SelectOfScalar 6 | 7 | from schemas import BaseModel, Hero, Team 8 | 9 | T = TypeVar("T", bound=BaseModel) 10 | 11 | 12 | class GenericRepository(Generic[T], ABC): 13 | """Generic base repository. 14 | """ 15 | 16 | @abstractmethod 17 | def get_by_id(self, id: int) -> Optional[T]: 18 | """Get a single record by id. 19 | 20 | Args: 21 | id (int): Record id. 22 | 23 | Returns: 24 | Optional[T]: Record or none. 25 | """ 26 | raise NotImplementedError() 27 | 28 | @abstractmethod 29 | def list(self, **filters) -> List[T]: 30 | """Gets a list of records 31 | 32 | Args: 33 | **filters: Filter conditions, several criteria are linked with a logical 'and'. 34 | 35 | Raises: 36 | ValueError: Invalid filter condition. 37 | 38 | Returns: 39 | List[T]: List of records. 40 | """ 41 | raise NotImplementedError() 42 | 43 | @abstractmethod 44 | def add(self, record: T) -> T: 45 | """Creates a new record. 46 | 47 | Args: 48 | record (T): The record to be created. 49 | 50 | Returns: 51 | T: The created record. 52 | """ 53 | raise NotImplementedError() 54 | 55 | @abstractmethod 56 | def update(self, record: T) -> T: 57 | """Updates an existing record. 58 | 59 | Args: 60 | record (T): The record to be updated incl. record id. 61 | 62 | Returns: 63 | T: The updated record. 64 | """ 65 | raise NotImplementedError() 66 | 67 | @abstractmethod 68 | def delete(self, id: int) -> None: 69 | """Deletes a record by id. 70 | 71 | Args: 72 | id (int): Record id. 73 | """ 74 | raise NotImplementedError() 75 | 76 | 77 | class GenericSqlRepository(GenericRepository[T], ABC): 78 | """Generic SQL Repository. 79 | """ 80 | 81 | def __init__(self, session: Session, model_cls: Type[T]) -> None: 82 | """Creates a new repository instance. 83 | 84 | Args: 85 | session (Session): SQLModel session. 86 | model_cls (Type[T]): SQLModel class type. 87 | """ 88 | self._session = session 89 | self._model_cls = model_cls 90 | 91 | def _construct_get_stmt(self, id: int) -> SelectOfScalar: 92 | """Creates a SELECT query for retrieving a single record. 93 | 94 | Args: 95 | id (int): Record id. 96 | 97 | Returns: 98 | SelectOfScalar: SELECT statement. 99 | """ 100 | stmt = select(self._model_cls).where(self._model_cls.id == id) 101 | return stmt 102 | 103 | def get_by_id(self, id: int) -> Optional[T]: 104 | stmt = self._construct_get_stmt(id) 105 | return self._session.exec(stmt).first() 106 | 107 | def _construct_list_stmt(self, **filters) -> SelectOfScalar: 108 | """Creates a SELECT query for retrieving a multiple records. 109 | 110 | Raises: 111 | ValueError: Invalid column name. 112 | 113 | Returns: 114 | SelectOfScalar: SELECT statment. 115 | """ 116 | stmt = select(self._model_cls) 117 | where_clauses = [] 118 | for c, v in filters.items(): 119 | if not hasattr(self._model_cls, c): 120 | raise ValueError(f"Invalid column name {c}") 121 | where_clauses.append(getattr(self._model_cls, c) == v) 122 | 123 | if len(where_clauses) == 1: 124 | stmt = stmt.where(where_clauses[0]) 125 | elif len(where_clauses) > 1: 126 | stmt = stmt.where(and_(*where_clauses)) 127 | return stmt 128 | 129 | def list(self, **filters) -> List[T]: 130 | stmt = self._construct_list_stmt(**filters) 131 | return self._session.exec(stmt).all() 132 | 133 | def add(self, record: T) -> T: 134 | self._session.add(record) 135 | self._session.flush() 136 | self._session.refresh(record) 137 | return record 138 | 139 | def update(self, record: T) -> T: 140 | self._session.add(record) 141 | self._session.flush() 142 | self._session.refresh(record) 143 | return record 144 | 145 | def delete(self, id: int) -> None: 146 | record = self.get_by_id(id) 147 | if record is not None: 148 | self._session.delete(record) 149 | self._session.flush() 150 | 151 | 152 | class HeroReposityBase(GenericRepository[Hero], ABC): 153 | """Hero repository. 154 | """ 155 | @abstractmethod 156 | def get_by_name(self, name: str) -> Optional[Hero]: 157 | raise NotImplementedError() 158 | 159 | 160 | class HeroRepository(GenericSqlRepository[Hero], HeroReposityBase): 161 | def __init__(self, session: Session) -> None: 162 | super().__init__(session, Hero) 163 | 164 | def get_by_name(self, name: str) -> Optional[Hero]: 165 | stmt = select(Hero).where(Hero.name == name) 166 | return self._session.exec(stmt).first() 167 | 168 | 169 | class TeamRepositoryBase(GenericRepository[Team], ABC): 170 | """Team repository. 171 | """ 172 | @abstractmethod 173 | def get_by_name(self, name: str) -> Optional[Team]: 174 | raise NotImplementedError() 175 | 176 | 177 | class TeamRepository(GenericSqlRepository[Team], TeamRepositoryBase): 178 | def __init__(self, session: Session) -> None: 179 | super().__init__(session, Team) 180 | 181 | def get_by_name(self, name: str) -> Optional[Team]: 182 | stmt = select(Team).where(Team.name == name) 183 | return self._session.exec(stmt).first() 184 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manukanne/sqlmodel-repository-pattern/fe792b8a39b551696ac8bdc24dac3655f3a12f40/requirements.txt -------------------------------------------------------------------------------- /schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from sqlmodel import SQLModel, Field, Column, Integer, String, ForeignKey, Relationship 4 | 5 | from utilities import to_camel 6 | 7 | 8 | class BaseModel(SQLModel): 9 | """Base SQL model class. 10 | """ 11 | 12 | id: Optional[int] = Field(sa_column=Column("Id", Integer, primary_key=True, autoincrement=True)) 13 | 14 | class Config: 15 | alias_generator = to_camel 16 | allow_population_by_field_name = True 17 | arbitrary_types_allowed = True 18 | 19 | 20 | class Hero(BaseModel, table=True): 21 | """Hero table. 22 | """ 23 | __tablename__ = "Hero" 24 | 25 | name: str = Field(sa_column=Column("Name", String(30), nullable=False)) 26 | secret_name: str = Field(sa_column=Column("SecretName", String(30), nullable=False)) 27 | age: Optional[int] = Field(sa_column=Column("Age", Integer, nullable=True, default=None)) 28 | team_id: Optional[int] = Field(sa_column=Column("TeamId", Integer, ForeignKey("Team.Id"))) 29 | 30 | team: Optional["Team"] = Relationship(back_populates="heroes") 31 | 32 | 33 | class Team(BaseModel, table=True): 34 | """Team table. 35 | """ 36 | __tablename__ = "Team" 37 | 38 | name: str = Field(sa_column=Column("Name", String(30), nullable=False, unique=True)) 39 | 40 | heroes: List["Hero"] = Relationship(back_populates="team") 41 | -------------------------------------------------------------------------------- /service_layer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manukanne/sqlmodel-repository-pattern/fe792b8a39b551696ac8bdc24dac3655f3a12f40/service_layer/__init__.py -------------------------------------------------------------------------------- /service_layer/uow.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Callable 3 | 4 | from orm.repositories import ( 5 | HeroReposityBase, 6 | TeamRepositoryBase, 7 | HeroRepository, 8 | TeamRepository 9 | ) 10 | 11 | from sqlmodel import Session 12 | 13 | 14 | class UnitOfWorkBase(ABC): 15 | """Unit of work. 16 | """ 17 | 18 | heroes: HeroReposityBase 19 | teams: TeamRepositoryBase 20 | 21 | def __enter__(self): 22 | return self 23 | 24 | def __exit__(self, exc_type, exc_value, traceback): 25 | self.rollback() 26 | 27 | @abstractmethod 28 | def commit(self): 29 | """Commits the current transaction. 30 | """ 31 | raise NotImplementedError() 32 | 33 | @abstractmethod 34 | def rollback(self): 35 | """Rollbacks the current transaction. 36 | """ 37 | raise NotImplementedError() 38 | 39 | 40 | class UnitOfWork(UnitOfWorkBase): 41 | def __init__(self, session_factory: Callable[[], Session]) -> None: 42 | """Creates a new uow instance. 43 | 44 | Args: 45 | session_factory (Callable[[], Session]): Session maker function. 46 | """ 47 | self._session_factory = session_factory 48 | 49 | def __enter__(self): 50 | self._session = self._session_factory() 51 | self.heroes = HeroRepository(self._session) 52 | self.teams = TeamRepository(self._session) 53 | return super().__enter__() 54 | 55 | def commit(self): 56 | self._session.commit() 57 | 58 | def rollback(self): 59 | self._session.rollback() 60 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings, Field 2 | 3 | 4 | class Settings(BaseSettings): 5 | """Application settings class, holds all app settings. 6 | """ 7 | database_connection_str: str = Field(..., env="DATABASE_CONNECTION_STR") 8 | 9 | class Config: 10 | env_prefix = "" 11 | case_sensitive = False 12 | env_file = ".env" 13 | env_file_encoding = "utf-8" 14 | -------------------------------------------------------------------------------- /utilities.py: -------------------------------------------------------------------------------- 1 | from re import sub 2 | 3 | def to_camel(s: str) -> str: 4 | """Converts an input string to a camel string. 5 | 6 | Args: 7 | s (str): Input string. 8 | 9 | Returns: 10 | str: Camel string. 11 | """ 12 | ret = sub(r"(_|-)+", " ", s).title().replace(" ", "") 13 | return ''.join([ret[0].lower(), ret[1:]]) --------------------------------------------------------------------------------