Introduced FastAPI and Postgres support (#173)

* added poc

* added check for sqlite db

* added events test

* draft docker-compose-test.yml

* improved docker-compose

* added support for postgres migrations

* add documentation

* added some qol to migrations

* added migration generation script

* removed settings.toml

* waiting for postgress in script

* commented script

* added sample web config

* fixed tests

* mock memory db

* reviewed PR
This commit is contained in:
Simone Robutti 2022-10-14 22:11:27 +02:00 committed by GitHub
parent 63a30bb483
commit 44340fde8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 695 additions and 229 deletions

1
.gitignore vendored
View File

@ -189,3 +189,4 @@ api_documentation/source/*
!api_documentation/source/conf.py
!api_documentation/source/index.rst
!api_documentation/source/_static/
./settings.toml

24
Dockerfile Normal file
View File

@ -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

View File

@ -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.

View File

@ -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

34
docker-compose-web.yml Normal file
View File

@ -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:

View File

@ -1,4 +0,0 @@
[aerich]
tortoise_orm = storage.db.TORTOISE_ORM
location = ./migrations
src_folder = ./.

View File

@ -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()

View File

@ -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={})

View File

@ -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()

View File

@ -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
);

View File

@ -0,0 +1,4 @@
[tool.aerich]
tortoise_orm = "mobilizon_reshare.storage.db.TORTOISE_ORM"
location = "./"
src_folder = "./."

View File

@ -0,0 +1,4 @@
[tool.aerich]
tortoise_orm = "mobilizon_reshare.storage.db.TORTOISE_ORM"
location = "."
src_folder = "./."

View File

@ -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]

View File

@ -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()

View File

View File

@ -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

363
poetry.lock generated
View File

@ -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 = []

View File

@ -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"]

View File

@ -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"

View File

@ -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']

View File

@ -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

0
settings.toml Executable file
View File

View File

@ -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()

0
tests/web/__init__.py Normal file
View File

26
tests/web/conftest.py Normal file
View File

@ -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)

View File

View File

@ -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]

37
tests/web/test_web_db.py Normal file
View File

@ -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 == []