From 2a31f27f440802a905254a93347584fd90cdf371 Mon Sep 17 00:00:00 2001 From: Kurbanov Bulat Date: Sat, 1 Jan 2022 20:59:14 +0300 Subject: [PATCH] Add linters configs --- .github/workflows/linters.yaml | 35 +++ .pre-commit-config.yaml | 20 ++ docker/build.dockerfile | 7 +- poetry.lock | 346 +++++++++++++++++++++++++ pyproject.toml | 58 +++++ requirements.txt | 5 - src/app/depends.py | 4 +- src/app/services/base.py | 4 +- src/app/services/book_library.py | 12 +- src/app/services/dowloaders_manager.py | 5 +- src/app/services/fl_downloader.py | 66 +++-- src/app/services/utils.py | 37 +-- src/app/views.py | 8 +- src/main.py | 1 + 14 files changed, 532 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/linters.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml new file mode 100644 index 0000000..3b3fb20 --- /dev/null +++ b/.github/workflows/linters.yaml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5cf5615 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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', + ] diff --git a/docker/build.dockerfile b/docker/build.dockerfile index 2100c50..5299c54 100644 --- a/docker/build.dockerfile +++ b/docker/build.dockerfile @@ -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 \ # && rm -rf /var/lib/apt/lists/* -WORKDIR / -COPY ./requirements.txt ./ +WORKDIR /root/poetry +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 RUN python -m venv $VENV_PATH \ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d1c1bb4 --- /dev/null +++ b/poetry.lock @@ -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"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7d1c5f4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[tool.poetry] +name = "books_downloader" +version = "0.1.0" +description = "" +authors = ["Kurbanov Bulat "] + +[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"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a398275..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi -pydantic -httpx -transliterate -uvicorn[standart] diff --git a/src/app/depends.py b/src/app/depends.py index b99768e..39e7e32 100644 --- a/src/app/depends.py +++ b/src/app/depends.py @@ -6,4 +6,6 @@ from core.config import env_config async def check_token(api_key: str = Security(default_security)): 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!" + ) diff --git a/src/app/services/base.py b/src/app/services/base.py index 11b177e..20fd9de 100644 --- a/src/app/services/base.py +++ b/src/app/services/base.py @@ -3,5 +3,7 @@ from typing import Protocol class BaseDownloader(Protocol): @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]: ... diff --git a/src/app/services/book_library.py b/src/app/services/book_library.py index dfc25fe..33ddcd0 100644 --- a/src/app/services/book_library.py +++ b/src/app/services/book_library.py @@ -1,15 +1,13 @@ +from datetime import date from typing import Generic, TypeVar -import json import httpx - -from datetime import date from pydantic import BaseModel from core.config import env_config -T = TypeVar('T') +T = TypeVar("T") class Page(BaseModel, Generic[T]): @@ -47,7 +45,7 @@ class BookLibraryClient: @classmethod @property def auth_headers(cls): - return {'Authorization': cls.API_KEY} + return {"Authorization": cls.API_KEY} @classmethod async def _make_request(cls, url) -> dict: @@ -65,6 +63,8 @@ class BookLibraryClient: @classmethod 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) diff --git a/src/app/services/dowloaders_manager.py b/src/app/services/dowloaders_manager.py index 232b61d..bfe84b9 100644 --- a/src/app/services/dowloaders_manager.py +++ b/src/app/services/dowloaders_manager.py @@ -1,13 +1,12 @@ from app.services.base import BaseDownloader -from app.services.fl_downloader import FLDownloader - from app.services.book_library import BookLibraryClient +from app.services.fl_downloader import FLDownloader class DownloadersManager: SOURCES_TABLE: dict[int, str] = {} DOWNLOADERS_TABLE: dict[str, type[BaseDownloader]] = { - 'flibusta': FLDownloader, + "flibusta": FLDownloader, } PREPARED = False diff --git a/src/app/services/fl_downloader.py b/src/app/services/fl_downloader.py index 1740d23..b5c31c1 100644 --- a/src/app/services/fl_downloader.py +++ b/src/app/services/fl_downloader.py @@ -1,13 +1,11 @@ -from asyncio.exceptions import CancelledError -from typing import Optional, cast import asyncio +from typing import Optional, cast import httpx 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.utils import zip, unzip, get_filename, process_pool_executor from core.config import env_config, SourceConfig @@ -40,17 +38,19 @@ class FLDownloader(BaseDownloader): await asyncio.wait_for(self.get_book_data_task, None) if self.book is None: - raise ValueError('Book is None!') + raise ValueError("Book is None!") return get_filename(self.book, self.file_type) async def get_final_filename(self) -> str: if self.need_zip: - return (await self.get_filename()) + '.zip' - + return (await self.get_filename()) + ".zip" + 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 proxy: Optional[str] = source_config.PROXY @@ -63,16 +63,14 @@ class FLDownloader(BaseDownloader): httpx_proxy = None if proxy is not None: - httpx_proxy = httpx.Proxy( - url=proxy - ) + httpx_proxy = httpx.Proxy(url=proxy) async with httpx.AsyncClient(proxies=httpx_proxy) as client: response = await client.get(url, follow_redirects=True, timeout=10 * 60) content_type = response.headers.get("Content-Type") 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: raise ReceivedHTML() @@ -82,11 +80,15 @@ class FLDownloader(BaseDownloader): 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 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: try: @@ -100,7 +102,7 @@ class FLDownloader(BaseDownloader): continue tasks_ = pending - + return None async def _download_with_converting(self) -> tuple[bytes, bool]: @@ -108,9 +110,7 @@ class FLDownloader(BaseDownloader): for source in env_config.FL_SOURCES: tasks.add( - asyncio.create_task( - self._download_from_source(source, file_type='fb2') - ) + asyncio.create_task(self._download_from_source(source, file_type="fb2")) ) data = await self._wait_until_some_done(tasks) @@ -122,13 +122,15 @@ class FLDownloader(BaseDownloader): if is_zip: 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: - form = {'format': self.file_type} - files = {'file': content} - response = await client.post(env_config.CONVERTER_URL, data=form, files=files, timeout=2 * 60) + form = {"format": self.file_type} + files = {"file": content} + response = await client.post( + env_config.CONVERTER_URL, data=form, files=files, timeout=2 * 60 + ) if response.status_code != 200: raise ValueError @@ -143,20 +145,12 @@ class FLDownloader(BaseDownloader): async def _get_content(self) -> tuple[bytes, str]: tasks = set() - if self.file_type in ['epub', 'mobi']: - tasks.add( - asyncio.create_task( - self._download_with_converting() - ) - ) + if self.file_type in ["epub", "mobi"]: + tasks.add(asyncio.create_task(self._download_with_converting())) for source in env_config.FL_SOURCES: - tasks.add( - asyncio.create_task( - self._download_from_source(source) - ) - ) - + tasks.add(asyncio.create_task(self._download_from_source(source))) + data = await self._wait_until_some_done(tasks) if data is None: @@ -192,6 +186,8 @@ class FLDownloader(BaseDownloader): return tasks[0].result() @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) return await downloader._download() diff --git a/src/app/services/utils.py b/src/app/services/utils.py index 9929e7a..4b579b8 100644 --- a/src/app/services/utils.py +++ b/src/app/services/utils.py @@ -1,8 +1,7 @@ +from concurrent.futures.process import ProcessPoolExecutor import io import zipfile -from concurrent.futures.process import ProcessPoolExecutor - import transliterate from app.services.book_library import Book, BookAuthor @@ -23,10 +22,10 @@ def zip(filename, content): buffer = io.BytesIO() zip_file = zipfile.ZipFile( file=buffer, - mode='w', + mode="w", compression=zipfile.ZIP_DEFLATED, allowZip64=False, - compresslevel=9 + compresslevel=9, ) zip_file.writestr(filename, content) @@ -60,29 +59,33 @@ def get_filename(book: Book, file_type: str) -> str: if book.authors: 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(" "): - filename_parts.append( - book.title[1:] - ) + filename_parts.append(book.title[1:]) else: - filename_parts.append( - book.title - ) + filename_parts.append(book.title) filename = "".join(filename_parts) - if book.lang in ['ru']: - filename = transliterate.translit(filename, 'ru', reversed=True) + if book.lang in ["ru"]: + filename = transliterate.translit(filename, "ru", reversed=True) 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) - 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 diff --git a/src/app/views.py b/src/app/views.py index 1f4bd0d..d8e623b 100644 --- a/src/app/views.py +++ b/src/app/views.py @@ -1,9 +1,8 @@ from fastapi import APIRouter, Depends from fastapi.responses import Response -from app.services.dowloaders_manager import DownloadersManager - from app.depends import check_token +from app.services.dowloaders_manager import DownloadersManager 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) return Response( - content, - headers={ - "Content-Disposition": f"attachment; filename={filename}" - } + content, headers={"Content-Disposition": f"attachment; filename={filename}"} ) diff --git a/src/main.py b/src/main.py index 2739482..0a4385b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,4 @@ from core.app import start_app + app = start_app()