From 30835e31fa51aaa31480ca138fabe46f8bebd9e0 Mon Sep 17 00:00:00 2001 From: Kurbanov Bulat Date: Sun, 14 Nov 2021 10:38:47 +0300 Subject: [PATCH] Init --- .gitignore | 5 + README.rst | 0 fastapi_book_server/__init__.py | 1 + fastapi_book_server/app/alembic.ini | 98 +++ fastapi_book_server/app/alembic/README | 1 + fastapi_book_server/app/alembic/env.py | 66 ++ .../app/alembic/script.py.mako | 24 + .../app/alembic/versions/6d0dbf1e4998_.py | 30 + .../app/alembic/versions/9c4b98440632_.py | 35 + .../app/alembic/versions/d172032adeaf_.py | 112 +++ fastapi_book_server/app/models.py | 127 +++ fastapi_book_server/app/serializers/author.py | 48 ++ .../app/serializers/author_annotation.py | 23 + fastapi_book_server/app/serializers/book.py | 42 + .../app/serializers/book_annotation.py | 24 + .../app/serializers/sequence.py | 12 + .../app/serializers/sequence_info.py | 54 ++ fastapi_book_server/app/serializers/source.py | 10 + .../app/serializers/translation.py | 34 + fastapi_book_server/app/services/author.py | 13 + fastapi_book_server/app/services/book.py | 67 ++ fastapi_book_server/app/services/common.py | 134 ++++ fastapi_book_server/app/services/sequence.py | 11 + .../app/services/sequence_info.py | 46 ++ .../app/services/translation.py | 49 ++ fastapi_book_server/app/utils/pagination.py | 33 + fastapi_book_server/app/views/__init__.py | 24 + fastapi_book_server/app/views/author.py | 83 ++ .../app/views/author_annotation.py | 49 ++ fastapi_book_server/app/views/book.py | 81 ++ .../app/views/book_annotation.py | 49 ++ fastapi_book_server/app/views/sequence.py | 39 + .../app/views/sequence_info.py | 46 ++ fastapi_book_server/app/views/source.py | 25 + fastapi_book_server/app/views/translation.py | 43 + fastapi_book_server/core/app.py | 31 + fastapi_book_server/core/config.py | 16 + fastapi_book_server/core/db.py | 15 + fastapi_book_server/main.py | 4 + poetry.lock | 734 ++++++++++++++++++ pyproject.toml | 23 + tests/__init__.py | 0 tests/test_fastapi_book_server.py | 5 + 43 files changed, 2366 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100644 fastapi_book_server/__init__.py create mode 100644 fastapi_book_server/app/alembic.ini create mode 100644 fastapi_book_server/app/alembic/README create mode 100644 fastapi_book_server/app/alembic/env.py create mode 100644 fastapi_book_server/app/alembic/script.py.mako create mode 100644 fastapi_book_server/app/alembic/versions/6d0dbf1e4998_.py create mode 100644 fastapi_book_server/app/alembic/versions/9c4b98440632_.py create mode 100644 fastapi_book_server/app/alembic/versions/d172032adeaf_.py create mode 100644 fastapi_book_server/app/models.py create mode 100644 fastapi_book_server/app/serializers/author.py create mode 100644 fastapi_book_server/app/serializers/author_annotation.py create mode 100644 fastapi_book_server/app/serializers/book.py create mode 100644 fastapi_book_server/app/serializers/book_annotation.py create mode 100644 fastapi_book_server/app/serializers/sequence.py create mode 100644 fastapi_book_server/app/serializers/sequence_info.py create mode 100644 fastapi_book_server/app/serializers/source.py create mode 100644 fastapi_book_server/app/serializers/translation.py create mode 100644 fastapi_book_server/app/services/author.py create mode 100644 fastapi_book_server/app/services/book.py create mode 100644 fastapi_book_server/app/services/common.py create mode 100644 fastapi_book_server/app/services/sequence.py create mode 100644 fastapi_book_server/app/services/sequence_info.py create mode 100644 fastapi_book_server/app/services/translation.py create mode 100644 fastapi_book_server/app/utils/pagination.py create mode 100644 fastapi_book_server/app/views/__init__.py create mode 100644 fastapi_book_server/app/views/author.py create mode 100644 fastapi_book_server/app/views/author_annotation.py create mode 100644 fastapi_book_server/app/views/book.py create mode 100644 fastapi_book_server/app/views/book_annotation.py create mode 100644 fastapi_book_server/app/views/sequence.py create mode 100644 fastapi_book_server/app/views/sequence_info.py create mode 100644 fastapi_book_server/app/views/source.py create mode 100644 fastapi_book_server/app/views/translation.py create mode 100644 fastapi_book_server/core/app.py create mode 100644 fastapi_book_server/core/config.py create mode 100644 fastapi_book_server/core/db.py create mode 100644 fastapi_book_server/main.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_fastapi_book_server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..684f953 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode + +__pycache__ + +venv diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/fastapi_book_server/__init__.py b/fastapi_book_server/__init__.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/fastapi_book_server/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/fastapi_book_server/app/alembic.ini b/fastapi_book_server/app/alembic.ini new file mode 100644 index 0000000..01b575f --- /dev/null +++ b/fastapi_book_server/app/alembic.ini @@ -0,0 +1,98 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = ./app/alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# 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 = + +# 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 alembic/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" +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. Valid values are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # default: use os.pathsep + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + + +[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 + +# 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 + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/fastapi_book_server/app/alembic/README b/fastapi_book_server/app/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/fastapi_book_server/app/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/fastapi_book_server/app/alembic/env.py b/fastapi_book_server/app/alembic/env.py new file mode 100644 index 0000000..9844b85 --- /dev/null +++ b/fastapi_book_server/app/alembic/env.py @@ -0,0 +1,66 @@ +from logging.config import fileConfig + +from alembic import context +import sys, os + +from sqlalchemy.engine import create_engine + +from core.db import DATABASE_URL + + +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath + '/../../') + +config = context.config + + +from app.models import BaseMeta +target_metadata = BaseMeta.metadata + + +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 = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +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. + + """ + connectable = create_engine(DATABASE_URL) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/fastapi_book_server/app/alembic/script.py.mako b/fastapi_book_server/app/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/fastapi_book_server/app/alembic/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(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/fastapi_book_server/app/alembic/versions/6d0dbf1e4998_.py b/fastapi_book_server/app/alembic/versions/6d0dbf1e4998_.py new file mode 100644 index 0000000..ab15fea --- /dev/null +++ b/fastapi_book_server/app/alembic/versions/6d0dbf1e4998_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 6d0dbf1e4998 +Revises: 9c4b98440632 +Create Date: 2021-11-07 20:23:39.656518 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6d0dbf1e4998' +down_revision = '9c4b98440632' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('uc_sequence_infos_sequence_position', 'sequence_infos', ['sequence', 'position']) + op.create_unique_constraint('uc_translations_book_position', 'translations', ['book', 'position']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uc_translations_book_position', 'translations', type_='unique') + op.drop_constraint('uc_sequence_infos_sequence_position', 'sequence_infos', type_='unique') + # ### end Alembic commands ### diff --git a/fastapi_book_server/app/alembic/versions/9c4b98440632_.py b/fastapi_book_server/app/alembic/versions/9c4b98440632_.py new file mode 100644 index 0000000..105d293 --- /dev/null +++ b/fastapi_book_server/app/alembic/versions/9c4b98440632_.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 9c4b98440632 +Revises: d172032adeaf +Create Date: 2021-11-07 16:21:08.210333 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9c4b98440632' +down_revision = 'd172032adeaf' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('books_authors', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('author', sa.Integer(), nullable=True), + sa.Column('book', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['author'], ['authors.id'], name='fk_books_authors_authors_author_id', onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['book'], ['books.id'], name='fk_books_authors_books_book_id', onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('books_authors') + # ### end Alembic commands ### diff --git a/fastapi_book_server/app/alembic/versions/d172032adeaf_.py b/fastapi_book_server/app/alembic/versions/d172032adeaf_.py new file mode 100644 index 0000000..963dded --- /dev/null +++ b/fastapi_book_server/app/alembic/versions/d172032adeaf_.py @@ -0,0 +1,112 @@ +"""empty message + +Revision ID: d172032adeaf +Revises: +Create Date: 2021-11-05 17:22:11.717389 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd172032adeaf' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sources', + sa.Column('id', sa.SmallInteger(), nullable=False), + sa.Column('name', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('authors', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('source', sa.SmallInteger(), nullable=False), + sa.Column('remote_id', sa.Integer(), nullable=False), + sa.Column('first_name', sa.String(length=256), nullable=False), + sa.Column('last_name', sa.String(length=256), nullable=False), + sa.Column('middle_name', sa.String(length=256), nullable=True), + sa.ForeignKeyConstraint(['source'], ['sources.id'], name='fk_authors_sources_id_source'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('source', 'remote_id', name='uc_authors_source_remote_id') + ) + op.create_table('books', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('source', sa.SmallInteger(), nullable=False), + sa.Column('remote_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=256), nullable=False), + sa.Column('lang', sa.String(length=2), nullable=False), + sa.Column('file_type', sa.String(length=4), nullable=False), + sa.Column('uploaded', sa.Date(), nullable=False), + sa.ForeignKeyConstraint(['source'], ['sources.id'], name='fk_books_sources_id_source'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('source', 'remote_id', name='uc_books_source_remote_id') + ) + op.create_table('sequences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('source', sa.SmallInteger(), nullable=False), + sa.Column('remote_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=256), nullable=False), + sa.ForeignKeyConstraint(['source'], ['sources.id'], name='fk_sequences_sources_id_source'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('source', 'remote_id', name='uc_sequences_source_remote_id') + ) + op.create_table('author_annotations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('author', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=256), nullable=False), + sa.Column('text', sa.Text(), nullable=False), + sa.Column('file', sa.String(length=256), nullable=True), + sa.ForeignKeyConstraint(['author'], ['authors.id'], name='fk_author_annotations_authors_id_author'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('author') + ) + op.create_table('book_annotations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('book', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=256), nullable=False), + sa.Column('text', sa.Text(), nullable=False), + sa.Column('file', sa.String(length=256), nullable=True), + sa.ForeignKeyConstraint(['book'], ['books.id'], name='fk_book_annotations_books_id_book'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('book') + ) + op.create_table('sequence_infos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('book', sa.Integer(), nullable=False), + sa.Column('sequence', sa.Integer(), nullable=False), + sa.Column('position', sa.SmallInteger(), nullable=False), + sa.ForeignKeyConstraint(['book'], ['books.id'], name='fk_sequence_infos_books_id_book'), + sa.ForeignKeyConstraint(['sequence'], ['sequences.id'], name='fk_sequence_infos_sequences_id_sequence'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('book', 'sequence', name='uc_sequence_infos_book_sequence') + ) + op.create_table('translations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('book', sa.Integer(), nullable=False), + sa.Column('translator', sa.Integer(), nullable=False), + sa.Column('position', sa.SmallInteger(), nullable=False), + sa.ForeignKeyConstraint(['book'], ['books.id'], name='fk_translations_books_id_book'), + sa.ForeignKeyConstraint(['translator'], ['authors.id'], name='fk_translations_authors_id_translator'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('book', 'translator', name='uc_translations_book_translator') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('translations') + op.drop_table('sequence_infos') + op.drop_table('book_annotations') + op.drop_table('author_annotations') + op.drop_table('sequences') + op.drop_table('books') + op.drop_table('authors') + op.drop_table('sources') + # ### end Alembic commands ### diff --git a/fastapi_book_server/app/models.py b/fastapi_book_server/app/models.py new file mode 100644 index 0000000..953c331 --- /dev/null +++ b/fastapi_book_server/app/models.py @@ -0,0 +1,127 @@ +from datetime import date + +import ormar + +from core.db import metadata, database + + +class BaseMeta(ormar.ModelMeta): + metadata = metadata + database = database + + +class Source(ormar.Model): + class Meta(BaseMeta): + tablename = "sources" + + id: int = ormar.SmallInteger(primary_key=True, nullable=False) # type: ignore + + name: str = ormar.String(max_length=32, nullable=False, unique=True) # type: ignore + + +class Author(ormar.Model): + class Meta(BaseMeta): + tablename = "authors" + constraints = [ + ormar.UniqueColumns("source", "remote_id"), + ] + + id: int = ormar.Integer(primary_key=True, nullable=False) # type: ignore + + source: Source = ormar.ForeignKey(Source, nullable=False) + remote_id: int = ormar.Integer(minimum=0, nullable=False) # type: ignore + + first_name: str = ormar.String(max_length=256, nullable=False) # type: ignore + last_name: str = ormar.String(max_length=256, nullable=False) # type: ignore + middle_name: str = ormar.String(max_length=256, nullable=True, default="") # type: ignore + + +class AuthorAnnotation(ormar.Model): + class Meta(BaseMeta): + tablename = "author_annotations" + + id = ormar.Integer(primary_key=True, nullable=False) + + author: Author = ormar.ForeignKey(Author, nullable=False, unique=True) + + title: str = ormar.String(max_length=256, nullable=False, default="") # type: ignore + text: str = ormar.Text(nullable=False, default="") # type: ignore + file: str = ormar.String(max_length=256, nullable=True) # type: ignore + + +class Book(ormar.Model): + class Meta(BaseMeta): + tablename = "books" + constraints = [ + ormar.UniqueColumns("source", "remote_id"), + ] + + id: int = ormar.Integer(primary_key=True, nullable=False) # type: ignore + + source: Source = ormar.ForeignKey(Source, nullable=False) + remote_id: int = ormar.Integer(minimum=0, nullable=False) # type: ignore + + title: str = ormar.String(max_length=256, nullable=False) # type: ignore + lang: str = ormar.String(max_length=2, nullable=False) # type: ignore + file_type: str = ormar.String(max_length=4, nullable=False) # type: ignore + uploaded: date = ormar.Date() # type: ignore + + authors = ormar.ManyToMany(Author) + + +class BookAnnotation(ormar.Model): + class Meta(BaseMeta): + tablename = "book_annotations" + + id = ormar.Integer(primary_key=True, nullable=False) + + book: Book = ormar.ForeignKey(Book, nullable=False, unique=True) + + title: str = ormar.String(max_length=256, nullable=False, default="") # type: ignore + text: str = ormar.Text(nullable=False, default="") # type: ignore + file: str = ormar.String(max_length=256, nullable=True) # type: ignore + + +class Translation(ormar.Model): + class Meta(BaseMeta): + tablename = "translations" + constraints = [ + ormar.UniqueColumns("book", "translator"), + ormar.UniqueColumns("book", "position"), + ] + + id: int = ormar.Integer(primary_key=True, nullable=False) # type: ignore + + book: Book = ormar.ForeignKey(Book, nullable=False) + translator: Author = ormar.ForeignKey(Author, nullable=False) + position: int = ormar.SmallInteger(nullable=False) # type: ignore + + +class Sequence(ormar.Model): + class Meta(BaseMeta): + tablename = "sequences" + constraints = [ + ormar.UniqueColumns("source", "remote_id"), + ] + + id: int = ormar.Integer(primary_key=True, nullable=False) # type: ignore + + source: Source = ormar.ForeignKey(Source, nullable=False) + remote_id: int = ormar.Integer(minimum=0, nullable=False) # type: ignore + + name: str = ormar.String(max_length=256, nullable=False) # type: ignore + + +class SequenceInfo(ormar.Model): + class Meta(BaseMeta): + tablename = "sequence_infos" + constraints = [ + ormar.UniqueColumns("book", "sequence"), + ormar.UniqueColumns("sequence", "position"), + ] + + id: int = ormar.Integer(primary_key=True, nullable=False) # type: ignore + + book: Book = ormar.ForeignKey(Book, nullable=False) + sequence: Sequence = ormar.ForeignKey(Sequence, nullable=False) + position: int = ormar.SmallInteger(minimum=0, nullable=False) # type: ignore diff --git a/fastapi_book_server/app/serializers/author.py b/fastapi_book_server/app/serializers/author.py new file mode 100644 index 0000000..8ac4e9f --- /dev/null +++ b/fastapi_book_server/app/serializers/author.py @@ -0,0 +1,48 @@ +from typing import Optional +from datetime import date + +from pydantic import BaseModel + + +class Author(BaseModel): + id: int + + first_name: str + last_name: str + middle_name: Optional[str] + + +class CreateAuthor(BaseModel): + source: int + remote_id: int + + first_name: str + last_name: str + middle_name: Optional[str] + + +class UpdateAuthor(BaseModel): + first_name: str + last_name: str + middle_name: Optional[str] + + +class AuthorBook(BaseModel): + id: int + title: str + lang: str + file_type: str + + +class Translation(BaseModel): + translator: Author + position: int + + +class TranslatedBook(BaseModel): + id: int + title: str + lang: str + file_type: str + authors: list[Author] + translations: list[Translation] diff --git a/fastapi_book_server/app/serializers/author_annotation.py b/fastapi_book_server/app/serializers/author_annotation.py new file mode 100644 index 0000000..7182ab9 --- /dev/null +++ b/fastapi_book_server/app/serializers/author_annotation.py @@ -0,0 +1,23 @@ +from typing import Optional + +from pydantic import BaseModel + + +class AuthorAnnotation(BaseModel): + id: int + title: str + text: str + file: Optional[str] + + +class CreateAuthorAnnotation(BaseModel): + author: int + title: str + text: str + file: Optional[str] + + +class UpdateAuthorAnnotation(BaseModel): + title: str + text: str + file: Optional[str] diff --git a/fastapi_book_server/app/serializers/book.py b/fastapi_book_server/app/serializers/book.py new file mode 100644 index 0000000..8b759ab --- /dev/null +++ b/fastapi_book_server/app/serializers/book.py @@ -0,0 +1,42 @@ +from datetime import date + +from pydantic import BaseModel + +from app.serializers.author import Author + + +class Book(BaseModel): + id: int + title: str + lang: str + file_type: str + uploaded: date + authors: list[Author] + + +class CreateBook(BaseModel): + source: int + remote_id: int + title: str + lang: str + file_type: str + uploaded: date + authors: list[int] + + +class UpdateBook(BaseModel): + title: str + lang: str + file_type: str + uploaded: date + authors: list[int] + + +class CreateRemoteBook(BaseModel): + source: int + remote_id: int + title: str + lang: str + file_type: str + uploaded: date + remote_authors: list[int] diff --git a/fastapi_book_server/app/serializers/book_annotation.py b/fastapi_book_server/app/serializers/book_annotation.py new file mode 100644 index 0000000..8c4c04a --- /dev/null +++ b/fastapi_book_server/app/serializers/book_annotation.py @@ -0,0 +1,24 @@ +from typing import Optional + +from pydantic import BaseModel + + +class BookAnnotation(BaseModel): + id: int + title: str + text: str + file: Optional[str] + + +class CreateBookAnnotation(BaseModel): + id: int + title: str + text: str + file: Optional[str] + + +class UpdateBookAnnotation(BaseModel): + id: int + title: str + text: str + file: Optional[str] diff --git a/fastapi_book_server/app/serializers/sequence.py b/fastapi_book_server/app/serializers/sequence.py new file mode 100644 index 0000000..e41c590 --- /dev/null +++ b/fastapi_book_server/app/serializers/sequence.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class Sequence(BaseModel): + id: int + name: str + + +class CreateSequence(BaseModel): + source: int + remote_id: int + name: str diff --git a/fastapi_book_server/app/serializers/sequence_info.py b/fastapi_book_server/app/serializers/sequence_info.py new file mode 100644 index 0000000..d2dc225 --- /dev/null +++ b/fastapi_book_server/app/serializers/sequence_info.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel + + +class SequenceBookAuthor(BaseModel): + id: int + first_name: str + last_name: str + middle_name: str + + +class SeqTranslationTranslator(BaseModel): + id: int + first_name: str + last_name: str + middle_name: str + + +class SequenceBookTranslation(BaseModel): + id: int + translator: SeqTranslationTranslator + + +class SequenceBook(BaseModel): + id: int + title: str + lang: str + file_type: str + authors: SequenceBookAuthor + translation: SequenceBookTranslation + + +class Sequence(BaseModel): + id: int + name: str + + +class SequenceInfo(BaseModel): + id: int + book: SequenceBook + sequence: Sequence + position: int + + +class CreateSequenceInfo(BaseModel): + book: int + sequence: int + position: int + + +class CreateRemoteSequenceInfo(BaseModel): + source: int + remote_book: int + remote_sequence: int + position: int diff --git a/fastapi_book_server/app/serializers/source.py b/fastapi_book_server/app/serializers/source.py new file mode 100644 index 0000000..05ce40f --- /dev/null +++ b/fastapi_book_server/app/serializers/source.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class Source(BaseModel): + id: int + name: str + + +class CreateSource(BaseModel): + name: str diff --git a/fastapi_book_server/app/serializers/translation.py b/fastapi_book_server/app/serializers/translation.py new file mode 100644 index 0000000..a7fa681 --- /dev/null +++ b/fastapi_book_server/app/serializers/translation.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class TranslationBook(BaseModel): + id: int + title: str + lang: str + file_type: str + + +class TranslationTranslator(BaseModel): + id: int + first_name: str + last_name: str + middle_name: str + + +class Translation(BaseModel): + book: TranslationBook + translator: TranslationTranslator + position: int + + +class CreateTranslation(BaseModel): + book: int + translator: int + position: int + + +class CreateRemoteTranslation(BaseModel): + source: int + remote_book: int + remote_translator: int + position: int diff --git a/fastapi_book_server/app/services/author.py b/fastapi_book_server/app/services/author.py new file mode 100644 index 0000000..3cbd096 --- /dev/null +++ b/fastapi_book_server/app/services/author.py @@ -0,0 +1,13 @@ +from app.models import Author + +from app.services.common import TRGMSearchService + + +class AuthorTGRMSearchService(TRGMSearchService): + MODEL = Author + FIELDS = [ + Author.Meta.table.c.last_name, + Author.Meta.table.c.first_name, + Author.Meta.table.c.middle_name + ] + PREFETCH_RELATED = ["source"] diff --git a/fastapi_book_server/app/services/book.py b/fastapi_book_server/app/services/book.py new file mode 100644 index 0000000..0a28c62 --- /dev/null +++ b/fastapi_book_server/app/services/book.py @@ -0,0 +1,67 @@ +from typing import Union + +from fastapi import HTTPException, status + +from app.models import Book as BookDB, Author as AuthorDB + +from app.services.common import TRGMSearchService +from app.serializers.book import CreateBook, CreateRemoteBook + + +class BookTGRMSearchService(TRGMSearchService): + MODEL = BookDB + FIELDS = [ + BookDB.Meta.table.c.title + ] + PREFETCH_RELATED = ["source"] + + +class BookCreator: + @classmethod + def _raise_bad_request(cls): + raise HTTPException(status.HTTP_404_NOT_FOUND) + + @classmethod + async def _create_book(cls, data: CreateBook) -> BookDB: + data_dict = data.dict() + + author_ids = data_dict.pop("authors", []) + authors = await AuthorDB.objects.filter(id__in=author_ids).all() + + if len(author_ids) != len(authors): + cls._raise_bad_request() + + book = await BookDB.objects.create( + **data_dict + ) + + for author in authors: + await book.authors.add(author) + + return book + + @classmethod + async def _create_remote_book(cls, data: CreateRemoteBook) -> BookDB: + data_dict = data.dict() + + author_ids = data_dict.pop("remote_authors", []) + authors = await AuthorDB.objects.filter(source__id=data.source, remote_id__in=author_ids).all() + + if len(author_ids) != len(authors): + cls._raise_bad_request() + + book = await BookDB.objects.create( + **data_dict + ) + + for author in authors: + await book.authors.add(author) + + return book + + @classmethod + async def create(cls, data: Union[CreateBook, CreateRemoteBook]) -> BookDB: + if isinstance(data, CreateBook): + return await cls._create_book(data) + if isinstance(data, CreateRemoteBook): + return await cls._create_remote_book(data) diff --git a/fastapi_book_server/app/services/common.py b/fastapi_book_server/app/services/common.py new file mode 100644 index 0000000..d22b044 --- /dev/null +++ b/fastapi_book_server/app/services/common.py @@ -0,0 +1,134 @@ +from typing import Optional, Generic, TypeVar, Union, Any, cast +from itertools import permutations + +from fastapi_pagination.api import resolve_params +from fastapi_pagination.bases import RawParams +from app.utils.pagination import CustomPage + +from ormar import Model, QuerySet +from sqlalchemy import text, func, select, desc, Table, Column +from databases import Database + + +def join_fields(fields): + result = fields[0] + + for el in fields[1:]: + result += text("' '") + el + + return result + + +T = TypeVar('T', bound=Model) + + +class TRGMSearchService(Generic[T]): + MODEL_CLASS: Optional[T] = None + FIELDS: Optional[list[Column]] = None + SELECT_RELATED: Optional[Union[list[str], str]] = None + PREFETCH_RELATED: Optional[Union[list[str], str]] = None + + @classmethod + def get_params(cls) -> RawParams: + return resolve_params().to_raw_params() + + @classmethod + @property + def model(cls) -> T: + assert cls.MODEL_CLASS is not None, f"MODEL in {cls.__name__} don't set!" + return cls.MODEL_CLASS + + @classmethod + @property + def table(cls) -> Table: + return cls.model.Meta.table + + @classmethod + @property + def database(cls) -> Database: + return cls.model.Meta.database + + @classmethod + @property + def fields_combinations(cls): + assert cls.FIELDS is not None, f"FIELDS in {cls.__name__} don't set!" + assert len(cls.FIELDS) == 0, f"FIELDS in {cls.__name__} must be not empty!" + + if len(cls.FIELDS) == 1: + return cls.FIELDS + + combinations = [] + + for i in range(1, len(cls.FIELDS)): + combinations += permutations(cls.FIELDS, i) + + return combinations + + @classmethod + def get_similarity_subquery(cls, query: str): + return func.greatest( + *[func.similarity(join_fields(comb), f"{query}::text") for comb in cls.fields_combinations] + ).label("sml") + + @classmethod + def get_object_ids_query(cls, query: str): + similarity = cls.get_similarity_subquery(query) + params = cls.get_params() + + return select( + [cls.table.c.id], + ).where( + similarity > 0.5 + ).order_by( + desc(similarity) + ).limit(params.limit).offset(params.offset) + + @classmethod + def get_objects_count_query(cls, query: str): + similarity = cls.get_similarity_subquery(query) + + return select( + func.count(cls.table.c.id) + ).where( + similarity > 0.5 + ) + + @classmethod + async def get_objects_count(cls, query: str) -> int: + count_query = cls.get_objects_count_query(query) + + count_row = await cls.database.fetch_one(count_query) + + assert count_row is not None + + return cast(int, count_row.get("count_1")) + + @classmethod + async def get_objects(cls, query: str) -> list[T]: + ids_query = cls.get_object_ids_query(query) + + ids = await cls.database.fetch_all(ids_query) + + queryset: QuerySet[T] = cls.model.objects + + if cls.PREFETCH_RELATED is not None: + queryset = queryset.prefetch_related(cls.PREFETCH_RELATED) + + if cls.SELECT_RELATED: + queryset = queryset.select_related(cls.SELECT_RELATED) + + return await queryset.filter(id__in=[r.get("id") for r in ids]).all() + + @classmethod + async def get(cls, query: str) -> CustomPage[T]: + params = cls.get_params() + + authors = await cls.get_objects(query) + total = await cls.get_objects_count(query) + + return CustomPage( + items=authors, + total=total, + limit=params.limit, + offset=params.offset + ) diff --git a/fastapi_book_server/app/services/sequence.py b/fastapi_book_server/app/services/sequence.py new file mode 100644 index 0000000..4b19bf2 --- /dev/null +++ b/fastapi_book_server/app/services/sequence.py @@ -0,0 +1,11 @@ +from app.models import Sequence + +from app.services.common import TRGMSearchService + + +class SequenceTGRMSearchService(TRGMSearchService): + MODEL = Sequence + FIELDS = [ + Sequence.Meta.table.c.name + ] + PREFETCH_RELATED = ["source"] diff --git a/fastapi_book_server/app/services/sequence_info.py b/fastapi_book_server/app/services/sequence_info.py new file mode 100644 index 0000000..94f693c --- /dev/null +++ b/fastapi_book_server/app/services/sequence_info.py @@ -0,0 +1,46 @@ +from typing import Union + +from fastapi import HTTPException, status + +from app.models import SequenceInfo as SequenceInfoDB, Source as SourceDB, Book as BookDB, Sequence as SequenceDB +from app.serializers.sequence_info import CreateSequenceInfo, CreateRemoteSequenceInfo + + +class SequenceInfoCreator: + @classmethod + def _raise_bad_request(cls): + raise HTTPException(status.HTTP_404_NOT_FOUND) + + @classmethod + async def _create_sequence_info(cls, data: CreateSequenceInfo) -> SequenceInfoDB: + return await SequenceInfoDB.objects.create(**data.dict()) + + @classmethod + async def _create_remote_sequence_info(cls, data: CreateRemoteSequenceInfo) -> SequenceInfoDB: + source = await SourceDB.objects.get_or_none(id=data.source) + + if source is None: + cls._raise_bad_request() + + book = await BookDB.objects.get_or_none(source__id=source.id, remote_id=data.remote_book) + + if book is None: + cls._raise_bad_request() + + sequence = await SequenceDB.objects.get_or_none(source__id=source.id, remote_id=data.remote_sequence) + + if sequence is None: + cls._raise_bad_request() + + return await SequenceInfoDB.objects.create( + book=book.id, + sequence=sequence.id, + position=data.position, + ) + + @classmethod + async def create(cls, data: Union[CreateSequenceInfo, CreateRemoteSequenceInfo]) -> SequenceInfoDB: + if isinstance(data, CreateSequenceInfo): + return await cls._create_sequence_info(data) + if isinstance(data, CreateRemoteSequenceInfo): + return await cls._create_remote_sequence_info(data) diff --git a/fastapi_book_server/app/services/translation.py b/fastapi_book_server/app/services/translation.py new file mode 100644 index 0000000..249cb24 --- /dev/null +++ b/fastapi_book_server/app/services/translation.py @@ -0,0 +1,49 @@ +from typing import Union + +from fastapi import HTTPException, status + +from app.serializers.translation import CreateTranslation, CreateRemoteTranslation + +from app.models import Translation as TranslationDB, Source as SourceDB, Book as BookDB, Author as AuthorDB + + +class TranslationCreator: + @classmethod + def _raise_bad_request(cls): + raise HTTPException(status.HTTP_404_NOT_FOUND) + + @classmethod + async def _create_translation(cls, data: CreateTranslation) -> TranslationDB: + return await TranslationDB.objects.create( + **data.dict() + ) + + @classmethod + async def _create_remote_translation(cls, data: CreateRemoteTranslation) -> TranslationDB: + source = await SourceDB.objects.get_or_none(id=data.source) + + if source is None: + cls._raise_bad_request() + + book = await BookDB.objects.get_or_none(source__id=source.id, remote_id=data.remote_book) + + if book is None: + cls._raise_bad_request() + + translator = await AuthorDB.objects.get_or_none(source__id=source.id, remote_id=data.remote_translator) + + if translator is None: + cls._raise_bad_request() + + return await TranslationDB.objects.create( + book=book.id, + translator=translator.id, + position=data.position, + ) + + @classmethod + async def create(cls, data: Union[CreateTranslation, CreateRemoteTranslation]) -> TranslationDB: + if isinstance(data, CreateTranslation): + return await cls._create_translation(data) + if isinstance(data, CreateRemoteTranslation): + return await cls._create_remote_translation(data) diff --git a/fastapi_book_server/app/utils/pagination.py b/fastapi_book_server/app/utils/pagination.py new file mode 100644 index 0000000..5dbe419 --- /dev/null +++ b/fastapi_book_server/app/utils/pagination.py @@ -0,0 +1,33 @@ +from typing import Protocol, TypeVar, Any, Generic, Sequence, runtime_checkable +from dataclasses import asdict + +from fastapi_pagination import Page, Params +from fastapi_pagination.bases import AbstractParams + + +@runtime_checkable +class ToDict(Protocol): + def dict(self) -> dict: + ... + + +T = TypeVar('T', ToDict, Any) + + +class CustomPage(Page[T], Generic[T]): + @classmethod + def create( + cls, + items: Sequence[T], + total: int, + params: AbstractParams, + ) -> Page[T]: + if not isinstance(params, Params): + raise ValueError("Page should be used with Params") + + return cls( + total=total, + items=[item.dict() for item in items], + page=params.page, + size=params.size, + ) diff --git a/fastapi_book_server/app/views/__init__.py b/fastapi_book_server/app/views/__init__.py new file mode 100644 index 0000000..fffdf7b --- /dev/null +++ b/fastapi_book_server/app/views/__init__.py @@ -0,0 +1,24 @@ +from app.views.source import source_router + +from app.views.author import author_router +from app.views.author_annotation import author_annotation_router + +from app.views.book import book_router +from app.views.book_annotation import book_annotation_router + +from app.views.translation import translation_router + +from app.views.sequence import sequence_router +from app.views.sequence_info import sequence_info_router + + +routers = [ + source_router, + author_router, + author_annotation_router, + book_router, + book_annotation_router, + translation_router, + sequence_router, + sequence_info_router, +] diff --git a/fastapi_book_server/app/views/author.py b/fastapi_book_server/app/views/author.py new file mode 100644 index 0000000..6336ceb --- /dev/null +++ b/fastapi_book_server/app/views/author.py @@ -0,0 +1,83 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params, Page +from fastapi_pagination.ext.ormar import paginate +from fastapi_book_server.app.utils.pagination import CustomPage + +from app.models import Author as AuthorDB, AuthorAnnotation as AuthorAnnotationDB, Book as BookDB +from app.serializers.author import Author, CreateAuthor, UpdateAuthor, AuthorBook, TranslatedBook +from app.serializers.author_annotation import AuthorAnnotation +from app.services.author import AuthorTGRMSearchService + + +author_router = APIRouter( + prefix="/api/v1/authors", + tags=["author"], +) + + +@author_router.get("/", response_model=Page[Author], dependencies=[Depends(Params)]) +async def get_authors(): + return await paginate( + AuthorDB.objects.prefetch_related("source") + ) + + +@author_router.post("/", response_model=Author) +async def create_author(data: CreateAuthor): + author = await AuthorDB.objects.create( + **data.dict() + ) + + return await AuthorDB.objects.prefetch_related("source").get(id=author.id) + + +@author_router.get("/{id}", response_model=Author) +async def get_author(id: int): + author = await AuthorDB.objects.prefetch_related("source").get_or_none(id=id) + + if author is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return author + + +@author_router.put("/{id}", response_model=Author) +async def update_author(id: int, data: UpdateAuthor): + author = await AuthorDB.objects.get_or_none(id=id) + + if author is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + author.update_from_dict(data.dict()) + + return await author.save() + + +@author_router.get("/{id}/annotation", response_model=AuthorAnnotation) +async def get_author_annotation(id: int): + annotation = await AuthorAnnotationDB.objects.get_or_none(author__id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return annotation + + +@author_router.get("/{id}/books", response_model=CustomPage[AuthorBook], dependencies=[Depends(Params)]) +async def get_author_books(id: int): + return await paginate( + BookDB.objects.filter(author__id=id).order_by('title') + ) + + +@author_router.get("/{id}/translated_books", response_model=CustomPage[TranslatedBook]) +async def get_translated_books(id: int): + return await paginate( + BookDB.objects.select_related(["translations", "translations__translator"]).filter(translations__translator__id=id) + ) + + +@author_router.get("/search/{query}", response_model=Page[Author], dependencies=[Depends(Params)]) +async def search_authors(query: str): + return await AuthorTGRMSearchService.get(query) diff --git a/fastapi_book_server/app/views/author_annotation.py b/fastapi_book_server/app/views/author_annotation.py new file mode 100644 index 0000000..21989d4 --- /dev/null +++ b/fastapi_book_server/app/views/author_annotation.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params, Page +from fastapi_pagination.ext.ormar import paginate + +from app.models import AuthorAnnotation as AuthorAnnotationDB +from app.serializers.author_annotation import AuthorAnnotation, CreateAuthorAnnotation, UpdateAuthorAnnotation + + +author_annotation_router = APIRouter( + prefix="/api/v1/author_annotations", + tags=["author_annotation"] +) + + +@author_annotation_router.get("/", response_model=Page[AuthorAnnotation], dependencies=[Depends(Params)]) +async def get_author_annotations(): + return await paginate( + AuthorAnnotationDB.objects + ) + + +@author_annotation_router.post("/", response_model=AuthorAnnotation) +async def create_author_annotation(data: CreateAuthorAnnotation): + return await AuthorAnnotationDB.objects.create( + **data.dict() + ) + + +@author_annotation_router.get("/{id}", response_model=AuthorAnnotation) +async def get_author_annotation(id: int): + annotation = await AuthorAnnotationDB.objects.get_or_none(id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return annotation + + +@author_annotation_router.put("/{id}", response_model=AuthorAnnotation) +async def update_author_annotation(id: int, data: UpdateAuthorAnnotation): + annotation = await AuthorAnnotationDB.objects.get_or_none(id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + annotation.update_from_dict(data.dict()) + + return await annotation.save() diff --git a/fastapi_book_server/app/views/book.py b/fastapi_book_server/app/views/book.py new file mode 100644 index 0000000..6f59625 --- /dev/null +++ b/fastapi_book_server/app/views/book.py @@ -0,0 +1,81 @@ +from typing import Union + +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params +from fastapi_pagination.ext.ormar import paginate +from app.utils.pagination import CustomPage + +from app.models import Book as BookDB, Author as AuthorDB, AuthorAnnotation as AuthorAnnotationDB +from app.serializers.book import Book, CreateBook, UpdateBook, CreateRemoteBook +from app.services.book import BookTGRMSearchService, BookCreator + + +book_router = APIRouter( + prefix="/api/v1/books", + tags=["book"], +) + + +@book_router.get("/", response_model=CustomPage[Book], dependencies=[Depends(Params)]) +async def get_books(): + return await paginate( + BookDB.objects.select_related("authors") + ) + + +@book_router.post("/", response_model=Book) +async def create_book(data: Union[CreateBook, CreateRemoteBook]): + book = await BookCreator.create(data) + + return await BookDB.objects.select_related("authors").get(id=book.id) + + +@book_router.get("/{id}", response_model=Book) +async def get_book(id: int): + book = await BookDB.objects.select_related("authors").get_or_none(id=id) + + if book is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return book + + +@book_router.put("/{id}", response_model=Book) +async def update_book(id: int, data: UpdateBook): + book = await BookDB.objects.select_related("authors").get_or_none(id=id) + + if book is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + for author in list(book.authors): + await book.authors.remove(author) + + data_dict = data.dict() + + author_ids = data_dict.pop("authors", []) + authors = await AuthorDB.objects.filter(id__in=author_ids).all() + + book = await BookDB.objects.create( + **data_dict + ) + + for author in authors: + await book.authors.add(author) + + return book + + +@book_router.get("/{id}/annotation") +async def get_book_annotation(id: int): + annotation = await AuthorAnnotationDB.objects.get(book__id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return annotation + + +@book_router.get("/search/{query}", response_model=CustomPage[Book], dependencies=[Depends(Params)]) +async def search_books(query: str): + return await BookTGRMSearchService.get(query) diff --git a/fastapi_book_server/app/views/book_annotation.py b/fastapi_book_server/app/views/book_annotation.py new file mode 100644 index 0000000..223ba6d --- /dev/null +++ b/fastapi_book_server/app/views/book_annotation.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params, Page +from fastapi_pagination.ext.ormar import paginate + +from app.models import BookAnnotation as BookAnnotationDB +from app.serializers.book_annotation import BookAnnotation, CreateBookAnnotation, UpdateBookAnnotation + + +book_annotation_router = APIRouter( + prefix="/api/v1/book_annotations", + tags=["book_annotation"] +) + + +@book_annotation_router.get("/", response_model=Page[BookAnnotation], dependencies=[Depends(Params)]) +async def get_book_annotations(): + return await paginate( + BookAnnotationDB.objects + ) + + +@book_annotation_router.post("/", response_model=BookAnnotation) +async def create_book_annotation(data: CreateBookAnnotation): + return await BookAnnotationDB.objects.create( + **data.dict() + ) + + +@book_annotation_router.get("/{id}", response_model=BookAnnotation) +async def get_book_annotation(id: int): + annotation = await BookAnnotationDB.objects.get_or_none(id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return annotation + + +@book_annotation_router.put("/{id}", response_model=BookAnnotation) +async def update_book_annotation(id: int, data: UpdateBookAnnotation): + annotation = await BookAnnotationDB.objects.get_or_none(id=id) + + if annotation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + annotation.update_from_dict(data.dict()) + + return annotation.save() diff --git a/fastapi_book_server/app/views/sequence.py b/fastapi_book_server/app/views/sequence.py new file mode 100644 index 0000000..a1a1bad --- /dev/null +++ b/fastapi_book_server/app/views/sequence.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends + +from fastapi_pagination import Params +from fastapi_pagination.ext.ormar import paginate +from app.utils.pagination import CustomPage + +from app.models import Sequence as SequenceDB +from app.serializers.sequence import Sequence, CreateSequence +from app.services.sequence import SequenceTGRMSearchService + + +sequence_router = APIRouter( + prefix="/api/v1/sequences", + tags=["sequence"] +) + + +@sequence_router.get("/", response_model=CustomPage[Sequence], dependencies=[Depends(Params)]) +async def get_sequences(): + return await paginate( + SequenceDB.objects + ) + + +@sequence_router.get("/{id}", response_model=Sequence) +async def get_sequence(id: int): + return await SequenceDB.objects.get(id=id) + + +@sequence_router.post("/", response_model=Sequence) +async def create_sequence(data: CreateSequence): + return await SequenceDB.objects.create( + **data.dict() + ) + + +@sequence_router.get("/search/{query}", response_model=CustomPage[Sequence], dependencies=[Depends(Params)]) +async def search_sequences(query: str): + return await SequenceTGRMSearchService.get(query) diff --git a/fastapi_book_server/app/views/sequence_info.py b/fastapi_book_server/app/views/sequence_info.py new file mode 100644 index 0000000..a13d9d7 --- /dev/null +++ b/fastapi_book_server/app/views/sequence_info.py @@ -0,0 +1,46 @@ +from typing import Union + +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params +from fastapi_pagination.ext.ormar import paginate +from app.utils.pagination import CustomPage + +from app.models import SequenceInfo as SequenceInfoDB +from app.serializers.sequence_info import SequenceInfo, CreateSequenceInfo, CreateRemoteSequenceInfo +from app.services.sequence_info import SequenceInfoCreator + + +sequence_info_router = APIRouter( + prefix="/api/v1/sequence_info", + tags=["sequence_info"] +) + + +@sequence_info_router.get("/", response_model=CustomPage[SequenceInfo], dependencies=[Depends(Params)]) +async def get_sequence_infos(): + return await paginate( + SequenceInfoDB.objects.prefetch_related(["book", "sequence"]) + .select_related(["book__authors", "book__translations", "book__translations__translator"]) + ) + + +@sequence_info_router.get("/{id}", response_model=SequenceInfo) +async def get_sequence_info(id: int): + sequence_info = SequenceInfoDB.objects.prefetch_related(["book", "sequence"]) \ + .select_related(["book__authors", "book__translations", "book__translations__translator"]) \ + .get_or_none(id=id) + + if sequence_info is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + return sequence_info + + +@sequence_info_router.post("/", response_model=SequenceInfo) +async def create_sequence_info(data: Union[CreateSequenceInfo, CreateRemoteSequenceInfo]): + sequence_info = await SequenceInfoCreator.create(data) + + return await SequenceInfoDB.objects.prefetch_related(["book", "sequence"]) \ + .select_related(["book__authors", "book__translations", "book__translations__translator"]) \ + .get(id=sequence_info.id) diff --git a/fastapi_book_server/app/views/source.py b/fastapi_book_server/app/views/source.py new file mode 100644 index 0000000..452c970 --- /dev/null +++ b/fastapi_book_server/app/views/source.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends + +from fastapi_pagination import Params, Page +from fastapi_pagination.ext.ormar import paginate + +from app.models import Source as SourceDB +from app.serializers.source import Source, CreateSource + + +source_router = APIRouter( + prefix="/api/v1/sources", + tags=["source"], +) + + +@source_router.get("", response_model=Page[Source], dependencies=[Depends(Params)]) +async def get_sources(): + return await paginate(SourceDB.objects) + + +@source_router.post("", response_model=Source) +async def create_source(data: CreateSource): + return await SourceDB.objects.create( + **data.dict() + ) diff --git a/fastapi_book_server/app/views/translation.py b/fastapi_book_server/app/views/translation.py new file mode 100644 index 0000000..ca9f46b --- /dev/null +++ b/fastapi_book_server/app/views/translation.py @@ -0,0 +1,43 @@ +from typing import Union + +from fastapi import APIRouter, Depends, HTTPException, status + +from fastapi_pagination import Params +from fastapi_pagination.ext.ormar import paginate +from app.utils.pagination import CustomPage + +from app.models import Translation as TranslationDB +from app.serializers.translation import Translation, CreateTranslation, CreateRemoteTranslation +from app.services.translation import TranslationCreator + + +translation_router = APIRouter( + prefix="/api/v1/translation", + tags=["translation"] +) + + +@translation_router.get("/", response_model=CustomPage[Translation], dependencies=[Depends(Params)]) +async def get_translations(): + return await paginate( + TranslationDB.objects.prefetch_related(["book", "translator"]) + ) + + +@translation_router.post("/", response_model=Translation) +async def create_translation(data: Union[CreateTranslation, CreateRemoteTranslation]): + translation = await TranslationCreator.create(data) + + return await TranslationDB.objects.prefetch_related(["book", "translator"]).get(id=translation.id) + + +@translation_router.delete("/{id}", response_model=Translation) +async def delete_translation(id: int): + translation = await TranslationDB.objects.prefetch_related(["book", "translator"]).get_or_none(id=id) + + if translation is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + await translation.delete() + + return translation diff --git a/fastapi_book_server/core/app.py b/fastapi_book_server/core/app.py new file mode 100644 index 0000000..5c75188 --- /dev/null +++ b/fastapi_book_server/core/app.py @@ -0,0 +1,31 @@ +from operator import add +from fastapi import FastAPI +from fastapi_pagination import add_pagination + +from core.db import database +from app.views import routers + + +def start_app() -> FastAPI: + app = FastAPI() + + app.state.database = database + + for router in routers: + app.include_router(router) + + add_pagination(app) + + @app.on_event('startup') + async def startup() -> None: + database_ = app.state.database + if not database_.is_connected: + await database_.connect() + + @app.on_event('shutdown') + async def shutdown() -> None: + database_ = app.state.database + if database_.is_connected: + await database_.disconnect() + + return app diff --git a/fastapi_book_server/core/config.py b/fastapi_book_server/core/config.py new file mode 100644 index 0000000..3916d1b --- /dev/null +++ b/fastapi_book_server/core/config.py @@ -0,0 +1,16 @@ +from pydantic import BaseSettings + + +class EnvConfig(BaseSettings): + POSTGRES_USER: str + POSTGRES_PASSWORD: str + POSTGRES_HOST: str + POSTGRES_PORT: int + POSTGRES_DB: str + + class Config: + env_file = '.env' + env_file_encoding = 'utf-8' + + +env_config = EnvConfig() diff --git a/fastapi_book_server/core/db.py b/fastapi_book_server/core/db.py new file mode 100644 index 0000000..6037ba3 --- /dev/null +++ b/fastapi_book_server/core/db.py @@ -0,0 +1,15 @@ +from urllib.parse import quote +from databases import Database + +from sqlalchemy import MetaData + +from core.config import env_config + + +DATABASE_URL = ( + f"postgresql://{env_config.POSTGRES_USER}:{quote(env_config.POSTGRES_PASSWORD)}@" + f"{env_config.POSTGRES_HOST}:{env_config.POSTGRES_PORT}/{env_config.POSTGRES_DB}" +) + +metadata = MetaData() +database = Database(DATABASE_URL) diff --git a/fastapi_book_server/main.py b/fastapi_book_server/main.py new file mode 100644 index 0000000..0a4385b --- /dev/null +++ b/fastapi_book_server/main.py @@ -0,0 +1,4 @@ +from core.app import start_app + + +app = start_app() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..db2e466 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,734 @@ +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + +[[package]] +name = "alembic" +version = "1.7.4" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "anyio" +version = "3.3.4" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +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)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "asyncpg" +version = "0.24.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +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)"] +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)"] +test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +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"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "databases" +version = "0.5.3" +description = "Async database support for Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +sqlalchemy = ">=1.4,<1.5" + +[package.extras] +mysql = ["aiomysql"] +postgresql = ["asyncpg"] +postgresql_aiopg = ["aiopg"] +sqlite = ["aiosqlite"] + +[[package]] +name = "fastapi" +version = "0.70.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +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" +starlette = "0.16.0" + +[package.extras] +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)"] +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)"] +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)"] +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)"] + +[[package]] +name = "fastapi-pagination" +version = "0.9.0" +description = "FastAPI pagination" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +fastapi = ">=0.61.2" +ormar = {version = ">=0.10.5", optional = true, markers = "extra == \"ormar\" or extra == \"all\""} +pydantic = ">=1.7.2" + +[package.extras] +gino = ["gino[starlette] (>=1.0.1)", "SQLAlchemy (>=1.3.20)"] +all = ["gino[starlette] (>=1.0.1)", "SQLAlchemy (>=1.3.20)", "databases[sqlite,postgresql,mysql] (>=0.4.0)", "orm (>=0.1.5)", "tortoise-orm[aiosqlite,aiomysql,asyncpg] (>=0.16.18,<0.18.0)", "asyncpg (>=0.24.0)", "ormar (>=0.10.5)", "Django (<3.3.0)", "piccolo (>=0.29,<0.35)"] +sqlalchemy = ["SQLAlchemy (>=1.3.20)"] +asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"] +databases = ["databases[sqlite,postgresql,mysql] (>=0.4.0)"] +orm = ["databases[sqlite,postgresql,mysql] (>=0.4.0)", "orm (>=0.1.5)", "typesystem (>=0.2.0,<0.3.0)"] +django = ["databases[sqlite,postgresql,mysql] (>=0.4.0)", "Django (<3.3.0)"] +tortoise = ["tortoise-orm[aiosqlite,aiomysql,asyncpg] (>=0.16.18,<0.18.0)"] +ormar = ["ormar (>=0.10.5)"] +piccolo = ["piccolo (>=0.29,<0.35)"] + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "mako" +version = "1.1.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "more-itertools" +version = "8.10.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ormar" +version = "0.10.22" +description = "A simple async ORM with fastapi in mind and pydantic validation." +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +aiosqlite = ">=0.17.0,<0.18.0" +databases = ">=0.3.2,<0.5.4" +pydantic = ">=1.6.1,<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,<=1.8.2" +SQLAlchemy = ">=1.3.18,<1.4.26" + +[package.extras] +postgresql = ["asyncpg (>=0.24.0,<0.25.0)", "psycopg2-binary (>=2.9.1,<3.0.0)"] +postgres = ["asyncpg (>=0.24.0,<0.25.0)", "psycopg2-binary (>=2.9.1,<3.0.0)"] +dev = ["asyncpg (>=0.24.0,<0.25.0)", "psycopg2-binary (>=2.9.1,<3.0.0)", "aiomysql (>=0.0.21,<0.0.22)", "cryptography (>=35.0.0,<36.0.0)", "orjson (>=3.6.4,<4.0.0)"] +mysql = ["aiomysql (>=0.0.21,<0.0.22)"] +crypto = ["cryptography (>=35.0.0,<36.0.0)"] +orjson = ["orjson (>=3.6.4,<4.0.0)"] + +[[package]] +name = "packaging" +version = "21.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "psycopg2" +version = "2.9.1" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "0.19.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sqlalchemy" +version = "1.4.25" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +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\")"} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.0)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "starlette" +version = "0.16.0" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "uvicorn" +version = "0.15.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +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)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "e750b0a06df1d4d45f85eddc65fedf25e468482a39f78e824da7415ae34dbb1d" + +[metadata.files] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] +alembic = [ + {file = "alembic-1.7.4-py3-none-any.whl", hash = "sha256:e3cab9e59778b3b6726bb2da9ced451c6622d558199fd3ef914f3b1e8f4ef704"}, + {file = "alembic-1.7.4.tar.gz", hash = "sha256:9d33f3ff1488c4bfab1e1a6dfebbf085e8a8e1a3e047a43ad29ad1f67f012a1d"}, +] +anyio = [ + {file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"}, + {file = "anyio-3.3.4.tar.gz", hash = "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +asyncpg = [ + {file = "asyncpg-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4fc0205fe4ddd5aeb3dfdc0f7bafd43411181e1f5650189608e5971cceacff1"}, + {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"}, + {file = "asyncpg-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ff5073d4b654e34bd5eaadc01dc4d68b8a9609084d835acd364cd934190a08d"}, + {file = "asyncpg-0.24.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36c6806883786b19551bb70a4882561f31135dc8105a59662e0376cf5b2cbc5"}, + {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"}, + {file = "asyncpg-0.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41704c561d354bef01353835a7846e5606faabbeb846214dfcf666cf53319f18"}, + {file = "asyncpg-0.24.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ef6ae0a617fc13cc2ac5dc8e9b367bb83cba220614b437af9b67766f4b6b20"}, + {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"}, + {file = "asyncpg-0.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:129d501f3d30616afd51eb8d3142ef51ba05374256bd5834cec3ef4956a9b317"}, + {file = "asyncpg-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a458fc69051fbb67d995fdda46d75a012b5d6200f91e17d23d4751482640ed4c"}, + {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"}, + {file = "asyncpg-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:a738f4807c853623d3f93f0fea11f61be6b0e5ca16ea8aeb42c2c7ee742aa853"}, + {file = "asyncpg-0.24.0.tar.gz", hash = "sha256:dd2fa063c3344823487d9ddccb40802f02622ddf8bf8a6cc53885ee7a2c1c0c6"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +click = [ + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +databases = [ + {file = "databases-0.5.3-py3-none-any.whl", hash = "sha256:23862bd96241d8fcbf97eea82995ccb3baa8415c3cb106832b7509f296322f86"}, + {file = "databases-0.5.3.tar.gz", hash = "sha256:b69d74ee0b47fa30bb6e76db0c58da998e973393259d29215d8fb29352162bd6"}, +] +fastapi = [ + {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, + {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, +] +fastapi-pagination = [ + {file = "fastapi-pagination-0.9.0.tar.gz", hash = "sha256:1ee9784a5fcf82c44a92673567845fbf4e4ad07e9b9d9f395783c0ae15c1dfe9"}, + {file = "fastapi_pagination-0.9.0-py3-none-any.whl", hash = "sha256:ade455825866f71560882e053381caa06a2141d36811d1bb3a37879df808f3b6"}, +] +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +mako = [ + {file = "Mako-1.1.5-py2.py3-none-any.whl", hash = "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23"}, + {file = "Mako-1.1.5.tar.gz", hash = "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +more-itertools = [ + {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, + {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, +] +ormar = [ + {file = "ormar-0.10.22-py3-none-any.whl", hash = "sha256:d38d53dc191442902c541db06a1c260f5a3820d1690cbcd79a3b6edca37b4653"}, + {file = "ormar-0.10.22.tar.gz", hash = "sha256:6235f2898c818350970ea3b60ddda41d92c75308be60520a6e5466dbb9ceafe3"}, +] +packaging = [ + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +psycopg2 = [ + {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, + {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, + {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, + {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, + {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, + {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, + {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, + {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, + {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +python-dotenv = [ + {file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"}, + {file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.4.25-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a36ea43919e51b0de0c0bc52bcfdad7683f6ea9fb81b340cdabb9df0e045e0f7"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:75cd5d48389a7635393ff5a9214b90695c06b3d74912109c3b00ce7392b69c6c"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-win32.whl", hash = "sha256:16ef07e102d2d4f974ba9b0d4ac46345a411ad20ad988b3654d59ff08e553b1c"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27m-win_amd64.whl", hash = "sha256:a79abdb404d9256afb8aeaa0d3a4bc7d3b6d8b66103d8b0f2f91febd3909976e"}, + {file = "SQLAlchemy-1.4.25-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ad59e2e16578b6c1a2873e4888134112365605b08a6067dd91e899e026efa1c"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a505ecc0642f52e7c65afb02cc6181377d833b7df0994ecde15943b18d0fa89c"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a28fe28c359835f3be20c89efd517b35e8f97dbb2ca09c6cf0d9ac07f62d7ef6"}, + {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"}, + {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"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-win32.whl", hash = "sha256:6b602e3351f59f3999e9fb8b87e5b95cb2faab6a6ecdb482382ac6fdfbee5266"}, + {file = "SQLAlchemy-1.4.25-cp36-cp36m-win_amd64.whl", hash = "sha256:6400b22e4e41cc27623a9a75630b7719579cd9a3a2027bcf16ad5aaa9a7806c0"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dd4ed12a775f2cde4519f4267d3601990a97d8ecde5c944ab06bfd6e8e8ea177"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b7778a205f956755e05721eebf9f11a6ac18b2409bff5db53ce5fe7ede79831"}, + {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"}, + {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"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-win32.whl", hash = "sha256:0566a6e90951590c0307c75f9176597c88ef4be2724958ca1d28e8ae05ec8822"}, + {file = "SQLAlchemy-1.4.25-cp37-cp37m-win_amd64.whl", hash = "sha256:0b08a53e40b34205acfeb5328b832f44437956d673a6c09fce55c66ab0e54916"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:33a1e86abad782e90976de36150d910748b58e02cd7d35680d441f9a76806c18"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ed67aae8cde4d32aacbdba4f7f38183d14443b714498eada5e5a7a37769c0b7"}, + {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"}, + {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"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-win32.whl", hash = "sha256:c211e8ec81522ce87b0b39f0cf0712c998d4305a030459a0e115a2b3dc71598f"}, + {file = "SQLAlchemy-1.4.25-cp38-cp38-win_amd64.whl", hash = "sha256:9a1df8c93a0dd9cef0839917f0c6c49f46c75810cf8852be49884da4a7de3c59"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1b38db2417b9f7005d6ceba7ce2a526bf10e3f6f635c0f163e6ed6a42b5b62b2"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37621b37c73b034997b5116678862f38ee70e5a054821c7b19d0e55df270dec"}, + {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"}, + {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"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-win32.whl", hash = "sha256:6003771ea597346ab1e97f2f58405c6cacbf6a308af3d28a9201a643c0ac7bb3"}, + {file = "SQLAlchemy-1.4.25-cp39-cp39-win_amd64.whl", hash = "sha256:9ebe49c3960aa2219292ea2e5df6acdc425fc828f2f3d50b4cfae1692bcb5f02"}, + {file = "SQLAlchemy-1.4.25.tar.gz", hash = "sha256:1adf3d25e2e33afbcd48cfad8076f9378793be43e7fec3e4334306cac6bec138"}, +] +starlette = [ + {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, + {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +uvicorn = [ + {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, + {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..253241c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "fastapi_book_server" +version = "0.1.0" +description = "" +authors = ["Kurbanov Bulat "] + +[tool.poetry.dependencies] +python = "^3.9" +fastapi = "^0.70.0" +pydantic = {extras = ["dotenv"], version = "^1.8.2"} +uvicorn = "^0.15.0" +ormar = "^0.10.22" +alembic = "^1.7.4" +asyncpg = "^0.24.0" +psycopg2 = "^2.9.1" +fastapi-pagination = {extras = ["ormar"], version = "^0.9.0"} + +[tool.poetry.dev-dependencies] +pytest = "^5.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fastapi_book_server.py b/tests/test_fastapi_book_server.py new file mode 100644 index 0000000..e1f39f1 --- /dev/null +++ b/tests/test_fastapi_book_server.py @@ -0,0 +1,5 @@ +from fastapi_book_server import __version__ + + +def test_version(): + assert __version__ == '0.1.0'