diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0aaffa2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "fastapi-rdf-utils"] + path = rdf-fastapi-utils + url = https://github.com/acdh-oeaw/rdf-fastapi-utils.git +[submodule "fastapi-versioning"] + path = fastapi-versioning + url = https://github.com/acdh-oeaw/fastapi-versioning.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc3d330 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.10 +#RUN addgroup --system app && adduser --system --group app +ARG USERNAME=app +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Create the user +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME + +WORKDIR /app/ +# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE +# Prevents Python from writing .pyc files to disk +ENV PYTHONDONTWRITEBYTECODE 1 + +# ensures that the python output is sent straight to terminal (e.g. your container log) +# without being first buffered and that you can see the output of your application (e.g. django logs) +# in real time. Equivalent to python -u: https://docs.python.org/3/using/cmdline.html#cmdoption-u +ENV PYTHONUNBUFFERED 1 +ENV ENVIRONMENT prod +ENV TESTING 0 + +RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ + cd /usr/local/bin && \ + ln -s /opt/poetry/bin/poetry && \ + poetry config virtualenvs.create false + +# +COPY ./pyproject.toml ./poetry.lock* /app/ + +# Allow installing dev dependencies to run tests +ARG INSTALL_DEV=false + +RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --without dev ; fi" + +COPY . /app + +ENV PYTHONPATH=/app + +# chown all the files to the app user +RUN chown -R app:app $HOME && chown -R app /usr/local && chown -R app /app + +# change to the app user +# Switch to a non-root user, which is recommended by Heroku. +USER app + +# +#CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["bash", "start.sh"] diff --git a/fastapi-versioning/.bumpversion.cfg b/fastapi-versioning/.bumpversion.cfg new file mode 100644 index 0000000..733076c --- /dev/null +++ b/fastapi-versioning/.bumpversion.cfg @@ -0,0 +1,12 @@ +[bumpversion] +current_version = 0.10.0 +commit = True +tag = True +tag_name = {new_version} +message = Bump version from {current_version} to {new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)((-rc(?P\d+))?) +serialize = + {major}.{minor}.{patch}-rc{rc} + {major}.{minor}.{patch} + +[bumpversion:file:setup.py] diff --git a/fastapi-versioning/.gitignore b/fastapi-versioning/.gitignore new file mode 100644 index 0000000..30edcc8 --- /dev/null +++ b/fastapi-versioning/.gitignore @@ -0,0 +1,109 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +Pipfile.Lock + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# editor config +.vscode/ +.idea/ diff --git a/fastapi-versioning/.travis.yml b/fastapi-versioning/.travis.yml new file mode 100644 index 0000000..56af5cf --- /dev/null +++ b/fastapi-versioning/.travis.yml @@ -0,0 +1,13 @@ +language: python +install: pip install tox-travis +script: tox +stages: + - name: test +jobs: + include: + - stage: test + python: 3.6 + - stage: test + python: 3.7 + - stage: test + python: 3.8 diff --git a/fastapi-versioning/LICENSE b/fastapi-versioning/LICENSE new file mode 100644 index 0000000..d93181b --- /dev/null +++ b/fastapi-versioning/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Dean Way + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fastapi-versioning/Pipfile b/fastapi-versioning/Pipfile new file mode 100644 index 0000000..080191b --- /dev/null +++ b/fastapi-versioning/Pipfile @@ -0,0 +1,21 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +uvicorn = "*" +tox = "*" +pytest = "*" +typing_extensions = "*" +bumpversion = "*" +requests = "*" +mypy = "*" +black = "==20.8b1" +isort = "*" +flake8 = "*" + +[packages] +fastapi = ">=0.56.0" +starlette = "==0.13.6" +fastapi-versioning = {path = ".",editable = true} diff --git a/fastapi-versioning/README.md b/fastapi-versioning/README.md new file mode 100644 index 0000000..3baef9e --- /dev/null +++ b/fastapi-versioning/README.md @@ -0,0 +1,150 @@ +# fastapi-versioning +api versioning for fastapi web applications + +# Installation + +`pip install fastapi-versioning` + +## Examples +```python +from fastapi import FastAPI +from fastapi_versioning import VersionedFastAPI, version + +app = FastAPI(title="My App") + + +@app.get("/greet") +@version(1, 0) +def greet_with_hello(): + return "Hello" + + +@app.get("/greet") +@version(1, 1) +def greet_with_hi(): + return "Hi" + + +app = VersionedFastAPI(app) +``` + +this will generate two endpoints: +``` +/v1_0/greet +/v1_1/greet +``` +as well as: +``` +/docs +/v1_0/docs +/v1_1/docs +/v1_0/openapi.json +/v1_1/openapi.json +``` + +There's also the possibility of adding a set of additional endpoints that +redirect the most recent API version. To do that make the argument +`enable_latest` true: + +```python +app = VersionedFastAPI(app, enable_latest=True) +``` + +this will generate the following additional endpoints: +``` +/latest/greet +/latest/docs +/latest/openapi.json +``` +In this example, `/latest` endpoints will reflect the same data as `/v1.1`. + +Try it out: +```sh +pip install pipenv +pipenv install --dev +pipenv run uvicorn example.annotation.app:app +# pipenv run uvicorn example.folder_name.app:app +``` + +## Usage without minor version +```python +from fastapi import FastAPI +from fastapi_versioning import VersionedFastAPI, version + +app = FastAPI(title='My App') + +@app.get('/greet') +@version(1) +def greet(): + return 'Hello' + +@app.get('/greet') +@version(2) +def greet(): + return 'Hi' + +app = VersionedFastAPI(app, + version_format='{major}', + prefix_format='/v{major}') +``` + +this will generate two endpoints: +``` +/v1/greet +/v2/greet +``` +as well as: +``` +/docs +/v1/docs +/v2/docs +/v1/openapi.json +/v2/openapi.json +``` + +## Extra FastAPI constructor arguments + +It's important to note that only the `title` from the original FastAPI will be +provided to the VersionedAPI app. If you have any middleware, event handlers +etc these arguments will also need to be provided to the VersionedAPI function +call, as in the example below + +```python +from fastapi import FastAPI, Request +from fastapi_versioning import VersionedFastAPI, version +from starlette.middleware import Middleware +from starlette.middleware.sessions import SessionMiddleware + +app = FastAPI( + title='My App', + description='Greet uses with a nice message', + middleware=[ + Middleware(SessionMiddleware, secret_key='mysecretkey') + ] +) + +@app.get('/greet') +@version(1) +def greet(request: Request): + request.session['last_version_used'] = 1 + return 'Hello' + +@app.get('/greet') +@version(2) +def greet(request: Request): + request.session['last_version_used'] = 2 + return 'Hi' + +@app.get('/version') +def last_version(request: Request): + return f'Your last greeting was sent from version {request.session["last_version_used"]}' + +app = VersionedFastAPI(app, + version_format='{major}', + prefix_format='/v{major}', + description='Greet users with a nice message', + middleware=[ + Middleware(SessionMiddleware, secret_key='mysecretkey') + ] +) +``` diff --git a/fastapi-versioning/example/__init__.py b/fastapi-versioning/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/example/annotation/__init__.py b/fastapi-versioning/example/annotation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/example/annotation/app.py b/fastapi-versioning/example/annotation/app.py new file mode 100644 index 0000000..09e9987 --- /dev/null +++ b/fastapi-versioning/example/annotation/app.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI + +from example.annotation import item, store +from fastapi_versioning import VersionedFastAPI + +app = FastAPI(title="My Online Store") +app.include_router(store.router) +app.include_router(item.router) +app = VersionedFastAPI(app, enable_latest=True) diff --git a/fastapi-versioning/example/annotation/item.py b/fastapi-versioning/example/annotation/item.py new file mode 100644 index 0000000..f04bbb7 --- /dev/null +++ b/fastapi-versioning/example/annotation/item.py @@ -0,0 +1,61 @@ +from typing import List + +from fastapi.routing import APIRouter +from pydantic import BaseModel + +from fastapi_versioning import version + +router = APIRouter() + + +class Item(BaseModel): + id: str + name: str + price: float + + +class ItemV1(Item): + quantity: int + + +class ComplexQuantity(BaseModel): + store_id: str + quantity: int + + +class ItemV2(Item): + quantity: List[ComplexQuantity] + + +@router.get("/item/{item_id}", response_model=ItemV1) +@version(1, 1) +def get_item_v1(item_id: str) -> ItemV1: + return ItemV1( + id=item_id, + name="ice cream", + price=1.2, + quantity=5, + ) + + +@router.get("/item/{item_id}", response_model=ItemV2) +@version(1, 2) +def get_item_v2(item_id: str) -> ItemV2: + return ItemV2( + id=item_id, + name="ice cream", + price=1.2, + quantity=[{"store_id": "1", "quantity": 5}], + ) + + +@router.delete("/item/{item_id}") +@version(1, 2) +def delete_item(item_id: str) -> None: + return None + + +@router.post("/item", response_model=ItemV2) +@version(1, 3) +def create_item(item: ItemV2) -> ItemV2: + return item diff --git a/fastapi-versioning/example/annotation/store.py b/fastapi-versioning/example/annotation/store.py new file mode 100644 index 0000000..2633512 --- /dev/null +++ b/fastapi-versioning/example/annotation/store.py @@ -0,0 +1,51 @@ +from typing import NoReturn + +from fastapi.exceptions import HTTPException +from fastapi.routing import APIRouter +from pydantic import BaseModel +from typing_extensions import Literal + +from fastapi_versioning import version + +router = APIRouter() + + +class StoreCommon(BaseModel): + id: str + name: str + country: str + + +class StoreV1(StoreCommon): + status: bool + + +class StoreV2(StoreCommon): + status: Literal["open", "closed", "closed_permanently"] + + +@router.get("/store/{store_id}", response_model=StoreV1) +def get_store_v1(store_id: str) -> StoreV1: + return StoreV1( + id=store_id, + name="ice cream shoppe", + country="Canada", + status=True, + ) + + +@router.get("/store/{store_id}", response_model=StoreV2) +@version(1, 1) +def get_store_v2(store_id: str) -> StoreV2: + return StoreV2( + id=store_id, + name="ice cream shoppe", + country="Canada", + status="open", + ) + + +@router.get("/store/{store_id}", include_in_schema=False) +@version(1, 3) +def get_store_v3(store_id: str) -> NoReturn: + raise HTTPException(status_code=404) diff --git a/fastapi-versioning/example/custom_default_version/__init__.py b/fastapi-versioning/example/custom_default_version/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/example/custom_default_version/app.py b/fastapi-versioning/example/custom_default_version/app.py new file mode 100644 index 0000000..2ca8711 --- /dev/null +++ b/fastapi-versioning/example/custom_default_version/app.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI + +from fastapi_versioning import VersionedFastAPI, version + +app = FastAPI(title="My Online Store") + + +@app.get("/") +def home() -> str: + return "Hello default version 2.0!" + + +@app.get("/") +@version(3, 0) +def home_v3() -> str: + return "Hello version 3.0!" + + +app = VersionedFastAPI(app, default_version=(2, 0)) diff --git a/fastapi-versioning/example/proxy/README.md b/fastapi-versioning/example/proxy/README.md new file mode 100644 index 0000000..2c28688 --- /dev/null +++ b/fastapi-versioning/example/proxy/README.md @@ -0,0 +1,17 @@ +### Running this example: +install traefik + +run: +```sh +traefik --configFile=traefik.toml +``` + +then in another shell run: +```sh +uvicorn app:app +``` + +alternatively, delete `root_path="/api"` from `app.py` and run: +```sh +uvicorn app:app --root-path /api +``` diff --git a/fastapi-versioning/example/proxy/__init__.py b/fastapi-versioning/example/proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/example/proxy/app.py b/fastapi-versioning/example/proxy/app.py new file mode 100644 index 0000000..9eb7223 --- /dev/null +++ b/fastapi-versioning/example/proxy/app.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI + +from fastapi_versioning import VersionedFastAPI, version + +app = FastAPI(title="My App") + + +@app.get("/greet") +@version(1, 0) +def greet_with_hello() -> str: + return "Hello" + + +@app.get("/greet") +@version(1, 1) +def greet_with_hi() -> str: + return "Hi" + + +app = VersionedFastAPI(app, root_path="/api") diff --git a/fastapi-versioning/example/proxy/routes.toml b/fastapi-versioning/example/proxy/routes.toml new file mode 100644 index 0000000..8b0377d --- /dev/null +++ b/fastapi-versioning/example/proxy/routes.toml @@ -0,0 +1,20 @@ +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" diff --git a/fastapi-versioning/example/proxy/traefik.toml b/fastapi-versioning/example/proxy/traefik.toml new file mode 100644 index 0000000..c994ccd --- /dev/null +++ b/fastapi-versioning/example/proxy/traefik.toml @@ -0,0 +1,7 @@ +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" diff --git a/fastapi-versioning/example/router/__init__.py b/fastapi-versioning/example/router/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/example/router/app.py b/fastapi-versioning/example/router/app.py new file mode 100644 index 0000000..3cf5605 --- /dev/null +++ b/fastapi-versioning/example/router/app.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI + +from example.router import v1_0, v1_1 +from fastapi_versioning import VersionedFastAPI + +app = FastAPI() +app.include_router(v1_0.router) +app.include_router(v1_1.router) +app = VersionedFastAPI(app) diff --git a/fastapi-versioning/example/router/v1_0.py b/fastapi-versioning/example/router/v1_0.py new file mode 100644 index 0000000..18d4023 --- /dev/null +++ b/fastapi-versioning/example/router/v1_0.py @@ -0,0 +1,8 @@ +from fastapi.routing import APIRouter + +router = APIRouter() + + +@router.get("/greet") +def greet() -> str: + return "Hello" diff --git a/fastapi-versioning/example/router/v1_1.py b/fastapi-versioning/example/router/v1_1.py new file mode 100644 index 0000000..ca7032a --- /dev/null +++ b/fastapi-versioning/example/router/v1_1.py @@ -0,0 +1,15 @@ +from fastapi.routing import APIRouter + +from fastapi_versioning import versioned_api_route + +router = APIRouter(route_class=versioned_api_route(1, 1)) + + +@router.get("/greet") +def greet() -> str: + return "Hi" + + +@router.delete("/greet") +def goodbye() -> str: + return "Goodbye" diff --git a/fastapi-versioning/fastapi_versioning/__init__.py b/fastapi-versioning/fastapi_versioning/__init__.py new file mode 100644 index 0000000..f86bcf7 --- /dev/null +++ b/fastapi-versioning/fastapi_versioning/__init__.py @@ -0,0 +1,8 @@ +from .routing import versioned_api_route +from .versioning import VersionedFastAPI, version + +__all__ = [ + "VersionedFastAPI", + "versioned_api_route", + "version", +] diff --git a/fastapi-versioning/fastapi_versioning/routing.py b/fastapi-versioning/fastapi_versioning/routing.py new file mode 100644 index 0000000..eeb34dc --- /dev/null +++ b/fastapi-versioning/fastapi_versioning/routing.py @@ -0,0 +1,18 @@ +from typing import Any, Type + +from fastapi.routing import APIRoute + + +def versioned_api_route( + major: int = 1, minor: int = 0, route_class: Type[APIRoute] = APIRoute +) -> Type[APIRoute]: + class VersionedAPIRoute(route_class): # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + try: + self.endpoint._api_version = (major, minor) + except AttributeError: + # Support bound methods + self.endpoint.__func__._api_version = (major, minor) + + return VersionedAPIRoute diff --git a/fastapi-versioning/fastapi_versioning/versioning.py b/fastapi-versioning/fastapi_versioning/versioning.py new file mode 100644 index 0000000..972010c --- /dev/null +++ b/fastapi-versioning/fastapi_versioning/versioning.py @@ -0,0 +1,88 @@ +from collections import defaultdict +from typing import Any, Callable, Dict, List, Tuple, TypeVar, cast + +from fastapi import FastAPI +from fastapi.routing import APIRoute +from starlette.routing import BaseRoute + +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def version(major: int, minor: int = 0) -> Callable[[CallableT], CallableT]: + def decorator(func: CallableT) -> CallableT: + func._api_version = (major, minor) # type: ignore + return func + + return decorator + + +def version_to_route( + route: BaseRoute, + default_version: Tuple[int, int], +) -> Tuple[Tuple[int, int], APIRoute]: + api_route = cast(APIRoute, route) + version = getattr(api_route.endpoint, "_api_version", default_version) + return version, api_route + + +def VersionedFastAPI( + app: FastAPI, + version_format: str = "{major}.{minor}", + prefix_format: str = "/v{major}_{minor}", + default_version: Tuple[int, int] = (1, 0), + enable_latest: bool = False, + **kwargs: Any, +) -> FastAPI: + parent_app = FastAPI( + title=app.title, + **kwargs, + ) + version_route_mapping: Dict[Tuple[int, int], List[APIRoute]] = defaultdict( + list + ) + version_routes = [ + version_to_route(route, default_version) for route in app.routes + ] + + for version, route in version_routes: + version_route_mapping[version].append(route) + + versions = sorted(version_route_mapping.keys()) + for version in versions: + unique_routes = {} + major, minor = version + prefix = prefix_format.format(major=major, minor=minor) + semver = version_format.format(major=major, minor=minor) + versioned_app = FastAPI( + title=app.title, + description=app.description, + version=semver, + ) + for route in version_route_mapping[version]: + for method in route.methods: + unique_routes[route.path + "|" + method] = route + for route in unique_routes.values(): + versioned_app.router.routes.append(route) + parent_app.mount(prefix, versioned_app) + + @parent_app.get( + f"{prefix}/openapi.json", name=semver, tags=["Versions"] + ) + @parent_app.get(f"{prefix}/docs", name=semver, tags=["Documentations"]) + def noop() -> None: + ... + + if enable_latest: + prefix = "/latest" + major, minor = version + semver = version_format.format(major=major, minor=minor) + versioned_app = FastAPI( + title=app.title, + description=app.description, + version=semver, + ) + for route in unique_routes.values(): + versioned_app.router.routes.append(route) + parent_app.mount(prefix, versioned_app) + + return parent_app diff --git a/fastapi-versioning/mypy.ini b/fastapi-versioning/mypy.ini new file mode 100644 index 0000000..b1efbe8 --- /dev/null +++ b/fastapi-versioning/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +strict = True diff --git a/fastapi-versioning/pyproject.toml b/fastapi-versioning/pyproject.toml new file mode 100644 index 0000000..dd23c7c --- /dev/null +++ b/fastapi-versioning/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta:__legacy__" + +[tool.isort] +profile = "black" + +[tool.black] +line-length = 79 diff --git a/fastapi-versioning/sample.py b/fastapi-versioning/sample.py new file mode 100644 index 0000000..5096b13 --- /dev/null +++ b/fastapi-versioning/sample.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI + +from fastapi_versioning import VersionedFastAPI, version + +app = FastAPI(title="My App") + + +@app.get("/") +@version(1, 0) +def greet_with_hello(): + return "Hello" + + +@app.get("/") +@version(1, 1) +def greet_with_hi(): + return "Hi" + + +app = VersionedFastAPI(app) diff --git a/fastapi-versioning/scripts/build.sh b/fastapi-versioning/scripts/build.sh new file mode 100755 index 0000000..9d6e014 --- /dev/null +++ b/fastapi-versioning/scripts/build.sh @@ -0,0 +1,6 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +rm -r dist/ +python -m build diff --git a/fastapi-versioning/scripts/bumpversion.sh b/fastapi-versioning/scripts/bumpversion.sh new file mode 100755 index 0000000..64c70d3 --- /dev/null +++ b/fastapi-versioning/scripts/bumpversion.sh @@ -0,0 +1,10 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +git fetch origin +git checkout -B master +git reset --soft origin/master +bumpversion "$@" +git push +git push --tags diff --git a/fastapi-versioning/scripts/ci.sh b/fastapi-versioning/scripts/ci.sh new file mode 100755 index 0000000..69ec015 --- /dev/null +++ b/fastapi-versioning/scripts/ci.sh @@ -0,0 +1,5 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" + +./static-analysis.sh +./test.sh diff --git a/fastapi-versioning/scripts/lint-fix.sh b/fastapi-versioning/scripts/lint-fix.sh new file mode 100755 index 0000000..42b73b5 --- /dev/null +++ b/fastapi-versioning/scripts/lint-fix.sh @@ -0,0 +1,6 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +isort . +black . diff --git a/fastapi-versioning/scripts/lint.sh b/fastapi-versioning/scripts/lint.sh new file mode 100755 index 0000000..3224ebc --- /dev/null +++ b/fastapi-versioning/scripts/lint.sh @@ -0,0 +1,7 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +isort --check . +black --check . +flake8 . diff --git a/fastapi-versioning/scripts/static-analysis.sh b/fastapi-versioning/scripts/static-analysis.sh new file mode 100755 index 0000000..0dd8f32 --- /dev/null +++ b/fastapi-versioning/scripts/static-analysis.sh @@ -0,0 +1,5 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" + +./lint.sh +./type-check.sh diff --git a/fastapi-versioning/scripts/test.sh b/fastapi-versioning/scripts/test.sh new file mode 100755 index 0000000..2ea622a --- /dev/null +++ b/fastapi-versioning/scripts/test.sh @@ -0,0 +1,5 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +pytest tests/ diff --git a/fastapi-versioning/scripts/type-check.sh b/fastapi-versioning/scripts/type-check.sh new file mode 100755 index 0000000..df8f281 --- /dev/null +++ b/fastapi-versioning/scripts/type-check.sh @@ -0,0 +1,7 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +mypy fastapi_versioning/ +mypy tests/ +mypy example/ diff --git a/fastapi-versioning/scripts/upload.sh b/fastapi-versioning/scripts/upload.sh new file mode 100755 index 0000000..6d24999 --- /dev/null +++ b/fastapi-versioning/scripts/upload.sh @@ -0,0 +1,5 @@ +#! /bin/bash -e +cd "$( dirname "${BASH_SOURCE[0]}" )" +cd .. + +twine upload dist/* diff --git a/fastapi-versioning/setup.py b/fastapi-versioning/setup.py new file mode 100644 index 0000000..16a2418 --- /dev/null +++ b/fastapi-versioning/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="fastapi_versioning", + version="0.10.0", + author="Dean Way", + description="api versioning for fastapi web applications", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/DeanWay/fastapi-versioning", + packages=["fastapi_versioning"], + classifiers=[ + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + install_requires=[ + "fastapi>=0.56.0", + "starlette", + ], + python_requires=">=3.6", +) diff --git a/fastapi-versioning/tests/test_example.py b/fastapi-versioning/tests/test_example.py new file mode 100644 index 0000000..37b19d0 --- /dev/null +++ b/fastapi-versioning/tests/test_example.py @@ -0,0 +1,89 @@ +from starlette.testclient import TestClient + +from example.annotation.app import app as annotation_app +from example.custom_default_version.app import app as default_version_app +from example.router.app import app as router_app + + +def test_annotation_app() -> None: + test_client = TestClient(annotation_app) + assert test_client.get("/docs").status_code == 200 + assert test_client.get("/v1_0/docs").status_code == 200 + assert test_client.get("/v1_1/docs").status_code == 200 + assert test_client.get("/v1_2/docs").status_code == 200 + assert test_client.get("/v1_3/docs").status_code == 200 + assert test_client.get("/latest/docs").status_code == 200 + assert test_client.get("/v1_4/docs").status_code == 404 + + assert test_client.get("/v1_0/item/1").status_code == 404 + assert test_client.get("/v1_1/item/1").status_code == 200 + assert test_client.get("/v1_1/item/1").json()["quantity"] == 5 + complex_quantity = [{"store_id": "1", "quantity": 5}] + + assert ( + test_client.get("/v1_2/item/1").json()["quantity"] == complex_quantity + ) + assert ( + test_client.get("/v1_3/item/1").json()["quantity"] == complex_quantity + ) + + assert ( + test_client.get("/latest/item/1").json()["quantity"] + == complex_quantity + ) + + item = { + "id": "1", + "name": "apple", + "price": 1.0, + "quantity": complex_quantity, + } + assert ( + test_client.post( + "/v1_3/item", + json=item, + ).json() + == item + ) + assert ( + test_client.post( + "/latest/item", + json=item, + ).json() + == item + ) + + assert test_client.delete("/v1_1/item/1").status_code == 405 + assert test_client.delete("/v1_2/item/1").status_code == 200 + + assert test_client.get("/v1_0/store/1").status_code == 200 + assert test_client.get("/v1_0/store/1").json()["status"] is True + assert test_client.get("/v1_1/store/1").json()["status"] == "open" + assert test_client.get("/v1_2/store/1").json()["status"] == "open" + assert test_client.get("/v1_3/store/1").status_code == 404 + + +def test_router_app() -> None: + test_client = TestClient(router_app) + assert test_client.get("/docs").status_code == 200 + assert test_client.get("/v1_0/docs").status_code == 200 + assert test_client.get("/v1_1/docs").status_code == 200 + assert test_client.get("/v1_3/docs").status_code == 404 + + assert test_client.get("/v1_0/greet").json() == "Hello" + assert test_client.get("/v1_1/greet").json() == "Hi" + + assert test_client.delete("/v1_0/greet").status_code == 405 + assert test_client.delete("/v1_1/greet").status_code == 200 + assert test_client.delete("/v1_1/greet").json() == "Goodbye" + + +def test_default_version() -> None: + test_client = TestClient(default_version_app) + assert test_client.get("/docs").status_code == 200 + assert test_client.get("/v1_0/docs").status_code == 404 + assert test_client.get("/v2_0/docs").status_code == 200 + assert test_client.get("/v3_0/docs").status_code == 200 + + assert test_client.get("/v2_0/").json() == "Hello default version 2.0!" + assert test_client.get("/v3_0/").json() == "Hello version 3.0!" diff --git a/fastapi-versioning/tests/unit/__init__.py b/fastapi-versioning/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-versioning/tests/unit/test_root_path.py b/fastapi-versioning/tests/unit/test_root_path.py new file mode 100644 index 0000000..14d0c84 --- /dev/null +++ b/fastapi-versioning/tests/unit/test_root_path.py @@ -0,0 +1,22 @@ +from typing import Any, Dict + +from fastapi import FastAPI, Request +from starlette.testclient import TestClient + +from fastapi_versioning import VersionedFastAPI + + +def test_root_path() -> None: + root_path = "/custom/root" + parent_app = FastAPI() + + @parent_app.get("/check-root-path") + def check_root_path(request: Request) -> Dict[str, Any]: + return {"root_path": request.scope.get("root_path")} + + versioned_app = VersionedFastAPI(app=parent_app, root_path=root_path) + test_client = TestClient(versioned_app, root_path=root_path) + + response = test_client.get("/v1_0/check-root-path") + assert response.status_code == 200 + assert response.json() == {"root_path": "/custom/root/v1_0"} diff --git a/fastapi-versioning/tox.ini b/fastapi-versioning/tox.ini new file mode 100644 index 0000000..efcf9f5 --- /dev/null +++ b/fastapi-versioning/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py36, py37, py38 + +[testenv] +deps = pipenv +setenv = + PYTHONPATH={toxinidir} +commands= + pipenv install --dev --system --skip-lock + pipenv run ./scripts/ci.sh diff --git a/fastapi_versioning b/fastapi_versioning new file mode 120000 index 0000000..290b401 --- /dev/null +++ b/fastapi_versioning @@ -0,0 +1 @@ +fastapi-versioning/fastapi_versioning/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b47ae11 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1105 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "aioredis" +version = "2.0.1" +description = "asyncio (PEP 3156) Redis support" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, + {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, +] + +[package.dependencies] +async-timeout = "*" +typing-extensions = "*" + +[package.extras] +hiredis = ["hiredis (>=1.0)"] + +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.108.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.108.0-py3-none-any.whl", hash = "sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7"}, + {file = "fastapi-0.108.0.tar.gz", hash = "sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.29.0,<0.33.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-cache2" +version = "0.1.9" +description = "Cache for FastAPI" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "fastapi-cache2-0.1.9.tar.gz", hash = "sha256:816612f7b29b4ea4ed3b4e03c55b7f96b4e4d6dffce6a95e2cf5cf36a980eaaa"}, + {file = "fastapi_cache2-0.1.9-py3-none-any.whl", hash = "sha256:5b6f32bc8e786d9fffe4f3ef343861cab276acc64fb5d8d09077524743ad3702"}, +] + +[package.dependencies] +fastapi = "*" +pendulum = "*" +redis = {version = ">=4.2.0rc1,<5.0.0", optional = true, markers = "extra == \"redis\" or extra == \"all\""} +uvicorn = "*" + +[package.extras] +all = ["aiobotocore (>=1.4.1,<2.0.0)", "aiomcache", "redis (>=4.2.0rc1,<5.0.0)"] +dynamodb = ["aiobotocore (>=1.4.1,<2.0.0)"] +memcache = ["aiomcache"] +redis = ["redis (>=4.2.0rc1,<5.0.0)"] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "geojson-pydantic" +version = "0.4.3" +description = "Pydantic data models for the GeoJSON spec." +optional = false +python-versions = ">=3.7" +files = [ + {file = "geojson-pydantic-0.4.3.tar.gz", hash = "sha256:34c9e43509012ef6ad7b0f600aa856da23fb13edbf55964dcca4a00a267385e0"}, + {file = "geojson_pydantic-0.4.3-py3-none-any.whl", hash = "sha256:716cff5bbb2d3abafb7f45f40b22cb74858a4e282126c7a5871fbee3b888924f"}, +] + +[package.dependencies] +pydantic = "*" + +[package.extras] +dev = ["pre-commit"] +test = ["pytest", "pytest-cov", "shapely"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pendulum" +version = "3.0.0" +description = "Python datetimes made easy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, + {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[package.dependencies] +python-dateutil = ">=2.6" +tzdata = ">=2020.1" + +[package.extras] +test = ["time-machine (>=2.6.0)"] + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pydantic" +version = "1.10.13" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pymemcache" +version = "3.5.2" +description = "A comprehensive, fast, pure Python memcached client" +optional = false +python-versions = "*" +files = [ + {file = "pymemcache-3.5.2-py2.py3-none-any.whl", hash = "sha256:3fca0215845d7b2ecd5f4c627fcf4ce2345a703a897b7e116380115b5a197be2"}, + {file = "pymemcache-3.5.2.tar.gz", hash = "sha256:8923ab59840f0d5338f1c52dba229fa835545b91c3c2f691c118e678d0fb974e"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {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"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "rdflib" +version = "6.3.2" +description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "rdflib-6.3.2-py3-none-any.whl", hash = "sha256:36b4e74a32aa1e4fa7b8719876fb192f19ecd45ff932ea5ebbd2e417a0247e63"}, + {file = "rdflib-6.3.2.tar.gz", hash = "sha256:72af591ff704f4caacea7ecc0c5a9056b8553e0489dd4f35a9bc52dbd41522e0"}, +] + +[package.dependencies] +isodate = ">=0.6.0,<0.7.0" +pyparsing = ">=2.1.0,<4" + +[package.extras] +berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"] +html = ["html5lib (>=1.0,<2.0)"] +lxml = ["lxml (>=4.3.0,<5.0.0)"] +networkx = ["networkx (>=2.0.0,<3.0.0)"] + +[[package]] +name = "redis" +version = "4.6.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "sentry-sdk" +version = "1.39.1" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"}, + {file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +grpcio = ["grpcio (>=1.21.1)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +loguru = ["loguru (>=0.5)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "simplejson" +version = "3.19.2" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "simplejson-3.19.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3471e95110dcaf901db16063b2e40fb394f8a9e99b3fe9ee3acc6f6ef72183a2"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3194cd0d2c959062b94094c0a9f8780ffd38417a5322450a0db0ca1a23e7fbd2"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:8a390e56a7963e3946ff2049ee1eb218380e87c8a0e7608f7f8790ba19390867"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1537b3dd62d8aae644f3518c407aa8469e3fd0f179cdf86c5992792713ed717a"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a8617625369d2d03766413bff9e64310feafc9fc4f0ad2b902136f1a5cd8c6b0"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2c433a412e96afb9a3ce36fa96c8e61a757af53e9c9192c97392f72871e18e69"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f1c70249b15e4ce1a7d5340c97670a95f305ca79f376887759b43bb33288c973"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:287e39ba24e141b046812c880f4619d0ca9e617235d74abc27267194fc0c7835"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6f0a0b41dd05eefab547576bed0cf066595f3b20b083956b1405a6f17d1be6ad"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f98d918f7f3aaf4b91f2b08c0c92b1774aea113334f7cde4fe40e777114dbe6"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d74beca677623481810c7052926365d5f07393c72cbf62d6cce29991b676402"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f2398361508c560d0bf1773af19e9fe644e218f2a814a02210ac2c97ad70db0"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ad331349b0b9ca6da86064a3599c425c7a21cd41616e175ddba0866da32df48"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:332c848f02d71a649272b3f1feccacb7e4f7e6de4a2e6dc70a32645326f3d428"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25785d038281cd106c0d91a68b9930049b6464288cea59ba95b35ee37c2d23a5"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18955c1da6fc39d957adfa346f75226246b6569e096ac9e40f67d102278c3bcb"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:11cc3afd8160d44582543838b7e4f9aa5e97865322844b75d51bf4e0e413bb3e"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b01fda3e95d07a6148702a641e5e293b6da7863f8bc9b967f62db9461330562c"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:778331444917108fa8441f59af45886270d33ce8a23bfc4f9b192c0b2ecef1b3"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9eb117db8d7ed733a7317c4215c35993b815bf6aeab67523f1f11e108c040672"}, + {file = "simplejson-3.19.2-cp310-cp310-win32.whl", hash = "sha256:39b6d79f5cbfa3eb63a869639cfacf7c41d753c64f7801efc72692c1b2637ac7"}, + {file = "simplejson-3.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:5675e9d8eeef0aa06093c1ff898413ade042d73dc920a03e8cea2fb68f62445a"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed628c1431100b0b65387419551e822987396bee3c088a15d68446d92f554e0c"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:adcb3332979cbc941b8fff07181f06d2b608625edc0a4d8bc3ffc0be414ad0c4"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08889f2f597ae965284d7b52a5c3928653a9406d88c93e3161180f0abc2433ba"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7938a78447174e2616be223f496ddccdbf7854f7bf2ce716dbccd958cc7d13"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a970a2e6d5281d56cacf3dc82081c95c1f4da5a559e52469287457811db6a79b"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554313db34d63eac3b3f42986aa9efddd1a481169c12b7be1e7512edebff8eaf"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d36081c0b1c12ea0ed62c202046dca11438bee48dd5240b7c8de8da62c620e9"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3cd18e03b0ee54ea4319cdcce48357719ea487b53f92a469ba8ca8e39df285e"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66e5dc13bfb17cd6ee764fc96ccafd6e405daa846a42baab81f4c60e15650414"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:972a7833d4a1fcf7a711c939e315721a88b988553fc770a5b6a5a64bd6ebeba3"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3e74355cb47e0cd399ead3477e29e2f50e1540952c22fb3504dda0184fc9819f"}, + {file = "simplejson-3.19.2-cp311-cp311-win32.whl", hash = "sha256:1dd4f692304854352c3e396e9b5f0a9c9e666868dd0bdc784e2ac4c93092d87b"}, + {file = "simplejson-3.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:9300aee2a8b5992d0f4293d88deb59c218989833e3396c824b69ba330d04a589"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b8d940fd28eb34a7084877747a60873956893e377f15a32ad445fe66c972c3b8"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4969d974d9db826a2c07671273e6b27bc48e940738d768fa8f33b577f0978378"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c594642d6b13d225e10df5c16ee15b3398e21a35ecd6aee824f107a625690374"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f5a398b5e77bb01b23d92872255e1bcb3c0c719a3be40b8df146570fe7781a"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176a1b524a3bd3314ed47029a86d02d5a95cc0bee15bd3063a1e1ec62b947de6"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3c7363a8cb8c5238878ec96c5eb0fc5ca2cb11fc0c7d2379863d342c6ee367a"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:346820ae96aa90c7d52653539a57766f10f33dd4be609206c001432b59ddf89f"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de9a2792612ec6def556d1dc621fd6b2073aff015d64fba9f3e53349ad292734"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1c768e7584c45094dca4b334af361e43b0aaa4844c04945ac7d43379eeda9bc2"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:9652e59c022e62a5b58a6f9948b104e5bb96d3b06940c6482588176f40f4914b"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9c1a4393242e321e344213a90a1e3bf35d2f624aa8b8f6174d43e3c6b0e8f6eb"}, + {file = "simplejson-3.19.2-cp312-cp312-win32.whl", hash = "sha256:7cb98be113911cb0ad09e5523d0e2a926c09a465c9abb0784c9269efe4f95917"}, + {file = "simplejson-3.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:6779105d2fcb7fcf794a6a2a233787f6bbd4731227333a072d8513b252ed374f"}, + {file = "simplejson-3.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:061e81ea2d62671fa9dea2c2bfbc1eec2617ae7651e366c7b4a2baf0a8c72cae"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4280e460e51f86ad76dc456acdbfa9513bdf329556ffc8c49e0200878ca57816"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11c39fbc4280d7420684494373b7c5904fa72a2b48ef543a56c2d412999c9e5d"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bccb3e88ec26ffa90f72229f983d3a5d1155e41a1171190fa723d4135523585b"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb5b50dc6dd671eb46a605a3e2eb98deb4a9af787a08fcdddabe5d824bb9664"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d94245caa3c61f760c4ce4953cfa76e7739b6f2cbfc94cc46fff6c050c2390c5"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d0e5ffc763678d48ecc8da836f2ae2dd1b6eb2d27a48671066f91694e575173c"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d222a9ed082cd9f38b58923775152003765016342a12f08f8c123bf893461f28"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8434dcdd347459f9fd9c526117c01fe7ca7b016b6008dddc3c13471098f4f0dc"}, + {file = "simplejson-3.19.2-cp36-cp36m-win32.whl", hash = "sha256:c9ac1c2678abf9270e7228133e5b77c6c3c930ad33a3c1dfbdd76ff2c33b7b50"}, + {file = "simplejson-3.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92c4a4a2b1f4846cd4364855cbac83efc48ff5a7d7c06ba014c792dd96483f6f"}, + {file = "simplejson-3.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0d551dc931638e2102b8549836a1632e6e7cf620af3d093a7456aa642bff601d"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a8a4653f2e809049999d63530180d7b5a344b23a793502413ad1ecea9a0290"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40847f617287a38623507d08cbcb75d51cf9d4f9551dd6321df40215128325a3"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be893258d5b68dd3a8cba8deb35dc6411db844a9d35268a8d3793b9d9a256f80"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9eb3cff1b7d71aa50c89a0536f469cb8d6dcdd585d8f14fb8500d822f3bdee4"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d0f402e787e6e7ee7876c8b05e2fe6464820d9f35ba3f172e95b5f8b699f6c7f"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbbcc6b0639aa09b9649f36f1bcb347b19403fe44109948392fbb5ea69e48c3e"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2fc697be37585eded0c8581c4788fcfac0e3f84ca635b73a5bf360e28c8ea1a2"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b0a3eb6dd39cce23801a50c01a0976971498da49bc8a0590ce311492b82c44b"}, + {file = "simplejson-3.19.2-cp37-cp37m-win32.whl", hash = "sha256:49f9da0d6cd17b600a178439d7d2d57c5ef01f816b1e0e875e8e8b3b42db2693"}, + {file = "simplejson-3.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c87c22bd6a987aca976e3d3e23806d17f65426191db36d40da4ae16a6a494cbc"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e4c166f743bb42c5fcc60760fb1c3623e8fda94f6619534217b083e08644b46"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a48679310e1dd5c9f03481799311a65d343748fe86850b7fb41df4e2c00c087"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0521e0f07cb56415fdb3aae0bbd8701eb31a9dfef47bb57206075a0584ab2a2"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2d5119b1d7a1ed286b8af37357116072fc96700bce3bec5bb81b2e7057ab41"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c1467d939932901a97ba4f979e8f2642415fcf02ea12f53a4e3206c9c03bc17"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49aaf4546f6023c44d7e7136be84a03a4237f0b2b5fb2b17c3e3770a758fc1a0"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60848ab779195b72382841fc3fa4f71698a98d9589b0a081a9399904487b5832"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0436a70d8eb42bea4fe1a1c32d371d9bb3b62c637969cb33970ad624d5a3336a"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:49e0e3faf3070abdf71a5c80a97c1afc059b4f45a5aa62de0c2ca0444b51669b"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ff836cd4041e16003549449cc0a5e372f6b6f871eb89007ab0ee18fb2800fded"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3848427b65e31bea2c11f521b6fc7a3145d6e501a1038529da2391aff5970f2f"}, + {file = "simplejson-3.19.2-cp38-cp38-win32.whl", hash = "sha256:3f39bb1f6e620f3e158c8b2eaf1b3e3e54408baca96a02fe891794705e788637"}, + {file = "simplejson-3.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:0405984f3ec1d3f8777c4adc33eac7ab7a3e629f3b1c05fdded63acc7cf01137"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:445a96543948c011a3a47c8e0f9d61e9785df2544ea5be5ab3bc2be4bd8a2565"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a8c3cc4f9dfc33220246760358c8265dad6e1104f25f0077bbca692d616d358"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af9c7e6669c4d0ad7362f79cb2ab6784d71147503e62b57e3d95c4a0f222c01c"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:064300a4ea17d1cd9ea1706aa0590dcb3be81112aac30233823ee494f02cb78a"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9453419ea2ab9b21d925d0fd7e3a132a178a191881fab4169b6f96e118cc25bb"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e038c615b3906df4c3be8db16b3e24821d26c55177638ea47b3f8f73615111c"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16ca9c90da4b1f50f089e14485db8c20cbfff2d55424062791a7392b5a9b3ff9"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1018bd0d70ce85f165185d2227c71e3b1e446186f9fa9f971b69eee223e1e3cd"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e8dd53a8706b15bc0e34f00e6150fbefb35d2fd9235d095b4f83b3c5ed4fa11d"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d022b14d7758bfb98405672953fe5c202ea8a9ccf9f6713c5bd0718eba286fd"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:febffa5b1eda6622d44b245b0685aff6fb555ce0ed734e2d7b1c3acd018a2cff"}, + {file = "simplejson-3.19.2-cp39-cp39-win32.whl", hash = "sha256:4edcd0bf70087b244ba77038db23cd98a1ace2f91b4a3ecef22036314d77ac23"}, + {file = "simplejson-3.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad7405c033d32c751d98d3a65801e2797ae77fac284a539f6c3a3e13005edc4"}, + {file = "simplejson-3.19.2-py3-none-any.whl", hash = "sha256:bcedf4cae0d47839fee7de344f96b5694ca53c786f28b5f773d4f0b265a159eb"}, + {file = "simplejson-3.19.2.tar.gz", hash = "sha256:9eb442a2442ce417801c912df68e1f6ccfcd41577ae7274953ab3ad24ef7d82c"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "sparqltransformer" +version = "2.3.0" +description = "Write your SPARQL query directly in the JSON-LD you would like to have in output" +optional = false +python-versions = "*" +files = [ + {file = "SPARQLTransformer-2.3.0.tar.gz", hash = "sha256:c4be13c7fdfa1876fc45711fa2d232144e924c03f2d2699bbe2d00aa7a797f14"}, +] + +[package.dependencies] +simplejson = "*" +SPARQLWrapper = "*" + +[[package]] +name = "sparqlwrapper" +version = "1.8.5" +description = "SPARQL Endpoint interface to Python" +optional = false +python-versions = "*" +files = [ + {file = "SPARQLWrapper-1.8.5-py2-none-any.whl", hash = "sha256:357ee8a27bc910ea13d77836dbddd0b914991495b8cc1bf70676578155e962a8"}, + {file = "SPARQLWrapper-1.8.5-py3-none-any.whl", hash = "sha256:c7f9c9d8ebb13428771bc3b6dee54197422507dcc3dea34e30d5dcfc53478dec"}, + {file = "SPARQLWrapper-1.8.5.tar.gz", hash = "sha256:d6a66b5b8cda141660e07aeb00472db077a98d22cb588c973209c7336850fb3c"}, +] + +[package.dependencies] +rdflib = ">=4.0" + +[package.extras] +keepalive = ["keepalive (>=0.5)"] + +[[package]] +name = "starlette" +version = "0.32.0.post1" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.32.0.post1-py3-none-any.whl", hash = "sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09"}, + {file = "starlette-0.32.0.post1.tar.gz", hash = "sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "tzdata" +version = "2023.4" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.17.6" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, + {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, +] + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=10.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "670c2363357dd05ec6f86425b75a25b9eab3db43154cc3900723019b7ac79902" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c0799ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[tool.black] +line-length = 120 +include = "\\.pyi?$" +exclude = """ +/( + \\.eggs + | \\.git + | \\.hg + | \\.mypy_cache + | \\.tox + | \\.venv + | _build + | buck-out + | build + | dist +)/ +""" + +[tool.isort] +profile = "black" + +[tool.poetry] +name = "semantic-kraus-api" +version = "0.1" +description = "" +authors = [ "Matthias Schlögl " ] +license = "MIT" + + [tool.poetry.dependencies] + python = "^3.10" + fastapi = "*" + uvicorn = "^0.17.6" + rdflib = "^6.1.1" + pydantic = ">=1.10.2,<2" + SPARQLWrapper = "^1.8.5" + geojson-pydantic = "^0.4.0" + Jinja2 = "^3.1.2" + pymemcache = "^3.5.2" + SPARQLTransformer = "^2.2.0" + sentry-sdk = "^1.6.0" + python-dateutil = "^2.8.2" + gunicorn = "^20.1.0" + aioredis = "^2.0.1" + requests = "^2.28.2" + + [tool.poetry.dependencies.fastapi-cache2] + extras = [ "redis" ] + version = "^0.1.9" + +[tool.poetry.group.dev.dependencies] +black = "^22.10.0" +flake8 = "^5.0.4" + +[build-system] +requires = [ "poetry-core>=1.0.0" ] +build-backend = "poetry.core.masonry.api" diff --git a/rdf-fastapi-utils/.gitignore b/rdf-fastapi-utils/.gitignore new file mode 100644 index 0000000..7d33cdb --- /dev/null +++ b/rdf-fastapi-utils/.gitignore @@ -0,0 +1,166 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/rdf-fastapi-utils/README.md b/rdf-fastapi-utils/README.md new file mode 100644 index 0000000..9cbce91 --- /dev/null +++ b/rdf-fastapi-utils/README.md @@ -0,0 +1,131 @@ +# RDF-FastAPI-Utils +This is a small utils library for providing access to data in a triplestore via a FastAPI Rest endpoint. +Currently it contains only those classes used to convert a Json resulting from a SPARQL query into a given pydantic model. + +Currently it contains two classes: +- `models.FieldConfigurationRDF` to add additional information for processing RDF data to the fields in a pydantic model +- and `models.RDFUtilsModelBaseClass` as a pydantic base class to inherit from for adding SPARQL Json to a pydantic model. + +## Minimal example +*this is taken from the tests* + +models.py: +```python +from pydantic import Field +from rdf_fastapi_utils.models import FieldConfigurationRDF, RDFUtilsModelBaseClass + + +class TCPaginatedResponse(RDFUtilsModelBaseClass): + count: int = Field(..., rdfconfig=FieldConfigurationRDF(path="count")) + results: list[Union["TCPersonFull", "TCPlaceFull"]] = Field( + ..., + rdfconfig=FieldConfigurationRDF(path="results", serialization_class_callback=lambda field, item: TCPersonFull), + ) + + +class TCPersonFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="person")) + name: str = Field(..., rdfconfig=FieldConfigurationRDF(path="entityLabel")) + events: list["TCEventFull"] = None + + +class TCPlaceFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="person")) + name: str = Field(..., rdfconfig=FieldConfigurationRDF(path="entityLabel")) + events: list["TCEventFull"] = None + + +class TCEventFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="event")) + label: str = Field(..., rdfconfig=FieldConfigurationRDF(path="eventLabel")) + + +TCPaginatedResponse.update_forward_refs() +TCPersonFull.update_forward_refs() +TCEventFull.update_forward_refs() + +``` + +test_data.json +```json +{ + "page": 1, + "count": 1, + "pages": 1, + "results": [ + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/deceased_person/27118", + "event": "http://www.intavia.eu/apis/deathevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "eventLabel": "Death of Nikola Tesla", + "end": "1943-01-07 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/deceased_person/27118", + "event": "http://www.intavia.eu/apis/deathevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "eventLabel": "Death of Nikola Tesla", + "end": "1943-01-07 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155790", + "event": "http://www.intavia.eu/apis/event/personplace/155790", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Smiljan", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155790", + "event": "http://www.intavia.eu/apis/event/personplace/155790", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Smiljan", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + } + ] +} +``` + +Running the following with the above files in place will result in a correctly nested python object: + +```python +import json +from .models import TCPaginatedResponse + +with open("test_data.json") as inp: + data = json.load(inp) + res = TCPaginatedResponse(**data) + +print(res) +``` \ No newline at end of file diff --git a/rdf-fastapi-utils/pyproject.toml b/rdf-fastapi-utils/pyproject.toml new file mode 100644 index 0000000..c44fc18 --- /dev/null +++ b/rdf-fastapi-utils/pyproject.toml @@ -0,0 +1,42 @@ +[tool.black] +line-length = 120 +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" + +[tool.poetry] +name = "rdf-fastapi-utils" +version = "0.1.3" +description = "Utils package for interacting with SPARQL endpoint via FastAPI" +authors = ["Matthias Schlögl "] +license = "MIT" +readme = "README.md" +packages = [{include = "rdf_fastapi_utils"}] + +[tool.poetry.dependencies] +python = "^3.10" +pydantic = "^1.10.2" +rdflib = "^6.2.0" + +[tool.poetry.group.dev.dependencies] +black = "^22.10.0" +flake8 = "^5.0.4" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/__init__.py b/rdf-fastapi-utils/rdf_fastapi_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/models.py b/rdf-fastapi-utils/rdf_fastapi_utils/models.py new file mode 100644 index 0000000..0e8409f --- /dev/null +++ b/rdf-fastapi-utils/rdf_fastapi_utils/models.py @@ -0,0 +1,419 @@ +from copy import deepcopy +import datetime +from typing import Any, Callable, List, Tuple +import typing +from pydantic import BaseModel, Field, HttpUrl, ValidationError, constr +from pydantic.fields import ModelField + + +def removeNullNoneEmpty(ob): + l = {} + check = False + for k, v in ob.items(): + if isinstance(v, dict): + x = removeNullNoneEmpty(v) + if len(x.keys()) > 0: + l[k] = x + if x != v: + check = True + elif isinstance(v, list): + p = [] + for c in v: + if isinstance(c, dict): + x = removeNullNoneEmpty(c) + if len(x.keys()) > 0: + p.append(x) + elif c is not None and c != "": + p.append(c) + if len(p) > 0: + l[k] = p + if p != v: + check = True + elif v is not None and v != "": + l[k] = v + if check: + l = removeNullNoneEmpty(l) + return l + + +class FieldConfigurationRDF(BaseModel): + """Configuration for how to use RDF data in the field""" + + path: constr(regex="^[a-zA-Z0-9\._]+$") | None = Field( + None, description="RDF variable to use for populating the field" + ) + anchor: bool = Field(False, description="Whether to use the RDF variable as an anchor") + default_value: Any = Field(None, description="Default value to use when populating the field") + callback_function: Callable | None = Field( + None, description="Callback for postprocessing data from the RDF variable" + ) + serialization_class_callback: Callable | None = Field( + None, + description="Callback function for deciding on the correct class for serialization. Function\ + gets two parameters: fields (array) and RDFData and needs to return a list of tuples with `(field, [data])`.", + ) + default_dict_key: constr(regex="^[a-zA-Z0-9_]+$") | None = Field( + None, desctiption="In a related field use this key as default" + ) + encode_function: Callable | None = Field( + None, + description="Callback for encoding data from the RDF variable. E.g for base64 encoding of URIs.\ + The function gets only the value of the field passed and returns the encoded field.", + ) + bypass_data_mapping: bool = Field( + False, + description="Wether to bypass the automated data mapping, e.g. to do it in a dedicated callback function.", + ) + + +class RDFUtilsModelBaseClass(BaseModel): + """Base class for models that use RDF data""" + + class Config: + RDF_utils_catch_errors = False + RDF_utils_error_field_name = "errors" + # RDF_utils_move_errors_to_top = False FIXME: add again when #3 is resolved + + @staticmethod + def harm_filter_sparql(data: list) -> list | None: + for ent in data: # FIXME: this is a hack to fix the problem with the filter_sparql function + if ent: + return data + return None + + def filter_sparql( + self, + data: list | dict, + filters: typing.List[typing.Tuple[str, str]] | None = None, + list_of_keys: typing.List[str] = None, + anchor: str | None = None, + additional_values: typing.List[str] | None = None, + ) -> typing.List[dict] | None: + """filters sparql result for key value pairs + + Args: + data (list): array of results from sparql endpoint (python object converted from json return) + filter (typing.List[tuple]): list of tuples containing key / value pair to filter on + list_of_keys (typing.List[str], optional): list of keys to return. Defaults to None. + additional_values (typing.List[str], optional): list of additional values to return. Defaults to None. + + Returns: + typing.List[dict] | None: list of dictionaries containing keys and values + """ + if isinstance(data, list): + if len(data) == 0: + return [] + if not isinstance(data[0], dict): + return data + if isinstance(data, dict): + data = [data] + if additional_values is None: + additional_values = [] + for ent in data: + for key in ent: + if key not in additional_values: + additional_values.append(key) + if filters is not None: + while len(filters) > 0 and len(data) > 0: + f1 = filters.pop(0) + data_res = list(filter(lambda x: (x[f1[0]] == f1[1]), data)) + data = data_res + if len(data) == 0: + return None + # if list_of_keys is not None: + # data = [{k: v for k, v in d.items() if k in list_of_keys or k in additional_values} for d in data] + if list_of_keys is None: + list_of_keys = [] + for ent in data: + for key in ent: + if key not in list_of_keys: + list_of_keys.append(key) + if anchor is not None: + lst_unique_vals = set([x[anchor] for x in data if anchor in x]) + res_fin_anchor = [] + for item in lst_unique_vals: + add_vals = [] + res1 = {} + for i2 in list(filter(lambda d: d[anchor] == item, [x for x in data if anchor in x])): + add_vals_dict = deepcopy(i2) + for k, v in i2.items(): + if k in list_of_keys or k == anchor: + if k not in res1: + res1[k] = v + else: + if ( + isinstance(res1[k], str) + or isinstance(res1[k], int) + or isinstance(res1[k], float) + or isinstance(res1[k], datetime.datetime) + ): + if v != res1[k]: + res1[k] = [res1[k], v] + elif v not in res1[k]: + res1[k].append(v) + del add_vals_dict[k] + if add_vals_dict: + if add_vals_dict not in add_vals: + add_vals.append(add_vals_dict) + if len(add_vals) > 0: + if not "_additional_values" in res1: + res1["_additional_values"] = add_vals + else: + res1["_additional_values"].extend(add_vals) + res_fin_anchor.append(res1) + return self.harm_filter_sparql(res_fin_anchor) + else: + res_fin = [] + for i1 in data: + for k, v in i1.items(): + if k in list_of_keys: + if isinstance(v, list): + for count, it in enumerate(v): + if len(res_fin) - 1 < count: + res_fin.append({k: it}) + else: + res_fin[count][k] = it + else: + if len(res_fin) > 0: + res_fin[0][k] = v + else: + res_fin.append({k: v}) + return self.harm_filter_sparql(res_fin) + # return self.harm_filter_sparql(data) + + def get_anchor_element_from_field(self, field: ModelField) -> typing.Tuple[str, ModelField] | None: + """takes a field class and returns a tuple of the anchor element and the field class + + Args: + field (ModelField): the field class + + Returns: + typing.Tuple[str, ModelField] | None: tuple of name and field class of the anchor element, None if no anchor element + """ + if not getattr(field.type_, "__fields__", False): + return None + for f_name, f_class in field.type_.__fields__.items(): + f_conf = f_class.field_info.extra.get("rdfconfig", object()) + if getattr(f_conf, "anchor", False): + if getattr(f_conf, "path", False): + f_name = getattr(f_conf, "path") + return f_name, f_class + return None + + def get_anchor_element_from_model(self, model: BaseModel) -> typing.Tuple[str, ModelField] | None: + """takes a model class and returns a tuple of the anchor element and the field class""" + if hasattr(model, "__fields__"): + for f_name, f_class in model.__fields__.items(): + f_conf = f_class.field_info.extra.get("rdfconfig", object()) + if getattr(f_conf, "anchor", False): + if getattr(f_conf, "path", False): + f_name = getattr(f_conf, "path") + return f_name, f_class + return None + + def get_rdf_variables_from_field(self, field: ModelField) -> typing.List[str]: + res = [] + for f_name, f_class in field.type_.__fields__.items(): + f_conf = f_class.field_info.extra.get("rdfconfig", object()) + if hasattr(f_conf, "path"): + res.append(f_conf.path) + else: + res.append(f_name) + return res + + def get_rdf_variables_from_model(self, model: BaseModel) -> typing.List[str]: + if not hasattr(model, "__fields__"): + return [] + res = [] + for f_name, f_class in model.__fields__.items(): + f_conf = f_class.field_info.extra.get("rdfconfig", object()) + if hasattr(f_conf, "path"): + res.append(f_conf.path) + else: + res.append(f_name) + return res + + def map_fields_data(self, data: dict) -> dict: + """Unses the field information to map the RDF values to the correct fields + + Args: + data (dict): input RDF data + + Returns: + dict: resulting data using the correct maps + """ + res = {} + for field in self.__fields__.values(): + path = getattr(field.field_info.extra.get("rdfconfig"), "path", None) + if path is None: + path = field.name + if path not in data: + # res[field.name] = data + anchor = self.get_anchor_element_from_model(model=field.type_) + if "_additional_values" in data: + if anchor is None and path in data["_additional_values"]: + res[field.name] = data["_additional_values"][path] + res[field.name] = self.filter_sparql( + data=data["_additional_values"] if "_additional_values" in data else data, + # data=data, + anchor=anchor[0] if anchor is not None else None, + list_of_keys=self.get_rdf_variables_from_model(model=field.type_), + ) + continue + if ( + hasattr(field.type_, "__fields__") + or hasattr(field.type_, "__args__") + and not getattr(field.field_info.extra.get("rdfconfig"), "bypass_data_mapping", False) + ): # FIXME: this test doesnt catch all the options + scallback_attr = getattr(field.field_info.extra.get("rdfconfig"), "serialization_class_callback", None) + default_dict_key = getattr(field.field_info.extra.get("rdfconfig"), "default_dict_key", None) + if scallback_attr is not None: + res[field.name] = [] + if not isinstance(data[path], list): + data[path] = [data[path]] + # for ent in data[path]: + cb1 = scallback_attr(field, data[path]) + for cb in cb1: + anchor = self.get_anchor_element_from_model(model=cb[0]) + rdf_data = self.filter_sparql( + data=cb[1], + anchor=anchor[0], + list_of_keys=self.get_rdf_variables_from_model(model=cb[0]), + ) + res[field.name].extend([cb[0](**ent) for ent in rdf_data]) + if field.outer_type_.__origin__ != list: + res[field.name] = res[field.name][0] + elif ( + (isinstance(data[path], list) or isinstance(data[path], str)) + and default_dict_key is not None + and isinstance(field.sub_fields, list) + ): + if isinstance(data[path], str): + d1 = [data[path]] + else: + d1 = data[path] + res[field.name] = [{default_dict_key: ent} for ent in d1] + elif (isinstance(data[path], list) or isinstance(data[path], str)) and default_dict_key is not None: + if isinstance(data[path], list): + d1 = data[path][0] + else: + d1 = data[path] + res[field.name] = {default_dict_key: d1} + elif isinstance(data[path], list): + anchor = self.get_anchor_element_from_model(model=field.type_) + res[field.name] = self.filter_sparql( + # data=data["_additional_values"] if "_additional_values" in data else data[path], + data=data[path], + anchor=anchor[0] if anchor is not None else None, + list_of_keys=self.get_rdf_variables_from_model(model=field.type_), + ) + elif field.sub_fields is None: + anchor = self.get_anchor_element_from_model(model=field.type_) + res[field.name] = self.filter_sparql( + # data=data["_additional_values"] if "_additional_values" in data else data, + data=data, + anchor=anchor[0] if anchor is not None else None, + list_of_keys=self.get_rdf_variables_from_model(model=field.type_), + )[0] + else: + anchor = self.get_anchor_element_from_model(model=field.type_) + res[field.name] = self.filter_sparql( + # data=data["_additional_values"] if "_additional_values" in data else data, + data=data, + anchor=anchor[0] if anchor is not None else None, + list_of_keys=self.get_rdf_variables_from_model(model=field.type_), + ) + else: + default_value = getattr(field.field_info.extra.get("rdfconfig"), "default_value", None) + res[field.name] = data.get(path, default_value) + return res + + def post_process_data(self, data: dict) -> dict: + + for field in self.__fields__.values(): + cb = getattr(field.field_info.extra.get("rdfconfig"), "callback_function", None) + if cb is not None and field.name in data: + if data[field.name] is not None: + data[field.name] = cb(field, data[field.name], data) + return data + + def encode_data(self, data: dict) -> dict: + + for field in self.__fields__.values(): + cb = getattr(field.field_info.extra.get("rdfconfig"), "encode_function", None) + if cb is not None and field.name in data: + if data[field.name] is not None: + data[field.name] = cb(data[field.name]) + return data + + def __init__(__pydantic_self__, **data: Any) -> None: + if "_results" in data: + data = data["_results"] + anchor = __pydantic_self__.get_anchor_element_from_model(model=__pydantic_self__) + data = __pydantic_self__.filter_sparql( + data, + anchor=anchor[0], + list_of_keys=__pydantic_self__.get_rdf_variables_from_model(model=__pydantic_self__), + )[0] + sort_key = getattr(__pydantic_self__.Config, "sort_key", None) + if sort_key is not None: + original_order = [] + for item in data[sort_key["object list"]]: + if item[sort_key["original key"]] not in original_order: + original_order.append(item[sort_key["original key"]]) + data = __pydantic_self__.map_fields_data(data=data) + data = __pydantic_self__.post_process_data(data=data) + data = __pydantic_self__.encode_data(data=data) + if sort_key is not None: + new_data = [] + for order_item in original_order: + for item in data[sort_key["object list"]]: + if item[sort_key["original key"]] == order_item: + new_data.append(item) + data[sort_key["object list"]] = new_data + + # if __pydantic_self__.__class__.__name__ == "Entity": + # if "gender" in data: + # if isinstance(data["gender"], list): + # data["gender"] = data["gender"][0] + # if "label" in data: + # data["label"] = data["label"][0] + if __pydantic_self__.Config.RDF_utils_catch_errors: + try: + super().__init__(**data) + except ValidationError as e: + print("Error in model", __pydantic_self__.__class__.__name__, e) + data["_error"] = e + data_copy = deepcopy(data) + for err in e.errors(): + c_back = list(err["loc"]) + while len(c_back) > 0: + d1 = data_copy + for val in c_back[:-1]: + if val in d1 or (isinstance(d1, list) and val < len(d1)): + d1 = d1[val] + else: + break + try: + error_key = __pydantic_self__.Config.RDF_utils_error_field_name + prev_error = None + if isinstance(d1[c_back[-1]], dict) and error_key in d1[c_back[-1]]: + prev_error = d1[c_back[-1]][error_key] + d1[c_back[-1]] = None + if data_copy[error_key] is None: + data_copy[error_key] = [] + if str(e).replace("\n ", "; ").replace("\n", "; ") not in data_copy[error_key]: + data_copy[error_key].append(str(e).replace("\n ", "; ").replace("\n", "; ")) + if prev_error is not None: + for err2 in prev_error: + if err2 not in data_copy[error_key]: + data_copy[error_key].append(err2) + break + except (KeyError, IndexError): + if len(c_back) == 1: + break + del c_back[-1] + data_copy = removeNullNoneEmpty(data_copy) + super().__init__(**data_copy) + else: + super().__init__(**data) diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/tests/__init__.py b/rdf-fastapi-utils/rdf_fastapi_utils/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/tests/baseclass_test.py b/rdf-fastapi-utils/rdf_fastapi_utils/tests/baseclass_test.py new file mode 100644 index 0000000..6776294 --- /dev/null +++ b/rdf-fastapi-utils/rdf_fastapi_utils/tests/baseclass_test.py @@ -0,0 +1,126 @@ +from typing import Union +import unittest +import json +from pydantic import Field, ValidationError +from rdf_fastapi_utils.models import FieldConfigurationRDF, RDFUtilsModelBaseClass + + +def results_callback(field, data): + return [(TCPersonFull, data)] + + +def results_callback_with_error(field, data): + return [(TCPersonFullWithError, data)] + + +class TCPaginatedResponse(RDFUtilsModelBaseClass): + count: int = Field(..., rdfconfig=FieldConfigurationRDF(path="count")) + results: list[Union["TCPersonFull", "TCPlaceFull"]] = Field( + ..., + rdfconfig=FieldConfigurationRDF(path="results", serialization_class_callback=results_callback), + ) + + +class TCPersonFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="person")) + name: str = Field(..., rdfconfig=FieldConfigurationRDF(path="entityLabel")) + events: list["TCEventFull"] = None + + +class TCPlaceFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="person")) + name: str = Field(..., rdfconfig=FieldConfigurationRDF(path="entityLabel")) + events: list["TCEventFull"] = None + + +class TCEventFull(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="event")) + label: str = Field(..., rdfconfig=FieldConfigurationRDF(path="eventLabel")) + + +class TCPaginatedResponseWithError(RDFUtilsModelBaseClass): + count: int = Field(..., rdfconfig=FieldConfigurationRDF(path="count")) + results: list[Union["TCPersonFullWithError", "TCPlaceFull"]] = Field( + ..., + rdfconfig=FieldConfigurationRDF(path="results", serialization_class_callback=results_callback_with_error), + ) + + +class TCPersonFullWithError(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="person")) + name: str = Field(..., rdfconfig=FieldConfigurationRDF(path="entityLabel")) + events: list["TCEventFullWithError"] = None + + class Config: + RDF_utils_catch_errors = True + RDF_utils_error_field_name = "error" + RDF_utils_move_errors_to_top = True + + +class TCEventFullWithError(RDFUtilsModelBaseClass): + id: str = Field(..., rdfconfig=FieldConfigurationRDF(anchor=True, path="event")) + label: str = Field(..., rdfconfig=FieldConfigurationRDF(path="eventLabel")) + + class Config: + RDF_utils_catch_errors = True + RDF_utils_error_field_name = "error" + RDF_utils_move_errors_to_top = True + + +TCPaginatedResponse.update_forward_refs() +TCPlaceFull.update_forward_refs() +TCPersonFull.update_forward_refs() +TCEventFull.update_forward_refs() +TCPersonFullWithError.update_forward_refs() +TCPaginatedResponseWithError.update_forward_refs() + + +class TestInTaViaBaseClass(unittest.TestCase): + def setUp(self) -> None: + with open("rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data.json") as f: + self.test_data = json.load(f) + with open("rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data_events.json") as f: + self.test_data_events = json.load(f) + return super().setUp() + + def test_filter_sparql(self): + res = RDFUtilsModelBaseClass().filter_sparql(self.test_data["results"], anchor="person") + self.assertEqual(len(res), 50) + + def test_filter_sparql_no_values_selected(self): + res = RDFUtilsModelBaseClass().filter_sparql(self.test_data["results"], anchor="person", list_of_keys=None) + for ent in res: + self.assertTrue("_additional_values" in ent) + + def test_filter_sparql_values_selected(self): + res = RDFUtilsModelBaseClass().filter_sparql( + self.test_data["results"], anchor="person", list_of_keys=["person", "entityLabel"] + ) + for ent in res: + self.assertTrue("_additional_values" in ent) + + def test_complex_example(self): + res = RDFUtilsModelBaseClass().filter_sparql( + self.test_data_events["results"], anchor="person", list_of_keys=["person", "entityLabel"] + ) + res2 = RDFUtilsModelBaseClass().filter_sparql( + res[0]["_additional_values"], anchor="event", list_of_keys=["event", "eventLabel", "start"] + ) + self.assertEqual(len(res2), 14) + + def test_model_field(self): + res = TCPaginatedResponse(**self.test_data_events) + self.assertEqual(len(res.results[0].events), 14) + + def test_validation_errors(self): + """test if validation errors are raised correctly when a required field is missing""" + testdata = self.test_data_events.copy() + event = testdata["results"][0]["event"] + idx = 0 + while testdata["results"][idx]["event"] == event: + del testdata["results"][idx]["eventLabel"] + idx += 1 + with self.assertRaises(ValidationError): + TCPaginatedResponse(**testdata) + x = TCPaginatedResponseWithError(**testdata) + print("test") diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data.json b/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data.json new file mode 100644 index 0000000..261f5d0 --- /dev/null +++ b/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data.json @@ -0,0 +1,499 @@ +{ + "page": 1, + "count": 1163, + "pages": 24, + "results": [ + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-10530280_03", + "entityTypeLabel": "person", + "entityLabel": "Ziegler , Frans (1893-1939) " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-10530280_03", + "entityTypeLabel": "person", + "entityLabel": "Ziegler , Franciscus Xaverius Wilhelmus Josephus (1893-1939) " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-10530280_03", + "entityTypeLabel": "person", + "entityLabel": "Ziegler , Franz (1893-1939) " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-10777578_02", + "entityTypeLabel": "person", + "entityLabel": "Feigl, Franz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_01", + "entityTypeLabel": "person", + "entityLabel": "Kaspar Karl Ferdinand Anton Franz de Weichs de Wenne" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_02", + "entityTypeLabel": "person", + "entityLabel": "Kaspar Karl Ferdinand Anton Franz de Weichs de Wenne" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_02", + "entityTypeLabel": "person", + "entityLabel": "Van Weichs van Winnen van Geysteren" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_04", + "entityTypeLabel": "person", + "entityLabel": "Kaspar Karl Ferdinand Anton Franz baron de Weichs de Wenne " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_05", + "entityTypeLabel": "person", + "entityLabel": "Van Weichs van Winnen van Geysteren" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-1298486_05", + "entityTypeLabel": "person", + "entityLabel": "Kaspar Karl Ferdinand Anton Franz baron de Weichs de Wenne " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-13616870_02", + "entityTypeLabel": "person", + "entityLabel": "Moormans , Franciscus Leonardus Johannes " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-13616870_02", + "entityTypeLabel": "person", + "entityLabel": "Moormans , Frans " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-13616870_02", + "entityTypeLabel": "person", + "entityLabel": "Moormans , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-16657664_02", + "entityTypeLabel": "person", + "entityLabel": "Brandt Buys, Gerrit Bernhard Franz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-20135608_02", + "entityTypeLabel": "person", + "entityLabel": "Eisner, Dr.Ing. Franz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-21492410_02", + "entityTypeLabel": "person", + "entityLabel": "Godin , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-21492410_02", + "entityTypeLabel": "person", + "entityLabel": "Codino , Francesco " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-22846496_02", + "entityTypeLabel": "person", + "entityLabel": "Melchers , Frans " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-22846496_02", + "entityTypeLabel": "person", + "entityLabel": "Melchers , Franz Marie Joseph Adolphe " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-22846496_02", + "entityTypeLabel": "person", + "entityLabel": "Melchers , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-22846496_02", + "entityTypeLabel": "person", + "entityLabel": "Melchers , Frantz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-23367507_02", + "entityTypeLabel": "person", + "entityLabel": "Deutmann, Franz Wilhelm" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-2518340_02", + "entityTypeLabel": "person", + "entityLabel": "Douwe , F. " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-2518340_02", + "entityTypeLabel": "person", + "entityLabel": "Douwe , Franciscus Carolus von " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-2518340_02", + "entityTypeLabel": "person", + "entityLabel": "Douwe , Franz Karl von " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-2518340_02", + "entityTypeLabel": "person", + "entityLabel": "Douwe , Franz Carl von " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-2518340_02", + "entityTypeLabel": "person", + "entityLabel": "Douwe , Franciscus Carolus van " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-25531806_02", + "entityTypeLabel": "person", + "entityLabel": "Braun , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-25531806_02", + "entityTypeLabel": "person", + "entityLabel": "Braun , Franz Joseph Heinrich " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27316740_01", + "entityTypeLabel": "person", + "entityLabel": "Steinmetz, Philipp Christian Jacob Franz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27316740_02", + "entityTypeLabel": "person", + "entityLabel": "Steinmetz , Philipp Christian Jacob Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27804918_02", + "entityTypeLabel": "person", + "entityLabel": "Timmer , Johannes Franz Coenraad " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27804918_02", + "entityTypeLabel": "person", + "entityLabel": "Timmer , Hans (1912-1997) " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27992942_02", + "entityTypeLabel": "person", + "entityLabel": "Backer, Franz Jozef de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27992942_03", + "entityTypeLabel": "person", + "entityLabel": "Backer, Franz Jozef de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27992942_04", + "entityTypeLabel": "person", + "entityLabel": "Backer, Franz Jozef de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27992942_05", + "entityTypeLabel": "person", + "entityLabel": "Backer, Franz Jozef de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-27992942_06", + "entityTypeLabel": "person", + "entityLabel": "Backer, Franz Jozef de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-29812239_01", + "entityTypeLabel": "person", + "entityLabel": "Franz Wilhelm Koenigs " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-29812239_04", + "entityTypeLabel": "person", + "entityLabel": "Koenigs , Franz Wilhelm " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-29812239_05", + "entityTypeLabel": "person", + "entityLabel": "Koenigs, Franz W." + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-29812239_06", + "entityTypeLabel": "person", + "entityLabel": "Franz Koenigs" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-29812239_07", + "entityTypeLabel": "person", + "entityLabel": "Koenigs, Franz Wilhelm " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-30028005_02", + "entityTypeLabel": "person", + "entityLabel": "Backer , Johann Franz de " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-30028005_02", + "entityTypeLabel": "person", + "entityLabel": "Backer , Johann Frans de " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-34784579_01", + "entityTypeLabel": "person", + "entityLabel": "Johann Michael Franz Birnbaum " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-34784579_03", + "entityTypeLabel": "person", + "entityLabel": "Birnbaum , Johann Michael Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-35260075_01", + "entityTypeLabel": "person", + "entityLabel": "Meerburg, George Franz Gezelle" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-35260075_04", + "entityTypeLabel": "person", + "entityLabel": "Meerburg , George Franz Gezelle " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-3747009_02", + "entityTypeLabel": "person", + "entityLabel": "Hentschel , Karl " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-3747009_02", + "entityTypeLabel": "person", + "entityLabel": "Hentschel , Karl Franz Gustav " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40213971_02", + "entityTypeLabel": "person", + "entityLabel": "Vismans, Franz Joseph Florenz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Asbroeck , Franciscus " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Asbroek , Franciscus " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Assbrugg , Franciscus " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Aspruck , Frantz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Aspruck , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Assbrugg , Frantz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-40729965_02", + "entityTypeLabel": "person", + "entityLabel": "Assbrugg , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-41115446_02", + "entityTypeLabel": "person", + "entityLabel": "Franz Karl Leopold von Klenze " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorff , Franz Peter " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorf , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorf , Frans " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorff , Frans " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorf , Frans Peter " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-44906695_02", + "entityTypeLabel": "person", + "entityLabel": "Hermesdorff , Frans Peter " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-46184703_01", + "entityTypeLabel": "person", + "entityLabel": "R\u00fctz , Franz Georg Christopher " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-46184703_02", + "entityTypeLabel": "person", + "entityLabel": "R\u00fctz , Franz Georg Christopher " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-49382496_01", + "entityTypeLabel": "person", + "entityLabel": "Franz Marius Theodor de Liagre B\u00f6hl " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-49382496_03", + "entityTypeLabel": "person", + "entityLabel": "Liagre B\u00f6hl, Franz Marius Theodor de" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-53500354_02", + "entityTypeLabel": "person", + "entityLabel": "G\u00fcntermann, Franz" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-53730616_02", + "entityTypeLabel": "person", + "entityLabel": "Deutmann , Franz Wilhelm Maria " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-53730616_02", + "entityTypeLabel": "person", + "entityLabel": "Deutmann , Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-55464819_02", + "entityTypeLabel": "person", + "entityLabel": "Gr\u00fctters, Johan Franz Joseph" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_01", + "entityTypeLabel": "person", + "entityLabel": "Wilhelm Franz Lichtenauer " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_05", + "entityTypeLabel": "person", + "entityLabel": "Lichtenauer , Wilhelm Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_06", + "entityTypeLabel": "person", + "entityLabel": " Dr. \n W.F. \n Lichtenauer \n " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_06", + "entityTypeLabel": "person", + "entityLabel": " Dr. \n Franz \n Lichtenauer \n " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_06", + "entityTypeLabel": "person", + "entityLabel": " Dr. \n Wilhelm \n Franz \n Lichtenauer \n " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_07", + "entityTypeLabel": "person", + "entityLabel": "Franz Lichtenauer" + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_08", + "entityTypeLabel": "person", + "entityLabel": "Lichtenauer, Wilhelm Franz " + }, + { + "count": 1163, + "person": "http://data.biographynet.nl/rdf/PersonDes-59053961_09", + "entityTypeLabel": "person", + "entityLabel": "Lichtenauer, Wilhelm Franz " + } + ] +} \ No newline at end of file diff --git a/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data_events.json b/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data_events.json new file mode 100644 index 0000000..4cb0b7f --- /dev/null +++ b/rdf-fastapi-utils/rdf_fastapi_utils/tests/test_data_events.json @@ -0,0 +1,415 @@ +{ + "page": 1, + "count": 1, + "pages": 1, + "results": [ + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/deceased_person/27118", + "event": "http://www.intavia.eu/apis/deathevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "eventLabel": "Death of Nikola Tesla", + "end": "1943-01-07 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/deceased_person/27118", + "event": "http://www.intavia.eu/apis/deathevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "eventLabel": "Death of Nikola Tesla", + "end": "1943-01-07 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155790", + "event": "http://www.intavia.eu/apis/event/personplace/155790", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Smiljan", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155790", + "event": "http://www.intavia.eu/apis/event/personplace/155790", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Smiljan", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155791", + "event": "http://www.intavia.eu/apis/event/personplace/155791", + "evPlace": "http://www.intavia.eu/apis/place/5584", + "evPlaceLatLong": "Point ( +15.37472 +44.54611 )", + "evPlaceLabel": "Gospi\u0107", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Gospi\u0107", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155791", + "event": "http://www.intavia.eu/apis/event/personplace/155791", + "evPlace": "http://www.intavia.eu/apis/place/5584", + "evPlaceLatLong": "Point ( +15.37472 +44.54611 )", + "evPlaceLabel": "Gospi\u0107", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Gospi\u0107", + "start": "1862-01-01 00:00:00+00:00", + "end": "1866-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155792", + "event": "http://www.intavia.eu/apis/event/personplace/155792", + "evPlace": "http://www.intavia.eu/apis/place/7912", + "evPlaceLatLong": "Point ( +15.54778 +45.48722 )", + "evPlaceLabel": "Karlovac", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Karlovac", + "start": "1866-01-01 00:00:00+00:00", + "end": "1874-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155792", + "event": "http://www.intavia.eu/apis/event/personplace/155792", + "evPlace": "http://www.intavia.eu/apis/place/7912", + "evPlaceLatLong": "Point ( +15.54778 +45.48722 )", + "evPlaceLabel": "Karlovac", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Karlovac", + "start": "1866-01-01 00:00:00+00:00", + "end": "1874-12-31 23:59:59+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/born_person/27118", + "event": "http://www.intavia.eu/apis/birthevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "eventLabel": "Birth of Nikola Tesla", + "start": "1856-07-10 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/born_person/27118", + "event": "http://www.intavia.eu/apis/birthevent/27118", + "evPlace": "http://www.intavia.eu/apis/place/129965", + "evPlaceLatLong": "Point ( +15.31806 +44.56389 )", + "evPlaceLabel": "Smiljan", + "eventLabel": "Birth of Nikola Tesla", + "start": "1856-07-10 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155794", + "event": "http://www.intavia.eu/apis/event/personplace/155794", + "evPlace": "http://www.intavia.eu/apis/place/14608", + "evPlaceLatLong": "Point ( +14.42076 +50.08804 )", + "evPlaceLabel": "Prag", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Prag", + "start": "1880-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155794", + "event": "http://www.intavia.eu/apis/event/personplace/155794", + "evPlace": "http://www.intavia.eu/apis/place/14608", + "evPlaceLatLong": "Point ( +14.42076 +50.08804 )", + "evPlaceLabel": "Prag", + "roleLabel": "ausgebildet in", + "eventLabel": "Tesla, Nikola ausgebildet in Prag", + "start": "1880-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155801", + "event": "http://www.intavia.eu/apis/event/personplace/155801", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in New York City", + "start": "1884-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155801", + "event": "http://www.intavia.eu/apis/event/personplace/155801", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in New York City", + "start": "1884-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155821", + "event": "http://www.intavia.eu/apis/event/personplace/155821", + "evPlace": "http://www.intavia.eu/apis/place/155818", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in Colorado Springs", + "start": "1899-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155821", + "event": "http://www.intavia.eu/apis/event/personplace/155821", + "evPlace": "http://www.intavia.eu/apis/place/155818", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in Colorado Springs", + "start": "1899-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155822", + "event": "http://www.intavia.eu/apis/event/personplace/155822", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in New York City", + "start": "1900-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155822", + "event": "http://www.intavia.eu/apis/event/personplace/155822", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "wirkte in", + "eventLabel": "Tesla, Nikola wirkte in New York City", + "start": "1900-01-01 00:00:00+00:00" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155813", + "event": "http://www.intavia.eu/apis/event/personplace/155813", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in New York City" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155813", + "event": "http://www.intavia.eu/apis/event/personplace/155813", + "evPlace": "http://www.intavia.eu/apis/place/25209", + "evPlaceLatLong": "Point ( -74.00597 +40.71427 )", + "evPlaceLabel": "New York City", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in New York City" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155814", + "event": "http://www.intavia.eu/apis/event/personplace/155814", + "evPlace": "http://www.intavia.eu/apis/place/54993", + "evPlaceLatLong": "Point ( -75.16379 +39.95233 )", + "evPlaceLabel": "Philadelphia", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in Philadelphia" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155814", + "event": "http://www.intavia.eu/apis/event/personplace/155814", + "evPlace": "http://www.intavia.eu/apis/place/54993", + "evPlaceLatLong": "Point ( -75.16379 +39.95233 )", + "evPlaceLabel": "Philadelphia", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in Philadelphia" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155815", + "event": "http://www.intavia.eu/apis/event/personplace/155815", + "evPlace": "http://www.intavia.eu/apis/place/71364", + "evPlaceLatLong": "Point ( -90.19789 +38.62727 )", + "evPlaceLabel": "St. Louis", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in St. Louis" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155815", + "event": "http://www.intavia.eu/apis/event/personplace/155815", + "evPlace": "http://www.intavia.eu/apis/place/71364", + "evPlaceLatLong": "Point ( -90.19789 +38.62727 )", + "evPlaceLabel": "St. Louis", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in St. Louis" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155816", + "event": "http://www.intavia.eu/apis/event/personplace/155816", + "evPlace": "http://www.intavia.eu/apis/place/10265", + "evPlaceLatLong": "Point ( -0.12574 +51.50853 )", + "evPlaceLabel": "London", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in London" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155816", + "event": "http://www.intavia.eu/apis/event/personplace/155816", + "evPlace": "http://www.intavia.eu/apis/place/10265", + "evPlaceLatLong": "Point ( -0.12574 +51.50853 )", + "evPlaceLabel": "London", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in London" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis-edits.acdh-dev.oeaw.ac.at/entity/27118/", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155817", + "event": "http://www.intavia.eu/apis/event/personplace/155817", + "evPlace": "http://www.intavia.eu/apis/place/13714", + "evPlaceLatLong": "Point ( +2.3488 +48.85341 )", + "evPlaceLabel": "Paris", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in Paris" + }, + { + "count": 1, + "person": "http://www.intavia.eu/apis/personproxy/27118", + "entityTypeLabel": "person", + "entityLabel": "Tesla, Nikola", + "linkedIds": "https://apis.acdh.oeaw.ac.at/entity/27118", + "role": "http://www.intavia.eu/apis/personplace/eventrole/155817", + "event": "http://www.intavia.eu/apis/event/personplace/155817", + "evPlace": "http://www.intavia.eu/apis/place/13714", + "evPlaceLatLong": "Point ( +2.3488 +48.85341 )", + "evPlaceLabel": "Paris", + "roleLabel": "hielt einen Vortrag in", + "eventLabel": "Tesla, Nikola hielt einen Vortrag in Paris" + } + ] +} \ No newline at end of file diff --git a/rdf_fastapi_utils b/rdf_fastapi_utils new file mode 120000 index 0000000..8d83b62 --- /dev/null +++ b/rdf_fastapi_utils @@ -0,0 +1 @@ +rdf-fastapi-utils/rdf_fastapi_utils/ \ No newline at end of file diff --git a/semantic_kraus_api/main.py b/semantic_kraus_api/main.py new file mode 100644 index 0000000..8b1d2ec --- /dev/null +++ b/semantic_kraus_api/main.py @@ -0,0 +1,28 @@ +import os +import aioredis +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI +from fastapi_versioning import VersionedFastAPI + +from .main_v1 import router as router_v1 + + +app = FastAPI( + docs_url="/", + title="Semantic Kraus API backend", + description="Development version of the Semantic Kraus backend.", + version="0.1.0", +) + +app.include_router(router_v1) +origins = ["*"] + +app = VersionedFastAPI(app, version_format="{major}", prefix_format="/v{major}") + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) diff --git a/semantic_kraus_api/main_v1.py b/semantic_kraus_api/main_v1.py new file mode 100644 index 0000000..c717dfc --- /dev/null +++ b/semantic_kraus_api/main_v1.py @@ -0,0 +1,29 @@ +import math +from fastapi import APIRouter, Depends, HTTPException +from fastapi_versioning import versioned_api_route + +from .query_parameters import Search +from .models_v1 import PaginatedResponseSubjects +from .utils import flatten_rdf_data, get_query_from_triplestore, toggle_urls_encoding + + +router = APIRouter(route_class=versioned_api_route(1, 0)) + + +@router.get( + "/api/entities/search", + response_model=PaginatedResponseSubjects, + response_model_exclude_none=True, + tags=["Entities endpoints"], + description="Endpoint that allows to query and retrieve entities including \ + the node history. Depending on the objects found the return object is \ + different.", +) +async def query_entities(search: Search = Depends()): + res = get_query_from_triplestore(search, "entities_search_v1.sparql") + res = flatten_rdf_data(res) + # pages = math.ceil(len(res) / search.limit) if len(res) > 0 else 0 + # count = len(res) if len(res) > 0 else 0 + pages = math.ceil(int(res[0]["count"]) / search.limit) if len(res) > 0 else 0 + count = int(res[0]["count"]) if len(res) > 0 else 0 + return {"page": search.page, "count": count, "pages": pages, "results": res} diff --git a/semantic_kraus_api/models_v1.py b/semantic_kraus_api/models_v1.py new file mode 100644 index 0000000..9db64f6 --- /dev/null +++ b/semantic_kraus_api/models_v1.py @@ -0,0 +1,55 @@ +from tkinter import SE +import typing + +from pydantic import Field, HttpUrl, NonNegativeInt +from rdf_fastapi_utils.models import FieldConfigurationRDF, RDFUtilsModelBaseClass +from fastapi_versioning import version, versioned_api_route +from fastapi import APIRouter, Depends, HTTPException + + +class SemanticKrausBackendBaseModel(RDFUtilsModelBaseClass): + errors: typing.List[str] | None = None + + class Config: + RDF_utils_catch_errors = True + RDF_utils_error_field_name = "errors" + RDF_utils_move_errors_to_top = True + + +class InternationalizedLabel(SemanticKrausBackendBaseModel): + """Used to provide internationalized labels""" + + default: str + en: typing.Optional[str] + de: str | None = None + fi: str | None = None + si: str | None = None + du: str | None = None + + def __init__(__pydantic_self__, **data: typing.Any) -> None: + super().__init__(**data) + + +class Entity(SemanticKrausBackendBaseModel): + id: str = Field( + ..., + rdfconfig=FieldConfigurationRDF(path="subject", anchor=True), + ) + label: InternationalizedLabel | None = Field( + None, rdfconfig=FieldConfigurationRDF(path="label", default_dict_key="default") + ) + entity_class: HttpUrl = Field(..., rdfconfig=FieldConfigurationRDF(path="type")) + entity_class_label: str = Field(..., rdfconfig=FieldConfigurationRDF(path="typeLabel")) + + +class PaginatedResponseBase(SemanticKrausBackendBaseModel): + count: NonNegativeInt = 0 + page: NonNegativeInt = 0 + pages: NonNegativeInt = 0 + + +class PaginatedResponseSubjects(PaginatedResponseBase): + results: typing.List[Entity] = Field([], rdfconfig=FieldConfigurationRDF(path="results")) + + class Config(PaginatedResponseBase.Config): + sort_key = {"original key": "subject", "object list": "results"} diff --git a/semantic_kraus_api/query_parameters.py b/semantic_kraus_api/query_parameters.py new file mode 100644 index 0000000..63d72f7 --- /dev/null +++ b/semantic_kraus_api/query_parameters.py @@ -0,0 +1,40 @@ +import dataclasses + +from fastapi import Query +from pydantic import PositiveInt +from enum import Enum +import typing + + +class EntityTypes(str, Enum): + person = "crm:E21_Person" + place = "crm:E53_Place" + expression = "frbroo:F22_Self-Contained_Expression" + publication = "frbroo:F24_Publication_Expression" + information_object = "crm:E73_Information_Object" + + +@dataclasses.dataclass(kw_only=True) +class QueryBase: + page: PositiveInt = Query(default=1, gte=1) + limit: int = Query(default=50, le=1000, gte=1) + _offset: int = Query(default=0, include_in_schema=False) + + def __post_init__(self): + if hasattr(self, "page"): + self._offset = (self.page - 1) * self.limit + + +@dataclasses.dataclass(kw_only=True) +class Search(QueryBase): + q: str = Query(default=None, max_length=200, description="Searches across labels of all subjects") + subject_types: typing.List[EntityTypes] = Query( + default=[ + EntityTypes.person, + EntityTypes.place, + EntityTypes.expression, + EntityTypes.publication, + EntityTypes.information_object, + ], + description="Filter by entity type. Can be multiple", + ) diff --git a/semantic_kraus_api/sparql/entities_search_v1.sparql b/semantic_kraus_api/sparql/entities_search_v1.sparql new file mode 100644 index 0000000..b54c563 --- /dev/null +++ b/semantic_kraus_api/sparql/entities_search_v1.sparql @@ -0,0 +1,35 @@ +PREFIX xsd: +PREFIX skos: +PREFIX rdfs: +PREFIX crm: +PREFIX bds: +PREFIX rdf: +PREFIX frbroo: + + +SELECT ?subject ?type ?typeLabel ?count ?label + +WITH { + SELECT (COUNT(?subject) as ?count) + WHERE { + {% include 'search_entities_sub_v1.sparql' %} + } +} as %count_set + +WITH { + SELECT ?subject ?type ?score + WHERE { + {% include 'search_entities_sub_v1.sparql' %} + } + {% if q %}ORDER BY DESC(?score) ?subject {% else %} ORDER BY ?subject {% endif %} +LIMIT {{limit}} +{% if _offset > 0 %}OFFSET {{_offset}}{% endif %} +} AS %query_set + +WHERE { + INCLUDE %count_set + INCLUDE %query_set + ?subject rdfs:label|skos:prefLabel ?label . + OPTIONAL {?type rdfs:label ?typeLabel . + FILTER(LANG(?typeLabel) = "en") +}} \ No newline at end of file diff --git a/semantic_kraus_api/sparql/search_entities_sub_v1.sparql b/semantic_kraus_api/sparql/search_entities_sub_v1.sparql new file mode 100644 index 0000000..c18c49f --- /dev/null +++ b/semantic_kraus_api/sparql/search_entities_sub_v1.sparql @@ -0,0 +1,13 @@ +{ ?subject (rdfs:label|skos:prefLabel) ?label. } + UNION + { + ?subject crm:P1_is_identified_by ?appellation. + ?appellation rdf:type crm:E33_E41_Linguistic_Appellation; + rdfs:label ?label. + } + ?subject rdf:type ?type. + FILTER(?type IN({% for type in subject_types %}{{type.value}}{% if not loop.last %}, {% endif %}{% endfor %})) + ?label bds:search "{{q}}*"^^xsd:string; + bds:minRelevance "0.5"; + bds:relevance ?score; + bds:matchAllTerms "true". \ No newline at end of file diff --git a/semantic_kraus_api/utils.py b/semantic_kraus_api/utils.py new file mode 100644 index 0000000..592cb8a --- /dev/null +++ b/semantic_kraus_api/utils.py @@ -0,0 +1,112 @@ +import base64 +from dataclasses import asdict +import datetime +import os +from urllib.parse import quote, unquote +from SPARQLWrapper import SPARQLWrapper, JSON +from jinja2 import Environment, FileSystemLoader + +from semantic_kraus_api.query_parameters import QueryBase, Search +from .conversion import convert_sparql_result +from SPARQLTransformer import pre_process + + +jinja_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "sparql")), autoescape=False) +sparql_endpoint = os.environ.get("SPARQL_ENDPOINT") +sparql = SPARQLWrapper(sparql_endpoint) +sparql.setMethod("POST") +sparql.setReturnFormat(JSON) +if not sparql_endpoint.startswith("http://127.0.0.1:8080"): + sparql.setHTTPAuth("BASIC") + sparql.setCredentials(user=os.environ.get("SPARQL_USER"), passwd=os.environ.get("SPARQL_PASSWORD")) + + +def get_query_from_triplestore(search: Search | QueryBase , sparql_template: str): + """creates the query from the template and the search parameters and returns the json + from the triplestore. This is v2 and doesnt need the proto config anymore + + Args: + search (Search): _description_ + sparql_template (str): _description_ + + Returns: + _type_: _description_ + """ + if isinstance(search, Search): + search = asdict(search) + query_template = jinja_env.get_template(sparql_template).render(**search) + sparql.setQuery(query_template) + res = sparql.queryAndConvert() + return res["results"]["bindings"] + + +def flatten_rdf_data(data: dict) -> list: + """Flatten the RDF data to a list of dicts. + + Args: + data (dict): The RDF data + + Returns: + list: A list of dicts + """ + flattened_data = [] + for ent in data: + d_res = {} + for k, v in ent.items(): + if isinstance(v, dict): + if "value" in v: + if "datatype" in v: + if v["datatype"] == "http://www.w3.org/2001/XMLSchema#dateTime": + try: + v["value"] = datetime.datetime.fromisoformat(str(v["value"]).replace("Z", "+00:00")) + except ValueError: + continue # FIXME: this is removing dates before 0, should be fixed + elif v["datatype"] == "http://www.w3.org/2001/XMLSchema#integer": + v["value"] = int(v["value"]) + elif v["datatype"] == "http://www.w3.org/2001/XMLSchema#boolean": + v["value"] = bool(v["value"]) + elif v["datatype"] == "http://www.w3.org/2001/XMLSchema#float": + v["value"] = float(v["value"]) + d_res[k] = v["value"] + else: + d_res[k] = v + else: + d_res[k] = v + flattened_data.append(d_res) + return flattened_data + + +def toggle_urls_encoding(url): + """Toggles the encoding of the url. + + Args: + url (str): The url + + Returns: + str: The encoded/decoded url + """ + if "/" in url: + return base64.urlsafe_b64encode(url.encode("utf-8")).decode("utf-8") + else: + return base64.urlsafe_b64decode(url.encode("utf-8")).decode("utf-8") + + +def calculate_date_range(start, end, intv): + diff = (end - start) / intv + for i in range(intv): + yield (start + diff * i) + yield end + + +def create_bins_from_range(start, end, intv): + bins = list(calculate_date_range(start, end, intv)) + bins_fin = [] + for i in range(0, intv): + bins_fin.append( + { + "values": (bins[i], bins[i + 1]), + "label": f"{bins[i].strftime('%Y')} - {bins[i+1].strftime('%Y')}", + "count": 0, + } + ) + return bins_fin diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..e51fccd --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +if [[ -z "${DEVELOP}" ]]; then + echo "starting gunicorn" + gunicorn semantic_kraus_api.main:app -w 1 --timeout=200 --threads=4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000 +fi