├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── alembic.ini ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── dependencies │ │ ├── __init__.py │ │ └── db.py │ └── routes │ │ ├── __init__.py │ │ ├── api.py │ │ └── coupons.py ├── core │ ├── __init__.py │ └── config.py ├── db │ ├── __init__.py │ ├── base.py │ ├── base_class.py │ ├── errors.py │ ├── migrations │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ ├── 750cdc702a91_.py │ │ │ └── 7db66d4b0914_.py │ ├── repositories │ │ ├── __init__.py │ │ ├── base.py │ │ └── coupons.py │ ├── session.py │ └── tables │ │ ├── __init__.py │ │ └── coupons.py ├── main.py └── models │ ├── __init__.py │ └── schema │ ├── __init__.py │ ├── base.py │ └── coupons.py ├── docker-compose.yml ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── app ├── __init__.py ├── api │ ├── __init__.py │ └── routes │ │ ├── __init__.py │ │ └── test_coupons.py └── test_main.py └── conftest.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,osx 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,osx 4 | 5 | ### OSX ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### PyCharm ### 35 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 36 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 37 | 38 | # User-specific stuff 39 | .idea/**/workspace.xml 40 | .idea/**/tasks.xml 41 | .idea/**/usage.statistics.xml 42 | .idea/**/dictionaries 43 | .idea/**/shelf 44 | 45 | # AWS User-specific 46 | .idea/**/aws.xml 47 | 48 | # Generated files 49 | .idea/**/contentModel.xml 50 | 51 | # Sensitive or high-churn files 52 | .idea/**/dataSources/ 53 | .idea/**/dataSources.ids 54 | .idea/**/dataSources.local.xml 55 | .idea/**/sqlDataSources.xml 56 | .idea/**/dynamic.xml 57 | .idea/**/uiDesigner.xml 58 | .idea/**/dbnavigator.xml 59 | 60 | # Gradle 61 | .idea/**/gradle.xml 62 | .idea/**/libraries 63 | 64 | # Gradle and Maven with auto-import 65 | # When using Gradle or Maven with auto-import, you should exclude module files, 66 | # since they will be recreated, and may cause churn. Uncomment if using 67 | # auto-import. 68 | # .idea/artifacts 69 | # .idea/compiler.xml 70 | # .idea/jarRepositories.xml 71 | # .idea/modules.xml 72 | # .idea/*.iml 73 | # .idea/modules 74 | # *.iml 75 | # *.ipr 76 | 77 | # CMake 78 | cmake-build-*/ 79 | 80 | # Mongo Explorer plugin 81 | .idea/**/mongoSettings.xml 82 | 83 | # File-based project format 84 | *.iws 85 | 86 | # IntelliJ 87 | out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Cursive Clojure plugin 96 | .idea/replstate.xml 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | fabric.properties 103 | 104 | # Editor-based Rest Client 105 | .idea/httpRequests 106 | 107 | # Android studio 3.1+ serialized cache file 108 | .idea/caches/build_file_checksums.ser 109 | 110 | ### PyCharm Patch ### 111 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 112 | 113 | # *.iml 114 | # modules.xml 115 | # .idea/misc.xml 116 | # *.ipr 117 | 118 | # Sonarlint plugin 119 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 120 | .idea/**/sonarlint/ 121 | 122 | # SonarQube Plugin 123 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 124 | .idea/**/sonarIssues.xml 125 | 126 | # Markdown Navigator plugin 127 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 128 | .idea/**/markdown-navigator.xml 129 | .idea/**/markdown-navigator-enh.xml 130 | .idea/**/markdown-navigator/ 131 | 132 | # Cache file creation bug 133 | # See https://youtrack.jetbrains.com/issue/JBR-2257 134 | .idea/$CACHE_FILE$ 135 | 136 | # CodeStream plugin 137 | # https://plugins.jetbrains.com/plugin/12206-codestream 138 | .idea/codestream.xml 139 | 140 | ### Python ### 141 | # Byte-compiled / optimized / DLL files 142 | __pycache__/ 143 | *.py[cod] 144 | *$py.class 145 | 146 | # C extensions 147 | *.so 148 | 149 | # Distribution / packaging 150 | .Python 151 | build/ 152 | develop-eggs/ 153 | dist/ 154 | downloads/ 155 | eggs/ 156 | .eggs/ 157 | lib/ 158 | lib64/ 159 | parts/ 160 | sdist/ 161 | var/ 162 | wheels/ 163 | share/python-wheels/ 164 | *.egg-info/ 165 | .installed.cfg 166 | *.egg 167 | MANIFEST 168 | 169 | # PyInstaller 170 | # Usually these files are written by a python script from a template 171 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 172 | *.manifest 173 | *.spec 174 | 175 | # Installer logs 176 | pip-log.txt 177 | pip-delete-this-directory.txt 178 | 179 | # Unit test / coverage reports 180 | htmlcov/ 181 | .tox/ 182 | .nox/ 183 | .coverage 184 | .coverage.* 185 | .cache 186 | nosetests.xml 187 | coverage.xml 188 | *.cover 189 | *.py,cover 190 | .hypothesis/ 191 | .pytest_cache/ 192 | cover/ 193 | 194 | # Translations 195 | *.mo 196 | *.pot 197 | 198 | # Django stuff: 199 | *.log 200 | local_settings.py 201 | db.sqlite3 202 | db.sqlite3-journal 203 | 204 | # Flask stuff: 205 | instance/ 206 | .webassets-cache 207 | 208 | # Scrapy stuff: 209 | .scrapy 210 | 211 | # Sphinx documentation 212 | docs/_build/ 213 | 214 | # PyBuilder 215 | .pybuilder/ 216 | target/ 217 | 218 | # Jupyter Notebook 219 | .ipynb_checkpoints 220 | 221 | # IPython 222 | profile_default/ 223 | ipython_config.py 224 | 225 | # pyenv 226 | # For a library or package, you might want to ignore these files since the code is 227 | # intended to run in multiple environments; otherwise, check them in: 228 | # .python-version 229 | 230 | # pipenv 231 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 232 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 233 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 234 | # install all needed dependencies. 235 | #Pipfile.lock 236 | 237 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 238 | __pypackages__/ 239 | 240 | # Celery stuff 241 | celerybeat-schedule 242 | celerybeat.pid 243 | 244 | # SageMath parsed files 245 | *.sage.py 246 | 247 | # Environments 248 | .env 249 | .venv 250 | env/ 251 | venv/ 252 | ENV/ 253 | env.bak/ 254 | venv.bak/ 255 | 256 | # Spyder project settings 257 | .spyderproject 258 | .spyproject 259 | 260 | # Rope project settings 261 | .ropeproject 262 | 263 | # mkdocs documentation 264 | /site 265 | 266 | # mypy 267 | .mypy_cache/ 268 | .dmypy.json 269 | dmypy.json 270 | 271 | # Pyre type checker 272 | .pyre/ 273 | 274 | # pytype static type analyzer 275 | .pytype/ 276 | 277 | # Cython debug symbols 278 | cython_debug/ 279 | 280 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm,osx -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6-buster as production 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get -y update && \ 6 | apt-get -y upgrade 7 | 8 | RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add && \ 9 | echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list && \ 10 | apt-get update && apt-get -y install postgresql-client-12 11 | 12 | COPY pyproject.toml poetry.lock ./ 13 | RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ 14 | cd /usr/local/bin && \ 15 | ln -s /opt/poetry/bin/poetry && \ 16 | poetry config virtualenvs.create false 17 | 18 | RUN poetry install --no-root --no-dev 19 | 20 | COPY ./app ./ 21 | ENV LOG_LEVEL info 22 | ENV PYTHONPATH=. 23 | CMD [ "sh", "-c", "uvicorn app.main:app" ] 24 | 25 | # For development build 26 | FROM production as development 27 | 28 | COPY ./tests ./tests 29 | 30 | RUN poetry install --no-root -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021, 2022 Piotr Rogulski 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 MERCHANTABLITY, 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.I 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy ORM 1.4 with FastAPI on asyncio 2 | 3 | This is a tutorial app build for my [blog post](https://rogulski.it/blog/sqlalchemy-14-async-orm-with-fastapi/) 4 | ## Run project 5 | `docker-compose up` 6 | 7 | ## Run tests 8 | `docker-compose run app pytest` 9 | 10 | ## Generate migrations 11 | `docker-compose run app alembic revision --autogenerate` 12 | 13 | ## Run migrations 14 | `docker-compose run app alembic upgrate head` -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | script_location = app/db/migrations 3 | 4 | [loggers] 5 | keys = root,sqlalchemy,alembic 6 | 7 | [handlers] 8 | keys = console 9 | 10 | [formatters] 11 | keys = generic 12 | 13 | [logger_root] 14 | level = WARN 15 | handlers = console 16 | qualname = 17 | 18 | [logger_sqlalchemy] 19 | level = WARN 20 | handlers = 21 | qualname = sqlalchemy.engine 22 | 23 | [logger_alembic] 24 | level = INFO 25 | handlers = 26 | qualname = alembic 27 | 28 | [handler_console] 29 | class = StreamHandler 30 | args = (sys.stderr,) 31 | level = NOTSET 32 | formatter = generic 33 | 34 | [formatter_generic] 35 | format = %(levelname)-5.5s [%(name)s] %(message)s 36 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/__init__.py -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/api/__init__.py -------------------------------------------------------------------------------- /app/api/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/api/dependencies/__init__.py -------------------------------------------------------------------------------- /app/api/dependencies/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession 2 | 3 | from app.db.session import async_session 4 | 5 | 6 | async def get_db() -> AsyncSession: 7 | """ 8 | Dependency function that yields db sessions 9 | """ 10 | async with async_session() as session: 11 | yield session 12 | await session.commit() 13 | -------------------------------------------------------------------------------- /app/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/api/routes/__init__.py -------------------------------------------------------------------------------- /app/api/routes/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.routes import coupons 4 | 5 | api_router = APIRouter() 6 | api_router.include_router(coupons.router, prefix="/coupons", tags=["coupons"]) 7 | -------------------------------------------------------------------------------- /app/api/routes/coupons.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from uuid import UUID 3 | 4 | from fastapi import APIRouter, Depends 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | from starlette import status 7 | 8 | from app.api.dependencies.db import get_db 9 | from app.db.repositories.coupons import CouponsRepository 10 | from app.models.schema.coupons import OutCouponSchema, InCouponSchema 11 | 12 | router = APIRouter() 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @router.post("/", status_code=status.HTTP_201_CREATED, response_model=OutCouponSchema) 17 | async def create_coupon( 18 | payload: InCouponSchema, db: AsyncSession = Depends(get_db) 19 | ) -> OutCouponSchema: 20 | coupons_repository = CouponsRepository(db) 21 | coupon = await coupons_repository.create(payload) 22 | return OutCouponSchema(**coupon.dict()) 23 | 24 | 25 | @router.get( 26 | "/{coupon_id}", status_code=status.HTTP_200_OK, response_model=OutCouponSchema 27 | ) 28 | async def create_coupon( 29 | coupon_id: UUID, db: AsyncSession = Depends(get_db) 30 | ) -> OutCouponSchema: 31 | coupons_repository = CouponsRepository(db) 32 | coupon = await coupons_repository.get_by_id(coupon_id) 33 | return OutCouponSchema(**coupon.dict()) 34 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/core/__init__.py -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from enum import Enum 4 | from functools import lru_cache 5 | from typing import Optional 6 | 7 | from pydantic import BaseSettings, PostgresDsn 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class EnvironmentEnum(str, Enum): 13 | PRODUCTION = "production" 14 | LOCAL = "local" 15 | 16 | 17 | class GlobalConfig(BaseSettings): 18 | TITLE: str = "Tutorial" 19 | DESCRIPTION: str = "This is a tutorial project for my blog" 20 | 21 | ENVIRONMENT: EnvironmentEnum 22 | DEBUG: bool = False 23 | TESTING: bool = False 24 | TIMEZONE: str = "UTC" 25 | 26 | DATABASE_URL: Optional[ 27 | PostgresDsn 28 | ] = "postgresql://postgres:postgres@127.0.0.1:5432/postgres" 29 | DB_ECHO_LOG: bool = False 30 | 31 | @property 32 | def async_database_url(self) -> Optional[str]: 33 | return ( 34 | self.DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://") 35 | if self.DATABASE_URL 36 | else self.DATABASE_URL 37 | ) 38 | 39 | # Api V1 prefix 40 | API_V1_STR = "/v1" 41 | 42 | class Config: 43 | case_sensitive = True 44 | 45 | 46 | class LocalConfig(GlobalConfig): 47 | """Local configurations.""" 48 | 49 | DEBUG: bool = True 50 | ENVIRONMENT: EnvironmentEnum = EnvironmentEnum.LOCAL 51 | 52 | 53 | class ProdConfig(GlobalConfig): 54 | """Production configurations.""" 55 | 56 | DEBUG: bool = False 57 | ENVIRONMENT: EnvironmentEnum = EnvironmentEnum.PRODUCTION 58 | 59 | 60 | class FactoryConfig: 61 | def __init__(self, environment: Optional[str]): 62 | self.environment = environment 63 | 64 | def __call__(self) -> GlobalConfig: 65 | if self.environment == EnvironmentEnum.LOCAL.value: 66 | return LocalConfig() 67 | return ProdConfig() 68 | 69 | 70 | @lru_cache() 71 | def get_configuration() -> GlobalConfig: 72 | return FactoryConfig(os.environ.get("ENVIRONMENT"))() 73 | 74 | 75 | settings = get_configuration() 76 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/db/__init__.py -------------------------------------------------------------------------------- /app/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being imported by Alembic 2 | 3 | from app.db.base_class import Base # noqa: F401 4 | from app.db.tables.coupons import Coupon # noqa: F401 5 | -------------------------------------------------------------------------------- /app/db/base_class.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from sqlalchemy import Column 4 | from sqlalchemy.dialects.postgresql import UUID 5 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 6 | 7 | 8 | @as_declarative() 9 | class Base: 10 | id: uuid.UUID = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 11 | __name__: str 12 | 13 | # Generate __tablename__ automatically 14 | @declared_attr 15 | def __tablename__(cls) -> str: 16 | return cls.__name__.lower() 17 | -------------------------------------------------------------------------------- /app/db/errors.py: -------------------------------------------------------------------------------- 1 | class DoesNotExist(Exception): 2 | """Raised when entity was not found in database.""" 3 | -------------------------------------------------------------------------------- /app/db/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /app/db/migrations/env.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from logging.config import fileConfig 3 | 4 | from sqlalchemy import engine_from_config 5 | from sqlalchemy import pool 6 | 7 | from alembic import context 8 | 9 | sys.path = ["", ".."] + sys.path[1:] # TODO: Fix it 10 | 11 | from app.core.config import settings # noqa 12 | from app.db.base import Base # noqa 13 | 14 | config = context.config 15 | config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) 16 | target_metadata = Base.metadata 17 | 18 | fileConfig(config.config_file_name) 19 | 20 | 21 | def run_migrations_offline(): 22 | """Run migrations in 'offline' mode. 23 | 24 | This configures the context with just a URL 25 | and not an Engine, though an Engine is acceptable 26 | here as well. By skipping the Engine creation 27 | we don't even need a DBAPI to be available. 28 | 29 | Calls to context.execute() here emit the given string to the 30 | script output. 31 | 32 | """ 33 | url = config.get_main_option("sqlalchemy.url") 34 | context.configure( 35 | url=url, 36 | target_metadata=target_metadata, 37 | literal_binds=True, 38 | dialect_opts={"paramstyle": "named"}, 39 | ) 40 | 41 | with context.begin_transaction(): 42 | context.run_migrations() 43 | 44 | 45 | def run_migrations_online(): 46 | """Run migrations in 'online' mode. 47 | 48 | In this scenario we need to create an Engine 49 | and associate a connection with the context. 50 | 51 | """ 52 | connectable = engine_from_config( 53 | config.get_section(config.config_ini_section), 54 | prefix="sqlalchemy.", 55 | poolclass=pool.NullPool, 56 | ) 57 | 58 | with connectable.connect() as connection: 59 | context.configure(connection=connection, target_metadata=target_metadata) 60 | 61 | with context.begin_transaction(): 62 | context.run_migrations() 63 | 64 | 65 | if context.is_offline_mode(): 66 | run_migrations_offline() 67 | else: 68 | run_migrations_online() 69 | -------------------------------------------------------------------------------- /app/db/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /app/db/migrations/versions/750cdc702a91_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 750cdc702a91 4 | Revises: 7db66d4b0914 5 | Create Date: 2021-10-09 15:00:00.008005 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '750cdc702a91' 14 | down_revision = '7db66d4b0914' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_unique_constraint(None, 'coupon', ['code']) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_constraint(None, 'coupon', type_='unique') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /app/db/migrations/versions/7db66d4b0914_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7db66d4b0914 4 | Revises: 5 | Create Date: 2021-10-09 14:47:19.845110 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7db66d4b0914' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('coupon', 22 | sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), 23 | sa.Column('code', sa.String(), nullable=False), 24 | sa.Column('init_count', sa.Integer(), nullable=True), 25 | sa.Column('remaining_count', sa.Integer(), nullable=True), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('coupon') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /app/db/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/db/repositories/__init__.py -------------------------------------------------------------------------------- /app/db/repositories/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Generic, TypeVar, Type 3 | from uuid import uuid4, UUID 4 | 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from app.db.errors import DoesNotExist 8 | from app.models.schema.base import BaseSchema 9 | 10 | IN_SCHEMA = TypeVar("IN_SCHEMA", bound=BaseSchema) 11 | SCHEMA = TypeVar("SCHEMA", bound=BaseSchema) 12 | TABLE = TypeVar("TABLE") 13 | 14 | 15 | class BaseRepository(Generic[IN_SCHEMA, SCHEMA, TABLE], metaclass=abc.ABCMeta): 16 | def __init__(self, db_session: AsyncSession, *args, **kwargs) -> None: 17 | self._db_session: AsyncSession = db_session 18 | 19 | @property 20 | @abc.abstractmethod 21 | def _table(self) -> Type[TABLE]: 22 | ... 23 | 24 | @property 25 | @abc.abstractmethod 26 | def _schema(self) -> Type[SCHEMA]: 27 | ... 28 | 29 | async def create(self, in_schema: IN_SCHEMA) -> SCHEMA: 30 | entry = self._table(id=uuid4(), **in_schema.dict()) 31 | self._db_session.add(entry) 32 | await self._db_session.commit() 33 | return self._schema.from_orm(entry) 34 | 35 | async def get_by_id(self, entry_id: UUID) -> SCHEMA: 36 | entry = await self._db_session.get(self._table, entry_id) 37 | if not entry: 38 | raise DoesNotExist(f"{self._table.__name__} does not exist") 39 | return self._schema.from_orm(entry) 40 | -------------------------------------------------------------------------------- /app/db/repositories/coupons.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from app.db.repositories.base import BaseRepository 4 | from app.db.tables.coupons import Coupon 5 | from app.models.schema.coupons import InCouponSchema, CouponSchema 6 | 7 | 8 | class CouponsRepository(BaseRepository[InCouponSchema, CouponSchema, Coupon]): 9 | @property 10 | def _in_schema(self) -> Type[InCouponSchema]: 11 | return InCouponSchema 12 | 13 | @property 14 | def _schema(self) -> Type[CouponSchema]: 15 | return CouponSchema 16 | 17 | @property 18 | def _table(self) -> Type[Coupon]: 19 | return Coupon 20 | -------------------------------------------------------------------------------- /app/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from app.core.config import settings 5 | 6 | 7 | engine = create_async_engine( 8 | settings.async_database_url, 9 | echo=settings.DB_ECHO_LOG, 10 | ) 11 | async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) 12 | -------------------------------------------------------------------------------- /app/db/tables/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/db/tables/__init__.py -------------------------------------------------------------------------------- /app/db/tables/coupons.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from app.db.base_class import Base 4 | 5 | 6 | class Coupon(Base): 7 | __tablename__ = "coupon" 8 | 9 | code = Column(String, nullable=False, unique=True) 10 | init_count = Column(Integer, nullable=False) 11 | remaining_count = Column(Integer, nullable=False) 12 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from fastapi import FastAPI 5 | 6 | from app.api.routes.api import api_router 7 | from app.core.config import settings 8 | 9 | os.environ["TZ"] = settings.TIMEZONE 10 | time.tzset() 11 | 12 | 13 | def get_application() -> FastAPI: 14 | application = FastAPI( 15 | title=settings.TITLE, 16 | description=settings.DESCRIPTION, 17 | debug=settings.DEBUG, 18 | docs_url=None, 19 | redoc_url=None, 20 | openapi_url=None, 21 | ) 22 | application.include_router(api_router, prefix=settings.API_V1_STR) 23 | return application 24 | 25 | 26 | app = get_application() 27 | 28 | 29 | @app.get("/") 30 | def main(): 31 | return {"status": "ok"} 32 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/app/models/schema/__init__.py -------------------------------------------------------------------------------- /app/models/schema/base.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class BaseSchema(BaseModel): 5 | class Config(BaseModel.Config): 6 | orm_mode = True 7 | -------------------------------------------------------------------------------- /app/models/schema/coupons.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, Dict 2 | from uuid import UUID 3 | 4 | from pydantic import validator 5 | 6 | from app.models.schema.base import BaseSchema 7 | 8 | 9 | class CouponSchemaBase(BaseSchema): 10 | code: str 11 | init_count: int 12 | 13 | 14 | class InCouponSchema(CouponSchemaBase): 15 | remaining_count: Optional[int] 16 | 17 | @validator("remaining_count", always=True) 18 | def remaining_count_update( 19 | cls, value: Optional[int], values: Dict[Any, Any], **kwargs 20 | ): 21 | return value or values["init_count"] 22 | 23 | 24 | class CouponSchema(CouponSchemaBase): 25 | id: UUID 26 | remaining_count: int 27 | 28 | 29 | class OutCouponSchema(CouponSchema): 30 | ... 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | postgres: 5 | image: postgres:12.5 6 | environment: 7 | POSTGRES_USER: postgres 8 | POSTGRES_PASSWORD: postgres 9 | POSTGRES_DB: postgres 10 | volumes: 11 | - postgresql_data:/var/lib/postgresql/data/ 12 | expose: 13 | - 5432 14 | ports: 15 | - 5432:5432 16 | 17 | app: 18 | container_name: tutorial 19 | build: 20 | context: . 21 | volumes: 22 | - ./:/app/ 23 | command: bash -c "uvicorn app.main:app --reload --host 0.0.0.0 --port 8080" 24 | environment: 25 | - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres 26 | - PYTHONUNBUFFERED 27 | - DEBUG 28 | - PORT 29 | expose: 30 | - 8080 31 | ports: 32 | - 8080:8080 33 | depends_on: 34 | - postgres 35 | 36 | volumes: 37 | postgresql_data: -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alembic" 3 | version = "1.7.4" 4 | description = "A database migration tool for SQLAlchemy." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | Mako = "*" 11 | SQLAlchemy = ">=1.3.0" 12 | 13 | [package.extras] 14 | tz = ["python-dateutil"] 15 | 16 | [[package]] 17 | name = "anyio" 18 | version = "3.3.2" 19 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 20 | category = "main" 21 | optional = false 22 | python-versions = ">=3.6.2" 23 | 24 | [package.dependencies] 25 | idna = ">=2.8" 26 | sniffio = ">=1.1" 27 | 28 | [package.extras] 29 | doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] 30 | test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] 31 | trio = ["trio (>=0.16)"] 32 | 33 | [[package]] 34 | name = "asgiref" 35 | version = "3.4.1" 36 | description = "ASGI specs, helper code, and adapters" 37 | category = "main" 38 | optional = false 39 | python-versions = ">=3.6" 40 | 41 | [package.extras] 42 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 43 | 44 | [[package]] 45 | name = "asyncpg" 46 | version = "0.24.0" 47 | description = "An asyncio PostgreSQL driver" 48 | category = "main" 49 | optional = false 50 | python-versions = ">=3.6.0" 51 | 52 | [package.extras] 53 | dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] 54 | docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] 55 | test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] 56 | 57 | [[package]] 58 | name = "atomicwrites" 59 | version = "1.4.0" 60 | description = "Atomic file writes." 61 | category = "dev" 62 | optional = false 63 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 64 | 65 | [[package]] 66 | name = "attrs" 67 | version = "21.2.0" 68 | description = "Classes Without Boilerplate" 69 | category = "dev" 70 | optional = false 71 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 72 | 73 | [package.extras] 74 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 75 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 76 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 77 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 78 | 79 | [[package]] 80 | name = "black" 81 | version = "21.9b0" 82 | description = "The uncompromising code formatter." 83 | category = "dev" 84 | optional = false 85 | python-versions = ">=3.6.2" 86 | 87 | [package.dependencies] 88 | click = ">=7.1.2" 89 | mypy-extensions = ">=0.4.3" 90 | pathspec = ">=0.9.0,<1" 91 | platformdirs = ">=2" 92 | regex = ">=2020.1.8" 93 | tomli = ">=0.2.6,<2.0.0" 94 | typing-extensions = [ 95 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 96 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 97 | ] 98 | 99 | [package.extras] 100 | colorama = ["colorama (>=0.4.3)"] 101 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 102 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 103 | python2 = ["typed-ast (>=1.4.2)"] 104 | uvloop = ["uvloop (>=0.15.2)"] 105 | 106 | [[package]] 107 | name = "certifi" 108 | version = "2021.10.8" 109 | description = "Python package for providing Mozilla's CA Bundle." 110 | category = "dev" 111 | optional = false 112 | python-versions = "*" 113 | 114 | [[package]] 115 | name = "charset-normalizer" 116 | version = "2.0.6" 117 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 118 | category = "dev" 119 | optional = false 120 | python-versions = ">=3.5.0" 121 | 122 | [package.extras] 123 | unicode_backport = ["unicodedata2"] 124 | 125 | [[package]] 126 | name = "click" 127 | version = "8.0.2" 128 | description = "Composable command line interface toolkit" 129 | category = "main" 130 | optional = false 131 | python-versions = ">=3.6" 132 | 133 | [package.dependencies] 134 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 135 | 136 | [[package]] 137 | name = "colorama" 138 | version = "0.4.4" 139 | description = "Cross-platform colored terminal text." 140 | category = "main" 141 | optional = false 142 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 143 | 144 | [[package]] 145 | name = "fastapi" 146 | version = "0.70.0" 147 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 148 | category = "main" 149 | optional = false 150 | python-versions = ">=3.6.1" 151 | 152 | [package.dependencies] 153 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 154 | starlette = "0.16.0" 155 | 156 | [package.extras] 157 | all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 158 | dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 159 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] 160 | test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] 161 | 162 | [[package]] 163 | name = "greenlet" 164 | version = "1.1.2" 165 | description = "Lightweight in-process concurrent programming" 166 | category = "main" 167 | optional = false 168 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 169 | 170 | [package.extras] 171 | docs = ["sphinx"] 172 | 173 | [[package]] 174 | name = "h11" 175 | version = "0.12.0" 176 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 177 | category = "main" 178 | optional = false 179 | python-versions = ">=3.6" 180 | 181 | [[package]] 182 | name = "httpcore" 183 | version = "0.13.7" 184 | description = "A minimal low-level HTTP client." 185 | category = "dev" 186 | optional = false 187 | python-versions = ">=3.6" 188 | 189 | [package.dependencies] 190 | anyio = ">=3.0.0,<4.0.0" 191 | h11 = ">=0.11,<0.13" 192 | sniffio = ">=1.0.0,<2.0.0" 193 | 194 | [package.extras] 195 | http2 = ["h2 (>=3,<5)"] 196 | 197 | [[package]] 198 | name = "httpx" 199 | version = "0.19.0" 200 | description = "The next generation HTTP client." 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.6" 204 | 205 | [package.dependencies] 206 | certifi = "*" 207 | charset-normalizer = "*" 208 | httpcore = ">=0.13.3,<0.14.0" 209 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 210 | sniffio = "*" 211 | 212 | [package.extras] 213 | brotli = ["brotlicffi", "brotli"] 214 | http2 = ["h2 (>=3,<5)"] 215 | 216 | [[package]] 217 | name = "idna" 218 | version = "3.2" 219 | description = "Internationalized Domain Names in Applications (IDNA)" 220 | category = "main" 221 | optional = false 222 | python-versions = ">=3.5" 223 | 224 | [[package]] 225 | name = "iniconfig" 226 | version = "1.1.1" 227 | description = "iniconfig: brain-dead simple config-ini parsing" 228 | category = "dev" 229 | optional = false 230 | python-versions = "*" 231 | 232 | [[package]] 233 | name = "mako" 234 | version = "1.1.5" 235 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 236 | category = "main" 237 | optional = false 238 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 239 | 240 | [package.dependencies] 241 | MarkupSafe = ">=0.9.2" 242 | 243 | [package.extras] 244 | babel = ["babel"] 245 | lingua = ["lingua"] 246 | 247 | [[package]] 248 | name = "markupsafe" 249 | version = "2.0.1" 250 | description = "Safely add untrusted strings to HTML/XML markup." 251 | category = "main" 252 | optional = false 253 | python-versions = ">=3.6" 254 | 255 | [[package]] 256 | name = "mypy-extensions" 257 | version = "0.4.3" 258 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 259 | category = "dev" 260 | optional = false 261 | python-versions = "*" 262 | 263 | [[package]] 264 | name = "packaging" 265 | version = "21.0" 266 | description = "Core utilities for Python packages" 267 | category = "dev" 268 | optional = false 269 | python-versions = ">=3.6" 270 | 271 | [package.dependencies] 272 | pyparsing = ">=2.0.2" 273 | 274 | [[package]] 275 | name = "pathspec" 276 | version = "0.9.0" 277 | description = "Utility library for gitignore style pattern matching of file paths." 278 | category = "dev" 279 | optional = false 280 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 281 | 282 | [[package]] 283 | name = "platformdirs" 284 | version = "2.4.0" 285 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 286 | category = "dev" 287 | optional = false 288 | python-versions = ">=3.6" 289 | 290 | [package.extras] 291 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 292 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 293 | 294 | [[package]] 295 | name = "pluggy" 296 | version = "1.0.0" 297 | description = "plugin and hook calling mechanisms for python" 298 | category = "dev" 299 | optional = false 300 | python-versions = ">=3.6" 301 | 302 | [package.extras] 303 | dev = ["pre-commit", "tox"] 304 | testing = ["pytest", "pytest-benchmark"] 305 | 306 | [[package]] 307 | name = "psycopg2" 308 | version = "2.9.1" 309 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 310 | category = "main" 311 | optional = false 312 | python-versions = ">=3.6" 313 | 314 | [[package]] 315 | name = "py" 316 | version = "1.10.0" 317 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 318 | category = "dev" 319 | optional = false 320 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 321 | 322 | [[package]] 323 | name = "pydantic" 324 | version = "1.8.2" 325 | description = "Data validation and settings management using python 3.6 type hinting" 326 | category = "main" 327 | optional = false 328 | python-versions = ">=3.6.1" 329 | 330 | [package.dependencies] 331 | typing-extensions = ">=3.7.4.3" 332 | 333 | [package.extras] 334 | dotenv = ["python-dotenv (>=0.10.4)"] 335 | email = ["email-validator (>=1.0.3)"] 336 | 337 | [[package]] 338 | name = "pyparsing" 339 | version = "2.4.7" 340 | description = "Python parsing module" 341 | category = "dev" 342 | optional = false 343 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 344 | 345 | [[package]] 346 | name = "pytest" 347 | version = "6.2.5" 348 | description = "pytest: simple powerful testing with Python" 349 | category = "dev" 350 | optional = false 351 | python-versions = ">=3.6" 352 | 353 | [package.dependencies] 354 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 355 | attrs = ">=19.2.0" 356 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 357 | iniconfig = "*" 358 | packaging = "*" 359 | pluggy = ">=0.12,<2.0" 360 | py = ">=1.8.2" 361 | toml = "*" 362 | 363 | [package.extras] 364 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 365 | 366 | [[package]] 367 | name = "pytest-asyncio" 368 | version = "0.15.1" 369 | description = "Pytest support for asyncio." 370 | category = "dev" 371 | optional = false 372 | python-versions = ">= 3.6" 373 | 374 | [package.dependencies] 375 | pytest = ">=5.4.0" 376 | 377 | [package.extras] 378 | testing = ["coverage", "hypothesis (>=5.7.1)"] 379 | 380 | [[package]] 381 | name = "regex" 382 | version = "2021.10.8" 383 | description = "Alternative regular expression module, to replace re." 384 | category = "dev" 385 | optional = false 386 | python-versions = "*" 387 | 388 | [[package]] 389 | name = "rfc3986" 390 | version = "1.5.0" 391 | description = "Validating URI References per RFC 3986" 392 | category = "dev" 393 | optional = false 394 | python-versions = "*" 395 | 396 | [package.dependencies] 397 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 398 | 399 | [package.extras] 400 | idna2008 = ["idna"] 401 | 402 | [[package]] 403 | name = "sniffio" 404 | version = "1.2.0" 405 | description = "Sniff out which async library your code is running under" 406 | category = "main" 407 | optional = false 408 | python-versions = ">=3.5" 409 | 410 | [[package]] 411 | name = "sqlalchemy" 412 | version = "1.4.25" 413 | description = "Database Abstraction Library" 414 | category = "main" 415 | optional = false 416 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 417 | 418 | [package.dependencies] 419 | greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (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\")"} 420 | 421 | [package.extras] 422 | aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] 423 | aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] 424 | asyncio = ["greenlet (!=0.4.17)"] 425 | asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.0)"] 426 | mariadb_connector = ["mariadb (>=1.0.1)"] 427 | mssql = ["pyodbc"] 428 | mssql_pymssql = ["pymssql"] 429 | mssql_pyodbc = ["pyodbc"] 430 | mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] 431 | mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] 432 | mysql_connector = ["mysql-connector-python"] 433 | oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] 434 | postgresql = ["psycopg2 (>=2.7)"] 435 | postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] 436 | postgresql_pg8000 = ["pg8000 (>=1.16.6)"] 437 | postgresql_psycopg2binary = ["psycopg2-binary"] 438 | postgresql_psycopg2cffi = ["psycopg2cffi"] 439 | pymysql = ["pymysql (<1)", "pymysql"] 440 | sqlcipher = ["sqlcipher3-binary"] 441 | 442 | [[package]] 443 | name = "starlette" 444 | version = "0.16.0" 445 | description = "The little ASGI library that shines." 446 | category = "main" 447 | optional = false 448 | python-versions = ">=3.6" 449 | 450 | [package.dependencies] 451 | anyio = ">=3.0.0,<4" 452 | 453 | [package.extras] 454 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] 455 | 456 | [[package]] 457 | name = "toml" 458 | version = "0.10.2" 459 | description = "Python Library for Tom's Obvious, Minimal Language" 460 | category = "dev" 461 | optional = false 462 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 463 | 464 | [[package]] 465 | name = "tomli" 466 | version = "1.2.1" 467 | description = "A lil' TOML parser" 468 | category = "dev" 469 | optional = false 470 | python-versions = ">=3.6" 471 | 472 | [[package]] 473 | name = "typing-extensions" 474 | version = "3.10.0.2" 475 | description = "Backported and Experimental Type Hints for Python 3.5+" 476 | category = "main" 477 | optional = false 478 | python-versions = "*" 479 | 480 | [[package]] 481 | name = "uvicorn" 482 | version = "0.15.0" 483 | description = "The lightning-fast ASGI server." 484 | category = "main" 485 | optional = false 486 | python-versions = "*" 487 | 488 | [package.dependencies] 489 | asgiref = ">=3.4.0" 490 | click = ">=7.0" 491 | h11 = ">=0.8" 492 | 493 | [package.extras] 494 | standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] 495 | 496 | [metadata] 497 | lock-version = "1.1" 498 | python-versions = "^3.9" 499 | content-hash = "2870bcb6a58ef0170163ea14da1845a066eb8e9ee119cffb95b9141bdc52479c" 500 | 501 | [metadata.files] 502 | alembic = [ 503 | {file = "alembic-1.7.4-py3-none-any.whl", hash = "sha256:e3cab9e59778b3b6726bb2da9ced451c6622d558199fd3ef914f3b1e8f4ef704"}, 504 | {file = "alembic-1.7.4.tar.gz", hash = "sha256:9d33f3ff1488c4bfab1e1a6dfebbf085e8a8e1a3e047a43ad29ad1f67f012a1d"}, 505 | ] 506 | anyio = [ 507 | {file = "anyio-3.3.2-py3-none-any.whl", hash = "sha256:c32da314c510b34a862f5afeaf8a446ffed2c2fde21583e654bd71ecfb5b744b"}, 508 | {file = "anyio-3.3.2.tar.gz", hash = "sha256:0b993a2ef6c1dc456815c2b5ca2819f382f20af98087cc2090a4afed3a501436"}, 509 | ] 510 | asgiref = [ 511 | {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, 512 | {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, 513 | ] 514 | asyncpg = [ 515 | {file = "asyncpg-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4fc0205fe4ddd5aeb3dfdc0f7bafd43411181e1f5650189608e5971cceacff1"}, 516 | {file = "asyncpg-0.24.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a7095890c96ba36f9f668eb552bb020dddb44f8e73e932f8573efc613ee83843"}, 517 | {file = "asyncpg-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ff5073d4b654e34bd5eaadc01dc4d68b8a9609084d835acd364cd934190a08d"}, 518 | {file = "asyncpg-0.24.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36c6806883786b19551bb70a4882561f31135dc8105a59662e0376cf5b2cbc5"}, 519 | {file = "asyncpg-0.24.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddffcb85227bf39cd1bedd4603e0082b243cf3b14ced64dce506a15b05232b83"}, 520 | {file = "asyncpg-0.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41704c561d354bef01353835a7846e5606faabbeb846214dfcf666cf53319f18"}, 521 | {file = "asyncpg-0.24.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ef6ae0a617fc13cc2ac5dc8e9b367bb83cba220614b437af9b67766f4b6b20"}, 522 | {file = "asyncpg-0.24.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eed43abc6ccf1dc02e0d0efc06ce46a411362f3358847c6b0ec9a43426f91ece"}, 523 | {file = "asyncpg-0.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:129d501f3d30616afd51eb8d3142ef51ba05374256bd5834cec3ef4956a9b317"}, 524 | {file = "asyncpg-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a458fc69051fbb67d995fdda46d75a012b5d6200f91e17d23d4751482640ed4c"}, 525 | {file = "asyncpg-0.24.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:556b0e92e2b75dc028b3c4bc9bd5162ddf0053b856437cf1f04c97f9c6837d03"}, 526 | {file = "asyncpg-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:a738f4807c853623d3f93f0fea11f61be6b0e5ca16ea8aeb42c2c7ee742aa853"}, 527 | {file = "asyncpg-0.24.0.tar.gz", hash = "sha256:dd2fa063c3344823487d9ddccb40802f02622ddf8bf8a6cc53885ee7a2c1c0c6"}, 528 | ] 529 | atomicwrites = [ 530 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 531 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 532 | ] 533 | attrs = [ 534 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 535 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 536 | ] 537 | black = [ 538 | {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, 539 | {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, 540 | ] 541 | certifi = [ 542 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 543 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 544 | ] 545 | charset-normalizer = [ 546 | {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, 547 | {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, 548 | ] 549 | click = [ 550 | {file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"}, 551 | {file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"}, 552 | ] 553 | colorama = [ 554 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 555 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 556 | ] 557 | fastapi = [ 558 | {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, 559 | {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, 560 | ] 561 | greenlet = [ 562 | {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, 563 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, 564 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, 565 | {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, 566 | {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, 567 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, 568 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, 569 | {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, 570 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, 571 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, 572 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, 573 | {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, 574 | {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, 575 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, 576 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, 577 | {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, 578 | {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, 579 | {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, 580 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, 581 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, 582 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, 583 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, 584 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, 585 | {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, 586 | {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, 587 | {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, 588 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, 589 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, 590 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, 591 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, 592 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, 593 | {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, 594 | {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, 595 | {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, 596 | {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, 597 | {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, 598 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, 599 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, 600 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, 601 | {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, 602 | {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, 603 | {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, 604 | {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, 605 | {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, 606 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, 607 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, 608 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, 609 | {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, 610 | {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, 611 | {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, 612 | ] 613 | h11 = [ 614 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 615 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 616 | ] 617 | httpcore = [ 618 | {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, 619 | {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, 620 | ] 621 | httpx = [ 622 | {file = "httpx-0.19.0-py3-none-any.whl", hash = "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435"}, 623 | {file = "httpx-0.19.0.tar.gz", hash = "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0"}, 624 | ] 625 | idna = [ 626 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 627 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 628 | ] 629 | iniconfig = [ 630 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 631 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 632 | ] 633 | mako = [ 634 | {file = "Mako-1.1.5-py2.py3-none-any.whl", hash = "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23"}, 635 | {file = "Mako-1.1.5.tar.gz", hash = "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3"}, 636 | ] 637 | markupsafe = [ 638 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 639 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 640 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 641 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 642 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 643 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 644 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 645 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 646 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 647 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 648 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 649 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 650 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 651 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 652 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 653 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 654 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 655 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 656 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 657 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 658 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 659 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 660 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 661 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 662 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 663 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 664 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 665 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 666 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 667 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 668 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 669 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 670 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 671 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 672 | ] 673 | mypy-extensions = [ 674 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 675 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 676 | ] 677 | packaging = [ 678 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 679 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 680 | ] 681 | pathspec = [ 682 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 683 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 684 | ] 685 | platformdirs = [ 686 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 687 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 688 | ] 689 | pluggy = [ 690 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 691 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 692 | ] 693 | psycopg2 = [ 694 | {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, 695 | {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, 696 | {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, 697 | {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, 698 | {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, 699 | {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, 700 | {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, 701 | {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, 702 | {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, 703 | ] 704 | py = [ 705 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 706 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 707 | ] 708 | pydantic = [ 709 | {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, 710 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, 711 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, 712 | {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, 713 | {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, 714 | {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, 715 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, 716 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, 717 | {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, 718 | {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, 719 | {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, 720 | {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, 721 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, 722 | {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, 723 | {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, 724 | {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, 725 | {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, 726 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, 727 | {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, 728 | {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, 729 | {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, 730 | {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, 731 | ] 732 | pyparsing = [ 733 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 734 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 735 | ] 736 | pytest = [ 737 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 738 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 739 | ] 740 | pytest-asyncio = [ 741 | {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, 742 | {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, 743 | ] 744 | regex = [ 745 | {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"}, 746 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"}, 747 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"}, 748 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"}, 749 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"}, 750 | {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"}, 751 | {file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"}, 752 | {file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"}, 753 | {file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"}, 754 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"}, 755 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"}, 756 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"}, 757 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"}, 758 | {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"}, 759 | {file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"}, 760 | {file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"}, 761 | {file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"}, 762 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"}, 763 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"}, 764 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"}, 765 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"}, 766 | {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"}, 767 | {file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"}, 768 | {file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"}, 769 | {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"}, 770 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"}, 771 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"}, 772 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"}, 773 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"}, 774 | {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"}, 775 | {file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"}, 776 | {file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"}, 777 | {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"}, 778 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"}, 779 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"}, 780 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"}, 781 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"}, 782 | {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"}, 783 | {file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"}, 784 | {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, 785 | {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, 786 | ] 787 | rfc3986 = [ 788 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 789 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 790 | ] 791 | sniffio = [ 792 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 793 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 794 | ] 795 | sqlalchemy = [ 796 | {file = "SQLAlchemy-1.4.25-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7"}, 797 | {file = "SQLAlchemy-1.4.25-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c"}, 798 | {file = "SQLAlchemy-1.4.25-cp27-cp27m-win32.whl", hash = "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c"}, 799 | {file = "SQLAlchemy-1.4.25-cp27-cp27m-win_amd64.whl", hash = "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e"}, 800 | {file = "SQLAlchemy-1.4.25-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c"}, 801 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c"}, 802 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6"}, 803 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41a916d815a3a23cb7fff8d11ad0c9b93369ac074e91e428075e088fe57d5358"}, 804 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:842c49dd584aedd75c2ee05f6c950730c3ffcddd21c5824ed0f820808387e1e3"}, 805 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-win32.whl", hash = "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266"}, 806 | {file = "SQLAlchemy-1.4.25-cp36-cp36m-win_amd64.whl", hash = "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0"}, 807 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177"}, 808 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831"}, 809 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08d9396a2a38e672133266b31ed39b2b1f2b5ec712b5bff5e08033970563316a"}, 810 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e93978993a2ad0af43f132be3ea8805f56b2f2cd223403ec28d3e7d5c6d39ed1"}, 811 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-win32.whl", hash = "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822"}, 812 | {file = "SQLAlchemy-1.4.25-cp37-cp37m-win_amd64.whl", hash = "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916"}, 813 | {file = "SQLAlchemy-1.4.25-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18"}, 814 | {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7"}, 815 | {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ebd69365717becaa1b618220a3df97f7c08aa68e759491de516d1c3667bba54"}, 816 | {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0cd2d5c7ea96d3230cb20acac3d89de3b593339c1447b4d64bfcf4eac1110"}, 817 | {file = "SQLAlchemy-1.4.25-cp38-cp38-win32.whl", hash = "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f"}, 818 | {file = "SQLAlchemy-1.4.25-cp38-cp38-win_amd64.whl", hash = "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59"}, 819 | {file = "SQLAlchemy-1.4.25-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2"}, 820 | {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec"}, 821 | {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:91cd87d1de0111eaca11ccc3d31af441c753fa2bc22df72e5009cfb0a1af5b03"}, 822 | {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90fe429285b171bcc252e21515703bdc2a4721008d1f13aa5b7150336f8a8493"}, 823 | {file = "SQLAlchemy-1.4.25-cp39-cp39-win32.whl", hash = "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3"}, 824 | {file = "SQLAlchemy-1.4.25-cp39-cp39-win_amd64.whl", hash = "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02"}, 825 | {file = "SQLAlchemy-1.4.25.tar.gz", hash = "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138"}, 826 | ] 827 | starlette = [ 828 | {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, 829 | {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, 830 | ] 831 | toml = [ 832 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 833 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 834 | ] 835 | tomli = [ 836 | {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, 837 | {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, 838 | ] 839 | typing-extensions = [ 840 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 841 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 842 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 843 | ] 844 | uvicorn = [ 845 | {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, 846 | {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, 847 | ] 848 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tutorial" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Piotr Rogulski "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | fastapi = "^0.70.0" 11 | SQLAlchemy = "^1.4.25" 12 | uvicorn = "^0.15.0" 13 | asyncpg = "^0.24.0" 14 | alembic = "^1.7.4" 15 | psycopg2 = "^2.9.1" 16 | 17 | [tool.poetry.dev-dependencies] 18 | black = "^21.9b0" 19 | pytest = "^6.2.5" 20 | httpx = "^0.19.0" 21 | pytest-asyncio = "^0.15.1" 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/tests/__init__.py -------------------------------------------------------------------------------- /tests/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/tests/app/__init__.py -------------------------------------------------------------------------------- /tests/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/tests/app/api/__init__.py -------------------------------------------------------------------------------- /tests/app/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rglsk/fastapi-sqlalchemy-1.4-async/8666013709fa5aba78a658723f33edcf16593016/tests/app/api/routes/__init__.py -------------------------------------------------------------------------------- /tests/app/api/routes/test_coupons.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from httpx import AsyncClient 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | from starlette import status 7 | 8 | from app.db.repositories.coupons import CouponsRepository 9 | from app.models.schema.coupons import InCouponSchema 10 | 11 | pytestmark = pytest.mark.asyncio 12 | 13 | 14 | async def test_coupon_create( 15 | async_client: AsyncClient, db_session: AsyncSession 16 | ) -> None: 17 | coupons_repository = CouponsRepository(db_session) 18 | payload = { 19 | "code": "PIOTR", 20 | "init_count": 100, 21 | } 22 | 23 | response = await async_client.post("/v1/coupons/", json=payload) 24 | coupon = await coupons_repository.get_by_id(response.json()["id"]) 25 | 26 | assert response.status_code == status.HTTP_201_CREATED 27 | assert response.json() == { 28 | "code": payload["code"], 29 | "init_count": payload["init_count"], 30 | "remaining_count": payload["init_count"], 31 | "id": str(coupon.id), 32 | } 33 | 34 | 35 | async def test_coupon_get_by_id( 36 | async_client: AsyncClient, db_session: AsyncSession 37 | ) -> None: 38 | payload = { 39 | "code": "PIOTR", 40 | "init_count": 100, 41 | } 42 | coupons_repository = CouponsRepository(db_session) 43 | coupon = await coupons_repository.create(InCouponSchema(**payload)) 44 | 45 | response = await async_client.get(f"/v1/coupons/{coupon.id}") 46 | 47 | assert response.status_code == status.HTTP_200_OK 48 | assert response.json() == { 49 | "code": payload["code"], 50 | "init_count": payload["init_count"], 51 | "remaining_count": payload["init_count"], 52 | "id": mock.ANY, 53 | } 54 | -------------------------------------------------------------------------------- /tests/app/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | from starlette import status 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | async def test_main(async_client: AsyncClient) -> None: 9 | response = await async_client.get("/") 10 | 11 | assert response.status_code == status.HTTP_200_OK 12 | assert response.json() == {"status": "ok"} 13 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import AsyncGenerator, Generator, Callable 3 | 4 | import pytest 5 | from fastapi import FastAPI 6 | 7 | from httpx import AsyncClient 8 | from sqlalchemy.ext.asyncio import AsyncSession 9 | from app.db.base import Base 10 | from app.db.session import async_session, engine 11 | 12 | 13 | @pytest.fixture(scope="session") 14 | def event_loop(request) -> Generator: 15 | """Create an instance of the default event loop for each test case.""" 16 | loop = asyncio.get_event_loop_policy().new_event_loop() 17 | yield loop 18 | loop.close() 19 | 20 | 21 | @pytest.fixture() 22 | async def db_session() -> AsyncSession: 23 | async with engine.begin() as connection: 24 | await connection.run_sync(Base.metadata.drop_all) 25 | await connection.run_sync(Base.metadata.create_all) 26 | async with async_session(bind=connection) as session: 27 | yield session 28 | await session.flush() 29 | await session.rollback() 30 | 31 | 32 | @pytest.fixture() 33 | def override_get_db(db_session: AsyncSession) -> Callable: 34 | async def _override_get_db(): 35 | yield db_session 36 | 37 | return _override_get_db 38 | 39 | 40 | @pytest.fixture() 41 | def app(override_get_db: Callable) -> FastAPI: 42 | from app.api.dependencies.db import get_db 43 | from app.main import app 44 | 45 | app.dependency_overrides[get_db] = override_get_db 46 | return app 47 | 48 | 49 | @pytest.fixture() 50 | async def async_client(app: FastAPI) -> AsyncGenerator: 51 | async with AsyncClient(app=app, base_url="http://test") as ac: 52 | yield ac 53 | --------------------------------------------------------------------------------