Add linters configs

This commit is contained in:
2022-01-01 20:59:14 +03:00
parent 2a70687df0
commit 2a31f27f44
14 changed files with 532 additions and 76 deletions

35
.github/workflows/linters.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Linters
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
jobs:
Run-Pre-Commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 32
- uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install pre-commit
run: pip3 install pre-commit
- name: Pre-commit (Push)
env:
SETUPTOOLS_USE_DISTUTILS: stdlib
if: ${{ github.event_name == 'push' }}
run: pre-commit run --source ${{ github.event.before }} --origin ${{ github.event.after }} --show-diff-on-failure
- name: Pre-commit (Pull-Request)
env:
SETUPTOOLS_USE_DISTUTILS: stdlib
if: ${{ github.event_name == 'pull_request' }}
run: pre-commit run --source ${{ github.event.pull_request.base.sha }} --origin ${{ github.event.pull_request.head.sha }} --show-diff-on-failure

20
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,20 @@
exclude: 'docs|node_modules|migrations|.git|.tox'
repos:
- repo: https://github.com/ambv/black
rev: 21.12b0
hooks:
- id: black
language_version: python3.9
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/csachs/pyproject-flake8
rev: v0.0.1a2.post1
hooks:
- id: pyproject-flake8
additional_dependencies: [
'-e', 'git+https://github.com/pycqa/pyflakes@1911c20#egg=pyflakes',
'-e', 'git+https://github.com/pycqa/pycodestyle@d219c68#egg=pycodestyle',
]

View File

@@ -4,8 +4,11 @@ FROM python:3.10-slim as build-image
# && apt-get install --no-install-recommends -y gcc build-essential python3-dev libpq-dev libffi-dev \ # && apt-get install --no-install-recommends -y gcc build-essential python3-dev libpq-dev libffi-dev \
# && rm -rf /var/lib/apt/lists/* # && rm -rf /var/lib/apt/lists/*
WORKDIR / WORKDIR /root/poetry
COPY ./requirements.txt ./ COPY pyproject.toml poetry.lock /root/poetry/
RUN pip install poetry --no-cache-dir \
&& poetry export --without-hashes > requirements.txt
ENV VENV_PATH=/opt/venv ENV VENV_PATH=/opt/venv
RUN python -m venv $VENV_PATH \ RUN python -m venv $VENV_PATH \

346
poetry.lock generated Normal file
View File

@@ -0,0 +1,346 @@
[[package]]
name = "anyio"
version = "3.4.0"
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", "contextlib2", "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 = "certifi"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.9"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[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 = "fastapi"
version = "0.70.1"
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 = "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 = "httpcore"
version = "0.14.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
anyio = ">=3.0.0,<4.0.0"
certifi = "*"
h11 = ">=0.11,<0.13"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
[[package]]
name = "httpx"
version = "0.21.1"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
certifi = "*"
charset-normalizer = "*"
httpcore = ">=0.14.0,<0.15.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*"
[package.extras]
brotli = ["brotlicffi", "brotli"]
cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"]
http2 = ["h2 (>=3,<5)"]
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "pydantic"
version = "1.9.0"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
python-versions = ">=3.6.1"
[package.dependencies]
typing-extensions = ">=3.7.4.3"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "rfc3986"
version = "1.5.0"
description = "Validating URI References per RFC 3986"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
[package.extras]
idna2008 = ["idna"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[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 = "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 = "transliterate"
version = "1.10.2"
description = "Bi-directional transliterator for Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.1.0"
[[package]]
name = "typing-extensions"
version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "uvicorn"
version = "0.16.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 = ["httptools (>=0.2.0,<0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "websockets (>=9.1)", "websockets (>=10.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "49aa23d6fb30b9df82433bb137f2692b36688c5e794dc1611d8247cb0b026c78"
[metadata.files]
anyio = [
{file = "anyio-3.4.0-py3-none-any.whl", hash = "sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020"},
{file = "anyio-3.4.0.tar.gz", hash = "sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d"},
]
asgiref = [
{file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"},
{file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
]
certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"},
{file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"},
]
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"},
]
fastapi = [
{file = "fastapi-0.70.1-py3-none-any.whl", hash = "sha256:5367226c7bcd7bfb2e17edaf225fd9a983095b1372281e9a3eb661336fb93748"},
{file = "fastapi-0.70.1.tar.gz", hash = "sha256:21d03979b5336375c66fa5d1f3126c6beca650d5d2166fbb78345a30d33c8d06"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
]
httpcore = [
{file = "httpcore-0.14.3-py3-none-any.whl", hash = "sha256:9a98d2416b78976fc5396ff1f6b26ae9885efbb3105d24eed490f20ab4c95ec1"},
{file = "httpcore-0.14.3.tar.gz", hash = "sha256:d10162a63265a0228d5807964bd964478cbdb5178f9a2eedfebb2faba27eef5d"},
]
httpx = [
{file = "httpx-0.21.1-py3-none-any.whl", hash = "sha256:208e5ef2ad4d105213463cfd541898ed9d11851b346473539a8425e644bb7c66"},
{file = "httpx-0.21.1.tar.gz", hash = "sha256:02af20df486b78892a614a7ccd4e4e86a5409ec4981ab0e422c579a887acad83"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
pydantic = [
{file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"},
{file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"},
{file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"},
{file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"},
{file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"},
{file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"},
{file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"},
{file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"},
{file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"},
{file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"},
{file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"},
{file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"},
{file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"},
{file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"},
{file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"},
{file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"},
{file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"},
{file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"},
{file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"},
{file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"},
{file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"},
{file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"},
{file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"},
{file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"},
{file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"},
{file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"},
{file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"},
{file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"},
{file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"},
{file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"},
{file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"},
{file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"},
{file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"},
{file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"},
{file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"},
]
rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sniffio = [
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
]
starlette = [
{file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"},
{file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"},
]
transliterate = [
{file = "transliterate-1.10.2-py2.py3-none-any.whl", hash = "sha256:010a5021bf6021689c4fade0985f3f7b3db1f2f16a48a09a56797f171c08ed42"},
{file = "transliterate-1.10.2.tar.gz", hash = "sha256:bc608e0d48e687db9c2b1d7ea7c381afe0d1849cad216087d8e03d8d06a57c85"},
]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
]
uvicorn = [
{file = "uvicorn-0.16.0-py3-none-any.whl", hash = "sha256:d8c839231f270adaa6d338d525e2652a0b4a5f4c2430b5c4ef6ae4d11776b0d2"},
{file = "uvicorn-0.16.0.tar.gz", hash = "sha256:eacb66afa65e0648fcbce5e746b135d09722231ffffc61883d4fac2b62fbea8d"},
]

58
pyproject.toml Normal file
View File

@@ -0,0 +1,58 @@
[tool.poetry]
name = "books_downloader"
version = "0.1.0"
description = ""
authors = ["Kurbanov Bulat <kurbanovbul@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.9"
fastapi = "^0.70.1"
httpx = "^0.21.1"
transliterate = "^1.10.2"
uvicorn = {extras = ["standart"], version = "^0.16.0"}
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.vscode
| \venv
| alembic
)/
'''
[tool.flake8]
ignore = [
# Whitespace before ':' ( https://www.flake8rules.com/rules/E203.html )
"E203"
]
max-line-length=88
max-complexity = 15
select = "B,C,E,F,W,T4,B9"
exclude = [
# No need to traverse our git directory
".git",
# There's no value in checking cache directories
"__pycache__",
# The conf file is mostly autogenerated, ignore it
"src/app/alembic/*",
# The old directory contains Flake8 2.0
]
[tool.isort]
profile = "black"
only_sections = true
force_sort_within_sections = true
lines_after_imports = 2
lexicographical = true
sections = ["FUTURE", "STDLIB", "BASEFRAMEWORK", "FRAMEWORKEXT", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
known_baseframework = ["fastapi",]
known_frameworkext = ["starlette",]
src_paths = ["src"]

View File

@@ -1,5 +0,0 @@
fastapi
pydantic
httpx
transliterate
uvicorn[standart]

View File

@@ -6,4 +6,6 @@ from core.config import env_config
async def check_token(api_key: str = Security(default_security)): async def check_token(api_key: str = Security(default_security)):
if api_key != env_config.API_KEY: if api_key != env_config.API_KEY:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Wrong api key!") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Wrong api key!"
)

View File

@@ -3,5 +3,7 @@ from typing import Protocol
class BaseDownloader(Protocol): class BaseDownloader(Protocol):
@classmethod @classmethod
async def download(cls, remote_id: int, file_type: str, source_id: int) -> tuple[bytes, str]: async def download(
cls, remote_id: int, file_type: str, source_id: int
) -> tuple[bytes, str]:
... ...

View File

@@ -1,15 +1,13 @@
from datetime import date
from typing import Generic, TypeVar from typing import Generic, TypeVar
import json
import httpx import httpx
from datetime import date
from pydantic import BaseModel from pydantic import BaseModel
from core.config import env_config from core.config import env_config
T = TypeVar('T') T = TypeVar("T")
class Page(BaseModel, Generic[T]): class Page(BaseModel, Generic[T]):
@@ -47,7 +45,7 @@ class BookLibraryClient:
@classmethod @classmethod
@property @property
def auth_headers(cls): def auth_headers(cls):
return {'Authorization': cls.API_KEY} return {"Authorization": cls.API_KEY}
@classmethod @classmethod
async def _make_request(cls, url) -> dict: async def _make_request(cls, url) -> dict:
@@ -65,6 +63,8 @@ class BookLibraryClient:
@classmethod @classmethod
async def get_remote_book(cls, source_id: int, book_id: int) -> Book: async def get_remote_book(cls, source_id: int, book_id: int) -> Book:
data = await cls._make_request(f"{cls.BASE_URL}/api/v1/books/remote/{source_id}/{book_id}") data = await cls._make_request(
f"{cls.BASE_URL}/api/v1/books/remote/{source_id}/{book_id}"
)
return Book.parse_obj(data) return Book.parse_obj(data)

View File

@@ -1,13 +1,12 @@
from app.services.base import BaseDownloader from app.services.base import BaseDownloader
from app.services.fl_downloader import FLDownloader
from app.services.book_library import BookLibraryClient from app.services.book_library import BookLibraryClient
from app.services.fl_downloader import FLDownloader
class DownloadersManager: class DownloadersManager:
SOURCES_TABLE: dict[int, str] = {} SOURCES_TABLE: dict[int, str] = {}
DOWNLOADERS_TABLE: dict[str, type[BaseDownloader]] = { DOWNLOADERS_TABLE: dict[str, type[BaseDownloader]] = {
'flibusta': FLDownloader, "flibusta": FLDownloader,
} }
PREPARED = False PREPARED = False

View File

@@ -1,13 +1,11 @@
from asyncio.exceptions import CancelledError
from typing import Optional, cast
import asyncio import asyncio
from typing import Optional, cast
import httpx import httpx
from app.services.base import BaseDownloader from app.services.base import BaseDownloader
from app.services.utils import zip, unzip, get_filename, process_pool_executor
from app.services.book_library import BookLibraryClient, Book from app.services.book_library import BookLibraryClient, Book
from app.services.utils import zip, unzip, get_filename, process_pool_executor
from core.config import env_config, SourceConfig from core.config import env_config, SourceConfig
@@ -40,17 +38,19 @@ class FLDownloader(BaseDownloader):
await asyncio.wait_for(self.get_book_data_task, None) await asyncio.wait_for(self.get_book_data_task, None)
if self.book is None: if self.book is None:
raise ValueError('Book is None!') raise ValueError("Book is None!")
return get_filename(self.book, self.file_type) return get_filename(self.book, self.file_type)
async def get_final_filename(self) -> str: async def get_final_filename(self) -> str:
if self.need_zip: if self.need_zip:
return (await self.get_filename()) + '.zip' return (await self.get_filename()) + ".zip"
return await self.get_filename() return await self.get_filename()
async def _download_from_source(self, source_config: SourceConfig, file_type: str = None) -> tuple[bytes, bool]: async def _download_from_source(
self, source_config: SourceConfig, file_type: str = None
) -> tuple[bytes, bool]:
basic_url: str = source_config.URL basic_url: str = source_config.URL
proxy: Optional[str] = source_config.PROXY proxy: Optional[str] = source_config.PROXY
@@ -63,16 +63,14 @@ class FLDownloader(BaseDownloader):
httpx_proxy = None httpx_proxy = None
if proxy is not None: if proxy is not None:
httpx_proxy = httpx.Proxy( httpx_proxy = httpx.Proxy(url=proxy)
url=proxy
)
async with httpx.AsyncClient(proxies=httpx_proxy) as client: async with httpx.AsyncClient(proxies=httpx_proxy) as client:
response = await client.get(url, follow_redirects=True, timeout=10 * 60) response = await client.get(url, follow_redirects=True, timeout=10 * 60)
content_type = response.headers.get("Content-Type") content_type = response.headers.get("Content-Type")
if response.status_code != 200: if response.status_code != 200:
raise NotSuccess(f'Status code is {response.status_code}!') raise NotSuccess(f"Status code is {response.status_code}!")
if "text/html" in content_type: if "text/html" in content_type:
raise ReceivedHTML() raise ReceivedHTML()
@@ -82,11 +80,15 @@ class FLDownloader(BaseDownloader):
return response.content, False return response.content, False
async def _wait_until_some_done(self, tasks: set[asyncio.Task]) -> Optional[tuple[bytes, bool]]: async def _wait_until_some_done(
self, tasks: set[asyncio.Task]
) -> Optional[tuple[bytes, bool]]:
tasks_ = tasks tasks_ = tasks
while tasks_: while tasks_:
done, pending = await asyncio.wait(tasks_, return_when=asyncio.FIRST_COMPLETED) done, pending = await asyncio.wait(
tasks_, return_when=asyncio.FIRST_COMPLETED
)
for task in done: for task in done:
try: try:
@@ -108,9 +110,7 @@ class FLDownloader(BaseDownloader):
for source in env_config.FL_SOURCES: for source in env_config.FL_SOURCES:
tasks.add( tasks.add(
asyncio.create_task( asyncio.create_task(self._download_from_source(source, file_type="fb2"))
self._download_from_source(source, file_type='fb2')
)
) )
data = await self._wait_until_some_done(tasks) data = await self._wait_until_some_done(tasks)
@@ -122,13 +122,15 @@ class FLDownloader(BaseDownloader):
if is_zip: if is_zip:
content = await asyncio.get_event_loop().run_in_executor( content = await asyncio.get_event_loop().run_in_executor(
process_pool_executor, unzip, content, 'fb2' process_pool_executor, unzip, content, "fb2"
) )
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
form = {'format': self.file_type} form = {"format": self.file_type}
files = {'file': content} files = {"file": content}
response = await client.post(env_config.CONVERTER_URL, data=form, files=files, timeout=2 * 60) response = await client.post(
env_config.CONVERTER_URL, data=form, files=files, timeout=2 * 60
)
if response.status_code != 200: if response.status_code != 200:
raise ValueError raise ValueError
@@ -143,19 +145,11 @@ class FLDownloader(BaseDownloader):
async def _get_content(self) -> tuple[bytes, str]: async def _get_content(self) -> tuple[bytes, str]:
tasks = set() tasks = set()
if self.file_type in ['epub', 'mobi']: if self.file_type in ["epub", "mobi"]:
tasks.add( tasks.add(asyncio.create_task(self._download_with_converting()))
asyncio.create_task(
self._download_with_converting()
)
)
for source in env_config.FL_SOURCES: for source in env_config.FL_SOURCES:
tasks.add( tasks.add(asyncio.create_task(self._download_from_source(source)))
asyncio.create_task(
self._download_from_source(source)
)
)
data = await self._wait_until_some_done(tasks) data = await self._wait_until_some_done(tasks)
@@ -192,6 +186,8 @@ class FLDownloader(BaseDownloader):
return tasks[0].result() return tasks[0].result()
@classmethod @classmethod
async def download(cls, remote_id: int, file_type: str, source_id: int) -> tuple[bytes, str]: async def download(
cls, remote_id: int, file_type: str, source_id: int
) -> tuple[bytes, str]:
downloader = cls(remote_id, file_type, source_id) downloader = cls(remote_id, file_type, source_id)
return await downloader._download() return await downloader._download()

View File

@@ -1,8 +1,7 @@
from concurrent.futures.process import ProcessPoolExecutor
import io import io
import zipfile import zipfile
from concurrent.futures.process import ProcessPoolExecutor
import transliterate import transliterate
from app.services.book_library import Book, BookAuthor from app.services.book_library import Book, BookAuthor
@@ -23,10 +22,10 @@ def zip(filename, content):
buffer = io.BytesIO() buffer = io.BytesIO()
zip_file = zipfile.ZipFile( zip_file = zipfile.ZipFile(
file=buffer, file=buffer,
mode='w', mode="w",
compression=zipfile.ZIP_DEFLATED, compression=zipfile.ZIP_DEFLATED,
allowZip64=False, allowZip64=False,
compresslevel=9 compresslevel=9,
) )
zip_file.writestr(filename, content) zip_file.writestr(filename, content)
@@ -60,29 +59,33 @@ def get_filename(book: Book, file_type: str) -> str:
if book.authors: if book.authors:
filename_parts.append( filename_parts.append(
'_'.join([get_short_name(a) for a in book.authors]) + '_-_' "_".join([get_short_name(a) for a in book.authors]) + "_-_"
) )
if book.title.startswith(" "): if book.title.startswith(" "):
filename_parts.append( filename_parts.append(book.title[1:])
book.title[1:]
)
else: else:
filename_parts.append( filename_parts.append(book.title)
book.title
)
filename = "".join(filename_parts) filename = "".join(filename_parts)
if book.lang in ['ru']: if book.lang in ["ru"]:
filename = transliterate.translit(filename, 'ru', reversed=True) filename = transliterate.translit(filename, "ru", reversed=True)
for c in "(),….!\"?»«':": for c in "(),….!\"?»«':":
filename = filename.replace(c, '') filename = filename.replace(c, "")
for c, r in (('', '-'), ('/', '_'), ('', 'N'), (' ', '_'), ('', '-'), ('á', 'a'), (' ', '_')): for c, r in (
("", "-"),
("/", "_"),
("", "N"),
(" ", "_"),
("", "-"),
("á", "a"),
(" ", "_"),
):
filename = filename.replace(c, r) filename = filename.replace(c, r)
right_part = f'.{book.id}.{file_type}' right_part = f".{book.id}.{file_type}"
return filename[:64 - len(right_part)] + right_part return filename[: 64 - len(right_part)] + right_part

View File

@@ -1,9 +1,8 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi.responses import Response from fastapi.responses import Response
from app.services.dowloaders_manager import DownloadersManager
from app.depends import check_token from app.depends import check_token
from app.services.dowloaders_manager import DownloadersManager
router = APIRouter( router = APIRouter(
@@ -19,8 +18,5 @@ async def download(source_id: int, remote_id: int, file_type: str):
content, filename = await downloader.download(remote_id, file_type, source_id) content, filename = await downloader.download(remote_id, file_type, source_id)
return Response( return Response(
content, content, headers={"Content-Disposition": f"attachment; filename={filename}"}
headers={
"Content-Disposition": f"attachment; filename={filename}"
}
) )

View File

@@ -1,3 +1,4 @@
from core.app import start_app from core.app import start_app
app = start_app() app = start_app()