diff --git a/.gitignore b/.gitignore index 02580d4..53b614d 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,4 @@ api_documentation/source/* !api_documentation/source/conf.py !api_documentation/source/index.rst !api_documentation/source/_static/ +./settings.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd457e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.10-alpine3.16 + +ENV ENV_FOR_DYNACONF=${ENV_FOR_DYNACONF} \ + PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + POETRY_VERSION=1.0.0 + +# System deps: +RUN pip install "poetry==$POETRY_VERSION" + +# Copy only requirements to cache them in docker layer +WORKDIR /app +COPY poetry.lock pyproject.toml /app/ + +# Project initialization: +RUN poetry config virtualenvs.create false \ + && poetry install $(test "$ENV_FOR_DYNACONF" == production && echo "--no-dev") --no-interaction --no-ansi + +# Creating folders, and files for a project: +COPY . /app \ No newline at end of file diff --git a/doc/contributing.md b/doc/contributing.md index 8ad073e..ef2b6d8 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -25,3 +25,25 @@ To run the test suite, run `scripts/run_pipeline_tests.sh` from the root of the At the moment no integration test is present and they are executed manually. Reach out to us if you want to access the testing environment or you want to help automate the integration tests. + +### How to handle migrations + +Changes to the data model need to be handled through migrations. We use aerich to manage the migration files. +Both our CLI and our web service are configured in such a way that migrations are run transparently when the package is +updated. If you want to test that the update doesn't corrupt your data, we suggest trying the update in a test database. + +To create a new migration file, use aerich CLI. It will take care of generating the file. If further code is necessary, +add it to the new migration file. + +Since we support two database (sqlite and postgres) that have slightly different dialects and since aerich doesn't +really support this scenario, it is necessary to generate migrations separately and place the migrations files in the +respective folders. + +Aerich picks up the migrations according to the scheme of the db in the configuration. + +Currently the consistency of the migrations for the different databases is not tested so please pay extra care when +committing a change and request special review. + +Aerich configuration is specified in the pyproject.toml file. Since it doesn't support multiple databases, we have two +configuration files that allow to run aerich on different databases if you enter their respective migration folders. +You can find them in mobilizon_reshare/migrations. \ No newline at end of file diff --git a/docker-compose-migration.yml b/docker-compose-migration.yml new file mode 100644 index 0000000..a2f88c7 --- /dev/null +++ b/docker-compose-migration.yml @@ -0,0 +1,12 @@ +version: "3.7" +services: + db: + image: postgres:13 + env_file: + - ./.env + healthcheck: + test: ["CMD", "pg_isready", "-U", "mobilizon_reshare"] + interval: 5s + retries: 5 + ports: + - 5432:5432 diff --git a/docker-compose-web.yml b/docker-compose-web.yml new file mode 100644 index 0000000..6f596ef --- /dev/null +++ b/docker-compose-web.yml @@ -0,0 +1,34 @@ +version: "3.7" +services: + db: + image: postgres:13 + env_file: + - ./.env + volumes: + - postgres-db-volume:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "mobilizon_reshare"] + interval: 5s + retries: 5 + ports: + - 5432:5432 + web: + build: . + command: poetry run mobilizon-reshare web + #command: sh + environment: + SECRETS_FOR_DYNACONF: /app/.secrets.toml + SETTINGS_FILE_FOR_DYNACONF: /app/settings.toml + ENV_FOR_DYNACONF: development + volumes: + - ./sample_settings/docker_web/.sample_secrets.toml:/app/.secrets.toml + - ./sample_settings/docker_web/settings.toml:/app/settings.toml + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - postgres-db-volume:/var/lib/postgresql + - ./:/app + ports: + - 8000:8000 + +volumes: + postgres-db-volume: diff --git a/mobilizon_reshare/aerich.ini b/mobilizon_reshare/aerich.ini deleted file mode 100644 index d466f43..0000000 --- a/mobilizon_reshare/aerich.ini +++ /dev/null @@ -1,4 +0,0 @@ -[aerich] -tortoise_orm = storage.db.TORTOISE_ORM -location = ./migrations -src_folder = ./. diff --git a/mobilizon_reshare/cli/__init__.py b/mobilizon_reshare/cli/__init__.py index 18ac5a5..d4114f7 100644 --- a/mobilizon_reshare/cli/__init__.py +++ b/mobilizon_reshare/cli/__init__.py @@ -1,14 +1,11 @@ import asyncio import functools import logging -import traceback -from logging.config import dictConfig -from pathlib import Path import sys +import traceback from mobilizon_reshare.config.command import CommandConfig -from mobilizon_reshare.config.config import get_settings -from mobilizon_reshare.storage.db import tear_down, MoReDB +from mobilizon_reshare.storage.db import tear_down, init logger = logging.getLogger(__name__) @@ -17,14 +14,6 @@ async def graceful_exit(): await tear_down() -async def init(): - settings = get_settings() - dictConfig(settings["logging"]) - db_path = Path(settings.db_path) - db = MoReDB(db_path) - await db.setup() - - async def _safe_execution(function): await init() diff --git a/mobilizon_reshare/cli/cli.py b/mobilizon_reshare/cli/cli.py index 4e31df1..8777ca0 100644 --- a/mobilizon_reshare/cli/cli.py +++ b/mobilizon_reshare/cli/cli.py @@ -1,24 +1,25 @@ import functools import click +import uvicorn from click import pass_context -from mobilizon_reshare.config.command import CommandConfig from mobilizon_reshare.cli import safe_execution from mobilizon_reshare.cli.commands.format.format import format_event from mobilizon_reshare.cli.commands.list.list_event import list_events from mobilizon_reshare.cli.commands.list.list_publication import list_publications -from mobilizon_reshare.cli.commands.recap.main import recap_command as recap_main -from mobilizon_reshare.cli.commands.start.main import start_command as start_main -from mobilizon_reshare.cli.commands.pull.main import pull_command as pull_main from mobilizon_reshare.cli.commands.publish.main import publish_command as publish_main -from mobilizon_reshare.config.config import current_version, get_settings -from mobilizon_reshare.config.publishers import publisher_names -from mobilizon_reshare.event.event import EventPublicationStatus +from mobilizon_reshare.cli.commands.pull.main import pull_command as pull_main +from mobilizon_reshare.cli.commands.recap.main import recap_command as recap_main from mobilizon_reshare.cli.commands.retry.main import ( retry_event_command, retry_publication_command, ) +from mobilizon_reshare.cli.commands.start.main import start_command as start_main +from mobilizon_reshare.config.command import CommandConfig +from mobilizon_reshare.config.config import current_version, get_settings +from mobilizon_reshare.config.publishers import publisher_names +from mobilizon_reshare.event.event import EventPublicationStatus from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.publishers import get_active_publishers @@ -249,5 +250,12 @@ def publication_retry(publication_id): safe_execution(functools.partial(retry_publication_command, publication_id),) +@mobilizon_reshare.command("web") +def web(): + uvicorn.run( + "mobilizon_reshare.web.backend.main:app", host="0.0.0.0", port=8000, reload=True + ) + + if __name__ == "__main__": mobilizon_reshare(obj={}) diff --git a/mobilizon_reshare/config/config.py b/mobilizon_reshare/config/config.py index 301d59b..2a29b2e 100644 --- a/mobilizon_reshare/config/config.py +++ b/mobilizon_reshare/config/config.py @@ -20,7 +20,7 @@ base_validators = [ # url of the main Mobilizon instance to download events from Validator("source.mobilizon.url", must_exist=True, is_type_of=str), Validator("source.mobilizon.group", must_exist=True, is_type_of=str), - Validator("db_path", must_exist=True, is_type_of=str), + Validator("db_url", must_exist=True, is_type_of=str), Validator("locale", must_exist=True, is_type_of=str, default="en-us"), ] @@ -130,3 +130,7 @@ class CustomConfig: def get_settings(): return CustomConfig.get_instance().settings + + +def get_settings_without_validation(): + return build_settings() diff --git a/mobilizon_reshare/migrations/postgres/models/0_20221006223256_init.sql b/mobilizon_reshare/migrations/postgres/models/0_20221006223256_init.sql new file mode 100755 index 0000000..be96542 --- /dev/null +++ b/mobilizon_reshare/migrations/postgres/models/0_20221006223256_init.sql @@ -0,0 +1,41 @@ +-- upgrade -- +CREATE TABLE IF NOT EXISTS "event" ( + "id" UUID NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "mobilizon_id" UUID NOT NULL, + "mobilizon_link" TEXT NOT NULL, + "thumbnail_link" TEXT, + "location" TEXT, + "begin_datetime" TIMESTAMPTZ NOT NULL, + "end_datetime" TIMESTAMPTZ NOT NULL, + "last_update_time" TIMESTAMPTZ NOT NULL +); +CREATE TABLE IF NOT EXISTS "publisher" ( + "id" UUID NOT NULL PRIMARY KEY, + "name" VARCHAR(256) NOT NULL, + "account_ref" TEXT +); +CREATE TABLE IF NOT EXISTS "publication" ( + "id" UUID NOT NULL PRIMARY KEY, + "status" SMALLINT NOT NULL, + "timestamp" TIMESTAMPTZ NOT NULL, + "reason" TEXT, + "event_id" UUID NOT NULL REFERENCES "event" ("id") ON DELETE CASCADE, + "publisher_id" UUID NOT NULL REFERENCES "publisher" ("id") ON DELETE CASCADE +); +COMMENT ON COLUMN "publication"."status" IS 'FAILED: 0\nCOMPLETED: 1'; +CREATE TABLE IF NOT EXISTS "notification" ( + "id" UUID NOT NULL PRIMARY KEY, + "status" SMALLINT NOT NULL, + "message" TEXT NOT NULL, + "publication_id" UUID REFERENCES "publication" ("id") ON DELETE CASCADE, + "target_id" UUID REFERENCES "publisher" ("id") ON DELETE CASCADE +); +COMMENT ON COLUMN "notification"."status" IS 'WAITING: 1\nFAILED: 2\nPARTIAL: 3\nCOMPLETED: 4'; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSONB NOT NULL +); diff --git a/mobilizon_reshare/migrations/postgres/pyproject.toml b/mobilizon_reshare/migrations/postgres/pyproject.toml new file mode 100644 index 0000000..05ffef4 --- /dev/null +++ b/mobilizon_reshare/migrations/postgres/pyproject.toml @@ -0,0 +1,4 @@ +[tool.aerich] +tortoise_orm = "mobilizon_reshare.storage.db.TORTOISE_ORM" +location = "./" +src_folder = "./." diff --git a/mobilizon_reshare/migrations/models/0_20211207110159_init.sql b/mobilizon_reshare/migrations/sqlite/models/0_20211207110159_init.sql similarity index 100% rename from mobilizon_reshare/migrations/models/0_20211207110159_init.sql rename to mobilizon_reshare/migrations/sqlite/models/0_20211207110159_init.sql diff --git a/mobilizon_reshare/migrations/models/1_20211218165547_add column last_update_time for event.sql b/mobilizon_reshare/migrations/sqlite/models/1_20211218165547_add column last_update_time for event.sql similarity index 100% rename from mobilizon_reshare/migrations/models/1_20211218165547_add column last_update_time for event.sql rename to mobilizon_reshare/migrations/sqlite/models/1_20211218165547_add column last_update_time for event.sql diff --git a/mobilizon_reshare/migrations/sqlite/pyproject.toml b/mobilizon_reshare/migrations/sqlite/pyproject.toml new file mode 100644 index 0000000..d099d9a --- /dev/null +++ b/mobilizon_reshare/migrations/sqlite/pyproject.toml @@ -0,0 +1,4 @@ +[tool.aerich] +tortoise_orm = "mobilizon_reshare.storage.db.TORTOISE_ORM" +location = "." +src_folder = "./." diff --git a/mobilizon_reshare/settings.toml b/mobilizon_reshare/settings.toml index eeda85f..89f9645 100644 --- a/mobilizon_reshare/settings.toml +++ b/mobilizon_reshare/settings.toml @@ -1,7 +1,6 @@ [default] local_state_dir = "/var/mobilizon_reshare" -db_name = "events.db" -db_path = "@format {this.local_state_dir}/{this.db_name}" +db_url = "sqlite:///var/mobilizon_reshare/events.db" locale= "en-us" [default.source.mobilizon] diff --git a/mobilizon_reshare/storage/db.py b/mobilizon_reshare/storage/db.py index aa305b6..9f4e537 100644 --- a/mobilizon_reshare/storage/db.py +++ b/mobilizon_reshare/storage/db.py @@ -1,32 +1,30 @@ import logging +from logging.config import dictConfig from pathlib import Path import pkg_resources -from tortoise import Tortoise +import urllib3.util from aerich import Command +from tortoise import Tortoise + +from mobilizon_reshare.config.config import ( + get_settings, + get_settings_without_validation, +) from mobilizon_reshare.config.publishers import publisher_names from mobilizon_reshare.storage.query.write import update_publishers -from mobilizon_reshare.config.config import get_settings - logger = logging.getLogger(__name__) -def get_db_url(): - """gets db url from settings - - Returns: - str : db url - """ - settings = get_settings() - db_path = Path(settings.db_path) - db_url = f"sqlite:///{db_path}" - return db_url +def get_db_url() -> urllib3.util.Url: + return urllib3.util.parse_url(get_settings_without_validation().db_url) def get_tortoise_orm(): + return { - "connections": {"default": get_db_url()}, + "connections": {"default": get_db_url().url}, "apps": { "models": { "models": [ @@ -48,39 +46,57 @@ TORTOISE_ORM = get_tortoise_orm() class MoReDB: - def __init__(self, path: Path): - self.path = path - # TODO: Check if DB is openable/"queriable" - self.is_init = self.path.exists() and (not self.path.is_dir()) - if not self.is_init: - self.path.parent.mkdir(parents=True, exist_ok=True) + def get_migration_location(self): + scheme = get_db_url().scheme + return pkg_resources.resource_filename( + "mobilizon_reshare", f"migrations/{scheme}" + ) async def _implement_db_changes(self): - migration_queries_location = pkg_resources.resource_filename( - "mobilizon_reshare", "migrations" - ) + logging.info("Performing aerich migrations.") command = Command( - tortoise_config=TORTOISE_ORM, + tortoise_config=get_tortoise_orm(), app="models", - location=migration_queries_location, + location=self.get_migration_location(), ) await command.init() migrations = await command.upgrade() if migrations: - logging.warning("Updated database to latest version") + logging.info("Updated database to latest version") async def setup(self): await self._implement_db_changes() - await Tortoise.init( - config=TORTOISE_ORM, - ) - if not self.is_init: - await Tortoise.generate_schemas() - self.is_init = True - logger.info(f"Successfully initialized database at {self.path}") - + await Tortoise.init(config=get_tortoise_orm(),) + await Tortoise.generate_schemas() await update_publishers(publisher_names) +class MoReSQLiteDB(MoReDB): + def __init__(self): + + if ( + get_db_url().path + ): # if in-memory sqlite there's no path so nothing has to be initialized + self.path = Path(get_db_url().path) + # TODO: Check if DB is openable/"queriable" + self.is_init = self.path.exists() and (not self.path.is_dir()) + if not self.is_init: + self.path.parent.mkdir(parents=True, exist_ok=True) + + async def tear_down(): return await Tortoise.close_connections() + + +async def init(init_logging=True): + + if init_logging: + dictConfig(get_settings()["logging"]) + + # init storage + url = get_db_url() + if url.scheme == "sqlite": + db = MoReSQLiteDB() + else: + db = MoReDB() + await db.setup() diff --git a/mobilizon_reshare/web/__init__.py b/mobilizon_reshare/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mobilizon_reshare/web/backend/__init__.py b/mobilizon_reshare/web/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mobilizon_reshare/web/backend/main.py b/mobilizon_reshare/web/backend/main.py new file mode 100644 index 0000000..dbe9c85 --- /dev/null +++ b/mobilizon_reshare/web/backend/main.py @@ -0,0 +1,37 @@ +import logging + +from fastapi import FastAPI +from tortoise.contrib.pydantic import pydantic_model_creator + +from mobilizon_reshare.models.event import Event +from mobilizon_reshare.storage.db import init as init_db, get_db_url + +app = FastAPI() +event_pydantic = pydantic_model_creator(Event) + + +logger = logging.getLogger(__name__) + + +def check_database(): + url = get_db_url() + if url.scheme == "sqlite": + logger.warning( + "Database is SQLite. This might create issues when running the web application. Please use a " + "PostgreSQL or MariaDB backend." + ) + + +def register_endpoints(app): + @app.get("/events", status_code=200) + async def get_event(): + + return await event_pydantic.from_queryset(Event.all()) + + +@app.on_event("startup") +async def init_app(init_logging=True): + check_database() + await init_db(init_logging=init_logging) + register_endpoints(app) + return app diff --git a/poetry.lock b/poetry.lock index 071d90c..12346b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,8 +14,8 @@ tomlkit = "*" tortoise-orm = "*" [package.extras] -asyncmy = ["asyncmy"] asyncpg = ["asyncpg"] +asyncmy = ["asyncmy"] [[package]] name = "aiosqlite" @@ -36,6 +36,23 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + [[package]] name = "appdirs" version = "1.4.4" @@ -55,6 +72,19 @@ python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.7.0" +[[package]] +name = "asyncpg" +version = "0.26.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["pycodestyle (>=2.7.0,<2.8.0)", "flake8 (>=3.9.2,<3.10.0)", "uvloop (>=0.15.3)"] + [[package]] name = "asynctest" version = "0.13.0" @@ -151,7 +181,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -222,6 +252,70 @@ python-versions = "*" [package.dependencies] requests = "*" +[[package]] +name = "fastapi" +version = "0.85.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.20.4" + +[package.extras] +all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.7.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-orjson (==3.6.2)", "types-ujson (==5.4.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.15.0" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.0" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.16.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + [[package]] name = "idna" version = "3.3" @@ -463,7 +557,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-lazy-fixture" @@ -559,6 +653,20 @@ urllib3 = ">=1.25.10" [package.extras] tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "six" version = "1.16.0" @@ -567,6 +675,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -657,8 +773,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -669,8 +785,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -681,8 +797,8 @@ optional = false python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -693,7 +809,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-napoleon" @@ -716,8 +832,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -731,6 +847,21 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "starlette" +version = "0.20.4" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + [[package]] name = "text-unidecode" version = "1.3" @@ -773,6 +904,7 @@ python-versions = ">=3.7,<4.0" [package.dependencies] aiosqlite = ">=0.16.0,<0.18.0" +asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""} iso8601 = ">=1.0.2,<2.0.0" pypika-tortoise = ">=0.1.6,<0.2.0" pytz = "*" @@ -832,6 +964,21 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "uvicorn" +version = "0.18.3" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] + [[package]] name = "zipp" version = "3.8.0" @@ -847,21 +994,16 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "9eccb917a541dec588301184f58ffa68a14c6def02bb988d2d6241598b8539ae" +content-hash = "56fecab376d8a94ff6dd64f853128388f458001b6f4eb63dddc3fd4bb2773df1" [metadata.files] -aerich = [ - {file = "aerich-0.6.3-py3-none-any.whl", hash = "sha256:d45f98214ed54b8ec9be949df264c5b0b9e8b79d8f8ba9e68714675bc81a5574"}, - {file = "aerich-0.6.3.tar.gz", hash = "sha256:96ac087922048470687264125cb9dfeaade982219c78e6a9b91a0fb67eaa1cd1"}, -] +aerich = [] aiosqlite = [ {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, ] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] +alabaster = [] +anyio = [] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -870,6 +1012,7 @@ arrow = [ {file = "arrow-1.1.1-py3-none-any.whl", hash = "sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510"}, {file = "arrow-1.1.1.tar.gz", hash = "sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a"}, ] +asyncpg = [] asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, @@ -892,24 +1035,21 @@ charset-normalizer = [ click = [] colorama = [] coverage = [] -css-html-js-minify = [ - {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, - {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, - {file = "css_html_js_minify-2.5.5-py3.6.egg", hash = "sha256:4704e04a0cd6dd56d61bbfa3bfffc630da6b2284be33519be0b456672e2a2438"}, -] +css-html-js-minify = [] dictdiffer = [ {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, ] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] +docutils = [] dynaconf = [] -facebook-sdk = [ - {file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"}, - {file = "facebook_sdk-3.1.0-py2.py3-none-any.whl", hash = "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e"}, +facebook-sdk = [] +fastapi = [] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] +httpcore = [] +httpx = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -920,62 +1060,15 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] -iso8601 = [] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +iso8601 = [ + {file = "iso8601-1.0.2-py3-none-any.whl", hash = "sha256:d7bc01b1c2a43b259570bb307f057abc578786ea734ba2b87b836c5efc5bd443"}, + {file = "iso8601-1.0.2.tar.gz", hash = "sha256:27f503220e6845d9db954fb212b95b0362d8b7e6c1b2326a87061c3de93594b1"}, ] +jinja2 = [] lxml = [] -markdownify = [ - {file = "markdownify-0.10.3-py3-none-any.whl", hash = "sha256:edad0ad3896ec7460d05537ad804bbb3614877c6cd0df27b56dee218236d9ce2"}, - {file = "markdownify-0.10.3.tar.gz", hash = "sha256:782e310390cd5e4bde7543ceb644598c78b9824ee9f8d7ef9f9f4f8782e46974"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -oauthlib = [ - {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"}, - {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"}, -] +markdownify = [] +markupsafe = [] +oauthlib = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -984,23 +1077,14 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pockets = [ - {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, - {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, -] +pockets = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pydantic = [] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] +pygments = [] +pyparsing = [] pypika-tortoise = [] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1019,10 +1103,7 @@ 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-slugify = [ - {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"}, - {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, -] +python-slugify = [] pytz = [ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, @@ -1031,66 +1112,33 @@ requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] -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"}, -] +requests-oauthlib = [] responses = [ {file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"}, {file = "responses-0.13.4.tar.gz", hash = "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899"}, ] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -soupsieve = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, -] -sphinx = [ - {file = "Sphinx-4.4.0-py3-none-any.whl", hash = "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe"}, - {file = "Sphinx-4.4.0.tar.gz", hash = "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"}, -] -sphinx-autodoc-typehints = [ - {file = "sphinx_autodoc_typehints-1.17.1-py3-none-any.whl", hash = "sha256:f16491cad05a13f4825ecdf9ee4ff02925d9a3b1cf103d4d02f2f81802cce653"}, - {file = "sphinx_autodoc_typehints-1.17.1.tar.gz", hash = "sha256:844d7237d3f6280b0416f5375d9556cfd84df1945356fcc34b82e8aaacab40f3"}, -] -sphinx-material = [ - {file = "sphinx_material-0.0.35-py3-none-any.whl", hash = "sha256:a62a0a48d4c32edc260f9bdbca658e7d149beb10e1d338848b0076bb13be0775"}, - {file = "sphinx_material-0.0.35.tar.gz", hash = "sha256:27f0f1084aa0201b43879aef24a0521b78dc8df4942b003a4e7d79ab11515852"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-napoleon = [ - {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, - {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] +sniffio = [] +snowballstemmer = [] +soupsieve = [] +sphinx = [] +sphinx-autodoc-typehints = [] +sphinx-material = [] +sphinxcontrib-applehelp = [] +sphinxcontrib-devhelp = [] +sphinxcontrib-htmlhelp = [] +sphinxcontrib-jsmath = [] +sphinxcontrib-napoleon = [] +sphinxcontrib-qthelp = [] +sphinxcontrib-serializinghtml = [] +starlette = [] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, @@ -1107,12 +1155,7 @@ tweepy = [ {file = "tweepy-4.4.0.tar.gz", hash = "sha256:8d4b4520271b796fa7efc4c5d5ef3228af4d79f6a4d3ace3900b2778ed8f6f1c"}, ] typing-extensions = [] -unidecode = [ - {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, - {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, -] +unidecode = [] urllib3 = [] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +uvicorn = [] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 41f8ac0..0208c51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Coopyleft" [tool.poetry.dependencies] python = "^3.9" dynaconf = "~3.1" -tortoise-orm = "~0.19" +tortoise-orm = {extras = ["asyncpg"], version = "^0.19.2"} aiosqlite = "~0.17" Jinja2 = "~3.1" requests = "~2.27" @@ -23,6 +23,8 @@ appdirs = "~1.4" tweepy = "~4.4" facebook-sdk = "~3.1" aerich = "~0.6" +fastapi = "^0.85.0" +uvicorn = "^0.18.3" [tool.poetry.dev-dependencies] responses = "~0.13" @@ -35,6 +37,9 @@ Sphinx = "~4.4" sphinxcontrib-napoleon = "~0.7" sphinx-material = "~0.0" sphinx-autodoc-typehints = "~1.17" +httpx = "^0.23.0" + + [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/sample_settings/docker_web/.sample_secrets.toml b/sample_settings/docker_web/.sample_secrets.toml new file mode 100644 index 0000000..4cc9f3c --- /dev/null +++ b/sample_settings/docker_web/.sample_secrets.toml @@ -0,0 +1,54 @@ +[default.publisher.telegram] +active=false +chat_id="xxx" +token="xxx" +username="xxx" +[default.publisher.zulip] +active=false +instance="xxx" +chat_id="xxx" +subject="xxx" +bot_token="xxx" +bot_email="xxx" +[default.publisher.twitter] +active=false +api_key="xxx" +api_key_secret="xxx" +access_token="xxx" +access_secret="xxx" +[default.publisher.mastodon] +active=false +instance="xxx" +token="xxx" +name="xxx" +toot_length=500 + +[default.publisher.facebook] + +active=false +page_access_token="xxx" + +[default.notifier.telegram] +active=false +chat_id="xxx" +token="xxx" +username="xxx" +[default.notifier.zulip] +active=false +instance="xxx" +chat_id="xxx" +subject="xxx" +bot_token="xxx" +bot_email="xxx" +[default.notifier.twitter] +active=false +api_key="xxx" +api_key_secret="xxx" +access_token="xxx" +access_secret="xxx" +[default.notifier.mastodon] +active=false + +[default.notifier.facebook] +active=false +page_access_token="xxx" \ No newline at end of file diff --git a/sample_settings/docker_web/settings.toml b/sample_settings/docker_web/settings.toml new file mode 100644 index 0000000..eeaf3df --- /dev/null +++ b/sample_settings/docker_web/settings.toml @@ -0,0 +1,33 @@ +[default] +debug = false +default = true +local_state_dir = "/var/lib/mobilizon-reshare" +#db_path = "@format {this.local_state_dir}/events.db" +db_url = "@format postgres://mobilizon_reshare:mobilizon_reshare@db:5432/mobilizon_reshare" + +[default.source.mobilizon] +url="https://some-mobilizon.com/api" +group="my_group" + +[default.selection] +strategy = "next_event" + +[default.selection.strategy_options] +break_between_events_in_minutes = 360 + +[default.logging] +version = 1 +disable_existing_loggers = false + +[default.logging.formatters.standard] +format = '[%(asctime)s] [%(levelno)s] [%(levelname)s] %(message)s' + +[default.logging.handlers.console] +level = "DEBUG" +class = "logging.StreamHandler" +formatter = "standard" +stream = "ext://sys.stderr" + +[default.logging.root] +level = "DEBUG" +handlers = ['console'] diff --git a/scripts/generate_aerich_migrations.sh b/scripts/generate_aerich_migrations.sh new file mode 100755 index 0000000..8974267 --- /dev/null +++ b/scripts/generate_aerich_migrations.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +set -eu + +echo "This script currently doesn't work correctly due to a bug in aerich. Do not use until said bug +is fixed" +echo "For more info: https://github.com/tortoise/aerich/issues/270" +exit +get_abs_filename() { + # $1 : relative filename + echo "$(cd "$(dirname "$1")" && pwd)/" +} + +PROJECT_DIR="$(get_abs_filename $0)/.." + +cleanup() { + # cleaning sqlite db + echo "Removing /tmp/foo" + rm -rf /tmp/tmp.db + + # shutting down postgres container + cd $PROJECT_DIR + docker-compose -f docker-compose-migration.yml down +} + +# making sure we leave the system clean +trap cleanup EXIT + + +poetry install + +# I activate the env instead of using poetry run because the pyproject in the migration folder gives it problems +. "$(poetry env info -p)/bin/activate" + +# I create a new SQLite db to run the migrations and generate a new one +echo "Generating SQLite migrations" +export DYNACONF_DB_URL="sqlite:///tmp/tmp.db" +cd "$PROJECT_DIR/mobilizon_reshare/migrations/sqlite/" + +aerich upgrade +aerich migrate + +# I use a dedicated docker-compose file to spin up a postgres instance, connect to it, run the migrations and generate a +# new one + +echo "Generating postgres migrations" +export DYNACONF_DB_URL="postgres://mobilizon_reshare:mobilizon_reshare@localhost:5432/mobilizon_reshare" +cd $PROJECT_DIR + +docker-compose -f docker-compose-migration.yml up -d + +cd "$PROJECT_DIR/mobilizon_reshare/migrations/postgres/" +until [ "$(docker inspect mo-re_db_1 --format='{{json .State.Health.Status}}')" = "\"healthy\"" ]; +do + echo "Waiting for postgres" + if [ "$(docker inspect mo-re_db_1 --format='{{json .State.Health.Status}}')" = "\"healthy\"" ] + then + break + fi + sleep 1s +done +aerich upgrade +aerich migrate diff --git a/settings.toml b/settings.toml new file mode 100755 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index f9c2fd0..a0b8065 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,8 @@ -import asyncio import importlib.resources import os +import time from collections import UserList from datetime import datetime, timedelta, timezone -import time from typing import Union from uuid import UUID @@ -130,7 +129,7 @@ async def stored_event(event) -> Event: @pytest.fixture(scope="function", autouse=True) -def initialize_db_tests(request) -> None: +async def initialize_db_tests(request) -> None: config = { "connections": { "default": os.environ.get("TORTOISE_TEST_DB", "sqlite://:memory:") @@ -161,10 +160,9 @@ def initialize_db_tests(request) -> None: await Tortoise.init(config, _create_db=True) await Tortoise.generate_schemas(safe=False) - loop = asyncio.get_event_loop() - loop.run_until_complete(_init_db()) - - request.addfinalizer(lambda: loop.run_until_complete(Tortoise._drop_databases())) + await _init_db() + yield + await Tortoise._drop_databases() @pytest.fixture() diff --git a/tests/web/__init__.py b/tests/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/web/conftest.py b/tests/web/conftest.py new file mode 100644 index 0000000..c90cd18 --- /dev/null +++ b/tests/web/conftest.py @@ -0,0 +1,26 @@ +import pytest +import urllib3 +from httpx import AsyncClient + +from mobilizon_reshare.storage import db +from mobilizon_reshare.web.backend.main import app, register_endpoints + + +@pytest.fixture(scope="session") +def anyio_backend(): + return "asyncio" + + +@pytest.fixture() +async def client(): + register_endpoints(app) + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + +@pytest.fixture(autouse=True) +def mock_sqlite_db(monkeypatch): + def get_url(): + return urllib3.util.parse_url("sqlite://memory") + + monkeypatch.setattr(db, "get_db_url", get_url) diff --git a/tests/web/endpoints/__init__.py b/tests/web/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/web/endpoints/test_events.py b/tests/web/endpoints/test_events.py new file mode 100644 index 0000000..17c6de1 --- /dev/null +++ b/tests/web/endpoints/test_events.py @@ -0,0 +1,16 @@ +import json + +import pytest +from httpx import AsyncClient + +from mobilizon_reshare.web.backend.main import event_pydantic + + +@pytest.mark.anyio +async def test_events(client: AsyncClient, event_model_generator): + event = event_model_generator() + await event.save() + + response = await client.get("/events") + assert response.status_code == 200 + assert response.json()[0] == [json.loads(event_pydantic.from_orm(event).json())][0] diff --git a/tests/web/test_web_db.py b/tests/web/test_web_db.py new file mode 100644 index 0000000..3c0bf0d --- /dev/null +++ b/tests/web/test_web_db.py @@ -0,0 +1,37 @@ +import logging + +import pytest +import urllib3.util + +from mobilizon_reshare.web.backend import main +from mobilizon_reshare.web.backend.main import check_database, init_app + + +def test_check_database_sqlite(caplog): + with caplog.at_level(logging.WARNING): + check_database() + assert caplog.messages == [ + "Database is SQLite. This might create issues when running the web application. " + "Please use a PostgreSQL or MariaDB backend." + ] + + +@pytest.mark.asyncio +async def test_check_database_cli(caplog): + with caplog.at_level(logging.WARNING): + await init_app(init_logging=False) + assert caplog.messages == [ + "Database is SQLite. This might create issues when running the web application. " + "Please use a PostgreSQL or MariaDB backend." + ] + + +@pytest.mark.asyncio +async def test_check_database_postgres(caplog, monkeypatch): + def get_url(): + return urllib3.util.parse_url("postgres://someone@something.it") + + monkeypatch.setattr(main, "get_db_url", get_url) + with caplog.at_level(logging.WARNING): + check_database() + assert caplog.messages == []