├── .flake8 ├── .github └── workflows │ ├── format.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── fastapi-sqlalchemy_examples.postman_collection.json ├── legacy │ ├── app.py │ └── models.py ├── multi_db │ ├── app.py │ └── models │ │ ├── __init__.py │ │ ├── posts.py │ │ └── users.py └── single_db │ ├── app.py │ └── models.py ├── fastapi_sqlalchemy ├── __init__.py ├── decorators.py ├── exceptions.py ├── extensions.py ├── middleware.py ├── py.typed ├── sqlalchemy_types.py └── types.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py └── test_session.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv 3 | max-line-length = 100 4 | extend-ignore = E203 -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format Code 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - development 8 | 9 | jobs: 10 | format-python: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: 3.11 21 | 22 | - name: Install dependencies 23 | run: pip install black 24 | 25 | - name: Format code with Black 26 | run: black . 27 | 28 | - name: Commit changes 29 | run: | 30 | git config --local user.email "action@github.com" 31 | git config --local user.name "GitHub Action" 32 | git commit -am "Auto-format Python code with Black" 33 | git push 34 | 35 | - name: Complete 36 | run: echo "Code formatting complete!" 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | create-pypi-release: 9 | runs-on: ubuntu-latest 10 | 11 | name: Build and publish package for Python >=3.7 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.11' 21 | 22 | - name: Install poetry 23 | run: | 24 | pip install poetry 25 | 26 | - name: Build the distribution 27 | run: poetry build 28 | 29 | - name: Publish Package 30 | run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} 31 | 32 | # create-gh-releases-release: 33 | # name: create-gh-releases-release 34 | # runs-on: ubuntu-latest 35 | # env: 36 | # VERSION: 37 | # steps: 38 | # - name: Checkout repository 39 | # uses: actions/checkout@v2 40 | 41 | # - name: Get the version 42 | # run: echo ::set-env name=VERSION::${GITHUB_REF#refs/tags/} 43 | 44 | # - name: Create release 45 | # env: 46 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | # uses: actions/create-release@v1 48 | # with: 49 | # tag_name: ${{ env.VERSION }} 50 | # release_name: ${{ env.VERSION }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build/ 3 | .vscode/ 4 | __pycache__/ 5 | .pytest_cache/ 6 | .venv/ 7 | .env 8 | .coverage 9 | htmlcov/ 10 | test.py 11 | *.egg-info 12 | coverage.xml 13 | *.db 14 | *env*/ 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | cover/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | db.sqlite3 76 | db.sqlite3-journal 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | .pybuilder/ 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | # For a library or package, you might want to ignore these files since the code is 101 | # intended to run in multiple environments; otherwise, check them in: 102 | # .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # poetry 112 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 113 | # This is especially recommended for binary packages to ensure reproducibility, and is more 114 | # commonly ignored for libraries. 115 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 116 | #poetry.lock 117 | 118 | # pdm 119 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 120 | #pdm.lock 121 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 122 | # in version control. 123 | # https://pdm.fming.dev/#use-with-ide 124 | .pdm.toml 125 | 126 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 127 | __pypackages__/ 128 | 129 | # Celery stuff 130 | celerybeat-schedule 131 | celerybeat.pid 132 | 133 | # SageMath parsed files 134 | *.sage.py 135 | 136 | # Environments 137 | .env 138 | .venv 139 | env/ 140 | venv/ 141 | ENV/ 142 | env.bak/ 143 | venv.bak/ 144 | 145 | # Spyder project settings 146 | .spyderproject 147 | .spyproject 148 | 149 | # Rope project settings 150 | .ropeproject 151 | 152 | # mkdocs documentation 153 | /site 154 | 155 | # mypy 156 | .mypy_cache/ 157 | .dmypy.json 158 | dmypy.json 159 | 160 | # Pyre type checker 161 | .pyre/ 162 | 163 | # pytype static type analyzer 164 | .pytype/ 165 | 166 | # Cython debug symbols 167 | cython_debug/ 168 | 169 | # PyCharm 170 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 171 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 172 | # and can be added to the global gitignore or merged into this file. For a more nuclear 173 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 174 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael Freeborn 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 | 2 | 3 | [![image](https://img.shields.io/pypi/v/FastAPI-SQLAlchemy?color=blue)](https://pypi.org/project/FastAPI-SQLAlchemy/) 4 | 5 | FastAPI-SQLAlchemy provides a simple integration between 6 | [FastAPI](https://github.com/tiangolo/fastapi) and 7 | [SQLAlchemy](https://github.com/pallets/flask-sqlalchemy) in your 8 | application. It gives access to useful helpers to facilitate the 9 | completion of common tasks. 10 | 11 | # Installing 12 | Install and update using 13 | [pip](https://pip.pypa.io/en/stable/quickstart/): 14 | ``` text 15 | $ pip install fastapi-sqlalchemy 16 | ``` 17 | # Examples 18 | ## Models definition 19 | ``` python 20 | from sqlalchemy import Column, Integer, String, create_engine 21 | from sqlalchemy.orm import DeclarativeMeta, declarative_base, sessionmaker 22 | 23 | from fastapi_sqlalchemy import SQLAlchemy 24 | db = SQLAlchemy(url="sqlite:///example.db") 25 | #Define User class 26 | class User(db.Base): 27 | __tablename__ = "items" 28 | 29 | id = Column(Integer, primary_key=True) 30 | name = Column(String) 31 | email = Column(String) 32 | 33 | def __repr__(self): 34 | return f"User(id={self.id}, name='{self.name}',email='{self.email}')" 35 | ``` 36 | ## Usage inside of a route 37 | ``` python 38 | from fastapi import FastAPI 39 | from models import User, db 40 | from pydantic import BaseModel 41 | 42 | from fastapi_sqlalchemy import DBSessionMiddleware 43 | 44 | app = FastAPI() 45 | 46 | # Add SQLAlchemy session middleware to manage database sessions 47 | app.add_middleware(DBSessionMiddleware, db=db) 48 | 49 | 50 | # Endpoint to retrieve all users 51 | @app.get("/users") 52 | def get_users(): 53 | """ 54 | Retrieve a list of all users. 55 | 56 | Returns: 57 | List[User]: A list of User objects. 58 | """ 59 | return User.query.all() 60 | 61 | 62 | # Pydantic model for creating new users 63 | class UserCreate(BaseModel): 64 | name: str 65 | email: str 66 | 67 | 68 | # Endpoint to add a new user 69 | @app.post("/add_user") 70 | def add_user(user_data: UserCreate): 71 | """ 72 | Add a new user to the database. 73 | 74 | Args: 75 | user_data (UserCreate): User data including name and email. 76 | 77 | Returns: 78 | dict: A message indicating the success of the operation. 79 | """ 80 | user = User(**user_data.model_dump()) 81 | print(user) 82 | user.save() 83 | return {"message": "User created successfully"} 84 | 85 | ``` 86 | You can initialize the SQLAlchemy() class similar to the way 87 | flask-sqlalchemy, this allows for multiple database connections to work 88 | at the same time. 89 | ## Usage outside of a route 90 | 91 | Sometimes it is useful to be able to access the database outside the 92 | context of a request, such as in scheduled tasks which run in the 93 | background: 94 | 95 | ``` python 96 | import pytz 97 | from apscheduler.schedulers.asyncio import AsyncIOScheduler # other schedulers are available 98 | from fastapi import FastAPI 99 | from models import User, db 100 | from fastapi_sqlalchemy import DBSessionMiddleware 101 | 102 | app = FastAPI() 103 | 104 | app.add_middleware(DBSessionMiddleware, db_url="sqlite:///example.db") 105 | 106 | 107 | @app.on_event('startup') 108 | async def startup_event(): 109 | scheduler = AsyncIOScheduler(timezone=pytz.utc) 110 | scheduler.start() 111 | scheduler.add_job(count_users_task, "cron", hour=0) # runs every night at midnight 112 | 113 | 114 | def count_users_task(): 115 | """Count the number of users in the database and save it into the user_counts table.""" 116 | 117 | # we are outside of a request context, therefore we cannot rely on ``DBSessionMiddleware`` 118 | # to create a database session for us. Instead, we can use the same ``db`` object and 119 | # use it as a context manager, like so: 120 | 121 | with db(): 122 | user_count = User.query.count() 123 | 124 | user_count = UserCount(user_count) 125 | user_count.save() 126 | 127 | # no longer able to access a database session once the db() context manager has ended 128 | 129 | return users 130 | ``` 131 | ## Custom Model Base 132 | You can define custom BaseModels, or extend the built in ModelBase to provide extended shared functionality for you database models. 133 | ```python 134 | import inspect 135 | from typing import List 136 | 137 | from sqlalchemy import Column 138 | 139 | from fastapi_sqlalchemy import ModelBase 140 | 141 | 142 | class BaseModel(ModelBase): 143 | @classmethod 144 | def new(cls, **kwargs): 145 | obj = cls(**kwargs) 146 | obj.save() 147 | return obj 148 | 149 | @classmethod 150 | def get(cls, **kwargs): 151 | result: cls = cls.query.filter_by(**kwargs).first() 152 | return result 153 | 154 | @classmethod 155 | def get_all(cls, **kwargs): 156 | result: List[cls] = cls.query.filter_by(**kwargs).all() 157 | return result 158 | 159 | def update(self, **kwargs): 160 | for column, value in kwargs.items(): 161 | setattr(self, column, value) 162 | 163 | self.save() 164 | return self 165 | 166 | ``` 167 | As you can see the above BaseModel class adds support for various common functions and operations. 168 | ## Complete examples 169 | 170 | - [Using single database](examples/single_db/) 171 | 172 | - [Using multiple databases](examples/multi_db/) 173 | 174 | - [Legacy method](examples/legacy/) 175 | # Legacy Examples 176 | 177 | ## Models definition 178 | Note the only change that you need to make is to add the db.Base inheritance to each of your 179 | model classes 180 | ``` python 181 | from sqlalchemy import Column, Integer, String, create_engine 182 | from sqlalchemy.orm import declarative_base, sessionmaker 183 | 184 | from fastapi_sqlalchemy import ModelBase, SQLAlchemy 185 | 186 | db = SQLAlchemy(url="sqlite:///example.db") 187 | 188 | 189 | # Define the User class representing the "users" database table 190 | # Using the SQLAlchemy Base property instead of defining your own 191 | # And inheriting from the BaseModel class for type hinting and helpful builtin methods and properties 192 | class User(ModelBase, db.Base): 193 | __tablename__ = "users" 194 | 195 | id = Column(Integer, primary_key=True) 196 | name = Column(String) 197 | email = Column(String) 198 | 199 | def __repr__(self): 200 | return f"User(id={self.id}, name='{self.name}',email='{self.email}')" 201 | 202 | ``` 203 | ## Usage inside of a route 204 | 205 | ``` python 206 | from fastapi import FastAPI 207 | from fastapi_sqlalchemy import DBSessionMiddleware # middleware helper 208 | from fastapi_sqlalchemy import db # an object to provide global access to a database session 209 | 210 | from app.models import User 211 | 212 | app = FastAPI() 213 | 214 | app.add_middleware(DBSessionMiddleware, db_url="sqlite:///example.db") 215 | 216 | # once the middleware is applied, any route can then access the database session 217 | # from the global ``db`` 218 | 219 | @app.get("/users") 220 | def get_users(): 221 | users = db.session.query(User).all() 222 | 223 | return users 224 | ``` 225 | 226 | Note that the session object provided by `db.session` is based on the 227 | Python3.7+ `ContextVar`. This means that each session is linked to the 228 | individual request context in which it was created. 229 | 230 | ## Usage outside of a route 231 | 232 | Sometimes it is useful to be able to access the database outside the 233 | context of a request, such as in scheduled tasks which run in the 234 | background: 235 | 236 | ``` python 237 | import pytz 238 | from apscheduler.schedulers.asyncio import AsyncIOScheduler # other schedulers are available 239 | from fastapi import FastAPI 240 | from fastapi_sqlalchemy import db 241 | 242 | from app.models import User, UserCount 243 | 244 | app = FastAPI() 245 | 246 | app.add_middleware(DBSessionMiddleware, db_url="sqlite:///example.db") 247 | 248 | 249 | @app.on_event('startup') 250 | async def startup_event(): 251 | scheduler = AsyncIOScheduler(timezone=pytz.utc) 252 | scheduler.start() 253 | scheduler.add_job(count_users_task, "cron", hour=0) # runs every night at midnight 254 | 255 | 256 | def count_users_task(): 257 | """Count the number of users in the database and save it into the user_counts table.""" 258 | 259 | # we are outside of a request context, therefore we cannot rely on ``DBSessionMiddleware`` 260 | # to create a database session for us. Instead, we can use the same ``db`` object and 261 | # use it as a context manager, like so: 262 | 263 | with db(): 264 | user_count = db.session.query(User).count() 265 | 266 | db.session.add(UserCount(user_count)) 267 | db.session.commit() 268 | 269 | # no longer able to access a database session once the db() context manager has ended 270 | 271 | return users 272 | ``` 273 | -------------------------------------------------------------------------------- /examples/fastapi-sqlalchemy_examples.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "eeaa761a-e9ab-4fa7-bc41-23b54b5c6a45", 4 | "name": "fastapi-sqlalchemy examples", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "21601362" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Single database", 11 | "item": [{ 12 | "name": "Get All Users", 13 | "request": { 14 | "method": "GET", 15 | "header": [], 16 | "body": {}, 17 | "url": { 18 | "raw": "{{base_url}}/users", 19 | "protocol": "http", 20 | "host": ["{{host}}"], 21 | "path": ["users"] 22 | } 23 | }, 24 | "response": [] 25 | }, 26 | { 27 | "name": "Add New User", 28 | "request": { 29 | "method": "POST", 30 | "header": [ 31 | { 32 | "key": "Content-Type", 33 | "value": "application/json" 34 | } 35 | ], 36 | "body": { 37 | "mode": "raw", 38 | "raw": "{\"name\": \"John Doe\",\"email\": \"john@example.com\"}" 39 | }, 40 | "url": { 41 | "raw": "{{base_url}}/add_user", 42 | "protocol": "http", 43 | "host": ["{{host}}"], 44 | "path": ["add_user"] 45 | } 46 | }, 47 | "response": [] 48 | }, 49 | { 50 | "name": "Update User", 51 | "request": { 52 | "method": "POST", 53 | "header": [ 54 | { 55 | "key": "Content-Type", 56 | "value": "application/json" 57 | } 58 | ], 59 | "body": { 60 | "mode": "raw", 61 | "raw": "{\"id\": 1, \"name\": \"Updated Name\",\"email\": \"updated@example.com\"}" 62 | }, 63 | "url": { 64 | "raw": "{{base_url}}/update_user", 65 | "protocol": "http", 66 | "host": ["{{host}}"], 67 | "path": ["update_user"] 68 | } 69 | }, 70 | "response": [] 71 | }] 72 | }, 73 | { 74 | "name": "Multi Database", 75 | "item": [{ 76 | "name": "Retrieve All Users", 77 | "request": { 78 | "method": "GET", 79 | "header": [], 80 | "url": { 81 | "raw": "{{base_url}}/users", 82 | "protocol": "http", 83 | "host": [ 84 | "{{host}}" 85 | ], 86 | "path": [ 87 | "users" 88 | ] 89 | } 90 | }, 91 | "response": [] 92 | }, 93 | { 94 | "name": "Add User", 95 | "request": { 96 | "method": "POST", 97 | "header": [ 98 | { 99 | "key": "Content-Type", 100 | "value": "application/json" 101 | } 102 | ], 103 | "body": { 104 | "mode": "raw", 105 | "raw": { 106 | "name": "John Doe", 107 | "email": "john@example.com" 108 | } 109 | }, 110 | "url": { 111 | "raw": "{{base_url}}/add_user", 112 | "protocol": "http", 113 | "host": [ 114 | "{{host}}" 115 | ], 116 | "path": [ 117 | "add_user" 118 | ] 119 | } 120 | }, 121 | "response": [] 122 | }, 123 | { 124 | "name": "Update User", 125 | "request": { 126 | "method": "POST", 127 | "header": [ 128 | { 129 | "key": "Content-Type", 130 | "value": "application/json" 131 | } 132 | ], 133 | "body": { 134 | "mode": "raw", 135 | "raw": { 136 | "id": 1, 137 | "name": "Updated Name" 138 | } 139 | }, 140 | "url": { 141 | "raw": "{{base_url}}/update_user", 142 | "protocol": "http", 143 | "host": [ 144 | "{{host}}" 145 | ], 146 | "path": [ 147 | "update_user" 148 | ] 149 | } 150 | }, 151 | "response": [] 152 | }, 153 | { 154 | "name": "Get Posts by User ID", 155 | "request": { 156 | "method": "GET", 157 | "header": [], 158 | "url": { 159 | "raw": "{{base_url}}/posts?user_id=1", 160 | "protocol": "http", 161 | "host": [ 162 | "{{host}}" 163 | ], 164 | "path": [ 165 | "posts" 166 | ], 167 | "query": [ 168 | { 169 | "key": "user_id", 170 | "value": "1" 171 | } 172 | ] 173 | } 174 | }, 175 | "response": [] 176 | }, 177 | { 178 | "name": "Add Post", 179 | "request": { 180 | "method": "POST", 181 | "header": [ 182 | { 183 | "key": "Content-Type", 184 | "value": "application/json" 185 | } 186 | ], 187 | "body": { 188 | "mode": "raw", 189 | "raw": { 190 | "user_id": 1, 191 | "title": "New Post", 192 | "content": "This is a new post content." 193 | } 194 | }, 195 | "url": { 196 | "raw": "{{base_url}}/add_post", 197 | "protocol": "http", 198 | "host": [ 199 | "{{host}}" 200 | ], 201 | "path": [ 202 | "add_post" 203 | ] 204 | } 205 | }, 206 | "response": [] 207 | }] 208 | }, 209 | { 210 | "name": "legacy", 211 | "item": [{ 212 | "name": "Retrieve All Users", 213 | "request": { 214 | "method": "GET", 215 | "header": [], 216 | "url": { 217 | "raw": "{{base_url}}/users", 218 | "protocol": "http", 219 | "host": [ 220 | "{{host}}" 221 | ], 222 | "path": [ 223 | "users" 224 | ] 225 | } 226 | }, 227 | "response": [] 228 | }, 229 | { 230 | "name": "Add a New User", 231 | "request": { 232 | "method": "POST", 233 | "header": [ 234 | { 235 | "key": "Content-Type", 236 | "value": "application/json" 237 | } 238 | ], 239 | "body": { 240 | "mode": "raw", 241 | "raw": "{\n \"name\": \"John Doe\",\n \"email\": \"johndoe@example.com\"\n}" 242 | }, 243 | "url": { 244 | "raw": "{{base_url}}/add_user", 245 | "protocol": "http", 246 | "host": [ 247 | "{{host}}" 248 | ], 249 | "path": [ 250 | "add_user" 251 | ] 252 | } 253 | }, 254 | "response": [] 255 | }, 256 | { 257 | "name": "Update User Information", 258 | "request": { 259 | "method": "POST", 260 | "header": [ 261 | { 262 | "key": "Content-Type", 263 | "value": "application/json" 264 | } 265 | ], 266 | "body": { 267 | "mode": "raw", 268 | "raw": "{\n \"id\": 1,\n \"name\": \"Updated Name\",\n \"email\": \"updated@example.com\"\n}" 269 | }, 270 | "url": { 271 | "raw": "{{base_url}}/update_user", 272 | "protocol": "http", 273 | "host": [ 274 | "{{host}}" 275 | ], 276 | "path": [ 277 | "update_user" 278 | ] 279 | } 280 | }, 281 | "response": [] 282 | }] 283 | } 284 | ] 285 | } -------------------------------------------------------------------------------- /examples/legacy/app.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import FastAPI 4 | from models import User, db 5 | from pydantic import BaseModel 6 | 7 | from fastapi_sqlalchemy import DBSessionMiddleware 8 | 9 | app = FastAPI() 10 | 11 | # Add DB session middleware with db_url specified 12 | app.add_middleware(DBSessionMiddleware, db_url="sqlite:///example.db") 13 | 14 | 15 | # Endpoint to retrieve all users 16 | @app.get("/users") 17 | def get_users(): 18 | """ 19 | Retrieve a list of all users. 20 | 21 | Returns: 22 | List[User]: A list of User objects. 23 | """ 24 | return db.session.query(User).all() 25 | 26 | 27 | # Pydantic model for creating new users 28 | class UserCreate(BaseModel): 29 | name: str 30 | email: str 31 | 32 | 33 | # Endpoint to add a new user 34 | @app.post("/add_user") 35 | def add_user(user_data: UserCreate): 36 | """ 37 | Add a new user to the database. 38 | 39 | Args: 40 | user_data (UserCreate): User data including name and email. 41 | 42 | Returns: 43 | dict: A message indicating the success of the operation. 44 | """ 45 | user = User(**user_data.dict()) 46 | db.session.add(user) 47 | db.session.commit() 48 | return {"message": "User created successfully"} 49 | 50 | 51 | # Pydantic model for updating user information 52 | class UserUpdate(UserCreate): 53 | id: int 54 | name: Optional[str] 55 | email: Optional[str] 56 | 57 | 58 | # Endpoint to update user information 59 | @app.post("/update_user") 60 | def update_user(user_data: UserUpdate): 61 | """ 62 | Update user information in the database. 63 | 64 | Args: 65 | user_data (UserUpdate): User data including ID, name, and email for updating. 66 | 67 | Returns: 68 | dict: A message indicating the success of the operation. 69 | """ 70 | user = db.session.query(User).filter_by(id=user_data.id).first() 71 | if user_data.name: 72 | user.name = user_data.name 73 | if user_data.email: 74 | user.email = user_data.email 75 | db.session.add(user) 76 | db.session.commit() 77 | return {"message": "User updated successfully"} 78 | 79 | 80 | if __name__ == "__main__": 81 | import uvicorn 82 | 83 | uvicorn.run(app, host="127.0.0.1", port=8000) 84 | -------------------------------------------------------------------------------- /examples/legacy/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, create_engine 2 | from sqlalchemy.orm import declarative_base, sessionmaker 3 | 4 | from fastapi_sqlalchemy import db 5 | 6 | 7 | # Define the User class representing the "users" database table 8 | # Using the SQLAlchemy Base property instead of defining your own 9 | class User(db.Base): 10 | __tablename__ = "users" 11 | 12 | id = Column(Integer, primary_key=True) 13 | name = Column(String) 14 | email = Column(String) 15 | 16 | def __repr__(self): 17 | return f"User(id={self.id}, name='{self.name}',email='{self.email}')" 18 | -------------------------------------------------------------------------------- /examples/multi_db/app.py: -------------------------------------------------------------------------------- 1 | # Import necessary modules and classes 2 | from typing import Optional 3 | 4 | from fastapi import FastAPI 5 | from models.posts import Post, post_db # Import the Post model and post_db database instance 6 | from models.users import User, user_db # Import the User model and user_db database instance 7 | from pydantic import BaseModel 8 | 9 | from fastapi_sqlalchemy import ( 10 | DBSessionMiddleware, # Import the DBSessionMiddleware for database sessions 11 | ) 12 | 13 | # Create a FastAPI application instance 14 | app = FastAPI() 15 | 16 | # Add the DBSessionMiddleware as a middleware to the FastAPI app, connecting it to the specified databases 17 | app.add_middleware(DBSessionMiddleware, db=[post_db, user_db]) 18 | 19 | 20 | # Define an endpoint for retrieving all users 21 | @app.get("/users") 22 | def get_users(): 23 | """ 24 | Endpoint to retrieve a list of all users. 25 | Returns: 26 | List[User]: A list of User objects representing all users in the database. 27 | """ 28 | return User.get_all() 29 | 30 | 31 | # Define a Pydantic model for creating a new user 32 | class UserCreate(BaseModel): 33 | name: str 34 | email: str 35 | 36 | 37 | # Define an endpoint for adding a new user 38 | @app.post("/add_user") 39 | def add_user(user_data: UserCreate): 40 | """ 41 | Endpoint to add a new user to the database. 42 | Args: 43 | user_data (UserCreate): User data provided in the request body. 44 | Returns: 45 | dict: A message indicating the success of the operation. 46 | """ 47 | user = User(**user_data.dict()) 48 | user.save() 49 | return {"message": "User created successfully"} 50 | 51 | 52 | # Define a Pydantic model for updating user information 53 | class UserUpdate(UserCreate): 54 | id: int 55 | name: Optional[str] 56 | email: Optional[str] 57 | 58 | 59 | # Define an endpoint for updating user information 60 | @app.post("/update_user") 61 | def update_user(user_data: UserUpdate): 62 | """ 63 | Endpoint to update user information in the database. 64 | Args: 65 | user_data (UserUpdate): User data provided in the request body. 66 | Returns: 67 | dict: A message indicating the success of the operation. 68 | """ 69 | user = User.get(id=user_data.id) 70 | user.update(**user_data.dict()) 71 | user.save() 72 | return {"message": "User updated successfully"} 73 | 74 | 75 | # Define a Pydantic model for retrieving posts by user ID 76 | class UserPosts(BaseModel): 77 | user_id: int 78 | 79 | 80 | # Define an endpoint for retrieving posts by user ID 81 | @app.get("/posts") 82 | def get_posts(user: UserPosts): 83 | """ 84 | Endpoint to retrieve posts by a specific user ID. 85 | Args: 86 | user (UserPosts): User ID provided in the query parameters. 87 | Returns: 88 | List[Post]: A list of Post objects belonging to the specified user. 89 | """ 90 | posts = Post.get_all(user_id=user.user_id) 91 | return posts 92 | 93 | 94 | # Define a Pydantic model for creating a new post 95 | class PostCreate(UserPosts): 96 | title: str 97 | content: str 98 | 99 | 100 | # Define an endpoint for adding a new post 101 | @app.post("/add_post") 102 | def add_post(post_data: PostCreate): 103 | """ 104 | Endpoint to add a new post to the database. 105 | Args: 106 | post_data (PostCreate): Post data provided in the request body. 107 | Returns: 108 | dict: A message indicating the success of the operation. 109 | """ 110 | post = Post(**post_data.dict()) 111 | post.save() 112 | return {"message": "Post created successfully"} 113 | 114 | 115 | if __name__ == "__main__": 116 | import uvicorn 117 | 118 | uvicorn.run(app, host="127.0.0.1", port=8000) 119 | -------------------------------------------------------------------------------- /examples/multi_db/models/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Dict, List 3 | 4 | from sqlalchemy import Column 5 | 6 | from fastapi_sqlalchemy import ModelBase 7 | 8 | 9 | class BaseModel(ModelBase): 10 | @classmethod 11 | def new(cls, **kwargs): 12 | obj = cls(**kwargs) 13 | obj.save() 14 | return obj 15 | 16 | @classmethod 17 | def get(cls, **kwargs): 18 | result: cls = cls.query.filter_by(**kwargs).first() 19 | return result 20 | 21 | @classmethod 22 | def get_all(cls, **kwargs): 23 | result: List[cls] = cls.query.filter_by(**kwargs).all() 24 | return result 25 | 26 | def update(self, **kwargs): 27 | for column, value in kwargs.items(): 28 | setattr(self, column, value) 29 | 30 | self.save() 31 | return self 32 | 33 | def __repr__(self): 34 | try: 35 | columns = dict( 36 | (column.name, getattr(self, column.name)) for column in self.__table__.columns 37 | ) 38 | 39 | except: 40 | o = {} 41 | members = inspect.getmembers(self) 42 | for name, obj in members: 43 | if type(obj) == Column: 44 | o[name] = obj 45 | columns = o 46 | 47 | column_strings = [] 48 | for column, value in columns.items(): 49 | column_strings.append(f"{column}: {value}") 50 | 51 | repr = f"<{self.__class__.__name__} {', '.join(column_strings)}>" 52 | return repr 53 | -------------------------------------------------------------------------------- /examples/multi_db/models/posts.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, MetaData, String, create_engine 2 | from sqlalchemy.orm import DeclarativeMeta, declarative_base, sessionmaker 3 | 4 | from fastapi_sqlalchemy import SQLAlchemy 5 | 6 | from . import BaseModel 7 | 8 | # Create a SQLAlchemy instance with a connection to the SQLite database "post.db" 9 | post_db = SQLAlchemy("sqlite:///post.db") 10 | 11 | 12 | # Define the User class representing the "posts" database table 13 | # using the SQLAlchemy Base property instead of defining your own 14 | # And inheriting from the BaseModel class for type hinting and helpful builtin methods and properties 15 | class Post(BaseModel, post_db.Base): 16 | __tablename__ = "posts" 17 | id = Column(Integer, primary_key=True) 18 | title = Column(String) 19 | content = Column(String) 20 | user_id = Column(Integer) 21 | -------------------------------------------------------------------------------- /examples/multi_db/models/users.py: -------------------------------------------------------------------------------- 1 | # Import necessary modules and classes 2 | from sqlalchemy import Column, Integer, String, create_engine # Import SQLAlchemy components 3 | from sqlalchemy.orm import ( # Import SQLAlchemy components 4 | DeclarativeMeta, 5 | declarative_base, 6 | sessionmaker, 7 | ) 8 | 9 | from fastapi_sqlalchemy import SQLAlchemy # Import the SQLAlchemy extension 10 | 11 | from . import BaseModel # Import the custom BaseModel 12 | 13 | # Create a SQLAlchemy instance with a connection to the SQLite database "user.db" 14 | user_db = SQLAlchemy("sqlite:///user.db") 15 | 16 | 17 | # Define the User class representing the "users" database table 18 | # Using the SQLAlchemy Base property instead of defining your own 19 | # And inheriting from the BaseModel class for type hinting and helpful builtin methods and properties 20 | class User(BaseModel, user_db.Base): 21 | """ 22 | Represents a user in the database. 23 | 24 | Attributes: 25 | id (int): The primary key of the user. 26 | name (str): The name of the user. 27 | email (str): The email address of the user. 28 | """ 29 | 30 | # Name of the database table associated with this class 31 | __tablename__ = "users" 32 | 33 | # Columns corresponding to the attributes of the User class 34 | id = Column(Integer, primary_key=True) 35 | name = Column(String) 36 | email = Column(String) 37 | -------------------------------------------------------------------------------- /examples/single_db/app.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import FastAPI 4 | from models import User, db 5 | from pydantic import BaseModel 6 | 7 | from fastapi_sqlalchemy import DBSessionMiddleware 8 | 9 | app = FastAPI() 10 | 11 | # Add SQLAlchemy session middleware to manage database sessions 12 | app.add_middleware(DBSessionMiddleware, db=db) 13 | 14 | 15 | # Endpoint to retrieve all users 16 | @app.get("/users") 17 | def get_users(): 18 | """ 19 | Retrieve a list of all users. 20 | 21 | Returns: 22 | List[User]: A list of User objects. 23 | """ 24 | return User.query.all() 25 | 26 | 27 | # Pydantic model for creating new users 28 | class UserCreate(BaseModel): 29 | name: str 30 | email: str 31 | 32 | 33 | # Endpoint to add a new user 34 | @app.post("/add_user") 35 | def add_user(user_data: UserCreate): 36 | """ 37 | Add a new user to the database. 38 | 39 | Args: 40 | user_data (UserCreate): User data including name and email. 41 | 42 | Returns: 43 | dict: A message indicating the success of the operation. 44 | """ 45 | user = User(**user_data.model_dump()) 46 | print(user) 47 | user.save() 48 | return {"message": "User created successfully"} 49 | 50 | 51 | # Pydantic model for updating user information 52 | class UserUpdate(UserCreate): 53 | id: int 54 | name: Optional[str] 55 | email: Optional[str] 56 | 57 | 58 | # Endpoint to update user information 59 | @app.post("/update_user") 60 | def update_user(user_data: UserUpdate): 61 | """ 62 | Update user information in the database. 63 | 64 | Args: 65 | user_data (UserUpdate): User data including ID, name, and email for updating. 66 | 67 | Returns: 68 | dict: A message indicating the success of the operation. 69 | """ 70 | user = User.query.filter_by(id=user_data.id).first() 71 | print(user) 72 | user.update(**user_data.model_dump()) 73 | user.save() 74 | return {"message": "User updated successfully"} 75 | 76 | 77 | if __name__ == "__main__": 78 | import uvicorn 79 | 80 | uvicorn.run(app, host="127.0.0.1", port=8000) 81 | -------------------------------------------------------------------------------- /examples/single_db/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, create_engine 2 | from sqlalchemy.orm import declarative_base, sessionmaker 3 | 4 | from fastapi_sqlalchemy import ModelBase, SQLAlchemy 5 | 6 | db = SQLAlchemy(url="sqlite:///example.db") 7 | 8 | 9 | # Define the User class representing the "users" database table 10 | # Using the SQLAlchemy Base property instead of defining your own 11 | # And inheriting from the BaseModel class for type hinting and helpful builtin methods and properties 12 | class User(ModelBase, db.Base): 13 | __tablename__ = "users" 14 | 15 | id = Column(Integer, primary_key=True) 16 | name = Column(String) 17 | email = Column(String) 18 | 19 | def __repr__(self): 20 | return f"User(id={self.id}, name='{self.name}',email='{self.email}')" 21 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | from .extensions import SQLAlchemy, db 2 | from .middleware import DBSessionMiddleware 3 | from .types import ModelBase 4 | 5 | __all__ = ["db", "DBSessionMiddleware", "SQLAlchemy"] 6 | 7 | __version__ = "0.5.3" 8 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/decorators.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import asyncio 3 | import inspect 4 | from functools import wraps 5 | from typing import Any, Awaitable, Callable, Self 6 | 7 | from curio.meta import from_coroutine 8 | 9 | 10 | def awaitable(asyncfunc): 11 | def coroutine(syncfunc): 12 | @wraps(syncfunc) 13 | def wrapper(cls, *args, **kwargs): 14 | is_awaited = False 15 | for code in inspect.getframeinfo(inspect.currentframe().f_back).code_context: 16 | try: 17 | ast_tree = ast.parse(code.strip()) 18 | for node in ast.walk(ast_tree): 19 | if isinstance(node, ast.Await): 20 | is_awaited = True 21 | except: 22 | pass 23 | if from_coroutine(): 24 | if is_awaited: 25 | return asyncfunc(cls, *args, **kwargs) 26 | else: 27 | return syncfunc(cls, *args, **kwargs) 28 | else: 29 | return syncfunc(cls, *args, **kwargs) 30 | 31 | return wrapper 32 | 33 | return coroutine 34 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class MissingSessionError(Exception): 5 | """Excetion raised for when the user tries to access a database session before it is created.""" 6 | 7 | def __init__(self): 8 | msg = """ 9 | No session found! Either you are not currently in a request context, 10 | or you need to manually create a session context by using a `db` instance as 11 | a context manager e.g.: 12 | 13 | with db(): 14 | db.session.query(User).all() 15 | """ 16 | 17 | super().__init__(msg) 18 | 19 | 20 | class SessionNotInitialisedError(Exception): 21 | """Exception raised when the user creates a new DB session without first initialising it.""" 22 | 23 | def __init__(self): 24 | msg = """ 25 | Session not initialised! Ensure that DBSessionMiddleware has been initialised before 26 | attempting database access. 27 | """ 28 | 29 | super().__init__(msg) 30 | 31 | 32 | class SessionNotAsync(TypeError): 33 | """Exception raised when the user calls sync_session from within a synchronous function.""" 34 | 35 | def __init__(self): 36 | msg = """ 37 | Session not async! Ensure that you are calling sync_session from within an asynchronous function. 38 | """ 39 | super().__init__(msg) 40 | 41 | 42 | class DBSessionType(TypeError): 43 | """Exception raised when the user passes an object to DBSessionMiddleware that is not of DBSession or List[DBSession] type.""" 44 | 45 | def __init__(self): 46 | msg = """ 47 | Middleware not initialised! Ensure that db is of type DBSession or List[DBSession]. 48 | """ 49 | 50 | super().__init__(msg) 51 | 52 | 53 | class SQLAlchemyType(TypeError): 54 | """Exception raised when the user passes an object to DBSessionMiddleware that is not of SQLAlchemy or List[SQLAlchemy] or URL type.""" 55 | 56 | def __init__(self): 57 | msg = """ 58 | Middleware not initialized! Ensure that db is of type SQLAlchemy or List[SQLAlchemy] or URL. 59 | """ 60 | 61 | super().__init__(msg) 62 | 63 | 64 | class NonTableQuery(TypeError): 65 | """Exception raised when the user attempts to call .query on a non-table object.""" 66 | 67 | def __init__(self): 68 | msg = """ 69 | Non-table object! Ensure that the object you are querying is a table. 70 | """ 71 | 72 | super().__init__(msg) 73 | 74 | 75 | class SQLAlchemyAsyncioMissing(ImportError): 76 | """Exception raised when the user attempts to use the async_ parameter without installing SQLAlchemy-Asyncio.""" 77 | 78 | def __init__(self, missing: str = "sqlalchemy.ext.asyncio"): 79 | if "sqlalchemy.ext.asyncio" not in missing: 80 | missing = "sqlalchemy.ext.asyncio" + str( 81 | missing if missing[0] == "." else "." + missing 82 | ) 83 | 84 | msg = """ 85 | {package} is missing, please install using 'pip install sqlalchemy[asyncio]' or set async_ = False when initializing fastapi_sqlalchemy.SQLAlchemy. 86 | """.format( 87 | package=missing 88 | ) 89 | 90 | super().__init__(msg) 91 | 92 | 93 | class BuiltinNonExistent(AttributeError): 94 | """Exception raised when the user attempts to map a builtin property that does not exist.""" 95 | 96 | def __init__(self, prop: str): 97 | msg = f"""Builtin {prop} does not exist!""" 98 | 99 | super().__init__(msg) 100 | 101 | 102 | class TooManyBuiltinOverrides(AttributeError): 103 | """Exception raised when the user attempts to map a builtin property that does not exist.""" 104 | 105 | def __init__(self, prop: str): 106 | msg = f"""Too many builtin overrides! Strict maximum of 1 builtin override per model.""" 107 | 108 | super().__init__(msg) 109 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/extensions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import asyncio 5 | import gc 6 | import inspect 7 | import warnings 8 | from contextvars import ContextVar, Token 9 | from functools import wraps 10 | from typing import Any, Dict, List, Literal, Optional, Type, Union 11 | 12 | from curio.meta import from_coroutine 13 | from sqlalchemy import create_engine 14 | from sqlalchemy.engine import Engine 15 | from sqlalchemy.engine.url import URL 16 | from sqlalchemy.orm import DeclarativeMeta as DeclarativeMeta_ 17 | from sqlalchemy.orm import Query, Session, declarative_base, sessionmaker 18 | from sqlalchemy.types import BigInteger 19 | 20 | from .exceptions import SessionNotAsync, SessionNotInitialisedError, SQLAlchemyAsyncioMissing 21 | from .types import ModelBase 22 | 23 | try: 24 | import sqlalchemy.ext.asyncio 25 | 26 | sqlalchemy_asyncio = True 27 | except ImportError: 28 | sqlalchemy_asyncio = False 29 | try: 30 | from sqlalchemy.ext.asyncio import ( 31 | AsyncEngine, 32 | AsyncSession, 33 | async_sessionmaker, 34 | create_async_engine, 35 | ) 36 | except ImportError: 37 | create_async_engine = None 38 | 39 | _session: ContextVar[Dict[str, Dict[SQLAlchemy, Session | AsyncSession]]] = ContextVar( 40 | "_session", default={"sync": {}, "async": {}} 41 | ) 42 | 43 | 44 | def start_session() -> Token[Dict[str, Session | AsyncSession]]: 45 | return _session.set({"sync": {}, "async": {}}) 46 | 47 | 48 | def reset_session(token: Token[Dict[str, Session | AsyncSession]]) -> None: 49 | _session.reset(token) 50 | 51 | 52 | class DBSession: 53 | def __init__(self, db: SQLAlchemy): 54 | self.db = db 55 | self.child_session_sync = False 56 | self.child_session_async = False 57 | 58 | def __enter__(self): 59 | if not isinstance(self.db.sync_session_maker, sessionmaker): 60 | raise SessionNotInitialisedError 61 | session = self.db.sync_session_maker(**self.db.sync_session_args) 62 | session_dict = _session.get() 63 | if not session_dict["sync"].get(self.db): 64 | session_dict["sync"][self.db] = session 65 | _session.set(session_dict) 66 | else: 67 | self.child_session_sync = True 68 | return self.db 69 | 70 | def __exit__(self, exc_type, exc_value, traceback): 71 | if exc_type is not None: 72 | self.db.sync_session.rollback() 73 | 74 | elif self.db.commit_on_exit: 75 | try: 76 | self.db.sync_session.commit() 77 | self.db.sync_session.rollback() 78 | except: 79 | pass 80 | try: 81 | if not self.child_session_sync: 82 | self.db.sync_session.close() 83 | session_dict = _session.get() 84 | session_dict["sync"].pop(self.db) 85 | _session.set(session_dict) 86 | except: 87 | pass 88 | 89 | async def __aenter__(self): 90 | if not isinstance(self.db.async_session_maker, async_sessionmaker): 91 | raise SessionNotInitialisedError 92 | session = self.db.async_session_maker(**self.db.async_session_args) 93 | session_dict = _session.get() 94 | if not session_dict["async"].get(self.db): 95 | session_dict["async"][self.db] = session 96 | _session.set(session_dict) 97 | else: 98 | self.child_session_async = True 99 | return self.db 100 | 101 | async def __aexit__(self, exc_type, exc_value, traceback): 102 | if exc_type is not None: 103 | await self.db.session.rollback() 104 | elif self.db.commit_on_exit: 105 | try: 106 | await self.db.session.commit() 107 | await self.db.session.rollback() 108 | except: 109 | pass 110 | try: 111 | if not self.child_session_async: 112 | await self.db.session.close() 113 | session_dict = _session.get() 114 | session_dict["async"].pop(self.db) 115 | _session.set(session_dict) 116 | except: 117 | pass 118 | 119 | 120 | class SQLAlchemy: 121 | def __init__( 122 | self, 123 | url: Optional[URL] = None, 124 | *, 125 | async_url: Optional[URL] = None, 126 | custom_engine: Optional[Engine] = None, 127 | async_custom_engine: Optional[AsyncEngine] = None, 128 | engine_args: Dict[str, Any] = None, 129 | async_engine_args: Dict[str, Any] = None, 130 | session_args: Dict[str, Any] = None, 131 | async_session_args: Dict[str, Any] = None, 132 | commit_on_exit: bool = False, 133 | verbose: Literal[0, 1, 2, 3] = 0, 134 | async_: bool = False, 135 | expire_on_commit: Optional[bool] = False, 136 | extended: bool = True, 137 | _session_manager: DBSession = DBSession, 138 | ): 139 | self.initiated = False 140 | self._Base: Type[DeclarativeMeta] = declarative_base( 141 | metaclass=DeclarativeMeta, cls=ModelBase 142 | ) 143 | setattr(self._Base, "db", self) 144 | self.url = url 145 | self.async_url = async_url 146 | self.custom_engine = custom_engine 147 | self.async_custom_engine = async_custom_engine 148 | self.engine_args = engine_args or {} 149 | self.async_engine_args = async_engine_args or {} 150 | self.sync_session_args = session_args or {} 151 | self.async_session_args = async_session_args or {} 152 | self.commit_on_exit = commit_on_exit 153 | self.session_manager: DBSession = _session_manager 154 | self.verbose = verbose 155 | self.extended = extended 156 | if async_ and not async_sessionmaker: 157 | raise SQLAlchemyAsyncioMissing("async_sessionmaker") 158 | self.async_ = async_ 159 | self.expire_on_commit = expire_on_commit 160 | self._check_optional_components() 161 | self._session_maker: sessionmaker = None 162 | self.engine: Engine = None 163 | self.async_engine: AsyncEngine = None 164 | self.sync_session_maker: sessionmaker = None 165 | self.async_session_maker: async_sessionmaker = None 166 | if self.url: 167 | self.init() 168 | self.sync_session_args["expire_on_commit"] = self.async_session_args["expire_on_commit"] = ( 169 | False 170 | ) 171 | 172 | def init(self, url: Optional[URL] = None, **options) -> None: 173 | if url: 174 | self.url = url 175 | for key, value in options.items(): 176 | if hasattr(self, key): 177 | setattr(self, key, value) 178 | else: 179 | raise AttributeError(f"Attribute {key} not a valid attribute.") 180 | if not self.custom_engine and not self.url: 181 | raise ValueError("You need to pass a url or a custom_engine parameter.") 182 | if not self.async_custom_engine and not self.async_url and self.async_: 183 | raise ValueError("You need to pass a async_url or a async_custom_engine parameter.") 184 | self.engine = self._create_sync_engine() 185 | self.async_engine = self._create_async_engine() 186 | self.sync_session_maker = self._make_sync_session_maker() 187 | self.async_session_maker = self._make_async_session_maker() 188 | 189 | self.initiated = True 190 | self.metadata = False 191 | 192 | def create_all(self): 193 | self._Base.metadata.create_all(self.engine) 194 | self.metadata = True 195 | return None 196 | 197 | def drop_all(self, *, confirmed=False): 198 | if not confirmed: 199 | inp = "" 200 | while not inp.lower() in ["y", "n"]: 201 | inp = input( 202 | "Are you sure you want to drop all tables? (This cannot be undone) (y/n): " 203 | ) 204 | if inp == "n": 205 | return None 206 | else: 207 | continue 208 | for obj in gc.get_objects(): 209 | if type(obj) == Session: 210 | if obj.get_bind() == self.engine.url: 211 | obj.rollback() 212 | obj.close() 213 | elif type(obj) == AsyncSession: 214 | loop = asyncio.get_event_loop() 215 | if obj.get_bind() == self.engine.url: 216 | loop.run_in_executor(None, obj.rollback) 217 | loop.run_in_executor(None, obj.close) 218 | self._Base.metadata.drop_all(self.engine) 219 | return None 220 | 221 | def print(self, *values): 222 | if self.verbose >= 3: 223 | print(*values, flush=True) 224 | 225 | def info(self, *values): 226 | if self.verbose >= 2: 227 | print(*values, flush=True) 228 | 229 | def warning(self, message: str): 230 | if self.verbose >= 1: 231 | warnings.warn(message) 232 | 233 | def sync_context(self, func): 234 | @wraps(func) 235 | def wrapper(*args, **kwargs): 236 | if not self.initiated: 237 | self.init() 238 | with self(): 239 | return func(*args, **kwargs) 240 | 241 | return wrapper 242 | 243 | def async_context(self, func): 244 | @wraps(func) 245 | async def wrapper(*args, **kwargs): 246 | if not self.initiated: 247 | self.init() 248 | async with self(): 249 | return await func(*args, **kwargs) 250 | 251 | return wrapper 252 | 253 | def _check_optional_components(self): 254 | exceptions = [] 255 | if not sqlalchemy_asyncio and self.async_: 256 | raise SQLAlchemyAsyncioMissing() 257 | async_classes = [async_sessionmaker, create_async_engine] 258 | if self.async_: 259 | for cls in async_classes: 260 | if not cls: 261 | exceptions.append(SQLAlchemyAsyncioMissing(cls.__name__)) 262 | if not self.async_ and all(async_classes): 263 | self.print( 264 | "sqlalchemy[asyncio] is installed, to use set async_=True in SQLAlchemy constructor." 265 | ) 266 | 267 | if exceptions: 268 | raise Exception(*exceptions) 269 | 270 | def _make_sync_session_maker(self) -> sessionmaker: 271 | return sessionmaker(bind=self.engine, **self.sync_session_args) 272 | 273 | def _make_async_session_maker(self) -> async_sessionmaker: 274 | if self.async_: 275 | return async_sessionmaker(bind=self.async_engine, **self.async_session_args) 276 | 277 | def _create_sync_engine(self) -> Union[AsyncEngine, Engine]: 278 | if self.custom_engine: 279 | return self.custom_engine 280 | else: 281 | return create_engine(self.url, **self.engine_args) 282 | 283 | def _create_async_engine(self) -> AsyncEngine: 284 | if self.async_: 285 | if self.async_custom_engine: 286 | return self.async_custom_engine 287 | else: 288 | return create_async_engine(self.async_url, **self.async_engine_args) 289 | 290 | def __call__(self) -> SQLAlchemy: 291 | local_session = self.session_manager(db=self) 292 | return local_session 293 | 294 | def __enter__(self) -> SQLAlchemy: 295 | return self() 296 | 297 | def __exit__(self) -> None: 298 | pass 299 | 300 | async def __aenter__(self) -> SQLAlchemy: 301 | return self() 302 | 303 | async def __aexit__(self) -> None: 304 | pass 305 | 306 | @property 307 | def session(self) -> Union[Session, AsyncSession]: 308 | sessions = _session.get() 309 | if sessions["async"].get(self): 310 | return sessions["async"][self] 311 | elif sessions["sync"].get(self): 312 | return sessions["sync"][self] 313 | else: 314 | raise SessionNotInitialisedError 315 | 316 | @property 317 | def sync_session(self) -> Session: 318 | sessions = _session.get() 319 | if sessions["sync"].get(self): 320 | return sessions["sync"][self] 321 | elif sessions["async"].get(self): 322 | return sessions["async"][self].sync_session 323 | else: 324 | raise SessionNotInitialisedError 325 | 326 | def _make_dialects(self) -> None: 327 | self.BigInteger = BigInteger() 328 | self.BigInteger.with_variant() 329 | return None 330 | 331 | @property 332 | def BaseModel(self) -> Type[ModelBase]: 333 | return self._Base 334 | 335 | @property 336 | def Base(self) -> Type[DeclarativeMeta]: 337 | return self._Base 338 | 339 | 340 | class DeclarativeMeta(DeclarativeMeta_): 341 | db: SQLAlchemy 342 | session: Session 343 | 344 | def __init__(self, name, bases, attrs): 345 | for base in bases: 346 | if hasattr(base, "db"): 347 | self.db = base.db 348 | break 349 | super().__init__(name, bases, attrs) 350 | 351 | @property 352 | def session(self) -> Union[Session, AsyncSession]: 353 | return self.db.session 354 | 355 | @property 356 | def query(self) -> Query: 357 | return self.db.session.query(self) 358 | 359 | 360 | db: SQLAlchemy = SQLAlchemy() 361 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/middleware.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import inspect 5 | import logging 6 | from contextlib import AsyncExitStack, ExitStack 7 | from typing import Dict, List, Optional, Union 8 | 9 | from curio.meta import from_coroutine 10 | from sqlalchemy.engine.url import URL 11 | from sqlalchemy.orm import sessionmaker 12 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint 13 | from starlette.requests import Request 14 | from starlette.types import ASGIApp 15 | 16 | from .exceptions import SQLAlchemyType 17 | from .extensions import SQLAlchemy 18 | from .extensions import db as db_ 19 | from .extensions import reset_session, start_session 20 | 21 | 22 | class DBStateMap: 23 | def __init__(self): 24 | self.dbs: Dict[URL, sessionmaker] = {} 25 | self.initialized = False 26 | 27 | def __getitem__(self, item: URL) -> sessionmaker: 28 | return self.dbs[item] 29 | 30 | def __setitem__(self, key: URL, value: sessionmaker) -> None: 31 | if not self.initialized: 32 | self.dbs[key] = value 33 | else: 34 | raise ValueError("DBStateMap is already initialized") 35 | 36 | 37 | def is_async(): 38 | try: 39 | asyncio.get_running_loop() 40 | return True 41 | except RuntimeError: 42 | return False 43 | 44 | 45 | class DBSessionMiddleware(BaseHTTPMiddleware): 46 | def __init__( 47 | self, 48 | app: ASGIApp, 49 | db: Optional[Union[List[SQLAlchemy], SQLAlchemy]] = None, 50 | db_url: Optional[URL] = None, 51 | **options, 52 | ): 53 | super().__init__(app) 54 | self.state_map = DBStateMap() 55 | if not (type(db) == list or type(db) == SQLAlchemy) and not db_url: 56 | raise SQLAlchemyType() 57 | if db_url and not db: 58 | global db_ 59 | if not db_.initiated: 60 | db_.init(url=db_url, **options) 61 | self.dbs = [db_] 62 | if type(db) == SQLAlchemy: 63 | self.dbs = [ 64 | db, 65 | ] 66 | elif type(db) == list: 67 | self.dbs = db 68 | for db in self.dbs: 69 | db.create_all() 70 | 71 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): 72 | req_async = False 73 | try: 74 | for route in self.app.app.app.routes: 75 | if route.path == request.scope["path"]: 76 | req_async = inspect.iscoroutinefunction(route.endpoint) 77 | except: 78 | req_async = False 79 | token = start_session() 80 | exception = None 81 | async with AsyncExitStack() as async_stack: 82 | with ExitStack() as sync_stack: 83 | contexts = [ 84 | await async_stack.enter_async_context(ctx()) 85 | for ctx in self.dbs 86 | if ctx.async_ and req_async 87 | ] 88 | contexts.extend([sync_stack.enter_context(ctx()) for ctx in self.dbs]) 89 | try: 90 | response = await call_next(request) 91 | except Exception as e: 92 | exception = e 93 | for db in self.dbs: 94 | db.session.rollback() 95 | 96 | if exception: 97 | raise exception 98 | 99 | reset_session(token) 100 | return response 101 | 102 | # if req_async: 103 | # return dispatch_inner() 104 | # else: 105 | # with ExitStack() as stack: 106 | # contexts = [stack.enter_context(ctx()) for ctx in self.dbs] 107 | # response = call_next(request) 108 | # return response 109 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfreeborn/fastapi-sqlalchemy/794f9d4a87bf2eca6e71916cfa60cd68d7d05c85/fastapi_sqlalchemy/py.typed -------------------------------------------------------------------------------- /fastapi_sqlalchemy/sqlalchemy_types.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import BigInteger, TypeDecorator 2 | 3 | 4 | class BigIntegerType(TypeDecorator): 5 | impl = BigInteger 6 | -------------------------------------------------------------------------------- /fastapi_sqlalchemy/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import asyncio 5 | import inspect 6 | from typing import Any, Awaitable, Callable, Coroutine, List, Optional, Self, Union, overload 7 | 8 | from curio.meta import from_coroutine 9 | from sqlalchemy import select 10 | from sqlalchemy.ext.asyncio import AsyncSession 11 | from sqlalchemy.orm import DeclarativeMeta as DeclarativeMeta_ 12 | from sqlalchemy.orm import Query, Session, aliased 13 | from sqlalchemy.sql import ColumnExpressionArgument 14 | 15 | from .decorators import awaitable 16 | 17 | 18 | class ModelBase(object): 19 | query: Query 20 | session: Session | AsyncSession 21 | 22 | def __new__(cls, *args, **kwargs): 23 | obj = super().__new__(cls) 24 | if isinstance(obj.db.session, AsyncSession): 25 | obj.query = cls.db.sync_session.query(cls) 26 | else: 27 | obj.query = cls.db.session.query(cls) 28 | return obj 29 | 30 | @property 31 | def session(self) -> Session | AsyncSession: 32 | return self.db.session 33 | 34 | @property 35 | def sync_session(self) -> Session: 36 | return self.db.sync_session 37 | 38 | async def new(cls, **kwargs) -> Self: 39 | obj: Self = cls(**kwargs) 40 | await obj.save() 41 | return obj 42 | 43 | @classmethod 44 | @awaitable(new) 45 | def new(cls: Self, **kwargs) -> Union[Coroutine[Any, Any, Self], Self]: 46 | obj: Self = cls(**kwargs) 47 | obj.save() 48 | return obj 49 | 50 | async def get_all(cls, *criterion: ColumnExpressionArgument[bool], **kwargs: Any) -> List[Self]: 51 | if criterion: 52 | stmt = select(cls).filter(*criterion) 53 | else: 54 | stmt = select(cls).filter_by(**kwargs) 55 | result = await cls.session.execute(stmt) 56 | objs = result.scalars().all() 57 | return objs 58 | 59 | @classmethod 60 | @awaitable(get_all) 61 | def get_all( 62 | cls, *criterion: ColumnExpressionArgument[bool], **kwargs: Any 63 | ) -> Union[List[Self], Coroutine[Any, Any, List[Self]]]: 64 | if criterion: 65 | lst: List[Self] = cls.query.filter(*criterion, **kwargs).all() 66 | else: 67 | lst: List[Self] = cls.query.filter_by(**kwargs).all() 68 | return lst 69 | 70 | async def get(cls, *criterion: ColumnExpressionArgument[bool], **kwargs: Any) -> Self: 71 | if criterion: 72 | result = await cls.session.execute(select(cls).filter(*criterion)) 73 | else: 74 | result = await cls.session.execute(select(cls).filter_by(**kwargs)) 75 | return result.scalars().first() 76 | 77 | @classmethod 78 | @awaitable(get) 79 | def get( 80 | cls, *criterion: ColumnExpressionArgument[bool], **kwargs: Any 81 | ) -> Union[Coroutine[Any, Any, Self], Self]: 82 | if criterion: 83 | return cls.query.filter(*criterion, **kwargs).first() 84 | return cls.query.filter_by(**kwargs).first() 85 | 86 | async def save(self) -> None: 87 | t_e = self.session.sync_session.expire_on_commit 88 | self.session.expire_on_commit = False 89 | try: 90 | self.session.add(self) 91 | except: 92 | pass 93 | await self.session.commit() 94 | self.session.sync_session.expire_on_commit = t_e 95 | 96 | @awaitable(save) 97 | def save(self) -> None: 98 | try: 99 | self.sync_session.add(self) 100 | except: 101 | pass 102 | self.sync_session.commit() 103 | 104 | async def update(self, **kwargs): 105 | for attr, value in kwargs.items(): 106 | setattr(self, attr, value) 107 | await self.save() 108 | 109 | @awaitable(update) 110 | def update(self, **kwargs): 111 | for attr, value in kwargs.items(): 112 | setattr(self, attr, value) 113 | self.save() 114 | 115 | async def delete(self): 116 | await self.session.delete(self) 117 | await self.session.commit() 118 | 119 | @awaitable(delete) 120 | def delete(self): 121 | self.sync_session.delete(self) 122 | self.sync_session.commit() 123 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.5.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, 11 | {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} 16 | 17 | [[package]] 18 | name = "anyio" 19 | version = "3.7.1" 20 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 21 | optional = false 22 | python-versions = ">=3.7" 23 | files = [ 24 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, 25 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, 26 | ] 27 | 28 | [package.dependencies] 29 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 30 | idna = ">=2.8" 31 | sniffio = ">=1.1" 32 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 33 | 34 | [package.extras] 35 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] 36 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 37 | trio = ["trio (<0.22)"] 38 | 39 | [[package]] 40 | name = "curio" 41 | version = "1.6" 42 | description = "Curio" 43 | optional = false 44 | python-versions = ">= 3.7" 45 | files = [ 46 | {file = "curio-1.6.tar.gz", hash = "sha256:562a586db20216ba7d2be8263deb9eb079e56048f9b8906d11d5f45aa81c5247"}, 47 | ] 48 | 49 | [package.extras] 50 | test = ["Sphinx", "pytest"] 51 | 52 | [[package]] 53 | name = "exceptiongroup" 54 | version = "1.2.0" 55 | description = "Backport of PEP 654 (exception groups)" 56 | optional = false 57 | python-versions = ">=3.7" 58 | files = [ 59 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 60 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 61 | ] 62 | 63 | [package.extras] 64 | test = ["pytest (>=6)"] 65 | 66 | [[package]] 67 | name = "fastapi" 68 | version = "0.103.2" 69 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 70 | optional = false 71 | python-versions = ">=3.7" 72 | files = [ 73 | {file = "fastapi-0.103.2-py3-none-any.whl", hash = "sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e"}, 74 | {file = "fastapi-0.103.2.tar.gz", hash = "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653"}, 75 | ] 76 | 77 | [package.dependencies] 78 | anyio = ">=3.7.1,<4.0.0" 79 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 80 | starlette = ">=0.27.0,<0.28.0" 81 | typing-extensions = ">=4.5.0" 82 | 83 | [package.extras] 84 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 85 | 86 | [[package]] 87 | name = "greenlet" 88 | version = "3.0.3" 89 | description = "Lightweight in-process concurrent programming" 90 | optional = false 91 | python-versions = ">=3.7" 92 | files = [ 93 | {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, 94 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, 95 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, 96 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, 97 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, 98 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, 99 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, 100 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, 101 | {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, 102 | {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, 103 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, 104 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, 105 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, 106 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, 107 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, 108 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, 109 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, 110 | {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, 111 | {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, 112 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, 113 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, 114 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, 115 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, 116 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, 117 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, 118 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, 119 | {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, 120 | {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, 121 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, 122 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, 123 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, 124 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, 125 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, 126 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, 127 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, 128 | {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, 129 | {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, 130 | {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, 131 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, 132 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, 133 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, 134 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, 135 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, 136 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, 137 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, 138 | {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, 139 | {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, 140 | {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, 141 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, 142 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, 143 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, 144 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, 145 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, 146 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, 147 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, 148 | {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, 149 | {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, 150 | {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, 151 | ] 152 | 153 | [package.extras] 154 | docs = ["Sphinx", "furo"] 155 | test = ["objgraph", "psutil"] 156 | 157 | [[package]] 158 | name = "idna" 159 | version = "3.6" 160 | description = "Internationalized Domain Names in Applications (IDNA)" 161 | optional = false 162 | python-versions = ">=3.5" 163 | files = [ 164 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 165 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 166 | ] 167 | 168 | [[package]] 169 | name = "importlib-metadata" 170 | version = "6.7.0" 171 | description = "Read metadata from Python packages" 172 | optional = false 173 | python-versions = ">=3.7" 174 | files = [ 175 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 176 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 177 | ] 178 | 179 | [package.dependencies] 180 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 181 | zipp = ">=0.5" 182 | 183 | [package.extras] 184 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 185 | perf = ["ipython"] 186 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 187 | 188 | [[package]] 189 | name = "pydantic" 190 | version = "2.5.3" 191 | description = "Data validation using Python type hints" 192 | optional = false 193 | python-versions = ">=3.7" 194 | files = [ 195 | {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, 196 | {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, 197 | ] 198 | 199 | [package.dependencies] 200 | annotated-types = ">=0.4.0" 201 | importlib-metadata = {version = "*", markers = "python_version == \"3.7\""} 202 | pydantic-core = "2.14.6" 203 | typing-extensions = ">=4.6.1" 204 | 205 | [package.extras] 206 | email = ["email-validator (>=2.0.0)"] 207 | 208 | [[package]] 209 | name = "pydantic-core" 210 | version = "2.14.6" 211 | description = "" 212 | optional = false 213 | python-versions = ">=3.7" 214 | files = [ 215 | {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, 216 | {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, 217 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, 218 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, 219 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, 220 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, 221 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, 222 | {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, 223 | {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, 224 | {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, 225 | {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, 226 | {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, 227 | {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, 228 | {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, 229 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, 230 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, 231 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, 232 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, 233 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, 234 | {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, 235 | {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, 236 | {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, 237 | {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, 238 | {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, 239 | {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, 240 | {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, 241 | {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, 242 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, 243 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, 244 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, 245 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, 246 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, 247 | {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, 248 | {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, 249 | {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, 250 | {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, 251 | {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, 252 | {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, 253 | {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, 254 | {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, 255 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, 256 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, 257 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, 258 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, 259 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, 260 | {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, 261 | {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, 262 | {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, 263 | {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, 264 | {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, 265 | {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, 266 | {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, 267 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, 268 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, 269 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, 270 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, 271 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, 272 | {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, 273 | {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, 274 | {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, 275 | {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, 276 | {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, 277 | {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, 278 | {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, 279 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, 280 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, 281 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, 282 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, 283 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, 284 | {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, 285 | {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, 286 | {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, 287 | {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, 288 | {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, 289 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, 290 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, 291 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, 292 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, 293 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, 294 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, 295 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, 296 | {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, 297 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, 298 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, 299 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, 300 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, 301 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, 302 | {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, 303 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, 304 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, 305 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, 306 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, 307 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, 308 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, 309 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, 310 | {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, 311 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, 312 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, 313 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, 314 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, 315 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, 316 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, 317 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, 318 | {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, 319 | {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, 320 | ] 321 | 322 | [package.dependencies] 323 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 324 | 325 | [[package]] 326 | name = "sniffio" 327 | version = "1.3.1" 328 | description = "Sniff out which async library your code is running under" 329 | optional = false 330 | python-versions = ">=3.7" 331 | files = [ 332 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 333 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 334 | ] 335 | 336 | [[package]] 337 | name = "sqlalchemy" 338 | version = "2.0.29" 339 | description = "Database Abstraction Library" 340 | optional = false 341 | python-versions = ">=3.7" 342 | files = [ 343 | {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, 344 | {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, 345 | {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, 346 | {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, 347 | {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, 348 | {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, 349 | {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, 350 | {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, 351 | {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, 352 | {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, 353 | {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, 354 | {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, 355 | {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, 356 | {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, 357 | {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, 358 | {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, 359 | {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, 360 | {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, 361 | {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, 362 | {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, 363 | {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, 364 | {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, 365 | {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, 366 | {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, 367 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, 368 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, 369 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, 370 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, 371 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, 372 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, 373 | {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, 374 | {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, 375 | {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, 376 | {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, 377 | {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, 378 | {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, 379 | {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, 380 | {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, 381 | {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, 382 | {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, 383 | {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, 384 | {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, 385 | {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, 386 | {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, 387 | {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, 388 | {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, 389 | {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, 390 | {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, 391 | {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, 392 | ] 393 | 394 | [package.dependencies] 395 | greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} 396 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 397 | typing-extensions = ">=4.6.0" 398 | 399 | [package.extras] 400 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 401 | aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] 402 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 403 | asyncio = ["greenlet (!=0.4.17)"] 404 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 405 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 406 | mssql = ["pyodbc"] 407 | mssql-pymssql = ["pymssql"] 408 | mssql-pyodbc = ["pyodbc"] 409 | mypy = ["mypy (>=0.910)"] 410 | mysql = ["mysqlclient (>=1.4.0)"] 411 | mysql-connector = ["mysql-connector-python"] 412 | oracle = ["cx_oracle (>=8)"] 413 | oracle-oracledb = ["oracledb (>=1.0.1)"] 414 | postgresql = ["psycopg2 (>=2.7)"] 415 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 416 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 417 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 418 | postgresql-psycopg2binary = ["psycopg2-binary"] 419 | postgresql-psycopg2cffi = ["psycopg2cffi"] 420 | postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] 421 | pymysql = ["pymysql"] 422 | sqlcipher = ["sqlcipher3_binary"] 423 | 424 | [[package]] 425 | name = "starlette" 426 | version = "0.27.0" 427 | description = "The little ASGI library that shines." 428 | optional = false 429 | python-versions = ">=3.7" 430 | files = [ 431 | {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, 432 | {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, 433 | ] 434 | 435 | [package.dependencies] 436 | anyio = ">=3.4.0,<5" 437 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 438 | 439 | [package.extras] 440 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 441 | 442 | [[package]] 443 | name = "typing-extensions" 444 | version = "4.7.1" 445 | description = "Backported and Experimental Type Hints for Python 3.7+" 446 | optional = false 447 | python-versions = ">=3.7" 448 | files = [ 449 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 450 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 451 | ] 452 | 453 | [[package]] 454 | name = "zipp" 455 | version = "3.15.0" 456 | description = "Backport of pathlib-compatible object wrapper for zip files" 457 | optional = false 458 | python-versions = ">=3.7" 459 | files = [ 460 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 461 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 462 | ] 463 | 464 | [package.extras] 465 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 466 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 467 | 468 | [metadata] 469 | lock-version = "2.0" 470 | python-versions = ">=3.7" 471 | content-hash = "3322c043d61ac856b85f4c5d0410c2cfe79db5b8653304c9a66cbe7f29a4fd1d" 472 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py37'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | ( 7 | | .git 8 | | .venv 9 | | build 10 | | dist 11 | ) 12 | ''' 13 | 14 | [tool.isort] 15 | multi_line_output = 3 16 | include_trailing_comma = true 17 | force_grid_wrap = 0 18 | use_parentheses = true 19 | line_length = 100 20 | [tool.poetry] 21 | name = "fastapi-sqlalchemy" 22 | version = "1.0.0" 23 | description = "Adds simple SQLAlchemy support with multiple databases to FastAPI" 24 | authors = ["Ewen Lorimer "] 25 | readme = "README.md" 26 | 27 | [tool.poetry.dependencies] 28 | python = ">=3.7" 29 | starlette = ">=0.12.9" 30 | sqlalchemy = ">=1.2" 31 | fastapi = ">=0.52.0" 32 | curio = ">=1.6" 33 | 34 | 35 | [build-system] 36 | requires = ["poetry-core"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | atomicwrites==1.3.0 3 | attrs==19.3.0 4 | black==19.10b0 5 | certifi==2019.9.11 6 | chardet==3.0.4 7 | click==7.1.1 8 | coverage==4.5.4 9 | entrypoints==0.3 10 | fastapi==0.52.0 11 | flake8==3.7.9 12 | idna==2.8 13 | importlib-metadata==1.5.0 14 | isort==4.3.21 15 | mccabe==0.6.1 16 | more-itertools==7.2.0 17 | packaging==19.2 18 | pathspec==0.7.0 19 | pluggy==0.13.0 20 | py==1.8.0 21 | pycodestyle==2.5.0 22 | pydantic==0.32.2 23 | pyflakes==2.1.1 24 | pyparsing==2.4.2 25 | pytest==5.2.2 26 | pytest-cov==2.8.1 27 | PyYAML==5.3.1 28 | regex==2020.2.20 29 | requests==2.22.0 30 | six==1.12.0 31 | SQLAlchemy==1.3.10 32 | starlette==0.13.2 33 | toml==0.10.0 34 | typed-ast==1.4.1 35 | urllib3==1.25.6 36 | wcwidth==0.1.7 37 | zipp==3.1.0 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from setuptools import setup 5 | 6 | with open(Path("fastapi_sqlalchemy") / "__init__.py", encoding="utf-8") as fh: 7 | version = re.search(r'__version__ = "(.*?)"', fh.read(), re.M).group(1) 8 | 9 | with open("README.md", encoding="utf-8") as fh: 10 | long_description = fh.read() 11 | 12 | setup( 13 | name="FastAPI-SQLAlchemy-improved", 14 | version=version, 15 | url="https://github.com/Ewen-Zippedscript/fastapi-sqlalchemy", 16 | project_urls={ 17 | "Code": "https://github.com/Ewen-Zippedscript/fastapi-sqlalchemy", 18 | "Issue tracker": "https://github.com/Ewen-Zippedscript/fastapi-sqlalchemy/issues", 19 | }, 20 | license="MIT", 21 | author="Ewen Lorimer", 22 | author_email="ewen@zippedscript.com", 23 | description="Adds simple SQLAlchemy support with multiple databases to FastAPI.", 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | packages=["fastapi_sqlalchemy"], 27 | package_data={"fastapi_sqlalchemy": ["py.typed"]}, 28 | zip_safe=False, 29 | python_requires=">=3.7", 30 | install_requires=["starlette>=0.12.9", "SQLAlchemy>=1.2", "fastapi>=0.52.0", "curio>=1.6"], 31 | classifiers=[ 32 | "Development Status :: 4 - Beta", 33 | "Environment :: Web Environment", 34 | "Framework :: AsyncIO", 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3.8", 41 | "Programming Language :: Python :: 3 :: Only", 42 | "Programming Language :: Python :: Implementation :: CPython", 43 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers", 44 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 45 | "Topic :: Software Development :: Libraries :: Python Modules", 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfreeborn/fastapi-sqlalchemy/794f9d4a87bf2eca6e71916cfa60cd68d7d05c85/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from fastapi import FastAPI 5 | from starlette.testclient import TestClient 6 | 7 | 8 | @pytest.fixture 9 | def app(): 10 | return FastAPI() 11 | 12 | 13 | @pytest.fixture 14 | def client(app): 15 | with TestClient(app) as c: 16 | yield c 17 | 18 | 19 | @pytest.fixture 20 | def DBSessionMiddleware(): 21 | from fastapi_sqlalchemy import DBSessionMiddleware 22 | 23 | yield DBSessionMiddleware 24 | 25 | 26 | @pytest.fixture 27 | def db(): 28 | from fastapi_sqlalchemy import db 29 | 30 | yield db 31 | 32 | # force reloading of module to clear global state 33 | 34 | try: 35 | del sys.modules["fastapi_sqlalchemy"] 36 | except KeyError: 37 | pass 38 | 39 | try: 40 | del sys.modules["fastapi_sqlalchemy.middleware"] 41 | except KeyError: 42 | pass 43 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import Session 6 | from starlette.middleware.base import BaseHTTPMiddleware 7 | 8 | from fastapi_sqlalchemy.exceptions import MissingSessionError, SessionNotInitialisedError 9 | 10 | # TODO Add tests. 11 | --------------------------------------------------------------------------------