diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3e8a1eb004dacd23f9a11ab67470582c0802736..c76da2982249392009c04fc1aeb5fc051cdd0698 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,22 @@ on: - "*" jobs: + pre_job: + # continue-on-error: true # Uncomment once integration is finished + runs-on: ubuntu-latest + # Map a step output to a job output + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@master + with: + # All of these options are optional, so you can remove them if you are happy with the defaults + concurrent_skipping: 'same_content' + skip_after_successful_duplicate: 'true' pytest: + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/fastapi_template/cli.py b/fastapi_template/cli.py index ac215033f19f0a02dcb43bb6bf3f3dd6241d2341..c0f0fded043ce8033684c7891eaca9fc5250de77 100644 --- a/fastapi_template/cli.py +++ b/fastapi_template/cli.py @@ -187,6 +187,7 @@ def read_user_input(current_context: BuilderContext) -> BuilderContext: if current_context.db == DatabaseType.none: current_context.enable_migrations = False current_context.add_dummy = False + current_context.orm == ORM.none elif current_context.orm is None: current_context.orm = radiolist_dialog( "ORM", diff --git a/fastapi_template/input_model.py b/fastapi_template/input_model.py index 3c5c3699a9059dfe2aaa042e28e5919a6979eb0e..9a35b8f0c0ba0bde505a692411177cf2cb5b015c 100644 --- a/fastapi_template/input_model.py +++ b/fastapi_template/input_model.py @@ -20,6 +20,8 @@ class CIType(enum.Enum): @enum.unique class ORM(enum.Enum): + none = "none" + ormar = "ormar" sqlalchemy = "sqlalchemy" tortoise = "tortoise" @@ -29,6 +31,7 @@ class Database(BaseModel): image: Optional[str] driver: Optional[str] async_driver: Optional[str] + driver_short: Optional[str] port: Optional[int] @@ -44,13 +47,15 @@ DB_INFO = { name=DatabaseType.postgresql.value, image="postgres:13.4-buster", async_driver="postgresql+asyncpg", - driver="postgres", + driver_short="postgres", + driver="postgresql", port=5432, ), DatabaseType.mysql: Database( name=DatabaseType.mysql.value, image="bitnami/mysql:8.0.26", async_driver="mysql+aiomysql", + driver_short="mysql", driver="mysql", port=3306, ), @@ -58,6 +63,7 @@ DB_INFO = { name=DatabaseType.sqlite.value, image=None, async_driver="sqlite+aiosqlite", + driver_short="sqlite", driver="sqlite", port=None, ), diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml index 173e5b9d22f705f8d955381998301eda6bde1873..e488140a7bdd2c972329ddf8da00655090b77310 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -58,6 +58,9 @@ repos: entry: poetry run mypy language: system types: [python] + pass_filenames: false + args: + - "{{cookiecutter.project_name}}" - id: yesqa name: Remove usless noqa diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/README.md b/fastapi_template/template/{{cookiecutter.project_name}}/README.md index defe621cd2d2dbbc3247102e4b28bf35f24237a6..5b2015b3d0c883403f924045376ed60246e4557a 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/README.md +++ b/fastapi_template/template/{{cookiecutter.project_name}}/README.md @@ -37,7 +37,7 @@ docker save --output {{cookiecutter.project_name}}.tar {{cookiecutter.project_na If you want to migrate your database, you should run following commands: ```bash -{%- if cookiecutter.orm == 'sqlalchemy' %} +{%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} # To run all migrations untill the migration with revision_id. alembic upgrade "<revision_id>" @@ -53,7 +53,7 @@ aerich upgrade If you want to revert migrations, you should run: ```bash -{%- if cookiecutter.orm == 'sqlalchemy' %} +{%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} # revert all migrations up to: revision_id. alembic downgrade <revision_id> @@ -68,7 +68,7 @@ aerich downgrade To generate migrations you should run: ```bash -{%- if cookiecutter.orm == 'sqlalchemy' %} +{%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} # For automatic change detection. alembic revision --autogenerate diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json index 78ebd23812f7c9bde90a800095003ae7176912d0..b875a9bc84d6df0520f25a2eef5e9228a80ff1d9 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json @@ -37,9 +37,16 @@ "aerich.ini", "alembic.ini", "{{cookiecutter.project_name}}/db_sa/migrations", + "{{cookiecutter.project_name}}/db_ormar/migrations", "{{cookiecutter.project_name}}/db_tortoise/migrations" ] }, + "Alembic migrations": { + "enabled": "{{cookiecutter.orm in ['ormar', 'sqlalchemy']}}", + "resources": [ + "alembic.ini" + ] + }, "Gitlab CI": { "enabled": "{{cookiecutter.ci_type == 'gitlab_ci'}}", "resources": [ @@ -69,10 +76,13 @@ "{{cookiecutter.project_name}}/web/api/dummy", "{{cookiecutter.project_name}}/db_sa/dao", "{{cookiecutter.project_name}}/db_sa/models/dummy_model.py", + "{{cookiecutter.project_name}}/db_ormar/dao", + "{{cookiecutter.project_name}}/db_ormar/models/dummy_model.py", "{{cookiecutter.project_name}}/db_tortoise/dao", "{{cookiecutter.project_name}}/db_tortoise/models/dummy_model.py", "{{cookiecutter.project_name}}/tests/test_dummy.py", "{{cookiecutter.project_name}}/db_sa/migrations/versions/2021-08-16-16-55_2b7380507a71.py", + "{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-55_2b7380507a71.py", "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_pg.sql", "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql", "{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql" @@ -88,7 +98,6 @@ "SQLAlchemy ORM": { "enabled": "{{cookiecutter.orm == 'sqlalchemy'}}", "resources": [ - "alembic.ini", "{{cookiecutter.project_name}}/db_sa" ] }, @@ -99,6 +108,12 @@ "{{cookiecutter.project_name}}/db_tortoise" ] }, + "Ormar ORM": { + "enabled": "{{cookiecutter.orm == 'ormar'}}", + "resources": [ + "{{cookiecutter.project_name}}/db_ormar" + ] + }, "Postgresql DB": { "enabled": "{{cookiecutter.db_info.name == 'postgresql'}}", "resources": [ diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile index fc939a62c83749e110f85944170e2f1d0e9f92e6..0661f48dc456c25761f4ffa3f4cd9cd9bca337dc 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/Dockerfile @@ -1,5 +1,11 @@ FROM python:3.9.6-slim-buster +{% if cookiecutter.db_info.name == "mysql" -%} +RUN apt-get update && apt-get install -y \ + default-libmysqlclient-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* +{%- endif %} RUN pip install poetry==1.1.8 @@ -13,6 +19,13 @@ WORKDIR /app/src # Installing requirements RUN poetry install +{%- if cookiecutter.db_info.name == "mysql" %} +# Removing gcc +RUN apt-get purge -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* +{%- endif %} + # Copying actuall application COPY . /app/src/ RUN poetry install diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml index 6b46853d24e5671a1693250c91ce3aa59587421a..4e67431a54a6bf7037be17f26f2502f691ff8c84 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.yml @@ -93,7 +93,7 @@ services: migrator: image: {{cookiecutter.project_name}}:{{"${" }}{{cookiecutter.project_name | upper }}_VERSION:-latest{{"}"}} restart: "no" - {%- if cookiecutter.orm == 'sqlalchemy' %} + {%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} command: alembic upgrade head {%- elif cookiecutter.orm == 'tortoise' %} command: aerich upgrade diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml index 45d316f9bde96dbc1113bd2126d17530e156f9d6..f0d843b1d537bc00e7688f02574eeb2dad9f7131 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/app.yml @@ -28,7 +28,7 @@ spec: args: - -c - >- - {%- if cookiecutter.orm == 'sqlalchemy' %} + {%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} alembic upgrade head && {%- elif cookiecutter.orm == 'tortoise' %} aerich upgrade && diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml index 817e177a7f13742896b06ab949f82d510f6daff5..32af3d7af557e0947dacd643b890f6029cadd09c 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/kube/db.yml @@ -66,7 +66,7 @@ spec: - name: migrator image: {{cookiecutter.project_name}}:latest command: - {%- if cookiecutter.orm == 'sqlalchemy' %} + {%- if cookiecutter.orm in ['sqlalchemy', 'ormar'] %} - "alembic" - "upgrade" - "head" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index 6787148918beb43e5aa9d17bad755071c1525728..c9edcd8c98fd1e4893e2d6140c8bcfa8ad91eec8 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -19,32 +19,48 @@ yarl = "^1.6.3" ujson = "^4.2.0" {%- if cookiecutter.orm == "sqlalchemy" %} SQLAlchemy = {version = "^1.4", extras = ["mypy", "asyncio"]} -{%- elif cookiecutter.orm == "tortoise" %} -tortoise-orm = "^0.17.7" -{%- endif %} {%- if cookiecutter.enable_migrations == "True" %} -{%- if cookiecutter.orm == "sqlalchemy" %} alembic = "^1.6.5" -{%- elif cookiecutter.orm == "tortoise" %} -aerich = "^0.5.8" -{%- endif %} {%- endif %} {%- if cookiecutter.db_info.name == "postgresql" %} -{%- if cookiecutter.orm == "sqlalchemy" %} asyncpg = {version = "^0.24.0", extras = ["sa"]} -{%- elif cookiecutter.orm == "tortoise" %} -asyncpg = {version = "^0.24.0"} +{%- elif cookiecutter.db_info.name == "sqlite" %} +aiosqlite = "^0.17.0" +{%- elif cookiecutter.db_info.name == "mysql" %} +aiomysql = "^0.0.21" +mysqlclient = "^2.0.3" {%- endif %} {%- endif %} -{%- if cookiecutter.db_info.name == "sqlite" %} -aiosqlite = "^0.17.0" +{%- if cookiecutter.orm == "tortoise" %} +tortoise-orm = "^0.17.7" +{%- if cookiecutter.enable_migrations == "True" %} +aerich = "^0.5.8" {%- endif %} -{%- if cookiecutter.db_info.name == "mysql" %} +{%- if cookiecutter.db_info.name == "postgresql" %} +asyncpg = "^0.24.0" +{%- elif cookiecutter.db_info.name == "sqlite" %} +aiosqlite = "^0.17.0" +{%- elif cookiecutter.db_info.name == "mysql" %} aiomysql = "^0.0.21" -{%- if cookiecutter.orm == "tortoise" %} +mysqlclient = "^2.0.3" cryptography = "^3.4.8" {%- endif %} {%- endif %} +{%- if cookiecutter.orm == "ormar" %} +ormar = "^0.10.20" +{%- if cookiecutter.enable_migrations == "True" %} +alembic = "^1.6.5" +{%- endif %} +{%- if cookiecutter.db_info.name == "postgresql" %} +asyncpg = "^0.24.0" +psycopg2-binary = "^2.9.1" +{%- elif cookiecutter.db_info.name == "sqlite" %} +aiosqlite = "^0.17.0" +{%- elif cookiecutter.db_info.name == "mysql" %} +aiomysql = "^0.0.21" +mysqlclient = "^2.0.3" +{%- endif %} +{%- endif %} {%- if cookiecutter.enable_redis == "True" %} aioredis = {version = "^2.0.0", extras = ["hiredis"]} {%- endif %} @@ -63,7 +79,7 @@ isort = "^5.9.3" yesqa = "^1.2.3" pre-commit = "^2.11.0" wemake-python-styleguide = "^0.15.3" -black = "^21.7b0" +black = "==21.7b0" autoflake = "^1.4" {%- if cookiecutter.orm == "sqlalchemy" %} SQLAlchemy = {version = "^1.4", extras = ["mypy"]} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json index 81b49bae4d227d1bfdc86b195395acfa56f94606..c0d903c64fcfa5367ccfc82a6a285c641369eadb 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/replaceable_files.json @@ -1,6 +1,7 @@ { "{{cookiecutter.project_name}}/db": [ "{{cookiecutter.project_name}}/db_sa", + "{{cookiecutter.project_name}}/db_ormar", "{{cookiecutter.project_name}}/db_tortoise" ] } diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py index 245d40828b41043542ae320aa7839e07304b5686..bc3a63241359e7293a0370391fad838fd0eae7da 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py @@ -15,7 +15,6 @@ from {{cookiecutter.project_name}}.settings import settings from {{cookiecutter.project_name}}.web.application import get_app -{%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.orm == "sqlalchemy" %} from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine, AsyncConnection from sqlalchemy.orm import sessionmaker @@ -24,7 +23,10 @@ from {{cookiecutter.project_name}}.db.utils import create_database, drop_databas {%- elif cookiecutter.orm == "tortoise" %} from tortoise.contrib.test import finalizer, initializer from {{cookiecutter.project_name}}.db.config import MODELS_MODULES -{%- endif %} +{%- elif cookiecutter.orm == "ormar" %} +from sqlalchemy.engine import create_engine +from {{cookiecutter.project_name}}.db.config import database +from {{cookiecutter.project_name}}.db.utils import create_database, drop_database {%- endif %} import nest_asyncio @@ -52,7 +54,6 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: yield loop loop.close() -{%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.orm == "sqlalchemy" %} @pytest.fixture(scope="session") @pytest.mark.asyncio @@ -142,7 +143,36 @@ def initialize_db(event_loop: AbstractEventLoop) -> Generator[None, None, None]: finalizer() -{%- endif %} +{%- elif cookiecutter.orm == "ormar" %} + +@pytest.fixture(autouse=True) +@pytest.mark.asyncio +async def initialize_db() -> AsyncGenerator[None, None]: + """ + Create models and databases. + + :yield: new engine. + """ + from {{cookiecutter.project_name}}.db.meta import meta # noqa: WPS433 + from {{cookiecutter.project_name}}.db.models import load_all_models # noqa: WPS433 + + load_all_models() + + create_database() + + engine = create_engine(str(settings.db_url)) + with engine.begin() as conn: + meta.create_all(conn) + + engine.dispose() + + await database.connect() + + yield None + + await database.disconnect() + drop_database() + {%- endif %} @@ -159,7 +189,7 @@ def fake_redis() -> FakeRedis: @pytest.fixture() def fastapi_app( - {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} + {%- if cookiecutter.orm == "sqlalchemy" %} dbsession: AsyncSession, {%- endif %} {% if cookiecutter.enable_redis == "True" -%} @@ -172,7 +202,7 @@ def fastapi_app( :return: fastapi app with mocked dependencies. """ application = get_app() - {% if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" -%} + {% if cookiecutter.orm == "sqlalchemy" -%} application.dependency_overrides[get_db_session] = lambda: dbsession {%- endif %} {%- if cookiecutter.enable_redis == "True" %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/base.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/base.py new file mode 100644 index 0000000000000000000000000000000000000000..da0638972c675cce03a86aeac038523d58a163a4 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/base.py @@ -0,0 +1,11 @@ +from ormar import ModelMeta + +from {{cookiecutter.project_name}}.db.config import database +from {{cookiecutter.project_name}}.db.meta import meta + + +class BaseMeta(ModelMeta): + """Base metadata for models.""" + + database = database + metadata = meta diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/config.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/config.py new file mode 100644 index 0000000000000000000000000000000000000000..e87824917091f12d8e26540cc73065b8437037c8 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/config.py @@ -0,0 +1,5 @@ +from databases import Database + +from {{cookiecutter.project_name}}.settings import settings + +database = Database(str(settings.db_url)) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..db62a0afb2db870dce3368e05380185d0e71bbdc --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/__init__.py @@ -0,0 +1 @@ +"""DAO classes.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/dummy_dao.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/dummy_dao.py new file mode 100644 index 0000000000000000000000000000000000000000..2601535a2856f419faa8b13fd329e7355bf3dfb4 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/dao/dummy_dao.py @@ -0,0 +1,40 @@ +from typing import List, Optional + +from {{cookiecutter.project_name}}.db.models.dummy_model import DummyModel + + +class DummyDAO: + """Class for accessing dummy table.""" + + async def create_dummy_model(self, name: str) -> None: + """ + Add single dummy to session. + + :param name: name of a dummy. + """ + await DummyModel.objects.create(name=name) + + async def get_all_dummies(self, limit: int, offset: int) -> List[DummyModel]: + """ + Get all dummy models with limit/offset pagination. + + :param limit: limit of dummies. + :param offset: offset of dummies. + :return: stream of dummies. + """ + return await DummyModel.objects.limit(limit).offset(offset).all() + + async def filter( + self, + name: Optional[str] = None, + ) -> List[DummyModel]: + """ + Get specific dummy model. + + :param name: name of dummy instance. + :return: dummy models. + """ + query = DummyModel.objects + if name: + query = query.filter(DummyModel.name == name) + return await query.all() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/meta.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/meta.py new file mode 100644 index 0000000000000000000000000000000000000000..4c952541a88d0e55b08b7193d4689c7a6b37209c --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/meta.py @@ -0,0 +1,3 @@ +import sqlalchemy as sa + +meta = sa.MetaData() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6dccb9503a0c21a353e62e6bc4f85050f532638b --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/__init__.py @@ -0,0 +1 @@ +"""Alembic migraions.""" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/env.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/env.py new file mode 100644 index 0000000000000000000000000000000000000000..d6d5b94c7598a507aa848133108bce391fac78d1 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/env.py @@ -0,0 +1,84 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy.engine import create_engine, Connection + +from {{cookiecutter.project_name}}.db.meta import meta +from {{cookiecutter.project_name}}.db.models import load_all_models +from {{cookiecutter.project_name}}.settings import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + + +load_all_models() +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = meta + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + context.configure( + url=str(settings.db_url), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """ + Run actual sync migrations. + + :param connection: connection to the database. + """ + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """ + Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = create_engine(str(settings.db_url)) + + with connectable.connect() as connection: + do_run_migrations(connection) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/script.py.mako b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..55df2863d206fa1678abb4c92e90c45d3f85c114 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-53_819cbf6e030b.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-53_819cbf6e030b.py new file mode 100644 index 0000000000000000000000000000000000000000..3b22742ecaddccd9bef02c4f15a23b9ade7d531d --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-53_819cbf6e030b.py @@ -0,0 +1,22 @@ +"""Initial migration. + +Revision ID: 819cbf6e030b +Revises: +Create Date: 2021-08-16 16:53:05.484024 + +""" + + +# revision identifiers, used by Alembic. +revision = "819cbf6e030b" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-55_2b7380507a71.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-55_2b7380507a71.py new file mode 100644 index 0000000000000000000000000000000000000000..f06778fef111c1482ff584bcbcb769fdf9b87014 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/2021-08-16-16-55_2b7380507a71.py @@ -0,0 +1,32 @@ +"""Created Dummy Model. + +Revision ID: 2b7380507a71 +Revises: 819cbf6e030b +Create Date: 2021-08-16 16:55:25.157309 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "2b7380507a71" +down_revision = "819cbf6e030b" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "dummy_model", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=200), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("dummy_model") + # ### end Alembic commands ### diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/migrations/versions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/__init__.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..490649f1dc9f8e69cc65fc24ee2af630bfc7d18e --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/__init__.py @@ -0,0 +1,14 @@ +"""{{cookiecutter.project_name}} models.""" +import pkgutil +from pathlib import Path + + +def load_all_models() -> None: + """Load all models from this folder.""" + package_dir = Path(__file__).resolve().parent + modules = pkgutil.walk_packages( + path=[str(package_dir)], + prefix="{{cookiecutter.project_name}}.db.models.", + ) + for module in modules: + __import__(module.name) # noqa: WPS421 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/dummy_model.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/dummy_model.py new file mode 100644 index 0000000000000000000000000000000000000000..ca5f2249d9264b5b6c7d8233543b444f4ee0bc1e --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/models/dummy_model.py @@ -0,0 +1,13 @@ +import ormar + +from {{cookiecutter.project_name}}.db.base import BaseMeta + + +class DummyModel(ormar.Model): + """Model for demo purpose.""" + + class Meta(BaseMeta): + tablename = "dummy_model" + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=200) # noqa: WPS432 diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/utils.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1b918e5ea894c7c93793e2b81a2d703ffd7b6520 --- /dev/null +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/db_ormar/utils.py @@ -0,0 +1,86 @@ +import os +from sqlalchemy import text +from sqlalchemy.engine import URL, make_url +from sqlalchemy.engine import create_engine +from {{cookiecutter.project_name}}.settings import settings + +{% if cookiecutter.db_info.name == "postgresql" -%} +def create_database() -> None: + """Create a databse.""" + db_url = make_url(str(settings.db_url.with_path('/postgres'))) + engine = create_engine(db_url, isolation_level="AUTOCOMMIT") + + with engine.connect() as conn: + database_existance = conn.execute( + text( + f"SELECT 1 FROM pg_database WHERE datname='{settings.db_base}'", # noqa: E501, S608 + ) + ) + database_exists = database_existance.scalar() == 1 + + if database_exists: + drop_database() + + with engine.connect() as conn: # noqa: WPS440 + conn.execute( + text( + f'CREATE DATABASE "{settings.db_base}" ENCODING "utf8" TEMPLATE template1', # noqa: E501 + ) + ) + +def drop_database() -> None: + """Drop current database.""" + db_url = make_url(str(settings.db_url.with_path('/postgres'))) + engine = create_engine(db_url, isolation_level="AUTOCOMMIT") + with engine.connect() as conn: + disc_users = ( + "SELECT pg_terminate_backend(pg_stat_activity.pid) " # noqa: S608 + "FROM pg_stat_activity " + f"WHERE pg_stat_activity.datname = '{settings.db_base}' " + "AND pid <> pg_backend_pid();" + ) + conn.execute(text(disc_users)) + conn.execute(text(f'DROP DATABASE "{settings.db_base}"')) + + +{%- endif %} +{%- if cookiecutter.db_info.name == "mysql" %} +def create_database() -> None: + """Create a databse.""" + engine = create_engine(str(settings.db_url.with_path("/mysql"))) + + with engine.connect() as conn: + database_existance = conn.execute( + text( + "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA" # noqa: S608 + f" WHERE SCHEMA_NAME='{settings.db_base}';", + ) + ) + database_exists = database_existance.scalar() == 1 + + if database_exists: + drop_database() + + with engine.connect() as conn: # noqa: WPS440 + conn.execute( + text( + f'CREATE DATABASE {settings.db_base};' + ) + ) + +def drop_database() -> None: + """Drop current database.""" + engine = create_engine(str(settings.db_url.with_path("/mysql"))) + with engine.connect() as conn: + conn.execute(text(f'DROP DATABASE {settings.db_base};')) +{%- endif %} +{%- if cookiecutter.db_info.name == "sqlite" %} +def create_database() -> None: + """Create a databse.""" + +def drop_database() -> None: + """Drop current database.""" + if settings.db_file.exists(): + os.remove(settings.db_file) + +{%- endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py index 89e199ca245de0c23de097fe615863802439af58..5044b806f91b7ec1d0faf3fc0f00169d70e331b6 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py @@ -52,6 +52,8 @@ class Settings(BaseSettings): {%- if cookiecutter.orm == "sqlalchemy" %} scheme="{{cookiecutter.db_info.async_driver}}", {%- elif cookiecutter.orm == "tortoise" %} + scheme="{{cookiecutter.db_info.driver_short}}", + {%- else %} scheme="{{cookiecutter.db_info.driver}}", {%- endif %} path=f"///{self.db_file}" @@ -61,6 +63,8 @@ class Settings(BaseSettings): {%- if cookiecutter.orm == "sqlalchemy" %} scheme="{{cookiecutter.db_info.async_driver}}", {%- elif cookiecutter.orm == "tortoise" %} + scheme="{{cookiecutter.db_info.driver_short}}", + {%- else %} scheme="{{cookiecutter.db_info.driver}}", {%- endif %} host=self.db_host, diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py index bd518fe490cb15372a5ecf463988361d4ff772f5..655e2ce26d461d9d3a2aeb00100186795826dff3 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/tests/test_dummy.py @@ -24,7 +24,7 @@ async def test_creation( assert response.status_code == status.HTTP_200_OK {%- if cookiecutter.orm == "sqlalchemy" %} dao = DummyDAO(dbsession) - {%- elif cookiecutter.orm == "tortoise" %} + {%- elif cookiecutter.orm in ["tortoise", "ormar"] %} dao = DummyDAO() {%- endif %} instances = await dao.filter(name=test_name) @@ -42,7 +42,7 @@ async def test_getting( """Tests dummy instance retrieval.""" {%- if cookiecutter.orm == "sqlalchemy" %} dao = DummyDAO(dbsession) - {%- elif cookiecutter.orm == "tortoise" %} + {%- elif cookiecutter.orm in ["tortoise", "ormar"] %} dao = DummyDAO() {%- endif %} test_name = uuid.uuid4().hex diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py index fb2a3b46b120e15136e13ea6bc10d7148742dcd5..4feb7c7e86d3e7b508b170cc1c8b58ea27b0350d 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/api/router.py @@ -2,11 +2,9 @@ from fastapi.routing import APIRouter {%- if cookiecutter.enable_routers == "True" %} from {{cookiecutter.project_name}}.web.api import echo -{%- if cookiecutter.db_info.name != "none" %} {%- if cookiecutter.add_dummy == 'True' %} from {{cookiecutter.project_name}}.web.api import dummy {%- endif %} -{%- endif %} {%- if cookiecutter.enable_redis == "True" %} from {{cookiecutter.project_name}}.web.api import redis {%- endif %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py index b69107afceeecb8e4a274eb69b89822ab3845b0c..13a189ff9c96a484893f40e9c11de27fbd5a9c7e 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py @@ -8,7 +8,11 @@ from {{cookiecutter.project_name}}.settings import settings import aioredis {%- endif %} -{%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} +{%- if cookiecutter.orm == "ormar" %} +from {{cookiecutter.project_name}}.db.config import database +{%- endif %} + +{%- if cookiecutter.orm == "sqlalchemy" %} from asyncio import current_task from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -61,8 +65,10 @@ def startup(app: FastAPI) -> Callable[[], Awaitable[None]]: """ async def _startup() -> None: # noqa: WPS430 - {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} + {%- if cookiecutter.orm == "sqlalchemy" %} _setup_db(app) + {% elif cookiecutter.orm == "ormar" %} + await database.connect() {%- endif %} {%- if cookiecutter.enable_redis == "True" %} _setup_redis(app) @@ -81,8 +87,10 @@ def shutdown(app: FastAPI) -> Callable[[], Awaitable[None]]: """ async def _shutdown() -> None: # noqa: WPS430 - {%- if cookiecutter.db_info.name != "none" and cookiecutter.orm == "sqlalchemy" %} + {%- if cookiecutter.orm == "sqlalchemy" %} await app.state.db_engine.dispose() + {% elif cookiecutter.orm == "ormar" %} + await database.disconnect() {%- endif %} {%- if cookiecutter.enable_redis == "True" %} await app.state.redis_pool.disconnect() diff --git a/fastapi_template/tests/conftest.py b/fastapi_template/tests/conftest.py index b54a50de81ccd2dde473405fc98c34f5dab8c8ff..31bfa83c529c77775de013e22bc0cbcb7a64d595 100644 --- a/fastapi_template/tests/conftest.py +++ b/fastapi_template/tests/conftest.py @@ -1,6 +1,6 @@ +import re import pytest import os -import uuid import tempfile import shutil from faker import Faker @@ -17,8 +17,10 @@ def project_name() -> str: :return: project name. """ fake = Faker() - raw_name: str = fake.name_female() - return raw_name.lower().replace(" ", "_").replace("-", "_").replace(".", "_") + raw_name: str = ( + fake.name_female().lower().replace(" ", "_").replace("-", "_").replace(".", "_") + ) + return re.sub("_+", "_", raw_name).strip("_") @pytest.fixture(scope="session", autouse=True) @@ -62,20 +64,6 @@ def default_context(project_name: str) -> None: force=True, ) - -@pytest.fixture(autouse=True) -def lock_remover(project_name: str) -> None: - """ - Automatically removes lock file - after each test. - - :param project_name: generated project. - """ - yield - - Path("poetry.lock").unlink(missing_ok=True) - - @pytest.fixture(autouse=True) def default_dir(generator_start_dir: str) -> None: """ @@ -103,23 +91,6 @@ def docker_module_shutdown(generator_start_dir: str, project_name: str) -> None: if not project_dir.exists(): return os.chdir(project_dir) + Path("poetry.lock").unlink(missing_ok=True) run_docker_compose_command("down -v") os.chdir(cwd) - - -@pytest.fixture(scope="session", autouse=True) -def docker_shutdown(generator_start_dir: str, project_name: str) -> None: - """ - Cleans up docker context. - - :param generator_start_dir: generator dir. - :param project_name: name of the project. - """ - yield - cwd = os.getcwd() - project_dir = Path(generator_start_dir) / project_name - if not project_dir.exists(): - return - os.chdir(project_dir) - run_docker_compose_command("down -v --rmi=all") - os.chdir(cwd) diff --git a/fastapi_template/tests/test_generator.py b/fastapi_template/tests/test_generator.py index 8fc80c4009d55862b8ce9858d1b74cf986172f03..b4c234d890eab2d833a52716854fbc52f465c10b 100644 --- a/fastapi_template/tests/test_generator.py +++ b/fastapi_template/tests/test_generator.py @@ -30,19 +30,19 @@ def test_default_without_db(default_context: BuilderContext): DatabaseType.mysql, ], ) -@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise, ORM.ormar]) def test_default_with_db(default_context: BuilderContext, db: DatabaseType, orm: ORM): run_default_check(init_context(default_context, db, orm)) -@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise, ORM.ormar]) def test_without_routers(default_context: BuilderContext, orm: ORM): context = init_context(default_context, DatabaseType.postgresql, orm) context.enable_routers = False run_default_check(context) -@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise, ORM.ormar]) def test_without_migrations(default_context: BuilderContext, orm: ORM): context = init_context(default_context, DatabaseType.postgresql, orm) context.enable_migrations = False @@ -54,7 +54,7 @@ def test_with_selfhosted_swagger(default_context: BuilderContext): run_default_check(default_context) -@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise]) +@pytest.mark.parametrize("orm", [ORM.sqlalchemy, ORM.tortoise, ORM.ormar]) def test_without_dummy(default_context: BuilderContext, orm: ORM): context = init_context(default_context, DatabaseType.postgresql, orm) context.add_dummy = False diff --git a/poetry.lock b/poetry.lock index 55018d978ffb081c8f64bcee4ae8ac332a65178c..ab934c86c10d447b8904146f97216adfc50ff151 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "arrow" -version = "1.1.1" +version = "1.2.0" description = "Better dates & times for Python" category = "main" optional = false @@ -56,7 +56,7 @@ chardet = ">=3.0.2" [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -102,7 +102,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "8.0.2" description = "Composable command line interface toolkit" category = "main" optional = false @@ -147,7 +147,7 @@ python-versions = "*" [[package]] name = "faker" -version = "8.14.0" +version = "8.16.0" description = "Faker is a Python package that generates fake data for you." category = "dev" optional = false @@ -159,19 +159,19 @@ text-unidecode = "1.3" [[package]] name = "filelock" -version = "3.1.0" +version = "3.3.0" description = "A platform independent file lock." category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.extras] docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["coverage (>=4)", "pytest (>=4)", "pytest-cov"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] name = "identify" -version = "2.2.15" +version = "2.3.0" description = "File identification library for Python" category = "main" optional = false @@ -198,7 +198,7 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.1" +version = "3.0.2" description = "A very fast and expressive template engine." category = "main" optional = false @@ -341,7 +341,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.6.1" +version = "1.7.0" description = "Python bindings for libgit2." category = "main" optional = false @@ -528,8 +528,8 @@ content-hash = "94fd225d67fed94f6fd7d887a3f8a5c0c19cf2d4c621a15967d5653cfa6cffad [metadata.files] arrow = [ - {file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"}, - {file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"}, + {file = "arrow-1.2.0-py3-none-any.whl", hash = "sha256:8fb7d9d3d4bf90e49e734c22fa077bdd0964135c4b8120de2510575a8d1f620c"}, + {file = "arrow-1.2.0.tar.gz", hash = "sha256:16fc29bbd9e425e3eb0fef3018297910a0f4568f21116fc31771e2760a50e074"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -548,8 +548,8 @@ binaryornot = [ {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, @@ -611,8 +611,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"}, + {file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -627,16 +627,16 @@ distlib = [ {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] faker = [ - {file = "Faker-8.14.0-py3-none-any.whl", hash = "sha256:7b116034973a9a977a34a8a380354028150edf69f6cfbe55c03a852dd0a4116b"}, - {file = "Faker-8.14.0.tar.gz", hash = "sha256:2649789e3e0c354dde1b8257d2ba7ed663fc3201e41277581de65c17e8aab10a"}, + {file = "Faker-8.16.0-py3-none-any.whl", hash = "sha256:bb10913b9d3ac2aa37180f816c82040e81f9e0c32cb08445533f293cec8930bf"}, + {file = "Faker-8.16.0.tar.gz", hash = "sha256:d70b375d0af0e4c3abd594003691a1055a96281a414884e623d27bccc7d781da"}, ] filelock = [ - {file = "filelock-3.1.0-py2.py3-none-any.whl", hash = "sha256:d9e9c7d8191e915339843c81c90d3e44f7c84e5fb03bdc6b1b4d019025cf953b"}, - {file = "filelock-3.1.0.tar.gz", hash = "sha256:78925788ce8c8945fac28a68c1d05cf33a6a6c4fba14fe02835122c53268ceef"}, + {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"}, + {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"}, ] identify = [ - {file = "identify-2.2.15-py2.py3-none-any.whl", hash = "sha256:de83a84d774921669774a2000bf87ebba46b4d1c04775f4a5d37deff0cf39f73"}, - {file = "identify-2.2.15.tar.gz", hash = "sha256:528a88021749035d5a39fe2ba67c0642b8341aaf71889da0e1ed669a429b87f0"}, + {file = "identify-2.3.0-py2.py3-none-any.whl", hash = "sha256:d1e82c83d063571bb88087676f81261a4eae913c492dafde184067c584bc7c05"}, + {file = "identify-2.3.0.tar.gz", hash = "sha256:fd08c97f23ceee72784081f1ce5125c8f53a02d3f2716dde79a6ab8f1039fea5"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -647,8 +647,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, + {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, ] jinja2-time = [ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, @@ -771,27 +771,25 @@ pydantic = [ {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, ] pygit2 = [ - {file = "pygit2-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:547429774c11f5bc9d20a49aa86e4bd13c90a55140504ef05f55cf424470ee34"}, - {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e75865d7b6fc161d93b16f10365eaad353cd546e302a98f2de2097ddea1066b"}, - {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4a64b6090308ffd1c82e2dd4316cb79483715387b13818156d516134a5b17c"}, - {file = "pygit2-1.6.1-cp36-cp36m-win32.whl", hash = "sha256:2666a3970b2ea1222a9f0463b466f98c8d564f29ec84cf0a58d9b0d3865dbaaf"}, - {file = "pygit2-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2de12ca2d3b7eb86106223b40b2edc0c61103c71e7962e53092c6ddef71a194"}, - {file = "pygit2-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9c1d96c66fb6e69ec710078a73c19edff420bc1db430caa9e03a825eede3f25c"}, - {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:454d42550fa6a6cd0e6a6ad9ab3f3262135fd157f57bad245ce156c36ee93370"}, - {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce0827b77dd2f8a3465bdc181c4e65f27dd12dbd92635c038e58030cc90c2de0"}, - {file = "pygit2-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:b0161a141888d450eb821472fdcdadd14a072ddeda841fee9984956d34d3e19d"}, - {file = "pygit2-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:af2fa259b6f7899227611ab978c600695724e85965836cb607d8b1e70cfea9b3"}, - {file = "pygit2-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0e1e02c28983ddc004c0f54063f3e46fca388225d468e32e16689cfb750e0bd6"}, - {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5dadc4844feb76cde5cc9a37656326a361dd8b5c8e8f8674dcd4a5ecf395db3"}, - {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07458e4172a31318663295083b43f957d611145738ff56aa76db593542a6e8"}, - {file = "pygit2-1.6.1-cp38-cp38-win32.whl", hash = "sha256:7a0c0a1f11fd41f57e8c6c64d903cc7fa4ec95d15592270be3217ed7f78eb023"}, - {file = "pygit2-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:2fd5c1b2d84dc6084f1bda836607afe37e95186a53a5a827a69083415e57fe4f"}, - {file = "pygit2-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9b88b7e9a5286a71be0b6c307f0523c9606aeedff6b61eb9c440e18817fa641"}, - {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac12d32b714c3383ebccffee5eb6aff0b69a2542a40a664fd5ad370afcb28ee7"}, - {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe682ed6afd2ab31127f6a502cf3e002dc1cc8d26c36a5d49dfd180250351eb6"}, - {file = "pygit2-1.6.1-cp39-cp39-win32.whl", hash = "sha256:dbbf66a23860aa899949068ac9b503b4bc21e6063e8f53870440adbdc909405e"}, - {file = "pygit2-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:f90775afb11f69376e2af21ab56fcfbb52f6bc84117059ddf0355f81e5e36352"}, - {file = "pygit2-1.6.1.tar.gz", hash = "sha256:c3303776f774d3e0115c1c4f6e1fc35470d15f113a7ae9401a0b90acfa1661ac"}, + {file = "pygit2-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca4c0e445efa0144082a385e83316abea92c749a95368397a64bc988aedc088e"}, + {file = "pygit2-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ef6da92eb3be2a5342759a194be7d7f8b1997ef6023eed75f7a97d7cf7ec191"}, + {file = "pygit2-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb91e8ba783e79111805595155a2177fbb2efedd796673c4b5daba0e50cb24"}, + {file = "pygit2-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2e6fc9cb4213d501f7dd11c23b02d9e6206e1a5615e23cfbc95aaeb300a0f5d6"}, + {file = "pygit2-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2ec6c77194e337c133c4a40c11992597a8c55ac91219cd03ec054de9d20b486"}, + {file = "pygit2-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc6758852a3969b7feb2f6395c3d6b90bc7c337f574134c1ed718216009aef98"}, + {file = "pygit2-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:4d6b8742fd30931e85bbc792237a9ec23a84548707d0f59312b59a96583565b1"}, + {file = "pygit2-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ddbba54869e0ed7878a3ef9ffebc756f47f1609294130c04b83a08896bff0b18"}, + {file = "pygit2-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3cbc2d212ea1c0c802a0f8141b3a4b6690ceb8c858731cb10cb60a41c6bc188"}, + {file = "pygit2-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9817105a3c116d3eb678b0289d6fae04668904018d5a08f61adc4f64707fb2ed"}, + {file = "pygit2-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3475d8a0171314e32e8559e689a573e4d6cf5fc6e4c9067c3b4e3e96fbec501b"}, + {file = "pygit2-1.7.0-cp38-cp38-win32.whl", hash = "sha256:e550bd9e8e20dcfd2593a60ff95225ad615419cfad4b66c50f40de64cb48bcf7"}, + {file = "pygit2-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2a012222743ecdd8f1e676e9bb9afc2e6b69abb3f350b8012b87d83749b85bb1"}, + {file = "pygit2-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:85c570305bf2c694e1224124e608cd72d4c2d6b92b0c677164cc015634dca17a"}, + {file = "pygit2-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:352a249778ea944d69bb58a0b43c715ba6e88b472f50583c6fbbfd0ce52540fc"}, + {file = "pygit2-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:422c588e6f00d1b3c13480afcd2776a835b442b89720e7f55f2212f8c292afae"}, + {file = "pygit2-1.7.0-cp39-cp39-win32.whl", hash = "sha256:61bfec915ae4f6dc885127978d041996ed9702a7c7ae55f1476f620ca33f561d"}, + {file = "pygit2-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:542aa9b855eb542b5fbe3b4e49fb29852775cc61959bca83eff03d8638e905c9"}, + {file = "pygit2-1.7.0.tar.gz", hash = "sha256:602bffa8b4dbc185a6c7f36515563b600e0ee9002583c97ae3150eedaf340edb"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},