diff --git a/.dockerignore b/.dockerignore
index 30454daa..62268d75 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -107,7 +107,7 @@ celerybeat.pid
*.sage.py
# Environments
-.envs/.env
+.env
.venv
env/
venv/
@@ -150,3 +150,11 @@ LICENSE
README.md
redis-data
redis.conf
+staticfiles/
+infra/
+docker/
+docker-compose.yml
+.gcloudignore
+.ipython/
+.editorconfig
+.env.example
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..23da48e3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,2 @@
+[*.py]
+max_line_length = 100
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..8f80d581
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,30 @@
+# Your local config to develop without docker
+
+# These values are already defaults, this is just for preview
+# DATABASE_URL=postgres://postgres:postgres@localhost:5432/sora
+# REDIS_URL=redis://localhost:8883
+# TYPESENSE_HOST=localhost
+
+# Whitespace-separated list
+# ALLOWED_HOSTS=*
+
+##########
+# DJANGO #
+##########
+
+DEBUG=1
+SECRET_KEY=DevServer
+
+#########
+# Other #
+#########
+
+SENTRY_DSN=
+
+GOOGLE_CLIENT=
+GOOGLE_SECRET=
+
+TYPESENSE_HOST=localhost
+TYPESENSE_API_KEY=tsapikey
+
+PROXY=
diff --git a/.envs.example/deployment.env.example b/.envs.example/deployment.env.example
deleted file mode 100644
index c4bc4cf8..00000000
--- a/.envs.example/deployment.env.example
+++ /dev/null
@@ -1,16 +0,0 @@
-# Used in compose from deployment repo
-
-# ########
-# DJANGO #
-# ########
-
-SECRET_KEY=SECRET_KEY
-PAGE_SIZE=20
-
-# #######
-# Other #
-# #######
-
-SENTRY_DSN=YOUR_DSN_KEY
-# WEBDRIVER_PATH=/chromedriver/chromedriver
-PROXY=
diff --git a/.envs.example/docker.env.example b/.envs.example/docker.env.example
deleted file mode 100644
index 22170b18..00000000
--- a/.envs.example/docker.env.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# Used in docker compose
-
-# ######
-# DATA #
-########
-
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
-POSTGRES_DB=sora
-
-DATABASE_HOST=db
-DATABASE_USER=postgres
-DATABASE_PASSWORD=postgres
-
-DATABASE_PORT=5432
-DATABASE_NAME=sora
-
-REDIS_URL=redis://redis:6379
-
-ELASTICSEARCH_HOST=elasticsearch:9200
-
-# ########
-# DJANGO #
-# ########
-
-DEBUG=1
-SECRET_KEY=DevServer
-# Whitespace-separated list
-ALLOWED_HOSTS=*
-PAGE_SIZE=20
-
-# #######
-# Other #
-# #######
-
-SENTRY_DSN=YOUR_DSN_KEY
-WEBDRIVER_PATH=/chromedriver/chromedriver
-PROXY=
diff --git a/.envs.example/local.env.example b/.envs.example/local.env.example
deleted file mode 100644
index 0ccc5e15..00000000
--- a/.envs.example/local.env.example
+++ /dev/null
@@ -1,36 +0,0 @@
-# Your local config to develop without docker
-
-# ######
-# DATA #
-# ######
-
-DATABASE_HOST=localhost
-DATABASE_USER=postgres
-DATABASE_PASSWORD=postgres
-
-# Port from docker-compose
-DATABASE_PORT=8882
-DATABASE_NAME=sora
-
-# Port from docker-compose
-REDIS_URL=redis://localhost:8883
-
-# Port from docker-compose
-ELASTICSEARCH_HOST=localhost:9200
-
-# ########
-# DJANGO #
-# ########
-
-DEBUG=1
-SECRET_KEY=DevServer
-# Whitespace-separated list
-ALLOWED_HOSTS=*
-PAGE_SIZE=20
-
-# #######
-# Other #
-# #######
-
-SENTRY_DSN=YOUR_DSN_KEY
-PROXY=
diff --git a/.gcloudignore b/.gcloudignore
new file mode 100644
index 00000000..3f74bf9f
--- /dev/null
+++ b/.gcloudignore
@@ -0,0 +1,221 @@
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+infra/
+.ipython/
+logs/
+.github/
+docker/
+LICENSE
+.flake8
+.env.example
+.editorconfig
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index c7bcea32..00000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Deploy
-
-on:
- push:
- tags:
- - 'v*'
-
-jobs:
- ssh-trigger-deploy:
- environment:
- name: "Main"
- url: "https://backend.sora-reader.app"
- runs-on: ubuntu-latest
- steps:
- - name: Deploy to ssh server
- uses: appleboy/ssh-action@v0.1.4
- with:
- debug: true
- host: ${{ secrets.SSH_HOST }}
- username: ${{ secrets.SSH_USERNAME }}
- password: ${{ secrets.SSH_PASSWORD }}
- script : |
- . $HOME/.profile
- cd "$DEPLOYMENT_DIR"
- make deploy backend
-
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 43b9fcc5..13eea2d5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -9,22 +9,21 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- - uses: dhvcc/python-poetry-setup@v1
- with:
- python-version: 3.8.1
+ - uses: actions/checkout@v3
- name: Install poetry
- uses: snok/install-poetry@v1.0.0
+ run: pipx install poetry
+
+ - uses: actions/setup-python@v4.1.0
+ id: python-setup
with:
- virtualenvs-create: true
- virtualenvs-in-project: true
+ python-version: '3.10'
+ cache: poetry
- name: Install dependencies
- if: steps.poetry-cache-test.outputs.cache-hit != 'true'
- run: |
- poetry install
+ if: steps.python-setup.outputs.cache-hit != 'true'
+ run: poetry install
- name: Lint
- run: |
- poetry run pre-commit run --all-files
+ run: make check
diff --git a/.gitignore b/.gitignore
index c38de282..98544554 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,30 @@
-.idea
-.vscode
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+.idea/dataSources.xml
+.idea/redisSettings.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# File-based project format
+*.iws
+
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -64,6 +89,8 @@ cover/
# Django stuff:
*.log
local_settings.py
+*.sqlite3
+*.sqlite
db.sqlite3
db.sqlite3-journal
@@ -84,10 +111,6 @@ target/
# Jupyter Notebook
.ipynb_checkpoints
-# IPython
-profile_default/
-ipython_config.py
-
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
@@ -144,7 +167,6 @@ dmypy.json
# Cython debug symbols
cython_debug/
-.idea/
.envs/
# sitemaps
@@ -157,3 +179,10 @@ redis-data
redis.conf
.vim
nohup.out
+.vscode
+tests/.jmeter*
+dbs/
+*.pid
+
+# Show be an executable of the webhook server
+infra/webhook
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..13566b81
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/backend.iml b/.idea/backend.iml
new file mode 100644
index 00000000..8397a56d
--- /dev/null
+++ b/.idea/backend.iml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml
new file mode 100644
index 00000000..02b915b8
--- /dev/null
+++ b/.idea/git_toolbox_prj.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..e8112868
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..e066844e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/parse_chapters.xml b/.idea/runConfigurations/parse_chapters.xml
new file mode 100644
index 00000000..204237a0
--- /dev/null
+++ b/.idea/runConfigurations/parse_chapters.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/parse_detail.xml b/.idea/runConfigurations/parse_detail.xml
new file mode 100644
index 00000000..59561fd2
--- /dev/null
+++ b/.idea/runConfigurations/parse_detail.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/parse_images.xml b/.idea/runConfigurations/parse_images.xml
new file mode 100644
index 00000000..80aec1ab
--- /dev/null
+++ b/.idea/runConfigurations/parse_images.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/parse_list.xml b/.idea/runConfigurations/parse_list.xml
new file mode 100644
index 00000000..eb20e6e3
--- /dev/null
+++ b/.idea/runConfigurations/parse_list.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/rebuild_index.xml b/.idea/runConfigurations/rebuild_index.xml
new file mode 100644
index 00000000..82d572de
--- /dev/null
+++ b/.idea/runConfigurations/rebuild_index.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/rq_worker.xml b/.idea/runConfigurations/rq_worker.xml
new file mode 100644
index 00000000..62839f72
--- /dev/null
+++ b/.idea/runConfigurations/rq_worker.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/scopes/Project_and_venv.xml b/.idea/scopes/Project_and_venv.xml
new file mode 100644
index 00000000..40b0daa4
--- /dev/null
+++ b/.idea/scopes/Project_and_venv.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 00000000..670094f1
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml
new file mode 100644
index 00000000..7874ce77
--- /dev/null
+++ b/.idea/watcherTasks.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/api_docs/__init__.py b/.ipython/profile_default/ipython_config.py
similarity index 100%
rename from apps/api_docs/__init__.py
rename to .ipython/profile_default/ipython_config.py
diff --git a/.ipython/profile_default/startup/10-config.py b/.ipython/profile_default/startup/10-config.py
new file mode 100644
index 00000000..fd04ee72
--- /dev/null
+++ b/.ipython/profile_default/startup/10-config.py
@@ -0,0 +1,8 @@
+from django.contrib.postgres.aggregates import ArrayAgg # noqa
+from django.core.cache import cache, caches # noqa
+from django.db import connection # noqa
+from django.db.models import * # noqa
+from django.db.models.functions import * # noqa
+from rich import inspect, pretty # noqa
+
+pretty.install()
diff --git a/.ipython/profile_default/startup/README b/.ipython/profile_default/startup/README
new file mode 100644
index 00000000..61d47000
--- /dev/null
+++ b/.ipython/profile_default/startup/README
@@ -0,0 +1,11 @@
+This is the IPython startup directory
+
+.py and .ipy files in this directory will be run *prior* to any code or files specified
+via the exec_lines or exec_files configurables whenever you load this profile.
+
+Files will be run in lexicographical order, so you can control the execution order of files
+with a prefix, e.g.::
+
+ 00-first.py
+ 50-middle.py
+ 99-last.ipy
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 62126468..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,38 +0,0 @@
-FROM python:3.8-slim as base
-
-RUN apt-get update
-
-# TODO: Revert when Selenium will be needed
-# Install chrome wed driver
-# RUN apt-get install -y gnupg wget curl unzip --no-install-recommends && \
-# wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
-# echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list && \
-# apt-get update -y && \
-# apt-get install -y google-chrome-stable && \
-# CHROMEVER=$(google-chrome --product-version | grep -o "[^\.]*\.[^\.]*\.[^\.]*") && \
-# DRIVERVER=$(curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROMEVER") && \
-# wget -q --continue -P /chromedriver "http://chromedriver.storage.googleapis.com/$DRIVERVER/chromedriver_linux64.zip" && \
-# unzip /chromedriver/chromedriver* -d /chromedriver
-
-RUN apt-get install -y \
- gcc g++ \
- libffi-dev \
- libpq-dev \
- '^postgresql-client-.+$' \
- gettext git make
-
-ENV PYTHONDONTWRITEBYTECODE=1 \
- PYTHONUNBUFFERED=1
-
-WORKDIR /app
-
-RUN pip install poetry
-COPY pyproject.toml poetry.lock ./
-RUN poetry config virtualenvs.create false
-RUN poetry install
-
-COPY . .
-
-RUN chmod +x docker-entrypoint.sh
-
-CMD ["./docker-entrypoint.sh"]
diff --git a/Dockerfile.dev b/Dockerfile.dev
deleted file mode 100644
index 713cae06..00000000
--- a/Dockerfile.dev
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM python:3.8-slim as base
-
-RUN apt-get update && apt-get install -y \
- gcc \
- g++ \
- libffi-dev \
- libpq-dev \
- '^postgresql-client-.+$' \
- gettext
-
-ENV PYTHONDONTWRITEBYTECODE=1 \
- PYTHONUNBUFFERED=1
-
-WORKDIR /app
-
-RUN pip install poetry
-COPY pyproject.toml poetry.lock ./
-RUN poetry config virtualenvs.create false
-RUN poetry install
-
-EXPOSE 8000
-
-CMD ["./docker-entrypoint.sh"]
diff --git a/Makefile b/Makefile
index f9c60651..4306ff4b 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,8 @@ COFF ?= \033[0m
COMPOSE = docker-compose.yml
port ?= 8000
-.PHONY: help env venv shell dev \
- check fix
+.PHONY: help env venv shell shell-sql \
+ jmeter test check fix githooks watch-sass
.ONESHELL:
@@ -27,65 +27,41 @@ interpreter := $(shell poetry env info > /dev/null 2>&1 && echo "poetry run")
extract_ignores = $(shell awk '/.*.py/{split($$1,a,":"); print a[1]}' .flake8 | tr '\n' ',')
check-dotenv:
- @$(eval DOTENVS := $(shell test -n "$$SECRET_KEY" || (test -f ./.envs/docker.env && test -f ./.envs/local.env) && echo 'nonzero string'))
- $(if $(DOTENVS),,$(error No .env files found, maybe run "make env"?))
+ @$(eval DOTENVS := $(shell test -f "/.dockerenv" || test -f ./.env && echo 'nonzero string'))
+ $(if $(DOTENVS),,$(error No .env file found, maybe run "make env"?))
check-venv:
$(if $(interpreter),, $(error No virtual environment found, run "make venv"))
env: ## Copy env examples and init .envs directory
- @mkdir -p .envs
- @for file in .envs.example/*; do
- [[ "$$file" != *deployment* ]] && \
- echo "$$file" ".envs/$$(basename ".envs/$${file%%.example}")"
- @done
- @$(print) "${CYAN}Done${COFF}"
-
-githooks:
+ @cp .env.example .env
+
+githooks: ## Install git hooks
@$(interpreter) pre-commit install -t=pre-commit -t=pre-push
venv: ## Create virtual environment and install all dependencies
- @python3.8 -m pip install poetry==1.1.4
+ @python3 -m pip install pipx && pipx install poetry
@poetry install && \
$(print); $(print) "${CYAN}Created venv and installed all dependencies${COFF}"
shell: check-dotenv check-venv ## Run django-extension's shell_plus
- @$(interpreter) ./manage.py shell_plus --ipython --print-sql -- -i -c """
- from rich import pretty, inspect
- from django.db import connection
- from django.db.models import *
- from django.db.models.functions import *
- from django.contrib.postgres.aggregates import ArrayAgg
- pretty.install()
- """
-
-shell-sql: check-dotenv check-venv ## Run django-extension's shell_plus
- @$(interpreter) ./manage.py shell_plus --ipython --print-sql -- -i -c """from rich import pretty, inspect
- pretty.install()
- """
-
-runserver: check-dotenv check-venv ## Run dev server on port 8000, or specify with "make dev port=1234"
- @. ./.envs/local.env && if [ "$(DEBUG)" = 0 ]; then ./manage.py collectstatic --noinput --clear; fi
- @$(interpreter) ./manage.py migrate --noinput
- @$(interpreter) ./manage.py runserver 0.0.0.0:$(port)
- @$(print) "${CYAN}Backend is running on localhost:$(port)${COFF}"
-
-development: ## run dev docker
- @# Force recreate to reload NGINX config
- @# as it won't rebuild because the config is passed as a volume
- @docker-compose -f ${COMPOSE} up -d --build --force-recreate
- @$(print) "${CYAN}Backend is running on http://localhost:8880${COFF}"
-
-stop: ## stop docker containers
- @docker-compose -f ${COMPOSE} down
-
-clear: ## down containers and clear volumes
- @docker-compose -f ${COMPOSE} down --volumes
+ @env IPYTHONDIR="./.ipython" $(interpreter) ./manage.py shell_plus --ipython $(args)
+
+shell-sql: check-dotenv check-venv ## Run shell plus with sql logging
+ @make shell args=--print-sql
###############
# Code checks #
###############
+jmeter: ## Run jmeter tests
+ cd tests
+ rm -rf .jmeter_report .jmeter_results
+ jmeter -n -t Manga.jmx -l .jmeter_results -e -o .jmeter_report && firefox .jmeter_report/index.html
+
+test: ## Run django tests
+ @$(interpreter) pytest
+
check: check-venv ## Run linters
@$(print) "flake8"
@$(print) "======"
@@ -119,3 +95,11 @@ fix: check-venv ## Run code formatters
watch-sass:
@/bin/find -type d -name 'scss' -not -path '*/staticfiles/*' | xargs -P 0 -l -i bash -c 'sass --watch "$$1:$${1:0:-5}/css"' - '{}'
+
+#########
+# Infra #
+#########
+
+webhook-server:
+ cd infra
+ nohup ./webhook -hooks hooks.yaml -verbose &
diff --git a/README.md b/README.md
index 55e87fcf..6b3a6427 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,11 @@

+## Notes
+
+- Postgres 15+ is mandatory for NULLS NOT DISTINCT indexes
+- [webhook-server](https://github.com/adnanh/webhook/releases)
+
## Installation requirements
- PostgreSQL(and `libpq-dev`)
diff --git a/apps/core/migrations/__init__.py b/apps/__init__.py
similarity index 100%
rename from apps/core/migrations/__init__.py
rename to apps/__init__.py
diff --git a/apps/api_docs/apps.py b/apps/api_docs/apps.py
deleted file mode 100644
index 29b741a1..00000000
--- a/apps/api_docs/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class ApiDocsConfig(AppConfig):
- name = "apps.api_docs"
diff --git a/apps/api_docs/templates/redoc-ui.html b/apps/api_docs/templates/redoc-ui.html
deleted file mode 100644
index f05aceb4..00000000
--- a/apps/api_docs/templates/redoc-ui.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% load static%}
-
-
- ReDoc
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/api_docs/templates/swagger-ui.html b/apps/api_docs/templates/swagger-ui.html
deleted file mode 100644
index 1a920737..00000000
--- a/apps/api_docs/templates/swagger-ui.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% load static%}
-
-
- API Documentation
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/api_docs/urls.py b/apps/api_docs/urls.py
deleted file mode 100644
index 2b572352..00000000
--- a/apps/api_docs/urls.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from django.urls import path
-from django.views.generic import TemplateView
-
-urlpatterns = [
- # UI
- path(
- "",
- TemplateView.as_view(
- template_name="swagger-ui.html", extra_context={"schema_url": "openapi-schema"}
- ),
- name="swagger-ui",
- ),
- path(
- "redoc/",
- TemplateView.as_view(
- template_name="redoc-ui.html", extra_context={"schema_url": "openapi-schema"}
- ),
- name="redoc-ui",
- ),
-]
diff --git a/apps/login/__init__.py b/apps/authentication/__init__.py
similarity index 100%
rename from apps/login/__init__.py
rename to apps/authentication/__init__.py
diff --git a/apps/login/migrations/__init__.py b/apps/authentication/admin.py
similarity index 100%
rename from apps/login/migrations/__init__.py
rename to apps/authentication/admin.py
diff --git a/apps/authentication/apps.py b/apps/authentication/apps.py
new file mode 100644
index 00000000..205d25ad
--- /dev/null
+++ b/apps/authentication/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class AuthenticationConfig(AppConfig):
+ name = "apps.authentication"
+
+ def ready(self):
+ from . import signals # noqa
diff --git a/apps/parse/api/__init__.py b/apps/authentication/migrations/__init__.py
similarity index 100%
rename from apps/parse/api/__init__.py
rename to apps/authentication/migrations/__init__.py
diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py
new file mode 100644
index 00000000..87e6bb41
--- /dev/null
+++ b/apps/authentication/signals.py
@@ -0,0 +1,33 @@
+from allauth.account.utils import perform_login
+from allauth.socialaccount.signals import pre_social_login
+from allauth.utils import get_user_model
+from django.conf import settings
+from django.dispatch import receiver
+
+from apps.authentication.utils import redirect_with_cookie
+
+
+@receiver(pre_social_login)
+def pre_social_login_handler(request, sociallogin, *args, **kwargs): # noqa
+ """
+ Take email out and try to find user with the same email
+ If the user exists, verify email if needed, login and redirect to home
+ """
+ email_address = sociallogin.account.extra_data["email"]
+ user_model = get_user_model()
+ users = user_model.objects.filter(email=email_address)
+ if users:
+ user = users[0]
+
+ email_addrs = sociallogin.email_addresses
+ if email_addrs:
+ addr = sociallogin.email_addresses[0]
+ # Verify if needed
+ if not addr.verified:
+ addr.verified = True
+ addr.save()
+
+ # allauth.account.app_settings.EmailVerificationMethod
+ perform_login(request, user, email_verification=settings.ACCOUNT_EMAIL_VERIFICATION)
+
+ redirect_with_cookie(request)
diff --git a/apps/authentication/urls.py b/apps/authentication/urls.py
new file mode 100644
index 00000000..735497aa
--- /dev/null
+++ b/apps/authentication/urls.py
@@ -0,0 +1,9 @@
+from allauth.socialaccount.providers.google.views import oauth2_login
+from django.urls import path
+from django.views.decorators.csrf import csrf_exempt
+
+from apps.authentication.utils import auto_save_frontend_url
+
+urlpatterns = [
+ path("oauth/", csrf_exempt(auto_save_frontend_url(oauth2_login))),
+]
diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py
new file mode 100644
index 00000000..63166d9b
--- /dev/null
+++ b/apps/authentication/utils.py
@@ -0,0 +1,40 @@
+from allauth.exceptions import ImmediateHttpResponse
+from django.conf import settings
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from ninja_jwt.tokens import RefreshToken
+
+from apps.core.api.schemas import ErrorSchema
+
+frontend_url_key = "frontend_url"
+
+
+def save_frontend_url(request):
+ """Use this method in initial API view that starts auth process to later send a session."""
+ # Save as http to allow it to work on localhost, production UI redirects to HTTPS by default
+ url = request.POST.get("redirectUrl", None)
+ if not url:
+ return "No redirect url"
+ if any([url.startswith(o) for o in settings.CSRF_TRUSTED_ORIGINS]):
+ request.session[frontend_url_key] = url
+ else:
+ return "Host is not in the trusted origins list."
+
+
+def auto_save_frontend_url(f):
+ """Just a view decorator to call save_frontend_url."""
+
+ def wrapper(request, *args, **kwargs):
+ if e := save_frontend_url(request):
+ return HttpResponse(ErrorSchema(error=e).json(), status=400)
+ return f(request, *args, **kwargs)
+
+ return wrapper
+
+
+def redirect_with_cookie(request):
+ """Get host url and session from session and send session token via a redirect."""
+ host = request.session[frontend_url_key]
+ resp = redirect(f"{host}?t={RefreshToken.for_user(request.user)}")
+
+ raise ImmediateHttpResponse(resp)
diff --git a/apps/core/abc/admin.py b/apps/core/abc/admin.py
index 29ff1280..dbe6b939 100644
--- a/apps/core/abc/admin.py
+++ b/apps/core/abc/admin.py
@@ -2,12 +2,12 @@
from typing import Dict, Optional, Tuple, Type, Union
from django.contrib import admin
-from django.db.models import Model
+from django.db.models import Model, QuerySet
from django.db.models.fields import Field
from django.utils.html import format_html
from apps.core.abc.models import BaseModel
-from apps.parse.models import Manga
+from apps.manga.models import Manga
class ImagePreviewMixin:
@@ -65,7 +65,7 @@ def short_description(self) -> str:
return self.lookup._meta.verbose_name_plural.capitalize()
return self.description
- def get_queryset(self) -> list:
+ def get_queryset(self) -> QuerySet:
queryset = None
if self.lookup_type is property:
queryset = self.lookup.__get__(self.obj)
@@ -79,7 +79,7 @@ def get_queryset(self) -> list:
queryset = getattr(self.obj, cached_relation)
else:
# Search for all fields, find first relation matching model and add it to cache
- for field in self.obj._meta.get_fields():
+ for field in self.obj._meta.get_fields(): # noqa
if getattr(field, "related_model", None) is self.lookup:
queryset = getattr(self.obj, field.attname)
self.__class__.RELATED_CACHE[cache_key] = field.attname
@@ -103,6 +103,7 @@ def get_values(self):
return queryset.filter(**filter_).all(), value_name
def format_values(self, values) -> str:
+ output = None
if self.lookup_type is property:
if self.html:
output = format_html(self.separator.join([self.to_html(value) for value in values]))
diff --git a/apps/core/abc/serializers.py b/apps/core/abc/serializers.py
deleted file mode 100644
index fc823083..00000000
--- a/apps/core/abc/serializers.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from rest_framework import serializers
-
-from apps.core.abc.models import BaseModel
-
-
-class NameRelatedField(serializers.StringRelatedField):
- def to_representation(self, obj: BaseModel):
- if not getattr(obj, "NAME_FIELD", None):
- return super().to_representation(obj)
- return str(getattr(obj, obj.__class__.NAME_FIELD))
diff --git a/apps/parse/migrations/__init__.py b/apps/core/api/__init__.py
similarity index 100%
rename from apps/parse/migrations/__init__.py
rename to apps/core/api/__init__.py
diff --git a/apps/core/api/schemas.py b/apps/core/api/schemas.py
new file mode 100644
index 00000000..bf3fef6d
--- /dev/null
+++ b/apps/core/api/schemas.py
@@ -0,0 +1,9 @@
+from ninja import Schema
+
+
+class MessageSchema(Schema):
+ message: str
+
+
+class ErrorSchema(Schema):
+ error: str
diff --git a/apps/core/api/utils.py b/apps/core/api/utils.py
new file mode 100644
index 00000000..efa964a7
--- /dev/null
+++ b/apps/core/api/utils.py
@@ -0,0 +1,17 @@
+from typing import List, Type
+
+from django.http import Http404
+
+from apps.core.abc.models import BaseModel
+from apps.core.api.schemas import ErrorSchema, MessageSchema
+
+
+def sora_schema(schema):
+ return {200: schema, 400: ErrorSchema, 425: MessageSchema}
+
+
+def get_model_or_404(cls: Type[BaseModel], pk, prefetch: List[str] = None):
+ obj = cls.objects.filter(pk=pk).prefetch_related(*(prefetch or [])).first()
+ if not obj:
+ raise Http404(f"No {cls.__name__.lower()} found")
+ return obj
diff --git a/apps/core/apps.py b/apps/core/apps.py
index e1ddbe1e..dfb0a5d1 100644
--- a/apps/core/apps.py
+++ b/apps/core/apps.py
@@ -1,13 +1,6 @@
from django.apps import AppConfig
-from django.core.checks import register
-
-from apps.core.checks import check_redis
class CoreConfig(AppConfig):
name = "apps.core"
verbose_name = "Core"
-
- def ready(self) -> None:
- register(check_redis)
- return super().ready()
diff --git a/apps/core/auth.py b/apps/core/auth.py
new file mode 100644
index 00000000..bf3da041
--- /dev/null
+++ b/apps/core/auth.py
@@ -0,0 +1,12 @@
+from ninja.security import APIKeyHeader
+
+
+class ApiKey(APIKeyHeader):
+ param_name = "X-API-Key"
+
+ def authenticate(self, request, key):
+ if key == "supersecret":
+ return key
+
+
+api_key_auth = ApiKey()
diff --git a/apps/core/checks.py b/apps/core/checks.py
deleted file mode 100644
index 99cebffb..00000000
--- a/apps/core/checks.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import redis
-from django.core.checks import Warning
-
-from apps.core.utils import init_redis_client
-
-
-def check_redis(*args, **kwargs):
- errors = []
- client = init_redis_client()
- try:
- client.ping()
- except redis.exceptions.ConnectionError as r_con_error:
- errors.append(
- Warning(
- "Redis unavailable",
- hint="Check REDIS_URL or if redis is running",
- obj=r_con_error,
- id="core.E001",
- )
- )
- return errors
diff --git a/apps/core/fast/__init__.py b/apps/core/fast/__init__.py
index aa843e5a..2208c9d9 100644
--- a/apps/core/fast/__init__.py
+++ b/apps/core/fast/__init__.py
@@ -1,4 +1,3 @@
"""Fast utilities."""
-from .pagination import FastLimitOffsetPagination
from .query import FastQuerySet
diff --git a/apps/core/fast/pagination.py b/apps/core/fast/pagination.py
deleted file mode 100644
index b2c9a9f9..00000000
--- a/apps/core/fast/pagination.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from rest_framework.pagination import LimitOffsetPagination
-
-from apps.core.fast.utils import get_fast_response
-
-
-class FastLimitOffsetPagination(LimitOffsetPagination):
- """Custom Pagination class to leverage FastQuerySet/orjson capabilities."""
-
- def paginate_queryset(self, queryset, request, view=None, values=()):
- self.limit = self.get_limit(request)
- if self.limit is None:
- return None
-
- self.count = self.get_count(queryset)
- self.offset = self.get_offset(request)
-
- self.request = request
- if self.count > self.limit and self.template is not None:
- self.display_page_controls = True
-
- if self.count == 0 or self.offset > self.count:
- return []
- return queryset[self.offset : self.offset + self.limit].parse_values(*values)
-
- def get_paginated_response(self, data):
- return get_fast_response(
- {
- "count": self.count,
- "next": self.get_next_link(),
- "previous": self.get_previous_link(),
- "results": data,
- }
- )
diff --git a/apps/core/fast/query.py b/apps/core/fast/query.py
index 81044b8d..f27af01a 100644
--- a/apps/core/fast/query.py
+++ b/apps/core/fast/query.py
@@ -1,10 +1,11 @@
from datetime import datetime
from decimal import Decimal
-from typing import Dict, Tuple, Union
+from typing import Tuple, Union
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models.expressions import Case, Value, When
from django.db.models.fields import TextField
+from django.db.models.functions import Cast
from django.db.models.query import QuerySet
from django.db.models.query_utils import Q
from typing_extensions import Annotated
@@ -20,6 +21,7 @@ class FastQuerySet(QuerySet):
"""
model: BaseModel
+ mangle_prefix = "_fast_"
TYPE_MAP: Annotated[dict, "Mapping to convert DB returned types into JSON-valid types"] = {
datetime: str,
@@ -27,26 +29,39 @@ class FastQuerySet(QuerySet):
Decimal: float,
}
- def mangle_annotation(self, field: str) -> str:
- """Mangle annotation if needed name to not coflict with any of model's fields."""
- return f"_fast_{field}"
+ @classmethod
+ def mangle_annotation(cls, field: str) -> str:
+ """Mangle annotation if needed name to not conflict with any of model's fields."""
+ return cls.mangle_prefix + field
@classmethod
def demangle_annotation(cls, field: str) -> str:
- """Mangle annotation if needed name to not coflict with any of model's fields."""
- return field.split("_fast_")[-1]
+ """Reverse field mangling."""
+ return field.split(cls.mangle_prefix)[-1]
+
+ def cast(self, **kwargs):
+ """
+ Simply cast values to a type.
+
+ Example: qs.cast(id=CharField()) will result in {"id": "123", ...}
+ """
+ return self.annotate(
+ **{
+ self.mangle_annotation(field): Cast(field, output_field=TextField())
+ for field, output_field in kwargs.items()
+ }
+ )
- def m2m_agg(self, **kwargs: Dict[str, Tuple[str, dict]]):
+ def m2m_agg(self, **kwargs: str | Tuple[str, Q]):
"""
Annotate M2Ms with distinct ArrayAgg for a specified field.
Accept kwargs with value of:
1. Field string, like 'field__nested_field'
- 2. A tuple of field string and a Expression filter, like ('field', Q(field='string'))
+ 2. A tuple of field string and an Expression filter, like ('field', Q(field='string'))
"""
annotation = {}
for field, args in kwargs.items():
- output = None
if type(args) is tuple:
output = ArrayAgg(args[0], filter=args[1], distinct=True)
else:
@@ -60,7 +75,7 @@ def m2m_agg(self, **kwargs: Dict[str, Tuple[str, dict]]):
return self.annotate(**annotation)
- def map(self, **kwargs: Dict[str, Tuple[str, dict]]):
+ def map(self, **kwargs: Tuple[str, dict]):
"""
Annotate queryset with CASE...WHEN generated to map provided field with python dict.
@@ -85,7 +100,9 @@ def parse_values(self, *args) -> Union[dict, list]:
result = []
annotations = self.query.annotations.keys()
values = map(
- lambda v: self.mangle_annotation(v) if self.mangle_annotation(v) in annotations else v,
+ lambda field: self.mangle_annotation(field)
+ if self.mangle_annotation(field) in annotations
+ else field,
args,
)
diff --git a/apps/core/fast/utils.py b/apps/core/fast/utils.py
deleted file mode 100644
index 00ddc1e6..00000000
--- a/apps/core/fast/utils.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import orjson
-from django.http.response import HttpResponse
-
-
-def get_fast_response(data: dict) -> HttpResponse:
- return HttpResponse(
- orjson.dumps(data),
- content_type="application/json",
- )
diff --git a/apps/core/migrations/0001_initial.py b/apps/core/migrations/0001_initial.py
deleted file mode 100644
index b6d32830..00000000
--- a/apps/core/migrations/0001_initial.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-16 20:56
-
-import django_extensions.db.fields
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='TaskControl',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
- ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
- ('task_status', models.BooleanField(default=False)),
- ('task_name', models.CharField(max_length=255, unique=True)),
- ],
- options={
- 'abstract': False,
- },
- ),
- ]
diff --git a/apps/core/migrations/0002_auto_20210822_1326.py b/apps/core/migrations/0002_auto_20210822_1326.py
deleted file mode 100644
index 82a2ca22..00000000
--- a/apps/core/migrations/0002_auto_20210822_1326.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 3.2.6 on 2021-08-22 13:26
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('core', '0001_initial'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='taskcontrol',
- old_name='task_name',
- new_name='name',
- ),
- migrations.RenameField(
- model_name='taskcontrol',
- old_name='task_status',
- new_name='status',
- ),
- ]
diff --git a/apps/core/migrations/0003_delete_taskcontrol.py b/apps/core/migrations/0003_delete_taskcontrol.py
deleted file mode 100644
index fb334105..00000000
--- a/apps/core/migrations/0003_delete_taskcontrol.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Generated by Django 3.1 on 2021-11-05 14:53
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('core', '0002_auto_20210822_1326'),
- ]
-
- operations = [
- migrations.DeleteModel(
- name='TaskControl',
- ),
- ]
diff --git a/apps/core/utils.py b/apps/core/utils.py
index d9c6dacb..608d6b15 100644
--- a/apps/core/utils.py
+++ b/apps/core/utils.py
@@ -1,24 +1,16 @@
import re
-from typing import Union
-
-import redis
-from django.conf import settings
-from rest_framework.response import Response
-from rest_framework.status import HTTP_400_BAD_REQUEST
-
-
-def format_error_response(
- error: Union[Exception, str],
- status_code=HTTP_400_BAD_REQUEST,
-) -> Response:
- """Format error to DRF's Response"""
-
- return Response({"error": str(error)}, status=status_code)
-
-
-def init_redis_client() -> redis.Redis:
- return redis.StrictRedis.from_url(settings.REDIS_URL, decode_responses=True)
def url_prefix(url: str) -> str:
- return re.match(r"(^http[s]?://(.*))/.*$", url).group(1)
+ """
+ Return host url prefix.
+
+ For example, when scraped url starts without specifying host '/somePath'
+ then, given some other url (like source_url) we can prepend it.
+
+ >>> url_prefix('https://readmanga.live/podniatie_urovnia_v_odinochku__A35c96')
+ 'https://readmanga.live'
+ >>> url_prefix('https://manga-chan.me/manga/8337-curtain.html')
+ 'https://manga-chan.me'
+ """
+ return re.match(r"(^https?://[^/]*)/.*$", url).group(1)
diff --git a/apps/login/apps.py b/apps/login/apps.py
deleted file mode 100644
index 44830be8..00000000
--- a/apps/login/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class LoginConfig(AppConfig):
- name = "apps.login"
diff --git a/apps/login/migrations/0001_initial.py b/apps/login/migrations/0001_initial.py
deleted file mode 100644
index bbb1b0ac..00000000
--- a/apps/login/migrations/0001_initial.py
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-import django.contrib.auth.models
-import django.contrib.auth.validators
-import django.utils.timezone
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('auth', '0012_alter_user_first_name_max_length'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Profile',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('password', models.CharField(max_length=128, verbose_name='password')),
- ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
- ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
- ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
- ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
- ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
- ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
- ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
- ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
- ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
- ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
- ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
- ],
- options={
- 'verbose_name': 'user',
- 'verbose_name_plural': 'users',
- 'abstract': False,
- },
- managers=[
- ('objects', django.contrib.auth.models.UserManager()),
- ],
- ),
- ]
diff --git a/apps/login/migrations/0003_delete_profile.py b/apps/login/migrations/0003_delete_profile.py
deleted file mode 100644
index c5e1b3db..00000000
--- a/apps/login/migrations/0003_delete_profile.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Generated by Django 3.2 on 2021-04-12 13:57
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("login", "0002_auto_20210310_0728"),
- ]
-
- operations = [
- migrations.DeleteModel(
- name="Profile",
- ),
- ]
diff --git a/apps/login/serializers/users.py b/apps/login/serializers/users.py
deleted file mode 100644
index a92dfe75..00000000
--- a/apps/login/serializers/users.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from django.contrib.auth.models import User
-from rest_framework import serializers
-from rest_framework_simplejwt.serializers import TokenObtainSerializer
-from rest_framework_simplejwt.tokens import RefreshToken
-
-
-class UserSerializer(serializers.ModelSerializer):
- def get_token(self) -> dict:
- refresh = RefreshToken.for_user(self.instance)
-
- return {
- "access": str(refresh.access_token),
- "refresh": str(refresh),
- }
-
- class Meta:
- model = User
- fields = ("username", "password")
-
- def create(self, validated_data):
- self.instance = User.objects.create_user(**validated_data)
- return self.instance
-
-
-class UserTokenResponseSerializer(serializers.Serializer):
- refresh = serializers.CharField()
- access = serializers.CharField()
- username = serializers.CharField()
-
-
-class UserTokenRequestSerializer(TokenObtainSerializer):
- @classmethod
- def get_token(cls, user):
- return RefreshToken.for_user(user)
-
- def validate(self, attrs):
- data = super().validate(attrs)
-
- refresh = self.get_token(self.user)
-
- data["refresh"] = str(refresh)
- data["access"] = str(refresh.access_token)
- data["username"] = self.user.get_username()
-
- return data
diff --git a/apps/login/urls.py b/apps/login/urls.py
deleted file mode 100644
index 87e04e97..00000000
--- a/apps/login/urls.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.urls import path
-from django.views.decorators.csrf import csrf_exempt
-from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
-
-from apps.login import views
-
-app_name = "auth"
-
-urlpatterns = [
- path("sign-in/", views.SignInView.as_view(), name="sign_in"),
- path("sign-up/", csrf_exempt(views.SignUpView.as_view()), name="sign_up"),
- path("sign-out/", views.SignOutView.as_view(), name="sign_out"),
- path("token-verify/", TokenVerifyView.as_view(), name="token_verify"),
- path("token-refresh/", TokenRefreshView.as_view(), name="token_refresh"),
-]
diff --git a/apps/login/views.py b/apps/login/views.py
deleted file mode 100644
index c11d1c0c..00000000
--- a/apps/login/views.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from django.http import HttpResponse
-from rest_framework import status
-from rest_framework.decorators import action
-from rest_framework.response import Response
-from rest_framework.views import APIView
-from rest_framework_simplejwt.exceptions import TokenBackendError, TokenError
-from rest_framework_simplejwt.tokens import RefreshToken
-from rest_framework_simplejwt.views import TokenViewBase
-
-from apps.login.serializers import users
-
-
-class SignUpView(APIView):
- @action(methods=("post",), detail=False, url_path="sign-up")
- def post(self, request, *args, **kwargs):
- user_serializer = users.UserSerializer(data=request.data)
- user_serializer.is_valid(raise_exception=True)
- user_serializer.create(validated_data=user_serializer.validated_data)
-
- token = user_serializer.get_token()
- token["username"] = user_serializer.validated_data["username"]
- return Response(token, status=status.HTTP_201_CREATED)
-
-
-class SignInView(TokenViewBase):
- serializer_class = users.UserTokenRequestSerializer
-
- def post(self, *args, **kwargs):
- return super().post(*args, **kwargs)
-
-
-class SignOutView(APIView):
- def get(self, request):
- print("Blacklisting")
- refresh = request.COOKIES.get("sora_refresh")
- if refresh:
- try:
- print("Blocking token ", refresh)
- token = RefreshToken(refresh)
- token.blacklist()
- except (TokenError, TokenBackendError):
- print("Token already expired")
- else:
- print("No token, nothing to blacklist")
- return HttpResponse(status=200)
diff --git a/apps/manga/__init__.py b/apps/manga/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/parse/admin.py b/apps/manga/admin.py
similarity index 75%
rename from apps/parse/admin.py
rename to apps/manga/admin.py
index 92cdd6cc..15c5a7c1 100644
--- a/apps/parse/admin.py
+++ b/apps/manga/admin.py
@@ -3,8 +3,8 @@
from django.db.models.query import QuerySet
from apps.core.abc.admin import BaseAdmin, BaseTabularInline, ImagePreviewMixin, RelatedField
-from apps.parse.const import CATALOGUE_NAMES
-from apps.parse.models import Author, Chapter, Genre, Manga, Person, PersonRelatedToManga
+from apps.manga.models import Author, Category, Chapter, Genre, Manga, Person, PersonRelatedToManga
+from apps.parse.catalogue import Catalogue
class ChapterInline(BaseTabularInline):
@@ -14,7 +14,8 @@ class ChapterInline(BaseTabularInline):
verbose_name_plural = "Chapters"
readonly_fields = ("chapter_link",)
- def chapter_link(self, obj):
+ @staticmethod
+ def chapter_link(obj):
return obj.chapter.link
@@ -29,7 +30,7 @@ class SourceFilter(SimpleListFilter):
parameter_name = "source"
def lookups(self, request, model_admin):
- return CATALOGUE_NAMES
+ return zip(Catalogue.map.sources, Catalogue.map.names)
def queryset(self, request, queryset: QuerySet):
value = self.value()
@@ -40,11 +41,11 @@ def queryset(self, request, queryset: QuerySet):
@admin.register(Manga)
class MangaAdmin(BaseAdmin, ImagePreviewMixin, admin.ModelAdmin):
- search_fields = ("title", "alt_title")
- inlines = [
+ search_fields = ("title",)
+ inlines = (
ChapterInline,
PersonInline,
- ]
+ )
list_display = (
"custom_title",
"source",
@@ -54,18 +55,19 @@ class MangaAdmin(BaseAdmin, ImagePreviewMixin, admin.ModelAdmin):
"status",
"genre_list",
)
- # list_filter = ("genres", SourceFilter)
+ readonly_fields = ("identifier", "source_url", "chapters_url")
+ list_filter = (
+ "genres",
+ SourceFilter,
+ )
def custom_title(self, obj: Manga):
concat = f"{obj.title}{', ' + obj.year if obj.year else ''}"
- if len(concat) < 30:
- if obj.alt_title:
- concat += f" ({obj.alt_title})"
- else:
+ if len(concat) > 30:
return concat[:30] + "..."
return concat
- custom_title.short_description = "Title"
+ custom_title.short_description = "Title" # noqa
authors = RelatedField(Manga.authors, description="Authors", html=True)
genre_list = RelatedField(Genre)
@@ -88,6 +90,12 @@ class GenreAdmin(BaseAdmin, admin.ModelAdmin):
list_display = ("name",)
+@admin.register(Category)
+class CategoryAdmin(BaseAdmin, admin.ModelAdmin):
+ search_fields = ("name",)
+ list_display = ("name",)
+
+
@admin.register(Chapter)
class ChapterAdmin(BaseAdmin, admin.ModelAdmin):
search_fields = ("title",)
@@ -97,5 +105,6 @@ class ChapterAdmin(BaseAdmin, admin.ModelAdmin):
"manga": 5,
}
- def manga_name(self, obj):
+ @staticmethod
+ def manga_name(obj):
return obj.manga.title
diff --git a/apps/manga/annotate.py b/apps/manga/annotate.py
new file mode 100644
index 00000000..8531b191
--- /dev/null
+++ b/apps/manga/annotate.py
@@ -0,0 +1,40 @@
+from typing import List
+
+from django.db.models import CharField, Q
+
+from apps.core.fast import FastQuerySet
+from apps.manga.models import Manga
+from apps.parse.catalogue import Catalogue
+
+
+def fast_annotate_manga_query(query: FastQuerySet) -> List[dict]:
+ """Use 'fast' module to annotate all required fields for manga"""
+ return (
+ query.cast(id=CharField())
+ .map(source=("source_url__startswith", Catalogue.map.source_to_name_map))
+ .m2m_agg(
+ authors=(
+ "person_relations__person__name",
+ Q(person_relations__role="author"),
+ ),
+ screenwriters=(
+ "person_relations__person__name",
+ Q(person_relations__role="screenwriter"),
+ ),
+ illustrators=(
+ "person_relations__person__name",
+ Q(person_relations__role="illustrator"),
+ ),
+ translators=(
+ "person_relations__person__name",
+ Q(person_relations__role="translator"),
+ ),
+ genres="genres__name",
+ categories="categories__name",
+ )
+ .parse_values()
+ )
+
+
+def manga_to_annotated_dict(obj: Manga) -> dict:
+ return fast_annotate_manga_query(Manga.objects.filter(pk=obj.pk))[0]
diff --git a/apps/manga/api/__init__.py b/apps/manga/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/manga/api/api.py b/apps/manga/api/api.py
new file mode 100644
index 00000000..8383d911
--- /dev/null
+++ b/apps/manga/api/api.py
@@ -0,0 +1,133 @@
+from typing import List, Optional, Tuple
+
+from django.conf import settings
+from django.core.cache import caches
+from ninja import Router
+
+from apps.core.api.schemas import ErrorSchema, MessageSchema
+from apps.core.api.utils import get_model_or_404, sora_schema
+from apps.manga.annotate import manga_to_annotated_dict
+from apps.manga.api.schemas import (
+ ChapterListOut,
+ ImageListOut,
+ MangaOut,
+ MangaSchema,
+ ParsingSchemaOut,
+)
+from apps.manga.models import Chapter, Manga
+from apps.mangachan import Mangachan
+from apps.parse.exceptions import ParsingError
+from apps.parse.tasks import run_spider_task
+from apps.parse.types import CacheType, ParserType, ParsingStatus
+from apps.typesense_bind.query import query_dict_list_by_title
+
+manga_router = Router(tags=["Manga"])
+
+
+def is_error_payload(data):
+ return hasattr(data, "error")
+
+
+def handle_parsing_with_caching(
+ parser_type: str,
+ catalogue: str,
+ link: str,
+ fallback: ParsingSchemaOut,
+ cache_type: Optional[str] = None,
+) -> Tuple[int, ParsingSchemaOut | ErrorSchema]:
+ """
+ Abstract logic of handling cache statuses and results/errors.
+
+ :param parser_type: Parser type.
+ :param catalogue: Catalogue name.
+ :param link: Parsing link and a cache key.
+ :param fallback: Fallback response value for when parsing just started or already running.
+ :param cache_type: Cache which will be used to extract data. Can be used when running,
+ for example, detail spider which also writes to chapter cache
+
+ Scenarios:
+ 1. "parsing" value inside cache means parsing is already running -> return fallback value.
+ 2. An error inside cache means previous run failed -> return error and clear cache.
+ (next request wil rerun parsing)
+ 3. Empty cache means we need to run parsing -> run spider and return fallback value.
+ 4. Nothing from above means there can only be parsed data inside cache -> return it.
+ """
+ if not cache_type:
+ cache_type = CacheType.from_parser_type(parser_type)
+ cache = caches[cache_type]
+ parsing_cache = cache.get(link)
+
+ if parsing_cache and parsing_cache != ParsingStatus.parsing.value:
+ # If there's an error in cache, then it means parsing failed
+ # Return the error and clear cache to rerun parsing on next request
+ if is_error_payload(parsing_cache):
+ cache.delete(link)
+ return 400, parsing_cache
+
+ # Return parsing payload if everything's ok
+ return parsing_cache
+
+ # Run tasks only if there's no results or parsing status inside cache
+ elif not parsing_cache:
+ # Put task into queue
+ f = run_spider_task
+ if not settings.DEBUG:
+ f = f.delay
+ try:
+ f(parser_type, catalogue_name=catalogue, url=link)
+ except ParsingError as e:
+ return 400, ErrorSchema(error=str(e))
+
+ return 200, fallback
+
+
+@manga_router.get("/search/", response=List[MangaSchema])
+def search_manga(_, title: str):
+ return query_dict_list_by_title(title)
+
+
+@manga_router.get("/{manga_id}/", response=sora_schema(MangaOut))
+def get_manga(_, manga_id: int):
+ manga = get_model_or_404(Manga, pk=manga_id)
+
+ return handle_parsing_with_caching(
+ ParserType.detail,
+ manga.source,
+ manga.source_url,
+ MangaOut(status=ParsingStatus.parsing.value, data=manga_to_annotated_dict(manga)),
+ )
+
+
+@manga_router.get("/{manga_id}/chapters/", response=sora_schema(ChapterListOut))
+def get_chapters(_, manga_id: int):
+ manga = get_model_or_404(Manga, pk=manga_id, prefetch=["chapters"])
+
+ if not manga.chapters_url:
+ return 425, MessageSchema(message="Details were not yet parsed.")
+
+ return handle_parsing_with_caching(
+ ParserType.detail if manga.source == Mangachan.name else ParserType.chapter,
+ manga.source,
+ manga.chapters_url,
+ ChapterListOut(status=ParsingStatus.parsing.value, data=list(manga.chapters.all())),
+ CacheType.chapter,
+ )
+
+
+@manga_router.get("/{manga_id}/chapters/{chapter_id}/images/", response=sora_schema(ImageListOut))
+def get_chapter_images(_, manga_id: int, chapter_id: int):
+ chapter = (
+ Chapter.objects.filter(pk=chapter_id, manga_id=manga_id).prefetch_related("manga").first()
+ )
+
+ if not chapter:
+ return 400, ErrorSchema(error="No chapter found.")
+
+ res = handle_parsing_with_caching(
+ ParserType.image,
+ chapter.manga.source,
+ chapter.link,
+ ImageListOut(status=ParsingStatus.parsing.value, data=[]),
+ )
+
+ return res
diff --git a/apps/manga/api/bookmarks/__init__.py b/apps/manga/api/bookmarks/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/manga/api/bookmarks/api.py b/apps/manga/api/bookmarks/api.py
new file mode 100644
index 00000000..73f0f267
--- /dev/null
+++ b/apps/manga/api/bookmarks/api.py
@@ -0,0 +1,46 @@
+from django.db import IntegrityError
+from django.db.transaction import atomic
+from ninja import Router
+from ninja_jwt.authentication import JWTAuth
+
+from apps.core.api.schemas import ErrorSchema
+from apps.core.api.utils import sora_schema
+from apps.manga.api.bookmarks.schemas import BookmarkEditOut, BookmarkOut
+from apps.manga.models import Bookmark
+
+bookmark_router = Router(tags=["Bookmarks"], auth=JWTAuth())
+
+
+@bookmark_router.get("/{manga_id}/", response=BookmarkOut)
+def get_bookmark(request, manga_id: int):
+ qs = (
+ Bookmark.objects.filter(user=request.user, manga_id=manga_id)
+ .values_list("chapter_id")
+ .first()
+ )
+
+ return BookmarkOut(chapter_id=qs[0] if qs else None)
+
+
+@bookmark_router.post("/{manga_id}/{chapter_id}/", response=sora_schema(BookmarkEditOut))
+def set_bookmarks(request, manga_id: int, chapter_id: int):
+ try:
+ with atomic():
+ Bookmark.objects.update_or_create(
+ user=request.user, manga_id=manga_id, defaults={"chapter_id": chapter_id}
+ )
+ return 200, BookmarkEditOut(count=1)
+ except IntegrityError as e:
+ return 400, ErrorSchema(error=str(e))
+
+
+@bookmark_router.delete("/{manga_id}/{chapter_id}/", response=sora_schema(BookmarkEditOut))
+def remove_bookmark(request, manga_id: int, chapter_id: int):
+ try:
+ with atomic():
+ Bookmark.objects.filter(
+ user=request.user, manga_id=manga_id, chapter_id=chapter_id
+ ).delete()
+ return 200, BookmarkEditOut(count=1)
+ except IntegrityError as e:
+ return 400, ErrorSchema(error=str(e))
diff --git a/apps/manga/api/bookmarks/schemas.py b/apps/manga/api/bookmarks/schemas.py
new file mode 100644
index 00000000..0b45d14f
--- /dev/null
+++ b/apps/manga/api/bookmarks/schemas.py
@@ -0,0 +1,11 @@
+from typing import Optional
+
+from ninja import Schema
+
+
+class BookmarkOut(Schema):
+ chapter_id: Optional[int]
+
+
+class BookmarkEditOut(Schema):
+ count: int
diff --git a/apps/manga/api/lists/__init__.py b/apps/manga/api/lists/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/manga/api/lists/api.py b/apps/manga/api/lists/api.py
new file mode 100644
index 00000000..1b89f58a
--- /dev/null
+++ b/apps/manga/api/lists/api.py
@@ -0,0 +1,55 @@
+from typing import List
+
+from django.db import IntegrityError
+from django.db.transaction import atomic
+from ninja import Router
+from ninja_jwt.authentication import JWTAuth
+
+from apps.core.api.schemas import ErrorSchema
+from apps.core.api.utils import sora_schema
+from apps.manga.annotate import fast_annotate_manga_query
+from apps.manga.api.lists.schemas import SaveListEditOut, SaveListOut
+from apps.manga.models import SaveList, SaveListMangaThrough
+
+list_router = Router(tags=["Lists"], auth=JWTAuth())
+
+
+@list_router.get("/", response=List[SaveListOut])
+def get_all_lists(request):
+ save_lists = SaveList.objects.filter(user=request.user).order_by("id").all()
+
+ return [
+ SaveListOut(
+ id=save_list.id,
+ name=save_list.name,
+ mangas=fast_annotate_manga_query(save_list.mangas.all()),
+ )
+ for save_list in save_lists
+ ]
+
+
+@list_router.post("/{list_id}/{manga_id}/", response=sora_schema(SaveListEditOut))
+def add_manga_to_list(request, list_id: int, manga_id: int):
+ qs = SaveList.objects.filter(user=request.user, id=list_id)
+ if not qs:
+ return ErrorSchema(error="List not found.")
+
+ try:
+ with atomic():
+ SaveListMangaThrough.objects.filter(
+ manga_id=manga_id, save_list__user=request.user
+ ).delete()
+ SaveListMangaThrough.objects.create(save_list_id=list_id, manga_id=manga_id)
+ return SaveListEditOut(count=1)
+ except IntegrityError as e:
+ if "already_exists" in str(e):
+ return ErrorSchema(error="Record already exists.")
+ return ErrorSchema(error=str(e))
+
+
+@list_router.delete("/{list_id}/{manga_id}/", response=SaveListEditOut)
+def remove_manga_from_list(request, list_id: int, manga_id: int):
+ count, _ = SaveListMangaThrough.objects.filter(
+ manga_id=manga_id, save_list_id=list_id, save_list__user=request.user
+ ).delete()
+ return SaveListEditOut(count=count)
diff --git a/apps/manga/api/lists/schemas.py b/apps/manga/api/lists/schemas.py
new file mode 100644
index 00000000..a76fc858
--- /dev/null
+++ b/apps/manga/api/lists/schemas.py
@@ -0,0 +1,21 @@
+from typing import List
+
+from ninja import ModelSchema, Schema
+
+from apps.manga.api.schemas import MangaSchema
+from apps.manga.models import SaveList
+
+
+class SaveListOut(ModelSchema):
+ mangas: List[MangaSchema] = []
+
+ class Config:
+ model = SaveList
+ model_fields = [
+ "id",
+ "name",
+ ]
+
+
+class SaveListEditOut(Schema):
+ count: int
diff --git a/apps/manga/api/notifications/__init__.py b/apps/manga/api/notifications/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/manga/api/notifications/api.py b/apps/manga/api/notifications/api.py
new file mode 100644
index 00000000..0057e16f
--- /dev/null
+++ b/apps/manga/api/notifications/api.py
@@ -0,0 +1,45 @@
+from django.db import IntegrityError
+from django.db.transaction import atomic
+from ninja import Router
+from ninja_jwt.authentication import JWTAuth
+
+from apps.core.api.schemas import ErrorSchema
+from apps.manga.api.notifications.schemas import (
+ ChapterNotificationEditOut,
+ ChapterNotificationList,
+ ChapterNotificationOut,
+)
+from apps.manga.models import ChapterNotification
+
+chapter_notification_router = Router(tags=["ChapterNotifications"], auth=JWTAuth())
+
+
+@chapter_notification_router.get("/", response=ChapterNotificationList)
+def get_chapter_notification(request):
+ qs = (
+ ChapterNotification.objects.filter(user_id=request.user)
+ .values("id", "chapter__title", "chapter__manga__thumbnail", "created")
+ .order_by("-id")
+ )
+
+ return [
+ ChapterNotificationOut(
+ id=notification["id"],
+ chapter_title=notification["chapter__title"],
+ manga_thumbnail=notification["chapter__manga__thumbnail"],
+ date_time=notification["created"],
+ )
+ for notification in qs
+ ]
+
+
+@chapter_notification_router.delete("/{notification_id}/", response=ChapterNotificationEditOut)
+def remove_chapter_notification(request, notification_id: int):
+ try:
+ with atomic():
+ ChapterNotification.objects.filter(
+ user=request.user, notification_id=notification_id
+ ).delete()
+ return ChapterNotificationEditOut(count=1)
+ except IntegrityError as e:
+ return ErrorSchema(error=str(e))
diff --git a/apps/manga/api/notifications/schemas.py b/apps/manga/api/notifications/schemas.py
new file mode 100644
index 00000000..e9ba14c9
--- /dev/null
+++ b/apps/manga/api/notifications/schemas.py
@@ -0,0 +1,18 @@
+from datetime import datetime
+from typing import List
+
+from ninja import Schema
+
+
+class ChapterNotificationOut(Schema):
+ id: int
+ chapter_title: str
+ manga_thumbnail: str
+ date_time: datetime
+
+
+ChapterNotificationList = List[ChapterNotificationOut]
+
+
+class ChapterNotificationEditOut(Schema):
+ count: int
diff --git a/apps/manga/api/notifications/utils.py b/apps/manga/api/notifications/utils.py
new file mode 100644
index 00000000..1a8afbaf
--- /dev/null
+++ b/apps/manga/api/notifications/utils.py
@@ -0,0 +1,12 @@
+from apps.manga.models import Chapter, ChapterNotification, SaveListMangaThrough
+
+
+def notify_about_chapter(chapter: Chapter):
+ qs = SaveListMangaThrough.objects.filter(manga_id=chapter.manga_id).values_list(
+ "save_list__user", flat=True
+ )
+ for user_to_notify in qs:
+ ChapterNotification.objects.create(
+ user_id=user_to_notify,
+ chapter=chapter,
+ )
diff --git a/apps/manga/api/schemas.py b/apps/manga/api/schemas.py
new file mode 100644
index 00000000..33fb341e
--- /dev/null
+++ b/apps/manga/api/schemas.py
@@ -0,0 +1,71 @@
+from typing import Any, List
+
+from ninja import ModelSchema, Schema
+
+from apps.manga.models import Chapter, Manga
+from apps.parse.types import ParsingStatus
+
+# Models
+
+
+class MangaSchema(ModelSchema):
+ source: str
+ authors: List[str]
+ screenwriters: List[str]
+ illustrators: List[str]
+ translators: List[str]
+ categories: List[str]
+ genres: List[str]
+
+ class Config:
+ model = Manga
+ model_fields = (
+ "id",
+ "source_url",
+ "chapters_url",
+ "title",
+ "rating",
+ "thumbnail",
+ "image",
+ "description",
+ "status",
+ "year",
+ "modified",
+ )
+
+
+class ChapterSchema(ModelSchema):
+ class Config:
+ model = Chapter
+ model_fields = [
+ "id",
+ "title",
+ "volume",
+ "number",
+ "link",
+ ]
+
+
+ImageSchema = str
+
+# Modifications and collections
+
+MangaList = List[MangaSchema]
+ImageList = List[str]
+
+
+class ParsingSchemaOut(Schema):
+ status: ParsingStatus
+ data: Any
+
+
+class MangaOut(ParsingSchemaOut):
+ data: MangaSchema
+
+
+class ChapterListOut(ParsingSchemaOut):
+ data: List[ChapterSchema]
+
+
+class ImageListOut(ParsingSchemaOut):
+ data: ImageList
diff --git a/apps/manga/apps.py b/apps/manga/apps.py
new file mode 100644
index 00000000..38296cd4
--- /dev/null
+++ b/apps/manga/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class MangaConfig(AppConfig):
+ name = "apps.manga"
+
+ def ready(self):
+ from . import signals # noqa
diff --git a/apps/manga/migrations/0001_initial.py b/apps/manga/migrations/0001_initial.py
new file mode 100644
index 00000000..ba580ee7
--- /dev/null
+++ b/apps/manga/migrations/0001_initial.py
@@ -0,0 +1,219 @@
+# Generated by Django 4.1.1 on 2022-09-14 20:45
+
+import django.db.models.deletion
+import django_extensions.db.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = []
+
+ operations = [
+ migrations.CreateModel(
+ name="Category",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("name", models.TextField(unique=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Genre",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("name", models.TextField(unique=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Manga",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("title", models.TextField()),
+ ("alt_title", models.TextField(blank=True, null=True)),
+ ("rating", models.DecimalField(decimal_places=2, default=0, max_digits=4)),
+ ("thumbnail", models.URLField(blank=True, default="", max_length=2000)),
+ ("image", models.URLField(blank=True, default="", max_length=2000)),
+ ("description", models.TextField(blank=True, default="")),
+ ("status", models.TextField(blank=True, null=True)),
+ ("year", models.TextField(blank=True, null=True)),
+ ("source_url", models.URLField(max_length=2000, unique=True)),
+ ("rss_url", models.URLField(blank=True, max_length=2000, null=True)),
+ (
+ "categories",
+ models.ManyToManyField(blank=True, related_name="mangas", to="manga.category"),
+ ),
+ (
+ "genres",
+ models.ManyToManyField(blank=True, related_name="mangas", to="manga.genre"),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Person",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("name", models.TextField(unique=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="PersonRelatedToManga",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "role",
+ models.TextField(
+ choices=[
+ ("author", "Author"),
+ ("illustrator", "Illustrator"),
+ ("screenwriter", "Screenwriter"),
+ ("translator", "Translator"),
+ ]
+ ),
+ ),
+ (
+ "manga",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="person_relations",
+ to="manga.manga",
+ ),
+ ),
+ (
+ "person",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="manga_relations",
+ to="manga.person",
+ ),
+ ),
+ ],
+ ),
+ migrations.AddField(
+ model_name="manga",
+ name="people_related",
+ field=models.ManyToManyField(
+ related_name="mangas", through="manga.PersonRelatedToManga", to="manga.person"
+ ),
+ ),
+ migrations.CreateModel(
+ name="Chapter",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ ("title", models.TextField()),
+ ("link", models.URLField(max_length=2000)),
+ ("number", models.FloatField()),
+ ("volume", models.FloatField()),
+ (
+ "manga",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="chapters",
+ to="manga.manga",
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="Author",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("manga.person",),
+ ),
+ ]
diff --git a/apps/login/migrations/0002_auto_20210310_0728.py b/apps/manga/migrations/0002_chapter_created_chapter_modified.py
similarity index 52%
rename from apps/login/migrations/0002_auto_20210310_0728.py
rename to apps/manga/migrations/0002_chapter_created_chapter_modified.py
index 98d01bb9..700b2d1d 100644
--- a/apps/login/migrations/0002_auto_20210310_0728.py
+++ b/apps/manga/migrations/0002_chapter_created_chapter_modified.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.1.7 on 2021-03-10 07:28
+# Generated by Django 4.1.1 on 2022-09-18 22:50
import django.utils.timezone
import django_extensions.db.fields
@@ -8,19 +8,23 @@
class Migration(migrations.Migration):
dependencies = [
- ('login', '0001_initial'),
+ ("manga", "0001_initial"),
]
operations = [
migrations.AddField(
- model_name='profile',
- name='created',
- field=django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created'),
+ model_name="chapter",
+ name="created",
+ field=django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, default=django.utils.timezone.now, verbose_name="created"
+ ),
preserve_default=False,
),
migrations.AddField(
- model_name='profile',
- name='modified',
- field=django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified'),
+ model_name="chapter",
+ name="modified",
+ field=django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
),
]
diff --git a/apps/manga/migrations/0003_alter_manga_options_alter_manga_rating.py b/apps/manga/migrations/0003_alter_manga_options_alter_manga_rating.py
new file mode 100644
index 00000000..8541ac2b
--- /dev/null
+++ b/apps/manga/migrations/0003_alter_manga_options_alter_manga_rating.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.1.1 on 2022-10-05 21:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("manga", "0002_chapter_created_chapter_modified"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="manga",
+ options={"verbose_name": "Manga", "verbose_name_plural": "Manga"},
+ ),
+ migrations.AlterField(
+ model_name="manga",
+ name="rating",
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True),
+ ),
+ ]
diff --git a/apps/manga/migrations/0004_remove_manga_alt_title_manga_identifier.py b/apps/manga/migrations/0004_remove_manga_alt_title_manga_identifier.py
new file mode 100644
index 00000000..9fbd735a
--- /dev/null
+++ b/apps/manga/migrations/0004_remove_manga_alt_title_manga_identifier.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.1 on 2022-10-06 00:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("manga", "0003_alter_manga_options_alter_manga_rating"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="manga",
+ name="alt_title",
+ ),
+ migrations.AddField(
+ model_name="manga",
+ name="identifier",
+ field=models.TextField(default=""),
+ preserve_default=False,
+ ),
+ ]
diff --git a/apps/manga/migrations/0005_savelist_alter_category_managers_and_more.py b/apps/manga/migrations/0005_savelist_alter_category_managers_and_more.py
new file mode 100644
index 00000000..5fe96d00
--- /dev/null
+++ b/apps/manga/migrations/0005_savelist_alter_category_managers_and_more.py
@@ -0,0 +1,109 @@
+# Generated by Django 4.1.1 on 2022-11-02 20:22
+
+import django.db.models.manager
+import django_extensions.db.fields
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("manga", "0004_remove_manga_alt_title_manga_identifier"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SaveList",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ (
+ "name",
+ models.TextField(
+ choices=[
+ ("Читаю", "Reading"),
+ ("Избранные", "Favorite"),
+ ("Читать позже", "Read Later"),
+ ("Брошенные", "Dropped"),
+ ]
+ ),
+ ),
+ ("session", models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="SaveListMangaThrough",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "manga",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="manga.manga"
+ ),
+ ),
+ (
+ "save_list",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="manga.savelist"
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("save_list", "manga")},
+ },
+ ),
+ migrations.AddField(
+ model_name="savelist",
+ name="mangas",
+ field=models.ManyToManyField(
+ related_name="lists", through="manga.SaveListMangaThrough", to="manga.manga"
+ ),
+ ),
+ migrations.AddField(
+ model_name="savelist",
+ name="user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="lists",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="savelist",
+ constraint=models.CheckConstraint(
+ check=models.Q(
+ ("user__isnull", False), ("session__isnull", False), _connector="OR"
+ ),
+ name="not_both_null",
+ ),
+ ),
+ migrations.AlterUniqueTogether(
+ name="savelist",
+ unique_together={("user", "session", "name")},
+ ),
+ ]
diff --git a/apps/manga/migrations/0006_rename_rss_url_manga_chapters_url_and_more.py b/apps/manga/migrations/0006_rename_rss_url_manga_chapters_url_and_more.py
new file mode 100644
index 00000000..77522295
--- /dev/null
+++ b/apps/manga/migrations/0006_rename_rss_url_manga_chapters_url_and_more.py
@@ -0,0 +1,50 @@
+# Generated by Django 4.1.1 on 2022-11-04 10:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("manga", "0005_savelist_alter_category_managers_and_more"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="manga",
+ old_name="rss_url",
+ new_name="chapters_url",
+ ),
+ migrations.AlterField(
+ model_name="manga",
+ name="identifier",
+ field=models.CharField(blank=True, max_length=1024, null=True),
+ ),
+ migrations.AlterField(
+ model_name="manga",
+ name="status",
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AlterField(
+ model_name="manga",
+ name="title",
+ field=models.CharField(blank=True, max_length=512, null=True),
+ ),
+ migrations.AlterField(
+ model_name="manga",
+ name="year",
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AlterField(
+ model_name="savelist",
+ name="name",
+ field=models.TextField(
+ choices=[
+ ("Читаю", "Reading"),
+ ("Избранные", "Favorite"),
+ ("Читать позже", "Read Later"),
+ ("Брошенные", "Dropped"),
+ ]
+ ),
+ ),
+ ]
diff --git a/apps/manga/migrations/0007_alter_unique_on_savelist.py b/apps/manga/migrations/0007_alter_unique_on_savelist.py
new file mode 100644
index 00000000..478e6e58
--- /dev/null
+++ b/apps/manga/migrations/0007_alter_unique_on_savelist.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.3 on 2022-11-08 11:31
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("manga", "0006_rename_rss_url_manga_chapters_url_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name="savelist",
+ unique_together=set(),
+ ),
+ # It's a PostgreSQL 15 feature
+ # https://www.postgresql.org/docs/15/release-15.html#id-1.11.6.5.5.3.4
+ migrations.RunSQL(
+ 'CREATE UNIQUE INDEX "save_list_user_name_unique" ON '
+ '"manga_savelist" ("user_id", "session", "name") NULLS NOT DISTINCT;',
+ reverse_sql='DROP INDEX "save_list_user_name_unique";',
+ ),
+ ]
diff --git a/apps/manga/migrations/0008_alter_category_options_alter_savelist_user_bookmark_and_more.py b/apps/manga/migrations/0008_alter_category_options_alter_savelist_user_bookmark_and_more.py
new file mode 100644
index 00000000..8d92f47b
--- /dev/null
+++ b/apps/manga/migrations/0008_alter_category_options_alter_savelist_user_bookmark_and_more.py
@@ -0,0 +1,96 @@
+# Generated by Django 4.1.3 on 2022-11-19 15:53
+
+import django.db.models.deletion
+import django_extensions.db.fields
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("manga", "0007_alter_unique_on_savelist"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="category",
+ options={"verbose_name": "Category", "verbose_name_plural": "Categories"},
+ ),
+ migrations.AlterField(
+ model_name="savelist",
+ name="user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.CreateModel(
+ name="Bookmark",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("session", models.TextField(blank=True, null=True)),
+ (
+ "chapter",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="bookmarks",
+ to="manga.chapter",
+ ),
+ ),
+ (
+ "manga",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="bookmarks",
+ to="manga.manga",
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.AddConstraint(
+ model_name="bookmark",
+ constraint=models.CheckConstraint(
+ check=models.Q(
+ ("user__isnull", False), ("session__isnull", False), _connector="OR"
+ ),
+ name="bookmark_not_both_null",
+ ),
+ ),
+ # It's a PostgreSQL 15 feature
+ # https://www.postgresql.org/docs/15/release-15.html#id-1.11.6.5.5.3.4
+ migrations.RunSQL(
+ 'CREATE UNIQUE INDEX "bookmark_user_name_unique" ON '
+ '"manga_bookmark" ("user_id", "session", "manga_id", "chapter_id") NULLS NOT DISTINCT;',
+ reverse_sql='DROP INDEX "bookmark_user_name_unique";',
+ ),
+ ]
diff --git a/apps/manga/migrations/0009_remove_bookmark_bookmark_not_both_null_and_more.py b/apps/manga/migrations/0009_remove_bookmark_bookmark_not_both_null_and_more.py
new file mode 100644
index 00000000..5548fcb9
--- /dev/null
+++ b/apps/manga/migrations/0009_remove_bookmark_bookmark_not_both_null_and_more.py
@@ -0,0 +1,59 @@
+# Generated by Django 4.1.3 on 2022-11-21 01:54
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("manga", "0008_alter_category_options_alter_savelist_user_bookmark_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name="bookmark",
+ name="bookmark_not_both_null",
+ ),
+ migrations.RemoveConstraint(
+ model_name="savelist",
+ name="not_both_null",
+ ),
+ migrations.RemoveField(
+ model_name="bookmark",
+ name="session",
+ ),
+ migrations.RemoveField(
+ model_name="savelist",
+ name="session",
+ ),
+ migrations.AlterField(
+ model_name="bookmark",
+ name="user",
+ field=models.ForeignKey(
+ default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+ ),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name="chapter",
+ name="manga",
+ field=models.ForeignKey(
+ default=1,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="chapters",
+ to="manga.manga",
+ ),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name="savelist",
+ name="user",
+ field=models.ForeignKey(
+ default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/apps/manga/migrations/0010_alter_savelist_unique_together.py b/apps/manga/migrations/0010_alter_savelist_unique_together.py
new file mode 100644
index 00000000..4f05be17
--- /dev/null
+++ b/apps/manga/migrations/0010_alter_savelist_unique_together.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.3 on 2022-11-21 11:30
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("manga", "0009_remove_bookmark_bookmark_not_both_null_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name="savelist",
+ unique_together={("user", "name")},
+ ),
+ ]
diff --git a/apps/manga/migrations/0011_manga_popularity.py b/apps/manga/migrations/0011_manga_popularity.py
new file mode 100644
index 00000000..372e236d
--- /dev/null
+++ b/apps/manga/migrations/0011_manga_popularity.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.3 on 2022-12-10 11:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("manga", "0010_alter_savelist_unique_together"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="manga",
+ name="popularity",
+ field=models.IntegerField(default=0, verbose_name="Позиция в рейтинге"),
+ preserve_default=False,
+ ),
+ ]
diff --git a/apps/manga/migrations/0012_chapternotification.py b/apps/manga/migrations/0012_chapternotification.py
new file mode 100644
index 00000000..15538b35
--- /dev/null
+++ b/apps/manga/migrations/0012_chapternotification.py
@@ -0,0 +1,55 @@
+# Generated by Django 4.1.3 on 2022-12-18 14:22
+
+import django.db.models.deletion
+import django_extensions.db.fields
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("manga", "0011_manga_popularity"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ChapterNotification",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+ ),
+ ),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ (
+ "chapter",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="manga.chapter"
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("user", "chapter")},
+ },
+ ),
+ ]
diff --git a/apps/manga/migrations/__init__.py b/apps/manga/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/manga/models.py b/apps/manga/models.py
new file mode 100644
index 00000000..7d1b74d7
--- /dev/null
+++ b/apps/manga/models.py
@@ -0,0 +1,170 @@
+from django.db import models
+from django.db.models.fields import (
+ CharField,
+ DecimalField,
+ FloatField,
+ IntegerField,
+ TextField,
+ URLField,
+)
+from django.db.models.fields.related import ForeignKey, ManyToManyField
+from django.db.models.query import QuerySet
+
+from apps.core.abc.models import BaseModel
+from apps.core.fast import FastQuerySet
+from apps.core.utils import url_prefix
+from apps.parse.catalogue import Catalogue
+
+
+class Category(BaseModel):
+ class Meta:
+ verbose_name = "Category"
+ verbose_name_plural = "Categories"
+
+ name = TextField(unique=True)
+
+
+class Genre(BaseModel):
+ name = TextField(unique=True)
+
+
+class Person(BaseModel):
+ name = TextField(unique=True)
+
+
+class Author(Person):
+ class AuthorManager(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().filter(manga_relations__role="author")
+
+ objects = AuthorManager()
+
+ class Meta:
+ proxy = True
+
+
+class PersonRole(models.TextChoices):
+ author = "author"
+ illustrator = "illustrator"
+ screenwriter = "screenwriter"
+ translator = "translator"
+
+
+class PersonRelatedToManga(models.Model):
+ person = ForeignKey("Person", models.CASCADE, related_name="manga_relations")
+ manga = ForeignKey("Manga", models.CASCADE, related_name="person_relations")
+ role = TextField(choices=PersonRole.choices)
+
+ @staticmethod
+ def save_persons(manga: "Manga", role: str, persons: list):
+ people_related: PersonRelatedToManga = manga.people_related.through
+ people_related.objects.filter(role=role, manga=manga).delete()
+ people_related.objects.bulk_create(
+ [
+ people_related( # noqa
+ person=Person.objects.get_or_create(name=person)[0],
+ manga=manga,
+ role=role,
+ )
+ for person in persons
+ ],
+ ignore_conflicts=True,
+ )
+
+
+class Chapter(BaseModel):
+ NAME_FIELD = "title"
+
+ manga = ForeignKey("Manga", related_name="chapters", on_delete=models.CASCADE)
+
+ title = TextField()
+ link = URLField(max_length=2000)
+ number = FloatField()
+ volume = FloatField()
+
+
+class Bookmark(BaseModel):
+ user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
+ manga = models.ForeignKey("Manga", related_name="bookmarks", on_delete=models.CASCADE)
+ chapter = models.ForeignKey("Chapter", related_name="bookmarks", on_delete=models.CASCADE)
+
+
+class Manga(BaseModel):
+ class Meta:
+ verbose_name = "Manga"
+ verbose_name_plural = "Manga"
+
+ objects = models.Manager.from_queryset(FastQuerySet)()
+ NAME_FIELD = "title"
+
+ title = CharField(max_length=512, null=True, blank=True)
+
+ # Identifier for source site. Can be a direct ID or a hash
+ identifier = CharField(max_length=1024, null=True, blank=True)
+ popularity = IntegerField("Позиция в рейтинге")
+
+ rating = DecimalField(null=True, blank=True, max_digits=4, decimal_places=2)
+ thumbnail = URLField(max_length=2000, default="", blank=True)
+ image = URLField(max_length=2000, default="", blank=True)
+ description = TextField(default="", blank=True)
+ status = CharField(max_length=255, null=True, blank=True)
+ year = CharField(max_length=255, null=True, blank=True)
+
+ # https://stackoverflow.com/questions/417142
+ source_url = URLField(max_length=2000, unique=True)
+ # There can be manga with no chapters, i.e. future releases
+ chapters_url = URLField(max_length=2000, null=True, blank=True)
+
+ genres = ManyToManyField("Genre", related_name="mangas", blank=True)
+ categories = ManyToManyField("Category", related_name="mangas", blank=True)
+ people_related = ManyToManyField(
+ "Person", through="PersonRelatedToManga", related_name="mangas"
+ )
+
+ @property
+ def source(self):
+ return Catalogue.from_source(url_prefix(self.source_url))
+
+ def save(self, **kwargs):
+ if not self.image:
+ self.image = self.thumbnail
+ if self.source == "mangachan":
+ self.chapters_url = self.source_url
+ return super().save(**kwargs)
+
+ @property
+ def authors(self) -> QuerySet[Author]:
+ return Author.objects.filter(manga_relations__manga=self)
+
+
+class SaveListMangaThrough(models.Model):
+ class Meta:
+ unique_together = ("save_list", "manga")
+
+ save_list = ForeignKey("SaveList", on_delete=models.CASCADE)
+ manga = ForeignKey("Manga", on_delete=models.CASCADE)
+
+
+class SaveListNameChoices(models.TextChoices):
+ reading = "Читаю"
+ favorite = "Избранные"
+ read_later = "Читать позже"
+ dropped = "Брошенные"
+
+
+class SaveList(BaseModel):
+ class Meta:
+ unique_together = ("user", "name")
+
+ user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
+
+ name = models.TextField(choices=SaveListNameChoices.choices, null=False, blank=False)
+ mangas = models.ManyToManyField("Manga", related_name="lists", through=SaveListMangaThrough)
+
+
+class ChapterNotification(BaseModel):
+ class Meta:
+ unique_together = ("user", "chapter")
+
+ user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
+ chapter = models.ForeignKey("Chapter", on_delete=models.CASCADE)
diff --git a/apps/manga/signals.py b/apps/manga/signals.py
new file mode 100644
index 00000000..799f6c05
--- /dev/null
+++ b/apps/manga/signals.py
@@ -0,0 +1,18 @@
+from django.contrib.auth.models import User
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from apps.manga.models import SaveList, SaveListNameChoices
+
+LIST_NAMES = [n for n, _ in SaveListNameChoices.choices]
+
+
+def create_save_lists(**kwargs):
+ SaveList.objects.bulk_create(
+ [SaveList(name=x, **kwargs) for x in LIST_NAMES], ignore_conflicts=True
+ )
+
+
+@receiver(post_save, sender=User)
+def list_create_handler(instance, **kwargs): # noqa
+ create_save_lists(user=instance)
diff --git a/apps/mangachan/__init__.py b/apps/mangachan/__init__.py
new file mode 100644
index 00000000..22f616e6
--- /dev/null
+++ b/apps/mangachan/__init__.py
@@ -0,0 +1,7 @@
+from apps.parse.catalogue import Catalogue
+
+
+class Mangachan(Catalogue):
+ name = "mangachan"
+ source = "https://manga-chan.me"
+ settings = "apps.mangachan.settings"
diff --git a/apps/mangachan/apps.py b/apps/mangachan/apps.py
new file mode 100644
index 00000000..3a0e6e56
--- /dev/null
+++ b/apps/mangachan/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+
+class Config(AppConfig):
+ name = "apps.mangachan"
+
+ def ready(self):
+ from . import Mangachan # noqa
+ from .detail import MangachanDetailSpider # noqa
+ from .image import MangachanImageSpider # noqa
+ from .list import MangachanListSpider # noqa
diff --git a/apps/mangachan/detail.py b/apps/mangachan/detail.py
new file mode 100644
index 00000000..834b18a2
--- /dev/null
+++ b/apps/mangachan/detail.py
@@ -0,0 +1,82 @@
+import re
+
+from scrapy.http import HtmlResponse
+
+from apps.core.utils import url_prefix
+from apps.mangachan import Mangachan
+from apps.parse.scrapy.items import ChapterItem, MangaItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+
+
+def clear_description(desc):
+ escapes = "".join([chr(char) for char in range(1, 32)])
+ translator = str.maketrans("", "", escapes)
+ return desc.translate(translator).replace(" ", "").strip(" ")
+
+
+@Mangachan.register(ParserType.detail)
+class MangachanDetailSpider(BaseSpider):
+ custom_settings = {
+ "ITEM_PIPELINES": {
+ "apps.mangachan.pipelines.MangachanPipeline": 300,
+ "apps.mangachan.pipelines.MangachanChapterPipeline": 310,
+ }
+ }
+
+ def parse(self, response: HtmlResponse, **kwargs):
+ identifier = response.url[len(url_prefix(response.url)) :]
+
+ description = response.xpath('//div[@id="description"]/text()').extract_first()
+
+ trs = response.xpath("//table[@class='mangatitle']//tr").extract()
+
+ other_fields = {}
+ for row in trs:
+ row = HtmlResponse(url="", body=row, encoding="utf-8")
+
+ name_var_map = {
+ "Тип": "categories",
+ "Автор": "authors",
+ "Тэги": "genres",
+ "Переводчики": "translators",
+ }
+ name = row.xpath("//td/text()").extract_first()
+ data = row.xpath("//td//a/text()").extract()
+
+ field = name_var_map.get(name, None)
+ if field:
+ other_fields[field] = data
+
+ description = clear_description(description)
+
+ chapters = []
+ for row in response.xpath('//div[@class="manga2"]/a'):
+ name = row.xpath("text()").extract_first()
+ link = row.xpath("@href").extract_first()
+
+ if not link.startswith("http"):
+ link = url_prefix(response.url) + link
+
+ volume, number, title = re.match(
+ r"^Том\s*(\d+)\s*Глава\s*([\d.]+)\s*(.*)\w*$", name
+ ).groups()
+
+ chapters.append(
+ dict(
+ volume=volume,
+ number=number,
+ title=title,
+ link=link,
+ )
+ )
+
+ return [
+ MangaItem(
+ identifier=identifier,
+ description=description,
+ source_url=response.url,
+ **other_fields,
+ chapters=ChapterItem(chapters=chapters, chapters_url=response.url),
+ )
+ ]
diff --git a/apps/mangachan/image.py b/apps/mangachan/image.py
new file mode 100644
index 00000000..137d18c2
--- /dev/null
+++ b/apps/mangachan/image.py
@@ -0,0 +1,20 @@
+import re
+
+from orjson import loads
+
+from apps.mangachan import Mangachan
+from apps.parse.scrapy.items import ImagesItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+
+
+@Mangachan.register(ParserType.image)
+class MangachanImageSpider(BaseSpider):
+ custom_settings = {"ITEM_PIPELINES": {"apps.mangachan.pipelines.MangachanImagePipeline": 300}}
+
+ def parse(self, response, **kwargs):
+ image_links = re.search(r'"fullimg":(\[.*\",?])', response.text).group(1)
+ without_trailing_comma = re.sub(r",?]$", "]", image_links)
+ image_links = loads(without_trailing_comma)
+ self.logger.info(f"Parsed {len(image_links)} image links")
+ return ImagesItem(chapter_url=self.start_urls[0], images=image_links)
diff --git a/apps/mangachan/items.py b/apps/mangachan/items.py
new file mode 100644
index 00000000..36be7e37
--- /dev/null
+++ b/apps/mangachan/items.py
@@ -0,0 +1,8 @@
+from scrapy import Field
+
+from apps.parse.scrapy.items import MangaItem
+
+
+class MangaDetailAndChaptersItem(MangaItem):
+ # MangaChapterItem
+ chapters = Field()
diff --git a/apps/mangachan/list.py b/apps/mangachan/list.py
new file mode 100644
index 00000000..339e3cfd
--- /dev/null
+++ b/apps/mangachan/list.py
@@ -0,0 +1,75 @@
+from typing import List
+
+from scrapy.http import HtmlResponse
+from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders.crawl import CrawlSpider, Rule
+
+from apps.core.utils import url_prefix
+from apps.mangachan import Mangachan
+from apps.parse.scrapy.items import MangaItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+
+_manga_tile = '//div[@class = "content_row"]'
+
+_title_selector = "//a[@class = 'title_link']"
+_thumbnail = "//div[@class = 'manga_images']//img/@src"
+_genres = "//div[@class = 'genre']/a/text()"
+
+
+@Mangachan.register(ParserType.list, url=False)
+class MangachanListSpider(BaseSpider, CrawlSpider):
+ start_urls = [f"{Mangachan.source}/catalog"]
+ rules = [
+ Rule(
+ LinkExtractor(
+ restrict_xpaths=["//div[@id='pagination']/a[contains(text(), 'Вперед')]"],
+ ),
+ follow=True,
+ callback="parse",
+ )
+ ]
+ custom_settings = {
+ "DEPTH_LIMIT": 1500,
+ }
+
+ def parse_start_url(self, response, **kwargs):
+ return self.parse(response)
+
+ def parse(self, response, **kwargs):
+ mangas: List[MangaItem] = []
+ descriptions = response.xpath(_manga_tile).extract()
+ for description in descriptions:
+ response = HtmlResponse(url="", body=description, encoding="utf-8")
+
+ title_selector = response.xpath(_title_selector)
+ title = title_selector.xpath("text()").extract_first()
+ identifier = title_selector.xpath("@href").extract_first()
+
+ thumbnail = response.xpath(_thumbnail).extract_first()
+ image = thumbnail
+
+ source_url = identifier
+ rating = None
+
+ genres = response.xpath(_genres).extract()
+
+ if not source_url.startswith("http"):
+ source_url = url_prefix(self.start_urls[0]) + source_url
+
+ mangas.append(
+ MangaItem(
+ identifier=identifier,
+ title=title,
+ thumbnail=thumbnail,
+ image=image,
+ source_url=source_url,
+ chapters_url=source_url,
+ rating=rating,
+ genres=genres,
+ )
+ )
+ self.logger.info('Parsed manga "{}"'.format(title))
+
+ self.logger.info("===================")
+ return mangas
diff --git a/apps/mangachan/pipelines.py b/apps/mangachan/pipelines.py
new file mode 100644
index 00000000..9b0fc9f2
--- /dev/null
+++ b/apps/mangachan/pipelines.py
@@ -0,0 +1,23 @@
+from copy import deepcopy
+
+from apps.readmanga.pipelines import (
+ ReadmangaChapterPipeline,
+ ReadmangaImagePipeline,
+ ReadmangaPipeline,
+)
+
+
+class MangachanImagePipeline(ReadmangaImagePipeline):
+ timeout = None
+
+
+class MangachanChapterPipeline(ReadmangaChapterPipeline):
+ pass
+
+
+class MangachanPipeline(ReadmangaPipeline):
+ def process_item(self, data, spider):
+ item = deepcopy(data)
+ chapters = item.pop("chapters")
+ super().process_item(item, spider)
+ return chapters
diff --git a/apps/mangachan/settings.py b/apps/mangachan/settings.py
new file mode 100644
index 00000000..287d00f8
--- /dev/null
+++ b/apps/mangachan/settings.py
@@ -0,0 +1,28 @@
+import os
+
+import django
+
+os.environ["DJANGO_SETTINGS_MODULE"] = "manga_reader.settings" # noqa
+django.setup() # noqa
+
+from apps.mangachan.pipelines import MangachanPipeline # noqa
+from apps.parse.scrapy.base_settings import * # noqa
+
+BOT_NAME = "mangachan"
+SPIDER_MODULES = [
+ "apps.mangachan.list",
+ "apps.mangachan.detail",
+ "apps.mangachan.chapter",
+ "apps.mangachan.images",
+]
+# Remove for now as GAE workspace is immutable
+LOG_FILE = "parse-mangachan.log"
+
+DOWNLOADER_MIDDLEWARES = {
+ "apps.parse.scrapy.middleware.ErrorLoggerMiddleware": 340,
+ "apps.parse.scrapy.middleware.ProxyMiddleware": 350,
+ "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,
+}
+ITEM_PIPELINES = {
+ MangachanPipeline: 300,
+}
diff --git a/apps/parse/api/serializers.py b/apps/parse/api/serializers.py
deleted file mode 100644
index 073a7a18..00000000
--- a/apps/parse/api/serializers.py
+++ /dev/null
@@ -1,28 +0,0 @@
-MANGA_FIELDS = (
- "id",
- "source",
- "source_url",
- "title",
- "alt_title",
- "rating",
- "thumbnail",
- "image",
- "description",
- "authors",
- "screenwriters",
- "illustrators",
- "translators",
- "genres",
- "categories",
- "status",
- "year",
- "updated_detail",
-)
-
-CHAPTER_FIELDS = (
- "id",
- "title",
- "link",
- "number",
- "volume",
-)
diff --git a/apps/parse/api/urls.py b/apps/parse/api/urls.py
deleted file mode 100644
index 738b3ec6..00000000
--- a/apps/parse/api/urls.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.urls import include, path
-from rest_framework import routers
-
-from apps.parse.api.views import MangaViewSet
-
-router = routers.DefaultRouter()
-
-router.register(r"", MangaViewSet, basename="manga")
-
-urlpatterns = [
- path(r"", include(router.urls)),
-]
diff --git a/apps/parse/api/views.py b/apps/parse/api/views.py
deleted file mode 100644
index 43f9afd9..00000000
--- a/apps/parse/api/views.py
+++ /dev/null
@@ -1,104 +0,0 @@
-from datetime import datetime
-
-from django.conf import settings
-from django.http.response import Http404
-from rest_framework import mixins, status, viewsets
-from rest_framework.decorators import action
-from rest_framework.generics import get_object_or_404
-from rest_framework.response import Response
-
-from apps.core.fast import FastLimitOffsetPagination
-from apps.core.fast.utils import get_fast_response
-from apps.core.utils import format_error_response, init_redis_client
-from apps.parse.api.serializers import CHAPTER_FIELDS, MANGA_FIELDS
-from apps.parse.const import CHAPTER_PARSER, DETAIL_PARSER, IMAGE_PARSER
-from apps.parse.documents import MangaDocument
-from apps.parse.models import Chapter, Manga
-from apps.parse.scrapy.utils import run_parser
-from apps.parse.utils import fast_annotate_manga_query, needs_update
-
-
-class MangaViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
- pagination_class = FastLimitOffsetPagination
- queryset = Manga.objects.all()
- redis_client = init_redis_client()
-
- @classmethod
- def get_fast_manga(cls, pk) -> dict:
- manga = fast_annotate_manga_query(Manga.objects.filter(pk=pk))
- if not manga.exists():
- raise Http404
- return manga.parse_values(*MANGA_FIELDS)[0]
-
- def retrieve(self, _, pk, *args, **kwargs):
- manga = self.get_fast_manga(pk)
- try:
- criterea = manga["updated_detail"]
- if not criterea or needs_update(criterea):
- run_parser(DETAIL_PARSER, manga["source"], manga["source_url"])
- run_parser(CHAPTER_PARSER, manga["source"], manga["source_url"])
- now = datetime.now()
- Manga.objects.filter(pk=pk).update(updated_detail=now)
- manga["updated_detail"] = now
- except Exception as e:
- return format_error_response("Errors occurred during parsing " + str(e))
- return get_fast_response(manga)
-
- def list(self, request):
- title: str = request.GET.get("title", None)
- if not title:
- return format_error_response("No title found")
-
- mangas = fast_annotate_manga_query(
- MangaDocument.search()
- .query("fuzzy", title=title)[: settings.REST_FRAMEWORK["PAGE_SIZE"]]
- .to_queryset()
- )
-
- page = self.paginator.paginate_queryset(
- mangas,
- request,
- values=MANGA_FIELDS,
- )
- if page is not None:
- return self.paginator.get_paginated_response(page)
-
- return Response(list(mangas.parse_values(*MANGA_FIELDS)), status=status.HTTP_200_OK)
-
- @action(
- detail=False,
- methods=("get",),
- url_path="(?P[^/.]+)/chapters",
- )
- def chapters_list(self, _, pk):
- manga: Manga = Manga.objects.prefetch_related("chapters").get(pk=pk)
-
- try:
- criterea = manga.updated_detail
- if not criterea or needs_update(criterea.isoformat()):
- run_parser(DETAIL_PARSER, manga.source, manga.source_url)
- run_parser(CHAPTER_PARSER, manga.source, manga.source_url)
- manga.updated_detail = datetime.now()
- manga.save()
- except Exception as e:
- return format_error_response("Errors occurred during parsing " + str(e))
- return get_fast_response(
- list(manga.chapters.order_by("-volume", "-number").values(*CHAPTER_FIELDS))
- )
-
- @action(
- detail=False,
- methods=("get",),
- url_path="(?P[^/.]+)/images",
- )
- def images_list(self, request, chapter_id):
- chapter: Chapter = get_object_or_404(Chapter, id=chapter_id)
- parse = request.GET.get("parse", None)
- images = self.redis_client.lrange(chapter.link, 0, -1)
- if not images or parse is not None:
- run_parser(IMAGE_PARSER, chapter.manga.source, chapter.link)
- return Response(
- {"message": "started parsing images, refetch later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
- return Response(images, status=status.HTTP_200_OK)
diff --git a/apps/parse/apps.py b/apps/parse/apps.py
index 7a002f0b..7e9ffdca 100644
--- a/apps/parse/apps.py
+++ b/apps/parse/apps.py
@@ -2,5 +2,4 @@
class ParseConfig(AppConfig):
- default_auto_field = "django.db.models.BigAutoField"
name = "apps.parse"
diff --git a/apps/parse/cache.py b/apps/parse/cache.py
new file mode 100644
index 00000000..e251ae3a
--- /dev/null
+++ b/apps/parse/cache.py
@@ -0,0 +1,14 @@
+from django.core.cache import caches
+
+
+class CacheConnector:
+ cache_key: str
+
+ @property
+ def cache(self):
+ return caches[self.cache_key]
+
+
+class WithCache:
+ def __call__(self, cache_key: str):
+ return type("CacheConnector", (CacheConnector,), {"cache_key": cache_key})
diff --git a/apps/parse/catalogue.py b/apps/parse/catalogue.py
new file mode 100644
index 00000000..db7a9edb
--- /dev/null
+++ b/apps/parse/catalogue.py
@@ -0,0 +1,119 @@
+from typing import Dict, List, Type
+
+from django.utils.functional import classproperty
+
+from apps.parse.exceptions import CatalogueNotFound, ParserNotFound
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+
+
+class CatalogueMap(dict, Dict[str, Type["Catalogue"]]):
+ @property
+ def names(self) -> List[str]:
+ return [c.name for c in self.values()]
+
+ @property
+ def sources(self) -> List[str]:
+ return [c.source for c in self.values()]
+
+ @property
+ def name_to_source_map(self) -> Dict[str, str]:
+ return {c.name: c.source for c in self.values()}
+
+ @property
+ def source_to_name_map(self) -> Dict[str, str]:
+ return {c.source: c.name for c in self.values()}
+
+
+class _AutoRegisterCatalogue(type):
+ catalogue_map = CatalogueMap()
+
+ def __init__(cls: Type["Catalogue"], cls_name, cls_bases, cls_dict):
+ super().__init__(cls_name, cls_bases, cls_dict)
+ # Applies to every catalogue, but not base Catalogue class
+ if hasattr(cls, "name"):
+ # Create parser map for catalogue
+ cls._parser_map = {}
+ _AutoRegisterCatalogue.catalogue_map[cls.name] = cls
+
+ def get_map(cls):
+ return cls.catalogue_map
+
+ def __str__(cls: Type["Catalogue"]):
+ return cls.name
+
+ def __hash__(cls: Type["Catalogue"]):
+ return hash(cls.name)
+
+ def __eq__(cls: Type["Catalogue"], other):
+ if type(other) == str:
+ return cls.name == other
+ return super().__eq__(other)
+
+
+class Catalogue(metaclass=_AutoRegisterCatalogue):
+ name: str
+ source: str
+ settings: str
+ _parser_map: Dict[str, Type["BaseSpider"]]
+
+ @staticmethod
+ def _create_init(cls):
+ original_init = cls.__init__
+
+ def _init(self, *args, **kwargs):
+ url = kwargs.pop("url", None)
+ if not getattr(self.__class__, "start_urls", None) and url:
+ original_init(self, *args, start_urls=[url]) # noqa
+ else:
+ original_init(self, *args, **kwargs)
+
+ return _init
+
+ @classmethod
+ def register(cls, type_: str | ParserType, url: bool = True):
+ def reg(parser: Type["BaseSpider"]):
+ cls._parser_map[type_] = parser
+ attrs = {
+ "name": f"{cls.name}_{type_}",
+ "type": type_,
+ }
+ if url:
+ parser.__init__ = cls._create_init(parser)
+ attrs["__init__"] = cls._create_init(parser)
+
+ parser.name = attrs["name"]
+ parser.type = attrs["type"]
+
+ return reg
+
+ @staticmethod
+ def from_name(name: str):
+ res = _AutoRegisterCatalogue.catalogue_map.get(name, None)
+ if not res:
+ raise CatalogueNotFound
+ return res
+
+ @staticmethod
+ def from_source(url: str):
+ filtered_list = [
+ c for c in _AutoRegisterCatalogue.catalogue_map.values() if c.source == url
+ ]
+ if not filtered_list:
+ raise CatalogueNotFound
+ return filtered_list[0]
+
+ @classproperty
+ def map(self):
+ return _AutoRegisterCatalogue.catalogue_map
+
+ @classmethod
+ def from_parser_name(cls, type_: str | ParserType):
+ res = cls._parser_map.get(type_, None)
+ if not res:
+ raise ParserNotFound
+ return res
+
+ @classproperty
+ def parser_map(self):
+ return self._parser_map
diff --git a/apps/parse/cleaning.py b/apps/parse/cleaning.py
new file mode 100644
index 00000000..f7fe5b2c
--- /dev/null
+++ b/apps/parse/cleaning.py
@@ -0,0 +1,34 @@
+"""Function for data cleaning."""
+
+from collections import Counter
+from typing import List
+
+
+def normalized_category_names(names: List[str]) -> List[str]:
+ return [name.lower().replace("_", " ") for name in names]
+
+
+def without_common_prefix(words):
+ """Returns the list of strings with the common prefix."""
+ cnt = Counter()
+ for word in words:
+ if not word:
+ return words
+ cnt[word[0]] += 1
+
+ if not cnt:
+ return words
+ first_letter = list(cnt)[0]
+
+ filter_list = [word for word in words if word[0] == first_letter]
+ filter_list.sort(key=lambda s: len(s)) # To avoid iob
+
+ prefix = ""
+ length = len(filter_list[0])
+ for i in range(length):
+ test = filter_list[0][i]
+ if all([word[i] == test for word in filter_list]):
+ prefix += test
+ else:
+ break
+ return [word[len(prefix) :] if word.startswith(prefix) else word for word in words]
diff --git a/apps/parse/const.py b/apps/parse/const.py
deleted file mode 100644
index e8920ac0..00000000
--- a/apps/parse/const.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from datetime import timedelta
-
-from apps.parse.readmanga.chapter import ReadmangaChapterSpider
-from apps.parse.readmanga.detail import ReadmangaDetailSpider
-from apps.parse.readmanga.images import ReadmangaImageSpider
-from apps.parse.readmanga.list import ReadmangaListSpider
-
-BASE_UPDATE_FREQUENCY = timedelta(hours=1)
-IMAGE_UPDATE_FREQUENCY = timedelta(hours=8)
-
-LIST_PARSER = "list"
-DETAIL_PARSER = "detail"
-CHAPTER_PARSER = "chapters"
-IMAGE_PARSER = "images"
-PARSER_TYPES = [LIST_PARSER, DETAIL_PARSER, CHAPTER_PARSER, IMAGE_PARSER]
-
-CATALOGUES = {
- "readmanga": {
- "source": "https://readmanga.io",
- "settings": "apps.parse.readmanga.settings",
- "parsers": {
- LIST_PARSER: (ReadmangaListSpider, None),
- DETAIL_PARSER: (ReadmangaDetailSpider, 10),
- CHAPTER_PARSER: (ReadmangaChapterSpider, 10),
- IMAGE_PARSER: (ReadmangaImageSpider, 10),
- },
- }
-}
-CATALOGUE_NAMES = [k.lower() for k in CATALOGUES.keys()]
-
-SOURCE_TO_CATALOGUE_MAP = {
- "https://readmanga.io": "readmanga",
-}
-CATALOGUE_TO_SOURCE_MAP = {v: k for k, v in SOURCE_TO_CATALOGUE_MAP.items()}
diff --git a/apps/parse/documents.py b/apps/parse/documents.py
deleted file mode 100644
index 26908239..00000000
--- a/apps/parse/documents.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from django_elasticsearch_dsl import Document
-from django_elasticsearch_dsl.registries import registry
-
-from .models import Manga
-
-
-@registry.register_document
-class MangaDocument(Document):
- class Index:
- name = "mangas"
- settings = {"number_of_shards": 1, "number_of_replicas": 0}
-
- class Django:
- model = Manga
-
- fields = [
- "title",
- "alt_title",
- ]
diff --git a/apps/parse/exceptions.py b/apps/parse/exceptions.py
new file mode 100644
index 00000000..69780c42
--- /dev/null
+++ b/apps/parse/exceptions.py
@@ -0,0 +1,21 @@
+from apps.core.api.schemas import ErrorSchema
+
+
+class NotFound(Exception):
+ pass
+
+
+class CatalogueNotFound(NotFound):
+ pass
+
+
+class ParserNotFound(NotFound):
+ pass
+
+
+class ParsingError(Exception):
+ """Any error that may occur during parsing"""
+
+
+def to_error_schema(error: str | ParsingError) -> ErrorSchema:
+ return ErrorSchema(error=str(error))
diff --git a/apps/parse/management/commands/crawl.py b/apps/parse/management/commands/crawl.py
deleted file mode 100644
index 0d590dd7..00000000
--- a/apps/parse/management/commands/crawl.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import logging
-import sys
-
-from django.core.management.base import BaseCommand, CommandParser
-
-from apps.parse.const import (
- CATALOGUE_NAMES,
- CHAPTER_PARSER,
- DETAIL_PARSER,
- IMAGE_PARSER,
- PARSER_TYPES,
-)
-from apps.parse.scrapy.utils import run_parser
-from apps.parse.utils import mute_logger_stdout
-
-logger = logging.getLogger("management")
-
-
-class Command(BaseCommand):
- def add_arguments(self, parser: CommandParser) -> None:
- parser.add_argument(
- "type",
- type=str,
- choices=PARSER_TYPES,
- help="which type of data to parse",
- )
- parser.add_argument(
- "catalogue",
- type=str,
- default="readmanga",
- choices=CATALOGUE_NAMES,
- help="parser to use which respresents a website source",
- )
- parser.add_argument(
- "--url",
- type=str,
- required=sys.argv[2] in [DETAIL_PARSER, CHAPTER_PARSER, IMAGE_PARSER],
- help="A link which to parse (detail/chapter/rss url)",
- )
-
- def handle(self, *args, **options):
- mute_logger_stdout("scrapy", "elasticsearch", "asyncio", "protego", "urllib3", "requests")
- try:
- catalogue_name: str = options["catalogue"]
- logger.info("Running parser")
- run_parser(options["type"], catalogue_name, url=options["url"])
- logger.info("Finished parsing")
- except (AttributeError, KeyError):
- logger.error(f"Can't find Catalogue [{catalogue_name}]")
- except Exception:
- logger.error(f"Some errors occured in the parser {catalogue_name}")
diff --git a/apps/parse/management/commands/estimator.py b/apps/parse/management/commands/estimator.py
new file mode 100644
index 00000000..f99af43c
--- /dev/null
+++ b/apps/parse/management/commands/estimator.py
@@ -0,0 +1,17 @@
+from django.core.management.base import BaseCommand
+
+from apps.manga.models import Manga
+from apps.parse.tasks import run_spider_task
+from apps.parse.types import ParserType
+
+
+class Command(BaseCommand):
+ def handle(self, *args, **options):
+ top_10 = Manga.objects.filter(popularity=2)
+
+ for manga in top_10:
+ run_spider_task(ParserType.detail, "readmanga", url=manga.source_url)
+ run_spider_task(ParserType.chapter, "readmanga", url=manga.chapters_url)
+ for num, chapter in enumerate(manga.chapters.all(), 1):
+ print(f"chapter images #{num}/{len(manga.chapters.all())}")
+ run_spider_task(ParserType.image, "readmanga", url=chapter.link)
diff --git a/apps/parse/management/commands/parse.py b/apps/parse/management/commands/parse.py
index c2f1383b..4fc23b4c 100644
--- a/apps/parse/management/commands/parse.py
+++ b/apps/parse/management/commands/parse.py
@@ -1,19 +1,11 @@
-import logging
import sys
from django.core.management.base import BaseCommand, CommandParser
-from apps.parse.const import (
- CATALOGUE_NAMES,
- CHAPTER_PARSER,
- DETAIL_PARSER,
- IMAGE_PARSER,
- PARSER_TYPES,
-)
-from apps.parse.scrapy.utils import run_parser
-from apps.parse.utils import mute_logger_stdout
-
-logger = logging.getLogger("management")
+from apps.parse.catalogue import Catalogue
+from apps.parse.exceptions import NotFound, ParsingError
+from apps.parse.tasks import run_spider_task
+from apps.parse.types import ParserType
class Command(BaseCommand):
@@ -21,33 +13,33 @@ def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument(
"type",
type=str,
- choices=PARSER_TYPES,
+ choices=list(ParserType),
help="which type of data to parse",
)
parser.add_argument(
"catalogue",
type=str,
default="readmanga",
- choices=CATALOGUE_NAMES,
- help="parser to use which respresents a website source",
+ choices=Catalogue.map.names,
+ help="parser to use which represents a website source",
)
parser.add_argument(
"--url",
type=str,
- required=sys.argv[2] in [DETAIL_PARSER, CHAPTER_PARSER, IMAGE_PARSER],
+ required=sys.argv[2] in [p for p in ParserType if p != ParserType.list],
help="A link which to parse (detail/chapter/rss url)",
)
def handle(self, *args, **options):
- mute_logger_stdout("scrapy", "elasticsearch", "asyncio", "protego", "urllib3", "requests")
+ catalogue_name: str = options["catalogue"]
try:
- catalogue_name: str = options["catalogue"]
- logger.info("Running parser")
- if options["type"] == DETAIL_PARSER or options["type"] == CHAPTER_PARSER:
- logger.warning("Warning! This will NOT update 'updated_detail' on a model")
- run_parser(options["type"], catalogue_name, url=options["url"])
- logger.info("Finished parsing")
- except (AttributeError, KeyError):
- logger.error(f"Can't find Catalogue [{catalogue_name}]")
+ self.stdout.write("Running parser")
+ run_spider_task(options["type"], catalogue_name, url=options["url"])
+ self.stdout.write("Finished parsing")
+ except NotFound as e:
+ self.stderr.write(f"Can't find {e}")
+ except ParsingError as e:
+ self.stderr.write(str(e))
except Exception as e:
- logger.error(f"Some errors occured in the parser {catalogue_name} {e}")
+ self.stderr.write(f"Some errors occurred in the parser {catalogue_name} {e}")
+ raise e
diff --git a/apps/parse/management/utils.py b/apps/parse/management/utils.py
new file mode 100644
index 00000000..a9637dc2
--- /dev/null
+++ b/apps/parse/management/utils.py
@@ -0,0 +1,12 @@
+import logging
+
+
+def mute_logger_stdout(logger_name: str, *other_loggers):
+ import warnings
+
+ logger_names = [logger_name, *other_loggers]
+ for name in logger_names:
+ warnings.filterwarnings("ignore", module=name)
+ logger = logging.getLogger(name)
+ logger.setLevel(logging.CRITICAL)
+ logger.propagate = False
diff --git a/apps/parse/migrations/0001_initial.py b/apps/parse/migrations/0001_initial.py
deleted file mode 100644
index 4038ab01..00000000
--- a/apps/parse/migrations/0001_initial.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# Generated by Django 3.2 on 2021-04-12 14:32
-
-import django.db.models.deletion
-import django_extensions.db.fields
-from django.db import migrations, models
-
-from apps.core.abc.models import BaseModel
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = []
-
- operations = [
- migrations.CreateModel(
- name="Author",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="author_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="Category",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="category_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="Genre",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="genre_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="Illustrator",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="illustrator_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="ScreenWriter",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="screenwriter_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="Translator",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(unique=True, verbose_name="translator_name")),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- migrations.CreateModel(
- name="Manga",
- fields=[
- (
- "created",
- django_extensions.db.fields.CreationDateTimeField(
- auto_now_add=True, verbose_name="created"
- ),
- ),
- (
- "modified",
- django_extensions.db.fields.ModificationDateTimeField(
- auto_now=True, verbose_name="modified"
- ),
- ),
- ("name", models.TextField(blank=True, null=True, verbose_name="manga_name")),
- (
- "self_url",
- models.URLField(max_length=1000, unique=True, verbose_name="manga_url"),
- ),
- ("description", models.TextField(verbose_name="manga_description")),
- ("status", models.TextField(blank=True, null=True, verbose_name="status")),
- ("year", models.TextField(blank=True, null=True, verbose_name="year")),
- ("image_url", models.URLField(default="", verbose_name="image_url")),
- (
- "chapters",
- models.JSONField(default=dict),
- ),
- ("technical_params", models.JSONField(default=dict)),
- (
- "author",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="mangas",
- to="parse.author",
- ),
- ),
- (
- "categories",
- models.ManyToManyField(related_name="mangas", to="parse.Category"),
- ),
- (
- "genres",
- models.ManyToManyField(related_name="mangas", to="parse.Genre"),
- ),
- (
- "illustrators",
- models.ManyToManyField(
- related_name="mangas", to="parse.Illustrator"
- ),
- ),
- (
- "screenwriters",
- models.ManyToManyField(
- related_name="mangas", to="parse.ScreenWriter"
- ),
- ),
- (
- "translators",
- models.ManyToManyField(related_name="mangas", to="parse.Translator"),
- ),
- ],
- options={
- "get_latest_by": "modified",
- "abstract": False,
- },
- bases=(BaseModel, models.Model),
- ),
- ]
diff --git a/apps/parse/migrations/0002_auto_20210413_0228.py b/apps/parse/migrations/0002_auto_20210413_0228.py
deleted file mode 100644
index 60d2f5d9..00000000
--- a/apps/parse/migrations/0002_auto_20210413_0228.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Generated by Django 3.2 on 2021-04-13 02:28
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("parse", "0001_initial"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="author",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="category",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="genre",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="illustrator",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="manga",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="screenwriter",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- migrations.AlterField(
- model_name="translator",
- name="id",
- field=models.AutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
- ),
- ]
diff --git a/apps/parse/migrations/0003_auto_20210529_0806.py b/apps/parse/migrations/0003_auto_20210529_0806.py
deleted file mode 100644
index ca359436..00000000
--- a/apps/parse/migrations/0003_auto_20210529_0806.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Generated by Django 3.2.3 on 2021-05-29 08:06
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0002_auto_20210413_0228'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='author',
- options={},
- ),
- migrations.AlterModelOptions(
- name='category',
- options={},
- ),
- migrations.AlterModelOptions(
- name='genre',
- options={},
- ),
- migrations.AlterModelOptions(
- name='illustrator',
- options={},
- ),
- migrations.AlterModelOptions(
- name='manga',
- options={},
- ),
- migrations.AlterModelOptions(
- name='screenwriter',
- options={},
- ),
- migrations.AlterModelOptions(
- name='translator',
- options={},
- ),
- ]
diff --git a/apps/parse/migrations/0004_auto_20210603_1543.py b/apps/parse/migrations/0004_auto_20210603_1543.py
deleted file mode 100644
index 20829ff9..00000000
--- a/apps/parse/migrations/0004_auto_20210603_1543.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-03 15:43
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("parse", "0003_auto_20210529_0806"),
- ]
-
- operations = [
- migrations.RenameField(
- model_name="manga",
- old_name="name",
- new_name="title",
- ),
- migrations.AlterField(
- model_name="manga",
- name="description",
- field=models.TextField(),
- ),
- migrations.AlterField(
- model_name="manga",
- name="self_url",
- field=models.URLField(max_length=1000, unique=True),
- ),
- ]
diff --git a/apps/parse/migrations/0005_auto_20210604_1118.py b/apps/parse/migrations/0005_auto_20210604_1118.py
deleted file mode 100644
index 1e633d2e..00000000
--- a/apps/parse/migrations/0005_auto_20210604_1118.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-04 11:18
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("parse", "0004_auto_20210603_1543"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="author",
- name="name",
- field=models.TextField(unique=True),
- ),
- migrations.AlterField(
- model_name="category",
- name="name",
- field=models.TextField(unique=True),
- ),
- migrations.AlterField(
- model_name="genre",
- name="name",
- field=models.TextField(unique=True),
- ),
- migrations.AlterField(
- model_name="illustrator",
- name="name",
- field=models.TextField(unique=True),
- ),
- migrations.AlterField(
- model_name="manga",
- name="image_url",
- field=models.URLField(default="", verbose_name="thumbnail url"),
- ),
- migrations.AlterField(
- model_name="manga",
- name="status",
- field=models.TextField(blank=True, null=True),
- ),
- migrations.AlterField(
- model_name="manga",
- name="title",
- field=models.TextField(blank=True, null=True),
- ),
- migrations.AlterField(
- model_name="manga",
- name="year",
- field=models.TextField(blank=True, null=True),
- ),
- migrations.AlterField(
- model_name="screenwriter",
- name="name",
- field=models.TextField(unique=True),
- ),
- migrations.AlterField(
- model_name="translator",
- name="name",
- field=models.TextField(unique=True),
- ),
- ]
diff --git a/apps/parse/migrations/0006_manga_alt_title.py b/apps/parse/migrations/0006_manga_alt_title.py
deleted file mode 100644
index b7880c65..00000000
--- a/apps/parse/migrations/0006_manga_alt_title.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-05 10:34
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0005_auto_20210604_1118'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='manga',
- name='alt_title',
- field=models.TextField(blank=True, null=True),
- ),
- ]
diff --git a/apps/parse/migrations/0007_auto_20210607_1923.py b/apps/parse/migrations/0007_auto_20210607_1923.py
deleted file mode 100644
index b4de9788..00000000
--- a/apps/parse/migrations/0007_auto_20210607_1923.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-07 19:23
-
-import django.db.models.deletion
-import django_extensions.db.fields
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0006_manga_alt_title'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Person',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
- ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
- ('name', models.TextField(unique=True)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.CreateModel(
- name='PersonRole',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
- ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
- ('person_role', models.CharField(choices=[('Illustrator', 'Illustrator'), ('SCREENWRITER', 'Screenwriter'), ('TRANSLATOR', 'Translator')], max_length=15)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.RemoveField(
- model_name='manga',
- name='illustrators',
- ),
- migrations.RemoveField(
- model_name='manga',
- name='screenwriters',
- ),
- migrations.RemoveField(
- model_name='manga',
- name='translators',
- ),
- migrations.DeleteModel(
- name='Illustrator',
- ),
- migrations.DeleteModel(
- name='ScreenWriter',
- ),
- migrations.DeleteModel(
- name='Translator',
- ),
- migrations.AddField(
- model_name='personrole',
- name='manga',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mangas', to='parse.manga'),
- ),
- migrations.AddField(
- model_name='personrole',
- name='person',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='persons', to='parse.person'),
- ),
- ]
diff --git a/apps/parse/migrations/0008_taskcontrol.py b/apps/parse/migrations/0008_taskcontrol.py
deleted file mode 100644
index 1f13e92b..00000000
--- a/apps/parse/migrations/0008_taskcontrol.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-12 13:57
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0007_auto_20210607_1923'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='TaskControl',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('task_status', models.BooleanField(default=False)),
- ('task_name', models.CharField(max_length=255)),
- ],
- ),
- ]
diff --git a/apps/parse/migrations/0009_auto_20210616_1353.py b/apps/parse/migrations/0009_auto_20210616_1353.py
deleted file mode 100644
index 4636fe8f..00000000
--- a/apps/parse/migrations/0009_auto_20210616_1353.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-16 13:53
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0008_taskcontrol'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='author',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='category',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='genre',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='manga',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='person',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='personrole',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='taskcontrol',
- name='id',
- field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- ]
diff --git a/apps/parse/migrations/0010_delete_taskcontrol.py b/apps/parse/migrations/0010_delete_taskcontrol.py
deleted file mode 100644
index 103e82b4..00000000
--- a/apps/parse/migrations/0010_delete_taskcontrol.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-16 20:55
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0009_auto_20210616_1353'),
- ]
-
- operations = [
- migrations.DeleteModel(
- name='TaskControl',
- ),
- ]
diff --git a/apps/parse/migrations/0011_auto_20210622_1319.py b/apps/parse/migrations/0011_auto_20210622_1319.py
deleted file mode 100644
index 64c6d02c..00000000
--- a/apps/parse/migrations/0011_auto_20210622_1319.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-22 13:19
-
-import django.db.models.deletion
-import django_extensions.db.fields
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0010_delete_taskcontrol'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='manga',
- old_name='self_url',
- new_name='source_url',
- ),
- migrations.AlterField(
- model_name='manga',
- name='source_url',
- field=models.URLField(default=1, max_length=2000, unique=True),
- preserve_default=False,
- ),
- migrations.CreateModel(
- name='PersonRelatedToManga',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
- ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
- ('role', models.TextField(choices=[('author', 'Author'), ('illustrator', 'Illustrator'), ('screenwriter', 'Screenwriter'), ('translator', 'Translator')])),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.RemoveField(
- model_name='personrole',
- name='manga',
- ),
- migrations.RemoveField(
- model_name='personrole',
- name='person',
- ),
- migrations.RemoveField(
- model_name='manga',
- name='author',
- ),
- migrations.RemoveField(
- model_name='manga',
- name='technical_params',
- ),
- migrations.DeleteModel(
- name='Author',
- ),
- migrations.DeleteModel(
- name='PersonRole',
- ),
- migrations.AddField(
- model_name='personrelatedtomanga',
- name='manga',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mangas', to='parse.manga'),
- ),
- migrations.AddField(
- model_name='personrelatedtomanga',
- name='person',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='persons', to='parse.person'),
- ),
- migrations.AddField(
- model_name='manga',
- name='people_related',
- field=models.ManyToManyField(related_name='mangas', through='parse.PersonRelatedToManga', to='parse.Person'),
- ),
- ]
diff --git a/apps/parse/migrations/0012_auto_20210622_1333.py b/apps/parse/migrations/0012_auto_20210622_1333.py
deleted file mode 100644
index 23e6a1b1..00000000
--- a/apps/parse/migrations/0012_auto_20210622_1333.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-22 13:33
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0011_auto_20210622_1319'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='personrelatedtomanga',
- name='created',
- ),
- migrations.RemoveField(
- model_name='personrelatedtomanga',
- name='modified',
- ),
- ]
diff --git a/apps/parse/migrations/0013_auto_20210623_1144.py b/apps/parse/migrations/0013_auto_20210623_1144.py
deleted file mode 100644
index ff3f9643..00000000
--- a/apps/parse/migrations/0013_auto_20210623_1144.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-23 11:44
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0012_auto_20210622_1333'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Author',
- fields=[
- ],
- options={
- 'proxy': True,
- 'indexes': [],
- 'constraints': [],
- },
- bases=('parse.person',),
- ),
- migrations.AlterField(
- model_name='personrelatedtomanga',
- name='manga',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='person_relations', to='parse.manga'),
- ),
- migrations.AlterField(
- model_name='personrelatedtomanga',
- name='person',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='manga_relations', to='parse.person'),
- ),
- ]
diff --git a/apps/parse/migrations/0014_auto_20210623_1239.py b/apps/parse/migrations/0014_auto_20210623_1239.py
deleted file mode 100644
index edbe90a9..00000000
--- a/apps/parse/migrations/0014_auto_20210623_1239.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-23 12:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0013_auto_20210623_1144'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='manga',
- old_name='image_url',
- new_name='thumbnail',
- ),
- migrations.AddField(
- model_name='manga',
- name='image',
- field=models.URLField(default=''),
- ),
- ]
diff --git a/apps/parse/migrations/0015_auto_20210623_1321.py b/apps/parse/migrations/0015_auto_20210623_1321.py
deleted file mode 100644
index 44072761..00000000
--- a/apps/parse/migrations/0015_auto_20210623_1321.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-23 13:21
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0014_auto_20210623_1239'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='manga',
- old_name='chapters',
- new_name='volumes',
- ),
- migrations.AlterField(
- model_name='manga',
- name='image',
- field=models.URLField(default='', max_length=2000),
- ),
- migrations.AlterField(
- model_name='manga',
- name='thumbnail',
- field=models.URLField(default='', max_length=2000),
- ),
- migrations.AlterField(
- model_name='manga',
- name='title',
- field=models.TextField(default=''),
- preserve_default=False,
- ),
- ]
diff --git a/apps/parse/migrations/0016_manga_rating.py b/apps/parse/migrations/0016_manga_rating.py
deleted file mode 100644
index 50eb9a9a..00000000
--- a/apps/parse/migrations/0016_manga_rating.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-24 14:31
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0015_auto_20210623_1321'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='manga',
- name='rating',
- field=models.FloatField(default=0),
- ),
- ]
diff --git a/apps/parse/migrations/0017_auto_20210618_1048.py b/apps/parse/migrations/0017_auto_20210618_1048.py
deleted file mode 100644
index 39ff9348..00000000
--- a/apps/parse/migrations/0017_auto_20210618_1048.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-18 10:48
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0016_manga_rating'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='manga',
- name='updated_chapters',
- field=models.DateTimeField(blank=True, null=True),
- ),
- migrations.AddField(
- model_name='manga',
- name='updated_detail',
- field=models.DateTimeField(blank=True, null=True),
- ),
- migrations.AddField(
- model_name='manga',
- name='rss_url',
- field=models.URLField(blank=True, null=True, max_length=2000),
- ),
-
- ]
diff --git a/apps/parse/migrations/0018_auto_20210629_1400.py b/apps/parse/migrations/0018_auto_20210629_1400.py
deleted file mode 100644
index e0666fee..00000000
--- a/apps/parse/migrations/0018_auto_20210629_1400.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Generated by Django 3.2.3 on 2021-06-29 14:00
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0017_auto_20210618_1048'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Chapter',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.TextField()),
- ('link', models.URLField(max_length=2000)),
- ('number', models.IntegerField()),
- ('volume', models.IntegerField()),
- ],
- ),
- migrations.RemoveField(
- model_name='manga',
- name='volumes',
- ),
- migrations.AddField(
- model_name='manga',
- name='volumes',
- field=models.ManyToManyField(to='parse.Chapter'),
- ),
- ]
diff --git a/apps/parse/migrations/0019_rename_volumes_manga_chapters.py b/apps/parse/migrations/0019_rename_volumes_manga_chapters.py
deleted file mode 100644
index e88a560c..00000000
--- a/apps/parse/migrations/0019_rename_volumes_manga_chapters.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.3 on 2021-07-18 13:46
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0018_auto_20210629_1400'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='manga',
- old_name='volumes',
- new_name='chapters',
- ),
- ]
diff --git a/apps/parse/migrations/0020_auto_20210727_2035.py b/apps/parse/migrations/0020_auto_20210727_2035.py
deleted file mode 100644
index 460f7c21..00000000
--- a/apps/parse/migrations/0020_auto_20210727_2035.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Generated by Django 3.2.3 on 2021-07-27 17:35
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0019_rename_volumes_manga_chapters'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='manga',
- name='categories',
- field=models.ManyToManyField(blank=True, related_name='mangas', to='parse.Category'),
- ),
- migrations.AlterField(
- model_name='manga',
- name='chapters',
- field=models.ManyToManyField(blank=True, to='parse.Chapter'),
- ),
- migrations.AlterField(
- model_name='manga',
- name='description',
- field=models.TextField(blank=True, default=''),
- ),
- migrations.AlterField(
- model_name='manga',
- name='genres',
- field=models.ManyToManyField(blank=True, related_name='mangas', to='parse.Genre'),
- ),
- migrations.AlterField(
- model_name='manga',
- name='image',
- field=models.URLField(blank=True, default='', max_length=2000),
- ),
- migrations.AlterField(
- model_name='manga',
- name='thumbnail',
- field=models.URLField(blank=True, default='', max_length=2000),
- ),
- ]
diff --git a/apps/parse/migrations/0021_change_rating_scale.py b/apps/parse/migrations/0021_change_rating_scale.py
deleted file mode 100644
index 650c374d..00000000
--- a/apps/parse/migrations/0021_change_rating_scale.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Generated by Django 3.2.3 on 2021-07-27 11:40
-
-from django.db import migrations
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("parse", "0020_auto_20210727_2035"),
- ]
-
- operations = [
- migrations.RunSQL("UPDATE parse_manga SET rating = rating / 2 WHERE rating > 5;"),
- ]
diff --git a/apps/parse/migrations/0022_auto_20210831_1152.py b/apps/parse/migrations/0022_auto_20210831_1152.py
deleted file mode 100644
index 2b9763b4..00000000
--- a/apps/parse/migrations/0022_auto_20210831_1152.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Generated by Django 3.1 on 2021-08-31 11:52
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0021_change_rating_scale'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='category',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='chapter',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='genre',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='manga',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='person',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='personrelatedtomanga',
- name='id',
- field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
- ),
- ]
diff --git a/apps/parse/migrations/0023_readmanga_live_to_io.py b/apps/parse/migrations/0023_readmanga_live_to_io.py
deleted file mode 100644
index 5dd6ac18..00000000
--- a/apps/parse/migrations/0023_readmanga_live_to_io.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.1 on 2021-08-31 11:52
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0022_auto_20210831_1152'),
- ]
-
- operations = [
- migrations.RunSQL(
- "UPDATE parse_manga SET " \
- "source_url = REPLACE(source_url, 'readmanga.live', 'readmanga.io'), " \
- "rss_url = REPLACE(rss_url, 'readmanga.live', 'readmanga.io')"
- ),
- ]
diff --git a/apps/parse/migrations/0024_auto_20210930_1807.py b/apps/parse/migrations/0024_auto_20210930_1807.py
deleted file mode 100644
index 5ec0b69c..00000000
--- a/apps/parse/migrations/0024_auto_20210930_1807.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 3.1 on 2021-09-30 18:07
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0023_readmanga_live_to_io'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='chapter',
- name='number',
- field=models.FloatField(),
- ),
- migrations.AlterField(
- model_name='chapter',
- name='volume',
- field=models.FloatField(),
- ),
- ]
diff --git a/apps/parse/migrations/0025_chapters_to_fk.py b/apps/parse/migrations/0025_chapters_to_fk.py
deleted file mode 100644
index 1bc6c45e..00000000
--- a/apps/parse/migrations/0025_chapters_to_fk.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0024_auto_20210930_1807'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='manga',
- name='updated_chapters',
- ),
- migrations.AddField(
- model_name='chapter',
- name='manga',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='parse.manga'),
- ),
- # move everything from M2M to column
- migrations.RunSQL("""
- UPDATE parse_chapter
- SET manga_id = pmc.manga_id
- FROM parse_manga_chapters AS pmc
- WHERE parse_chapter.id = pmc.chapter_id;
- """),
- migrations.RemoveField(
- model_name='manga',
- name='chapters',
- ),
- migrations.AlterField(
- model_name='chapter',
- name='manga',
- field=models.ForeignKey(null=False, on_delete=django.db.models.deletion.CASCADE, related_name='chapters', to='parse.manga'),
- ),
- ]
diff --git a/apps/parse/migrations/0026_auto_20211112_1448.py b/apps/parse/migrations/0026_auto_20211112_1448.py
deleted file mode 100644
index 6e6be5e4..00000000
--- a/apps/parse/migrations/0026_auto_20211112_1448.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 3.1 on 2021-11-12 14:48
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0025_chapters_to_fk'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='chapter',
- name='manga',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chapters', to='parse.manga'),
- ),
- ]
diff --git a/apps/parse/migrations/0027_auto_20211226_2232.py b/apps/parse/migrations/0027_auto_20211226_2232.py
deleted file mode 100644
index 0f459352..00000000
--- a/apps/parse/migrations/0027_auto_20211226_2232.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.1 on 2021-12-26 22:32
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('parse', '0026_auto_20211112_1448'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='manga',
- name='rating',
- field=models.DecimalField(decimal_places=2, default=0, max_digits=4),
- ),
- ]
diff --git a/apps/parse/models.py b/apps/parse/models.py
deleted file mode 100644
index c389342e..00000000
--- a/apps/parse/models.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from datetime import timedelta
-
-from django.db import models
-from django.db.models.fields import DateTimeField, DecimalField, FloatField, TextField, URLField
-from django.db.models.fields.related import ForeignKey, ManyToManyField
-from django.db.models.query import QuerySet
-
-from apps.core.abc.models import BaseModel
-from apps.core.fast import FastQuerySet
-from apps.core.utils import url_prefix
-from apps.parse.const import SOURCE_TO_CATALOGUE_MAP
-
-
-class Category(BaseModel):
- name = TextField(unique=True)
-
-
-class Genre(BaseModel):
- name = TextField(unique=True)
-
-
-class Person(BaseModel):
- name = TextField(unique=True)
-
-
-class Author(Person):
- class AuthorManager(models.Manager):
- def get_queryset(self):
- return super().get_queryset().filter(manga_relations__role="author")
-
- objects = AuthorManager()
-
- class Meta:
- proxy = True
-
-
-class PersonRelatedToManga(models.Model):
- class Roles(models.TextChoices):
- author = "author"
- illustrator = "illustrator"
- screenwriter = "screenwriter"
- translator = "translator"
-
- person = ForeignKey("Person", models.CASCADE, related_name="manga_relations")
- manga = ForeignKey("Manga", models.CASCADE, related_name="person_relations")
- role = TextField(choices=Roles.choices)
-
-
-class Chapter(models.Model):
- manga = ForeignKey("Manga", models.CASCADE, related_name="chapters", null=True)
-
- title = TextField()
- link = URLField(max_length=2000)
- number = FloatField()
- volume = FloatField()
-
- def __str__(self) -> str:
- return self.title
-
-
-class Manga(BaseModel):
- objects = models.Manager.from_queryset(FastQuerySet)()
- NAME_FIELD = "title"
-
- BASE_UPDATE_FREQUENCY = timedelta(hours=1)
- IMAGE_UPDATE_FREQUENCY = timedelta(hours=8)
-
- title = TextField()
- alt_title = TextField(null=True, blank=True)
- rating = DecimalField(default=0, max_digits=4, decimal_places=2)
- thumbnail = URLField(max_length=2000, default="", blank=True)
- image = URLField(max_length=2000, default="", blank=True)
- description = TextField(default="", blank=True)
- status = TextField(null=True, blank=True)
- year = TextField(null=True, blank=True)
- # https://stackoverflow.com/questions/417142
- source_url = URLField(max_length=2000, unique=True)
- # There can be manga with no chapters, i.e. future releases
- rss_url = URLField(max_length=2000, null=True, blank=True)
- genres = ManyToManyField("Genre", related_name="mangas", blank=True)
- categories = ManyToManyField("Category", related_name="mangas", blank=True)
- updated_detail = DateTimeField(blank=True, null=True)
- people_related = ManyToManyField(
- "Person", through="PersonRelatedToManga", related_name="mangas"
- )
-
- @property
- def source(self):
- return SOURCE_TO_CATALOGUE_MAP[url_prefix(self.source_url)]
-
- @property
- def authors(self) -> QuerySet["Person"]:
- """For admin use only"""
- return Person.objects.filter(
- manga_relations__manga=self, manga_relations__role=PersonRelatedToManga.Roles.author
- )
-
- def save(self, **kwargs):
- if not self.image:
- self.image = self.thumbnail
- return super().save(**kwargs)
diff --git a/apps/parse/readmanga/chapter.py b/apps/parse/readmanga/chapter.py
deleted file mode 100644
index 0cef58a1..00000000
--- a/apps/parse/readmanga/chapter.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import re
-from typing import List
-
-import scrapy
-from scrapy.http import XmlResponse
-
-from apps.parse.scrapy.items import MangaChapterItem
-from apps.parse.scrapy.spider import InjectUrlMixin
-
-ITEM_TAG = "//item"
-LINK_TAG = 'guid[@isPermaLink="true"]/text()'
-TITLE_TAG = ".//title/text()"
-
-
-class ReadmangaChapterSpider(InjectUrlMixin, scrapy.Spider):
- name = "readmanga_chapter"
- custom_settings = {
- "ITEM_PIPELINES": {"apps.parse.readmanga.pipelines.ReadmangaChapterPipeline": 300}
- }
-
- def parse(self, response: XmlResponse) -> List[MangaChapterItem]:
- chapters = []
-
- items = response.xpath(ITEM_TAG)
- for item in items:
- link = item.xpath(LINK_TAG).extract_first()
-
- chapter_title = item.xpath(TITLE_TAG).extract_first()
-
- res_reg = re.search(r":\s*(.*)$", chapter_title)
- if res_reg:
- chapter_title = res_reg.group(1)
-
- volume, number = link.split("/")[-2:]
- volume = int(volume.replace("vol", ""))
-
- chapters.append(
- MangaChapterItem(
- **{
- "manga_rss_url": response.url,
- "title": chapter_title,
- "volume": volume,
- "number": number,
- "link": link,
- }
- )
- )
- return chapters
diff --git a/apps/parse/readmanga/detail.py b/apps/parse/readmanga/detail.py
deleted file mode 100644
index 018876b2..00000000
--- a/apps/parse/readmanga/detail.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import scrapy
-from scrapy.http import HtmlResponse
-
-from apps.parse.scrapy.items import MangaItem
-from apps.parse.scrapy.spider import InjectUrlMixin
-
-RSS_TAG = "//head/link[@type='application/rss+xml'][1]/@href"
-AUTHORS_TAG = "//span[@class='elem_author ']/a[@class='person-link']/text()"
-YEAR_TAG = "//span[@class='elem_year ']/a[@class='element-link'][1]/text()"
-TRANSLATORS_TAG = "//span[@class='elem_translator ']/a[@class='person-link']/text()"
-ILLUSTRATOR_TAG = '//span[@class = "elem_illustrator "]/a[@class="person-link"]/text()'
-SCREENWRITER_TAG = '//span[@class="elem_screenwriter "]/a[@class="person-link"]/text()'
-CATEGORY_TAG = '//span[@class = "elem_category "]/a[@class="element-link"]/text()'
-DESCRIPTION_TAG = "//meta[@itemprop='description'][1]/@content"
-STAR_RATING_TAG = "//span[@class='rating-block']/@data-score"
-
-
-class ReadmangaDetailSpider(InjectUrlMixin, scrapy.Spider):
- name = "readmanga_detail"
-
- def parse(self, response: HtmlResponse):
- year = response.xpath(YEAR_TAG).extract_first("")
- description = response.xpath(DESCRIPTION_TAG).extract_first("")
- rating = response.xpath(STAR_RATING_TAG).extract_first(0.0)
- rss_url = response.xpath(RSS_TAG).extract_first("")
- authors = response.xpath(AUTHORS_TAG).extract()
- screenwriters = response.xpath(SCREENWRITER_TAG).extract()
- translators = response.xpath(TRANSLATORS_TAG).extract()
- categories = response.xpath(CATEGORY_TAG).extract()
- illustrators = response.xpath(ILLUSTRATOR_TAG).extract()
- return [
- MangaItem(
- **{
- "source_url": response.url,
- "authors": authors,
- "year": year,
- "rating": rating,
- "description": description,
- "translators": translators,
- "illustrators": illustrators,
- "screenwriters": screenwriters,
- "categories": categories,
- "rss_url": rss_url,
- }
- )
- ]
diff --git a/apps/parse/readmanga/images.py b/apps/parse/readmanga/images.py
deleted file mode 100644
index cd9653c1..00000000
--- a/apps/parse/readmanga/images.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import re
-
-import scrapy
-import ujson
-from scrapy.http import HtmlResponse
-
-from apps.core.utils import init_redis_client
-from apps.parse.scrapy.spider import InjectUrlMixin
-
-COUNT_LINK_ELEMENTS = 3
-
-
-class ReadmangaImageSpider(InjectUrlMixin, scrapy.Spider):
- name = "readmanga_image"
- custom_settings = {
- "ITEM_PIPELINES": {"apps.parse.readmanga.pipelines.ReadmangaImagePipeline": 300}
- }
-
- def __init__(self, *args, url: str, **kwargs):
- super().__init__(*args, **kwargs, start_urls=[url], redis_client=init_redis_client())
-
- def parse(self, response: HtmlResponse):
- images = re.search(r"rm_h.initReader\(.*(\[{2}.*\]{2}).*\)", response.text)
- image_links = []
- if images:
- image_links = [
- "".join(image[:COUNT_LINK_ELEMENTS])
- for image in ujson.loads(images.group(1).replace("'", '"'))
- ]
- return {self.start_urls[0]: image_links}
diff --git a/apps/parse/readmanga/list/__init__.py b/apps/parse/readmanga/list/__init__.py
deleted file mode 100644
index 421fbccf..00000000
--- a/apps/parse/readmanga/list/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .spider import ReadmangaListSpider
diff --git a/apps/parse/readmanga/list/spider.py b/apps/parse/readmanga/list/spider.py
deleted file mode 100644
index ae8dd9fb..00000000
--- a/apps/parse/readmanga/list/spider.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from typing import List
-
-from scrapy.http import HtmlResponse
-from scrapy.linkextractors import LinkExtractor
-from scrapy.spiders.crawl import CrawlSpider, Rule
-
-from apps.parse.readmanga.list.utils import parse_rating
-from apps.parse.scrapy.items import MangaItem
-from apps.parse.scrapy.spider import InjectUrlMixin
-
-READMANGA_URL = "https://readmanga.io"
-LIST_URL = f"{READMANGA_URL}/list"
-
-MANGA_TILE_TAG = '//div[@class = "tiles row"]//div[contains(@class, "tile col-md-6")]'
-STAR_RATE_TAG = '//div[@class = "rating"]/@title'
-TITLE_TAG = "//h3/a[1]/@title"
-SOURCE_URL_TAG = "//h3/a[1]/@href"
-GENRES_TAG = '//div[@class = "tile-info"]//a[contains(@class, "badge")]/text()'
-THUMBNAIL_IMG_URL_TAG = '//img[contains(@class, "lazy")][1]/@data-original'
-ALT_TITLE_URL = "//h4[@title]//text()"
-
-
-class ReadmangaListSpider(InjectUrlMixin, CrawlSpider):
- name = "readmanga_list"
- start_urls = [LIST_URL]
- rules = [
- Rule(
- LinkExtractor(restrict_xpaths=["//a[@class='nextLink']"]),
- follow=True,
- callback="parse",
- ),
- ]
- custom_settings = {
- "DEPTH_LIMIT": 400,
- }
-
- def parse_start_url(self, response, **kwargs):
- return self.parse(response, **kwargs)
-
- def parse(self, response):
- mangas: List[MangaItem] = []
- descriptions = response.xpath(MANGA_TILE_TAG).extract()
- for description in descriptions:
- response = HtmlResponse(url="", body=description, encoding="utf-8")
-
- rating = parse_rating(response.xpath(STAR_RATE_TAG).extract_first("")) / 2
- title = response.xpath(TITLE_TAG).extract_first("")
- source_url = response.xpath(SOURCE_URL_TAG).extract_first("")
- genres = response.xpath(GENRES_TAG).extract()
- thumbnail = response.xpath(THUMBNAIL_IMG_URL_TAG).extract_first("")
- image = thumbnail.replace("_p", "")
- alt_title = response.xpath(ALT_TITLE_URL).extract_first("")
-
- mangas.append(
- MangaItem(
- **{
- "rating": rating,
- "title": title,
- "alt_title": alt_title,
- "thumbnail": thumbnail,
- "image": image,
- "genres": genres,
- "source_url": READMANGA_URL + source_url,
- }
- )
- )
- self.logger.info('Parsed manga "{}"'.format(title))
-
- self.logger.info("===================")
- return mangas
diff --git a/apps/parse/readmanga/list/utils.py b/apps/parse/readmanga/list/utils.py
deleted file mode 100644
index b3eb3330..00000000
--- a/apps/parse/readmanga/list/utils.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import re
-
-
-def chapters_into_dict(chapters: list) -> dict:
- regex = "[\n]+|[ ]{2,}"
- chapters = [re.sub(regex, "", chapter) for chapter in chapters]
- readmanga_base_url = "https://readmanga.io"
- links = filter(lambda m: m.startswith("/"), chapters)
- names = filter(lambda n: not n.startswith("/"), chapters)
-
- chapters_catalogue = {}
- for link, name in zip(links, names):
- chapters_catalogue.update({readmanga_base_url + link: name})
-
- return chapters_catalogue
-
-
-def parse_rating(rate_str: str):
- """
- >>> parse_rating("9.439212799072266 из 10")
- 9.43
- """
- try:
- return float(re.match(r"^(\d\.\d{2})\d* из 10$", rate_str).group(1))
- except (AttributeError, ValueError):
- return 0.0
diff --git a/apps/parse/readmanga/pipelines.py b/apps/parse/readmanga/pipelines.py
deleted file mode 100644
index e9e3d755..00000000
--- a/apps/parse/readmanga/pipelines.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import logging
-from copy import deepcopy
-from typing import Dict, List, Tuple, Type
-
-from django.db import transaction
-from django.utils import timezone
-from scrapy.spiders import Spider
-
-from apps.core.abc.models import BaseModel
-from apps.core.utils import url_prefix
-from apps.parse.const import IMAGE_UPDATE_FREQUENCY
-from apps.parse.models import Category, Chapter, Genre, Manga, PersonRelatedToManga
-from apps.parse.readmanga.chapter import ReadmangaChapterSpider
-from apps.parse.readmanga.detail import ReadmangaDetailSpider
-from apps.parse.readmanga.images import ReadmangaImageSpider
-from apps.parse.readmanga.list.spider import ReadmangaListSpider
-from apps.parse.scrapy.items import MangaChapterItem
-from apps.parse.utils import save_persons
-
-logger = logging.getLogger("scrapy")
-
-
-@transaction.atomic
-def bulk_get_or_create(cls: Type[BaseModel], names: List[str]) -> Tuple:
- objects = []
- for name in names:
- objects.append(cls.objects.get_or_create(name=name))
- return tuple(obj for obj, _ in objects)
-
-
-class ReadmangaImagePipeline:
- @staticmethod
- def process_item(item: Dict[str, List[str]], spider: ReadmangaImageSpider):
- url, images = next(iter(item.items()))
- spider.redis_client.delete(url)
- spider.redis_client.expire(url, IMAGE_UPDATE_FREQUENCY)
- spider.redis_client.rpush(url, *images)
-
-
-class ReadmangaChapterPipeline:
- @staticmethod
- def process_item(chapter: MangaChapterItem, _: ReadmangaChapterSpider):
- rss_url = chapter.pop("manga_rss_url")
- manga = Manga.objects.get(rss_url=rss_url)
- Chapter.objects.get_or_create(
- manga=manga,
- **chapter,
- )
-
-
-class ReadmangaPipeline:
- @staticmethod
- def get_or_create_or_update_manga(spider: Spider, source_url, **data) -> Manga:
- """Explicit is better than implicit."""
- manga, _ = Manga.objects.get_or_create(source_url=source_url)
- manga: Manga
- manga_already = Manga.objects.filter(source_url=source_url)
- if manga_already.exists():
- manga_already.update(**data)
- manga = manga_already.first()
- spider.logger.info(f'Updated item "{manga}"')
- else:
- manga = Manga.objects.create(
- source_url=source_url,
- **data,
- )
- spider.logger.info(f'Created item "{manga}"')
- return manga
-
- @classmethod
- def process_item(cls, item: dict, spider: Spider) -> dict:
- data = deepcopy(item)
-
- if isinstance(spider, ReadmangaListSpider) and not data.get("title", None):
- message = f"Error processing {data}: No title name was set"
- spider.logger.error(message)
- raise KeyError(message)
-
- source_url = data.pop("source_url")
- rss_url = data.get("rss_url", None)
- if rss_url:
- data["rss_url"] = url_prefix(spider.start_urls[0]) + rss_url
-
- genres = data.pop("genres", [])
- authors = data.pop("authors", [])
- illustrators = data.pop("illustrators", [])
- screenwriters = data.pop("screenwriters", [])
- translators = data.pop("translators", [])
- categories = data.pop("categories", [])
-
- manga = cls.get_or_create_or_update_manga(spider, source_url, **data)
-
- genres = bulk_get_or_create(Genre, genres)
- manga.genres.add(*genres)
-
- save_persons(manga, PersonRelatedToManga.Roles.author, authors)
- save_persons(manga, PersonRelatedToManga.Roles.illustrator, illustrators)
- save_persons(manga, PersonRelatedToManga.Roles.screenwriter, screenwriters)
- save_persons(manga, PersonRelatedToManga.Roles.translator, translators)
-
- categories = [Category.objects.get_or_create(name=category)[0] for category in categories]
- manga.categories.clear()
- manga.categories.set(categories)
-
- if isinstance(spider, ReadmangaDetailSpider):
- data["updated_detail"] = timezone.now()
-
- return item
diff --git a/apps/parse/readmanga/settings.py b/apps/parse/readmanga/settings.py
deleted file mode 100644
index e5aaf360..00000000
--- a/apps/parse/readmanga/settings.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from apps.parse.readmanga.pipelines import ReadmangaPipeline
-
-ROBOTSTXT_OBEY = True
-
-DOWNLOAD_DELAY = 2
-
-AUTOTHROTTLE_ENABLED = True
-AUTOTHROTTLE_DEBUG = True
-AUTOTHROTTLE_START_DELAY = 2
-AUTOTHROTTLE_MAX_DELAY = 15
-AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
-
-
-BOT_NAME = "readmanga"
-SPIDER_MODULES = ["apps.parse.readmanga.list"]
-NEWSPIDER_MODULE = "apps.parse.readmanga"
-
-DOWNLOADER_MIDDLEWARES = {
- "apps.parse.scrapy.middleware.ErrbackMiddleware": 340,
- "apps.parse.scrapy.middleware.ProxyMiddleware": 350,
- "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,
-}
-ITEM_PIPELINES = {
- ReadmangaPipeline: 300,
-}
-
-LOG_FILE = "parse-readmanga.log"
diff --git a/apps/parse/scrapy/base_settings.py b/apps/parse/scrapy/base_settings.py
index 35b67915..39ae560a 100644
--- a/apps/parse/scrapy/base_settings.py
+++ b/apps/parse/scrapy/base_settings.py
@@ -1,5 +1,6 @@
import logging
+REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 2
diff --git a/apps/parse/scrapy/items.py b/apps/parse/scrapy/items.py
index da0919f1..34b1ba21 100644
--- a/apps/parse/scrapy/items.py
+++ b/apps/parse/scrapy/items.py
@@ -2,33 +2,36 @@
class MangaItem(Item):
+ identifier = Field()
+ popularity = Field()
+
title = Field()
- alt_title = Field()
+ description = Field()
rating = Field()
thumbnail = Field()
image = Field()
- description = Field()
status = Field()
year = Field()
source_url = Field()
- rss_url = Field()
+ chapters_url = Field()
# FKs
chapters = Field()
# M2Ms
genres = Field()
categories = Field()
+ # M2M persons
authors = Field()
translators = Field()
illustrators = Field()
screenwriters = Field()
- # Other
- updated_detail = Field()
-class MangaChapterItem(Item):
- title = Field()
- volume = Field()
- number = Field()
- link = Field()
+class ChapterItem(Item):
+ chapters = Field()
# Meta
- manga_rss_url = Field()
+ chapters_url = Field()
+
+
+class ImagesItem(Item):
+ chapter_url = Field()
+ images = Field()
diff --git a/apps/parse/scrapy/middleware.py b/apps/parse/scrapy/middleware.py
index 3a1de63a..0961a566 100644
--- a/apps/parse/scrapy/middleware.py
+++ b/apps/parse/scrapy/middleware.py
@@ -2,20 +2,25 @@
from django.conf import settings
from scrapy.http import Request
+from scrapy.spidermiddlewares.httperror import HttpError
from scrapy.spiders import Spider
-from twisted.python.failure import Failure
-def errback(spider: Spider, failure: Failure = None, *args, **kwargs):
- spider.logger.warning(f"Error on {failure.response.url}\n{repr(failure)}")
+class ErrorLoggerMiddleware(object):
+ @staticmethod
+ def error_logger(spider: Spider, failure: HttpError, *args, **kwargs): # noqa
+ if hasattr(failure, "response"):
+ url = failure.response.url
+ else:
+ url = failure.request.url # noqa
+ spider.logger.warning(f"Error on {url}\n{repr(failure)}")
-
-class ErrbackMiddleware(object):
def process_request(self, request: Request, spider: Spider, **_):
if not request.errback:
- request.errback = partial(errback, spider)
+ request.errback = partial(self.error_logger, spider)
class ProxyMiddleware(object):
- def process_request(self, request: Request, **_):
+ @staticmethod
+ def process_request(request: Request, **_):
request.meta["proxy"] = settings.PROXY
diff --git a/apps/parse/scrapy/pipeline.py b/apps/parse/scrapy/pipeline.py
new file mode 100644
index 00000000..13851a20
--- /dev/null
+++ b/apps/parse/scrapy/pipeline.py
@@ -0,0 +1,47 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+from django.core.cache import caches
+
+from apps.parse.types import CacheType
+
+
+class BasePipeline:
+ pass
+
+
+class CachedPipeline(BasePipeline, ABC):
+ timeout = 0
+ convert = True
+ type: str
+
+ @abstractmethod
+ def get_cache_key(self, data) -> str:
+ ...
+
+ @abstractmethod
+ def process(self, item, spider) -> Any:
+ return
+
+ def convert_data(self, data) -> Any:
+ ...
+
+ def process_item(self, item, spider) -> Any:
+ res = self.process(item, spider)
+ self.save_to_cache(item, spider)
+ return res
+
+ def save_to_cache(self, data, spider):
+ """Save item to cache."""
+ spider.logger.info("Saving data to cache")
+
+ key = self.get_cache_key(data)
+ if self.__class__.convert:
+ data = self.convert_data(data)
+
+ cache = caches[CacheType.from_parser_type(self.__class__.type)]
+ cache.set(key, data, timeout=self.__class__.timeout)
+
+
+class ChapterPipeline(CachedPipeline, ABC):
+ pass
diff --git a/apps/parse/scrapy/spider.py b/apps/parse/scrapy/spider.py
index 7237c993..9cb0259b 100644
--- a/apps/parse/scrapy/spider.py
+++ b/apps/parse/scrapy/spider.py
@@ -1,7 +1,14 @@
-class InjectUrlMixin:
- def __init__(self, *args, **kwargs):
- url = kwargs.pop("url", None)
- if not getattr(self.__class__, "start_urls", None) and url:
- super().__init__(*args, start_urls=[url])
- else:
- super().__init__(*args, **kwargs)
+import logging
+from abc import ABC
+
+import scrapy
+
+
+class RegisteredSpider(scrapy.Spider, ABC):
+ type: str
+
+
+class BaseSpider(RegisteredSpider, ABC):
+ @property
+ def logger(self):
+ return logging.getLogger("scrapyscript")
diff --git a/apps/parse/scrapy/utils.py b/apps/parse/scrapy/utils.py
deleted file mode 100644
index b75d3f85..00000000
--- a/apps/parse/scrapy/utils.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from crochet import run_in_reactor, setup
-from scrapy.crawler import CrawlerProcess, CrawlerRunner
-from scrapy.utils.project import get_project_settings
-
-from apps.parse.const import CATALOGUES
-
-
-def run(spider, *args, **kwargs):
- runner = CrawlerRunner(get_project_settings())
- return runner.crawl(spider, *args, **kwargs)
-
-
-def run_parser(parser_type: str, catalogue_name: str = "readmanga", url: str = None):
- catalogue = CATALOGUES[catalogue_name]
- spider, wait_timeout = catalogue["parsers"][parser_type]
-
- if wait_timeout:
- setup()
- d = run_in_reactor(run)(spider, url=url)
- d.wait(wait_timeout)
- else:
- # Don't use reactor for spiders without timeout
- runner = CrawlerProcess(get_project_settings())
- runner.crawl(spider, url=url)
- runner.start()
diff --git a/apps/parse/tasks.py b/apps/parse/tasks.py
new file mode 100644
index 00000000..4b08b45d
--- /dev/null
+++ b/apps/parse/tasks.py
@@ -0,0 +1,132 @@
+import logging
+
+import django_rq
+import requests
+from django.conf import settings
+from django.core.cache import caches
+from django_rq import job
+from scrapy.utils.project import get_project_settings
+from scrapyscript import Job, Processor
+
+from apps.manga.models import Chapter, Manga, SaveListMangaThrough
+from apps.mangachan import Mangachan
+from apps.parse.catalogue import Catalogue
+from apps.parse.exceptions import ParsingError, to_error_schema
+from apps.parse.types import CacheType, ParserType, ParsingStatus
+from apps.readmanga import Readmanga
+
+logger = logging.getLogger("scrapyscript")
+
+
+@job
+def run_spider_task(parser_type: str, catalogue_name: str = "readmanga", url: str = None):
+ catalogue = Catalogue.from_name(catalogue_name)
+ spider = catalogue.from_parser_name(parser_type)
+ cache = None
+
+ j = Job(spider, url=url)
+ p = Processor(settings=get_project_settings())
+
+ if url:
+ cache = caches[CacheType.from_parser_type(parser_type)]
+ cache.set(url, ParsingStatus.parsing)
+
+ p.run(j)
+
+ if p.errors:
+ errors = [f"{str(cls)}={val}" for cls, val in p.errors]
+ msg = "Parsing failed with errors:\n" + "\n".join(errors)
+ if url:
+ cache.set(url, to_error_schema(msg))
+ raise ParsingError(msg)
+
+ if url:
+ cache_res = cache.get(url)
+ if cache_res == ParsingStatus.parsing.value:
+ msg = "Parsing failed but returned no errors, please, try again later."
+ cache.set(url, to_error_schema(msg))
+ raise ParsingError(msg)
+
+
+def parse_manga_deep(manga_id: int, parse_images=True):
+ # TODO: Add cache checks
+ manga = Manga.objects.get(id=manga_id)
+
+ # Details
+ logger.info(f"Running detail parser for {manga}")
+ run_spider_task(ParserType.detail, "readmanga", url=manga.source_url)
+ manga.refresh_from_db()
+
+ # Chapters
+ logger.info("Running chapter parser")
+ run_spider_task(ParserType.chapter, "readmanga", url=manga.chapters_url)
+
+ if not parse_images:
+ logger.info("Skipping further steps as parse_images is set to False")
+ return
+
+ # Get images from middle chapter
+ chapters = manga.chapters.all()
+ chapters_len = len(chapters)
+
+ if not chapters:
+ logger.warning(f"No chapters found for {manga}")
+ return
+
+ middle_chapter: Chapter = chapters[chapters_len // 2]
+ logger.info(f"Running image parser for {middle_chapter}")
+ run_spider_task(ParserType.image, "readmanga", url=middle_chapter.link)
+
+ # Get middle image size
+ images = caches[CacheType.image].get(middle_chapter.link)
+ images_len = len(images)
+
+ if not images:
+ logger.warning(f"No images found chapter {middle_chapter}")
+ return
+
+ middle_image = images[images_len // 2]
+ image_response = requests.get(middle_image)
+
+ chapter_size_kb = len(image_response.content) / 1024 * images_len
+ manga_size_kb = chapter_size_kb * chapters_len
+
+ if not manga_size_kb > settings.MANGA_MAX_SIZE_KB:
+ logger.info(f"Manga size is ok (~{manga_size_kb // 1024}MB)")
+
+ # Remove middle chapter from parsing
+ del chapters[chapters_len // 2]
+ chapters_len -= 1
+
+ for num, chapter in enumerate(chapters, 1):
+ logger.info(f"Running image parser for chapter {num}/{chapters_len}")
+ run_spider_task(ParserType.image, "readmanga", url=chapter.link)
+
+
+@job
+def update_lists_details():
+ manga_ids = SaveListMangaThrough.objects.values_list("manga_id", flat=True).distinct()
+ ids_len = len(manga_ids)
+ for num, manga_id in enumerate(manga_ids, 1):
+ logger.info(f"Updating deep #{manga_id} ({num}/{ids_len})")
+ parse_manga_deep(manga_id, parse_images=False)
+
+
+scheduler = django_rq.get_scheduler("default")
+scheduler.cron(
+ "0 0 * * *",
+ func=run_spider_task,
+ args=[ParserType.list, Readmanga.name],
+ queue_name="default",
+ use_local_timezone=False,
+)
+scheduler.cron(
+ "0 1 * * *",
+ func=run_spider_task,
+ args=[ParserType.list, Mangachan.name],
+ queue_name="default",
+ use_local_timezone=False,
+)
+scheduler.cron(
+ "0 2 * * *", func=update_lists_details, queue_name="default", use_local_timezone=False
+)
diff --git a/apps/parse/tests/__init__.py b/apps/parse/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/parse/tests/test_catalogue.py b/apps/parse/tests/test_catalogue.py
new file mode 100644
index 00000000..a43f482e
--- /dev/null
+++ b/apps/parse/tests/test_catalogue.py
@@ -0,0 +1,44 @@
+import functools
+
+from django.test import TestCase
+
+from apps.parse.catalogue import Catalogue
+from apps.parse.types import ParserType
+
+
+def parametrize(param_list):
+ """Decorates a test case to run it as a set of subtests."""
+
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapped(self):
+ for param in param_list:
+ with self.subTest(**param):
+ f(self, **param)
+
+ return wrapped
+
+ return decorator
+
+
+catalogue_names = Catalogue.map.names
+
+
+class CatalogueTestCase(TestCase):
+ def test_every_catalogue_is_registered(self):
+ self.assertEqual(len(catalogue_names), 2)
+
+ @parametrize([dict(catalogue_name=name) for name in catalogue_names])
+ def test_every_catalogue_at_least_1_parser(self, catalogue_name):
+ self.assertTrue(Catalogue.from_name(catalogue_name).parser_map)
+
+ @parametrize([dict(catalogue_name=name) for name in catalogue_names])
+ def test_finds_list_parser(self, catalogue_name):
+ self.assertTrue(Catalogue.from_name(catalogue_name).from_parser_name(ParserType.list))
+
+ @parametrize([dict(catalogue_name=name) for name in catalogue_names])
+ def test_finds_detail_parser(self, catalogue_name):
+ self.assertTrue(Catalogue.from_name(catalogue_name).from_parser_name(ParserType.detail))
+
+ # TODO: add tests for chapter_parser in mangachan after refactor
+ # TODO: add tests for image_parser
diff --git a/apps/parse/types.py b/apps/parse/types.py
new file mode 100644
index 00000000..3c1c9d7f
--- /dev/null
+++ b/apps/parse/types.py
@@ -0,0 +1,32 @@
+from enum import Enum
+
+
+class ParserType(str, Enum):
+ list = "list"
+ detail = "detail"
+ chapter = "chapter"
+ image = "image"
+
+
+class CacheType(str, Enum):
+ detail = "detail"
+ chapter = "chapter"
+ image = "image"
+
+ @classmethod
+ def from_parser_type(cls, type_: str | ParserType) -> str:
+ return getattr(cls, type_)
+
+
+class ParsingStatus(str, Enum):
+ """Parsing status ENUM."""
+
+ parsing = "parsing"
+ up_to_date = "upToDate"
+
+
+# Scraped data type
+ParsingResult = dict | list
+
+# Possible cache values. Either the status of the data itself
+ParsingCacheValue = ParsingStatus | ParsingResult
diff --git a/apps/parse/utils.py b/apps/parse/utils.py
deleted file mode 100644
index 44cd3f31..00000000
--- a/apps/parse/utils.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import logging
-from datetime import datetime
-
-from django.db.models import Q
-from django.utils import timezone
-
-from apps.core.fast import FastQuerySet
-from apps.parse.const import SOURCE_TO_CATALOGUE_MAP
-from apps.parse.models import Manga, Person, PersonRelatedToManga
-
-
-def fast_annotate_manga_query(query: FastQuerySet) -> FastQuerySet:
- return query.map(source=("source_url__startswith", SOURCE_TO_CATALOGUE_MAP)).m2m_agg(
- authors=(
- "person_relations__person__name",
- Q(person_relations__role="author"),
- ),
- screenwriters=(
- "person_relations__person__name",
- Q(person_relations__role="screenwriter"),
- ),
- illustrators=(
- "person_relations__person__name",
- Q(person_relations__role="illustrator"),
- ),
- translators=(
- "person_relations__person__name",
- Q(person_relations__role="translator"),
- ),
- genres="genres__name",
- categories="categories__name",
- )
-
-
-def needs_update(updated_detail: str):
- updated_detail = datetime.fromisoformat(updated_detail)
- if updated_detail:
- update_deadline = updated_detail + Manga.BASE_UPDATE_FREQUENCY
- if timezone.now() >= update_deadline:
- return True
- return False
-
-
-def save_persons(manga, role, persons):
- INSTANCE = 0
- PeopleRelated: PersonRelatedToManga = manga.people_related.through
- PeopleRelated.objects.filter(role=role, manga=manga).delete()
- PeopleRelated.objects.bulk_create(
- [
- PeopleRelated(
- person=Person.objects.get_or_create(name=person)[INSTANCE],
- manga=manga,
- role=role,
- )
- for person in persons
- ],
- ignore_conflicts=True,
- )
-
-
-def mute_logger_stdout(logger_name: str, *other_loggers):
- import warnings
-
- logger_names = [logger_name, *other_loggers]
- for name in logger_names:
- warnings.filterwarnings("ignore", module=name)
- logger = logging.getLogger(name)
- logger.setLevel(logging.CRITICAL)
- logger.propagate = False
diff --git a/apps/readmanga/__init__.py b/apps/readmanga/__init__.py
new file mode 100644
index 00000000..5b781ba4
--- /dev/null
+++ b/apps/readmanga/__init__.py
@@ -0,0 +1,22 @@
+from apps.parse.catalogue import Catalogue
+
+
+class Readmanga(Catalogue):
+ name = "readmanga"
+ source = "https://readmanga.live"
+ settings = "apps.readmanga.settings"
+
+
+# TODO: create multi-catalogue or port this with inheritance or something
+# "mintmanga": {
+# "source": "https://mintmanga.live",
+# "settings": "apps.readmanga.settings",
+# },
+# "selfmanga": {
+# "source": "https://selfmanga.live",
+# "settings": "apps.readmanga.settings",
+# },
+# "rumix": {
+# "source": "https://rumix.me",
+# "settings": "apps.readmanga.settings",
+# }
diff --git a/apps/readmanga/apps.py b/apps/readmanga/apps.py
new file mode 100644
index 00000000..ddf2fd64
--- /dev/null
+++ b/apps/readmanga/apps.py
@@ -0,0 +1,12 @@
+from django.apps import AppConfig
+
+
+class Config(AppConfig):
+ name = "apps.readmanga"
+
+ def ready(self):
+ from . import Readmanga # noqa
+ from .chapter import ReadmangaChapterSpider # noqa
+ from .detail import ReadmangaDetailSpider # noqa
+ from .image import ReadmangaImageSpider # noqa
+ from .list import ReadmangaListSpider # noqa
diff --git a/apps/readmanga/chapter.py b/apps/readmanga/chapter.py
new file mode 100644
index 00000000..8a57dab1
--- /dev/null
+++ b/apps/readmanga/chapter.py
@@ -0,0 +1,43 @@
+import re
+
+from scrapy.http import XmlResponse
+
+from apps.parse.scrapy.items import ChapterItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+from apps.readmanga import Readmanga
+
+ITEM_TAG = "//item"
+LINK_TAG = 'guid[@isPermaLink="true"]/text()'
+TITLE_TAG = ".//title/text()"
+
+
+@Readmanga.register(ParserType.chapter)
+class ReadmangaChapterSpider(BaseSpider):
+ custom_settings = {"ITEM_PIPELINES": {"apps.readmanga.pipelines.ReadmangaChapterPipeline": 300}}
+
+ def parse(self, response: XmlResponse, **kwargs) -> ChapterItem:
+ chapters = []
+
+ items = response.xpath(ITEM_TAG)
+ for item in items:
+ link = item.xpath(LINK_TAG).extract_first()
+
+ chapter_title = item.xpath(TITLE_TAG).extract_first()
+
+ res_reg = re.search(r":\s*(.*)$", chapter_title)
+ if res_reg:
+ chapter_title = res_reg.group(1)
+
+ volume, number = link.split("/")[-2:]
+ volume = int(volume.replace("vol", ""))
+
+ chapters.append(
+ dict(
+ title=chapter_title,
+ volume=volume,
+ number=number,
+ link=link,
+ )
+ )
+ return ChapterItem(chapters=chapters, chapters_url=response.url)
diff --git a/apps/readmanga/detail.py b/apps/readmanga/detail.py
new file mode 100644
index 00000000..8223908d
--- /dev/null
+++ b/apps/readmanga/detail.py
@@ -0,0 +1,61 @@
+from scrapy.http import HtmlResponse
+
+from apps.core.utils import url_prefix
+from apps.parse.scrapy.items import MangaItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+from apps.readmanga import Readmanga
+
+_identifier = "//span[contains(@class, 'rating-block')]/@data-subject-id"
+
+_description = "//meta[@itemprop='description'][1]/@content"
+_chapters_url = "//head/link[@type='application/rss+xml'][1]/@href"
+_year = "//span[@class='elem_year']/a[@class='element-link'][1]/text()"
+_rating = "//span[@class='rating-block']/@data-score"
+
+_genres = '//span[@class = "elem_genre "]/a[@class="element-link"]/text()'
+_categories = '//span[@class = "elem_category "]/a[@class="element-link"]/text()'
+
+_authors = "//span[@class='elem_author']/a[@class='person-link']/text()"
+_translators = "//span[@class='elem_translator']/a[@class='person-link']/text()"
+_illustrators = '//span[@class = "elem_illustrator"]/a[@class="person-link"]/text()'
+_screenwriters = '//span[@class="elem_screenwriter"]/a[@class="person-link"]/text()'
+
+
+@Readmanga.register(ParserType.detail)
+class ReadmangaDetailSpider(BaseSpider):
+ def parse(self, response: HtmlResponse, **kwargs):
+ identifier = response.xpath(_identifier).extract_first()
+
+ description = response.xpath(_description).extract_first("")
+ chapters_url = response.xpath(_chapters_url).extract_first("")
+ year = response.xpath(_year).extract_first("")
+ rating = response.xpath(_rating).extract_first(0.0)
+
+ genres = response.xpath(_genres).extract()
+ categories = response.xpath(_categories).extract()
+
+ authors = response.xpath(_authors).extract()
+ screenwriters = response.xpath(_screenwriters).extract()
+ translators = response.xpath(_translators).extract()
+ illustrators = response.xpath(_illustrators).extract()
+
+ if chapters_url:
+ chapters_url = url_prefix(self.start_urls[0]) + chapters_url
+
+ return [
+ MangaItem(
+ identifier=identifier,
+ description=description,
+ source_url=response.url,
+ chapters_url=chapters_url,
+ year=year,
+ rating=rating,
+ genres=genres,
+ categories=categories,
+ authors=authors,
+ translators=translators,
+ illustrators=illustrators,
+ screenwriters=screenwriters,
+ )
+ ]
diff --git a/apps/readmanga/image.py b/apps/readmanga/image.py
new file mode 100644
index 00000000..ac774310
--- /dev/null
+++ b/apps/readmanga/image.py
@@ -0,0 +1,28 @@
+import re
+
+from orjson import loads
+from scrapy.http import HtmlResponse
+
+from apps.parse.exceptions import ParsingError
+from apps.parse.scrapy.items import ImagesItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+from apps.readmanga import Readmanga
+
+COUNT_LINK_ELEMENTS = 3
+
+
+@Readmanga.register(ParserType.image)
+class ReadmangaImageSpider(BaseSpider):
+ custom_settings = {"ITEM_PIPELINES": {"apps.readmanga.pipelines.ReadmangaImagePipeline": 300}}
+
+ def parse(self, response: HtmlResponse, **kwargs):
+ images = re.search(r"rm_h.initReader\(.*(\[{2}.*]{2}).*\)", response.text)
+ if not images:
+ raise ParsingError("No image list was found")
+
+ image_links = [
+ "".join(image[:COUNT_LINK_ELEMENTS])
+ for image in loads(images.group(1).replace("'", '"'))
+ ]
+ return ImagesItem(chapter_url=self.start_urls[0], images=image_links)
diff --git a/apps/readmanga/list.py b/apps/readmanga/list.py
new file mode 100644
index 00000000..09ea60a9
--- /dev/null
+++ b/apps/readmanga/list.py
@@ -0,0 +1,79 @@
+import re
+from typing import List
+
+from scrapy.http import HtmlResponse
+from scrapy.linkextractors import LinkExtractor
+from scrapy.spiders.crawl import CrawlSpider, Rule
+
+from apps.core.utils import url_prefix
+from apps.parse.scrapy.items import MangaItem
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import ParserType
+from apps.readmanga import Readmanga
+
+_manga_tile = '//div[@class = "tiles row"]//div[contains(@class, "tile col-md-6")]'
+_identifier = '//div[contains(@class, "tile col-md-6 ")]/@class'
+
+_title = "//h3/a[1]/@title"
+_thumbnail = '//img[contains(@class, "lazy")][1]/@data-original'
+_source_url = "//h3/a[1]/@href"
+_rating = '//div[@class = "compact-rate"]/@title'
+_genres = '//div[@class = "tile-info"]//a[contains(@class, "badge")]/text()'
+
+
+@Readmanga.register(ParserType.list, url=False)
+class ReadmangaListSpider(CrawlSpider, BaseSpider):
+ start_urls = [f"{Readmanga.source}/list?sortType=rate"]
+ rules = [
+ Rule(
+ LinkExtractor(restrict_xpaths=["//a[@class='nextLink']"]), follow=True, callback="parse"
+ )
+ ]
+ custom_settings = {
+ "DEPTH_LIMIT": 400,
+ }
+
+ def parse_start_url(self, response, **kwargs):
+ return self.parse(response)
+
+ def parse(self, response, **kwargs):
+ mangas: List[MangaItem] = []
+
+ url_offset = re.findall(r"offset=(\d+)", response.url)
+ offset = int(url_offset[-1]) if url_offset else 0
+
+ descriptions = response.xpath(_manga_tile).extract()
+ for popularity, description in enumerate(descriptions, offset + 1):
+ response = HtmlResponse(url="", body=description, encoding="utf-8")
+
+ identifier = response.xpath(_identifier).extract_first()
+
+ title = response.xpath(_title).extract_first("")
+ thumbnail = response.xpath(_thumbnail).extract_first("")
+ source_url = response.xpath(_source_url).extract_first("")
+ rating = response.xpath(_rating).extract_first("")
+ genres = response.xpath(_genres).extract()
+
+ # Post-processing
+ identifier = re.match(r".*el_(\d+).*", identifier).group(1)
+ image = thumbnail.replace("_p", "")
+
+ if not source_url.startswith("http"):
+ source_url = url_prefix(self.start_urls[0]) + source_url
+
+ mangas.append(
+ MangaItem(
+ identifier=identifier,
+ popularity=popularity,
+ title=title,
+ thumbnail=thumbnail,
+ image=image,
+ source_url=source_url,
+ rating=rating,
+ genres=genres,
+ )
+ )
+ self.logger.info('Parsed manga "{}"'.format(title))
+
+ self.logger.info("===================")
+ return mangas
diff --git a/apps/readmanga/pipelines.py b/apps/readmanga/pipelines.py
new file mode 100644
index 00000000..218029cd
--- /dev/null
+++ b/apps/readmanga/pipelines.py
@@ -0,0 +1,174 @@
+import logging
+from copy import deepcopy
+from typing import List, Tuple, Type
+
+from django.db import transaction
+from scrapy.spiders import Spider
+
+from apps.core.abc.models import BaseModel
+from apps.manga.annotate import manga_to_annotated_dict
+from apps.manga.api.notifications.utils import notify_about_chapter
+from apps.manga.api.schemas import ChapterListOut, ImageListOut, MangaOut
+from apps.manga.models import Category, Chapter, Genre, Manga, PersonRelatedToManga, PersonRole
+from apps.parse.cleaning import normalized_category_names, without_common_prefix
+from apps.parse.scrapy.items import ChapterItem, ImagesItem
+from apps.parse.scrapy.pipeline import CachedPipeline
+from apps.parse.scrapy.spider import BaseSpider
+from apps.parse.types import CacheType, ParserType, ParsingStatus
+from apps.readmanga.chapter import ReadmangaChapterSpider
+from apps.readmanga.image import ReadmangaImageSpider
+
+logger = logging.getLogger("scrapy")
+
+
+@transaction.atomic
+def bulk_get_or_create(cls: Type[BaseModel], values: List[str], keyword: str = "name") -> Tuple:
+ objects = []
+ for value in values:
+ objects.append(cls.objects.get_or_create(**{keyword: value}))
+ return tuple(obj for obj, _ in objects)
+
+
+class ReadmangaImagePipeline(CachedPipeline):
+ timeout = 8 * 3600
+ type = CacheType.image
+
+ def get_cache_key(self, data: ImagesItem) -> str:
+ return data["chapter_url"]
+
+ def convert_data(self, data: ImagesItem):
+ return ImageListOut(status=ParsingStatus.up_to_date, data=data["images"])
+
+ def process(self, item: ImagesItem, spider: ReadmangaImageSpider):
+ # No need to process, just save to cache
+ pass
+
+
+class ReadmangaChapterPipeline(CachedPipeline):
+ timeout = 3600
+ type = CacheType.chapter
+
+ def get_cache_key(self, data: ChapterItem) -> str:
+ return data["chapters_url"]
+
+ def convert_data(self, data: ChapterItem):
+ return ChapterListOut(
+ status=ParsingStatus.up_to_date.value,
+ data=data["chapters"],
+ )
+
+ def process(self, item, spider):
+ # No need to process since we're overriding process_item and manually calling save_to_cache
+ pass
+
+ def process_item(self, chapters_data: ChapterItem, spider: ReadmangaChapterSpider):
+ chapter_list, chapters_url = chapters_data.values()
+ manga = Manga.objects.get(chapters_url=chapters_url)
+
+ latest_chapter = None
+ if manga.chapters.exists():
+ latest_chapter = manga.chapters.latest("id")
+
+ clean_chapter_titles = without_common_prefix([c["title"] for c in chapter_list])
+
+ chapter_list = self.bulk_get_or_create(
+ [
+ {**c, "title": title, "manga": manga}
+ for title, c in zip(clean_chapter_titles, chapter_list)
+ ]
+ )
+
+ # Create Notifications only if chapters were already parsed once
+ if latest_chapter:
+ new_chapters = [chapter for chapter in chapter_list if chapter.id > latest_chapter.id]
+ for new_chapter in new_chapters:
+ notify_about_chapter(new_chapter)
+
+ chapter = {"chapters_url": chapters_url, "chapters": chapter_list}
+ self.save_to_cache(chapter, spider)
+
+ return chapter
+
+ @staticmethod
+ @transaction.atomic
+ def bulk_get_or_create(chapters: List[dict]) -> List[Chapter]:
+ objects: List[Chapter, ...] = []
+ for chapter in chapters:
+ objects.append(Chapter.objects.get_or_create(**chapter))
+ return [obj for obj, _ in objects]
+
+
+class ReadmangaPipeline(CachedPipeline):
+ timeout = 7 * 24 * 3600
+ type = CacheType.detail
+
+ def get_cache_key(self, data: Manga) -> str:
+ return data.source_url
+
+ def convert_data(self, data: Manga):
+ return MangaOut(
+ status=ParsingStatus.up_to_date.value,
+ data=manga_to_annotated_dict(data),
+ )
+
+ def process(self, item, spider):
+ # Once again, manual cache call
+ pass
+
+ def process_item(self, item: dict, spider: BaseSpider) -> dict:
+ # spider.logger.info(f"Processing item {item}")
+ data = deepcopy(item)
+
+ if "title" not in data and spider.type == ParserType.list.value:
+ message = f"Error processing {data}: No title name was set"
+ spider.logger.error(message)
+ raise ValueError(message)
+
+ genres = data.pop("genres", [])
+ authors = data.pop("authors", [])
+ illustrators = data.pop("illustrators", [])
+ screenwriters = data.pop("screenwriters", [])
+ translators = data.pop("translators", [])
+ categories = data.pop("categories", [])
+
+ manga = self.get_or_create_or_update_manga(spider, data.pop("identifier"), **data)
+
+ # TODO: move it to model, like for save_persons use atomic transaction
+ genres = bulk_get_or_create(Genre, normalized_category_names(genres))
+ manga.genres.clear()
+ manga.genres.add(*genres)
+
+ categories = bulk_get_or_create(Category, normalized_category_names(categories))
+ manga.categories.clear()
+ manga.categories.add(*categories)
+
+ PersonRelatedToManga.save_persons(manga, PersonRole.author, authors)
+ PersonRelatedToManga.save_persons(manga, PersonRole.illustrator, illustrators)
+ PersonRelatedToManga.save_persons(manga, PersonRole.screenwriter, screenwriters)
+ PersonRelatedToManga.save_persons(manga, PersonRole.translator, translators)
+
+ spider.logger.info(f"Saved manga {manga}")
+
+ if not spider.type == ParserType.list.value:
+ self.save_to_cache(manga, spider)
+
+ return item
+
+ @staticmethod
+ def get_or_create_or_update_manga(spider: Spider, identifier, **data) -> Manga:
+ """Explicit is better than implicit."""
+ matching_query = Manga.objects.filter(identifier=identifier)
+
+ if matching_query.exists():
+ spider.logger.info(f"Manga exists, updating with {data}")
+ matching_query.update(**data)
+ manga = matching_query.first()
+ spider.logger.info(f'Updated item "{manga}"')
+ else:
+ manga = Manga.objects.create(
+ identifier=identifier,
+ **data,
+ )
+ spider.logger.info(f'Created item "{manga}"')
+
+ return manga
diff --git a/apps/readmanga/settings.py b/apps/readmanga/settings.py
new file mode 100644
index 00000000..96e6fc44
--- /dev/null
+++ b/apps/readmanga/settings.py
@@ -0,0 +1,28 @@
+import os
+
+import django
+
+os.environ["DJANGO_SETTINGS_MODULE"] = "manga_reader.settings" # noqa
+django.setup() # noqa
+
+from apps.parse.scrapy.base_settings import * # noqa
+from apps.readmanga.pipelines import ReadmangaPipeline # noqa
+
+BOT_NAME = "readmanga"
+SPIDER_MODULES = [
+ "apps.readmanga.list",
+ "apps.readmanga.detail",
+ "apps.readmanga.chapter",
+ "apps.readmanga.image",
+]
+# Remove for now as GAE workspace is immutable
+LOG_FILE = "parse-readmanga.log"
+
+DOWNLOADER_MIDDLEWARES = {
+ "apps.parse.scrapy.middleware.ErrorLoggerMiddleware": 340,
+ "apps.parse.scrapy.middleware.ProxyMiddleware": 350,
+ "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,
+}
+ITEM_PIPELINES = {
+ ReadmangaPipeline: 300,
+}
diff --git a/apps/settings/__init__.py b/apps/settings/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/settings/admin.py b/apps/settings/admin.py
new file mode 100644
index 00000000..846f6b40
--- /dev/null
+++ b/apps/settings/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/apps/settings/apps.py b/apps/settings/apps.py
new file mode 100644
index 00000000..f9d05cd8
--- /dev/null
+++ b/apps/settings/apps.py
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class SettingsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "apps.settings"
+
+ def ready(self):
+ from . import signals # noqa
diff --git a/apps/settings/migrations/__init__.py b/apps/settings/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/settings/models.py b/apps/settings/models.py
new file mode 100644
index 00000000..a290ae81
--- /dev/null
+++ b/apps/settings/models.py
@@ -0,0 +1,9 @@
+from django.contrib.postgres.fields import JSONField
+from django.db.models import CASCADE, ForeignKey
+
+from apps.core.abc.models import BaseModel
+
+
+class UserPreferences(BaseModel):
+ user = ForeignKey("auth.User", null=False, blank=False, on_delete=CASCADE)
+ catalogues = JSONField(default={})
diff --git a/apps/settings/signals.py b/apps/settings/signals.py
new file mode 100644
index 00000000..cd621d04
--- /dev/null
+++ b/apps/settings/signals.py
@@ -0,0 +1,10 @@
+from django.contrib.auth.models import User
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from apps.settings.models import UserPreferences
+
+
+@receiver(post_save, sender=User)
+def create_prefs_handler(instance, **kwargs): # noqa
+ UserPreferences.objects.create(user=instance)
diff --git a/apps/typesense_bind/__init__.py b/apps/typesense_bind/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/typesense_bind/apps.py b/apps/typesense_bind/apps.py
new file mode 100644
index 00000000..b13f437f
--- /dev/null
+++ b/apps/typesense_bind/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class TypesenseBindConfig(AppConfig):
+ name = "apps.typesense_bind"
+
+ def ready(self):
+ from . import signals # noqa
diff --git a/apps/typesense_bind/client.py b/apps/typesense_bind/client.py
new file mode 100644
index 00000000..b07fc6b7
--- /dev/null
+++ b/apps/typesense_bind/client.py
@@ -0,0 +1,22 @@
+import typesense
+from django.conf import settings
+
+
+def create_client(host, key, protocol, port=8108):
+ return typesense.Client(
+ {
+ "nodes": [
+ {
+ "host": host,
+ "port": port if protocol != "https" else "443",
+ "protocol": protocol,
+ },
+ ],
+ "api_key": key,
+ "connection_timeout_seconds": 20,
+ }
+ )
+
+
+def get_ts_client():
+ return settings.TS_CLIENT
diff --git a/apps/typesense_bind/functions.py b/apps/typesense_bind/functions.py
new file mode 100644
index 00000000..9882896f
--- /dev/null
+++ b/apps/typesense_bind/functions.py
@@ -0,0 +1,7 @@
+from typing import List
+
+from apps.typesense_bind.schema import schema_name
+
+
+def upsert_collection(client, data: List[dict]):
+ return client.collections[schema_name].documents.import_(data, {"action": "upsert"})
diff --git a/apps/typesense_bind/management/commands/rebuild_index.py b/apps/typesense_bind/management/commands/rebuild_index.py
new file mode 100644
index 00000000..9b60e0bd
--- /dev/null
+++ b/apps/typesense_bind/management/commands/rebuild_index.py
@@ -0,0 +1,35 @@
+
+from django.core.management.base import BaseCommand
+from typesense.exceptions import ObjectNotFound
+
+from apps.manga.annotate import fast_annotate_manga_query
+from apps.manga.models import Manga
+from apps.typesense_bind.client import get_ts_client
+from apps.typesense_bind.functions import upsert_collection
+from apps.typesense_bind.schema import schema, schema_name
+
+
+class Command(BaseCommand):
+ def handle(self, *args, **options):
+ client = get_ts_client()
+
+ try:
+ client.collections[schema_name].delete()
+ except ObjectNotFound:
+ pass
+
+ client.collections.create(schema)
+ self.stdout.write("Recreated collection")
+
+ mangas = fast_annotate_manga_query(Manga.objects.all())
+
+ start = 0
+ step = 1000
+ end = len(mangas)
+ inserted = 0
+ self.stdout.write(f"Importing {end} documents")
+ for chunk_start in range(start, end, step):
+ inserted += len(upsert_collection(client, mangas[chunk_start:chunk_start + step]))
+ self.stdout.write(f'=> imported {inserted} documents')
+
+ self.stdout.write(f"finished importing {inserted} documents")
diff --git a/apps/typesense_bind/query.py b/apps/typesense_bind/query.py
new file mode 100644
index 00000000..6abcb94b
--- /dev/null
+++ b/apps/typesense_bind/query.py
@@ -0,0 +1,36 @@
+from typing import List
+
+from django.db.models import Case, IntegerField, When
+from typesense.documents import Documents
+
+from apps.manga.models import Manga
+from apps.typesense_bind.client import get_ts_client
+from apps.typesense_bind.schema import schema_name
+
+
+def get_query_base() -> Documents:
+ return get_ts_client().collections[schema_name].documents
+
+
+def query_models_by_title(title: str):
+ search_parameters = {
+ "include_fields": ["id"],
+ "q": title,
+ "query_by": "title",
+ }
+
+ res = get_query_base().search(search_parameters)["hits"]
+ pks = [r["document"]["id"] for r in res]
+
+ preserved_order = Case(
+ *[When(pk=pk, then=pos) for pos, pk in enumerate(pks)], output_field=IntegerField()
+ )
+ return Manga.objects.filter(pk__in=pks).order_by(preserved_order)
+
+
+def query_dict_list_by_title(title: str) -> List[dict]:
+ search_parameters = {
+ "q": title,
+ "query_by": "title",
+ }
+ return [hit["document"] for hit in get_query_base().search(search_parameters)["hits"]]
diff --git a/apps/typesense_bind/schema.py b/apps/typesense_bind/schema.py
new file mode 100644
index 00000000..c45a1e92
--- /dev/null
+++ b/apps/typesense_bind/schema.py
@@ -0,0 +1,15 @@
+from apps.typesense_bind.client import get_ts_client
+
+schema_name = "mangas"
+schema = {
+ "name": schema_name,
+ "fields": [
+ # There's only title since we only search by it
+ # In reality there's more data in each doc
+ {"name": "title", "type": "string", "locale": "ru"},
+ ],
+}
+
+
+def upsert_schema():
+ get_ts_client()
diff --git a/apps/typesense_bind/signals.py b/apps/typesense_bind/signals.py
new file mode 100644
index 00000000..169a8684
--- /dev/null
+++ b/apps/typesense_bind/signals.py
@@ -0,0 +1,17 @@
+from django.contrib.auth import get_user_model
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from apps.manga.annotate import manga_to_annotated_dict
+from apps.manga.models import Manga
+from apps.typesense_bind.client import get_ts_client
+from apps.typesense_bind.functions import upsert_collection
+
+User = get_user_model()
+
+
+@receiver(post_save, sender=Manga)
+def update_collection(instance: Manga, **kwargs): # noqa
+ client = get_ts_client()
+ data = manga_to_annotated_dict(instance)
+ upsert_collection(client, [data])
diff --git a/docker-compose.yml b/docker-compose.yml
index c406c13b..f5061dfd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,40 +1,111 @@
-# Compose for development using volumes to ensure hot reload
-version: "3"
-
services:
web:
+ profiles:
+ - prod
+ build:
+ context: .
+ dockerfile: docker/web/Dockerfile
+ restart: unless-stopped
+ expose:
+ - 8000
+ environment:
+ - ALLOWED_HOSTS
+ - SECRET_KEY
+ - PROXY
+ - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-sora}
+ # Static
+ - DEBUG=0
+ - HOST=0.0.0.0
+ - PORT=8000
+ - REDIS_URL=redis://redis:6379
+ - TYPESENSE_HOST=typesense
+ ports:
+ - "${PORT:-80}:8000"
+ healthcheck:
+ test: [ "CMD", "curl", "http://localhost:8000/api/health" ]
+ interval: 5s
+ timeout: 5s
+
+ web_dev:
+ profiles:
+ - dev
build:
- context: ./
- dockerfile: Dockerfile.dev
+ context: .
+ dockerfile: docker/web/Dockerfile.dev
expose:
- 8000
environment:
+ - PROXY
+ - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-sora}
+ # Static
+ - SECRET_KEY=Dev
+ - HOST=0.0.0.0
- PORT=8000
+ - REDIS_URL=redis://redis:6379
+ - TYPESENSE_HOST=typesense
ports:
- - "8888:8000"
+ - "${PORT:-8000}:8000"
volumes:
- ./:/app
- # exclude dotenvs in volume
- - /app/.envs
- env_file:
- - ./.envs/docker.env
- depends_on:
- - db
- - redis
+ - sora_poetry:/home/sora/.cache/pypoetry
+
+ rq:
+ profiles:
+ - prod
+ - prod-partial
+ build:
+ context: .
+ dockerfile: docker/web/Dockerfile.rq
+ restart: unless-stopped
+ command: poetry run python manage.py rqworker default
+ environment:
+ - PROXY
+ - SECRET_KEY
+ - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-sora}
+ # Static
+ - DEBUG=0
+ - REDIS_URL=redis://redis:6379
+ - TYPESENSE_HOST=typesense
+
+ rq_scheduler:
+ profiles:
+ - prod
+ - prod-partial
+ build:
+ context: .
+ dockerfile: docker/web/Dockerfile.rq
+ restart: unless-stopped
+ command: poetry run python manage.py rqscheduler
+ environment:
+ - PROXY
+ - SECRET_KEY
+ - DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-sora}
+ # Static
+ - DEBUG=0
+ - REDIS_URL=redis://redis:6379
+ - TYPESENSE_HOST=typesense
db:
restart: unless-stopped
- image: postgres:12
+ image: postgres:15
ports:
- "8882:5432"
volumes:
- sora_postgres:/var/lib/postgresql/data
- env_file:
- - .envs/docker.env
+ environment:
+ - POSTGRES_DB=${POSTGRES_DB:-sora}
+ - POSTGRES_USER=${POSTGRES_USER:-postgres}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
+ healthcheck:
+ # Wait for postgres to actually start before consider service 'healthy'
+ test: [ "CMD-SHELL", "pg_isready -U postgres" ]
+ interval: 5s
+ timeout: 5s
redis:
image: redis
- command: redis-server
+ # Disabled RDB snapshots
+ command: redis-server --save '' --appendonly no
restart: unless-stopped
expose:
- 6379
@@ -45,18 +116,22 @@ services:
- sora_redis:/usr/local/etc/redis/redis.conf
environment:
- REDIS_REPLICATION_MODE=master
+ healthcheck:
+ test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
+ interval: 5s
+ timeout: 5s
- elasticsearch:
- image: elasticsearch:7.14.2
+ typesense:
+ image: typesense/typesense:0.23.1
restart: unless-stopped
- environment:
- - discovery.type=single-node
+ command: --data-dir /data --api-key=${TYPESENSE_API_KEY:-api} --enable-cors
volumes:
- - esdata1:/usr/share/elasticsearch/data
+ - tsdata:/data
ports:
- - 9200:9200
+ - "8108:8108"
volumes:
- esdata1:
+ tsdata:
+ sora_poetry:
sora_redis:
sora_postgres:
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
deleted file mode 100755
index 9e154a47..00000000
--- a/docker-entrypoint.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-
-until pg_isready -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER"; do
- echo >&2 "Postgres is unavailable - sleeping"
- sleep 1
-done
-
-echo
-echo "Database is available"
-
-if [ "$DEBUG" = 0 ]; then
- echo "Production mode"
- echo "==============="
- echo "Collecting static files to /app/staticfiles"
- ./manage.py collectstatic --no-input --clear
-else
- echo "Debug mode"
- echo "=========="
-fi
-
-echo
-echo "Running migrations"
-./manage.py migrate --no-input
-
-until curl --output /dev/null --silent --head --fail "http://$ELASTICSEARCH_HOST"; do
- echo >&2 "Elasticsearch is unavailable - sleeping"
- sleep 1
-done
-echo
-echo "Rebuilding index"
-./manage.py search_index --rebuild -f
-
-PORT="${PORT:-8000}"
-echo "Running the server on port $PORT"
-
-core_count=$(grep 'cpu[0-9]+' /proc/stat | wc -l)
-gunicorn manga_reader.wsgi:application --bind $HOST:$PORT --workers $(expr $core_count \* 2 + 1)
diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile
new file mode 100644
index 00000000..a92e22a8
--- /dev/null
+++ b/docker/web/Dockerfile
@@ -0,0 +1,40 @@
+FROM python:3.10-slim as base
+
+RUN apt-get update && apt-get install -y \
+ sudo curl gcc g++ \
+ libffi-dev libpq-dev \
+ postgresql-client
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+WORKDIR /app
+
+ARG USERNAME=sora
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+# Create the user
+RUN groupadd --gid $USER_GID $USERNAME \
+ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
+ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+ && chmod 0440 /etc/sudoers.d/$USERNAME
+
+USER $USERNAME
+ENV HOME="/home/$USERNAME"
+ENV PATH="${PATH}:$HOME/.local/bin"
+
+RUN pip install --upgrade pip pipx && pipx install poetry
+COPY pyproject.toml poetry.lock ./
+RUN poetry install
+
+COPY . .
+RUN sudo chown -R $USERNAME:$USERNAME .
+
+RUN SECRET_KEY=1 poetry run python manage.py collectstatic --noinput
+
+RUN chmod +x scripts/entrypoint.sh
+CMD ["./scripts/entrypoint.sh"]
+
+EXPOSE "$PORT"
+EXPOSE 1233
\ No newline at end of file
diff --git a/docker/web/Dockerfile.dev b/docker/web/Dockerfile.dev
new file mode 100644
index 00000000..654d330f
--- /dev/null
+++ b/docker/web/Dockerfile.dev
@@ -0,0 +1,33 @@
+FROM python:3.10-slim as base
+
+RUN apt-get update && apt-get install -y \
+ sudo gcc g++ \
+ libffi-dev \
+ libpq-dev \
+ '^postgresql-client-.+$' \
+ gettext
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+WORKDIR /app
+
+ARG USERNAME=sora
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+# Create the user
+RUN groupadd --gid $USER_GID $USERNAME \
+ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
+ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+ && chmod 0440 /etc/sudoers.d/$USERNAME
+
+USER $USERNAME
+ENV HOME="/home/$USERNAME"
+ENV PATH="${PATH}:$HOME/.local/bin"
+
+RUN pip install --upgrade pip pipx && pipx install poetry
+
+EXPOSE 8000
+
+CMD ["./scripts/dev-entrypoint.sh"]
diff --git a/docker/web/Dockerfile.rq b/docker/web/Dockerfile.rq
new file mode 100644
index 00000000..8481bf05
--- /dev/null
+++ b/docker/web/Dockerfile.rq
@@ -0,0 +1,32 @@
+FROM python:3.10-slim as base
+
+RUN apt-get update && apt-get install -y \
+ sudo curl gcc g++ \
+ libffi-dev libpq-dev \
+ postgresql-client
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+WORKDIR /app
+
+ARG USERNAME=sora
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+# Create the user
+RUN groupadd --gid $USER_GID $USERNAME \
+ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
+ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+ && chmod 0440 /etc/sudoers.d/$USERNAME
+
+USER $USERNAME
+ENV HOME="/home/$USERNAME"
+ENV PATH="${PATH}:$HOME/.local/bin"
+
+RUN pip install --upgrade pip pipx && pipx install poetry
+COPY pyproject.toml poetry.lock ./
+RUN poetry install
+
+COPY . .
+RUN sudo chown -R $USERNAME:$USERNAME .
diff --git a/infra/app.yaml b/infra/app.yaml
new file mode 100644
index 00000000..d4b20e89
--- /dev/null
+++ b/infra/app.yaml
@@ -0,0 +1,5 @@
+runtime: python310
+service: backend
+env_variables:
+ DEBUG: 0
+
diff --git a/infra/cloudbuild.yaml b/infra/cloudbuild.yaml
new file mode 100644
index 00000000..3ab0d7d8
--- /dev/null
+++ b/infra/cloudbuild.yaml
@@ -0,0 +1,47 @@
+steps:
+ - name: ubuntu
+ id: poetry-export
+ waitFor: [ '-' ]
+ entrypoint: bash
+ args:
+ - '-c'
+ - |
+ apt update
+ apt install python3 python3-pip -y
+ pip3 install poetry
+ poetry export --with dev --without-hashes >| requirements.txt
+
+ - name: ubuntu
+ id: env-setup
+ waitFor: [ '-' ]
+ entrypoint: bash
+ args:
+ - '-c'
+ - |
+ printenv >| .env
+ env:
+ - 'SECRET_KEY=${_SECRET_KEY}'
+ - 'DATABASE_URL=${_DATABASE_URL}'
+ - 'REDIS_URL=${_REDIS_URL}'
+ - 'TYPESENSE_HOST=${_TYPESENSE_HOST}'
+ - 'TYPESENSE_PROTOCOL=${_TYPESENSE_PROTOCOL}'
+ - 'TYPESENSE_API_KEY=${_TYPESENSE_API_KEY}'
+ - 'GOOGLE_CLIENT=${_GOOGLE_CLIENT}'
+ - 'GOOGLE_SECRET=${_GOOGLE_SECRET}'
+ - 'SENTRY_DSN=${_SENTRY_DSN}'
+
+ - name: python
+ id: install-deps
+ waitFor: [ poetry-export ]
+ entrypoint: pip
+ args: [ "install", "-r", "requirements.txt", "--user", "--no-warn-script-location" ]
+
+ - name: python
+ id: collectstatic
+ waitFor: [ env-setup, install-deps ]
+ entrypoint: python
+ args: [ "manage.py", "collectstatic", "--noinput" ]
+
+ - name: "gcr.io/cloud-builders/gcloud"
+ waitFor: [ collectstatic ]
+ args: [ "app", "deploy", "--appyaml=infra/app.yaml" ]
diff --git a/infra/dispatch.yaml b/infra/dispatch.yaml
new file mode 100644
index 00000000..e62c417b
--- /dev/null
+++ b/infra/dispatch.yaml
@@ -0,0 +1,5 @@
+dispatch:
+ - url: "backend.sora-reader.app/*"
+ service: backend
+ - url: "sora-reader.app/*"
+ service: frontend
diff --git a/infra/hooks.yaml b/infra/hooks.yaml
new file mode 100644
index 00000000..084de763
--- /dev/null
+++ b/infra/hooks.yaml
@@ -0,0 +1,3 @@
+- id: redeploy-webhook
+ execute-command: "../scripts/redeploy-hook.sh"
+ command-working-directory: "./"
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 00000000..cbed7113
--- /dev/null
+++ b/main.py
@@ -0,0 +1,3 @@
+from manga_reader.wsgi import application
+
+app = application
diff --git a/manage.py b/manage.py
index eec977da..d72aca70 100755
--- a/manage.py
+++ b/manage.py
@@ -3,13 +3,11 @@
import os
import sys
-from dotenv import load_dotenv
-
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manga_reader.settings")
- load_dotenv(".envs/local.env")
+ os.environ.setdefault("APM_NAME", "django-backend")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
diff --git a/manga_reader/api.py b/manga_reader/api.py
new file mode 100644
index 00000000..b59af1e7
--- /dev/null
+++ b/manga_reader/api.py
@@ -0,0 +1,20 @@
+from ninja_extra import NinjaExtraAPI
+from ninja_jwt.controller import NinjaJWTDefaultController
+
+from apps.manga.api.api import manga_router
+from apps.manga.api.bookmarks.api import bookmark_router
+from apps.manga.api.lists.api import list_router
+from apps.manga.api.notifications.api import chapter_notification_router
+
+api = NinjaExtraAPI(title="Sora API", docs_url="/docs/")
+api.register_controllers(NinjaJWTDefaultController)
+
+api.add_router("/manga/", manga_router)
+api.add_router("/lists/", list_router)
+api.add_router("/bookmarks/", bookmark_router)
+api.add_router("/chapter_notifications/", chapter_notification_router)
+
+
+@api.get("/health", tags=["Meta"])
+def healthcheck(request):
+ return {}
diff --git a/manga_reader/settings.py b/manga_reader/settings.py
index 38d7b51f..7ceee1a7 100644
--- a/manga_reader/settings.py
+++ b/manga_reader/settings.py
@@ -1,35 +1,135 @@
import copy
import logging
+import logging.config
import os
-from datetime import timedelta
from functools import partial
from pathlib import Path
+import dj_database_url
+import environ
import scrapy.utils.log
import sentry_sdk
from colorlog import ColoredFormatter
+from django.core.management import BaseCommand
from sentry_sdk.integrations.django import DjangoIntegration
-from sentry_sdk.integrations.logging import ignore_logger
+
+from apps.parse.types import CacheType
+from apps.typesense_bind.client import create_client
+
+# TODO: docstrings in utils and stuff
+
+BASE_DIR = Path(__file__).resolve().parent.parent
+env = environ.Env(
+ DEBUG=(bool, True),
+ ALLOWED_HOSTS=(lambda a: a.split(" "), ["*"]),
+ DJANGO_LOG_LEVEL=(str, "INFO"),
+ REDIS_URL=(str, "redis://localhost:8883"),
+ TYPESENSE_HOST=(str, "localhost"),
+ TYPESENSE_PROTOCOL=(str, "http"),
+ TYPESENSE_API_KEY=(str, "api"),
+ PROXY=(str, ""),
+ GOOGLE_CLIENT=(str, ""),
+ GOOGLE_SECRET=(str, ""),
+ APM_NAME=(str, "rq-worker"),
+ SENTRY_DSN=(str, ""),
+)
+environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
+
+# Django debug toolbar IPs
+INTERNAL_IPS = ["127.0.0.1"]
###########
# Project #
###########
SHELL_PLUS_PRINT_SQL_TRUNCATE = None
-BASE_DIR = Path(__file__).resolve().parent.parent
ROOT_URLCONF = "manga_reader.urls"
WSGI_APPLICATION = "manga_reader.wsgi.application"
-WEBDRIVER_PATH = os.getenv("WEBDRIVER_PATH", None)
+
+sentry_sdk.init(
+ dsn=env("SENTRY_DSN"),
+ integrations=[DjangoIntegration()],
+ traces_sample_rate=1.0,
+ send_default_pii=True,
+)
############
# Security #
############
-SECRET_KEY = os.getenv("SECRET_KEY")
-DEBUG = int(os.getenv("DEBUG", 1))
-ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(" ")
+SECRET_KEY = env("SECRET_KEY")
+DEBUG = env("DEBUG")
+ALLOWED_HOSTS = env("ALLOWED_HOSTS")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
+# Stub so it works while using cloudflare, I don't want to fix it for now, so here it is
+CSRF_TRUSTED_ORIGINS = [
+ "https://sora-reader.app",
+ "https://*.sora-reader.app",
+ "http://localhost:3000",
+]
+
+CORS_ALLOWED_ORIGINS = CSRF_TRUSTED_ORIGINS
+CORS_ALLOW_CREDENTIALS = True
+CORS_ALLOW_HEADERS = [
+ "accept",
+ "accept-encoding",
+ "authorization",
+ "content-type",
+ "dnt",
+ "origin",
+ "user-agent",
+ "x-csrftoken",
+ "x-requested-with",
+ "baggage",
+ "sentry-trace",
+]
+
+SESSION_COOKIE_SAMESITE = None
+SESSION_COOKIE_NAME = "sessionId"
+
+############
+# Scraping #
+############
+
+PROXY = env("PROXY")
+HEADERS = {
+ "user-agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0",
+}
+REDIS_URL = env("REDIS_URL")
+RQ_QUEUES = {
+ "default": {
+ "URL": REDIS_URL,
+ }
+}
+
+TS_CLIENT = create_client(
+ env("TYPESENSE_HOST"), env("TYPESENSE_API_KEY"), env("TYPESENSE_PROTOCOL")
+)
+
+CACHES = {
+ "default": {
+ "BACKEND": "django.core.cache.backends.redis.RedisCache",
+ "LOCATION": REDIS_URL,
+ },
+ CacheType.detail.value: {
+ "BACKEND": "django.core.cache.backends.redis.RedisCache",
+ "LOCATION": f"{REDIS_URL}/2",
+ },
+ CacheType.chapter.value: {
+ "BACKEND": "django.core.cache.backends.redis.RedisCache",
+ "LOCATION": f"{REDIS_URL}/3",
+ },
+ CacheType.image.value: {
+ "BACKEND": "django.core.cache.backends.redis.RedisCache",
+ "LOCATION": f"{REDIS_URL}/4",
+ },
+}
+
+# TODO: Use while providing custom CDN
+# Should be able to fit 50 mangas in 10GB
+MANGA_MAX_SIZE_KB = 10485760 / 50
+
########
# Apps #
########
@@ -42,21 +142,25 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
- "rest_framework",
- "rest_framework_simplejwt.token_blacklist",
"corsheaders",
- "apps.api_docs",
- "apps.login.apps.LoginConfig",
- "apps.parse",
- "apps.core.apps.CoreConfig",
"django_extensions",
- "django.contrib.postgres",
- "django_elasticsearch_dsl",
+ "ninja_jwt",
+ "ninja_extra",
+ "allauth",
+ "allauth.account",
+ "allauth.socialaccount",
+ "allauth.socialaccount.providers.google",
+ "django_rq",
+ "apps.typesense_bind.apps.TypesenseBindConfig",
+ "apps.core",
+ "apps.authentication.apps.AuthenticationConfig",
+ "apps.manga.apps.MangaConfig",
+ "apps.parse",
+ "apps.readmanga.apps.Config",
+ "apps.mangachan.apps.Config",
]
-
-ELASTICSEARCH_DSL = {
- "default": {"hosts": os.getenv("ELASTICSEARCH_HOST", "localhost:92000")},
-}
+if DEBUG:
+ INSTALLED_APPS.append("debug_toolbar")
#########
# ADMIN #
@@ -107,7 +211,6 @@
"parse.genre",
"auth",
"login",
- "authtoken",
"core",
],
"default_icon_parents": "fas fa-chevron-circle-right",
@@ -124,46 +227,6 @@
"actions_sticky_top": True,
}
-#######
-# API #
-#######
-
-REST_FRAMEWORK = {
- "DEFAULT_PERMISSION_CLASSES": (),
- "DEFAULT_AUTHENTICATION_CLASSES": (),
- "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
- "PAGE_SIZE": int(os.getenv("PAGE_SIZE", 20)),
-}
-
-HEADERS = {
- "user-agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0",
-}
-
-SIMPLE_JWT = {
- "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
- "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
- "ROTATE_REFRESH_TOKENS": False,
- "BLACKLIST_AFTER_ROTATION": True,
- "UPDATE_LAST_LOGIN": False,
- "ALGORITHM": "HS256",
- "SIGNING_KEY": SECRET_KEY,
- "VERIFYING_KEY": None,
- "AUDIENCE": None,
- "ISSUER": None,
- "AUTH_HEADER_TYPES": ("Bearer",),
- "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
- "USER_ID_FIELD": "id",
- "USER_ID_CLAIM": "user_id",
- "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication."
- "default_user_authentication_rule",
- "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
- "TOKEN_TYPE_CLAIM": "token_type",
- "JTI_CLAIM": "jti",
- "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
- "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
- "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
-}
-
##############
# Middleware
##############
@@ -178,52 +241,57 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "debug_toolbar.middleware.DebugToolbarMiddleware",
]
-########
-# CORS #
-########
-
-CORS_ALLOW_ALL_ORIGINS = True
-
############
# Database #
############
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
DATABASES = {
- "default": {
- "ENGINE": "django.db.backends.postgresql_psycopg2",
- "NAME": os.getenv("DATABASE_NAME"),
- "USER": os.getenv("DATABASE_USER"),
- "PASSWORD": os.getenv("DATABASE_PASSWORD"),
- "HOST": os.getenv("DATABASE_HOST"),
- "PORT": os.getenv("DATABASE_PORT"),
- }
+ "default": dj_database_url.config(default="postgres://postgres:postgres@localhost:8882/sora"),
}
#################
# Authorization #
#################
+LOGIN_REDIRECT_URL = "/"
+ACCOUNT_EMAIL_VERIFICATION = "optional"
+AUTHENTICATION_BACKENDS = (
+ "django.contrib.auth.backends.ModelBackend",
+ "allauth.account.auth_backends.AuthenticationBackend",
+)
+
+# Provider specific settings
+SOCIALACCOUNT_PROVIDERS = {
+ "google": {
+ "APP": {
+ "client_id": env("GOOGLE_CLIENT"),
+ "secret": env("GOOGLE_SECRET"),
+ },
+ "SCOPE": [
+ "profile",
+ "email",
+ ],
+ "AUTH_PARAMS": {
+ "access_type": "online",
+ },
+ }
+}
+
AUTH_PASSWORD_VALIDATORS = [
- {
- "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
- },
+ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
PASSWORD_HASHERS = [
- "django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
+ "django.contrib.auth.hashers.ScryptPasswordHasher",
]
###########
@@ -246,15 +314,13 @@
},
]
-STATIC_URL = "/static/"
+STATIC_URL = "static/"
STATIC_ROOT = "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
-STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
MEDIA_URL = "/media/"
-MEDIA_ROOT = os.path.join(BASE_DIR, "media")
-
-REDIS_URL = os.getenv("REDIS_URL")
+MEDIA_ROOT = BASE_DIR / "media"
################
# Localization #
@@ -263,47 +329,25 @@
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
-USE_L10N = True
USE_TZ = True
-DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
-
-
-#############
-# GlitchTip #
-#############
-
-SENTRY_DSN = os.getenv("SENTRY_DSN", "")
-sentry_sdk.init(
- dsn=SENTRY_DSN,
- integrations=[DjangoIntegration()],
- send_default_pii=True,
-)
-ignore_logger("django.security.DisallowedHost")
-
-########
-# Silk #
-########
-
-if DEBUG:
- INSTALLED_APPS.append("silk")
- MIDDLEWARE.append("silk.middleware.SilkyMiddleware")
-
##########
# Logging #
##########
+# Colored formatting into masses
+
COLORED_FORMAT = (
"%(log_color)s%(levelname)-8s%(reset)s"
"%(bold_white)s[%(asctime)s]%(reset)s "
"%(log_color)s%(message)s%(reset)s"
)
COLORLESS_FORMAT = "%(levelname)-8s[%(asctime)s] %(message)s"
-DATEFMT = "%Y-%m-%d %H:%M:%S"
+LOGGER_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
SoraColoredLogger = partial(
ColoredFormatter,
- datefmt=DATEFMT,
+ datefmt=LOGGER_DATE_FORMAT,
log_colors={
"DEBUG": "white",
"INFO": "bold_cyan",
@@ -313,36 +357,34 @@
},
)
-color_formatter = SoraColoredLogger(COLORED_FORMAT)
-colorless_formatter = logging.Formatter(COLORLESS_FORMAT, datefmt=DATEFMT)
-
LOGGING = {
"version": 1,
- "disable_existing_loggers": False,
+ "disable_existing_loggers": True,
"formatters": {
"colored": {
- "()": "manga_reader.settings.SoraColoredLogger",
+ "()": SoraColoredLogger,
"format": COLORED_FORMAT,
}
},
"handlers": {
"console": {
- "level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "colored",
+ "level": "INFO",
},
},
- "root": {
- "handlers": ["console"],
- "level": "WARNING",
- },
"loggers": {
"django": {
"handlers": ["console"],
- "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
+ "level": "INFO",
+ "propagate": True,
+ },
+ "system": {
+ "handlers": ["console"],
+ "level": "INFO",
"propagate": False,
},
- "management": {
+ "scrapyscript": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
@@ -350,22 +392,52 @@
},
}
-##########
-# Scrapy #
-##########
-
-PROXY = os.getenv("PROXY")
+# Hijack Scrapy's logging
-_get_handler = copy.copy(scrapy.utils.log._get_handler)
+_get_handler = copy.copy(scrapy.utils.log._get_handler) # noqa
def _get_handler_custom(*args, **kwargs):
handler = _get_handler(*args, **kwargs)
- formatter = color_formatter
+ formatter = SoraColoredLogger(COLORED_FORMAT)
if isinstance(handler, logging.FileHandler):
- formatter = colorless_formatter
+ formatter = logging.Formatter(COLORLESS_FORMAT, datefmt=LOGGER_DATE_FORMAT)
handler.setFormatter(formatter)
return handler
scrapy.utils.log._get_handler = _get_handler_custom
+
+# Hack into BaseCommand to use logging instead of stdout/stderr
+
+_system_logger = logging.getLogger("system")
+
+
+class _StdoutLoggingStub:
+ def write(self, arg, *args, **kwargs):
+ _system_logger.info(arg.strip("\n"))
+
+ @staticmethod
+ def flush(*args, **kwargs):
+ pass
+
+
+class _StderrLoggingStub:
+ def write(self, arg, *args, **kwargs):
+ _system_logger.error(arg.strip("\n"))
+
+ @staticmethod
+ def flush(*args, **kwargs):
+ pass
+
+
+_base_command_init = BaseCommand.__init__
+
+
+def _init_patch_stdio(self, *args, **kwargs):
+ _base_command_init(self, *args, **kwargs)
+ self.stdout = _StdoutLoggingStub()
+ self.stderr = _StderrLoggingStub()
+
+
+BaseCommand.__init__ = _init_patch_stdio
diff --git a/manga_reader/urls.py b/manga_reader/urls.py
index 708c9442..f534714f 100644
--- a/manga_reader/urls.py
+++ b/manga_reader/urls.py
@@ -1,18 +1,16 @@
-from django.conf import settings
-from django.conf.urls import include
from django.contrib import admin
-from django.urls import path, re_path
+from django.urls import include, path, re_path
-apipatterns = [
- path("docs/", include("apps.api_docs.urls")),
- path("manga/", include("apps.parse.api.urls")),
- path("auth/", include("apps.login.urls")),
-]
+from manga_reader.api import api
urlpatterns = [
- path("api/", include(apipatterns)),
+ path("__debug__/", include("debug_toolbar.urls")),
+ #
+ path("api/auth/", include("apps.authentication.urls")),
+ path("api/", api.urls),
+ #
+ path("django-rq/", include("django_rq.urls")),
+ path("accounts/", include("allauth.urls")),
+ #
re_path(r"^(?!api)\w*?", admin.site.urls),
]
-
-if settings.DEBUG:
- urlpatterns.append(path("silk/", include("silk.urls", namespace="silk")))
diff --git a/poetry.lock b/poetry.lock
index 182ef3b7..d703e2ec 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,94 +1,73 @@
-[[package]]
-name = "amqp"
-version = "5.0.9"
-description = "Low-level AMQP client for Python (fork of amqplib)."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-vine = "5.0.0"
-
-[[package]]
-name = "appdirs"
-version = "1.4.4"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
-optional = false
-python-versions = "*"
-
[[package]]
name = "appnope"
-version = "0.1.2"
+version = "0.1.3"
description = "Disable App Nap on macOS >= 10.9"
category = "dev"
optional = false
python-versions = "*"
[[package]]
-name = "argon2-cffi"
-version = "20.1.0"
-description = "The secure Argon2 password hashing algorithm."
+name = "asgiref"
+version = "3.5.2"
+description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
-python-versions = "*"
-
-[package.dependencies]
-cffi = ">=1.0.0"
-six = "*"
+python-versions = ">=3.7"
[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"]
-docs = ["sphinx"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
-name = "asgiref"
-version = "3.2.10"
-description = "ASGI specs, helper code, and adapters"
-category = "main"
+name = "asttokens"
+version = "2.1.0"
+description = "Annotate AST trees with source code positions"
+category = "dev"
optional = false
-python-versions = ">=3.5"
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
[package.extras]
-tests = ["pytest", "pytest-asyncio"]
+test = ["astroid (<=2.5.3)", "pytest"]
[[package]]
-name = "atomicwrites"
-version = "1.4.0"
-description = "Atomic file writes."
-category = "dev"
+name = "async-timeout"
+version = "4.0.2"
+description = "Timeout context manager for asyncio programs"
+category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
[[package]]
name = "attrs"
-version = "21.4.0"
+version = "22.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5"
[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", "cloudpickle"]
-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", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
+tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "autoflake"
-version = "1.4"
+version = "1.7.7"
description = "Removes unused imports and unused variables"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
[package.dependencies]
pyflakes = ">=1.1.0"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[[package]]
name = "automat"
-version = "20.2.0"
+version = "22.10.0"
description = "Self-service finite-state machines for the programmer on the go."
category = "main"
optional = false
@@ -99,19 +78,7 @@ attrs = ">=19.2.0"
six = "*"
[package.extras]
-visualize = ["graphviz (>0.5.1)", "Twisted (>=16.1.1)"]
-
-[[package]]
-name = "autopep8"
-version = "1.5.7"
-description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-pycodestyle = ">=2.7.0"
-toml = "*"
+visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"]
[[package]]
name = "backcall"
@@ -131,88 +98,44 @@ python-versions = "*"
[[package]]
name = "black"
-version = "20.8b1"
+version = "22.10.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
-appdirs = "*"
-click = ">=7.1.2"
+click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
-pathspec = ">=0.6,<1"
-regex = ">=2020.1.8"
-toml = ">=0.10.1"
-typed-ast = ">=1.4.0"
-typing-extensions = ">=3.7.4"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
[[package]]
-name = "celery"
-version = "5.2.3"
-description = "Distributed Task Queue."
+name = "brotli"
+version = "1.0.9"
+description = "Python bindings for the Brotli compression library"
category = "main"
optional = false
-python-versions = ">=3.7,"
-
-[package.dependencies]
-billiard = ">=3.6.4.0,<4.0"
-click = ">=8.0.3,<9.0"
-click-didyoumean = ">=0.0.3"
-click-plugins = ">=1.1.1"
-click-repl = ">=0.2.0"
-kombu = ">=5.2.3,<6.0"
-pytz = ">=2021.3"
-vine = ">=5.0.0,<6.0"
-
-[package.extras]
-arangodb = ["pyArango (>=1.3.2)"]
-auth = ["cryptography"]
-azureblockblob = ["azure-storage-blob (==12.9.0)"]
-brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
-cassandra = ["cassandra-driver (<3.21.0)"]
-consul = ["python-consul2"]
-cosmosdbsql = ["pydocumentdb (==2.3.2)"]
-couchbase = ["couchbase (>=3.0.0)"]
-couchdb = ["pycouchdb"]
-django = ["Django (>=1.11)"]
-dynamodb = ["boto3 (>=1.9.178)"]
-elasticsearch = ["elasticsearch"]
-eventlet = ["eventlet (>=0.32.0)"]
-gevent = ["gevent (>=1.5.0)"]
-librabbitmq = ["librabbitmq (>=1.5.0)"]
-memcache = ["pylibmc"]
-mongodb = ["pymongo[srv] (>=3.11.1)"]
-msgpack = ["msgpack"]
-pymemcache = ["python-memcached"]
-pyro = ["pyro4"]
-pytest = ["pytest-celery"]
-redis = ["redis (>=3.4.1,!=4.0.0,!=4.0.1)"]
-s3 = ["boto3 (>=1.9.125)"]
-slmq = ["softlayer-messaging (>=1.0.3)"]
-solar = ["ephem"]
-sqlalchemy = ["sqlalchemy"]
-sqs = ["kombu"]
-tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
-yaml = ["PyYAML (>=3.10)"]
-zookeeper = ["kazoo (>=1.3.1)"]
-zstd = ["zstandard"]
+python-versions = "*"
[[package]]
name = "certifi"
-version = "2021.10.8"
+version = "2022.9.24"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
[[package]]
name = "cffi"
-version = "1.15.0"
+version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
@@ -231,75 +154,37 @@ python-versions = ">=3.6.1"
[[package]]
name = "charset-normalizer"
-version = "2.0.9"
+version = "2.1.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
-python-versions = ">=3.5.0"
+python-versions = ">=3.6.0"
[package.extras]
-unicode_backport = ["unicodedata2"]
+unicode-backport = ["unicodedata2"]
[[package]]
name = "click"
-version = "8.0.3"
+version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
-[[package]]
-name = "click-didyoumean"
-version = "0.3.0"
-description = "Enables git-like *did-you-mean* feature in click"
-category = "main"
-optional = false
-python-versions = ">=3.6.2,<4.0.0"
-
-[package.dependencies]
-click = ">=7"
-
-[[package]]
-name = "click-plugins"
-version = "1.1.1"
-description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-click = ">=4.0"
-
-[package.extras]
-dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"]
-
-[[package]]
-name = "click-repl"
-version = "0.2.0"
-description = "REPL plugin for Click"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-click = "*"
-prompt-toolkit = "*"
-six = "*"
-
[[package]]
name = "colorama"
-version = "0.4.4"
+version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "colorlog"
-version = "6.6.0"
+version = "6.7.0"
description = "Add colours to the output of Python's logging module."
category = "main"
optional = false
@@ -331,20 +216,27 @@ optional = false
python-versions = "*"
[[package]]
-name = "crochet"
-version = "2.0.0"
-description = "Use Twisted anywhere!"
+name = "contextlib2"
+version = "21.6.0"
+description = "Backports and enhancements for the contextlib module"
category = "main"
optional = false
-python-versions = ">=3.6.0"
+python-versions = ">=3.6"
+
+[[package]]
+name = "croniter"
+version = "1.3.8"
+description = "croniter provides iteration for datetime object with cron like format"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
-Twisted = ">=16.0"
-wrapt = "*"
+python-dateutil = "*"
[[package]]
name = "cryptography"
-version = "36.0.1"
+version = "38.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@@ -355,272 +247,309 @@ cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
-docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
-sdist = ["setuptools_rust (>=0.11.4)"]
+sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
-test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
+test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
[[package]]
name = "cssselect"
-version = "1.1.0"
+version = "1.2.0"
description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "dateutils"
-version = "0.6.12"
-description = "Various utilities for working with date and datetime objects"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-python-dateutil = "*"
-pytz = "*"
+python-versions = ">=3.7"
[[package]]
name = "decorator"
-version = "5.1.0"
+version = "5.1.1"
description = "Decorators for Humans"
category = "dev"
optional = false
python-versions = ">=3.5"
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "deprecated"
+version = "1.2.13"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"]
+
[[package]]
name = "distlib"
-version = "0.3.4"
+version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "dj-database-url"
+version = "1.0.0"
+description = "Use Database URLs in your Django Application."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+Django = ">3.2"
+
[[package]]
name = "django"
-version = "3.1"
-description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+version = "4.1.3"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
[package.dependencies]
-asgiref = ">=3.2.10,<3.3.0"
-pytz = "*"
+asgiref = ">=3.5.2,<4"
sqlparse = ">=0.2.2"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
-argon2 = ["argon2-cffi (>=16.1.0)"]
+argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
-name = "django-cors-headers"
-version = "3.10.1"
-description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
+name = "django-allauth"
+version = "0.51.0"
+description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = "*"
[package.dependencies]
-Django = ">=2.2"
+Django = ">=2.0"
+pyjwt = {version = ">=1.7", extras = ["crypto"]}
+python3-openid = ">=3.0.8"
+requests = "*"
+requests-oauthlib = ">=0.3.0"
[[package]]
-name = "django-elasticsearch-dsl"
-version = "7.2.1"
-description = ""
+name = "django-cors-headers"
+version = "3.13.0"
+description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
category = "main"
optional = false
-python-versions = "*"
-develop = false
+python-versions = ">=3.7"
[package.dependencies]
-elasticsearch-dsl = ">=7.2.0"
-six = "*"
+Django = ">=3.2"
-[package.source]
-type = "git"
-url = "https://github.com/dhvcc/django-elasticsearch-dsl.git"
-reference = "53598a336915a795467233f091f3c30836f0f758"
-resolved_reference = "53598a336915a795467233f091f3c30836f0f758"
+[[package]]
+name = "django-debug-toolbar"
+version = "3.7.0"
+description = "A configurable set of panels that display various debug information about the current request/response."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+Django = ">=3.2.4"
+sqlparse = ">=0.2.0"
[[package]]
-name = "django-extensions"
-version = "3.1.5"
-description = "Extensions for Django"
+name = "django-environ"
+version = "0.9.0"
+description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.4,<4"
-[package.dependencies]
-Django = ">=2.2"
+[package.extras]
+develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
+docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
+testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
[[package]]
-name = "django-filter"
-version = "2.4.0"
-description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
+name = "django-extensions"
+version = "3.2.1"
+description = "Extensions for Django"
category = "main"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
[package.dependencies]
-Django = ">=2.2"
+Django = ">=3.2"
[[package]]
name = "django-jazzmin"
-version = "2.4.5"
+version = "2.6.0"
description = "Drop-in theme for django admin, that utilises AdminLTE 3 & Bootstrap 4 to make yo' admin look jazzy"
category = "main"
optional = false
-python-versions = ">=3.5"
-develop = false
+python-versions = ">=3.6.2"
[package.dependencies]
-django = ">=2"
-
-[package.source]
-type = "git"
-url = "https://github.com/dhvcc/django-jazzmin.git"
-reference = "488036718b0c2a9d9b928c8dca257164db64857d"
-resolved_reference = "488036718b0c2a9d9b928c8dca257164db64857d"
+django = ">=2.2"
[[package]]
-name = "django-silk"
-version = "4.2.0"
-description = "Silky smooth profiling for the Django Framework"
+name = "django-ninja"
+version = "0.19.1"
+description = "Django Ninja - Fast Django REST framework"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-autopep8 = "*"
Django = ">=2.2"
-gprof2dot = ">=2017.09.19"
-Jinja2 = "*"
-Pygments = "*"
-python-dateutil = "*"
-pytz = "*"
-requests = "*"
-sqlparse = "*"
+pydantic = ">=1.6,<2.0.0"
-[[package]]
-name = "django-types"
-version = "0.9.1"
-description = "Type stubs for Django"
-category = "dev"
-optional = false
-python-versions = ">=3.6.2,<4.0.0"
+[package.extras]
+dev = ["pre-commit"]
+doc = ["markdown-include", "mkdocs", "mkdocs-material", "mkdocstrings"]
+test = ["black", "django-stubs", "flake8", "isort", "mypy (==0.931)", "psycopg2-binary", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django"]
[[package]]
-name = "django-typomatic"
-version = "1.6.1"
-description = "A simple solution for generating Typescript interfaces from your Django Rest Framework Serializers."
-category = "dev"
+name = "django-ninja-extra"
+version = "0.16.0"
+description = "Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"
+category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
[package.dependencies]
-django = "*"
-djangorestframework = "*"
+asgiref = "*"
+contextlib2 = "*"
+Django = ">=2.2"
+django-ninja = ">=0.17.0"
+injector = "0.19.0"
+
+[package.extras]
+dev = ["pre-commit"]
+doc = ["markdown-include", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mkdocstrings"]
+test = ["black", "django-stubs", "flake8", "injector (==0.19.0)", "isort", "mypy (==0.931)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django"]
[[package]]
-name = "djangorestframework"
-version = "3.13.1"
-description = "Web APIs for Django, made easy."
+name = "django-ninja-jwt"
+version = "5.2.2"
+description = "Django Ninja JWT - JSON Web Token for Django-Ninja"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-django = ">=2.2"
-pytz = "*"
+Django = ">=2.1"
+django-ninja-extra = ">=0.14.2"
+ninja-schema = ">=0.12.8"
+pyjwt = [
+ {version = ">=1.7.1,<3"},
+ {version = "*", extras = ["crypto"]},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.3.1)"]
+doc = ["markdown-include", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material"]
+test = ["black (==21.12b0)", "click (==8.0.4)", "cryptography", "django-stubs", "flake8 (==4.0.1)", "isort (==5.10.1)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django", "python-jose (==3.3.0)"]
[[package]]
-name = "djangorestframework-simplejwt"
-version = "4.8.0"
-description = "A minimal JSON Web Token authentication plugin for Django REST Framework"
+name = "django-rq"
+version = "2.6.0"
+description = "An app that provides django integration for RQ (Redis Queue)"
category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = "*"
[package.dependencies]
-django = "*"
-djangorestframework = "*"
-pyjwt = ">=2,<3"
+django = ">=2.0"
+redis = ">=3"
+rq = ">=1.2"
[package.extras]
-dev = ["pytest-watch", "wheel", "twine", "ipython", "cryptography", "pytest-cov", "pytest-django", "pytest-xdist", "pytest", "tox", "flake8", "pep8", "isort", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "python-jose (==3.0.0)"]
-doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"]
-lint = ["flake8", "pep8", "isort"]
-python-jose = ["python-jose (==3.0.0)"]
-test = ["cryptography", "pytest-cov", "pytest-django", "pytest-xdist", "pytest", "tox"]
+sentry = ["raven (>=6.1.0)"]
+testing = ["mock (>=2.0.0)"]
[[package]]
-name = "elasticsearch"
-version = "7.16.2"
-description = "Python client for Elasticsearch"
+name = "dnspython"
+version = "2.2.1"
+description = "DNS toolkit"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
-
-[package.dependencies]
-certifi = "*"
-urllib3 = ">=1.21.1,<2"
+python-versions = ">=3.6,<4.0"
[package.extras]
-async = ["aiohttp (>=3,<4)"]
-develop = ["requests (>=2.0.0,<3.0.0)", "coverage", "mock", "pyyaml", "pytest", "pytest-cov", "sphinx (<1.7)", "sphinx-rtd-theme", "black", "jinja2"]
-docs = ["sphinx (<1.7)", "sphinx-rtd-theme"]
-requests = ["requests (>=2.4.0,<3.0.0)"]
+curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
+dnssec = ["cryptography (>=2.6,<37.0)"]
+doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"]
+idna = ["idna (>=2.1,<4.0)"]
+trio = ["trio (>=0.14,<0.20)"]
+wmi = ["wmi (>=1.5.1,<2.0.0)"]
[[package]]
-name = "elasticsearch-dsl"
-version = "7.4.0"
-description = "Python client for Elasticsearch"
+name = "email-validator"
+version = "1.3.0"
+description = "A robust email address syntax and deliverability validation library."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-elasticsearch = ">=7.0.0,<8.0.0"
-python-dateutil = "*"
-six = "*"
+dnspython = ">=1.15.0"
+idna = ">=2.0.0"
+
+[[package]]
+name = "exceptiongroup"
+version = "1.0.1"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
[package.extras]
-develop = ["mock", "pytest (>=3.0.0)", "pytest-cov", "pytest-mock (<3.0.0)", "pytz", "coverage (<5.0.0)", "sphinx", "sphinx-rtd-theme"]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "executing"
+version = "1.2.0"
+description = "Get the currently executing AST node of a frame, and other information"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+tests = ["asttokens", "littleutils", "pytest", "rich"]
[[package]]
name = "filelock"
-version = "3.4.2"
+version = "3.8.0"
description = "A platform independent file lock."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
-docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
-testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
+testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
[[package]]
name = "flake8"
-version = "3.9.2"
+version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.6.1"
[package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.7.0,<2.8.0"
-pyflakes = ">=2.3.0,<2.4.0"
-
-[[package]]
-name = "gprof2dot"
-version = "2021.2.21"
-description = "Generate a dot graph from the output of several profilers."
-category = "main"
-optional = false
-python-versions = "*"
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.9.0,<2.10.0"
+pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "gunicorn"
@@ -630,40 +559,15 @@ category = "main"
optional = false
python-versions = ">=3.5"
+[package.dependencies]
+setuptools = ">=3.0"
+
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
-[[package]]
-name = "h2"
-version = "3.2.0"
-description = "HTTP/2 State-Machine based protocol implementation"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-hpack = ">=3.0,<4"
-hyperframe = ">=5.2.0,<6"
-
-[[package]]
-name = "hpack"
-version = "3.0.0"
-description = "Pure-Python HPACK header compression"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "hyperframe"
-version = "5.2.0"
-description = "HTTP/2 framing layer for Python"
-category = "main"
-optional = false
-python-versions = "*"
-
[[package]]
name = "hyperlink"
version = "21.0.0"
@@ -677,48 +581,33 @@ idna = ">=2.5"
[[package]]
name = "identify"
-version = "2.4.1"
+version = "2.5.8"
description = "File identification library for Python"
category = "dev"
optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.7"
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
-version = "3.3"
+version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "importlib-metadata"
-version = "4.10.0"
-description = "Read metadata from Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-perf = ["ipython"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "incremental"
-version = "21.3.0"
-description = "A small library that versions your Python projects."
+version = "22.10.0"
+description = "\"A small library that versions your Python projects.\""
category = "main"
optional = false
python-versions = "*"
[package.extras]
+mypy = ["click (>=6.0)", "mypy (==0.812)", "twisted (>=16.4.0)"]
scripts = ["click (>=6.0)", "twisted (>=16.4.0)"]
[[package]]
@@ -729,6 +618,17 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "injector"
+version = "0.19.0"
+description = "Injector - Python dependency injection framework, inspired by Guice"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+dev = ["black", "check-manifest", "dataclasses", "mypy", "pytest", "pytest-cov (>=2.5.1)"]
+
[[package]]
name = "ipdb"
version = "0.13.9"
@@ -740,15 +640,16 @@ python-versions = ">=2.7"
[package.dependencies]
decorator = {version = "*", markers = "python_version > \"3.6\""}
ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""}
+setuptools = "*"
toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""}
[[package]]
name = "ipython"
-version = "7.30.1"
+version = "8.6.0"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
[package.dependencies]
appnope = {version = "*", markers = "sys_platform == \"darwin\""}
@@ -759,20 +660,23 @@ jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
-prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
-pygments = "*"
-traitlets = ">=4.2"
+prompt-toolkit = ">3.0.1,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
[package.extras]
-all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"]
-doc = ["Sphinx (>=1.3)"]
+all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.20)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
-notebook = ["notebook", "ipywidgets"]
+notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
-test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
[[package]]
name = "isort"
@@ -783,14 +687,14 @@ optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
-pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
-requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "itemadapter"
-version = "0.4.0"
+version = "0.7.0"
description = "Common interface for data container classes"
category = "main"
optional = false
@@ -798,7 +702,7 @@ python-versions = ">=3.6"
[[package]]
name = "itemloaders"
-version = "1.0.4"
+version = "1.0.6"
description = "Base library for scrapy's ItemLoader"
category = "main"
optional = false
@@ -825,59 +729,17 @@ parso = ">=0.8.0,<0.9.0"
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"]
-[[package]]
-name = "jinja2"
-version = "3.0.3"
-description = "A very fast and expressive template engine."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-MarkupSafe = ">=2.0"
-
-[package.extras]
-i18n = ["Babel (>=2.7)"]
-
[[package]]
name = "jmespath"
-version = "0.10.0"
+version = "1.0.1"
description = "JSON Matching Expressions"
category = "main"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "kombu"
-version = "5.2.3"
-description = "Messaging library for Python."
-category = "main"
-optional = false
python-versions = ">=3.7"
-[package.dependencies]
-amqp = ">=5.0.9,<6.0.0"
-vine = "*"
-
-[package.extras]
-azureservicebus = ["azure-servicebus (>=7.0.0)"]
-azurestoragequeues = ["azure-storage-queue"]
-consul = ["python-consul (>=0.6.0)"]
-librabbitmq = ["librabbitmq (>=2.0.0)"]
-mongodb = ["pymongo (>=3.3.0,<3.12.1)"]
-msgpack = ["msgpack"]
-pyro = ["pyro4"]
-qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
-redis = ["redis (>=3.4.1,!=4.0.0,!=4.0.1)"]
-slmq = ["softlayer-messaging (>=1.0.3)"]
-sqlalchemy = ["sqlalchemy"]
-sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"]
-yaml = ["PyYAML (>=3.10)"]
-zookeeper = ["kazoo (>=1.3.1)"]
-
[[package]]
name = "lxml"
-version = "4.7.1"
+version = "4.9.1"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
category = "main"
optional = false
@@ -886,34 +748,23 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
[package.extras]
cssselect = ["cssselect (>=0.7)"]
html5 = ["html5lib"]
-htmlsoup = ["beautifulsoup4"]
+htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=0.29.7)"]
[[package]]
name = "markdown"
-version = "3.3.6"
+version = "3.4.1"
description = "Python implementation of Markdown."
category = "main"
optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+python-versions = ">=3.7"
[package.extras]
testing = ["coverage", "pyyaml"]
-[[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 = "matplotlib-inline"
-version = "0.1.3"
+version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
category = "dev"
optional = false
@@ -924,11 +775,11 @@ traitlets = "*"
[[package]]
name = "mccabe"
-version = "0.6.1"
+version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
[[package]]
name = "mypy-extensions"
@@ -938,17 +789,51 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "ninja-schema"
+version = "0.13.0"
+description = "Django Schema - Builds Pydantic Schemas from Django Models with default field type validations"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.0"
+pydantic = [
+ {version = "*"},
+ {version = "*", extras = ["email"]},
+]
+
+[package.extras]
+test = ["black", "django-stubs", "flake8", "isort", "mypy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django"]
+
[[package]]
name = "nodeenv"
-version = "1.6.0"
+version = "1.7.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "oauthlib"
+version = "3.2.2"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "orjson"
-version = "3.6.5"
+version = "3.8.1"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
category = "main"
optional = false
@@ -958,7 +843,7 @@ python-versions = ">=3.7"
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -967,16 +852,16 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "parsel"
-version = "1.6.0"
+version = "1.7.0"
description = "Parsel is a library to extract data from HTML and XML using XPath and CSS selectors"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
[package.dependencies]
cssselect = ">=0.9"
lxml = "*"
-six = ">=1.6.0"
+packaging = "*"
w3lib = ">=1.19.0"
[[package]]
@@ -993,11 +878,11 @@ testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pathspec"
-version = "0.9.0"
+version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.7"
[[package]]
name = "pexpect"
@@ -1020,14 +905,14 @@ python-versions = "*"
[[package]]
name = "platformdirs"
-version = "2.4.1"
+version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
-docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
@@ -1044,11 +929,11 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
-version = "2.16.0"
+version = "2.20.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.7"
[package.dependencies]
cfgv = ">=2.0.0"
@@ -1058,19 +943,11 @@ pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"
-[[package]]
-name = "priority"
-version = "1.3.0"
-description = "A pure-Python implementation of the HTTP/2 priority tree"
-category = "main"
-optional = false
-python-versions = "*"
-
[[package]]
name = "prompt-toolkit"
-version = "3.0.24"
+version = "3.0.32"
description = "Library for building powerful interactive command lines in Python"
-category = "main"
+category = "dev"
optional = false
python-versions = ">=3.6.2"
@@ -1079,7 +956,7 @@ wcwidth = "*"
[[package]]
name = "protego"
-version = "0.1.16"
+version = "0.2.1"
description = "Pure-Python robots.txt parser with support for modern conventions"
category = "main"
optional = false
@@ -1090,7 +967,7 @@ six = "*"
[[package]]
name = "psycopg2"
-version = "2.9.3"
+version = "2.9.5"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = false
@@ -1105,12 +982,15 @@ optional = false
python-versions = "*"
[[package]]
-name = "py"
-version = "1.11.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "*"
+
+[package.extras]
+tests = ["pytest"]
[[package]]
name = "pyasn1"
@@ -1133,11 +1013,11 @@ pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
name = "pycodestyle"
-version = "2.7.0"
+version = "2.9.1"
description = "Python style guide checker"
-category = "main"
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
[[package]]
name = "pycparser"
@@ -1147,67 +1027,88 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+[[package]]
+name = "pydantic"
+version = "1.10.2"
+description = "Data validation and settings management using python type hints"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
[[package]]
name = "pydispatcher"
-version = "2.0.5"
-description = "Multi-producer-multi-consumer signal dispatching mechanism"
+version = "2.0.6"
+description = "Multi-Producer Multi-Consumer Observer Pattern for Python"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyflakes"
-version = "2.3.1"
+version = "2.5.0"
description = "passive checker of Python programs"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
[[package]]
name = "pygments"
-version = "2.11.1"
+version = "2.13.0"
description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
+category = "dev"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
+
+[package.extras]
+plugins = ["importlib-metadata"]
[[package]]
name = "pyjwt"
-version = "2.3.0"
+version = "2.6.0"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
[package.extras]
-crypto = ["cryptography (>=3.3.1)"]
-dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
-docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
-tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pyopenssl"
-version = "21.0.0"
+version = "22.1.0"
description = "Python wrapper module around the OpenSSL library"
category = "main"
optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+python-versions = ">=3.6"
[package.dependencies]
-cryptography = ">=3.3"
-six = ">=1.5.2"
+cryptography = ">=38.0.0,<39"
[package.extras]
-docs = ["sphinx", "sphinx-rtd-theme"]
+docs = ["sphinx (!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"]
test = ["flaky", "pretend", "pytest (>=3.0.1)"]
[[package]]
name = "pyparsing"
-version = "3.0.6"
-description = "Python parsing module"
-category = "dev"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
@@ -1222,24 +1123,38 @@ python-versions = "*"
[[package]]
name = "pytest"
-version = "6.2.5"
+version = "7.2.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
-atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-toml = "*"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-django"
+version = "4.5.2"
+description = "A Django plugin for pytest."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+pytest = ">=5.4.0"
+
+[package.extras]
+docs = ["sphinx", "sphinx-rtd-theme"]
+testing = ["Django", "django-configurations (>=2.0)"]
[[package]]
name = "python-dateutil"
@@ -1253,23 +1168,19 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
six = ">=1.5"
[[package]]
-name = "python-dotenv"
-version = "0.15.0"
-description = "Add .env support to your django/flask apps in development and deployments"
+name = "python3-openid"
+version = "3.2.0"
+description = "OpenID support for modern servers and consumers."
category = "main"
optional = false
python-versions = "*"
-[package.extras]
-cli = ["click (>=5.0)"]
+[package.dependencies]
+defusedxml = "*"
-[[package]]
-name = "pytz"
-version = "2021.3"
-description = "World timezone definitions, modern and historical"
-category = "main"
-optional = false
-python-versions = "*"
+[package.extras]
+mysql = ["mysql-connector-python"]
+postgresql = ["psycopg2"]
[[package]]
name = "pyyaml"
@@ -1289,89 +1200,156 @@ python-versions = ">=3.5"
[[package]]
name = "redis"
-version = "3.5.3"
-description = "Python client for Redis key-value store"
+version = "4.3.4"
+description = "Python client for Redis database and key-value store"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.6"
-[package.extras]
-hiredis = ["hiredis (>=0.1.3)"]
+[package.dependencies]
+async-timeout = ">=4.0.2"
+deprecated = ">=1.2.3"
+packaging = ">=20.4"
-[[package]]
-name = "regex"
-version = "2021.11.10"
-description = "Alternative regular expression module, to replace re."
-category = "dev"
-optional = false
-python-versions = "*"
+[package.extras]
+hiredis = ["hiredis (>=1.0.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
name = "requests"
-version = "2.26.0"
+version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+python-versions = ">=3.7, <4"
[package.dependencies]
certifi = ">=2017.4.17"
-charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
-idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "requests-file"
+version = "1.5.1"
+description = "File transport adapter for Requests"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=1.0.0"
+six = "*"
+
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.1"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "rich"
-version = "10.16.1"
+version = "12.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "dev"
optional = false
-python-versions = ">=3.6.2,<4.0.0"
+python-versions = ">=3.6.3,<4.0.0"
[package.dependencies]
-colorama = ">=0.4.0,<0.5.0"
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+[[package]]
+name = "rq"
+version = "1.11.1"
+description = "RQ is a simple, lightweight, library for creating background jobs, and processing them."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+click = ">=5.0.0"
+redis = ">=3.5.0"
+
+[[package]]
+name = "rq-scheduler"
+version = "0.11.0"
+description = "Provides job scheduling capabilities to RQ (Redis Queue)"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+croniter = ">=0.3.9"
+python-dateutil = "*"
+rq = ">=0.13"
+
[[package]]
name = "scrapy"
-version = "2.5.1"
+version = "2.7.1"
description = "A high-level Web Crawling and Web Scraping framework"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
-cryptography = ">=2.0"
+cryptography = ">=3.3"
cssselect = ">=0.9.1"
-h2 = ">=3.0,<4.0"
itemadapter = ">=0.1.0"
itemloaders = ">=1.0.1"
-lxml = [
- {version = ">=3.5.0", markers = "platform_python_implementation == \"CPython\""},
- {version = ">=4.0.0", markers = "platform_python_implementation == \"PyPy\""},
-]
+lxml = ">=4.3.0"
+packaging = "*"
parsel = ">=1.5.0"
protego = ">=0.1.15"
PyDispatcher = {version = ">=2.0.5", markers = "platform_python_implementation == \"CPython\""}
-pyOpenSSL = ">=16.2.0"
+pyOpenSSL = ">=21.0.0"
PyPyDispatcher = {version = ">=2.1.0", markers = "platform_python_implementation == \"PyPy\""}
queuelib = ">=1.4.2"
-service-identity = ">=16.0.0"
-Twisted = {version = ">=17.9.0", extras = ["http2"]}
+service-identity = ">=18.1.0"
+setuptools = "*"
+tldextract = "*"
+Twisted = ">=18.9.0"
w3lib = ">=1.17.0"
-"zope.interface" = ">=4.1.3"
+"zope.interface" = ">=5.1.0"
+
+[[package]]
+name = "scrapyscript"
+version = "0.0.0"
+description = "Run a Scrapy spider programmatically from a script or a Celery task - no project required."
+category = "main"
+optional = false
+python-versions = "^3.8"
+develop = false
+
+[package.dependencies]
+billiard = "^3.6"
+Scrapy = "^2.5.1"
+
+[package.source]
+type = "git"
+url = "https://github.com/dhvcc/scrapyscript.git"
+reference = "error-handling"
+resolved_reference = "f948561adcdc1cf38dfbdd9da5a984ae23770bdb"
[[package]]
name = "sentry-sdk"
-version = "1.5.1"
+version = "1.11.0"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
@@ -1379,7 +1357,7 @@ python-versions = "*"
[package.dependencies]
certifi = "*"
-urllib3 = ">=1.10.0"
+urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
@@ -1389,13 +1367,17 @@ celery = ["celery (>=3)"]
chalice = ["chalice (>=1.16.0)"]
django = ["django (>=1.8)"]
falcon = ["falcon (>=1.4)"]
-flask = ["flask (>=0.11)", "blinker (>=1.1)"]
+fastapi = ["fastapi (>=0.79.0)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)"]
httpx = ["httpx (>=0.16.0)"]
-pure_eval = ["pure-eval", "executing", "asttokens"]
+pure-eval = ["asttokens", "executing", "pure-eval"]
+pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
+quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
rq = ["rq (>=0.6)"]
sanic = ["sanic (>=0.8)"]
sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
tornado = ["tornado (>=5)"]
[[package]]
@@ -1414,11 +1396,24 @@ pyasn1-modules = "*"
six = "*"
[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "pytest", "sphinx", "furo", "idna", "pyopenssl"]
-docs = ["sphinx", "furo"]
+dev = ["coverage[toml] (>=5.0.2)", "furo", "idna", "pyOpenSSL", "pytest", "sphinx"]
+docs = ["furo", "sphinx"]
idna = ["idna"]
tests = ["coverage[toml] (>=5.0.2)", "pytest"]
+[[package]]
+name = "setuptools"
+version = "65.5.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
[[package]]
name = "six"
version = "1.16.0"
@@ -1429,65 +1424,104 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sqlparse"
-version = "0.4.2"
+version = "0.4.3"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
+[[package]]
+name = "stack-data"
+version = "0.6.0"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
+
+[[package]]
+name = "tldextract"
+version = "3.4.0"
+description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+filelock = ">=3.0.8"
+idna = "*"
+requests = ">=2.1.0"
+requests-file = ">=1.4"
+
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
-category = "main"
+category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
[[package]]
name = "traitlets"
-version = "5.1.1"
-description = "Traitlets Python configuration system"
+version = "5.5.0"
+description = ""
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
-test = ["pytest"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["pre-commit", "pytest"]
[[package]]
name = "twisted"
-version = "21.7.0"
+version = "22.10.0"
description = "An asynchronous networking framework written in Python"
category = "main"
optional = false
-python-versions = ">=3.6.7"
+python-versions = ">=3.7.1"
[package.dependencies]
attrs = ">=19.2.0"
Automat = ">=0.8.0"
constantly = ">=15.1"
-h2 = {version = ">=3.0,<4.0", optional = true, markers = "extra == \"http2\""}
hyperlink = ">=17.1.1"
incremental = ">=21.3.0"
-priority = {version = ">=1.1.0,<2.0", optional = true, markers = "extra == \"http2\""}
-twisted-iocpsupport = {version = ">=1.0.0,<1.1.0", markers = "platform_system == \"Windows\""}
+twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""}
typing-extensions = ">=3.6.5"
"zope.interface" = ">=4.4.2"
[package.extras]
-all_non_platform = ["cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)", "pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)", "contextvars (>=2.4,<3)"]
-conch = ["pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)"]
+all-non-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"]
+conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"]
+conch-nacl = ["PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"]
contextvars = ["contextvars (>=2.4,<3)"]
-dev = ["towncrier (>=19.2,<20.0)", "sphinx-rtd-theme (>=0.5,<1.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=3.3,<4.0)", "pyflakes (>=2.2,<3.0)", "twistedchecker (>=0.7,<1.0)", "coverage (>=6b1,<7)", "python-subunit (>=1.4,<2.0)", "pydoctor (>=21.2.2,<21.3.0)"]
-dev_release = ["towncrier (>=19.2,<20.0)", "sphinx-rtd-theme (>=0.5,<1.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=3.3,<4.0)", "pydoctor (>=21.2.2,<21.3.0)"]
-http2 = ["h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)"]
-macos_platform = ["pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)", "pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)", "contextvars (>=2.4,<3)"]
-mypy = ["mypy (==0.910)", "mypy-zope (==0.3.2)", "types-setuptools", "towncrier (>=19.2,<20.0)", "sphinx-rtd-theme (>=0.5,<1.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=3.3,<4.0)", "pyflakes (>=2.2,<3.0)", "twistedchecker (>=0.7,<1.0)", "coverage (>=6b1,<7)", "cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)", "pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)", "python-subunit (>=1.4,<2.0)", "contextvars (>=2.4,<3)", "pydoctor (>=21.2.2,<21.3.0)"]
-osx_platform = ["pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)", "pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)", "contextvars (>=2.4,<3)"]
+dev = ["coverage (>=6b1,<7)", "pydoctor (>=22.9.0,<22.10.0)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)", "twistedchecker (>=0.7,<1.0)"]
+dev-release = ["pydoctor (>=22.9.0,<22.10.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)"]
+gtk-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pygobject", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"]
+http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"]
+macos-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"]
+mypy = ["PyHamcrest (>=1.9.0)", "PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "coverage (>=6b1,<7)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "mypy (==0.930)", "mypy-zope (==0.3.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pydoctor (>=22.9.0,<22.10.0)", "pyflakes (>=2.2,<3.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "service-identity (>=18.1.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)", "twistedchecker (>=0.7,<1.0)", "types-pyOpenSSL", "types-setuptools"]
+osx-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"]
serial = ["pyserial (>=3.0)", "pywin32 (!=226)"]
-test = ["cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)"]
-tls = ["pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)"]
-windows_platform = ["pywin32 (!=226)", "cython-test-exception-raiser (>=1.0,<2.0)", "PyHamcrest (>=1.9.0)", "pyopenssl (>=16.0.0)", "service-identity (>=18.1.0)", "idna (>=2.4)", "pyasn1", "cryptography (>=2.6)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)", "contextvars (>=2.4,<3)"]
+test = ["PyHamcrest (>=1.9.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.0,<7.0)"]
+tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"]
+windows-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)"]
[[package]]
name = "twisted-iocpsupport"
@@ -1498,84 +1532,75 @@ optional = false
python-versions = "*"
[[package]]
-name = "typed-ast"
-version = "1.5.1"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
+name = "typesense"
+version = "0.14.0"
+description = "Python client for Typesense, an open source and typo tolerant search engine."
+category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3"
+
+[package.dependencies]
+requests = "*"
[[package]]
name = "typing-extensions"
-version = "4.0.1"
-description = "Backported and Experimental Type Hints for Python 3.6+"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[[package]]
-name = "ujson"
-version = "4.3.0"
-description = "Ultra fast JSON encoder and decoder for Python"
+name = "tzdata"
+version = "2022.6"
+description = "Provider of IANA time zone data"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=2"
[[package]]
name = "urllib3"
-version = "1.26.7"
+version = "1.26.12"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
[package.extras]
-brotli = ["brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
-[[package]]
-name = "vine"
-version = "5.0.0"
-description = "Promises, promises, promises."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
[[package]]
name = "virtualenv"
-version = "20.12.1"
+version = "20.16.6"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.6"
[package.dependencies]
-distlib = ">=0.3.1,<1"
-filelock = ">=3.2,<4"
-platformdirs = ">=2,<3"
-six = ">=1.9.0,<2"
+distlib = ">=0.3.6,<1"
+filelock = ">=3.4.1,<4"
+platformdirs = ">=2.4,<3"
[package.extras]
-docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
-testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
+testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
[[package]]
name = "w3lib"
-version = "1.22.0"
+version = "2.0.1"
description = "Library of web-related functions"
category = "main"
optional = false
-python-versions = "*"
-
-[package.dependencies]
-six = ">=1.4.1"
+python-versions = ">=3.6"
[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
-category = "main"
+category = "dev"
optional = false
python-versions = "*"
@@ -1588,118 +1613,81 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
-dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
+dev = ["coverage", "pallets-sphinx-themes", "pytest", "sphinx", "sphinx-issues", "tox"]
termcolor = ["termcolor"]
watchdog = ["watchdog"]
[[package]]
name = "whitenoise"
-version = "5.3.0"
+version = "6.2.0"
description = "Radically simplified static file serving for WSGI applications"
category = "main"
optional = false
-python-versions = ">=3.5, <4"
+python-versions = ">=3.7"
+
+[package.dependencies]
+Brotli = {version = "*", optional = true, markers = "extra == \"brotli\""}
[package.extras]
-brotli = ["brotli"]
+brotli = ["Brotli"]
[[package]]
name = "wrapt"
-version = "1.13.3"
+version = "1.14.1"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
-name = "zipp"
-version = "3.7.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
-
-[[package]]
-name = "zope.interface"
-version = "5.4.0"
+name = "zope-interface"
+version = "5.5.1"
description = "Interfaces for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+[package.dependencies]
+setuptools = "*"
+
[package.extras]
-docs = ["sphinx", "repoze.sphinx.autointerface"]
+docs = ["Sphinx", "repoze.sphinx.autointerface"]
test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "1.1"
-python-versions = "^3.8"
-content-hash = "e26afa823cfedb47d4961cf115141f0ffda33f2f13846f385b89e10cc60478d8"
+python-versions = "^3.10"
+content-hash = "aeef9aed398bb7d639dac66a735e4063c5a805eb2afc525e4fbec71430e3a7c3"
[metadata.files]
-amqp = [
- {file = "amqp-5.0.9-py3-none-any.whl", hash = "sha256:9cd81f7b023fc04bbb108718fbac674f06901b77bfcdce85b10e2a5d0ee91be5"},
- {file = "amqp-5.0.9.tar.gz", hash = "sha256:1e5f707424e544078ca196e72ae6a14887ce74e02bd126be54b7c03c971bef18"},
-]
-appdirs = [
- {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
- {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
-]
appnope = [
- {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
- {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
-]
-argon2-cffi = [
- {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"},
- {file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"},
- {file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"},
- {file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"},
- {file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"},
- {file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"},
- {file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"},
- {file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"},
- {file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"},
- {file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"},
- {file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"},
- {file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"},
- {file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"},
- {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"},
- {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"},
- {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"},
- {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"},
- {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"},
- {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b94042e5dcaa5d08cf104a54bfae614be502c6f44c9c89ad1535b2ebdaacbd4c"},
- {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:8282b84ceb46b5b75c3a882b28856b8cd7e647ac71995e71b6705ec06fc232c3"},
- {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3aa804c0e52f208973845e8b10c70d8957c9e5a666f702793256242e9167c4e0"},
- {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:36320372133a003374ef4275fbfce78b7ab581440dfca9f9471be3dd9a522428"},
+ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
+ {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
]
asgiref = [
- {file = "asgiref-3.2.10-py3-none-any.whl", hash = "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"},
- {file = "asgiref-3.2.10.tar.gz", hash = "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a"},
+ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
+ {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
+]
+asttokens = [
+ {file = "asttokens-2.1.0-py2.py3-none-any.whl", hash = "sha256:1b28ed85e254b724439afc783d4bee767f780b936c3fe8b3275332f42cf5f561"},
+ {file = "asttokens-2.1.0.tar.gz", hash = "sha256:4aa76401a151c8cc572d906aad7aea2a841780834a19d780f4321c0fe1b54635"},
]
-atomicwrites = [
- {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
- {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+async-timeout = [
+ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
+ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
attrs = [
- {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
- {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
autoflake = [
- {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"},
+ {file = "autoflake-1.7.7-py3-none-any.whl", hash = "sha256:a9b43d08f8e455824e4f7b3f078399f59ba538ba53872f466c09e55c827773ef"},
+ {file = "autoflake-1.7.7.tar.gz", hash = "sha256:c8e4fc41aa3eae0f5c94b939e3a3d50923d7a9306786a6cbf4866a077b8f6832"},
]
automat = [
- {file = "Automat-20.2.0-py2.py3-none-any.whl", hash = "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"},
- {file = "Automat-20.2.0.tar.gz", hash = "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33"},
-]
-autopep8 = [
- {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
- {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
+ {file = "Automat-22.10.0-py2.py3-none-any.whl", hash = "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180"},
+ {file = "Automat-22.10.0.tar.gz", hash = "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e"},
]
backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
@@ -1710,99 +1698,181 @@ billiard = [
{file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"},
]
black = [
- {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
-]
-celery = [
- {file = "celery-5.2.3-py3-none-any.whl", hash = "sha256:8aacd02fc23a02760686d63dde1eb0daa9f594e735e73ea8fb15c2ff15cb608c"},
- {file = "celery-5.2.3.tar.gz", hash = "sha256:e2cd41667ad97d4f6a2f4672d1c6a6ebada194c619253058b5f23704aaadaa82"},
+ {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
+ {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
+ {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
+ {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
+ {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
+ {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
+ {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
+ {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
+ {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
+ {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
+ {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
+ {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
+ {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
+ {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
+ {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
+ {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
+ {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
+ {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
+ {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
+ {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
+ {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
+]
+brotli = [
+ {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
+ {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
+ {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
+ {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
+ {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
+ {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
+ {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
+ {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
+ {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
+ {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
+ {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
+ {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
+ {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
+ {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
+ {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
+ {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
+ {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
+ {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
+ {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
+ {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
+ {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
+ {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
+ {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
+ {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
+ {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
+ {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
+ {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
+ {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
+ {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
+ {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
+ {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
+ {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
]
certifi = [
- {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
- {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cffi = [
- {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
- {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
- {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
- {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
- {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
- {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
- {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
- {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
- {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
- {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
- {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
- {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
- {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
- {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
- {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
- {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
- {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
- {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
- {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
- {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
- {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
- {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
- {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
- {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
- {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
- {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
- {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
- {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
- {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
- {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
- {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
- {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
- {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
- {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
- {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
- {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
- {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
- {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
- {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
- {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
- {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
- {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
- {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
- {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
- {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
- {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
- {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
- {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
- {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
- {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
+ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+ {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+ {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+ {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+ {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+ {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+ {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+ {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+ {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+ {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+ {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+ {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+ {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+ {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+ {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+ {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+ {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+ {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+ {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
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"},
+ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
+ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [
- {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
- {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
-]
-click-didyoumean = [
- {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"},
- {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"},
-]
-click-plugins = [
- {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
- {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
-]
-click-repl = [
- {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"},
- {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"},
+ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
- {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
- {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
colorlog = [
- {file = "colorlog-6.6.0-py2.py3-none-any.whl", hash = "sha256:351c51e866c86c3217f08e4b067a7974a678be78f07f85fc2d55b8babde6d94e"},
- {file = "colorlog-6.6.0.tar.gz", hash = "sha256:344f73204009e4c83c5b6beb00b3c45dc70fcdae3c80db919e0a4171d006fde8"},
+ {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"},
+ {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"},
]
commonmark = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
@@ -1812,374 +1882,354 @@ constantly = [
{file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"},
{file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"},
]
-crochet = [
- {file = "crochet-2.0.0-py3-none-any.whl", hash = "sha256:de7306f0548581b5222c2d91f2175045e672591519896f717affb81ef66c0f5f"},
- {file = "crochet-2.0.0.tar.gz", hash = "sha256:5f7f6c0d41ec418da16080f0202faac6b30f84a6fca9d8911e9db541f8e4e521"},
+contextlib2 = [
+ {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"},
+ {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"},
+]
+croniter = [
+ {file = "croniter-1.3.8-py2.py3-none-any.whl", hash = "sha256:d6ed8386d5f4bbb29419dc1b65c4909c04a2322bd15ec0dc5b2877bfa1b75c7a"},
+ {file = "croniter-1.3.8.tar.gz", hash = "sha256:32a5ec04e97ec0837bcdf013767abd2e71cceeefd3c2e14c804098ce51ad6cd9"},
]
cryptography = [
- {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"},
- {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"},
- {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"},
- {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"},
- {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"},
- {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"},
- {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"},
- {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"},
- {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"},
- {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"},
- {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"},
- {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"},
- {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"},
- {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"},
- {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"},
- {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"},
- {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"},
- {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"},
- {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"},
- {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"},
+ {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"},
+ {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"},
+ {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"},
+ {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"},
+ {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"},
+ {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"},
+ {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"},
+ {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"},
+ {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"},
+ {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"},
+ {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"},
]
cssselect = [
- {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"},
- {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"},
-]
-dateutils = [
- {file = "dateutils-0.6.12-py2.py3-none-any.whl", hash = "sha256:f33b6ab430fa4166e7e9cb8b21ee9f6c9843c48df1a964466f52c79b2a8d53b3"},
- {file = "dateutils-0.6.12.tar.gz", hash = "sha256:03dd90bcb21541bd4eb4b013637e4f1b5f944881c46cc6e4b67a6059e370e3f1"},
+ {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"},
+ {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
]
decorator = [
- {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
- {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+defusedxml = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+deprecated = [
+ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
+ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
]
distlib = [
- {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
- {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
+ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
+ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+]
+dj-database-url = [
+ {file = "dj-database-url-1.0.0.tar.gz", hash = "sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc"},
+ {file = "dj_database_url-1.0.0-py3-none-any.whl", hash = "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"},
]
django = [
- {file = "Django-3.1-py3-none-any.whl", hash = "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b"},
- {file = "Django-3.1.tar.gz", hash = "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b"},
+ {file = "Django-4.1.3-py3-none-any.whl", hash = "sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5"},
+ {file = "Django-4.1.3.tar.gz", hash = "sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1"},
+]
+django-allauth = [
+ {file = "django-allauth-0.51.0.tar.gz", hash = "sha256:ca1622733b6faa591580ccd3984042f12d8c79ade93438212de249b7ffb6f91f"},
]
django-cors-headers = [
- {file = "django-cors-headers-3.10.1.tar.gz", hash = "sha256:b5a874b492bcad99f544bb76ef679472259eb41ee5644ca62d1a94ddb26b7f6e"},
- {file = "django_cors_headers-3.10.1-py3-none-any.whl", hash = "sha256:1390b5846e9835b0911e2574409788af87cd9154246aafbdc8ec546c93698fe6"},
+ {file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"},
+ {file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"},
+]
+django-debug-toolbar = [
+ {file = "django-debug-toolbar-3.7.0.tar.gz", hash = "sha256:1e3acad24e3d351ba45c6fa2072e4164820307332a776b16c9f06d1f89503465"},
+ {file = "django_debug_toolbar-3.7.0-py3-none-any.whl", hash = "sha256:80de23066b624d3970fd296cf02d61988e5d56c31aa0dc4a428970b46e2883a8"},
+]
+django-environ = [
+ {file = "django-environ-0.9.0.tar.gz", hash = "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21"},
+ {file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"},
]
-django-elasticsearch-dsl = []
django-extensions = [
- {file = "django-extensions-3.1.5.tar.gz", hash = "sha256:28e1e1bf49f0e00307ba574d645b0af3564c981a6dfc87209d48cb98f77d0b1a"},
- {file = "django_extensions-3.1.5-py3-none-any.whl", hash = "sha256:9238b9e016bb0009d621e05cf56ea8ce5cce9b32e91ad2026996a7377ca28069"},
+ {file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"},
+ {file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"},
+]
+django-jazzmin = [
+ {file = "django_jazzmin-2.6.0-py3-none-any.whl", hash = "sha256:fb554c2d564649c65243b13385121fdbdda58521f49544f9d7cb9c414a4908d4"},
+ {file = "django_jazzmin-2.6.0.tar.gz", hash = "sha256:5bb07055cf19183030724f976904fd8b6337559727959340a43832fab0531812"},
]
-django-filter = [
- {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"},
- {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"},
+django-ninja = [
+ {file = "django-ninja-0.19.1.tar.gz", hash = "sha256:f52e6b5f25a83f7bda80398b3b97001011142911cff52a9dd711e63d711a36ab"},
+ {file = "django_ninja-0.19.1-py3-none-any.whl", hash = "sha256:062ed2b18e357f0556bba73f14132a7c12a5a61ebb79552162b9903e4f975008"},
]
-django-jazzmin = []
-django-silk = [
- {file = "django-silk-4.2.0.tar.gz", hash = "sha256:fcc2e3b2d20aab24a0b4b856d9692c532f8071b0e678cb5f4de47b802e85fe3e"},
- {file = "django_silk-4.2.0-py3-none-any.whl", hash = "sha256:5e70b5351f04e3c4ea4fe6bc16c95bfee776006422dd5e7827e6e3db23dc41ec"},
+django-ninja-extra = [
+ {file = "django-ninja-extra-0.16.0.tar.gz", hash = "sha256:34a8ae82a817a7601bb5b732bead66688fc843043851da08a048fba74209a827"},
+ {file = "django_ninja_extra-0.16.0-py3-none-any.whl", hash = "sha256:188755097d6b14c7130f8990a08ed003873413a05d24c47ddef168398235cac9"},
]
-django-types = [
- {file = "django-types-0.9.1.tar.gz", hash = "sha256:4341c9076d2a8fd9f8ba322f66587c9140a441d17dde6ab6f895b15662de1abf"},
- {file = "django_types-0.9.1-py3-none-any.whl", hash = "sha256:382ea3354526058753ae438495c53412d4912d0edaabf9bc0f9d6069713d4dbe"},
+django-ninja-jwt = [
+ {file = "django-ninja-jwt-5.2.2.tar.gz", hash = "sha256:64a7d7ba525069ba9e4f931fc06bd1327a541c9485089dbcb6826a74cda3cab5"},
+ {file = "django_ninja_jwt-5.2.2-py3-none-any.whl", hash = "sha256:345371837b279ea327a2e6cc21b9a8fd0490acc1b029b1b526e844cf2643aff7"},
]
-django-typomatic = [
- {file = "django-typomatic-1.6.1.tar.gz", hash = "sha256:9329899c7f7d37027aaa291023f7aaece17d39ab48ae56aa9573a37290c1c868"},
- {file = "django_typomatic-1.6.1-py3-none-any.whl", hash = "sha256:1f42c5ed4ed1623ddf421d9162815ed83ce59ef4b950b77a7d6fff5d42912b26"},
+django-rq = [
+ {file = "django-rq-2.6.0.tar.gz", hash = "sha256:f03b1eb68afe218175989c14b1266c7b446d5152f629888a078d0022059b255e"},
+ {file = "django_rq-2.6.0-py2.py3-none-any.whl", hash = "sha256:b1964ad656ae103d8b4714f8745e739586917b126f02c844791d9059940b01a0"},
]
-djangorestframework = [
- {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"},
- {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"},
+dnspython = [
+ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
+ {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
]
-djangorestframework-simplejwt = [
- {file = "djangorestframework_simplejwt-4.8.0-py3-none-any.whl", hash = "sha256:6f09f97cb015265e85d1d02dc6bfc299c72c231eecbe261c5bee5c6b2867f2b4"},
- {file = "djangorestframework_simplejwt-4.8.0.tar.gz", hash = "sha256:153c973c5c154baf566be431de8527c2bd62557fde7373ebcb0f02b73b28e07a"},
+email-validator = [
+ {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"},
+ {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"},
]
-elasticsearch = [
- {file = "elasticsearch-7.16.2-py2.py3-none-any.whl", hash = "sha256:c05aa792a52b1e6ad9d226340dc19165c4a491ac48fbd91af51ec839bf953210"},
- {file = "elasticsearch-7.16.2.tar.gz", hash = "sha256:23ac0afb4398c48990e359ac73ab6963741bd05321345299c62d9d23e209eee2"},
+exceptiongroup = [
+ {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
+ {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
]
-elasticsearch-dsl = [
- {file = "elasticsearch-dsl-7.4.0.tar.gz", hash = "sha256:c4a7b93882918a413b63bed54018a1685d7410ffd8facbc860ee7fd57f214a6d"},
- {file = "elasticsearch_dsl-7.4.0-py2.py3-none-any.whl", hash = "sha256:046ea10820b94c075081b528b4526c5bc776bda4226d702f269a5f203232064b"},
+executing = [
+ {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"},
+ {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"},
]
filelock = [
- {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"},
- {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"},
+ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
+ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
flake8 = [
- {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
- {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
-]
-gprof2dot = [
- {file = "gprof2dot-2021.2.21.tar.gz", hash = "sha256:1223189383b53dcc8ecfd45787ac48c0ed7b4dbc16ee8b88695d053eea1acabf"},
+ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
+ {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
gunicorn = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
]
-h2 = [
- {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"},
- {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"},
-]
-hpack = [
- {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"},
- {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"},
-]
-hyperframe = [
- {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"},
- {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"},
-]
hyperlink = [
{file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"},
{file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"},
]
identify = [
- {file = "identify-2.4.1-py2.py3-none-any.whl", hash = "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c"},
- {file = "identify-2.4.1.tar.gz", hash = "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9"},
+ {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"},
+ {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"},
]
idna = [
- {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
- {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
-]
-importlib-metadata = [
- {file = "importlib_metadata-4.10.0-py3-none-any.whl", hash = "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"},
- {file = "importlib_metadata-4.10.0.tar.gz", hash = "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6"},
+ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
incremental = [
- {file = "incremental-21.3.0-py2.py3-none-any.whl", hash = "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321"},
- {file = "incremental-21.3.0.tar.gz", hash = "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57"},
+ {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"},
+ {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
+injector = [
+ {file = "injector-0.19.0-py2.py3-none-any.whl", hash = "sha256:2e5f5629d9c42f8ed8a3f8a5b4e9dc8ac80547cd95c7ce0bd0fc3f4baae6bc77"},
+ {file = "injector-0.19.0.tar.gz", hash = "sha256:3eaaf51cd3ba7be1354d92a5210c8bba43dd324300eafd214e1f2568834a912f"},
+]
ipdb = [
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
]
ipython = [
- {file = "ipython-7.30.1-py3-none-any.whl", hash = "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c"},
- {file = "ipython-7.30.1.tar.gz", hash = "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97"},
+ {file = "ipython-8.6.0-py3-none-any.whl", hash = "sha256:91ef03016bcf72dd17190f863476e7c799c6126ec7e8be97719d1bc9a78a59a4"},
+ {file = "ipython-8.6.0.tar.gz", hash = "sha256:7c959e3dedbf7ed81f9b9d8833df252c430610e2a4a6464ec13cd20975ce20a5"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
itemadapter = [
- {file = "itemadapter-0.4.0-py3-none-any.whl", hash = "sha256:695809a4e2f42174f0392dd66c2ceb2b2454d3ebbf65a930e5c85910d8d88d8f"},
- {file = "itemadapter-0.4.0.tar.gz", hash = "sha256:f05df8da52619da4b8c7f155d8a15af19083c0c7ad941d8c1de799560ad994ca"},
+ {file = "itemadapter-0.7.0-py3-none-any.whl", hash = "sha256:0e0ab4ddf92c71af57c2386952a61756ae2ecf6c65f976ffaee9ba91ae87a91c"},
+ {file = "itemadapter-0.7.0.tar.gz", hash = "sha256:32c061ec9ab47d5343e8011b268730f48ff632a0192b95292d118b18dbd7687a"},
]
itemloaders = [
- {file = "itemloaders-1.0.4-py3-none-any.whl", hash = "sha256:4cb46a0f8915e910c770242ae3b60b1149913ed37162804f1e40e8535d6ec497"},
- {file = "itemloaders-1.0.4.tar.gz", hash = "sha256:1277cd8ca3e4c02dcdfbc1bcae9134ad89acfa6041bd15b4561c6290203a0c96"},
+ {file = "itemloaders-1.0.6-py3-none-any.whl", hash = "sha256:248702909af3ab45ae32846f5bdefa0166dc88cffb5f758d662223dcd0953bd9"},
+ {file = "itemloaders-1.0.6.tar.gz", hash = "sha256:8a6b2945a4233a14042a368e17950f447eb1d42494d75634552586342090cb4a"},
]
jedi = [
{file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
]
-jinja2 = [
- {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
- {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
-]
jmespath = [
- {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"},
- {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
-]
-kombu = [
- {file = "kombu-5.2.3-py3-none-any.whl", hash = "sha256:eeaeb8024f3a5cfc71c9250e45cddb8493f269d74ada2f74909a93c59c4b4179"},
- {file = "kombu-5.2.3.tar.gz", hash = "sha256:81a90c1de97e08d3db37dbf163eaaf667445e1068c98bfd89f051a40e9f6dbbd"},
+ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
+ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
lxml = [
- {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"},
- {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"},
- {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"},
- {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"},
- {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"},
- {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"},
- {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"},
- {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"},
- {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"},
- {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"},
- {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"},
- {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"},
- {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"},
- {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"},
- {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"},
- {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"},
- {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"},
- {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"},
- {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"},
- {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"},
- {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"},
- {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"},
- {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"},
- {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"},
- {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"},
- {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"},
- {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"},
- {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"},
- {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"},
- {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"},
- {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"},
- {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"},
- {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"},
- {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"},
- {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"},
- {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"},
- {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"},
- {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"},
- {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"},
- {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"},
- {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"},
- {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"},
- {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"},
- {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"},
- {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"},
- {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"},
- {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"},
- {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"},
- {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"},
- {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"},
- {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"},
- {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"},
- {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"},
- {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"},
- {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"},
- {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"},
- {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"},
- {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"},
- {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"},
- {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"},
+ {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"},
+ {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"},
+ {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"},
+ {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"},
+ {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"},
+ {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"},
+ {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"},
+ {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"},
+ {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"},
+ {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"},
+ {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"},
+ {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"},
+ {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"},
+ {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"},
+ {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"},
+ {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"},
+ {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"},
+ {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"},
+ {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"},
+ {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"},
+ {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"},
+ {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"},
+ {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"},
+ {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"},
+ {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"},
+ {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"},
+ {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"},
+ {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"},
+ {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"},
+ {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"},
+ {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"},
+ {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"},
+ {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"},
+ {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"},
+ {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"},
+ {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"},
+ {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"},
+ {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"},
+ {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"},
+ {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"},
+ {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"},
+ {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"},
+ {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"},
+ {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"},
+ {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"},
+ {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"},
+ {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"},
+ {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"},
+ {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"},
+ {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"},
+ {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"},
+ {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"},
+ {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"},
+ {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"},
+ {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"},
+ {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"},
+ {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"},
+ {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"},
+ {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"},
+ {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"},
+ {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"},
+ {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"},
+ {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"},
+ {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"},
+ {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"},
+ {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"},
+ {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"},
+ {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"},
+ {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"},
+ {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"},
]
markdown = [
- {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"},
- {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"},
-]
-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-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
- {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
- {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
- {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-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
- {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
- {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
- {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-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
- {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
- {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
- {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-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
- {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
- {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
- {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-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
- {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
- {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
- {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"},
+ {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"},
+ {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"},
]
matplotlib-inline = [
- {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
- {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
+ {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
+ {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
]
mccabe = [
- {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
- {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
+ninja-schema = [
+ {file = "ninja-schema-0.13.0.tar.gz", hash = "sha256:4063c70ddac3943ea10f25918beb032a709f983b0951caf350d431c6f1d034c5"},
+ {file = "ninja_schema-0.13.0-py3-none-any.whl", hash = "sha256:7296a3557e82ef9c9a6f545e811332f42d04a4fefbf2cfcec36d2edf1ad253a3"},
+]
nodeenv = [
- {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
- {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
+ {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
+]
+oauthlib = [
+ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
+ {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
]
orjson = [
- {file = "orjson-3.6.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:6c444edc073eb69cf85b28851a7a957807a41ce9bb3a9c14eefa8b33030cf050"},
- {file = "orjson-3.6.5-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:432c6da3d8d4630739f5303dcc45e8029d357b7ff8e70b7239be7bd047df6b19"},
- {file = "orjson-3.6.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:0fa32319072fadf0732d2c1746152f868a1b0f83c8cce2cad4996f5f3ca4e979"},
- {file = "orjson-3.6.5-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:0d65cc67f2e358712e33bc53810022ef5181c2378a7603249cd0898aa6cd28d4"},
- {file = "orjson-3.6.5-cp310-none-win_amd64.whl", hash = "sha256:fa8e3d0f0466b7d771a8f067bd8961bc17ca6ea4c89a91cd34d6648e6b1d1e47"},
- {file = "orjson-3.6.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:470596fbe300a7350fd7bbcf94d2647156401ab6465decb672a00e201af1813a"},
- {file = "orjson-3.6.5-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d2680d9edc98171b0c59e52c1ed964619be5cb9661289c0dd2e667773fa87f15"},
- {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001962a334e1ab2162d2f695f2770d2383c7ffd2805cec6dbb63ea2ad96bf0ad"},
- {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:522c088679c69e0dd2c72f43cd26a9e73df4ccf9ed725ac73c151bbe816fe51a"},
- {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:d2b871a745a64f72631b633271577c99da628a9b63e10bd5c9c20706e19fe282"},
- {file = "orjson-3.6.5-cp37-none-win_amd64.whl", hash = "sha256:51ab01fed3b3e21561f21386a2f86a0415338541938883b6ca095001a3014a3e"},
- {file = "orjson-3.6.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:fc7e62edbc7ece95779a034d9e206d7ba9e2b638cc548fd3a82dc5225f656625"},
- {file = "orjson-3.6.5-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0720d60db3fa25956011a573274a269eb37de98070f3bc186582af1222a2d084"},
- {file = "orjson-3.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169a8876aed7a5bff413c53257ef1fa1d9b68c855eb05d658c4e73ed8dff508"},
- {file = "orjson-3.6.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:331f9a3bdba30a6913ad1d149df08e4837581e3ce92bf614277d84efccaf796f"},
- {file = "orjson-3.6.5-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:ece5dfe346b91b442590a41af7afe61df0af369195fed13a1b29b96b1ba82905"},
- {file = "orjson-3.6.5-cp38-none-win_amd64.whl", hash = "sha256:6a5e9eb031b44b7a429c705ca48820371d25b9467c9323b6ae7a712daf15fbef"},
- {file = "orjson-3.6.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:206237fa5e45164a678b12acc02aac7c5b50272f7f31116e1e08f8bcaf654f93"},
- {file = "orjson-3.6.5-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d5aceeb226b060d11ccb5a84a4cfd760f8024289e3810ec446ef2993a85dbaca"},
- {file = "orjson-3.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80dba3dbc0563c49719e8cc7d1568a5cf738accfcd1aa6ca5e8222b57436e75e"},
- {file = "orjson-3.6.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:443f39bc5e7966880142430ce091e502aea068b38cb9db5f1ffdcfee682bc2d4"},
- {file = "orjson-3.6.5-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:a06f2dd88323a480ac1b14d5829fb6cdd9b0d72d505fabbfbd394da2e2e07f6f"},
- {file = "orjson-3.6.5-cp39-none-win_amd64.whl", hash = "sha256:82cb42dbd45a3856dbad0a22b54deb5e90b2567cdc2b8ea6708e0c4fe2e12be3"},
- {file = "orjson-3.6.5.tar.gz", hash = "sha256:eb3a7d92d783c89df26951ef3e5aca9d96c9c6f2284c752aa3382c736f950597"},
+ {file = "orjson-3.8.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:a70aaa2e56356e58c6e1b49f7b7f069df5b15e55db002a74db3ff3f7af67c7ff"},
+ {file = "orjson-3.8.1-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d45db052d01d0ab7579470141d5c3592f4402d43cfacb67f023bc1210a67b7bc"},
+ {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2aae92398c0023ac26a6cd026375f765ef5afe127eccabf563c78af7b572d59"},
+ {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bd5b4e539db8a9635776bdf9a25c3db84e37165e65d45c8ca90437adc46d6d8"},
+ {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21efb87b168066201a120b0f54a2381f6f51ff3727e07b3908993732412b314a"},
+ {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e073338e422f518c1d4d80efc713cd17f3ed6d37c8c7459af04a95459f3206d1"},
+ {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8f672f3987f6424f60ab2e86ea7ed76dd2806b8e9b506a373fc8499aed85ddb5"},
+ {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:231c30958ed99c23128a21993c5ac0a70e1e568e6a898a47f70d5d37461ca47c"},
+ {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59b4baf71c9f39125d7e535974b146cc180926462969f6d8821b4c5e975e11b3"},
+ {file = "orjson-3.8.1-cp310-none-win_amd64.whl", hash = "sha256:fe25f50dc3d45364428baa0dbe3f613a5171c64eb0286eb775136b74e61ba58a"},
+ {file = "orjson-3.8.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6802edf98f6918e89df355f56be6e7db369b31eed64ff2496324febb8b0aa43b"},
+ {file = "orjson-3.8.1-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a4244f4199a160717f0027e434abb886e322093ceadb2f790ff0c73ed3e17662"},
+ {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6956cf7a1ac97523e96f75b11534ff851df99a6474a561ad836b6e82004acbb8"},
+ {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b4e3857dd2416b479f700e9bdf4fcec8c690d2716622397d2b7e848f9833e50"},
+ {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8873e490dea0f9cd975d66f84618b6fb57b1ba45ecb218313707a71173d764f"},
+ {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:124207d2cd04e845eaf2a6171933cde40aebcb8c2d7d3b081e01be066d3014b6"},
+ {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d8ed77098c2e22181fce971f49a34204c38b79ca91c01d515d07015339ae8165"},
+ {file = "orjson-3.8.1-cp311-none-win_amd64.whl", hash = "sha256:8623ac25fa0850a44ac845e9333c4da9ae5707b7cec8ac87cbe9d4e41137180f"},
+ {file = "orjson-3.8.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:d67a0bd0283a3b17ac43c5ab8e4a7e9d3aa758d6ec5d51c232343c408825a5ad"},
+ {file = "orjson-3.8.1-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d89ef8a4444d83e0a5171d14f2ab4895936ab1773165b020f97d29cf289a2d88"},
+ {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97839a6abbebb06099294e6057d5b3061721ada08b76ae792e7041b6cb54c97f"},
+ {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6071bcf51f0ae4d53b9d3e9164f7138164df4291c484a7b14562075aaa7a2b7b"},
+ {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15e7d691cee75b5192fc1fa8487bf541d463246dc25c926b9b40f5b6ab56770"},
+ {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:b9abc49c014def1b832fcd53bdc670474b6fe41f373d16f40409882c0d0eccba"},
+ {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:3fd5472020042482d7da4c26a0ee65dbd931f691e1c838c6cf4232823179ecc1"},
+ {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e399ed1b0d6f8089b9b6ff2cb3e71ba63a56d8ea88e1d95467949795cc74adfd"},
+ {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e3db6496463c3000d15b7a712da5a9601c6c43682f23f81862fe1d2a338f295"},
+ {file = "orjson-3.8.1-cp37-none-win_amd64.whl", hash = "sha256:0f21eed14697083c01f7e00a87e21056fc8fb5851e8a7bca98345189abcdb4d4"},
+ {file = "orjson-3.8.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5a9e324213220578d324e0858baeab47808a13d3c3fbc6ba55a3f4f069d757cf"},
+ {file = "orjson-3.8.1-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69097c50c3ccbcc61292192b045927f1688ca57ce80525dc5d120e0b91e19bb0"},
+ {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7822cba140f7ca48ed0256229f422dbae69e3a3475176185db0c0538cfadb57"},
+ {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03389e3750c521a7f3d4837de23cfd21a7f24574b4b3985c9498f440d21adb03"},
+ {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f9d9b5c6692097de07dd0b2d5ff20fd135bacd1b2fb7ea383ee717a4150c93"},
+ {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:c2c9ef10b6344465fd5ac002be2d34f818211274dd79b44c75b2c14a979f84f3"},
+ {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7adaac93678ac61f5dc070f615b18639d16ee66f6a946d5221dbf315e8b74bec"},
+ {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c1750f73658906b82cabbf4be2f74300644c17cb037fbc8b48d746c3b90c76"},
+ {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:da6306e1f03e7085fe0db61d4a3377f70c6fd865118d0afe17f80ae9a8f6f124"},
+ {file = "orjson-3.8.1-cp38-none-win_amd64.whl", hash = "sha256:f532c2cbe8c140faffaebcfb34d43c9946599ea8138971f181a399bec7d6b123"},
+ {file = "orjson-3.8.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:6a7b76d4b44bca418f7797b1e157907b56b7d31caa9091db4e99ebee51c16933"},
+ {file = "orjson-3.8.1-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f850489d89ea12be486492e68f0fd63e402fa28e426d4f0b5fc1eec0595e6109"},
+ {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4449e70b98f3ad3e43958360e4be1189c549865c0a128e8629ec96ce92d251c3"},
+ {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45357eea9114bd41ef19280066591e9069bb4f6f5bffd533e9bfc12a439d735f"},
+ {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5a9bc5bc4d730153529cb0584c63ff286d50663ccd48c9435423660b1bb12d"},
+ {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a806aca6b80fa1d996aa16593e4995a71126a085ee1a59fff19ccad29a4e47fd"},
+ {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:395d02fd6be45f960da014372e7ecefc9e5f8df57a0558b7111a5fa8423c0669"},
+ {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:caff3c1e964cfee044a03a46244ecf6373f3c56142ad16458a1446ac6d69824a"},
+ {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ded261268d5dfd307078fe3370295e5eb15bdde838bbb882acf8538e061c451"},
+ {file = "orjson-3.8.1-cp39-none-win_amd64.whl", hash = "sha256:45c1914795ffedb2970bfcd3ed83daf49124c7c37943ed0a7368971c6ea5e278"},
+ {file = "orjson-3.8.1.tar.gz", hash = "sha256:07c42de52dfef56cdcaf2278f58e837b26f5b5af5f1fd133a68c4af203851fc7"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parsel = [
- {file = "parsel-1.6.0-py2.py3-none-any.whl", hash = "sha256:9e1fa8db1c0b4a878bf34b35c043d89c9d1cbebc23b4d34dbc3c0ec33f2e087d"},
- {file = "parsel-1.6.0.tar.gz", hash = "sha256:70efef0b651a996cceebc69e55a85eb2233be0890959203ba7c3a03c72725c79"},
+ {file = "parsel-1.7.0-py2.py3-none-any.whl", hash = "sha256:80ba5797b2a4968cdcdbd51c355e596f4441d0acd7f6d70f63a9e441e7fe45df"},
+ {file = "parsel-1.7.0.tar.gz", hash = "sha256:0254133cb0304de13fcc4857bb8214ff70d698872761fa6be8374e1bbbd58192"},
]
parso = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
]
pathspec = [
- {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
- {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+ {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
+ {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
pexpect = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
@@ -2190,129 +2240,141 @@ pickleshare = [
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
platformdirs = [
- {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"},
- {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"},
+ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
+ {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
pre-commit = [
- {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"},
- {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"},
-]
-priority = [
- {file = "priority-1.3.0-py2.py3-none-any.whl", hash = "sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb"},
- {file = "priority-1.3.0.tar.gz", hash = "sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe"},
+ {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
+ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
]
prompt-toolkit = [
- {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"},
- {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"},
+ {file = "prompt_toolkit-3.0.32-py3-none-any.whl", hash = "sha256:24becda58d49ceac4dc26232eb179ef2b21f133fecda7eed6018d341766ed76e"},
+ {file = "prompt_toolkit-3.0.32.tar.gz", hash = "sha256:e7f2129cba4ff3b3656bbdda0e74ee00d2f874a8bcdb9dd16f5fec7b3e173cae"},
]
protego = [
- {file = "Protego-0.1.16.tar.gz", hash = "sha256:a682771bc7b51b2ff41466460896c1a5a653f9a1e71639ef365a72e66d8734b4"},
+ {file = "Protego-0.2.1-py2.py3-none-any.whl", hash = "sha256:04419b18f20e8909f1691c6b678392988271cc2a324a72f9663cb3af838b4bf7"},
+ {file = "Protego-0.2.1.tar.gz", hash = "sha256:df666d4304dab774e2dc9feb208bb1ac8d71ea5ceec12f4c99eba30fbd642ff2"},
]
psycopg2 = [
- {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"},
- {file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"},
- {file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"},
- {file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"},
- {file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"},
- {file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"},
- {file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"},
- {file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"},
- {file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"},
- {file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"},
- {file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"},
+ {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"},
+ {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"},
+ {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"},
+ {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"},
+ {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"},
+ {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"},
+ {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"},
+ {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"},
+ {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"},
+ {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"},
+ {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"},
]
ptyprocess = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
-py = [
- {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
- {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+pure-eval = [
+ {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+ {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
pyasn1 = [
- {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
- {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
- {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
- {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
- {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
- {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
- {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
- {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
- {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
- {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
- {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pyasn1-modules = [
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
- {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
- {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
- {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
- {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
- {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
- {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
- {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
- {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
- {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
- {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
- {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
]
pycodestyle = [
- {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
- {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
+ {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
+pydantic = [
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
+ {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
+ {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
+ {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
+ {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
+ {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
+ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
+ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
+]
pydispatcher = [
- {file = "PyDispatcher-2.0.5.tar.gz", hash = "sha256:5570069e1b1769af1fe481de6dd1d3a388492acddd2cdad7a3bde145615d5caf"},
- {file = "PyDispatcher-2.0.5.zip", hash = "sha256:5be4a8be12805ef7d712dd9a93284fb8bc53f309867e573f653a72e5fd10e433"},
+ {file = "PyDispatcher-2.0.6.tar.gz", hash = "sha256:3d7e4f43c70000a1dca31f92694e99d0101934fa6eab5d5455a758858d86df95"},
]
pyflakes = [
- {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
- {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
+ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
pygments = [
- {file = "Pygments-2.11.1-py3-none-any.whl", hash = "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"},
- {file = "Pygments-2.11.1.tar.gz", hash = "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4"},
+ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
+ {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
]
pyjwt = [
- {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"},
- {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"},
+ {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
+ {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
]
pyopenssl = [
- {file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"},
- {file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"},
+ {file = "pyOpenSSL-22.1.0-py3-none-any.whl", hash = "sha256:b28437c9773bb6c6958628cf9c3bebe585de661dba6f63df17111966363dd15e"},
+ {file = "pyOpenSSL-22.1.0.tar.gz", hash = "sha256:7a83b7b272dd595222d672f5ce29aa030f1fb837630ef229f62e72e395ce8968"},
]
pyparsing = [
- {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
- {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pypydispatcher = [
{file = "PyPyDispatcher-2.1.2.tar.gz", hash = "sha256:b6bec5dfcff9d2535bca2b23c80eae367b1ac250a645106948d315fcfa9130f2"},
]
pytest = [
- {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
- {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
+ {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+]
+pytest-django = [
+ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
+ {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
-python-dotenv = [
- {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"},
- {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"},
-]
-pytz = [
- {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
- {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
+python3-openid = [
+ {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
+ {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
]
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@@ -2322,6 +2384,13 @@ pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
+ {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
+ {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
@@ -2354,124 +2423,81 @@ queuelib = [
{file = "queuelib-1.6.2.tar.gz", hash = "sha256:4b207267f2642a8699a1f806045c56eb7ad1a85a10c0e249884580d139c2fcd2"},
]
redis = [
- {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
- {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
-]
-regex = [
- {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"},
- {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"},
- {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"},
- {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"},
- {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"},
- {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"},
- {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"},
- {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"},
- {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"},
- {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"},
- {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"},
- {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"},
- {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"},
- {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"},
- {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"},
- {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"},
- {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"},
- {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"},
- {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"},
- {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"},
- {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"},
- {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"},
- {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"},
- {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"},
- {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"},
- {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"},
- {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"},
- {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"},
- {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"},
- {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"},
- {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"},
- {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"},
- {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"},
- {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"},
- {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"},
- {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"},
- {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"},
- {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"},
- {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"},
- {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"},
- {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"},
- {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"},
- {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"},
- {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"},
- {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"},
- {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"},
- {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"},
- {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"},
- {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"},
+ {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"},
+ {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"},
]
requests = [
- {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
- {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
+ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+]
+requests-file = [
+ {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"},
+ {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"},
+]
+requests-oauthlib = [
+ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
+ {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
]
rich = [
- {file = "rich-10.16.1-py3-none-any.whl", hash = "sha256:bbe04dd6ac09e4b00d22cb1051aa127beaf6e16c3d8687b026e96d3fca6aad52"},
- {file = "rich-10.16.1.tar.gz", hash = "sha256:4949e73de321784ef6664ebbc854ac82b20ff60b2865097b93f3b9b41e30da27"},
+ {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
+ {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
+]
+rq = [
+ {file = "rq-1.11.1-py2.py3-none-any.whl", hash = "sha256:433882bde50ac462eb489dc1ffa476209c42b284d0031270631da26363923702"},
+ {file = "rq-1.11.1.tar.gz", hash = "sha256:31c07e55255bdc05c804902d4e15779185603b04b9161b43c3e7bcac84b3343b"},
+]
+rq-scheduler = [
+ {file = "rq-scheduler-0.11.0.tar.gz", hash = "sha256:db79bb56cdbc4f7ffdd8bd659e389e91aa0db9c1abf002dc46f5dd6f0dbd2910"},
+ {file = "rq_scheduler-0.11.0-py2.py3-none-any.whl", hash = "sha256:da94e9b6badf112995ff38fe16192e4f4c43c412b3c9614684ed8c8f7ca517d2"},
]
scrapy = [
- {file = "Scrapy-2.5.1-py2.py3-none-any.whl", hash = "sha256:1a9a36970004950ee3c519a14c4db945f9d9a63fecb3d593dddcda477331dde9"},
- {file = "Scrapy-2.5.1.tar.gz", hash = "sha256:13af6032476ab4256158220e530411290b3b934dd602bb6dacacbf6d16141f49"},
+ {file = "Scrapy-2.7.1-py2.py3-none-any.whl", hash = "sha256:ba5c99a0a6bc5d51708fb1919843cad650711d2baef3124e4ac3f2e1759f4ca6"},
+ {file = "Scrapy-2.7.1.tar.gz", hash = "sha256:30fa408353d24b1df979df2ea4afbd19b4ae02fb2207f218d246332f1e1cf14e"},
]
+scrapyscript = []
sentry-sdk = [
- {file = "sentry-sdk-1.5.1.tar.gz", hash = "sha256:2a1757d6611e4bec7d672c2b7ef45afef79fed201d064f53994753303944f5a8"},
- {file = "sentry_sdk-1.5.1-py2.py3-none-any.whl", hash = "sha256:e4cb107e305b2c1b919414775fa73a9997f996447417d22b98e7610ded1e9eb5"},
+ {file = "sentry-sdk-1.11.0.tar.gz", hash = "sha256:e7b78a1ddf97a5f715a50ab8c3f7a93f78b114c67307785ee828ef67a5d6f117"},
+ {file = "sentry_sdk-1.11.0-py2.py3-none-any.whl", hash = "sha256:f467e6c7fac23d4d42bc83eb049c400f756cd2d65ab44f0cc1165d0c7c3d40bc"},
]
service-identity = [
{file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"},
{file = "service_identity-21.1.0-py2.py3-none-any.whl", hash = "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"},
]
+setuptools = [
+ {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
+ {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
+]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sqlparse = [
- {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
- {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
+ {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
+ {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
+]
+stack-data = [
+ {file = "stack_data-0.6.0-py3-none-any.whl", hash = "sha256:b92d206ef355a367d14316b786ab41cb99eb453a21f2cb216a4204625ff7bc07"},
+ {file = "stack_data-0.6.0.tar.gz", hash = "sha256:8e515439f818efaa251036af72d89e4026e2b03993f3453c000b200fb4f2d6aa"},
+]
+tldextract = [
+ {file = "tldextract-3.4.0-py3-none-any.whl", hash = "sha256:47aa4d8f1a4da79a44529c9a2ddc518663b25d371b805194ec5ce2a5f615ccd2"},
+ {file = "tldextract-3.4.0.tar.gz", hash = "sha256:78aef13ac1459d519b457a03f1f74c1bf1c2808122a6bcc0e6840f81ba55ad73"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
traitlets = [
- {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"},
- {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"},
+ {file = "traitlets-5.5.0-py3-none-any.whl", hash = "sha256:1201b2c9f76097195989cdf7f65db9897593b0dfd69e4ac96016661bb6f0d30f"},
+ {file = "traitlets-5.5.0.tar.gz", hash = "sha256:b122f9ff2f2f6c1709dab289a05555be011c87828e911c0cf4074b85cb780a79"},
]
twisted = [
- {file = "Twisted-21.7.0-py3-none-any.whl", hash = "sha256:13c1d1d2421ae556d91e81e66cf0d4f4e4e1e4a36a0486933bee4305c6a4fb9b"},
- {file = "Twisted-21.7.0.tar.gz", hash = "sha256:2cd652542463277378b0d349f47c62f20d9306e57d1247baabd6d1d38a109006"},
+ {file = "Twisted-22.10.0-py3-none-any.whl", hash = "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0"},
+ {file = "Twisted-22.10.0.tar.gz", hash = "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31"},
]
twisted-iocpsupport = [
{file = "twisted-iocpsupport-1.0.2.tar.gz", hash = "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9"},
@@ -2487,92 +2513,28 @@ twisted-iocpsupport = [
{file = "twisted_iocpsupport-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546"},
{file = "twisted_iocpsupport-1.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf"},
]
-typed-ast = [
- {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"},
- {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"},
- {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"},
- {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"},
- {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"},
- {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"},
- {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"},
- {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"},
- {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"},
- {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"},
- {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"},
- {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"},
- {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"},
- {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"},
- {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"},
- {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"},
- {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"},
- {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"},
- {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"},
+typesense = [
+ {file = "typesense-0.14.0-py2.py3-none-any.whl", hash = "sha256:0ee444351d59243b51d1ea7502dc41e14a3997f954269519c3a445b39b137bba"},
]
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"},
-]
-ujson = [
- {file = "ujson-4.3.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3609e0514f6f721c6c9818b9374ec91b994e59fb193af2f924ca3f2f32009f1c"},
- {file = "ujson-4.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de42986e2602b6a0baca452ff50e9cbe66faf256761295d5d07ae3f6757b487d"},
- {file = "ujson-4.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:843fd8b3246b2b20bbae48b2334d26507c9531b2b014533adfc6132e3ec8e60c"},
- {file = "ujson-4.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d1083a0dcb39b43cfcd948f09e480c23eb4af66d7d08f6b36951f4c629c3bd1"},
- {file = "ujson-4.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:01d12df8eb25afb939a003284b5b5adca9788c1176c445641e5980fa892562ac"},
- {file = "ujson-4.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b0b9cde57eebaac26de040f8ebf0541e06fe9bcf7e42872dc036d2ced7d99ccf"},
- {file = "ujson-4.3.0-cp310-cp310-win32.whl", hash = "sha256:3d8eaab72ad8129c12ed90ebf310230bd014b6bbf99145ebf2bc890238e0254f"},
- {file = "ujson-4.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:85f28c38952b8a94183ab15ec6c6e89c117d00ceeae5d754ef1a33e01e28b845"},
- {file = "ujson-4.3.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8a0d9dde58937976cd06cd776411b77b0e5d38db0a3c1be28ee8bb428ff5a42b"},
- {file = "ujson-4.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4a34386785a33600ac7442fec34c3d8b2d7e5309cfc94bc7c9ba93f12640c2"},
- {file = "ujson-4.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8e2a52fbeee55db306b9306892f5cde7e78c56069c1212abf176d1886fff60a"},
- {file = "ujson-4.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c5330692122b999997911252466a7d17e4e428d7d9a8db0b99ba81b8b9c010c"},
- {file = "ujson-4.3.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9baa160ba1d3f712a356e77718251c9d9eee43ed548debdcc9d75b06a75b3e82"},
- {file = "ujson-4.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a6c32356145d95a0403b5895d60c36798a48af13b8863e43ad7457a0361afad0"},
- {file = "ujson-4.3.0-cp36-cp36m-win32.whl", hash = "sha256:b72fadeea5727204674c9f77166da7feaafdf70f1ed50bb15bf321f7c39c7194"},
- {file = "ujson-4.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1601354caaab0697a9b24815a31611ad013d29cf957d545fc1cd59835b82e3c1"},
- {file = "ujson-4.3.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b80a35bad8fad1772f992bae8086b0cde788cd3b37f35d0d4506c93e6edad645"},
- {file = "ujson-4.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a318df321d7adc3de876b29640cca8de1ad4d4e4fe7c4a76d64d9d6f1676304"},
- {file = "ujson-4.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9a508efb829bf0542be9b2578d8da08f0ab1fa712e086ebb777d6ec9e6d8d2"},
- {file = "ujson-4.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43d2403451d7bd27b6a600f89d4bd2cf6e1b3494254509d8b5ef3c8e94ae4d8e"},
- {file = "ujson-4.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fd0901db652a58f46550074596227dbddb7a02d2de744d3cd2358101f78037bb"},
- {file = "ujson-4.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00fd67952b1a8a46cf5b0a51b3838187332d13d2e8d178423c5a5405c21d9e7c"},
- {file = "ujson-4.3.0-cp37-cp37m-win32.whl", hash = "sha256:b0e9510e867c72a87db2d16377c2bef912f29afd8381d1fdae332b9b7f697efa"},
- {file = "ujson-4.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:294e907f134fb5d83e0a4439cf4040d74da77157938b4db5730cd174621dcf8b"},
- {file = "ujson-4.3.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:087cd977f4f63f885a49607244e7e157801a22aadcc075a262d3c3633138573c"},
- {file = "ujson-4.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f35dcf6d2a67e913a7135809006bd000d55ad5b5834b5dbe5b82dcf8db1ac05"},
- {file = "ujson-4.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f158fdb08e022f2f16f0fba317a80558b0cebc7e2c84ae783e5f75616d5c90d5"},
- {file = "ujson-4.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a06006dad34c8cfaa734bd6458452e46702b368da53b56e7732351082aa0420"},
- {file = "ujson-4.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6df94e675b05ecf4e7a57883a73b916ffcb5872d7b1298ac5cef8ac1cbce73c6"},
- {file = "ujson-4.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:47af81df5d575e36d4be9396db94f35c8f62de3077a405f9af94f9756255cef5"},
- {file = "ujson-4.3.0-cp38-cp38-win32.whl", hash = "sha256:e46c1462761db518fae51ab0d89a6256aeac148a795f7244d9084c459b477af5"},
- {file = "ujson-4.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bf199015910fcfa19b6e12881abeb462498791b2ab0111ff8b17095d0477e9d4"},
- {file = "ujson-4.3.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:32ee97ec37af31b35ca4395732d883bf74fb70309d38485f7fb9a5cc3332c53e"},
- {file = "ujson-4.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f211c7c0c9377cbf4650aa990118d0c2cce3c5fad476c39ecd35b6714ba4463"},
- {file = "ujson-4.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c81159d3f1bcb5729ba019e63e78ee6c91b556e1ac0e67c7579768720fd3c4e"},
- {file = "ujson-4.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b850029d64008e970cae04ada69aa33e1cd412106a1efde221269c1cda1b40cc"},
- {file = "ujson-4.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:327ec982bb89abe779fe463e1013c47aae6ed53b76600af7cb1e8b8cb0ee9f85"},
- {file = "ujson-4.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:103cbabe4e6fd70c957219519e37d65be612d7c74d91ef19022a2c8f8c5e4e82"},
- {file = "ujson-4.3.0-cp39-cp39-win32.whl", hash = "sha256:7b0a63865ec2978ebafb0906bf982eb52bea26fc98e2ae5e59b9d204afe2d762"},
- {file = "ujson-4.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:18040475d997d93a6851d8bee474fba2ec94869e8f826dddd66cdae4aa3fdb92"},
- {file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df481d4e13ca34d870d1fdf387742867edff3f78a1eea1bbcd72ea2fa68d9a6e"},
- {file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e73ec5ba1b42c2027773f69b70eff28df132907aa98b28166c39d3ea45e85b"},
- {file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b270088e472f1d65a0a0aab3190010b9ac1a5b2969d39bf2b53c0fbf339bc87a"},
- {file = "ujson-4.3.0.tar.gz", hash = "sha256:baee56eca35cb5fbe02c28bd9c0936be41a96fa5c0812d9d4b7edeb5c3d568a0"},
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
-urllib3 = [
- {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
- {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
+tzdata = [
+ {file = "tzdata-2022.6-py2.py3-none-any.whl", hash = "sha256:04a680bdc5b15750c39c12a448885a51134a27ec9af83667663f0b3a1bf3f342"},
+ {file = "tzdata-2022.6.tar.gz", hash = "sha256:91f11db4503385928c15598c98573e3af07e7229181bee5375bd30f1695ddcae"},
]
-vine = [
- {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"},
- {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"},
+urllib3 = [
+ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
+ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
]
virtualenv = [
- {file = "virtualenv-20.12.1-py2.py3-none-any.whl", hash = "sha256:a5bb9afc076462ea736b0c060829ed6aef707413d0e5946294cc26e3c821436a"},
- {file = "virtualenv-20.12.1.tar.gz", hash = "sha256:d51ae01ef49e7de4d2b9d85b4926ac5aabc3f3879a4b4e4c4a8027fa2f0e4f6a"},
+ {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"},
+ {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"},
]
w3lib = [
- {file = "w3lib-1.22.0-py2.py3-none-any.whl", hash = "sha256:0161d55537063e00d95a241663ede3395c4c6d7b777972ba2fd58bbab2001e53"},
- {file = "w3lib-1.22.0.tar.gz", hash = "sha256:0ad6d0203157d61149fd45aaed2e24f53902989c32fc1dccc2e2bfba371560df"},
+ {file = "w3lib-2.0.1-py3-none-any.whl", hash = "sha256:c5d966f86ae3fb546854478c769250c3ccb7581515b3221bcd2f864440000188"},
+ {file = "w3lib-2.0.1.tar.gz", hash = "sha256:13df15f8c17b163de0fd5faa892c1ad143e190dfcbdb98534bb975eb37c6c7d6"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
@@ -2583,116 +2545,114 @@ werkzeug = [
{file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"},
]
whitenoise = [
- {file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"},
- {file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"},
+ {file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"},
+ {file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"},
]
wrapt = [
- {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"},
- {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"},
- {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"},
- {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"},
- {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"},
- {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"},
- {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"},
- {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"},
- {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"},
- {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"},
- {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"},
- {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"},
- {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"},
- {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"},
- {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"},
- {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"},
- {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"},
- {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"},
- {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"},
- {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"},
- {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"},
- {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"},
- {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"},
- {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"},
- {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"},
- {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"},
- {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"},
- {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"},
- {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"},
- {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"},
- {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"},
- {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"},
- {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"},
- {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"},
- {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"},
- {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"},
- {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"},
- {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"},
- {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"},
- {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"},
- {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"},
- {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"},
- {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"},
- {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"},
- {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"},
- {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"},
- {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"},
- {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"},
- {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"},
- {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"},
- {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"},
-]
-zipp = [
- {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"},
- {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"},
-]
-"zope.interface" = [
- {file = "zope.interface-5.4.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7"},
- {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021"},
- {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192"},
- {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a"},
- {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531"},
- {file = "zope.interface-5.4.0-cp27-cp27m-win32.whl", hash = "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325"},
- {file = "zope.interface-5.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155"},
- {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"},
- {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959"},
- {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e"},
- {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c"},
- {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702"},
- {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f"},
- {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05"},
- {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004"},
- {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117"},
- {file = "zope.interface-5.4.0-cp35-cp35m-win32.whl", hash = "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8"},
- {file = "zope.interface-5.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63"},
- {file = "zope.interface-5.4.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f"},
- {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920"},
- {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46"},
- {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc"},
- {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9"},
- {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2"},
- {file = "zope.interface-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78"},
- {file = "zope.interface-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1"},
- {file = "zope.interface-5.4.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e"},
- {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b"},
- {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f"},
- {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d"},
- {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8"},
- {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf"},
- {file = "zope.interface-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7"},
- {file = "zope.interface-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94"},
- {file = "zope.interface-5.4.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3"},
- {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e"},
- {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7"},
- {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120"},
- {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48"},
- {file = "zope.interface-5.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4"},
- {file = "zope.interface-5.4.0-cp38-cp38-win32.whl", hash = "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb"},
- {file = "zope.interface-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54"},
- {file = "zope.interface-5.4.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4"},
- {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d"},
- {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83"},
- {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25"},
- {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1"},
- {file = "zope.interface-5.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c"},
- {file = "zope.interface-5.4.0-cp39-cp39-win32.whl", hash = "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e"},
- {file = "zope.interface-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09"},
- {file = "zope.interface-5.4.0.tar.gz", hash = "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e"},
+ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
+ {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
+ {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
+ {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
+ {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
+ {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
+ {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
+ {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
+ {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
+ {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
+ {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
+ {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
+ {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
+ {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
+ {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
+ {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
+ {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
+ {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
+ {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
+ {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
+ {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
+ {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
+ {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
+ {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
+ {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
+ {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
+ {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
+ {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
+ {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
+ {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
+ {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
+ {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
+ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
+ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
+]
+zope-interface = [
+ {file = "zope.interface-5.5.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dd4b9251e95020c3d5d104b528dbf53629d09c146ce9c8dfaaf8f619ae1cce35"},
+ {file = "zope.interface-5.5.1-cp27-cp27m-win32.whl", hash = "sha256:061a41a3f96f076686d7f1cb87f3deec6f0c9f0325dcc054ac7b504ae9bb0d82"},
+ {file = "zope.interface-5.5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:7f2e4ebe0a000c5727ee04227cf0ff5ae612fe599f88d494216e695b1dac744d"},
+ {file = "zope.interface-5.5.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:c9552ee9e123b7997c7630fb95c466ee816d19e721c67e4da35351c5f4032726"},
+ {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a923d2dec50f2b4d41ce198af3516517f2e458220942cf393839d2f9e22000"},
+ {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a20fc9cccbda2a28e8db8cabf2f47fead7e9e49d317547af6bf86a7269e4b9a1"},
+ {file = "zope.interface-5.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6f51ffbdcf865f140f55c484001415505f5e68eb0a9eab1d37d0743b503b423"},
+ {file = "zope.interface-5.5.1-cp310-cp310-win32.whl", hash = "sha256:8de7bde839d72d96e0c92e8d1fdb4862e89b8fc52514d14b101ca317d9bcf87c"},
+ {file = "zope.interface-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:90f611d4cdf82fb28837fe15c3940255755572a4edf4c72e2306dbce7dcb3092"},
+ {file = "zope.interface-5.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2f2ec42fbc21e1af5f129ec295e29fee6f93563e6388656975caebc5f851561"},
+ {file = "zope.interface-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489c4c46fcbd9364f60ff0dcb93ec9026eca64b2f43dc3b05d0724092f205e27"},
+ {file = "zope.interface-5.5.1-cp311-cp311-win32.whl", hash = "sha256:9ad58724fabb429d1ebb6f334361f0a3b35f96be0e74bfca6f7de8530688b2df"},
+ {file = "zope.interface-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:a69f6d8b639f2317ba54278b64fef51d8250ad2c87acac1408b9cc461e4d6bb6"},
+ {file = "zope.interface-5.5.1-cp35-cp35m-win32.whl", hash = "sha256:d743b03a72fefed807a4512c079fb1aa5e7777036cc7a4b6ff79ae4650a14f73"},
+ {file = "zope.interface-5.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:3e42b1c3f4fd863323a8275c52c78681281a8f2e1790f0e869d911c1c7b25c46"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:7b4547a2f624a537e90fb99cec4d8b3b6be4af3f449c3477155aae65396724ad"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a96d499ff6faa9b85b1309f50bf3744eb786e24833f7b500cbb7052dc4ae29"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3c293c5c0e1cabe59c33e0d02fcee5c3eb365f79a20b8199a26ca784e406bd0d"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8c8764226daad39004b7873c3880eb4860c594ff549ea47c045cdf313e1bad5"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-win32.whl", hash = "sha256:4477930451521ac7da97cc31d49f7b83086d5ae76e52baf16aac659053119f6d"},
+ {file = "zope.interface-5.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:27c53aa2f46d42940ccdcb015fd525a42bf73f94acd886296794a41f229d5946"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:2204a9d545fdbe0d9b0bf4d5e2fc67e7977de59666f7131c1433fde292fc3b41"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:475b6e371cdbeb024f2302e826222bdc202186531f6dc095e8986c034e4b7961"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a1393229c9c126dd1b4356338421e8882347347ab6fe3230cb7044edc813e424"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e4988d94962f517f6da2d52337170b84856905b31b7dc504ed9c7b7e4bab2fc3"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-win32.whl", hash = "sha256:0eda7f61da6606a28b5efa5d8ad79b4b5bb242488e53a58993b2ec46c924ffee"},
+ {file = "zope.interface-5.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:185f0faf6c3d8f2203e8755f7ca16b8964d97da0abde89c367177a04e36f2568"},
+ {file = "zope.interface-5.5.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:026e7da51147910435950a46c55159d68af319f6e909f14873d35d411f4961db"},
+ {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58331d2766e8e409360154d3178449d116220348d46386430097e63d02a1b6d2"},
+ {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0587d238b7867544134f4dcca19328371b8fd03fc2c56d15786f410792d0a68"},
+ {file = "zope.interface-5.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd423d49abcf0ebf02c29c3daffe246ff756addb891f8aab717b3a4e2e1fd675"},
+ {file = "zope.interface-5.5.1-cp38-cp38-win32.whl", hash = "sha256:13a7c6e3df8aa453583412de5725bf761217d06f66ff4ed776d44fbcd13ec4e4"},
+ {file = "zope.interface-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:72a93445937cc71f0b8372b0c9e7c185328e0db5e94d06383a1cb56705df1df4"},
+ {file = "zope.interface-5.5.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:6cb8f9a1db47017929634264b3fc7ea4c1a42a3e28d67a14f14aa7b71deaa0d2"},
+ {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e5540b7d703774fd171b7a7dc2a3cb70e98fc273b8b260b1bf2f7d3928f125b"},
+ {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d1f2d91c9c6cd54d750fa34f18bd73c71b372d0e6d06843bc7a5f21f5fd66fe0"},
+ {file = "zope.interface-5.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:76cf472c79d15dce5f438a4905a1309be57d2d01bc1de2de30bda61972a79ab4"},
+ {file = "zope.interface-5.5.1-cp39-cp39-win32.whl", hash = "sha256:509a8d39b64a5e8d473f3f3db981f3ca603d27d2bc023c482605c1b52ec15662"},
+ {file = "zope.interface-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c"},
+ {file = "zope.interface-5.5.1.tar.gz", hash = "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2"},
]
diff --git a/pyproject.toml b/pyproject.toml
index 1a1d47ec..211cefdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,55 +2,48 @@
name = "backend"
version = "0.1.0"
description = "Backend for Sora Reader"
-authors = [
- "dhvcc <1337kwiz@gmail.com>",
- "ScriptHound ",
- "NikDark ",
-]
+authors = ["dhvcc <1337kwiz@gmail.com>"]
license = "GPLv3"
[tool.poetry.dependencies]
-python = "^3.8"
-Django = "3.1"
-psycopg2 = "^2.8.6"
-djangorestframework = "^3.12.2"
+python = "^3.10"
Markdown = "^3.3.3"
-django-filter = "^2.4.0"
-python-dotenv = "^0.15.0"
-djangorestframework-simplejwt = "^4.6.0"
-Scrapy = "^2.4.1"
-argon2-cffi = "^20.1.0"
-django-extensions = "^3.1.1"
-whitenoise = "^5.2.0"
-django-jazzmin = { git = "https://github.com/dhvcc/django-jazzmin.git", rev = "488036718b0c2a9d9b928c8dca257164db64857d" }
+psycopg2 = "^2.9.5"
+Django = "^4.1.3"
+django-extensions = "^3.2.1"
+Werkzeug = "0.16.0"
+django-environ = "^0.9.0"
+dj-database-url = "^1.0.0"
+django-cors-headers = "^3.13.0"
+whitenoise = { extras = ["brotli"], version = "^6.2.0" }
+django-jazzmin = "^2.6.0"
+django-ninja = "^0.19.1"
+django-ninja-jwt = "^5.2.2"
+django-allauth = "^0.51.0"
+django-rq = "^2.6.0"
+rq-scheduler = "^0.11.0"
+Scrapy = "^2.7.1"
+scrapyscript = {git = "https://github.com/dhvcc/scrapyscript.git", rev = "error-handling"}
requests = "^2.25.1"
-pytz = "^2021.1"
-dateutils = "^0.6.12"
-django-cors-headers = "^3.7.0"
-redis = "^3.5.3"
-autoflake = "^1.4"
-sentry-sdk = "^1.3.0"
+redis = "^4.3.4"
+autoflake = "^1.7.7"
gunicorn = "^20.1.0"
-Werkzeug = "0.16.0"
-django-silk = "^4.1.0"
-orjson = "^3.6.4"
-ujson = "^4.2.0"
-django-elasticsearch-dsl = { git = "https://github.com/dhvcc/django-elasticsearch-dsl.git", rev = "53598a336915a795467233f091f3c30836f0f758" }
-colorlog = "^6.5.0"
-celery = "^5.2.0"
-crochet = "^2.0.0"
+orjson = "^3.8.1"
+typesense = "^0.14.0"
+colorlog = "^6.7.0"
+sentry-sdk = "^1.11.0"
-[tool.poetry.dev-dependencies]
-flake8 = "^3.8.4"
-black = "^20.8b1"
+[tool.poetry.group.dev.dependencies]
+flake8 = "^5.0.4"
+black = "^22.8.0"
isort = "^5.7.0"
-pytest = "^6.2.2"
-ipython = "^7.21.0"
pre-commit = "^2.11.1"
-rich = "^10.1.0"
-django-typomatic = "^1.5.0"
+rich = "^12.5.1"
ipdb = "^0.13.9"
-django-types = "^0.9.0"
+autoflake = "^1.6.0"
+django-debug-toolbar = "^3.7.0"
+pytest = "^7.2.0"
+pytest-django = "^4.5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -58,7 +51,7 @@ build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 100
-target-version = ['py37', 'py38']
+target-version = ['py310']
force-exclude = '''
(
\.eggs
@@ -67,7 +60,6 @@ force-exclude = '''
| dist
| venv
| .venv
- | migrations
)
'''
@@ -80,4 +72,4 @@ use_parentheses = true
ensure_newline_before_comments = true
line_length = 100
skip_gitignore = true
-skip_glob = ['**/migrations/**', '**/.venv/**']
+skip_glob = ['**/.venv/**']
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..69d065e0
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = manga_reader.settings
+python_files = tests.py test_*.py *_tests.py
+
diff --git a/scrapy.cfg b/scrapy.cfg
index 8e5864c0..0f7c09f1 100644
--- a/scrapy.cfg
+++ b/scrapy.cfg
@@ -1,3 +1,5 @@
[settings]
-default = apps.parse.readmanga.settings
-readmanga = apps.parse.readmanga.settings
+default = apps.readmanga.settings
+readmanga = apps.readmanga.settings
+mangachan = apps.mangachan.settings
+
diff --git a/scripts/dev-entrypoint.sh b/scripts/dev-entrypoint.sh
new file mode 100755
index 00000000..65df3da1
--- /dev/null
+++ b/scripts/dev-entrypoint.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+info "Debug mode" "Installing dependencies"
+sudo chown sora:sora ~/.cache/pypoetry/
+poetry install
+
+./scripts/entrypoint.sh
\ No newline at end of file
diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh
new file mode 100755
index 00000000..6904ce35
--- /dev/null
+++ b/scripts/entrypoint.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+ . "$(poetry env info --path)/bin/activate"
+
+function info () {
+ echo ""
+ echo "$1"
+ echo "==============="
+ echo "$2"
+}
+export -f info
+
+info "Waiting for postgres"
+until psql "$DATABASE_URL" -c ';'; do
+ echo >&2 "Postgres is unavailable - sleeping"
+ sleep 1
+done
+
+info "Running migrations"
+python ./manage.py migrate --no-input
+
+info "Waiting for typesense"
+until curl "http://$TYPESENSE_HOST:8108/health"; do
+ echo >&2 "Typesense is unavailable - sleeping"
+ sleep 1
+done
+
+./scripts/run-webserver.sh
diff --git a/scripts/redeploy-hook.sh b/scripts/redeploy-hook.sh
new file mode 100755
index 00000000..34a941bc
--- /dev/null
+++ b/scripts/redeploy-hook.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# Script to redeploy data stuff and rq worker. Used by server's webhook server on push
+
+git fetch --all
+git reset --hard "origin/$(git branch --show-current)"
+
+cd ..
+docker-compose --profile prod-partial up -d --build
\ No newline at end of file
diff --git a/scripts/run-webserver.sh b/scripts/run-webserver.sh
new file mode 100755
index 00000000..943453af
--- /dev/null
+++ b/scripts/run-webserver.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+HOST="${HOST:=0.0.0.0}"
+PORT="${PORT:=8000}"
+
+# Get core count
+core_count=$(grep 'cpu[0-9]+' /proc/stat | wc -l)
+# Calculate worker count, max 12
+worker_count=$(expr $core_count \* 2 + 1)
+worker_count=$([ "$worker_count" -gt 12 ] && echo 12 || echo "$worker_count")
+
+info "Running the server on $HOST:$PORT"
+gunicorn manga_reader.asgi:application \
+ --host $HOST --port $PORT --workers $core_count
diff --git a/static/admin/css/custom_jazzmin.css b/static/admin/css/custom_jazzmin.css
index f535cb12..b7dd2a42 100644
--- a/static/admin/css/custom_jazzmin.css
+++ b/static/admin/css/custom_jazzmin.css
@@ -48,4 +48,8 @@ a:hover {
color: #4db6ac !important;
}
+.selector-chosen h2 {
+ background-color: #4db6ac !important;
+}
+
/*# sourceMappingURL=custom_jazzmin.css.map */
diff --git a/static/admin/scss/custom_jazzmin.scss b/static/admin/scss/custom_jazzmin.scss
index 43ec71da..7f02c372 100644
--- a/static/admin/scss/custom_jazzmin.scss
+++ b/static/admin/scss/custom_jazzmin.scss
@@ -65,3 +65,7 @@ a {
color: $teal-main !important;
}
}
+
+.selector-chosen h2 {
+ background-color: $teal-main !important;
+}
diff --git a/static/schema.yaml b/static/schema.yaml
deleted file mode 100644
index 9b8e3645..00000000
--- a/static/schema.yaml
+++ /dev/null
@@ -1,531 +0,0 @@
-openapi: 3.0.0
-info:
- description: Sora-reader backend API
- version: "1.0.0"
- title: Sora
- contact:
- email: 1337kwiz@gmail.com
- license:
- name: GPLv3
- url: 'https://www.gnu.org/licenses/gpl-3.0.en.html'
-tags:
- - name: auth
- description: Authentication and user/token manipulation
- - name: manga
- description: Endpoints for fetching manga data
- - name: docs
- description: Meta endpoints with information about API
-paths:
- /api/auth/sign-in/:
- post:
- description: Sign in with user credentials
- operationId: signIn
- tags:
- - auth
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UserCredentials'
- required: true
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TokenResponse'
- description: 'successful operation'
- /api/auth/sign-out/:
- get:
- description: Sign user out and blacklist his token
- operationId: signOut
- tags:
- - auth
- responses:
- '200':
- description: No response body
- /api/auth/sign-up/:
- post:
- description: Sign up and receive JWT token pair and a username
- operationId: signUp
- tags:
- - auth
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UserCredentials'
- required: true
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TokenResponse'
- description: 'successful operation'
- /api/auth/token-refresh/:
- post:
- operationId: refreshToken
- description: |-
- Takes a refresh type JSON web token and returns an access type JSON web
- token if the refresh token is valid.
- tags:
- - auth
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/RefreshToken'
- required: true
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AccessToken'
- description: 'successful operation'
- /api/auth/token-verify/:
- post:
- operationId: verifyToken
- description: |-
- Takes a token and indicates if it is valid. This view provides no
- information about a token's fitness for a particular use.
- tags:
- - auth
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VerifyToken'
- required: true
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VerifyToken'
- description: 'successful operation'
- /api/manga:
- get:
- tags:
- - manga
- parameters:
- - name: title
- in: query
- description: Manga title
- required: true
- schema:
- type: string
- - name: limit
- in: query
- description: Query limit
- required: false
- schema:
- type: number
- - name: offset
- in: query
- description: Query offset
- required: false
- schema:
- type: number
- summary: Search manga by title
- description: Returns a list of manga items
- operationId: search
- responses:
- '200':
- description: successful operation
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/MangaSearchResult'
- '/api/manga/{mangaId}':
- get:
- tags:
- - manga
- summary: Get manga detail by ID
- description: Returns detailed info about manga
- operationId: mangaDetail
- parameters:
- - name: mangaId
- in: path
- description: ID of manga to return
- required: true
- schema:
- type: integer
- format: int64
- responses:
- '200':
- description: successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Manga'
- '404':
- description: Manga not found
- '/api/manga/{mangaId}/chapters':
- get:
- tags:
- - manga
- summary: Get manga chapters by ID
- description: Returns chapters for manga
- operationId: mangaChapters
- parameters:
- - name: mangaId
- in: path
- description: ID of manga to get chapters for
- required: true
- schema:
- type: integer
- format: int64
- responses:
- '200':
- description: successful operation
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/MangaChapter'
- '404':
- description: Manga not found
- '/api/manga/{chapterId}/images':
- get:
- tags:
- - manga
- summary: Get images for chapter by ID
- description: Returns images for chapter
- operationId: mangaChapterImages
- parameters:
- - name: chapterId
- in: path
- description: ID of chapter to get images for
- required: true
- schema:
- type: number
- - name: parse
- in: query
- description: Whether to run parsing of images again (if links are broken)
- required: false
- schema:
- type: boolean
- responses:
- '200':
- description: successful operation
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/MangaChapterImage'
- example: ["https://cdn.com/vol1/0?token=some_token", "https://cdn.com/vol1/1?token=some_token"]
- '404':
- description: Manga or volume not found
- /api/docs/schema/:
- get:
- summary: Get OpenApi 3.0 schema
- operationId: getSchema
- description: |-
- OpenApi3 schema for this API. Format can be selected via content negotiation.
-
- - YAML: application/vnd.oai.openapi
- - JSON: application/vnd.oai.openapi+json
- parameters:
- - in: query
- name: format
- schema:
- type: string
- enum:
- - json
- - yaml
- - in: query
- name: lang
- schema:
- type: string
- enum:
- - af
- - ar
- - ar-dz
- - ast
- - az
- - be
- - bg
- - bn
- - br
- - bs
- - ca
- - cs
- - cy
- - da
- - de
- - dsb
- - el
- - en
- - en-au
- - en-gb
- - eo
- - es
- - es-ar
- - es-co
- - es-mx
- - es-ni
- - es-ve
- - et
- - eu
- - fa
- - fi
- - fr
- - fy
- - ga
- - gd
- - gl
- - he
- - hi
- - hr
- - hsb
- - hu
- - hy
- - ia
- - id
- - ig
- - io
- - is
- - it
- - ja
- - ka
- - kab
- - kk
- - km
- - kn
- - ko
- - ky
- - lb
- - lt
- - lv
- - mk
- - ml
- - mn
- - mr
- - my
- - nb
- - ne
- - nl
- - nn
- - os
- - pa
- - pl
- - pt
- - pt-br
- - ro
- - ru
- - sk
- - sl
- - sq
- - sr
- - sr-latn
- - sv
- - sw
- - ta
- - te
- - tg
- - th
- - tk
- - tr
- - tt
- - udm
- - uk
- - ur
- - uz
- - vi
- - zh-hans
- - zh-hant
- tags:
- - docs
- responses:
- '200':
- content:
- application/vnd.oai.openapi:
- schema:
- type: object
- additionalProperties: {}
- application/yaml:
- schema:
- type: object
- additionalProperties: {}
- application/vnd.oai.openapi+json:
- schema:
- type: object
- additionalProperties: {}
- application/json:
- schema:
- type: object
- additionalProperties: {}
- description: 'successful operation'
-components:
- schemas:
- Manga:
- type: object
- properties:
- id:
- type: integer
- title:
- type: string
- alt_title:
- type: string
- rating:
- type: number
- format: float
- minimum: 0
- maximum: 10
- thumbnail:
- type: string
- format: uri
- maxLength: 2000
- image:
- type: string
- format: uri
- maxLength: 2000
- description:
- type: string
- source:
- type: string
- authors:
- type: array
- items:
- type: string
- screenwriters:
- type: array
- items:
- type: string
- illustrators:
- type: array
- items:
- type: string
- translators:
- type: array
- items:
- type: string
- genres:
- type: array
- items:
- type: string
- categories:
- type: array
- items:
- type: string
- status:
- type: string
- year:
- type: string
- updated_detail:
- type: string
- format: date-time
- required:
- - id
- - title
- - description
- example:
- id: 0
- title: some_manga
- alt_title: some_manga_in_japanese
- rating: 9.32
- thumbnail: "https://cdn.com/some_image_compressed/"
- image: "https://cdn.com/some_image_full/"
- description: another isekai
- source: MangaSite
- authors: ["SomeGuy", "MayBeOtherGuy"]
- screenwriters: []
- illustrators: []
- translators: []
- updated_chapters: "2021-07-01T18:07:19.600463Z"
- updated_detail: "2021-07-01T18:07:16.272237Z"
- MangaSearchResult:
- type: object
- properties:
- count:
- type: number
- next:
- type: string
- format: uri
- nullable: true
- previous:
- type: string
- format: uri
- nullable: true
- results:
- type: array
- items:
- $ref: '#/components/schemas/Manga'
- MangaChapter:
- type: object
- properties:
- id:
- type: number
- title:
- type: string
- link:
- type: string
- format: uri
- number:
- type: number
- volume:
- type: number
- example:
- id: 0
- title: some_manga
- link: https://mangasite.com/some_manga
- number: 0
- volume: 1
- MangaChapterImage:
- type: string
- example: "https://cdn.com/vol1/0?token=some_token"
- VerifyToken:
- type: object
- properties:
- token:
- type: string
- required:
- - token
- AccessToken:
- type: object
- properties:
- access:
- type: string
- required:
- - access
- RefreshToken:
- type: object
- properties:
- refresh:
- type: string
- required:
- - string
- UserCredentials:
- type: object
- properties:
- username:
- type: string
- description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
- only.
- pattern: ^[\w.@+-]+$
- maxLength: 150
- password:
- type: string
- maxLength: 128
- required:
- - password
- - username
- TokenResponse:
- type: object
- properties:
- refresh:
- type: string
- access:
- type: string
- username:
- type: string
- required:
- - access
- - refresh
- - username
diff --git a/tests/Manga.jmx b/tests/Manga.jmx
new file mode 100644
index 00000000..5e6bb58d
--- /dev/null
+++ b/tests/Manga.jmx
@@ -0,0 +1,94 @@
+
+
+
+
+
+ false
+ true
+ false
+
+
+
+ HOST
+ localhost
+ =
+
+
+ PORT
+ 8000
+ =
+
+
+ PROTOCOL
+ http
+ =
+
+
+
+
+
+
+
+ continue
+
+ false
+ 10
+
+ 5
+ 1
+ false
+
+
+ true
+
+
+
+
+
+
+ ${HOST}
+ ${PORT}
+
+
+ /api/manga/555
+ GET
+ true
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+ true
+ Поднятие одиночку
+ =
+ true
+ title
+
+
+
+ ${HOST}
+ ${PORT}
+ ${PROTOCOL}
+
+ /api/manga/search
+ GET
+ true
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+