From 96b4e18aafd4bfef7a5198f5ddc0e3c72322e30c Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Tue, 8 Nov 2022 16:11:42 +0400 Subject: [PATCH 01/11] remove useless files --- .dockerignore | 7 ------- .editorconfig | 17 ----------------- .mypy.ini | 36 ------------------------------------ init.sql | 1 - 4 files changed, 61 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .editorconfig delete mode 100644 .mypy.ini delete mode 100644 init.sql diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8eaf35d..0000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -.venv/ -.vscode/ -.ops/.helm/ -.git/ -Dockerfile -*.Dockerfile -README.md diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8506cb5..0000000 --- a/.editorconfig +++ /dev/null @@ -1,17 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 2 - -[*.py] -indent_size = 4 -line_length = 88 -known_first_party = app -multi_line_output = 3 -recursive = true -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true diff --git a/.mypy.ini b/.mypy.ini deleted file mode 100644 index 47db4e7..0000000 --- a/.mypy.ini +++ /dev/null @@ -1,36 +0,0 @@ -[mypy] -plugins = pydantic.mypy, sqlmypy - - -; Output format options -error_summary = True -show_error_context = True -show_error_codes = True -color_output = True - -; Usual checks -warn_return_any = True -warn_unused_configs = True -warn_unused_ignores = True - -; Important checks -strict_equality = True -warn_unreachable = True - -; Additional moderate checks -warn_no_return = True - -; Additional strict checks -disallow_untyped_defs = True -no_implicit_optional = True - - -; Module-level whitelist -[mypy-alembic.*] -ignore_missing_imports = True -[mypy-asyncpg.exceptions.*] -ignore_missing_imports = True -[mypy-pytest.*] -ignore_missing_imports = True -[mypy-sqlalchemy_utils.*] -ignore_missing_imports = True diff --git a/init.sql b/init.sql deleted file mode 100644 index 14379bd..0000000 --- a/init.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE test; From abcf49a65347d7205f976f43e4065dadc592776f Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Tue, 8 Nov 2022 16:13:33 +0400 Subject: [PATCH 02/11] update pre-commit --- .flake8 | 8 +-- .pre-commit-config.yaml | 56 ++++++++++++------- .../pg_versions/2022-04-29_12-12_init_db.py | 6 +- .../2022-06-01_12-35_add_user_table.py | 38 +++++++------ app/api/deps.py | 3 +- app/core/config.py | 3 +- app/crud/base.py | 2 +- app/db/session.py | 2 +- app/utils/email.py | 15 +++-- tests/api/v1/test_user.py | 8 +-- tests/conftest.py | 4 -- tests/fixtures/fastapi.py | 2 +- tests/fixtures/postgres.py | 2 +- 13 files changed, 81 insertions(+), 68 deletions(-) diff --git a/.flake8 b/.flake8 index 1db6598..0eadb76 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -max-line-length = 120 -exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache,app/db/base.py -select = C,E,F,W,B,B950 -extend-ignore = E203, E501 +max-line-length = 88 +select = C,E,F,W,B,B9 +ignore = E203, E501, W503 +exclude = __init__.py,app/db/base.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 763c5b3..9d96ad7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,45 @@ -exclude: 'node_modules|alembic|migrations|.git|.tox' -default_stages: [commit] - repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.3.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml + - id: check-added-large-files - id: check-toml - - - repo: https://github.com/psf/black - rev: 22.3.0 + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v2.37.1 hooks: - - id: black - - - repo: https://github.com/timothycrosley/isort - rev: 5.9.3 + - id: pyupgrade + args: + - --py39-plus + - --keep-runtime-typing + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: + - --recursive + - --in-place + - --remove-all-unused-imports + - --remove-unused-variables + - --exclude + - __init__.py,base.py,app/db/base.py + - --remove-duplicate-keys + - repo: https://github.com/pycqa/isort + rev: 5.10.1 hooks: - id: isort - - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + args: + - --line-length=88 + - --profile + - black + - repo: https://github.com/psf/black + rev: 22.6.0 hooks: - - id: flake8 - additional_dependencies: [flake8-isort] + - id: black + args: + - --preview + - --line-length=88 diff --git a/alembic/pg_versions/2022-04-29_12-12_init_db.py b/alembic/pg_versions/2022-04-29_12-12_init_db.py index 423ee4b..8acd0fa 100644 --- a/alembic/pg_versions/2022-04-29_12-12_init_db.py +++ b/alembic/pg_versions/2022-04-29_12-12_init_db.py @@ -1,16 +1,14 @@ """init db Revision ID: 2a124563c49c -Revises: +Revises: Create Date: 2022-04-29 12:12:18.292607 """ -from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '2a124563c49c' +revision = "2a124563c49c" down_revision = None branch_labels = None depends_on = None diff --git a/alembic/pg_versions/2022-06-01_12-35_add_user_table.py b/alembic/pg_versions/2022-06-01_12-35_add_user_table.py index 393a796..bb48573 100644 --- a/alembic/pg_versions/2022-06-01_12-35_add_user_table.py +++ b/alembic/pg_versions/2022-06-01_12-35_add_user_table.py @@ -5,38 +5,40 @@ Create Date: 2022-06-01 12:35:15.854573 """ -from alembic import op import sqlalchemy as sa import sqlalchemy_utils.types.email +from alembic import op + # revision identifiers, used by Alembic. -revision = 'e0538c856957' -down_revision = '2a124563c49c' +revision = "e0538c856957" +down_revision = "2a124563c49c" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sqlalchemy_utils.types.email.EmailType(), nullable=False), - sa.Column('hashed_password', sa.String(), nullable=False), - sa.Column('phone_number', sa.String(), nullable=True), - sa.Column('first_name', sa.String(), nullable=True), - sa.Column('last_name', sa.String(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_superuser', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("email", sqlalchemy_utils.types.email.EmailType(), nullable=False), + sa.Column("hashed_password", sa.String(), nullable=False), + sa.Column("phone_number", sa.String(), nullable=True), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_superuser", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) - op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_user_id'), table_name='user') - op.drop_index(op.f('ix_user_email'), table_name='user') - op.drop_table('user') + op.drop_index(op.f("ix_user_id"), table_name="user") + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") # ### end Alembic commands ### diff --git a/app/api/deps.py b/app/api/deps.py index a7a6911..eea9c97 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,4 +1,5 @@ -from typing import Any, Generator, Optional +from collections.abc import Generator +from typing import Any, Optional from databases import Database from fastapi import Depends, HTTPException, status diff --git a/app/core/config.py b/app/core/config.py index f715df2..5fee309 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Mapping from pathlib import Path -from typing import Any, Mapping +from typing import Any from pydantic import AnyHttpUrl, BaseSettings, EmailStr, PostgresDsn, validator diff --git a/app/crud/base.py b/app/crud/base.py index 8d5a114..37ae0e5 100644 --- a/app/crud/base.py +++ b/app/crud/base.py @@ -10,7 +10,7 @@ class CRUDBase(Generic[ModelTable, CreateSchemaType, UpdateSchemaType]): - def __init__(self, model: Type[ModelTable]): + def __init__(self, model: type[ModelTable]): """ CRUD object with async default methods to Create, Read, Update, Delete (CRUD). diff --git a/app/db/session.py b/app/db/session.py index 4779c95..ceb0487 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,5 +1,5 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Generator from sqlalchemy import create_engine from sqlalchemy.engine import Engine diff --git a/app/utils/email.py b/app/utils/email.py index c0a2d6e..e060700 100644 --- a/app/utils/email.py +++ b/app/utils/email.py @@ -12,9 +12,7 @@ def send_email(email_to: str, email_subject: str, email_message: str) -> None: return message = ( - f"From: {settings.EMAILS_FROM_NAME}\n" - f"Subject: {email_subject}\n" - f"{email_message}" + f"From: {settings.EMAILS_FROM_NAME}\nSubject: {email_subject}\n{email_message}" ) context = ssl.create_default_context() with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server: @@ -41,10 +39,11 @@ def send_reset_password_email(email_to: str, fullname: str, token: str) -> None: """ subject = f"{settings.PROJECT_NAME} - Reset Password" message = ( - f"We received a request to recover the password for {fullname} with email {email_to}\n" - f"Reset your password by clicking the link below:\n" - f"{settings.SERVER_HOST}/reset-password?token={token}\n" - f"The reset password link will expire in {settings.EMAIL_RESET_TOKEN_EXPIRE_MINUTES} minutes.\n" - f"If you didn't request a password recovery you can disregard this email." + f"We received a request to recover the password for {fullname} with email" + f" {email_to}\nReset your password by clicking the link" + f" below:\n{settings.SERVER_HOST}/reset-password?token={token}\nThe reset" + " password link will expire in" + f" {settings.EMAIL_RESET_TOKEN_EXPIRE_MINUTES} minutes.\nIf you didn't request" + " a password recovery you can disregard this email." ) send_email(email_to=email_to, email_subject=subject, email_message=message) diff --git a/tests/api/v1/test_user.py b/tests/api/v1/test_user.py index c7b146e..d31313c 100644 --- a/tests/api/v1/test_user.py +++ b/tests/api/v1/test_user.py @@ -1,5 +1,3 @@ -from typing import Dict - import pytest from databases import Database from httpx import AsyncClient @@ -13,7 +11,7 @@ async def test_get_users_superuser_me( - api_client: AsyncClient, superuser_token_headers: Dict[str, str] + api_client: AsyncClient, superuser_token_headers: dict[str, str] ) -> None: response = await api_client.get( f"{settings.API_V1_STR}/user/me", headers=superuser_token_headers @@ -29,7 +27,7 @@ async def test_get_users_superuser_me( async def test_get_users_normal_user_me( - api_client: AsyncClient, normal_user_token_headers: Dict[str, str] + api_client: AsyncClient, normal_user_token_headers: dict[str, str] ) -> None: response = await api_client.get( f"{settings.API_V1_STR}/user/me", headers=normal_user_token_headers @@ -120,7 +118,7 @@ async def test_create_user_existing_username( async def test_create_user_by_normal_user( - api_client: AsyncClient, normal_user_token_headers: Dict[str, str] + api_client: AsyncClient, normal_user_token_headers: dict[str, str] ) -> None: username = random_email() password = random_lower_string() diff --git a/tests/conftest.py b/tests/conftest.py index 6358d6d..9c0fa90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1 @@ # flake8: noqa -from .fixtures.common import * -from .fixtures.db import * -from .fixtures.fastapi import * -from .fixtures.postgres import * diff --git a/tests/fixtures/fastapi.py b/tests/fixtures/fastapi.py index ba620bc..c76a304 100644 --- a/tests/fixtures/fastapi.py +++ b/tests/fixtures/fastapi.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator import pytest import pytest_asyncio diff --git a/tests/fixtures/postgres.py b/tests/fixtures/postgres.py index b05767d..5181060 100644 --- a/tests/fixtures/postgres.py +++ b/tests/fixtures/postgres.py @@ -1,4 +1,4 @@ -from typing import Generator +from collections.abc import Generator import pytest import pytest_asyncio From 7ad134e67c556d64aa3223ed84e39f47dc6a90dd Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Tue, 8 Nov 2022 16:16:36 +0400 Subject: [PATCH 03/11] add template environment file --- .env.template | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .env.template diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..9a27c3e --- /dev/null +++ b/.env.template @@ -0,0 +1,22 @@ +# Base +SECRET_KEY=some-secret-key +PROJECT_NAME='Some Project Name' +SERVER_HOST=http://0.0.0.0:8881 + +# Databases +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_HOST=postgres +POSTGRES_DB=postgres +POSTGRES_TEST_DB=test +PGDATA=/var/lib/postgresql/data/pgdata + +# SMTP +SMTP_USER=some-email +SMTP_PASSWORD=some-password + +# Auth +FIRST_SUPERUSER_USERNAME=user@user.com +FIRST_SUPERUSER_PASSWORD=password +FIRST_SUPERUSER_FIRST_NAME='first name' +FIRST_SUPERUSER_LAST_NAME='last name' From fe925c39409c268dc13cf1e21a71e1a138f214c9 Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Tue, 8 Nov 2022 16:41:16 +0400 Subject: [PATCH 04/11] update alembic config --- alembic.ini | 73 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/alembic.ini b/alembic.ini index 859dba2..dc66b60 100644 --- a/alembic.ini +++ b/alembic.ini @@ -1,18 +1,73 @@ +# A generic, single database configuration. + [alembic] -databases = postgres +# path to migration scripts +script_location = migrations -[DEFAULT] -prepend_sys_path = . -script_location = alembic +# template used to generate migration files file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(hour).2d-%%(minute).2d_%%(slug)s -truncate_slug_length = 60 -version_path_separator = space +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +timezone = UTC + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os +# Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/postgres + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples -[postgres] -version_locations = alembic/pg_versions -sqlalchemy.engine = app.db.session.postgres_engine +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME +# Logging configuration [loggers] keys = root,sqlalchemy,alembic From 2da168b0952d536fcbeb5692f426cff3ecc4b12d Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 15:47:28 +0400 Subject: [PATCH 05/11] add debug mode --- .env.template | 1 + app/core/config.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.template b/.env.template index 9a27c3e..85f2ca6 100644 --- a/.env.template +++ b/.env.template @@ -2,6 +2,7 @@ SECRET_KEY=some-secret-key PROJECT_NAME='Some Project Name' SERVER_HOST=http://0.0.0.0:8881 +DEBUG=false # Databases POSTGRES_USER=postgres diff --git a/app/core/config.py b/app/core/config.py index 5fee309..0ca74b1 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -18,6 +18,7 @@ class Settings(BaseSettings): # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \ # "http://localhost:8080", "http://0.0.0.0:8001"]' BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = [] + DEBUG: bool = False @validator("BACKEND_CORS_ORIGINS", pre=True) def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: From 40d7ac1fbcbbbaa19df917ec2088e3d1e7247f1f Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 15:48:09 +0400 Subject: [PATCH 06/11] rename docker-compose --- docker-compose.yml => docker-compose.dev.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker-compose.yml => docker-compose.dev.yml (100%) diff --git a/docker-compose.yml b/docker-compose.dev.yml similarity index 100% rename from docker-compose.yml rename to docker-compose.dev.yml From 8cf8a72ec95ba2938bf385431d31412b84c27e8b Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 15:48:40 +0400 Subject: [PATCH 07/11] update alembic --- alembic.ini | 2 +- alembic/env.py | 122 +++++++++++++++++++++++++++------------------- app/db/session.py | 35 ++++--------- 3 files changed, 83 insertions(+), 76 deletions(-) diff --git a/alembic.ini b/alembic.ini index dc66b60..86a4e7f 100644 --- a/alembic.ini +++ b/alembic.ini @@ -2,7 +2,7 @@ [alembic] # path to migration scripts -script_location = migrations +script_location = alembic # template used to generate migration files file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(hour).2d-%%(minute).2d_%%(slug)s diff --git a/alembic/env.py b/alembic/env.py index 737009c..3ba5074 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,68 +1,90 @@ -from __future__ import with_statement - -import os -import sys +import asyncio from logging.config import fileConfig -from sqlalchemy.engine import Engine -from sqlalchemy.schema import MetaData +from sqlalchemy import engine_from_config, pool +from sqlalchemy.ext.asyncio import AsyncEngine from alembic import context +from app.core.config import settings from app.db.base import postgres_metadata -from app.db.session import postgres_engine - -parent_dir = os.path.abspath(os.getcwd()) -sys.path.append(parent_dir) +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. 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 = postgres_metadata -def render_item(obj_type, obj, autogen_context): - """Apply custom rendering for selected items.""" - if obj_type == "type" and obj.__class__.__module__.startswith("sqlalchemy_utils."): - autogen_context.imports.add(f"import {obj.__class__.__module__}") - if hasattr(obj, "choices"): - return f"{obj.__class__.__module__}.{obj.__class__.__name__}(choices={obj.choices})" - else: - return f"{obj.__class__.__module__}.{obj.__class__.__name__}()" - - # default rendering for other objects - return False - - -class Migrator: - def __init__(self, engine: Engine, target_metadata: MetaData) -> None: - self.engine = engine - self.target_metadata = target_metadata - - def migrate(self) -> None: - connectable = config.attributes.get("connection", None) - if connectable is None: - connectable = self.engine.connect() - context.configure( - connection=connectable, - target_metadata=self.target_metadata, - compare_type=True, - render_item=render_item, - ) - with context.begin_transaction(): - self.prepare_migration_context() - context.run_migrations() - def prepare_migration_context(self) -> None: - pass +# 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(): + """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. + """ + url = settings.POSTGRES_URL + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) -class PostgresMigrator(Migrator): - pass + with context.begin_transaction(): + context.run_migrations() -def run_migrations_online() -> None: - if config.config_ini_section == "postgres": - migrator = PostgresMigrator(postgres_engine, postgres_metadata) +def do_run_migrations(connection): + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): + """Run migrations in "online" mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = settings.POSTGRES_URL + connectable = AsyncEngine( + engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + future=True, + ) + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) - migrator.migrate() + await connectable.dispose() -run_migrations_online() +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/app/db/session.py b/app/db/session.py index ceb0487..abf6a3a 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,30 +1,15 @@ -from collections.abc import Generator -from contextlib import contextmanager - -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from app.core.config import settings - -def create_postgres_engine() -> Engine: - return create_engine(settings.POSTGRES_URL, pool_pre_ping=True) - - -postgres_engine = create_postgres_engine() - -engines = {"postgres": postgres_engine} - -SessionLocalPG = sessionmaker( - autocommit=False, autoflush=False, bind=engines["postgres"] +engine = create_async_engine( + settings.POSTGRES_URL, echo=settings.DEBUG, pool_pre_ping=True +) +Session = sessionmaker( + engine, + class_=AsyncSession, + autoflush=False, + autocommit=False, + expire_on_commit=False, ) - - -@contextmanager -def postgres_session() -> Generator: - session = SessionLocalPG() - try: - yield session - finally: - session.close() From 15f805012233b21dfbaa645fe62e0d3a20573a4d Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 16:29:46 +0400 Subject: [PATCH 08/11] add requirements in text file --- requirements.txt | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6e7506e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,45 @@ +alembic==1.8.1 +anyio==3.6.2 +asyncpg==0.27.0 +attrs==22.1.0 +certifi==2022.9.24 +click==8.1.3 +databases==0.6.2 +dnspython==2.2.1 +ecdsa==0.18.0 +email-validator==1.3.0 +exceptiongroup==1.0.4 +factory-boy==3.2.1 +Faker==15.3.4 +fastapi==0.88.0 +greenlet==2.0.1 +h11==0.14.0 +httpcore==0.16.2 +httpx==0.23.1 +idna==3.4 +iniconfig==1.1.1 +Mako==1.2.4 +MarkupSafe==2.1.1 +orjson==3.8.3 +packaging==21.3 +passlib==1.7.4 +pluggy==1.0.0 +psycopg2==2.9.5 +pyasn1==0.4.8 +pydantic==1.10.2 +pyparsing==3.0.9 +pytest==7.2.0 +pytest-asyncio==0.20.2 +python-dateutil==2.8.2 +python-jose==3.3.0 +python-multipart==0.0.5 +rfc3986==1.5.0 +rsa==4.9 +six==1.16.0 +sniffio==1.3.0 +SQLAlchemy==1.4.41 +SQLAlchemy-Utils==0.38.3 +starlette==0.22.0 +tomli==2.0.1 +typing_extensions==4.4.0 +uvicorn==0.20.0 From 5da289e85541f9945a5897123390d287e1d7233c Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 16:30:10 +0400 Subject: [PATCH 09/11] update docker with requirements --- Dockerfile | 24 ++++++++++++++---------- docker-compose.dev.yml | 2 -- docker-entrypoint.sh | 7 ++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index d1c9591..a2b676f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,11 @@ -FROM python:3.10 +# Dockerfile -ENV PATH="${PATH}:/root/.poetry/bin" - -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - && \ - poetry config virtualenvs.create false -COPY ./pyproject.toml ./poetry.lock ./ -RUN poetry install --no-interaction --no-dev --no-root --no-ansi -vvv +FROM python:3.10-bullseye +# copy source and install dependencies +ENV VIRTUAL_ENV=/venv +RUN python3.10 -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" ENV PYTHONPATH=/app @@ -14,9 +13,14 @@ WORKDIR /app COPY . . COPY docker-entrypoint.sh ./docker-entrypoint.sh -RUN chmod +x ./docker-entrypoint.sh && \ +COPY requirements.txt ./requirements.txt +RUN apt-get update && \ + apt-get install -y libpq-dev python3-dev && \ + chmod +x ./docker-entrypoint.sh && \ ln -s ./docker-entrypoint.sh / -EXPOSE 8080 +RUN pip install -r ./requirements.txt -ENTRYPOINT ["sh", "./docker-entrypoint.sh" ] +# start service +STOPSIGNAL SIGTERM +CMD ["sh", "docker-entrypoint.sh"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a0c5658..163a4c8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -3,8 +3,6 @@ version: "3.10" services: web: build: . - volumes: - - './:/app' depends_on: - postgres ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index f48a345..fa61091 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,13 +1,10 @@ #!/bin/bash -# Set local server config -APP_UVICORN_OPTIONS="${APP_UVICORN_OPTIONS:---host 0.0.0.0 --port=8080}" - # Run migrations -alembic -n postgres upgrade head +alembic upgrade head # Create initial data in DB python app/initial_data.py # Run local server -uvicorn ${APP_UVICORN_OPTIONS} app.fastapi_app:app +uvicorn app.fastapi_app:app "${APP_UVICORN_OPTIONS}" From 4c5cd7ba5d94cfa09bc072966fe26a7070aa8aa2 Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 16:42:00 +0400 Subject: [PATCH 10/11] add user table with sqlalchemy base --- alembic/env.py | 4 ++-- app/db/base.py | 2 +- app/models/user.py | 29 ++++++++++++----------------- requirements.txt | 1 - 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index 3ba5074..09be5c6 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -6,7 +6,7 @@ from alembic import context from app.core.config import settings -from app.db.base import postgres_metadata +from app.db.base import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -20,7 +20,7 @@ # for "autogenerate" support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = postgres_metadata +target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, diff --git a/app/db/base.py b/app/db/base.py index ef89b00..0bb56b3 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,2 +1,2 @@ -from app.db.metadata import postgres_metadata +from app.db.base_class import Base from app.models import * diff --git a/app/models/user.py b/app/models/user.py index b900bd2..70b1e55 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,19 +1,14 @@ -import sqlalchemy -import sqlalchemy_utils +from sqlalchemy import Boolean, Column, Integer, String -from app.db.metadata import postgres_metadata +from app.db.base_class import Base -user = sqlalchemy.Table( - "user", - postgres_metadata, - sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True, index=True), - sqlalchemy.Column( - "email", sqlalchemy_utils.EmailType, unique=True, index=True, nullable=False - ), - sqlalchemy.Column("hashed_password", sqlalchemy.String, nullable=False), - sqlalchemy.Column("phone_number", sqlalchemy.String), - sqlalchemy.Column("first_name", sqlalchemy.String), - sqlalchemy.Column("last_name", sqlalchemy.String), - sqlalchemy.Column("is_active", sqlalchemy.Boolean, default=True), - sqlalchemy.Column("is_superuser", sqlalchemy.Boolean, default=False), -) + +class User(Base): + id: int = Column(Integer, primary_key=True, auto_created=True) + email: str = Column(String(255), unique=True, index=True, nullable=False) + hashed_password: str = Column(String(127), nullable=False) + phone_number: str = Column(String(63)) + first_name: str = Column(String(63)) + last_name: str = Column(String(63)) + is_active: bool = Column(Boolean, server_default=True) + is_superuser: bool = Column(Boolean, server_default=True) diff --git a/requirements.txt b/requirements.txt index 6e7506e..44b5238 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,6 @@ rsa==4.9 six==1.16.0 sniffio==1.3.0 SQLAlchemy==1.4.41 -SQLAlchemy-Utils==0.38.3 starlette==0.22.0 tomli==2.0.1 typing_extensions==4.4.0 From 1c780d8e446f23c70a118f1b940b411d8e4ef04a Mon Sep 17 00:00:00 2001 From: pavelprokhorenko Date: Wed, 7 Dec 2022 16:44:15 +0400 Subject: [PATCH 11/11] add docstring to user model --- app/models/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/user.py b/app/models/user.py index 70b1e55..28601e0 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -4,6 +4,10 @@ class User(Base): + """ + Table that contains each user of system. + """ + id: int = Column(Integer, primary_key=True, auto_created=True) email: str = Column(String(255), unique=True, index=True, nullable=False) hashed_password: str = Column(String(127), nullable=False) @@ -11,4 +15,4 @@ class User(Base): first_name: str = Column(String(63)) last_name: str = Column(String(63)) is_active: bool = Column(Boolean, server_default=True) - is_superuser: bool = Column(Boolean, server_default=True) + is_superuser: bool = Column(Boolean, server_default=False)