From 979ebc97431d3d4029b71bbf2c27963a093678de Mon Sep 17 00:00:00 2001 From: Anton Dubovik Date: Tue, 3 Sep 2024 15:01:03 +0000 Subject: [PATCH] feat: initial commit --- .dockerignore | 28 + .env.example | 2 + .flake8 | 15 + .github/CODEOWNERS | 2 + .github/ISSUE_TEMPLATE/config.yml | 1 + .github/dependabot.yml | 22 + .github/workflows/pr-title-check.yml | 14 + .github/workflows/pr.yml | 13 + .github/workflows/release.yml | 13 + .github/workflows/slash-command-dispatch.yml | 26 + .gitignore | 16 + .gitlab-ci.yml | 198 ++ .ort.yml | 24 + .vscode/extensions.json | 7 + .vscode/settings.json | 15 + CONTRIBUTING.md | 5 + Dockerfile | 54 + LICENSE | 201 ++ Makefile | 54 + README.md | 221 ++ SECURITY.md | 11 + aidial_interceptors_sdk/assets/watermark.png | Bin 0 -> 69341 bytes .../chat_completion/__init__.py | 3 + .../chat_completion/adapter.py | 106 + .../chat_completion/annotated_chunk.py | 8 + .../chat_completion/base.py | 50 + .../chat_completion/element_path.py | 46 + .../chat_completion/helpers.py | 155 + .../chat_completion/index_mapper.py | 37 + .../chat_completion/request_handler.py | 46 + .../request_message_handler.py | 108 + .../chat_completion/response_handler.py | 145 + aidial_interceptors_sdk/dial_client.py | 51 + .../embeddings/__init__.py | 2 + aidial_interceptors_sdk/embeddings/adapter.py | 37 + aidial_interceptors_sdk/embeddings/base.py | 48 + aidial_interceptors_sdk/utils/debug.py | 32 + aidial_interceptors_sdk/utils/dial_sdk.py | 31 + aidial_interceptors_sdk/utils/dict.py | 173 + aidial_interceptors_sdk/utils/env.py | 22 + aidial_interceptors_sdk/utils/exceptions.py | 78 + aidial_interceptors_sdk/utils/http_client.py | 20 + aidial_interceptors_sdk/utils/not_given.py | 22 + aidial_interceptors_sdk/utils/reflection.py | 43 + aidial_interceptors_sdk/utils/storage.py | 165 + aidial_interceptors_sdk/utils/streaming.py | 142 + examples/app.py | 28 + .../interceptor/chat_completion/__init__.py | 20 + .../chat_completion/blacklisted_words.py | 42 + examples/interceptor/chat_completion/cache.py | 67 + .../chat_completion/image_watermark.py | 52 + .../pii_anonymiser/__init__.py | 3 + .../chat_completion/pii_anonymiser/impl.py | 130 + .../pii_anonymiser/spacy_anonymizer.py | 111 + .../interceptor/chat_completion/pirate.py | 18 + .../chat_completion/reject_external_links.py | 28 + .../interceptor/chat_completion/replicator.py | 183 ++ .../chat_completion/statistics_reporter.py | 174 + examples/interceptor/embeddings/__init__.py | 9 + .../embeddings/blacklisted_words.py | 19 + .../embeddings/normalize_vector.py | 24 + .../interceptor/embeddings/project_vector.py | 31 + examples/interceptor/registry.py | 50 + examples/utils/embedding_encoding.py | 15 + examples/utils/log_config.py | 40 + examples/utils/lru_cache.py | 25 + examples/utils/markdown.py | 66 + examples/utils/path.py | 5 + examples/utils/watermark.py | 55 + helm/development.yaml | 28 + helm/review.yaml | 44 + noxfile.py | 45 + poetry.lock | 2886 +++++++++++++++++ poetry.toml | 2 + pyproject.toml | 102 + scripts/clean.py | 32 + scripts/codegen.py | 43 + scripts/docker_entrypoint.sh | 5 + scripts/gen_watermark.py | 102 + tests/conftest.py | 18 + tests/test_dict.py | 244 ++ tests/test_embedding_encoding.py | 39 + tests/test_markdown.py | 50 + tests/test_response_handler.py | 164 + tests/test_spacy_anonymizer.py | 59 + 85 files changed, 7570 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .flake8 create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pr-title-check.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/slash-command-dispatch.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .ort.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 aidial_interceptors_sdk/assets/watermark.png create mode 100644 aidial_interceptors_sdk/chat_completion/__init__.py create mode 100644 aidial_interceptors_sdk/chat_completion/adapter.py create mode 100644 aidial_interceptors_sdk/chat_completion/annotated_chunk.py create mode 100644 aidial_interceptors_sdk/chat_completion/base.py create mode 100644 aidial_interceptors_sdk/chat_completion/element_path.py create mode 100644 aidial_interceptors_sdk/chat_completion/helpers.py create mode 100644 aidial_interceptors_sdk/chat_completion/index_mapper.py create mode 100644 aidial_interceptors_sdk/chat_completion/request_handler.py create mode 100644 aidial_interceptors_sdk/chat_completion/request_message_handler.py create mode 100644 aidial_interceptors_sdk/chat_completion/response_handler.py create mode 100644 aidial_interceptors_sdk/dial_client.py create mode 100644 aidial_interceptors_sdk/embeddings/__init__.py create mode 100644 aidial_interceptors_sdk/embeddings/adapter.py create mode 100644 aidial_interceptors_sdk/embeddings/base.py create mode 100644 aidial_interceptors_sdk/utils/debug.py create mode 100644 aidial_interceptors_sdk/utils/dial_sdk.py create mode 100644 aidial_interceptors_sdk/utils/dict.py create mode 100644 aidial_interceptors_sdk/utils/env.py create mode 100644 aidial_interceptors_sdk/utils/exceptions.py create mode 100644 aidial_interceptors_sdk/utils/http_client.py create mode 100644 aidial_interceptors_sdk/utils/not_given.py create mode 100644 aidial_interceptors_sdk/utils/reflection.py create mode 100644 aidial_interceptors_sdk/utils/storage.py create mode 100644 aidial_interceptors_sdk/utils/streaming.py create mode 100644 examples/app.py create mode 100644 examples/interceptor/chat_completion/__init__.py create mode 100644 examples/interceptor/chat_completion/blacklisted_words.py create mode 100644 examples/interceptor/chat_completion/cache.py create mode 100644 examples/interceptor/chat_completion/image_watermark.py create mode 100644 examples/interceptor/chat_completion/pii_anonymiser/__init__.py create mode 100644 examples/interceptor/chat_completion/pii_anonymiser/impl.py create mode 100644 examples/interceptor/chat_completion/pii_anonymiser/spacy_anonymizer.py create mode 100644 examples/interceptor/chat_completion/pirate.py create mode 100644 examples/interceptor/chat_completion/reject_external_links.py create mode 100644 examples/interceptor/chat_completion/replicator.py create mode 100644 examples/interceptor/chat_completion/statistics_reporter.py create mode 100644 examples/interceptor/embeddings/__init__.py create mode 100644 examples/interceptor/embeddings/blacklisted_words.py create mode 100644 examples/interceptor/embeddings/normalize_vector.py create mode 100644 examples/interceptor/embeddings/project_vector.py create mode 100644 examples/interceptor/registry.py create mode 100644 examples/utils/embedding_encoding.py create mode 100644 examples/utils/log_config.py create mode 100644 examples/utils/lru_cache.py create mode 100644 examples/utils/markdown.py create mode 100644 examples/utils/path.py create mode 100644 examples/utils/watermark.py create mode 100644 helm/development.yaml create mode 100644 helm/review.yaml create mode 100644 noxfile.py create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml create mode 100644 scripts/clean.py create mode 100644 scripts/codegen.py create mode 100644 scripts/docker_entrypoint.sh create mode 100644 scripts/gen_watermark.py create mode 100644 tests/conftest.py create mode 100644 tests/test_dict.py create mode 100644 tests/test_embedding_encoding.py create mode 100644 tests/test_markdown.py create mode 100644 tests/test_response_handler.py create mode 100644 tests/test_spacy_anonymizer.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2dce444 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/.nox +LICENSE +.history diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..256a76e --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +LOG_LEVEL=DEBUG +DIAL_URL=DIAL_URL \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b43b56e --- /dev/null +++ b/.flake8 @@ -0,0 +1,15 @@ +[flake8] +# E501 string literal is too long +# W503 line break before binary operator +# E203 whitespace before ':' (triggered on list slices like xs[i : i + 5]) +# E704 multiple statements on one line (def) +ignore = E501, W503, E203, E704 +exclude = + .git, + .tmp, + .venv, + .conda, + .nox, + .pytest_cache + __pycache__, + __init__.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7a8f93f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @adubovik +/.github/ @nepalevov @alexey-ban \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..85fa281 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + time: "09:00" + # Disable version updates, keep security updates only + open-pull-requests-limit: 0 + commit-message: + # Prefix all commit messages with "chore: " + prefix: "chore" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + time: "09:00" + commit-message: + # Prefix all commit messages with "chore: " + prefix: "chore" diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000..7a621e9 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,14 @@ +name: "Validate PR title" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + pr-title-check: + uses: epam/ai-dial-ci/.github/workflows/pr-title-check.yml@1.9.0 + secrets: + ACTIONS_BOT_TOKEN: ${{ secrets.ACTIONS_BOT_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..a7a5197 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,13 @@ +name: PR Workflow + +on: + pull_request: + branches: [development, release-*] + +jobs: + run_tests: + uses: epam/ai-dial-ci/.github/workflows/python_package_pr.yml@1.9.0 + secrets: inherit + with: + python_version: 3.11 + test_python_versions: '["3.11", "3.12"]' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..39e9f48 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,13 @@ +name: Release Workflow + +on: + push: + branches: [development, release-*] + +jobs: + release: + uses: epam/ai-dial-ci/.github/workflows/python_package_release.yml@1.9.0 + secrets: inherit + with: + python_version: 3.11 + test_python_versions: '["3.11", "3.12"]' diff --git a/.github/workflows/slash-command-dispatch.yml b/.github/workflows/slash-command-dispatch.yml new file mode 100644 index 0000000..4cb86d9 --- /dev/null +++ b/.github/workflows/slash-command-dispatch.yml @@ -0,0 +1,26 @@ +name: Slash Command Dispatch +on: + issue_comment: + types: [created] +jobs: + slashCommandDispatch: + runs-on: ubuntu-latest + steps: + - name: Slash Command Dispatch + id: scd + uses: peter-evans/slash-command-dispatch@13bc09769d122a64f75aa5037256f6f2d78be8c4 # v4.0.0 + with: + token: ${{ secrets.ACTIONS_BOT_TOKEN }} + reaction-token: ${{ secrets.ACTIONS_BOT_TOKEN }} + config: > + [ + { + "command": "deploy-review", + "permission": "write", + "issue_type": "pull-request", + "repository": "epam/ai-dial-ci", + "static_args": [ + "application=${{ github.event.repository.name }}" + ] + } + ] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..128470d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +venv +.venv +.env +.history +.idea +__pycache__ +.nox +dist +.vscode/launch.json +.pytest_cache +core-data +core-logs +config.json +~* +response_message_handler.py \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2461ba1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,198 @@ +include: + - template: Jobs/Container-Scanning.gitlab-ci.yml + - project: Gitlab/ci + ref: 0.2.27 + file: helm/helm-environment.gitlab-ci.yml + +variables: + DOCKER_PATH: "ai/dial/application/dial-interceptor-example" + DOCKER_REGISTRY_SERVER: "registry-dev.deltixhub.com" + HELM_REPO: https://charts.epam-rail.com + PRIVATE_HELM_REPO: "false" + CHART: "dial-extension" # Helm chart name, e.g. "gitlab-runner" + VERSION: "1.0.1" # Helm chart version + NAMESPACE: "dial-development" + # HELM_EXTRA_ARGS: "-f ${HELM_SECRET_FILE}" + RELEASE: ${CI_PROJECT_NAME} + + development_ENV_ID: "staging" + development_HELM_VALUES_FILE: "helm/development.yaml" + development_ENV_URL: "https://dial-interceptor-example.staging.deltixhub.io" + development_AWS_ACCESS_KEY_ID: ${staging_AWS_ACCESS_KEY_ID} + development_AWS_SECRET_ACCESS_KEY: ${staging_AWS_SECRET_ACCESS_KEY} + + staging_DEPLOY_ENV: "false" + + review_DEPLOY_ENV: "true" + review_ENV_ID: "staging" + review_HELM_VALUES_FILE: "helm/review.yaml" + review_ENV_URL: "https://${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}.staging.deltixhub.io" + review_HELM_CUSTOM_VALUES: "ingress.hosts[0]=${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID}.staging.deltixhub.io,fullnameOverride=mr-${CI_MERGE_REQUEST_IID},image.tag=mr-${CI_MERGE_REQUEST_IID}" + review_RELEASE: ${CI_PROJECT_NAME}-mr-${CI_MERGE_REQUEST_IID} + review_SECRET_NAME: "epm-rtc-registry-test" + review_DOCKER_REGISTRY_SERVER: "registry-test.deltixhub.com" + # review_HELM_EXTRA_ARGS: "-f ${HELM_SECRET_FILE_review}" + +stages: + - tag + - lint + - test + - publish # publish docker images + - deploy + - promote # copy docker image to public repo + - production # Stage name used in include. Not used. ToDo - delete this + +lint: + image: python:3.11-slim + stage: lint + before_script: + - apt-get update && apt-get install -y make + script: + - pip install poetry==1.6.1 + - make lint + rules: + - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development" + - if: $CI_COMMIT_BRANCH == "development" + tags: + - kubernetes + +test: + image: python:3.11-slim + stage: test + before_script: + - apt-get update && apt-get install -y make + script: + - pip install poetry==1.6.1 + - make test + rules: + - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development" + - if: $CI_COMMIT_BRANCH == "development" + tags: + - kubernetes + +deploy_development: + image: "registry.deltixhub.com/deltix.docker/devops/kubernetes-tools:0.17.1" + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == "development" + +destroy_development: + image: "registry.deltixhub.com/deltix.docker/devops/kubernetes-tools:0.17.1" + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == "development" + when: manual + allow_failure: true + +deploy_staging: + stage: deploy + +destroy_staging: + stage: deploy + +deploy_review: + image: "registry.deltixhub.com/deltix.docker/devops/kubernetes-tools:0.17.1" + stage: deploy + rules: + - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development" && $review_DEPLOY_ENV == "true"' + +destroy_review: + image: "registry.deltixhub.com/deltix.docker/devops/kubernetes-tools:0.17.1" + stage: deploy + rules: + - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development" && $review_DEPLOY_ENV == "true"' + when: manual + allow_failure: true + +publish: + image: dockerhub.deltixhub.com/docker:23.0.6 + stage: publish + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" + cache: {} + services: + - docker:23.0.6-dind + before_script: + - mkdir -p $HOME/.docker + # Use DOCKER_AUTH_CONFIG for login to deltix repo + - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json + script: + - imageUrl=$REPOSITORY/${DOCKER_PATH} + - docker build -t $imageUrl:${DOCKER_TAG} -t $imageUrl:${DOCKER_ADDITIONAL_TAG} . + - docker push $imageUrl:${DOCKER_TAG} + - docker push $imageUrl:${DOCKER_ADDITIONAL_TAG} + rules: + - if: $CI_COMMIT_TAG + variables: + DOCKER_TAG: ${CI_COMMIT_TAG} + DOCKER_ADDITIONAL_TAG: latest + REPOSITORY: ${ARTIFACTORY_DOCKER_DEV_REPOSITORY} + - if: $CI_COMMIT_BRANCH == "development" + variables: + DOCKER_TAG: ${CI_COMMIT_REF_SLUG} + DOCKER_ADDITIONAL_TAG: alpha + REPOSITORY: ${ARTIFACTORY_DOCKER_DEV_REPOSITORY} + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development"' + variables: + DOCKER_TAG: gitlab-mr${CI_MERGE_REQUEST_ID} + DOCKER_ADDITIONAL_TAG: mr-${CI_MERGE_REQUEST_IID} + REPOSITORY: ${ARTIFACTORY_DOCKER_TEST_REPOSITORY} + tags: + - AWS + - DockerExecutor + +"Tag branch": + image: alpine/git:v2.32.0 + stage: tag + script: + - ver=$(echo -n $CI_COMMIT_REF_NAME | cut -f 2 -d '-') || ver=0.0 + - patch_tag=$(git describe --abbrev=0 --tags | cut -f 3 -d '.') || patch_tag=-1 + - release_tag=${ver}.$(( patch_tag+1 )) + - git remote set-url origin https://$GITLAB_ROBOT_NAME:$GITLAB_ROBOT_PUSH_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git + - git config user.name $GITLAB_ROBOT_NAME + - git config user.email ${GITLAB_ROBOT_EMAIL} + - git diff --quiet && git diff --staged --quiet || git commit -am "RobotBuild ${release_tag}" + - git tag ${release_tag} + - git push origin HEAD:${CI_COMMIT_REF_NAME} --tags + dependencies: [] + rules: + - if: "$CI_COMMIT_REF_SLUG =~ /^release-/ && $CI_COMMIT_MESSAGE !~ /^RobotBuild/" + tags: + - kubernetes + +container_scanning: + stage: promote + variables: + GIT_STRATEGY: none + CS_IMAGE: ${ARTIFACTORY_DOCKER_DEV_REPOSITORY}/${DOCKER_PATH}:$CI_COMMIT_TAG + CS_REGISTRY_USER: ${ARTIFACTORY_USER} + CS_REGISTRY_PASSWORD: ${ARTIFACTORY_PASS} + dependencies: [] + rules: + - if: $CI_COMMIT_TAG + tags: + - AWS + - DockerExecutor + +### Copy frontend and backend docker images to public repository. Run on git tag manually +promote: + image: dockerhub.deltixhub.com/curlimages/curl:7.88.1 + stage: promote + variables: + GIT_STRATEGY: none + script: + - 'curl --retry 6 --retry-all-errors --no-progress-meter --fail -X POST "${ARTIFACTORY_URL}/api/docker/epm-rtc-docker/v2/promote" -H "Content-Type: application/json" -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASS} -d ''{ "targetRepo": "epm-rtc-public-docker", "dockerRepository": "''"${DOCKER_PATH}"''", "tag" : "''"$CI_COMMIT_TAG"''", "copy": true }''' + - > + if [ ! -z "$DOCKER_ADDITIONAL_TAG" ]; then + curl --retry 6 --retry-all-errors --no-progress-meter --fail -X POST "${ARTIFACTORY_URL}/api/docker/epm-rtc-docker/v2/promote" -H "Content-Type: application/json" -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASS} -d '{ "targetRepo": "epm-rtc-public-docker", "dockerRepository": '\"${DOCKER_PATH}\"', "tag" : '\"$DOCKER_ADDITIONAL_TAG\"', "copy": true }'; + fi + dependencies: [] + rules: + - if: $CI_COMMIT_TAG + when: manual + variables: + DOCKER_ADDITIONAL_TAG: latest + tags: + - AWS + - DockerExecutor diff --git a/.ort.yml b/.ort.yml new file mode 100644 index 0000000..3167771 --- /dev/null +++ b/.ort.yml @@ -0,0 +1,24 @@ +--- +excludes: + scopes: + - pattern: "dev" + reason: "DEV_DEPENDENCY_OF" + comment: "Packages for development only." + - pattern: "lint" + reason: "DEV_DEPENDENCY_OF" + comment: "Packages for static code analysis only." + - pattern: "test" + reason: "TEST_DEPENDENCY_OF" + comment: "Packages for testing only." +license_choices: + repository_license_choices: + - given: FTL OR GPL-2.0-or-later + choice: FTL +resolutions: + rule_violations: + - message: ".*PyPI::httpcore:0\\.18\\.0.*" + reason: "CANT_FIX_EXCEPTION" + comment: "BSD 3-Clause New or Revised License: https://github.com/encode/httpcore/blob/0.18.0/LICENSE.md" + - message: ".*PyPI::httpx:0\\.25\\.0.*" + reason: "CANT_FIX_EXCEPTION" + comment: "BSD 3-Clause New or Revised License: https://github.com/encode/httpx/blob/0.25.0/LICENSE.md" \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a1f8dbe --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.black-formatter", + "ms-python.isort" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ab64be2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "files.trimTrailingWhitespace": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.tabSize": 4 + }, + "python.testing.pytestArgs": ["."], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.analysis.typeCheckingMode": "basic" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..349e1fd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# How to contribute + +As an open-source project in a rapidly developing field, we are open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation. + +For detailed information on how to contribute, see the full [contributing documentation](https://github.com/epam/ai-dial/blob/main/CONTRIBUTING.md). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..29901f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# Stage 1: Builder +FROM python:3.11-slim-buster as builder + +# Update and install necessary build dependencies +RUN apt-get update && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends \ + build-essential \ + python3-dev \ + && pip install --upgrade pip \ + && pip install poetry \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install dependencies using Poetry +COPY pyproject.toml poetry.lock poetry.toml ./ +RUN poetry install --no-interaction --no-ansi --no-cache --no-root --with=main,examples + +# Copy the rest of the application +COPY . . +RUN poetry install --no-interaction --no-ansi --no-cache --with=main,examples +RUN poetry run codegen + +# Stage 2: Final image +FROM python:3.11-slim-buster as server + +# Update and upgrade system packages, including specific security fixes +RUN apt-get update && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends \ + build-essential \ + python3-dev \ + && pip install --upgrade pip \ + && pip install poetry \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the application code and installed dependencies from the builder stage +RUN useradd -m -u 1001 appuser +COPY --chown=appuser --from=builder /app . + +# Add and make the entrypoint script executable +COPY ./scripts/docker_entrypoint.sh /docker_entrypoint.sh +RUN chmod +x /docker_entrypoint.sh + +# Expose port 5000 and set user +EXPOSE 5000 + +USER appuser +ENTRYPOINT ["/docker_entrypoint.sh"] + +CMD uvicorn examples.app:app --host 0.0.0.0 --port 5000 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81911d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 EPAM Systems, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eecbfce --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +PORT ?= 5001 +IMAGE_NAME ?= ai-dial-interceptors-sdk +PLATFORM ?= linux/amd64 +DEV_PYTHON ?= 3.11 +ARGS= + +.PHONY: all install build clean lint format test examples_serve examples_docker_serve + +all: build + +install: + poetry env use python$(DEV_PYTHON) + poetry install --all-extras + poetry run codegen + +build: install + poetry build + +clean: + poetry run clean + poetry env remove --all + +publish: build + poetry publish -u __token__ -p ${PYPI_TOKEN} --skip-existing + +lint: install + poetry run nox -s lint + +format: install + poetry run nox -s format + +test: install + poetry run nox -s test -- $(ARGS) + +examples_serve: install + poetry run uvicorn "examples.app:app" --reload --host "0.0.0.0" --port $(PORT) --workers=1 --env-file ./.env + +examples_docker_serve: + docker build --platform $(PLATFORM) -t $(IMAGE_NAME):dev . + docker run --platform $(PLATFORM) --env-file ./.env --rm -p $(PORT):5000 $(IMAGE_NAME):dev + +help: + @echo '====================' + @echo 'build - build the source and wheels archives' + @echo 'clean - clean virtual env and build artifacts' + @echo 'publish - publish the library to PyPi' + @echo '-- LINTING --' + @echo 'format - run code formatters' + @echo 'lint - run linters' + @echo '-- RUNNING EXAMPLES --' + @echo 'examples_serve - run the dev server with examples locally' + @echo 'examples_docker_serve - run the dev server with examples from the docker' + @echo '-- TESTS --' + @echo 'test - run unit tests' diff --git a/README.md b/README.md new file mode 100644 index 0000000..2790163 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# AI DIAL Interceptors Python SDK + +## Overview + +The framework provides useful classes for creating DIAL Interceptors in Python for chat completion and embeddings models. + +An interceptor could be thought of as a middleware that + +1. modifies an incoming DIAL request received from the client *(or it may leave it as is)* +2. calls upstream DIAL application *(**the upstream** for short)* with the modified request +3. modifies the response from the upstream *(or it may leave it as is)* +4. returns the modified response to the client + +The upstream is encapsulated behind a special deployment id `interceptor`. This deployment id is resolved by the DIAL Core into an appropriate deployment id. + +Interceptors could be classified into the following categories: + +1. **Pre-interceptors** that only modify the incoming request from the client *(e.g. rejecting requests following certain criteria)* +2. **Post-interceptors** that only modify the response received from the upstream *(e.g. censoring the response)* +3. **Generic interceptors** that modify both the incoming request and the response from the upstream *(e.g. caching the responses)* + +To create chat completion interceptor one needs to implement instance of the class `ChatCompletionInterceptor` and for embeddings interceptor - `EmbeddingsInterceptor`. See examples for more details. + +## Developer environment + +This project uses [Python>=3.11](https://www.python.org/downloads/) and [Poetry>=1.6.1](https://python-poetry.org/) as a dependency manager. + +Check out Poetry's [documentation on how to install it](https://python-poetry.org/docs/#installation) on your system before proceeding. + +To install requirements: + +```sh +poetry install +``` + +This will install all requirements for running the package, linting, formatting and tests. + +### IDE configuration + +The recommended IDE is [VSCode](https://code.visualstudio.com/). +Open the project in VSCode and install the recommended extensions. + +The VSCode is configured to use PEP-8 compatible formatter [Black](https://black.readthedocs.io/en/stable/index.html). + +Alternatively you can use [PyCharm](https://www.jetbrains.com/pycharm/). + +Set-up the Black formatter for PyCharm [manually](https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea) or +install PyCharm>=2023.2 with [built-in Black support](https://blog.jetbrains.com/pycharm/2023/07/2023-2/#black). + +## Run + +Run the development server: + +```sh +make serve +``` + +### Make on Windows + +As of now, Windows distributions do not include the make tool. To run make commands, the tool can be installed using +the following command (since [Windows 10](https://learn.microsoft.com/en-us/windows/package-manager/winget/)): + +```sh +winget install GnuWin32.Make +``` + +For convenience, the tool folder can be added to the PATH environment variable as `C:\Program Files (x86)\GnuWin32\bin`. +The command definitions inside Makefile should be cross-platform to keep the development environment setup simple. + +## Environment Variables + +Copy `.env.example` to `.env` and customize it for your environment: + +|Variable|Default|Description| +|---|---|---| +|LOG_LEVEL|INFO|Log level. Use DEBUG for dev purposes and INFO in prod| +|WEB_CONCURRENCY|1|Number of workers for the server| +|DIAL_URL||The URL of the DIAL Core server| + +## Lint + +Run the linting before committing: + +```sh +make lint +``` + +To auto-fix formatting issues run: + +```sh +make format +``` + +## Test + +Run unit tests locally: + +```sh +make test +``` + +## Clean + +To remove the virtual environment and build artifacts: + +```sh +make clean +``` + +## Examples + +The repository also provides examples of various DIAL Interceptors all packed into a DIAL service. + +Keep in mind, that the following example interceptors aren't ready for a production use. +They are provided solely as examples to demonstrate basic use cases of interceptors. Use at your discretion. + +### Chat completion interceptors + +|Interceptor name|Category|Description| +|---|---|---| +|reply-as-pirate|Pre|Injects systems prompt `Reply as a pirate` to the request| +|reject-external-links|Pre|Rejects any URL in DIAL attachments which do not point to DIAL Core storage| +|reject-blacklisted-words|Generic|Rejects the request if it contains any blacklisted words| +|image-watermark|Post|Stamps "EPAM DIAL" watermark on all image attachments in the response. Demonstrates how to work with files stored on DIAL File Storage.| +|statistics-reporter|Post|Collects statistics on the response stream *(tokens/sec, finish reason, completion tokens etc)* and reports it in a new stage when response is finished| +|pii-anonymizer|Generic|Anonymizes any PII in the request, calls the upstream, deanonymizes the response| +|replicator:N|Generic|Calls the upstream N times and combines the N response into a single response. Could be useful for stabilization of model's output, since certain models aren't deterministic.| +|cache|Generic|Caches incoming chat completion requests. **Not ready for production use. Use at your discretion**| +|no-op|Generic|No-op interceptor - does not modify the request or the response, simply proxies the upstream| + +### Embeddings interceptors + +|Interceptor name|Category|Description| +|---|---|---| +|reject-blacklisted-words|Pre|Rejects the request if it contains any blacklisted words| +|normalize-vector|Post|Normalizes the vector in the response| +|project-vector:N|Post|Changes the dimensionality of the vectors in the response to N, where N is an integer path parameter.| +|no-op|Generic|No-op interceptor - does not modify the request or the response, simply proxies the upstream| + +### Environment variables + +Copy `.env.example` to `.env` and customize it for your environment: + +|Variable|Default|Description| +|---|---|---| +|PII_ANONYMIZER_LABELS_TO_REDACT|PERSON,ORG,GPE,PRODUCT|Comma-separated list of spaCy entity types to redact. Find the full list of entities [here](https://github.com/explosion/spacy-models/blob/e46017f5c8241096c1b30fae080f0e0709c8038c/meta/en_core_web_sm-3.7.0.json#L121-L140).| + +### Running interceptor as a DIAL service + +To run dev application locally run: + +```sh +make examples_serve +``` + +To run from Docker container: + +```sh +make examples_docker_serve +``` + +Either of the commands will start the server on `http://localhost:5000` exposing endpoints for each of the interceptors like these: + +- `http://localhost:5000/openai/deployments/pii-anonymizer/chat/completions` +- `http://localhost:5000/openai/deployments/normalize-vector/embeddings/completions` + +### DIAL Core configuration + +The interceptor endpoints are defined in the `interceptors` section of the DIAL Core configuration like this: + +```json +{ + "interceptors": { + "chat-reply-as-pirate": { + "endpoint": "${INTERCEPTOR_SERVICE_URL}/openai/deployments/reply-as-pirate/chat/completions" + }, + "chat-statistics-reporter": { + "endpoint": "${INTERCEPTOR_SERVICE_URL}/openai/deployments/statistics-reporter/chat/completions" + } + } +} +``` + +where `INTERCEPTOR_SERVICE_URL` is the URL of the interceptor service. + +They could be then attached to particular models and applications: + +```json +{ + "models": { + "anthropic.claude-v3-haiku": { + "type": "chat", + "iconUrl": "anthropic.svg", + "endpoint": "${BEDROCK_ADAPTER_SERVICE_URL}/openai/deployments/anthropic.claude-3-haiku-20240307-v1:0/chat/completions", + "interceptors": [ + "chat-statistics-reporter", + "chat-reply-as-pirate" + ] + } + } +} +``` + +Make sure that + +1. chat completion interceptors are only used in chat models or application, +2. embeddings interceptors are only used in embeddings models. + +The stack of interceptors in DIAL works similarly to a stack of middlewares in Express.js or Django: + +```txt +Client -> (original request) -> + Interceptor 1 -> (modified request #1) -> + Interceptor 2 -> (modified request #2) -> + Upstream -> (original response) -> + Interceptor 2 -> (modified response #1) -> + Interceptor 1 -> (modified response #2) -> +Client +``` + +**Every** request/response in the diagram above goes through the DIAL Core. This is hidden from the diagram for brevity. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e7100e6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Reporting Security Issues + +We take all security reports seriously. We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions. + +⚠️ Please do *not* file GitHub issues for security vulnerabilities as they are public! ⚠️ + +To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/epam/ai-dial-interceptors-sdk/security/advisories/new) tab. Tip: In this form, only the title and description are mandatory. + +We will send a response indicating the next steps in handling your report. After the initial reply to your report, we will keep you informed of the progress toward a fix and full announcement and may ask for additional information or guidance. + +When we receive such reports, we will investigate and subsequently address any potential vulnerabilities as quickly as possible. diff --git a/aidial_interceptors_sdk/assets/watermark.png b/aidial_interceptors_sdk/assets/watermark.png new file mode 100644 index 0000000000000000000000000000000000000000..411de642a9649718de67fde34bddb7fb38629a6c GIT binary patch literal 69341 zcmZs@Wmr^w)Hk|G0cjK!q!py3yBjGLkdy|gA*7`QBoz=PBn0X1QfZYEq#Fb*U;yco zK5OphdCz-39IuNn-1p3E_WrN6ezhj*o|Xy;5e*T7AS8EG6}1rrs}A!+fDgY3EOwYh zun^>qqMWYJ+rQ0E9#Tyl$(-F_B9~1txhE@oiPBX?NjLh#c-})i^|gZiqvC^l4x^H> z`gtQEgSy*x74OHU1ItehjGx}-(lcJs6EZiEE0i_ByHEa#`!BBi>v;x`zt+ij9=-l* z9ddB$yZ1m$z`b5_scGOzzk5bR&X8Tu9n~9@d_y&_SX8KS5H8cE56`QJh6th*nUhp+ zXy^BQzLS%OGHIpwbX=Rzi*F}LC7HZ4Q03Ov$a5h#W{JgS;@)C$LLm04i5Y>qdJasbO2mn zbhLmJDGd#c=zHf`Hv|z4II10}vg*P{5UmVJu|M$kI0&+`x+)g_r|fP^;TipP(hJnw zM!U#{p_^MZL&wT8&&%a>8f^84PSsfXtbzeHeD`nDR{9;R^CHM@|L3u>I*YzkA+hdx zwLD7rJN!fCEi~2C)RN0j-oAZHv*7mex5P%1yVYQ(9EbRdoxK%$>D^O$16|!eBh%9k zFSXvllTx?4c%Lri`h~i>y6+|W6~33LsgH~e4L!ZRy^Vi1+uGQS(UFsH<`x!qyX>uu ziG2V5{gxvUdaf#6=II;@CnqOCdcEt<&wg@pa%=nh`-<=Ghfk@Jv~A<(ef)GIYng*8 zm?^}G9lgFDPAB0NS+)H5@#E!5LS{BL6<+^;u1c1c*-!25i(a-gT_C)$_mCJj=f=u#MKZ3i+QW6sDG9#fA-zXNC9 zL>IB^vC26^uGg<$H+P*H32$faHZzC23UFQM zukmbp^8p95mpqIN3=B-o&CPS(y)%1%vlg$2h3fZ4f4ameg4{N3x~irUxU)1ca8)MI zpEl72O>>TihbN36@M#&LS_gZ3We;2Y4;45(hw*CSX@YD>)ZE+NhUS-$5*%?|_xaQ)=BRQ-;CubU{cu*L27IV%DeJU zcSr8>{E1lG!J-oN+57>&mz3W*IH*7ngIc?x*+fqLd}usoEhh(J?u*~yT3@a9N(cz( z{9AZq7Mrcj(9ro*S63HrPK?nc6u<2?)RA!c;0?+s11{=wEQFEz1#QZ0S~krdYdY7S zD^yaeB@*=;t>=O09F><%L-x8)FBIvn+)7kaiGSk#cd{xBo@K>tPR`$Cw8GJY4U}Z) z`L6HP@}Y3Y-La6Cv!hi44__4OuT$2&$*7aQ&ch%=c$F2=yMc|)^%io4yzOl_PBXK4 z=O!08lb5Ker%$$fd8`n`W9}mf!0r$ohi8t}iLI^eOa>e|W{$h;iQ;G+PA><7 z$$EK(Kv`>kOSi60W%&{Ph?WK`<667>*M!#=`_fK}{>a(LB_t+Z4!vG@4!131cJ{;0 zmD_Ri5hQSl!8jL+FA@cZ=)k>E473Mf1$i5R+^V%SG|d^JG3NaZ#u=v%v6>2NFWKD1)r3 zKHg_e@uR)9R*jykNeWt8vEH7ZbrRwMr$>ACIQYbjLO~vTZ*Ic@!9&`zm0nb1S=kKkxHHl{EgdXN_M9uf3*%fs7C2y$J_?M7)$%?SPm9VchW z`dyjRU2l#<8i6xoBO_4+AtfbcWn$1<8zoQn3)`1K#sc z(ZUtYHat*^nBn}>(A~JvlE!fL>OX>w^w?O^PY3f)WfIg{P;}>|H-(TzycDO<@It-h zTfLD@xrzfvlvN);ek@{PX0CmB3iUxu1b47$C+Edr+|$MnAM6m{{_kf^lO`{@FI-H~ zpssXZ>i^o&(GdbOd%5{<-=n6}{h5yLLn0(dE$#?ghb;Tg|-$|RD(6{$6vB+ zNw-aoL~q={JR5r zPy0wf@@adamw__9icZ|)7B-K`mF^Q(jbOndCYccBoS%S;`ok_>k*9DUoO$mefj&vb zPgaYYlC{$y)@Ge9;*?=68=0P&`T6PD1)fKW4Sz;vW{eZl(pFI}F7vTktuT2%)YsE< z->(`=J<&s5+J=qv1u`UG_l}_xLahaa`#(b+wL5`_*wk?WH*qw?d z#CtwKgn_e{;I>J_A2lJF>?48M+uE$BNB!pbWb_Y75OR3wA9x_c64(tZ@?%YJNJ7Ke zjWu?^T^+H>pNZmrStR|SWXyGM<-wc3fBzy#(Eh%cF%pDn#^-DA=ar9-kM+N0NF5W( zzAPRX*Nd4>aYi7D#T4G+ZBpOMrB;%SM3|q%6*uBR9jn{@O1z0)X-!l ztAVTN^**?PH4HL=e@{ATT@q-u_3qpuJa3Ppir;JfN}9rt>*My!$NvE4kI!n^d$r}; z0(^W!t3SU)%ykEF+mv{2~Igy{FiwG7L*2{VS9nFW1 zcLL&gukv2In4askJbZHko_Iv&2nMfU`TCD9_lWZI^V9Mf3gbmnSl3h1U^n2i&&=Gp zg29S_zaq6OKf%YnnV6U$GidZG(^6Mp{ySaA*;G=}YcwH|G(BNWUH$O3KHcTZiF^*D zg>&QA@v(Wnzie%N8m4-qn3RNMY}s&*GBT#jGeIpcmOtfNV}1QNUi#U<)3Y6Xr0vJ2 zXZBI&U6(vEiWU|Y4w-L+xApWCOYrhmT258lhKF_0|38L4YskBgF^-^Jd(@UqIpx}sw)T5@D>4Y3E{WK{~O{JMoh#*z^v#>z& zZ>6L{Sx;YoM9o<@to=2suN8jK&AO4RH*VaBt|%%R5+C&P@=C(lE$mqwc=s;YN3aX$ zmzm4PTr*YF+^1)CbwZaeUnZ=i_9MW&f8J{bW7)kzEF`nCa_}kqPx0z%YAAr+_(i&% zeZgNM$TmLH@cf-`mB}`14fGw@npC5)tE`K~Oth|idqEm4TU)ld)za`efja1_Pkw~S z$exR0K;ctymNCE606=(R0*}7g@#a=5GS-xJ32@ehMr9QDgs{siyp<53`-ptnUz=eW zb*LL>w(pF-HiaPZY8cVXE#Z$CCRPGIeFVSCU?Fa~m1Sz4}Nz{R~<>#4Aq^Y5Qe+0nN&5gx10 zp@g|1#?=q{BJgQfuQD*mkkCz18#1;W%=zILq5JTfbH`_ANuUBPh{Aq_P1xz{PlU(C z#j#Bil0;ql2Gfco@wrMuL43T<@7Fig-FCLOuWwm$Uc)Xf(NJd;#I?SF=!AwRSe3^~ zPJX(8R9buqug%YAEr0)BWL-}#Py5qQj!GCz_cfmtH2?{VtE)4zH_LVW{3QO)HtI)f zzSoWDxBe47uvD@>-^%j`NSVot$-%+F5I~q437|mUM3Lr;NHC-K1==S zSC$U1ld9CfXL(d7y=j>KoK!U+YOn3#a=tu6+2EF_$iPR)m1_x2l!z~i;|I)MNF{d!S#`SRsZTvcja+(d49 zO=@dhY7HuyphNj>sE+u%(yy?0zq7C$#diJv1f)!-ZE5F$k8=nS5zz_O*|vigKAglK zYiqNP!(YFCz2+Ph8}YR}MZ4&a6Ce*FMD~vgc_tgVjlX}>hs?~jS1aiSObX#1IEbjY zcnh!Kh@Ifu_I5>W(p;=bcU!e9ley1zOU5=2TF;wt)8^*p`W^J2`!Le^;!lNTjd+~i zN=ZrC$1t|zgN>JMTJk>0qK~stK(FfHCa@9*so)=Zp-_|##hKN#LPA+74P4a=>NhBh zjLPsi0=NI`M|pW2<;BGgjN<9N3&lWhr&m$I4$Y-yWsTt^wENayU*G&t9yv)`EfHB- z%GKD`*0vuS8fq$GEBJqiX-^{O2oN~cileO+KJWGK-zqD4ha2rZ=uvdXsHv*cqtyy$ z7}Xb*&lcg?B0+s=BCCQ#*0ibvxcBehZ}u`3asGZ)c30Xmq|s@nzILj{c7Ec``D~;2 zGYTnR2Z!QDBd5t&{Wm|V9~6EKL7keh%Htqvq4t%C_p3;#?ufWGE6J|@H@8h_R2GeKRgEVl z_ou3@++AGlIV~&IE!qVBlCV6&4PgHn^U?M-1?VC z3c?U%()@+N137hfBs|cA1f_<8+0=FQ<4@9K>M}WOV`5_7f^>2*O#bHxAlaCq#Dh}9nrha& z8Ko0?ASgwHump}8)Z1TZ$4Oc#X1f2 z_3bG}L?MW(n%auZwWtehY;1k-w058|7sIH{)9Dx)AAfWKaXtCB^%Zb0p+LLFRjK8@ zoANm3UN#d~2uU)RqSyLF=0~+lJiNT7xgLJtO+(X~M8?J@S&Zzj&q{P?6OsfqycH7{ zcbEev?T-WrIgaPD=*9bd0I)ZCezGJJ3>ZJIdGmET zV>h>La}>=~_qvAy^Q%Y_C(fJAeLyUFHa(Vf-=;tB_|;j_dX~ByHi5yzw(kO1&z1|` zNj5e%W;LdrJWf+&ezh+ouh7iT-Uyo6r8 z;#Fo_3JMBMAj>Nt^(-wD1sWL|E`NM_erAs#T`w6Jo?mgm&$QK#I6DTI_nXV&Rh7X00dm>ug5cS|-!I#s zIx2^UwH=!-ck~kIkOsAA0pGoL?b^OUpMryfLm|_Xz6_~GX&#<|D`|meCx`d4zfNnL zN575g_@xXaVywGFE6jADBj!9U)q#$XRDiFFbt;hx1*pN7A=xgg%vH?a29IZotbop=sWkr9Q-yKeva z>Ka5p?{|23D4feN8)5Mrt4KJ^MU0}k@5&b2;gCr@5NBPug#{~UI418W!ei=e1g zQV^*ai$teN>~+J34j-M_v-+FcF^=~3pG?H=r`+nWd~D_F>RJP*>{rUJCCD<^ zUTf1g@extam4ugP)VrF4Rn-C_S<9R+)(NIt&(9iJj$8oogz*8&;eC2`;((eSkU5uR zx^=7j7ZeJ;7}bl880J@nhRm;2J<~rj9R`4+^=oBiCD0&(6ey@rL;2-BJl-lRfKSy5 zR5VE2YDKH5>~0IYT-U#XCLM*#0K5;6j`VJWSd!IpaHjF*HG4rrLuy)bvM4r^+0rt1 z=We>{!qgiJbWbb>&^iX{TSJxkvEEKnt;g zo)?M*L{QpBE6{Zp%?Y_BB_-eI=C*fUh2~C96qXLmc3n?T&*)JDKv+}njk&RF&jw%1 zE|Gqooi)MGNlwZr4*d$#^jOj4PHc53F|)wK1wmGVG9KT9jg=K_uB<~KD3X5rrSHTD zN!mVax$qV}@$)O$Bpu^{Hnu*X7PZwP;F0}S%>5D0w$!-CQEO;E2?tJ4OsH}`X*gw? zQCT?71yVZ_Ymj<+iizl|v@y!G8xMEC7YV{CMW9Y}z;LF3S`Fsxy)XNiVO~B*)!Q*VLi< zEqkHLVyCjYdZoU;UJt|HShrjvLc1+|xA734g+zUR8~&+Gkj-C%%$xR5%9g_t2l)_q z+Mr`(G<^%uB!1|RZ$kx0z_ZE9dAE-bg7n5k`J~ZD%`rYm1foK`e!p?#18u0gMu7F|2jv<+pEf za8TZ0V~e2BRdmqBdW1{+fGk{|p^8t_EJxU{`3EOVZ_`mro$tt;>zEoD z6?<+OGLqi{3CUK^kx4;r)Lug+Cy&+#8)YAwzadfkK-Y{_5DB^y&$?joAMXV6M99-G z?YIBU1m>s>zu`Fs=OkyF9tAQW`>|O!)sx~#CEAnf(W@I6YvgFAH?LRy3-s^0>vWu# zh_Db@3yZ9dj=9P|az$k5dPC-{SBKJUY_>jTZ16$Q><_l%#eH|t{78^ZZ_;dSUENI& z+K6_Ec-0%fRwpV}JsivlxIBsRKyoC)#V!ln+23dV9#xgcjbFIM-aH_TE|#&KsdutO ze47NXOZj^EC@F=FzifQ6^N1Zm!0usntZ92ab_*5EPM~RaCO(%}_E0#pv^1J*kjk4p zy#*s3L7Nu(4~bon^4UbZ^J2qGN*on5G&C|aSl!?9S>dlQ*eaOYTd4x)0KMv;%-O#j zy?6b|d@RQ>^FxF0P7PD@|NeCSHzKwuA3<7EU5!l|fzw&|d29MOWE6>J6aM(HU(9DS zbG$>((Q&o>uJqxbq^u{Yv`NCZrf_wrQp??vZ@nQA^f%~S}0=MUr4 zE6_`Gb8|!SadCB_ul`%@alw$lCr_S8t0!@Nr4zIx-$OTvR@U#pZ6Js^hR#n}nzI^!* z#+zq_QQgWSsOL?#-;(b~adj3VGp5|2RmM&ZkBnSEPV%N@LHcp;dM$-Ep3bz!NoE!_ zSzyYXX_QGD1jZ-f^_Sdgq{DKH!LsTDMS4K83-zvY|I0*pAUQ+dVK`A&R}YA5L^W)iRathBxVX3^D(7HX5%jmRkCw({P2=6h*3>Ti{4T-5EYz9?=L{&PLbN;h9))s!WyZVtyPL+g9HJ%ViG1H zEUQ6HRaeK*isZwCmKw1kz(O#VOcAg#GCbrK50A*?Z-ErPc4|rFL{);2j)qp8&<6%+)n^2`2UcH1 zsKa~RN|U%(KVB?`h!MtUJPAmd6n6gpPePa*e5LBFtYBBiu?jPpzUP-Yp96uYf%dh$ zTM7>6FpspfDHqsE7!7yKp+|)Q$vi&bJLt!M0i``5AtB-Tga??C#;rFq0#4Nd(unz^ z6FN+p0-22~_x|VKGCV%6R|P)R{1Zv~qMB)N*M4Fg(B0OjaMN(FnOt!vr=1}nd2j)|^ABV;b*6PknL9d`7&Q58 z1t3A7RInE%JUfv_#a(7*oF?NMasfTyc}sqK{$UV1#$ZwdAi?9g8%juJZT)4UP?tkh zQ*#eNx_f$=qY-}AZ_`kLeZhB`pTqk+mUfqOgBR6H#t*)C;XSMXAu9=j6H;GBA zNxSR%N70fm?-l7*V(|CmE?~*Q7Fj!FaiWNdP|u7-5a^Atb@u6wF+1EbuTBh!(gcs` zeME!`iTLUJ`9PK?!DzhuGAhc-k5-S?0rV5--ntY57aI--f=rKt-D%n&W^!`URAM$H z1pDcq=eXGJ0~a)cRh5OErf&#^g6$O&64HPm^-eRad%L@(pZowIxPmtPY*myY#x$JS zLlaw)p6vOH7f!&R*~LvjJSd_+tGc#GY0dn}a&Wj(|1clAq|NJZ3TkjhDP}NyxvG(f zho{08I^%M|hOr6%6`czFEv@B3bz!i%ERY zy<2~A(L)j0BfY)7&E5Vk?B+&sX5belD)aMT?w&FV*sM{+O5|!oQWCYix@rJ_*atqN zW?xTJK;ISB(C>KLzt4yoR`v(=Hm_rP-G zCW}X-9LC4~0@7cE&j65;|C+^C1{ON1K)LIQB50oeXNOBhhn36U!nAm^bZl9&QhqhH z>8|-3_D_=1ONttSjK=(QRs1DyZAFDi<0ABh)Y$7{*%O5~!1X6ae(X({J}+P4CfkeDT-^jDBu-!QspfQW zGHpE_t*mg)&dx4dZ|72V#nN4jeEIU7*TK3;v%{4J^Oegqv_P}Y#tPVH4y*r8QuCOW zdg|-K3D_Jx>)aKh3^oGdRiKgbI%jqE^Jn>|>Hwk;KR>^#{~x<&H_&XVD#d-yu)$;5 ziuu7GOhaw~;Cm%v`GyeU%NqQC&bK7t%TK%HxBi%iBS=HC1QnQtNfK->Er0=f4@w zKR`PzQkG1oHR^8(IA#SPIF&VWa(c=+=et^_t+T(k(EG*&tXCn+wjiuNO-;?y6tqsR zRL!Hl9t?(wj)~zG?sz*gHAQ&y=FJcoq}-fMV4Tf?w{U&E{Y5zg{bccT>yai1w)EUl zRc#YGFspNTaMTuzV-`&{oQvOfwnJdDr@#wAf)$`&zTe)82{?j}m%Fh(Xu?n(@{RS= z=L8;B-$&hyLi!Rz3;aU`VBz@7$jB^!ect)LzJ7XUYRa?r{rgtYxSv}-9EoZjzgR@$*MyXy@}QX8wF$;DH-@EJj@@{mY0?hf##CLms3; z;EL5NC@wsNG5)J*q5H>&<-V)qWy`uNQTQ{FF@@!esY{>;hz-1xd4B`|#&zKucVmL; z+Q3V|Jm2o1eD>U2UG1Fg?U^KXcA#N_M8VE0D4U+OC!cvYZc5FL08Nv-=vwJl~tjJwLAYPdq2l<^Q?y-<09SIUZ;d;kk8TX@?CHYORgAx`m!Sm}-!stAsG*tB02$W*quVz~ug_)WRPpK-IIl-#5TiwT%oQY3Zmn+q z{KAAFx&$Oh=7$fH?^{{|kred;AA=9BKXn2&D(~bM21P&0FD?D%hmpA2Q^ZZ)e9pEfVx%cGG`}(@O$?vCsn^Hz$;w4|! zyZ=Bu3dUG3gr>e!+w?k5Pft_gvp0iQa}`ETe{V0X>*EaR+W$#_)wR#v>XrMoZ0Gcq(2p~6bFpFmr z6zqpedJGQ4f1VNg!zF3|f1BligdEcQ3i9*A_g5$GR*XlK zXYb|1w2MJ5a@k}JG=00{&_ddbU79_q(gMM8eg%;$4l^^e1r>$>4=C;Z`f`6vGVQB1 zzYF{gAY0~pPAKcCay`x#u|{~OwA{o0Otl2~DeCKAVW72gz?evwXppk9@);i>3k`UY zlCFWjbnAqdJPbKJY`Fqy7b1h`t9j+f|ZT>Afvu&P)Xd@JDUT(KBokn;UAG&S`*>%~Q6 z@cZ|_u3(4i|9x>3G-2ni*#ezfEm(HYQ6woL;ZjIw2S(XR6LCo*3j55T?T;BTSO^RV z%Ey~OJ`Ewrg9i^re>srLzKbJIiLXf+B)@kJ(V(Q!P7;eG*-D-t8_T=A9nkUovPiyG z`gHq(n*}CG70LGnZsUXJgZ|GT!Ug9W}qkeCmG1 z@}et-R>kLUQ1|cuv}DK^;X4S@WI7lJ{Qz7u!XL^G($(<7M?4aKtIqG{ELEDRf<+Uc zlG&k9f3P#xi)LE_B>sRIkl@XRR!;NVp8fFHibqIYRkfR{);DorNdbaG@f|O&%tL1> z&Qge=G$lyvU5I^Je3Pyf<1B>}Nz>>acYhC1!?VT@N+EFCeRuAi+emC;<0wc&Do#v{ zw1Mq1+|=Am_?`feim=O^|H!9LS#$%M_e@OEYXG`Fdw82IfT6d)pLDT5eWusjLQAWE z6Q-GClglKrNwv0)&Tk?FT#P56Wj1##?IZ>V2RF-`Fbyg23kZY(HDNHjf8RpM(_-MK zYbIDwARbi_rU(9L!y0z56t-`QY!kM0cX!tzyT-4$TKWO7H!0uh1alRqemOB;E;$@5 zn=gMHZEWx+O|IT=`vOprBQ7WpGpPqX%O6AYqd<_Yt*u`duiXBC74(Ty0@cib#Zd4J6~_y$VYZRT*-0UG zLfhNhb?a+uCoLYla;QrMe_g1Ds{2>R3-wC-FX1G9A>SLT1>@4~z;QYf1&kd46>l#s zEgfog0x*cl?c?_?IGhx^#GPQ0_$k@6z#S3gppTx+-D1Wzj*RJD_zbif;nUl z-rNI3OH*na04XBIOGK*;4O{}{Y*SI=b93~6*VcXlGPoUhetIwho^=sr0Duo+H4ve` z?p75P6wuIK!$N*QA|s){V={Yd7_XjuCi$tV1yU0nsVh=Rkx#{3m#>KjQ! z!LkTQkr{!3ftS&{;J3f7-Xta?b9tqDV;IgVN0q@zM7g&BKfj`rlam+NHt+nw5Fk@1 zEvl6N?3I;|&F{3#$@E0S5M0l{Qcb&{+xFyRqN*P!Yu^PPv5!tq|B4+q4O&qIj#voq zs-1Q3pRU>F$kX%V`H{1ocjvg+XwundqTb4xAS{AZzXGvK(i>?OeC9Y+W22*XMcyV4 zADZsAQWpVR?WMR#sopt%es-XZ`c{u2f2|;BJ*|Ec2_p;i;%O<6c=ZM`z2-JM`%fIi z!`plH_zxaA)3Q2bri!oz^$AH3Mn*>2s8|myWN>QAo{ExE-sj*o0igBoo13oOmKZ$( z=6xXawTjECiz>el_cunGK`}IVL5AqcKJn z&>YUW<0Mjd$~5gF+|oC1pw?Dy*&I`PQfB)E>U~@4qrrQHl6@ z*&2)Zl>nr^g=7bqg~2dNM`xr#I4Up+PK|$xe;s zPGRijRX?77_fI)v#E3wh<0~Uu+X78Lzs8P@QN(vNa?-tOKJeJ-TM!nGswTc-e4?}` zp6ttyn-Du~2S(!zKKrv}w?3<{ykm&ZH9#FdfM<{>QKd9D2S_9;QPBYSEOv2r9@l=c zy(^5clj+ff)L)ENk$~`Y^th?77x| z_Et7iwIHu1iX)Mz+^kg^JAE7+RFOyDlCQ*;)Db|4f%Daio&YT9sYcEy3^f2;;!Eb@ zsi}u?AHmSobochI81Y_AN=uU{EiAO_#>508!&36-yGO7ZE+GFyV2VJV=8sq~%; zC%$GG$NLxrqy``DB3(%tnbsa?;>Jz&od#kGUQX~Q>@{@yzyUbNyp?JoT$(UyGA{Ns zz5L-v`9q@co56Q1)$iO{wkeVTWI!x^&SNaatqt-gf4>WHZya0*;+UulVSLqzV4jqy zVbDQpeD_wGe*F0HNbz6PwnEdLZtLoxbO~=Qc%;-2$001N&#MS3$1>M~1S{$6*8VE0u&!4a4somAoB&mDw#yI3P0=vl2&~1P@f^r3LoT%%jfEt zbl_aG`ZO*6jNFnUCj_o;LZ7%n`9Ld8xNM?X5Es*Ei*-55+^<*bKj)&$XRHS^q_V1N z$RFY_Mz$?ftQv7K*97w4c*au~K~jSUr;5>nvRl#Sd8N?Y+*~ms73(gMKBN`H9!I8z z^+&lul#C2|Ogwm`%zU%aeKi#!D~LI5)+_e;t10U%DXDtY5N0gZ(cc^A}D-L~L11`gR>FjRf< z(vNFQwZ(hNOH2QpF3Fsyh?}&R+|$u?`VD$V-DhSQw3w zJu>`-Xh<&H_k=X_-Nu09`mZKxDZ|MYP(IrjGouk69AmiEBBNkX3x(aK3X>yf$aOl# zz*tkoViY4YhU%;o)*~!DYm$9cW4OB>ny0ONx);Yp;jVEBKdw9(opL+n(dtJgI>wzD z5S2cNLlU?fI()ACD|T-bg1`xI|9tdJ5N~&n-FOkF_%!A~dWx?N56$;N@JbdIZ;e$= zeSJ8{N1q^x)>AYdye4}QG1jvSm!ID-FHhv5lBj=^K8W~Ax?Y#`ah--mfjNfEu7d2S z6_%U6%3W72ak8)~ZKX4Z=R3)1;N#}!Td@bYUfc7^p;(lo?;*QaqcTUT-rVCRMCrl( zVm9Qxl6B2U_;Sj6Z>aL6`_rF31rJVbxU`-MQ!mHw?s|N#yx-Y2+Wttb+nRrvf@Tst zs8c5{xS_7mQ9n>b$wrr_=H^IY6KIU%8Mm>Ag1LjxbFpCOG{{G$_+$A7vZz=76{N;E z3ZN1^d3@dnvEawh4F@21As|5b>>oD5s1^Q(%kcHa;$r^zUO7|cC||q+w38yfhEwWpidvydOHZdWXL-2v=MV9fC`HdCw9n*g{j{T?+P>F>nr8Go zI9ZT!gw>2!<2E)BoU#KI`w~$Y3C7^WP-_Qu$gwdT<(tyKhI3?^sTpXY4Xm0`zyEAwjdjqXWdtP-n=80 zM)@^BL_fffq(>0UHVfJVy5oZ)-L~Ib7?Q#Yip-_VuDrM9CqVvEaS&dsPU7J=@Af_o zcMiggIYbbQ-8cRq?>|G0D6Qi^L(QraalAv)=FtuTQ!xORryxt_FEp@$PV4u{4galrb zLokJfdKTQeHa9oF17e_ExJVV87n21o;)yw+p}XK8UVQ%)0$G}vO_chZwPCRWf?Ng# zfr?*ri&fhnUR_PrWsS!(aI?Fr{vap$;v#SV;2UqVvw%&1z45Uzv5E2VmgIlTw(uwrKJqW?OnDIN$tjT9+3_Pzg7CNh! zW4ZX|;+8L9E1|fo%=1(J24w10vxA5c$J3?_yG7xA@)u zsD{ZhVocsdZb;`pgCpdTeg!fGdop1Ej$&9HoDN=P9Gg2gWv}VCJ>JP-Sra0MT~d#CHm&xsq~`WOxDle zzem>o{w?+7SwG%reaCOz9p{YnCX=DB`;|)TeEEFI7IlI2`qc;$Co*JnNQL1Jc&n*1 z27c0viE4CMzVc6aO@wI)NOYj*TmuX<6#}^;3Dm<<$i?!IAZ=4qhD)J0;M`#Jd={11 z$l{U`&(5159J*XUAOhFFe{dkuT|0HjRyrTMzTDu@V%1+(#BRPwNKnuf@%`olcyTR9 zmqV^d_cN@TMKIzz6uv=pN}a3nJ7EKg{%+#iw|`ZHc{CP(H%R%H!oXqT_o6TAAgOXv-<{7}Ba9AFBV={cO z!gK)s3(OUHcH$`Zet(T%%vRGY#(V-dH@9#wshd9J!bVfurA(&+J!9kEX5YQyD@Yp@ zP(VXtV;Kl&hG0$0AhPD>KOm9CEQ8t92?`2gLlDR_7YhF2zd2hn0}E7s);2cwDKrqJ z*`!C>;PFitjlXm+|Q&a%eiP41K@)Zx`}IhH~?V?0x`9a>ks7 zaqk?Hk{>ej>4Ns!a}eHHDNIsdx{;|}gxNl8{uUzwM$nqVf0lAGxwQc8BVN6FzWDiG zMHFUr69&na^4&I=Ydf%^SP;Lb@M6O3)vH%ilO^H?;4z2EXOVN6H0aeeIpEkU60$3{*4Fnd$r{Fl zG0E0%F_5IfDg5*4QL@h7CO<`LIsYAh9?UkA?kmhTlh5*3Ww#3_uBMj=ctU(NM6w3O~VDj-#a4;tN)VJQu*cEFq~Xriv! z2c@N@4X|K0GJi*(6rF+_%OE3N3wnDRyF3JTEZglC|Ia3-U>P}OCf(=Dhb2A;puu!lm9b>@d+yuDv8EB5G7cfN7Kw zxmj%H;)EnIa3~wUH+VYY&K|BjgW4TP72>>z1g%`g=Rp7c8w$bAhtXQbU|C3+OeAn6 zP7dy|D#2d>B4kZ%Ny)`%z9%X@XZtfQO5kC9*&{W-{UEm}YvcMO(6YxcQh~vHXA>H? zpQG13vmo1&?Ekoj8m`zQE;T28k{>*=_Ks2ToJ^`~YH+}CGeeg_Zh{DD1H5Sor}QqY z?{yP%+rr_tL^S~6Q8F}45fM)Buh-Yp+lKS0f`wr2&5LsXsG$GZwUP=r8npsUznpKN zx&$O062lK|_*0V7KZ7@TtFESo<>=e5ND^bS=G^d!wld@QYOue$d6;l}X|)|gX?noT z+|01}q7#Re7KK^8`nkMpxC097Vp^d%V%~(Q&wHNaR8l$Os(n*hxhU2ipc?MG+uIHj z5WBGRFrbqB|5?AH7k(~4G0Av0JVaFhlh(u7w=T_ldhD|{tgNiQQvUx2X@|lgpbHC`ew!m2ZI|AsCrXcZr7=H-}08F%}fh371ld33CQr$33mq5G^)bYy* zLbpy&hH2y&nI*qCEi2^PBS;T-G1i584F^MlfD{XX%wP5&j(+HGq(~(MJ<^lH6({Q) z^?jhl2@SccuiNrpG=v4P*yqoC+GwV(JzHOQhVqWko5G0O2>S7*vQ=YsM&TxL%G6x} zO~{<`@$q5Q^_QC&HrCdW0LS}$<{QK6PG9_a@>45rGBYt*G(EiS-NqtV+N#WjF{I#lIAIK_ ztDY$Y(1I&nf6B}6y}NlJVAvOJRJI5buh*|4odSPAsehK1eoxcf3x*Birmare_k;GW zu$}xbGn27-{XCjZKm)0W{gC^JR9>MHqq<|(UL~r*%d?RjJLx{*)xaJIhSf7~ViJ8ns1Z^&9o3D=GjUX)rB6s6JeN5cJ0+5?bN7hHo+W z#?yF%zhm(Hp3wO@8{E5R4%$8?XdQd>54F<8g>1nhBLRo031r$K{-eDj`bcVASbEdB zU&4FGZjtya6>LVu0NVY(TUoFv>!e*W*0ylAhlb6I6EF|%y1KhN78Vz;ED&}>zUULg z;VD+#?=i%g&^ORy2g`1O9HhffZH43U_1dq_63BIAu1;cY=^QSmxy%t05ZnX1>4$3} zp4bq4uLTJCC9tq`K0*{E3CdhFh%0DaIcD?U_erT$=|1{IB@42%2z@pmD}HdzXBie< zNudG?nj}8ee|a0fadNm*0=6EH5R3Sw7myeE4&p)|#(Ki~*E>y2hS{o#I`MkLW9F5` zvK_)C@er1^knNJYtc04ML*gV21C) zIfG5HJn4Ju5|NyR1=CMMTH`ErEe!XAbsw6Olapdt5ca*8*_9)`FV%i`0rEPcVsLsT zkv1r}?uaXhk6eg_g~b!}F0h6;FZaR{ZWPD@tt}ZTz%km!M%o0njWDgb&Ft}D z_Ycr#zrc5W_!iZI)y9=K@+*}+ZipB*T`D=)*`pGI`*ZKaZzD*dvdG~?>zNSTm#V5Q zrG&=}4DX6z0;bijilU4sMc2h>V!~gGhu;AJs{)h81Z0&EEiwWEAIX5DUAAWz$m%te zj^DNI3}&mVLZev2EJwn;XOLrM4te%WVHN6uA9hwz3$Pwnwh_RLPCJ2ZYHPW$$z(=B zog|x{>bscjQP|cZEqOk=)PYUr_VKOz<`20t14j&{#@I~NU^lB3ds^Xi_g6t+%nMr6 z8bl=M&)}eauHnF>N`TIb{-1AWq1AZ_rIUMq%RKbYr}4)X?m*GswtIoLKw+SbiA2CU zRvw?p74hKE5MagcucpVowIC0efMCY*M8&&Bj57kqi(Vza6s(6O9HeQxhf~Nvh|+M^ z#)^=?ecw!ekt)s^J@7wbWJgM#Y_1!WiBJ`0BNP)9mFd8Z-axy?JZz6jUSZ?_QJ`@ z&OpNF(Ml5RU}Sq|XAO>KE!Wp_NO_O4_xCeHoD$7j_1%LgkM7EC<@ z0|(&fHhutT#CS@Q;lGO~k25$Qi}@;?bCDY_1N~37x+#%aX~Z5$wEPeovX3EYXDo$Hf{>NDrQWAxEeiyVA%v^ zdu2C`85;K?LX?6wI0ms}HJ87wZC=x>gIxf>k=St>Z+oM<-` zs#LEPoGV{!L{M0mCh%WB^AF!k7HJ&YfC0aQAZ@fN&&D4GTlj-^5ygv&fFYLv#u+VL z2p5$QYDZf@2v!M(rVsw0`<?z$`+~^`K_t;XEiJ9iivoQ5 z2K9mo68E;NV)3xi9GS;O5!dlwa-}NkQ5+rS8$voQS8j}%x$i$iZ`UQDaoqn-ZeGs| zJec*S;s!MjIXc*=0G)lX0TsB=lUM~?M7dJCwDK^cpPp=~n9Iw{8^E{Icw!vQo8im%c6V2HIncx6HZdje(?>s*OQoNl`P->PD7mNJcMHdrfpsj{ZQr@g!^6;I zJ3!AzxqIo_Jajkb+mK~yKhq+ZG8Fgt`1LiVlvU~^1dP$bU{92B3+&&1aTDi0Q}(Nb zgxraboI&ef9Y86!N1mGOwm$=s`4-I2CXD?98LH3Au6kxI}2 zpRr1=(;J0CRm`73c+mA*p-%rF zzTN|z>%VXR|JW+B8b*|nN)(bxk&sQ1BqB31QbIo6b0 zW_}s16x}a=3h%$>OV@F0wO1JIAm#!hqtG)j9D)ry=H!))H`M2&w^$1USycS_P3g|( zFJ5SGAug8Bp1A(9>Dp5d-#$}Nxx&0M5m%KJUMkGYN~NDZeryfYer5GS!zxi}(-(Ci z_6-rczQBgH5YmF{=d_C6%FTjPD?FAp>kS$%T8*-2jgqV)-L4Vc4snO6m} zI~&4^*ZU%nEh;e+FFleiqk-Dw-A4FwYNFY+Rw;-T1A*kTcp`_OoK1jYFnC4hkb%MA zFN9(bO!T~ZWqRfFgY%8G*VPynY`R#v4B=3h;U2$F?lvwUCaox3Da}d)M0JC$0v0tNQ%|0tRs5>)^CIj3CKQe7ikRYVtaA zFo3w6;ix9<`(g(I<_oDUKtP}@I+ILL$#i4WbGE|5!bZM}JS?dnmZ(>+=oiTU{MkNp z=SsMtMl4G;S{6+zSuHk4mPFr7QRyV^lh9-Yv->PQt=*bjf+t3riCaqbA>4+$pHxmOr8CneNaNSq|!eAOEQs ziid(Q&bg_$$NQ8&-?(*g`PJ|*oo|p!2 z-Z}ZKEJaDY@zqih$>0W9d9oCd_JIXn)>pukgMa7F?r)fcZ~l-cVYQ>~{YfbvX?xX| zFJBf|UsBTAQFCsp?T}#D`o&R_DgGZ-mJjAfe|Ym?&#^C>qa9!vlr;MJ)3RD!E;vzq z#-1oV<}X>I1>kcPW)xey)+)4!W(oIV$=rQ?JjJ)|*oEAVs!v|Iq-jyVSI^IxBn}A{ zDD=@m&2Q=0S;0W?j?on6nS@uF849}eOjg77)1PCu+BvZfEDtnnUgbw`%FP_V2mGMa zH#rdZI)M{AN3C6?tN2xl-Z|@pO9Jl7FxowVF2rczo$b)KZ-wgWm=-l@-ZdHm$kn$^ zTnRf~;;1!ZU-DZ{Go9G7I`Q25liRsr<-vFSVh-DDm}{u~8Cl98mm<-BE?v1IG}8B$ zQ#|GUmP@S^%|lHvyuQG*k!IF{vE^t8*OqGgQQD{lb@QL7SL+^Id#-*;{@APxY{miA zv^hC5uNw{acTTFiH^7j_RnJys2%>y zat=D=DqSNZjYms%YY0o<3-;O6*t8<4?W#>y^ycjhNu8XzD>zSqdUK2AlPgAm=xL9TY%c>G+R}dGS4<1~8 zHAKd+i&J6N;dS9DLlFYz3z67sq{&`((RQzx@jgd&^ZJH{k*myoo3L?-cKVj3Fsn=Z z3`rzzK6=0}(`*TFCP#zyZ%|FLUPmBCi7*th-M{wV#NXH7dv$ljhaL3FCy9ZSn8>;v z%VRxeCahM<W`{PD;9TJP-izYZHZq85L_ioa*&%3+2Y0U$z{ADammZl;HKfM)dVX@;(vhEXAi-%WB{aYgIW>$XG^;U{W znXmN$O=`HDjwl76S0-`;%ztv#KdN<2)qI;*nfk8iwOX4GyPD>nA2(NRKWQqqaOSmC zdYktLoX2!~op55-PhL=W;2r)&p^Q&WZQXwTeAe3lhtorUUFg)Y`7Xoy$QVY}r&!5} zyHDC=#Mb4nJ};t~aA|UUe81q*fF`UE6Q0qPU0bN~>Vl4EpNC`LIwNqo{bSsy^Nv)@ zde6W9e+R41%~V5c?jO+f?X-YJ%`zTt+b!s z^F8+V{8EyA-wvAd;B@r}z(P8i5qOv^ZpFq ziHL~YqEKXHWbQfG+mlD?9MquN6+uc3hq8`*k(Syo++%xF|%uZW^P^;kWbO3as_&VB9m8t_|AsS1rP0G&<<~`QTc` z0UBjB1nrNyRKG(ggK$SU=;-O?L_|d=UrnFGZ+Vz_)eH=x?6Ky%=+!5AvOhL4r=<{U zdMSivSENA_Uy9P(lQAD$e7mF8Y?TILD>7(7$-*dd4%z!%OJmEB)y~8CAvA6mbB-7% z)-WA2v-hD`v#szK8s2(9XT06%V<4l3E#6d7X=!~s8nfE7kGH2&*&Mo_UNQT*bIT~w z!_~k*a_{=O6XeHDM){&k7Twfdfs7KqCX!KtkvY@q##4A$p!jN~>8_Kmyj4}Ltf6?| zdhm5Cn>Pq|nXNMw`pMjBL+j>s@kmRN>j?MzJ_{-3E1WtLKqkpO#FWH}t`?in8?*&9Q%;$X;-SeSwGfa7#X1@(Po0F$L-E>zn!vfy$$1RoF2R z+?O|O-(E{RFsb-Emd?nZTs%sZ|QH>3_V&~i8vH?`CC6aCIM3lu1VF2WqF=^ zz=GZFL~0{pwBiD`oFWgS`_UBLu;Qt)zk&*(TYhpy3g`fZlKp<9GdVGl24^4>Wrd`q zdy!T(7)WkSo1C1~8O$p>J#Y-yjRLl(XOf&+u`kxkBJ2ndm1~730~Pj<6J2>j$KZBd zBP67hFl?MJ2va5ZY>5-@_?La-uJ2Og$VlC%-NPL(>r#D zjmN05y`3IQ3P(~4)U;8kNiIO+e)EWbe)jQ*hAp)e1_%;#8aBk%MK#Izia6zEe#am^ zO>6sV$l&6h@uP24>;?8DU@7lIWGTy6N-Lc2;8nNBp7-lwxuBvGp#|x9$jl+Ir9GQWnPgZYpg4uoY3*Fa9fJWvqel_g_$S985cb@Mp7!Z4Es0S!2@ABxyc8aA;@Lrxw8Lz^&M#_6FyfyT1d;7n+7{N;_N= zh^T?5hc6=|!wolC!R#f@sk09-Avyr4l(_qZC1RDMP<~*w*512%j)_-h@)_bJjIn!% zuWfw+UqY4FGhQ|m3nDQ)*xB{!J~UKX6Is6lE{_;kEB1Z*{5h|}vj(iSA1$D4^mG&p zG^*GW{EdLITx_GYD$#O(UG8)a?J{u(AMfgWHj-znF#sC;7I7%uKDzTQPJ0lg7&DJ+*l& zTyFgsCRh*sdg8Wb1m0kMeTe+l_5`8xGz_r8fn=?SU9}ici^HbvMcRC&}^5=e}K`Y7z;m z>E(hfGyWv}8$mVFW8r_|nU>VcfZ=)euYEX)6Ri+Ma*}6Y>J{U&6An)EP%$t(1W$UhACQaf+wsv+JZYrs%snrLe zV#E1Of290ePtfSBPVCRlg)COpF|`pXdf?}JVJSQLa1qQIzIW;a_a8kv)(S&>DO?{x zdi9$C>`+osd0XM>Z=hu)aEEUj$h)&JW1MzUQww6|{n~=i*`w_CelUL$5Y=n(3<-#E zd>!;kH?ZM|pTFtML>~)ahNpNJ{Ti=iNq+tIcEa6J&G<|hs|g13gn5hO>p}@948MUW zyDB$3sEq`HgYgU|?!iE(zhMtjItxIiwal=p>qJ3S&>L2Bi`-bqMh~Du9vPD-2ckE{ zE_bCQB`2=pfNq6()DZIG?i{xVo=AT4!iP*pS)dr|Zt$)OWW)Qvisj>LhV$}kvO6v} z7BeK!uJ0k_aZm}v&NL3!dy(Xp0=c5+YoS0XBK7wlg=TRAM>30(?2c?VW_kNDTFeVTUrw)}lx8%+PJK>_(lO~K40(C{oX z^?qZs`I+GBA-P)2#tw>Ci7Eoke$zRHJ()BY~-qo~$w2}aoO1r}P55sYh3ye;E3SB)EdTlG48jD%2w z=O5t>-XSFPagkwZ-N09VgyWvU4l;%tSX*O);-Ym77`%~Nwsr$%en0=%;zFG?QkVQq zT47}ckqNdhm0vU7D#i7U2c`KE96GZ3gF#C6IGb7qG!Y#f93z+;m+tyN03JVPXQgp& zYTl9!vm{x9Xl}-Zhi|!+wMAHXkMawg)g6bLQwjh^xK^7=p{xO}x)8I;FF*(CE74mk zaY|aMp20GUtBD1NjI4yjDbCJaCWX%g9tkAt?m6IdC$9gV#qr}C$`ccx@No??;;zOA z1qC{xAGPTs0Op3jcC?$5E7;H2 z+eAGG0ip9?9a4($>SPYKlCgs%e)!90tT>i0pQ^%U$PYNSNz?Ecghr2FuM3F6qj zI0o$e%mp-4Ke*4q6R&}ImJz!uM^f%Y|GI{`^x&3=hBfsxgn!4v#AN>#)S#HFS6SY3 zM&ii!UqP#x)>15Hg)q~X-Zc>m%BD59u;cCh@WJUzi5-mUI$h@;EXSatWx{;?=gjE> ze}8%fDn-!mbe4tXDtF>E;MM;bs#vn|x!$34$wYtlOl1RsJ=4Zu?Az2Nxo^#7a0%5!%$ zJeO}eI&30L?>T=M^ZwT-wHmytiFo!M~}J}?w|lDK-p)>gow=L;^Z!@(REpW<34j1guAs7me=r;Paf+Z*ZG6q%kUHd_ zZu!d!o&leu-g8@*%@zDjQvvr>{uWtZAMIY4{~$@#oha#`xA^!R+#(oCKkc%7e8o~q9&P4>^f|$etl1Yk0!4G&b|u|6&!PWU zgo@lHef>Yh+!GKGhzWhWKe7&Xl5N|NPJ2uePl48s8nU5jG|u&M3*o6RcXiV#FPC}M zhVs)d;s~f5TE3t2RPxzh^Ik2qZWgT7b-rp@!v`lMj2ch*K00& z+ImqH)6*T8?tuG6;vHryn~zu$GawEHf=70riU=g4G!-Q|EzQn-$19iK_&TMaBcJ4O zp%jnXx?#mxxuaamm%Ho+;jwhH61DU)=i0JAzPhRQFl3It1JKM5w8Q4-&)4mXyR{SV zjLFVLB9A@Z0ktu}*vQe>*}Ny<77RBhsVH!IxrCfLFMoSKz9Jk}zxI!G7nA~nbkrct z!^5K;_U$y{Q+aaE`U^diO}1H${(%EY5A7fgh7CVxmsqaZ)LvVw>JFLkKFv6xvR9g9 zaHL>_+Vuy_{)n)(eB$JUGOi*kW#PM|?25SlH;~I`>yqkorf~wkN1-(O5y>n{&Z}6* zFI6{v4JJZ7H_Nc$1rox`6xnK_jMk5<VT zcAu4o-oHLHk7B01EH(>IBMy^ocM}ViHSVZ^;rZ^rr=u>~(GuEqpv=k&`0{GjR14Wm z?f^||bq?5j-4>;XVYb{{$@yxZKSfq8`a%bpb>dz<=(3IQKQ=#i7`n9{%X8-%P){JB zE<5$e!amMxxt>+L14Og4idKf1`o8jO8Zpe2A3m1?cVf!^^~rsbLvYaR>l``qS|eQI z)CZ-~(OA(%f0QT&28L~F!1Zo&5zPgWir z`gA~BQIRGfgJYitb$C@il*Vjag;xet5jaMaxS0YEuX{M%0frF@sMlei6b4QH{ z?h+fs#2l`J1V^KAa(4Fm>5sK+O{7P`QT2QT_3!ZDC?=mA;(hAD&sz42tJyzt9a#)S zWV3rgKrqKzF2bj}DWJzmEmBooHRjhEV0*Z_PU@TIU*=%NuyPW%FpC#G*REdOcM#mV z0o-;T5DWB$uhGwjeSAVBCP@fpS&-6lEUA9V7L3~gNuj|+T2t@`yJqJQv7O$DPK=r$FiYmi$L_bPT+lOXln3Br_I3(KSwp*@a}lV)$4;? zN!sF+`H37W+bB=n+mBk>Y_|eWuW?12bhf}8HoW`TbIu1ly(}(tV9uDZ9#oF^PDO_E zVszB!_k@@=Fee5j-e*T+LPKM2daW}yy~My~gJC-tKF$e`7gO9RlOYtY05DClXSnlqZ>zEE}u|5nMfmGYYdlOwO zYxpf$on2fG3&u@~v3a@I98(wHYhyW~B>bW*5UjyZU~3( z#D8b#X5)!rP=2krk!hBMxvdzjQCNJuBNU}*wGQW!2a*>PYg{)HZ{Nt5I7S>%Y(j#9 z+`&=TQuVW}_{b^wOgor~B=a6>9NXG-h!LT#o%wJN94Xn)j&$H=V}l=&Z2hYQQ{}cD z+lWAF8ai((Q=!x?$~g{O7%erGC?F5^$eVDH0fX!kl_VUget}|=W>R>vLMy3B-rc>Z zf-gQj?mRb5cxYE3o1l85_weHI7ZGX|q1pvX3W}x~YHN1wDv4zHMWQkGcU(0py@Ry2 zbz~2E9uC5ropH$T66jfVKpC;dj0E`m<(EOckZlNCX#>WRPCr_gO&~Sp}~{a?H506APl4T z7xUlZTVOUay4)|_o*@u+dRdOa>ssedxGOb_%oDA>4lWNGNX4@LM&q=rYx~ZM`aY)+ z6wvhJElT|K$@Yx#7(B%VG!#r!tWo5#kr=_q^I+^&H92A zzSl;xBBE_m^wMJ<+0qN?rqR?}a$a2nB>h04;1sj!!o6yyDR$FKZDQ@MoO?*=SsM<8 z(xV;N3JpYVgwQuivzunvUvS3OUYWnp)U7GjOVQjY_UpePD$N#lgT8N*9b?9b-=*Ag|b+a&|=*Xb7L!J#?2>KpV?+K5&?*9AyE>? z#ba}0Y1Cw#AvgoyVa;6G6iVb*faye#*)^((P}{RovsA0P67-@%T(Uo1Ub(rr2Tw*6 z%*PlSrWV|sT#(cO{QnhX-d#%qaMPQ9X(*9T_Q*|9W`pgx- zdPN705IZJfiEnI{3t*;7?1)2_KO5Pn4noNJ1_*fd;H*AeCP;FcV*a!i=j(kN4#g?s zC+#8`J{QOIoSah>%42e6=(tl|@uaoeQWS*mmN86lTnW(yLn(S1O6Xdt$wwIF&f%Ng zE-2{B?%9~0zhTp+{Gj0A(qJuub-2BtR7w8)ADJg>a}s)*^twB?&6spIJ$mHH+cX_G zrvw0ZHvT^s2L=YZLo>Jah!?4Z{S(oUaR66FcT+*YzJ}5+t0f_PvNEqR=HeRWr7Qtn zi%y0$UCz(ne+Bx4-tD+i-1x8<`p$o}o_*GMKN21BJ~Fs*aQ%Q)XHHvtyAz48p4TR& zaL153=g~R4O{5$nY0|2a)bslFr!QG!x7k*$`YDcAJGZdV0B+SMQNE|cf1JC8DYmTi zNLu_k1<&RPcCY#f-ka&aDMqC|J(NkeIfnn$Z8j)XxfiPW22vQNqLa>Ie776x?c$P2 zg5-T(C(g5|VVTKJrk?1b+I_$eZXW3X&#RS;WCrsHnOx83MQte zMyh}4r|81uozOsVck~qbS4&@DD)$Ac$({t=Z8G`=|CM{9o%D6+{#h zm{^8{sZ#8{WYYcy)p#DAfnr2}gG6A~c6y=JEKIdkZIXAz|4-5>y)`UEZ4Lf4VhI-C zF(aT1GK$=~16 zIpzr-xPRBd3F7DC>iU7NRZ?p~2Z9O9Ra0r|BfQnP30YH7G=Z0TdSr?suIY~p*Bwbf z;jZw;M|hZ6Gj?@*o_L6JatC6@JXqe{etdIa0E=9D-@biGy*F{xcchkoSt=>S93*63 zcdoi?zj#|34p+NyofM_e`}*PEBdRYfQ4s0QTBG~IFd)z?wj;2s0FavW91{gBHpuII zhwiHcW#CVSX~Uj=rvIB^nvxJ^6%#SGHEp}%zo9y92Lzq)^m$a1oWw8s1;-u{0;B?p z(EGi4Kus^+qUDYq*=BuMZF{LCxEz2aYd`dYS z#g$eHg>dTM!|dK96WWLQf|ybM>&GNq7<|Xr=JNhx(qO<33qqmEF1RH{=I5u&BXyr$ z&8$-PZ_FN_{Eg5%dFqrzCq-Nz6_&r-qI7uU`{g!TX6z;&P~xyGMzfE@;EzX2QP9KM zan*=@>((3Wq*zDnowsh@)Tq97)K+!go)Y(2T-}!99eaO`H-?)fN3ff(MyV*nw;n(^ z`#DSgCX~)p;*mi>Y5pg-$V^Uf2PxxPOgRa0mesi!%efu;CHEdXc5Jg_pADeS$5fP* zq@pv<`PYfx#ITUJw}rh+jlo$=+@%5mr8jThoX|M<-~!=YyvKOc?^L{KQGAeP%}3!m zga0Z`+pOKv`99*B<6%q&*TzIlV2qUB^WUB##5xRYqY7CGg;TsEmTnr2zrBaKJOcpRl;` z+KA%VYVJ6r@;jeb zq%Fyhw$qgX{hQ(2gpAWGmKh@?yJ4JL2m5kq`*zZW8!cg>tO(baBZ^v;Rp#j$H&;fi zD!5wr-O@=jYKsudw>xwdBpHxI`>*?oH3z{l=CMDU~**q(V{n2$4E zmj{sweE{m5<_@VFj|vSA!6g4_$ExNN+ZQD`-U#|y);g%bMIHU+nb0Z0DKkkYs(gD< z!X8Q;EBiDoMQ#$ZJLP@^R! zHYZO+$N%CQ07hiuNkg$_9KJ~ zadEKQ(MP5;OBXM;1&XgNeIRE+KS61VVBDDzRN%zpw`_58wcLa=XGh_wq=so$hT1NY%2^@8Zh?XSUEc0Ury)#YkrOm^Fu+7Ms4-U&*fFAJaB7{S{=`T z^cowR+@qDt{%--JM4BVWGBZUlTnXuB8bVKV3gDz22>3Mm5f>B{{;8+oEU>`P&UTqC z-g@?^fE4XI^FnS}=jYguER=yS7jo0dRO@d>iqFANMgi_gC*FHG(q9W@zv_s++Y z6&vXchKkX8q}B#Df^7V}+@D{mp;H;9(_?1M+hK7NbP1`^Zo@DhFP^+bo0l75pXVzVUu?LmpD9psjCwrsXerXFw{b`HyMgDH zZ;8#!;6{AjYMo=3QM6^@bk#3jLH^^bgR6nSC{q0{B87Psv&=s0CE0UVG;Fb4GXvOn z5H8e>l)*1|u6n7Uixu~b-_u_>M44KDrybP?m{5!zx69oeNsovC?O%{gY6L@0PVIU7 z*vxmU5KoW;oE3&y&v4Yb1r*}x=zG5D*>Ku9-`n7F^5l5SZ%Q5Rz!^F3uQ?~cxOnnI zRnRje-NUU>HH%SNn4CO;1(qp;&loya27oS3YFnkX687(x`}K_|E+0Q`pMv62pMGXR zagpZGr0IEEnC#C8K;EB{HXrLCU!v%uEN?UY_j(7m!%SR~$*`zk#B67GD=O;FD{C%l z0V?wrr3z_t#Z7CIno!MF7{GPlHkK~#&qW<%MCq^2=N7*FnJr;@5L%s$YDd7D7wIGM&euPcAq7+KA&+KEI+rr;z5~*7KXA0|SRF8a ze>fiPKFtt`*3tazb9-k+8=LD0WF6gs@4Fjby!f{Fmn^6c3NJM*-wh1B5@Th|yQ0V^ zcXqoH&UlV?NzQa3H{9Oq_}TPP`HW>I<>Z8&o}D}nuyNSFJ+y>pEzs+_fI0>oXLD<0 zpmn(S&)(I0XUiT+?H_Rz{xnVyBdA7`9Aj!#S}4BuDPLhok+r==f|c(}9S4=%e_Irt zb(N*j;%7ynAX_K1_K=F#qH3aTXdT@-$XR~;`0)^csO}$K4L8mbOd0mypTk+KZI*B) zmD7g5R@iNj_gognUg}qH$;sKdwFD>V8TfC9kke>(Nw)w8@LzJQN@T#bjt&E^*)bxz zgsJMaL2xbc7Ko^Ly)z%VhKi0nK)GX=dTY^~`h|wN1h<4^P?w}sRZq~8*y*cC zR~DP7@D5-LWOI&VW3OV?u~1(Vem(t3yC&iq{#v9+?W+ERf9s*5P~OF!c~gWOWozIx zACI1;y$^rxOJVQ;%OoU&ZiAnV~#a~|y?_AachEM&7<^2*3&Ne8f{(R_h1!jGvE9jtPB!P++QN|Gg^3S-hXDBSgmWxKvY`#Z5g9T?n~67 znJzX5zj1-Bw_`P7>tP@v{hn*nL8ZNW4-!U*giyGb`nRqX2j3a4=ibsph=`C3VppO$ zYkuHu={Z___lDCHf9G|6JGpD9>}?w7g373!uuyUR*&2KTvxBs{co#fqh(&=V?J zRWdfRXfXU!+V=V4=xda=^^?woS5v_o9#`$zd4W8wQ1@iCX2lMdg{p-!zo2J2zv=K} z_d~YDF*Y{@Qw>+}B41Vc*O3|f&LxSBr2K2$E}by#?uU)^ppMS_BC^3qvN6@lJASm2 z5$=M_m4%!9EX+lM68*zC7n)UfxMo3trNxd4`Gt*g;FTQvg4xb@+s+Npkd~_~$JeaZ=e{ zuOfpQR*?vif9+(ws*PI|7RmPO8xBmGPzh3RxWC#tZl)00C^5o7aakKz;6`hJTDI>P zTBS+svGHnJbJbWsCPB_A-yn0t;Cbn3QXdJ3n6xu3wgbv3^AeYQmm=&D_8k)##1 z#EpZ_Xq7t;Vdbq$;EEy=Q=DeGy`S<&@zs=fNYV5B0Pi!9m@lB4`I#jJ%6HvZ-or71 zBHTXJGSVT~mw(ZgcaVC5Wg^@n5v)ugca$6^i0G*qJujlGS%IFj5#f#~%l^omOz3e(-4XeQET0C&|iK*jvz zwm{pbp5Cv9Y+cUYU_}*gi?76pZf9rLhY{>3+gWpIaQocH-pKxdH^OC?%m@xL<#ZI* zWy>s2z2MUFqPNrLG{0k}d6;%cIdY%N^s7il=JzYB-pgJfBF*>N+5ClDS}-WYjl*&< zpC4yQMU~wBtgL=i=CLXdq-fewQh8PX;vD-BZSrAV)q+ErSoj5rx!^fC&@;t{!czkYoL{oeMOKdNy6)I5U}F5)HI zL2kLtz{1BgfM3vlzn-Xm){$srkw}87$!GfY8Ytj%^2M$rF&y$&mk20+U9&_ZY8g|y zAct?89v(aXX#X~Qj)R_7dkQYf{IxEW~%gDQiP zi$I6OC9?mb(GFBbBSkMqp%AuJ@r0sY)SsrT(DsDFie&o6ZS?HX%a<8-DWsG`8D)*S zv`}k>0Ku$jrX%S(M%5+&dVgf4o2DVZWu+!uWL0nNk*YHPw_+8a_GxTGdK|pzR4#h4j9B8)(#o!#jn9g|!8K$l=$SdN=dZF?sTpA6Jw)`yG7cd^?;nnp1 z8exM^ZkCJf8_i4ZRprCWw!y|b9Kz;3v^1x;_UR=(e0T$Aks&i|@}1BlT&g>qQ6sU| zK{$O$vETWI{-?O@#&(#xV~*ta1g&rTdCx+)q@+qrPvZy-w^%vdO%qZnwGyC>$KSpL z7QWcR&_OI8TMkw&mNrFD+)&2=?MkEc6od=q3zxt@5Pu92ucuJ-+A}pUez?Q$QGjNR z)nX*Ybl#5v6}K?iH($~sC}A5?NTBpKJ4MWrAz=q2| zBgd9k#9~^AiA1Vy!(|84VK*h!r>m-}S71jzadd4yutfPkW@ZlFZXq-WVh`ZWn;pu)J>lT(G>o>x`r-50loz#k;CyiHW(F!3C;1jf=>{4}%lf-SyN z#OaLovnVIbR0EJb`TqHJ(SftLNuby(nV(1se1<479y87xoIf&U3qGYDW;D5IPdhBKc?rB zj=aKtqrq~50&xS9UW6VR7j&?T&s3 zgvCc%Kl-tF8`3Kh=P3esPkM?|DsD;s%w9698 z7tZJJ#0v-tKESQ$=0iPs(jJ3;Fc}wI%@ZgTFr4XYnAauEnl1qlVU5luHaJ4o2?40D zld1neJJ^>YIYd))#c!dUjJ5wg&O|g9n)I_OkzM{Z!%Zb98PRnvhLbc#z$c-|bQ9OP zPDZUmMM4vy?6Sjl8MT9hHewHVC-&iT74kwNZFpNSIGWUJPTIWwKW%F2mu)gH1jgk! zv`Jtw^#J`yY-}wq?TgT{QCxd1u;Kc4K=H;wG@i#Ko@uQ|z#l9gT7?$#T~(>0JBKA? zoM@@DZ+|X9{0ot>2Ze=hjM8)X-u)c%l*Gjx0$r9(bXNGxa0Cn)Bnf?gx+N<Ifk6`rdka`NA|2y*Q{RQPcWwcZvqxn?McoK3- zyi}DR4)Jq5@#KNS$tUsGcxruA%0?}ls7c9pz)nE&y>3}np?@h8;|N7tY#4KF!=khX z)&-jdb4KtdF9reRgAnEMzm=z~gjG9Cy!!fvIur-pW4amM7+9wwKk!}3W#93+^>DHMo~87>!(;fTkD z5w@e^JnYKtj}BB;5(_SAP_qXtE9AzFikcJmXunZ7ZmY!lPuc#GWaE*XG0)^V>N)bT z8$Psf+jEFiwqrHdGml-Iv?tHJ<5~LJP^$lfs{T9`EOpM&WP<1GP-` zx&tGYy20vx3xTH}!~XTqgm?$9y~L5EO-nd@22HP}>wR!e*!}&TuQP&;zFk)}Y>DX; zUlUK#T}kk&685jB1=e`IJD39ThPE9`ML#<2U~tSe{u>l@U+vC?9;0BApW?O{-Sj8# zg!zX4Bz%2mO3sk4f3??S6K{#NlDPjuJFw;*s;SrtoTUcNnV7tJhfyO=h3mrG1GKr0 zEdjcZfbfo;mF&?z&HMIFn{6Q|r1cmW50}q=x*J4Hcof-*87!N@K&;;X9{+15exj?( zFbk%|J^m|gA9BMnyPHbOmO>#cxxjYXC0-+Adu<_p8KV4vibCA;C$+SK@0MLUY-DtX zAc-G2)@H;87Xk}diP2%+=zfwOz*+7DBW+jB9`q;%b}66vjO`|b?mA{=?`13f)7H>P z-*uP-ZW24h^B74h^#?&2G?d9&@jtD9zmrYRCckLG$NvkSuq3Lyu`%-sEYIZMpr=Ib za?jo41WL$n%D3d$wd3fGc<;5JRD-cSrptA*-wvc2r z?CH)ilAs<-H^6;v`AFKrGAa|N$gD5VzPUDEB=M#VNIxe{G~jKn$D7Ttf9r%oHe{i3 zGC(yR3mq9=nVdRCBV=Rk;W4}3E1D21sVF7$v%~vp^)3Vh4($v6XHe7ee_cH4dmRAp zJU#Tqg*g?Ah$WQy{P(hg)ZKy1MZbcC#Yq%}Xf8B^MeVnTJ>hIGCdm|<$ta~3h6+8@FvLCWUTuXvOHjFp+uL5m0UWe%6pfQvv#5Zhn&SFsDw zH&xR=!UFaXWhEx&<{ub8$F8LuJ9kn@kBV#TMfJM}FXdV;mC4`{SO;*=a9=3L`L$ym zxO3THE_eS`g#<+&+oKV5m&W=BWpddm#uV8%2Jmuv8e7Z1N`X8<;MscVdhC2h2vxro zcW}!;p2!xYFNxkU;Z2cCF#7pM z);=VY_=?{m}(xk=iho7xQj?3oh%37|?DPK9!V9ta{l#eYZh_<*%@XyaciM-@su zVs4^dXN$Xy45aFNFq^$xn46`pPkZoy18Ng39&WS2$v>rVM;?{UzCWK?Rh=62>!s6E zx1n%ht-7R2uu9jNqKlPmRPbD_8(gH7uBV``erX>87{fR)wAgM8V;)k$^}3{FR}C(z zuUd3ej3qV4%dUqwMN>K{j(IM8p zTn6#%US2NXza^m?(Tkkf+}+)6hA-3HUZyRHI;l6k0mBf!nS^K|*yRaJi*9g6M^aMK z=1dye^Jplt^x}igLM4SEGU`EbIXcVLq-w*cUP!^Ow1dEyf~J{XHeO(AEDtJL_YRi9 zgBwkbqU~vm{8gP5*Q)HTbA|SK@ZFO8@A- zj5S;2qETx2_2S10_O5mDX9ZA9Cx8iDd^)oA+Gu%3QHu&jNE+5{p?a0<=f&EZ@?+78*C_9wt#9zg0th5P5O(oBU}bBE(y>M{Q{rKKLl9p4pRz!Fr9R)S~GL1 zRr!53Zuys7-N;mS4zD^(@@kr zRs1HffN}_)U(YP7a(3k5tK3F!qK2_Oov*m?{ISF~z+|zac!Wff;L(@dM>b6YukZ4c z;MG29mW&7|c=s+4zx_{Sy4|VK;1s_51Q^P{6{c%*We_Ql-Sdc@hRW$19FcwgCT543 zp-F3vP3Nj^-)#xId;yW9GeB4Y838Pw;3q#Moh2R_E&9Mp9BE=E^V%ppdn^V@a3g)#KTJ zgGlBr_{4YSB+j&gTq~t^(M{;Ea|p~jlEt75$Fu2dTV7h^d%uys(`6K&h0AwV zmT(UP;0>7@MMYaBO&V>aR(bK)Hf8&LU4(2fvQj)v-h>{3r(ss4Q*|-r>~q9&VKr#HOFt`?IszOr#Tr?C1~0FK~YEJ_nYw7`x)? z&izjl_g?mx3H$gvgcN&jgBWhJwTphsW@L~8*{mBF)F=#k6$^h9A*dS&$!Ql6oG_w| zt!)>Smd-z z@>w9L&q3TzD1Kr0O#3?M=@NgC962kj-2Mxpt`}b@XT~^q&FeIKOj+)raQ<8GUp;8h z!*~7xlJlmHlg4`Da5>rHuK4s4$$PRdTc%V6< zM|kj=Yo`rbz$=T9zEg*%HdvFF>BezzC=jNI^4B5?6ZHJNBf?%-K}CS3!Sy2O)>@pKY+R33yE_F7Ec%?56hK z2dJFXzLRsU$Z4ECbU$u0rACHnXXBlJGECpO;4K;Mlf3dG2pi!wfFHKoLXkQ=`}W+9 zJ<8w2_=T>bsVVGuNy)9>0j9V;Y}rAfv9S)GI9;qf)#W^u{&cjJmGhA=>*c4xNW(3F zGK=q3;if(Z&&1B6QqzR`(+J3|DKJXUy{nE zC?P51+nzPms2nBM=|T8#BpnZcmgj@5yS7+=SwZmr`_MT11}J!a-OX#s`u0^RS>x$b4e+&AK%(LTy0k-PO}nULDUb(h~MZN zUGs+jveysTU)Xy-h}TG8KcWZp)pe$IH@L&o5^kS%xy$k=$s|z1+E5*IO%zXMlo*=` z(?w`tCF{02Xqj9Wluq7dVaYva^f%4)VmR;iZ_eKBLl14fd{U7byk*1oFd0|T(oGjI z_B>L7Z9`%eZ`CO>!o|R%6Cu=IJuya3bkBV4@~=dKJNqUWeWVGSJ= zJfg>mmkdN`+tik0Y9c`d7`s4C4zJ1;ogdi`hZ+bB>ID<`vQRk+;@M85VSc)~N_lM= zGNYW5Rt7SfDVIKD@f1zqS6(|qQc~dgk(^GJKSTB+ zfZroIV+nFk2UiGhxC)D7%Oohmfe!eF7u&zyj^(YRz;X5HXz<9I7{(t0Inm*bOky|h zX&O^305wx1?_-dop?aou%78<}@aR_}% zpov#9q15~@++OX6vj!5U=_eBbx%*|Ql4cJ9I?Fm~(l=42DTy68JJ z)D{fQxW*P?73Ig{ARsI2b$tgTGnJ7ro#GFkv7K<`5%9$eN>so9h9Ip?k*87Hb;l*A zt*fhe7?!yrE1S6fYVV+ilTk5^&NnKRikvGfla*5~P&Ivo7migtHkQpJiRlc@NGaS3 z{5jpU$jhrPLhZb*x6!4nIL1@~>7pYqo&l5R4ZDO`WfI<#ogg*%7s**u&1=}VRxC>x z5@Dh0Ib<*h0C#Z430JIWhYzG`N~Et`sz#}Pp-W>o1(_UUq3aw!o zk6bl`q@*-+ic8PVemG_%6iMs5Pq$0bv#~Rb#kl=5zFv1Tl z_cz)Uxf5qb5r7&iNk$Em6u$u~cviB=KLJa5J=jRwUQE7+;itu49KC;ZANbAjLiTFC zCJMZ6?6C=BzqKkI}1$nZ#(e%A*_NOKLBJ!3wbXu*)${z*3`5vC^c!X z6kwEr|6~H!=E*I$FWsZ&sYyc1hhWprC}C-7l&ejkKwS92Ih6dKA$?z$dPj)JTBdaO zH#S9O{jzqbL4%VK?=VJKk1IFn#pUs(XVz`fQPDWyMX2L6ut%N6Ek5VGUvbg;eQHsm_WrXrlF>eBS@Z1lhRp#{*05T3c{_9M3ELNq=}(T z$%GIxykoE3%;Gc8`OX64zs)D7FLm=PEln3?sD@vRBhy;Xi|?`k>W2x^OGh~V zWk5$q2U?o2cIRMHl;HQi7a$%%%43`*cTN%ZtdWjRg`h{IXh)$!>qSiEcGaZ9n~MJX8eCd)%f_ znXwU?x^9gMpDRen3u&-B0gi;V`(*_9s*y7w#md9uKH{Z03bew0NRDF}GH)p}9oPeq zpi0S55i4RyROIoI(B1Tq{TJ4T znP6sISym&0F(*HfD53+|9C8Y*P9Wy3J6@m27LW#V6}Wd+r|WwO32>pLMt$ebpbe^s zRDN=T)Sl$LjeH}Y#GKO89#iGyC;QGQvVxU$y@)l}X`GGGPdjxuMWCdG(o zdBeSq1i^t!o~0DgiIPU7!SNfu=kxcF#mD`Bi!PrTl{DQew6_X7tgo$5B2&Vd(sXX; zCp!PLaOt%Z1g03@HCJ_GG|nv)6_l0r+IEq_9LW4_nS;;fpQ^DdQR52~#RA{wtJ_|g zP{7#>M%~LToB~9SjMQm}iQ7KlXF4U#y?QfIjjxiTprEh>NH=xn%jEx0?3@Lk(i)>C z0NQWNhdQ{&oy7{be9BrO)3O#2nVJ_fW3D#+pUC&@(`RXckIUY~uiy93A-*8^JFYQa zy_#2=L()k{ft&=@t1=-|@_#%>oPA_x(O_zlNSFc0I}0)t&u;*o92;b0iVz-kxB-yb ziChPIejG}d$kZj)gtE3RtC02DcMGj=TV4 zPyBFydFoMAHw=px8n6z&EO7t3xl2UjHxXB&0kY?xz!1~}{?&+XoA?vT^f}yRQwG(a z&b3gkT77?qZu-2(V#^L2+u!!DL%uLXEoxWJ!v0i%WKSranO;Ck>U!LjM87j@@R<6H}Blt>_PyAnwqrQ)1;!-1H4izD&azZymQVC<@541IxI6lARcy54m zX_%ysKv%rMi_My!lT-6cB{HnI*td)1QVu!FNbeX7m82RjT_?isX3H@)4QEs(nNtw8 zRDsse|8^Ux_bDYww_jWTIEIYM{e|Fdb(yA(rKM7bcO*NWMM=`v?Ukng`in@sf$T!P z5^F`a=RlZcyB`wg|hpQXtlxvV&0>&Q3k zZz7=cGBz?YIuB;GNs`BjG`ocbH~(;-c&vT*Zvj#{zCAY|ekVv)xK7KctJ`j}njyWL z`gaUO4BZ%!fL2Pf!E08BCUl=DfRN}V=4UboaDsfKK4y8BATW)E)IemU3jDXnNY3>M zgiQBdG76E+CD1uCRR<^s{`r0OzNKNe{@)HHL|#qnsY#$*TZ0SPa2YKd&p?EJN>^8R zKYmk5#wJZYMsAG5ylIK8CaRi6)z0<%XsPVhzroCv;Wx>@^$^ryIsYoXI^e(UK)X33 z&1vZbgFrL0IFl}$L^8hdV1d0By=op<{cT8wMx@mfn$ve#v#$qZ4kXs2ptT*xF6ZPS z^cORZD}^)9EJ+%ztiutMUuP%`XYBQT8z_i>>M^!8X(G8O`gt$*?9V>6?0hBHBKy04 zxNjU+Tti+wk&2R6;;c|R=3N`I%IZf3SxE&Ly`Yl$x!*wi((xTTcD%()`yGR#Mlu+= ztNEv|cW0q+G5M_l=-1=pbH*=iZ0>c?>`LOb+@XR7qKti!7yc*1=hTG zCOU;+$ByN$^zkXUIIBTv52aLW)FwCZmZ<)su@1iV_&x|YwH;`KcFUMSc0CnAMjK>y zkZp;zo{S&jJ8>-tU|V~OF4x%n9XW(MTjwTba}Er{z(&XtXDCauGwq<0p-3qsP=Wc)S*L7@Dy>fm?C#uCU1W>B7K#LT?~=A3K)Ebf;JMoxsL%W1N3rB-x=lIuD33>(r@`_b$hg zONk(dU=VMtgr>GO^@MFPS?RjJ^l&8Mo>?k2@NX*@Z>r>)%PYU9SE7bzU{k8WdIS~C z@X2jRe7eQd$|cpzo0iAu(+C?0I>GCR5 zlG_10OzI6+N`^#)I)W;gBMCot9t0^swIs1{p#a(#>KR>))?cX-4@9Y6TkjH|$|DKu z*&Zl+(ok!3ZOws{Qg#7>MWnnLyrMBg8%F-G%zwLMsR+D}TZ}E8XOaE5Ob`g(Vd)22 z{kO>#uk|V&s*#J8rL|_?IRA}$do3In{_4voyQWWYt}^V{#EH9Szn%q*FMV)BqG|fn z%kWQ_vB>(f)bBeDd9)urkR~nsyl$e>iNbbnsW_uSecb@;n#cRlX|`{?dCtEALciaNagj^{uJCjq=$eAUx2Th)A2WbS_8B&~>AcouO>tD3=a$V2GMiTQZ2iry4bv#%^GFCP;dTrzt>Kl6Nj zy>5JEZ)@xNQk1H%zwL^EH8lW*%s8VP)ZB4x- zI(F$-w@v|C*V+wE^7tgJB8ayhzQbUpz7P#mhq50ZWoBk$o2*g&g!g@DT}t(Z2Y1fX zTaIprcYHPFMvXl?hK`{R%>wIRDq;hd9I6Jb`oi>#27q)Sc%aVliXxWtMBdx4Sqy8&S*Onyhs} zxo8Je%Zv?XbX#BPBsLLOtfGSEUmbR9VmVIb9P)K^bdk6EH%G8~>!q-RGVcfitC|fP z6H|xPqn6<9%gE+Rc`LPLOW`pXIImr>{^<}Mb=lWw@7`@Hw3A=Hbgs6adHLvwdN)zx zg=h%aPDw^A`vybmZUI7nq!20Iw%G#iG}R^d*OY|iQPQGA=z~?WOK@VG6+b3rVTJkC zXfdhk&lfbSv~$9?;5tO+T-tF)er^Jc$jCt1fycj)AWH&KW07m|;7qoy-fXRafB^Q! zKZu6VUT2alpCMR%&+S6R-psZLCH|=2~Whb1xuOI89DHfw~%F$Rgz0%ova>E(!}{0EFQxyeSHnTg_(_C z9&T1e^vfT{KPa8a7L8Q~#IqrqX(^$Vpg1j_l~4>myk{IyA^UIJivB=&70D}pL68%o z>+We}454OJr>?F}jpM*cY1pc%%HMp(X7a+Lc+KBjyUUxvd<=^Sy8xtP>V!!wsdwE9Sx%&IwTx471vJq9rcOUe-PWY%( z2c|WO^s(wSuzmFvE;_C(bX9GT)uLu^*^Fo5GTw67%OcPR(1@8{961-~&k718)2EcQ zxRQ?)mL3Tq(*f8nRr4%1X(tM|{>GJ(9pLZZu6i*vyPcE*Od}(dH6xzQQuBE?mOdWR z$GJ%MMMB$T7N_zB(ZCxG@+{a)H*lX&jne_Ox+(DI=g20PK95RDvx?Y#reuFr@P)lh!&&BDq` z6?}waH~F?jI33u#x9XCl9uuWo13`*U_!DP%=#QPIIswOhG3&%X;f~l46RVSNi+=!-|TEwg!K59b6EM z)(pPF0)le?sfqV0#g87Dz3w`GVU@i5;;gvh*4ECQ#eVM2Sh2tOnTVmteU(@2AXuC1 zgE|&D<_>Vqgxt=tVL0G)(pPDnND5}O#WLT~pNOOuXi9WO?&G=TAJIXt(?y^02n!Ef z+MYx3R^@`I6DEO()SQxIbT;dio%izBV0x^FNN#LfJ=mHVNJ{Do;!c8&s9xH4TiXpq z=s%?*HmGRdGS_Xw6LHMxeZ>a*+SyV~x39fg#Vb}u9-^^~Q}34|Z_}jIf%`hpU4+DK zhs4j%d0Lf}5k^6CTMD|f@6?JS4nIKcMy()MQWU8;bO*KE6yfSI?)QWQW9Q#4!-G7B zN*KnBDdc08NDDO^cEr^JY_ST#Da$$o?mqXO} z$@8uEt{EnD=l0e>B=)5^GnPRhNyT&!Ba%NaZF3zUROG^|;>cJA$q7rW9D6D{;h zTJ~95enYM0G9Na!KoXU>y4IrsdXwN^Hfk`=(c-f|V=kf8-Kw{+wH=RMR>LtDGj{Q! z?D~t`)()NpvC0I2ZZ%0POgFAY@0XMK@Cbn2$^DY?>b4m0=mH|+rs)#9HIf*k4Cc)# zz0zi2ZGE1rs=)MN{rSeVn3z@@3yU>Zj7c}4V!!FH^)oLel;tGl%>u{e@J zD_pa|m~Gy|gvsn0_cyI`80@(ZChCeGFLo~fpbj=wYwy1(w^r-LY$>SqToL65qK6MZ zJ?79#4tS#BB#Wj>MJOMA@76{kd4E&vn$GLC29;&U-|oIa#g7 zCEsNy*|N5h@%HDlVJg-IS&yoe3hM9X-N<>~(4e9BfEm(*r;w%hisArbT(xRMXlGz~ zWM6?vHrJl4cKS!%_Gix*D=QPp={(h6s37duX+E$=+Xq2q!k9o!b6AEi6rwD05EQ=|9s5AK$dzfR3b5pK!Hup$L^+;qfuj zN&oVvsZniWeF<9I84}-xjv|usME-E9)$03`1Mfl_lnW0Tc>q&p1)?c@*(3mIZ*p_9 zvz$A$SU^kD;GL3l=m0=bJ2ml5ESc90)!V-a{R?n>Z;IJ1{ssx!!*_LWr4j^yb?(U4Lx^8GSv4J^dr{UA)EE_@k0oHgQW+gEPv>{P8s(@yRc~QxDk8}# z@l7p5WMy))-sK@5e@eXzM-GIrt29>roQ$t{2$ZxP%Ng6TV+GsAISFESR{I3h5r-+v z+2HL(e2OJXnI#z@BR#wXL5cwtfu&$I`+0Yn*4Wn^XHitDfY`@F!olO}8{~~CyZyMw zG>*VmG!C!9*hG&rlcRu=$2I!uVetzlYcD_N_slfzX7an}gsST1+qcI;<~~X;3qaQSA|InGFhpR&1^FgIMxR@Fir@PI^sLgvrhAHHOsdwS&ZzD>?WR zTsczKv@gYy&h%X?NwE`@jr7dC1bA45*r-RHg8>MNDZspmCoq&E z8}$YsA%XJFC7*IPT{HDMb-_w{_1ndUQx@dq_o3CWgy^gP`soujc0I{v2h2JM_ggn@ z>ep@2tT#)vD_3&zC+vWzua*v~s`&lBbI&c>;Th;c>BnNT+N4ck$!$MP$L&2Qru2eC zI3Ob`_1b)aO5jVTL*Aka=L?7#vcHg%RaIhkk+_^{wdekzh!-|R6r+9@b#-cxcv4KnY6K4XZFzv)6*G#ce~vD za!t3WviSSAfOqU4$#U@VopFWb$v5$H6WvrzSSh3REQgJq_O43Ft(4osNph*znSIKS zQH~!h-RUDPJ$Y@DSH^VZJOG()(Gui=Y2jAWDZEWU%8-QGvfnaBo2AMccm%$3j!KliO#htebiFvh?s=B%Nq=qZ`brc;2nN+%yt1` zFSUzm$Q3~&-VDbovhb0Is7981;PF$ZByE&cfbMT6hNAaKi7FkKDi)K4p8xzo8RawH z*!@Ee7uvWp2|b%&I~UC2h`XW#g`x#5{nLoKRhZXXBk_a|NnGI-FH5#%IJ~;!Ym&Fq z$3+p!oNwEYzTeK3*8~aL6%49JSF^IdBb%y!qPtF-YC(UVUY>r8R$l+k@^EIfK2EgV zezRkTQQ_Q z0jLfUyro%}Ay{d9j-Pd7Iwa)P!vIBB2tgnsUAPSRyE|EZ-tzjjU>PKN^M5xt0)DB08&rtuAl;@zInjcIW{&yQrC*2HtdpYl?C-sQR&)oOdfmIP zTe{l%`k3H^igD_|iprw9omUHO3cSQ1WV~E${FihiqM~F|AzXZ{XV>$OfHf#(=rpHIm(E$=YJBCmolhpxq&Z znx3xi4&aI-Uv#WvRC_(rpMpB=!&tyO{`Kp-$g|7`FOhe$?#vD8uXcKh?XrD23P^+uT0m<@^H9$vb_TdH-y}_3M1J#4&6mABSfUpf+z6)PAfuoTbVwME}W6 z4bLeGW!*Ftgd2(~h8@5WA}?LK@9FL?a?=XXE?=VDVLRL|qvV`3iI9WcKHdQV&#e9Z z%WSgD&WxkBhV%AMleNxOY+Z~HykzuX-P5fQ2Xkb#LGgxA*R0f7D3;0acn&Mr(|K^{ z=OKjYD{hVzJ^`+1M*O+)urG#KG)mrGSiu5oWqs(73Uf+-8kBFd%CD8Qbs|#6PYkEG z7)2R0*ragChsss{cKnUW=R&J{(DHQJG$6MP!jCMZ6V^4}mE}PA&@)vvHCu#fJw!0) zN3m3sx%V{}q{oWC>8y>I5LcB|Qu@WIdb{rO4yQiU9y6;tDpRA3I}tS101kqQiZj!9 zr*RP6q9V*KE!Ail80NnFsP22vNrT1nE|O6^r+#V0V^Qak+YU-sS3dMJf8S? z(=*Z|Z)?oJeTY-YxKl5Y!tm-fkSZic3Nm`^MaEns23US{ z0%&czw54CugcdLU8&FaC;E~fT?apM~GEph%pDv`0*tRBd80=K8?&S}a5%poVKlSC^ z+r!biU2#z(L5_-I%|J>g~ZPN04BxAG8{gyX-^QzKuLaK?m1nA zzyJ4UgQI)3#iWI)Qtm8ymc^N9+_|G~!LGR?1~yGu3z%LX(wZgHJ0OWm+bb1UDADl^ z0%41Lo!=c0nW~4?SKTM=+g2P!;sGTumD`!)y_ss$*d zuHg(yq$OQx57Xk4WEO9p1PMfq-KP$ri_#AsKAe00?1i0SET!&!xHIYl#fV@m|Eo13 z<4%UQ?mO}-GJngS-m1HsMm}E*=FExZVXhXcb6MS80{e>kl`B`y@^W$MlrA^;1P6R`XroD^MutyZfcw_F`DC`6Flp8A6i@#L*djt+K zxcFPZtn+XH2Scqo*DS#7hrr(7Ul-(f+PeZY{=+vKSf(rLK72@>qM0El2tOQ2avL`A z)>~BM7CYpl)$8O^JPY1|eT#)VIjkWQdXSxs(B@t;yNrFDrBz82Th3v8NW>N8)$Lc_ zrSqUubpq2Vu#2w&=x)lm#$Rm;M6Atum1TBEO@ zX!U;mnYH?v=kwz{=o6RLTk*n72LZ-8dMZz5_DIdVeH*UfTNAToHG9yRflNs&mL7GR z@COezT|sXXf9fgZb42RS&8>BO%#a~+kVIm@vAmy-|9jb#T~tXJs80mtU000Zc$<$E z;_!E21LXP}{Co{i#5xk(KuX)tQyer}_%GsrWqQ_~U5_^QxRTO8!_~KiV?4uHs>@?s zQAOT`&K{$;y8kZf8Y?NQXi(YFfeM(Jc0*afkxzqDMXYW-lyGx*=lJVg?U5NGQCy{k zsRFx)Yh7Gj!#O{+oOXOHl_BDXdRcGCMNfUSU4+ebql#RHmig%b=1SyM{lx>wX=B^j z-^XM{4Q}1I;lE0*o&ExF$Svfj1YU5jPafwr71tfF&BA5rV;i*haj2NwXuq_u?}FV! z281~QcAr8PsPV-2?O2ce%`91>pneHrf3%AA}%8)A3M#7_{xWPKW{&=gJA zu(7b@EdhoKN_tZw_R|c~8IJ(C)-|om_uo+mzOz{x%2|}!ryUU%Rd$7Xi^3!vU>P_23F!_t8NXx;_9u^kgq!?Qgnr|`an!i9F7 zal_*8-$e*P>9p5tQrNmREHNo5{F1Jy(Dey<+Mo0O zm44p}E`exdNuJ~An0Gv{w7Wh-Y|EA(y|_)(jtf7c#xtx#5a!aICs#>j?7KHm=DTTR zSu_of%KX`%+;DrdD=I2(KMO6y(^*Q+lC;b|Q&$?l+ej7vqmMyEV7ATzQD3CJ0bKw) zA?DvaqVWrjD7^%M*XAfrGt)RJF`5I8L$zvIOl5bLMimV1jyfpiP<1yKL<^}Y%dB5N zID)(Jh1oetWLF~y=1eP;h(~W!J(EKd4K=p!T`?c(cG2kCH?!;L=y)c{96Hg}({sJ; z1Q}oxU;uX>;iIzk`k1MYkK)G2pXa-uTI~|`O)4lTScLpMcNH^pk$-E6RLmh>)u>!= z6!X0C#R_Q)eOPoR=Ol&PqQrQ9z0%>lU}FCC#7 z-bxUdzb;Z;;SWf>_KXA>oVymEQCS=~KsVm=c-K`wJCEX!KF*>7zaNhAj7_)~wTK2j zx}K9~yPf{wShx}lctVV~ZL|K!1dGYSb~C84&bN9A&j{~4Do6N{^CuJ3tdFX&8%uEi#uGG$!XSsVK37=EEVKNWIs2w-tZ;?9@nlfuVBW`_EZDell>PnLLcx3Uw_x0=7Hxczmwyg<4 zSt0bmgqeH^ITpMx)~rwGmk#~sN7_XqBe3#pDj0!?dipUqlxpx`&kPI=mkPuGU^d+N zJZE}lMiDFH&o#kqXTrwp>d%kCBE5fZtOqrfZHG(s`gR3D-P_k2D4a$}UueLyP9J=H z{z9a|K0SC$nS(1I%ZtY;_A6i@XBF0vp(Ff|uV_E!i|vVy2=H*!10?r>`FOH z+iktB(JWa^Y3I(WH5gDoU>wQ%`tuUviPN@u+tCoMSHGV_`wIgROg_;mVJ1O9rlxcJ z?c$0qooS{8+{P=mi6|;v<%5B`Z%JM;HIoA3xIbhUX{j$y+M%P^Js6=Q^ZQ2NW~ z*Al@HV%t6MI7%x&;TyO-|E+@{G~hrl^fBi{0D7Nm>0{j8gOE-i^_v@_>RYp!gP%H| zqYnT?TKY;uzV;MfVPWApMj17=l=g7Ww(d;a+<4NP7}g)Y-Q}kE2}|&EY69TbrdfPA znuv*WrqyBN@k=I8Uqw5!wQlU@l((HFr$jFgkDu)x#gdCV@$i>b;Mg#0(U+OhzK8tX z{#b*oXn-z$r7!e-&%m6^h0Wt5erIeCb8@zL1_Ok_Nc$oOWNqNjCMn5R4!Z?q zRBMEI1XUFD=<{Xt97lgN2zdWtCgkDfzHHlBO0Byvir72l!W?|Lc4C){w0y@zRQa_+ z@F1xH-0}qw>R5S|%P4qb-?llmU-PP?d*Ko&I6Hsh{k=~7k3}Dn)z#Hm4jwu8nK-_e zZRe1`+0rHLPiHIe7bRc^e8oVlZ|A0!f60o+?Tn$}<(?yU7*o{yyS#xM%Qh+H1Lv7@ zp)it?xevlv!k(6+mr<2*@3os7+UZjZcWqAE8F}Rj1xJMeMVD>IJ@fly>^dMra-CpF zE;uJ$70-UMW{ie%KJSs9D94?VY5Ab#@|35piFlg*imX+x=Pf0>kvI4BLW z+8K5e=AsC|TS@-W;N;%klCbLdY@$XHS4Wkyu_>=cNYiE9G2T-_<%600(Fd_s<~|LVWO;5W@8HV#@Z~ zd#i&^i{?R6BHyXA7`lnRBjKT;?i{Cs6p?H_uHym^B4Sss(m}r-=$+K7|$R-#VH|wsVrM{@LM{);rXCvUTE`Z^3d&FmJYRYk8 z?b<7Q3+lV^JDDLGB(6gR!^7@%TY#Bf3!w_S#86Ki*>^H~VHPjRSd7q0RdaJ|?~A|>6!WCa;XFm0#IC;M-d-8h+*WapuE>qX%2 zX-#lD+yBe)=We*BI6#Tp{QhLu|U=1XYG z8LQs@ruN+ClK{BREJqNH45Y=oJaR%n<%Dx5zI0EFcIt%b26u1owdg;p*1hZvgi9~+ zlG!v>iO;&W5uXC^-pMc?s2}!6gMf@6j3`naZBLps-@4pw3H<)cpR6$7bBGi*M92u3 zR9rJku(h<5LL(f%6O-wQ%G#Ub*0;x^M;ko%Vo_NhHP7z&_|gqd{@0?DtmiceU~bzY zKi=sW(S>fom(0}?@xLlpaEe|e=~deLGcmG;_dCCn*_kh6|F9njgD=N(crg%BxV^jN zf74nwcf%qZp1z`g^BiT5o3p)tgCzHAs%JrNOymK()j6WsBE-e}m!P56^VyLdG{KWg zyBCQ4@?E@=L3h%EmV4@et2lF=hFbK1;})--vERtTaj{(UclaB_NLLwCg-eU2{Fc^y ztK$3SlJUiWhI#BO^xM!MebWq(iVumIp)`!uqu5_wHRc{PM?<8)`*Av7`FYuhynFW= zkKmS+9X@1r?p(#g)WmfOx?RZ=D@c<${L?{$|>9d63~-f^2j%e zZggsGYJS&fC=P;*+^`y7d6mB(32(8PWPInFuz}NJt1o4$8d-yMK&EHVmhjvUhR` z2nA}F3vb+6P2Vj~fmXq>usfJ%8w)FI{sV*$J82&?mrgi^F#Fl#Usljv;I}bVkM=G@ zFJPAveaJ6Rt7iagYK`Y;wK$$1nlUgpOO?pwAXNTb22@<64DPn6U3Kd!YxX|UdEo)$ zQQ^T{mE)gi53kaMCs{Z$>gd4Q-V}s4dvzlgXzwol*-l7BbFKV!oxtXu3%Tm`!z5Zz zh0|q1Qz72oKmFIQU!62Wu3bf$xU6hm@Rj$Jr!AYly-ctEyx8EsLN19*7%!^~k3F-s zXL78&6z8ePkKs0mOXAxl7VFhMeKI49H<>s+nxDtteQPKd4O$}8C^MfIZ=`>Zx&#CR|YLYP`|>?ok7fZcVtPNX`?H#U6N?sFnfj^ zXhhB4=*f4kL>|MVIt2k*d`;kAV0^+axSc9xDfXd#!rPd&Ca3Kmy(=heVZzx6T6YOX z;ic;)>i4Ksto4*U9xGJ`Yy+Nj?x3~xXj)9nKE)$1xnGJXC+E^od#ah~=v`@^?+E1$q_zX{C00&6VM3qw~kjoZ=1+^PUR&+owf3^Bt>5 zl}v-!B<;NF5&p9iyQLE@otVdZgpEnBb6ns}T5m z%SX6#lEX~Ug7S=wn0hv_G;vG9_cQ0yx590{TDr|WHwq6KdzAFQ+PaIfgo4vCG>g)! z$Y0XjoHU>*5N`1x6f<~+Sx(UplCvp1vER+l-(SPs%j*}(-5mL@shXt9$%7m_k7Igz z&s)KleY=oKE)U4{wR|03(jCAy9xfahgGBdjX<&LHtnyeTo^^qv-6kWGdl#_**evyx zT$=^ocLzAz-{_uNczc14DIeSFqX#?Po}Q=)`|&+k=t*+Ofjcg8-_>t+WTFV7tq8}VvPwB+s*iEk>)GIQrqR!3DtbY{ds8T!J5%!poja070Z z49KNu>F7B4Cjjs=fROiFK$pb&ExgS8z)lrzzbg@tK6?SeR)~P>KJQybY7#xkEVYgP zy7?hZlO`uKk8zybXCQ~#W3y{l_=7AA^{xH=6Q_VwKCP|Qw%RXxccE&xIEpS`QUTuC zbyaXx=6cZ^!nynZNwrQjctZ^NUG=A8YBfpDdw$(L{G#9JEZdg#f5nmJIOnS93RmTW%tER~YX!XJ6cnaQ>j)rAUSwv{AK2c+eeRN~7i!)* zwG5|Eu_AJE-lDdg4)<3Kv)YdmL+u+>E6U3J8JD^Kgyea3-xAacn2%)&3EOCC;*(dk zS#J#wPpSQoQh^GoNRmVb-P+w?B*cL8^kq)}=z!h&q32d`NP~-_g#MCC5s5DMG zd8>&4^SW42UVav_g@Tt&fJRn?oP9%6{`7C|rh7$2g`{CSLJiCSH_bo9ty=$n8fz_~f^j8Q87VBnGvn9x1k}}$01H8UZAm-aqHCnO`6CQ3TM1%@ zdClI=Wj!(FF$Qj;n&f6yRX=SplzNCiejyYEzk)jhcO(w415=c~iM3gG1@~5(1BT<^ z%3nh8n%3bi>mvw=>YUYmR%0^extgx)QIeYrEt~a?$;-MFP?J6+9-+-v4N*h^x-l+j zU8Fbt&h0n3NkgNOchl^pP^fLuQud=g%TP{VS9i)%o{hn8QukltDES$iR*8~pgup&Q8XHK-7gM$W(xZdYcilsmElY5ltpeGAFZZHMyMFpbBHewtSkm4fw3K(niv@$_tkvseXsV$vbVQ3jl6U(O7HxsPY8<| zG&|Z})9o7WmPfE}^=qRy>p~iIRpee?$pa8hszX!8BHy4%m7mZ=9ok2J_0H3I=F;jU zY!1Qj`?qgD8(Sp(2NNI&6O(c#amocU@4x)+-LGd{*Fa~0O#H6puiL6}8v6SVyHa$O zZ+DGqZE&_=An~_4hk~nZ1-f;80j( z$N0KOo)non-_Dx~OJ6$DIj{L}f6ezpzL;)327JaZG zGC#IQe(frjVEel+uO9AIKFn-c)tyXVk_y|IFK&Ycp_6kQooz#7 z|JELZ{80w7QuEdnvJAJ(u2V!@!*O(7V(q`OD>U1w&IV2NabI>>`KW1TK_3G(27WyCB+eOn)Y;NH%M-^X3 zTkrL+pW}au=V7g;NunP>2HXpzpscClr?_W_lbH{-$19Wld$Q9VHy-2d|aZ9vw~-T&|W)t_N-N_>k_*u zzU32U=iP`Q&aZJ2hS1L87|wm?MOQ#ki}?ErXQf4%c(BD4hC7V@^2gL!UBAVt!JG$o z2O_tx=UPK9V+%2z7jSyxBS*4QvS^Cz9)`or^{Q}$T>Kl)n<*w!0)W=Q@ap**lAO!Q zFXNeq2acob`xj!Ur}_#Ubf^USJrQ^P37i5az!;k6hwTa-|I$E`jy!tbA{3}MRu-Wnr6YU z(B>T5!@$D-b{2!|dE68h6<`yM|Iy2!@%Z_VdJjDg;=53F|Eu5<>g$)c@_aiY( zba$qB&_YMrW?Fjst&d*2?6p7g0_uVHGw-aub0Q{oxfGB}8BE7-{JqPdlp$kY!wj;1 z-T+>one|KjF5PpMHdh`}uMwM`EDZ&QxQ<)V*p@uHDSLFRB}`HAt~;(3C=%NOF@Nsf0Q zy4(qZbmXAE>x7?-^*P;ad)@^arRhsPuoOW%urfy|ToUv!Il@O`&Dz(a+ZGQ8HrJ!US zlDD%Vx-yzbx>3LR^0AEsiz4VaU03cXq-h&+NUf{XmRX|%emMx$SxwuILHA~MMwJYW zq7T*1fUxDWPN_}c#j$=2rH5Qyvq`u!`xc0+mbl8`Vz&I~zBPK4cON@D7;5i_B8O%GD>1xrKswQJ|BC+4 zPI{o4mVYKywVG*sqi!5~-PJW$2v`3r+zQl<+g4eo>ZQc5lN+^7b zYOFH*A5>H<9YKamZ-2jCx|l(Jfp$_?Vu9lBdvU}33$L0JL+0$gF}vFe681c)x){L<#8=m^J|+PL&q zHFpYShKkgEpLq%&$z@`Nbzda?CygaEfg69o!>LOI16#BDNXO23B{FiOPWB26DIqmm z3)g!xt;JoOqELbi)EngjGle{rG%AS~i20}I{;}(nVSAW@#A+|%piVOrn()JZIewlE zCLWO-s^m$cWJh|qc4qpEgCIy=6rbENSRCp}Lzfg`{IIL;FK^R<*O&pF(pw&O9$~0&Xee=- zdrk(tMLm{$NguqWJ*?CfGrr%8t3p4aSJ{JsYrXIbdeMVN1bx)R`$ED2THydz=x&$pf5Iub&pp`Qtfx||pncY=>tFD-3wafSPd z*6EgS_{NlI&|qf zFOt^k|4-Y_Z)7&mQ}fd3bAAvY?K@YM@XpenKYu=Yvt9YH$zzq93~OB|QBhHn0WZlD z4l0V6wEXhzq%8)ZmQa{+OH)(Luly{vS+qh`fUJB7yqi0+=={y_=NCyD8uE{gn>J$~oPtQPGo>OH{)9h*ne!%Qq zIsaAmy3so_P>TA|U^XHjI)2a0%_yQDtxj_sl7 zF77a&yP5p2s|WVK1ji1L>MzY)m?*E{1LZILNd5Ed4$h>nid0495dl2!wH ziwv>OEH8gQ2{x%NO?`Be$*1vguWJANrIwXHEwEw!WAm}A|2jO(h^DXQzZq}Zl0_^d z)FX?OdM7+F?PM}|^o>Tq74jJf7`}7%rvJu~#3z8OQXtvL0Vv`)w8iR%R8sZz;FH|b zi5;@>8@un1;q& zoDnMvxV#SxNWzwv3I28J-P^a~kSJXF^J^tUU<5C3FA@y!&u_h+`wVqB0NZSFa7o^t zv_`44hOPGjjhe(}G03PRspKazsd!!8i3X=lMxXc6eeHeayi$cA@FC>ie~{|u*CM$j zPeTO!(HD%{)c2lYldKv89^-63bQzHc+j*Bx1JW5qo{ub43&98*w#z;Bk>qVin~$^m z#g>A1%`wI^n9$+yQLH7>t@7S&QBXK}3D-81JEx6+FwhXypx=#W*KJi$$SM-35$pD7 zus{I^C+V8JN{iHm9j_B{z*kln&8~2W>AR)gy!m>eqc7n7WA4W7%_S!}9)$B=A0}+p zD_ZlN3Jk1#0g3BGW(RU(lpMFXhn#+#3~8wt#R@8e$)xoh@(!+zrTQmd#evxP2v^1* z@>)6vk*77j(sa}oO+-n-s1dWOW0s-7n1Z8#n~F#u>PE6R9`G-TU)@ za9k4rSuL6cT9e>j>MbKwDLCGwUGq{i8TV@-|J* zTPv+VU6JvwzN^?0;#M@y>;!H&vQGwXre?!8F}*Xr?L-4#-%)TYE|{R z!XDkD4RNg1655$&B}!pYHwTef++sXi2~Vlv2;)^wEwD8Je7A2;7Ce}T;+3pgMvi2` zzeXgI)1o7GgVmaXpQh#+o4^~v>S3WhIkgtIt8x}cf9Xekf|ff)w}KU_HX%IqPFsG; zgvv&EA%2UWaH44XlpU1$`eoki_VJs>j*qX0@u#v@>*13y7_#)$)KLAe;|KK3v5K{_ zS`cu_s%{Vo_8O4tIqxD_GowlR`z%O>4IVbbMi(#hm#&;8y=m$D*d*GN$|kKhaM$MB zKKuL^Mq6{zw80C!ku_*?CFOQ~KR>^yiXcxzd$xy%QdVCyb6=l?Ug3_g3K-{A7Q9Is z=kN0T$|F3QiiRG!5$!ymy{%a*8_&CxaWLd=*uTf;&5f&y_ z&D3s+o?Bv~Ym~!~`l~Fy9HsUCazL?yhb8`ENkbo;F8dSm9kH-XeB%=q-g__Wg*ZiN z!$BIu?~UIALFW_$-o%B^;HcLmCMGsXMqf9_?Inqu!$B!AAySswCODjbTuJs0ahXt=S!)eu2tf;{=UB^& zP+wAe@iFl8O5woYADRE=^y-UjNly-utz_@1h34AmLZzdURNH^~eBN~9Jz3oF8=gJY z@`&4?%^l{h^HVz)d=twsZA$qZ_~6y&)$G;=d-o<~0w+(iE!2+&K!=~u z++6-u*uFEYg?YVt2f%3W=MW>55qq+af_CuN3)%^M` zh01zj;QFHWxOYfc18T4TAkyMun*tNb_Rd@v0}loFg$@;g|5~T%r;ekBES@IrRo5M4 ze=9qw`F?+EPh={tRbA?e)>3IZY2$ubeXf~9QAuePrY1F7Vh|6h4uWZfpp@ou41!XB zjs*P3SE;VH7R491;15UvxpSWXbRL#aSOS)iU!;G#V~xzgV?2aC;&W_$Mqen&yVuDv z=k#RmFDyRu+81FowGaksWgJU%LqU5iAYSb87)Tb!KxPKHXw~(WY*oiK{ zEx-YKZ5iGjuEw9hswWSk90fT!un5^b3TV=tT|DM>{rCS`d-{-u$yt3bwUNQ`F%OKG zA1!gaxBp3f%ONCWs{u5KUbO`u3gh}k=v~7dlx*PY`QnLjbYjT&wdFz2x?HtOh|^^& zd9|K;E#n)TGZb3>FRPI(bGbEWo1)@wV!6UJngNO6bs(KKQC%^;v-IZ=f8>OhxA$o^ zM4u}r7;gMmP~M{2R^evz?3A%Ts+z1sQecq+_*3jRvJ}}$ng2H z{X|v)OFldQT?)yKpX5bvm`cdhO)=OUVdWNUdL=?tmPOqT9SF(~^31pm!d- zwcQ?-PI&n(!O|`#+nROlSO_yzZ#r0ycYq69n_7wTy{yd4q3No_x{h_5&p^tLy!0fj z5e$-&lFFt{I@ujiZrwVJoB9<{bf?IP$GSZiBbS3(h()QT9kPylBtIt|pa^^DE5GR8n)cvNay-DCLf_SX{{bI3${+@9?F-6Z;gJlj1p&$)D zg6pI_V&LZHo{P@V=k@bGm!M50(bv5;(|v z8Yb20HB6mnSnn&p=FO?N7vkb|$VQ2B_c(mREfCsm87?BTH-+I4+!#6$aMZ(t)5XO_ ze|aIu`%&3;RaFntxK2aBtKi-^@kO>HBn(BUIdiS za_e~6Ykjq*TjP5tmQit+7xA8lERd~7r6)n4JE_W}^S&7*)=#inla46uEzWrsqsagU zP^A&}>}I{x>$II!>`SKst{0d~dy$^nw177~;2z#Dd67vsnCE=St7YkjO ztV@n1M+e%#m(8Em@})=SUBpAL1@2N3RKNRhMW0x09Ca4feQ}pECSGaRg1|4`mzsq2 zzcAnEycyqhGuh>Mrx(Y~tgk&?cTL2v0lzcub?lf5?;f!Dk{F!_EXDRtX;uDriYH!n z7ddKRkzZnUD*i&cen!Dx771=F+c2r_5e zi<zY#!`5+|rl5||w1o`e+1?iBf^B6weQFJxlj&*?+gB;)| zeWpkzi=5Dq5Ef`Y41)g_tiyJ={|O3D48id z0za$EWN^)~(a6`9g}q06;Ul*H^f}q(`S%)qL&H$PulZ*ku_76l{yxHL$~S9W=s zfiAvSRc-K63>;Ms{Kqez>rZ)kxf{-1h0}h2m6tjM%I@YJm>;4WY#`q!U%Ph=XmAbE zQc|^2>T_r`JfKVz7FlI;1yvT|@NcS*B9xU^&7bnD# z_dV0v?0EY_+Kt!{AUk7@8R*moyM-| z4~>Q04(CTfi}p8|;<|u4$9CQNqa1Ufsb3F@L{DR2uO)IFa94|}b>=z)}lZd|C zS(=QAM5GCd7QS3yVhEkHb@Hj{x1P@^3UQj=grxA@rSi1VX_&eRt10tPVQOb;%$MUoISG zgRdC~JLCi%I;8k>Vn@$G8grBQ!{0h??03B>4c;yW-pa9*v!Gy9^^h8!5hG4Uq5M(g z3#T{)XaWLlvfd{k!MXt;yw$@)e%}%qzqDHfb++7}gfp^l= z>kdE*RKYHB#{6svXQuKwoV`&$9 z%)VKvLjjXihv{J5ho z;>O-j#d_V~G1$KMIgJ~e1079!BGT>Yb>n1y`^uYs>?A_f5 zchdbyGIZ;H8?@20U0-9qn9wF4J80tvo5&iFPr#@ComJ(>C+!kWt>i30}? z?CY=mtHgBHU?H@*XI8uBe_}z9q&$zV*-JTBy57nm2iCt1Lc|f*V}euacFn{{E%qbu zcs~Hs5PpoTw!!V$N*g|O(ZmzQR)yxM>ja^$#F*rV8pXmFo=R3XGZ;2BR>1CdpM~_f zR<^{6&1SEw(JDF8?$mf=VQt+PgvVw*z+7IWB=ftmpNcGv@2`AyZG*l&{gPsyY(CLS znXIH;@%Ll&i-z;ldI|;JIpd0*EZ59}U?g#~w_p8ztLc{eOQnD*H&^5%!7H<7u7p3J znK8aoGZ-UBVFt~ve6}J~Sk>30XksKRggdkvr}1^8cczGYfyUK-;*_)xNCLOmlvY2_ zhDt`2MV&h+I+<#Ih#u4pHZXw&QaH}j5H;VeiY8mR|W-u;0pBj zdopG=Q@7%y5L6FDHa#BG{b8PLhGDsyygBWj`FRnzwZ^euGKyo2sFaY9k($ixxvBIBiDiyQA>dj4nQ-dc7A zpV^piP3CTw7m7;1{kJV{O?ybMA+v>ORmz$DxvTDQO=BgV82RzT-S7{Aui*?+ZNIAc zEiDMOTXfzITew1nRB;49RTAoB|-8I@SM=WxY z)F64z36l-64Q#(&y5+}sTRS?YyZi@utBPlWkS^wqIqIC)de)TvzFp38)rapR*gZ6Y zD>yC~UojCt!{JF`Vi#66aCCaTk1xqx+ugyh!cJJDy z)eBH@+F8xt^Zr8EDzGfoyA-IOc|X$B*7iB_g7FDLM(XbFj@Yz$uaf%rSa|Na%I`@-{0USdc`TdRk}TE|spk zL#)iC!DG8);~l4kd(lbD#jMgNdoKV8ZCWHg8xj=KIKXT!S{>n71p{4;gGT@0N2jqd z+#EAwsk3zRd2~tsB}+6?5kxqCP1U}lg^)xDDGSbOD72>P^93{t4a)NJ^UW^<1~iU> zPw})@g7;X_JfrC;i+P^qnuMC8>e5Qi8^%72L^LY% zlPK8cj(eMFLe*uf?tUJC0y{Ipk2%)o<^Kl5Q!}71+1I9X+ zYzz{x?N6X@Z!4=-e2$BHk2G}3@87?j5m2<_IbC>3?O>3EYPoC4YKKPEd{LOt%ZrQK zU3`j3UreE5<7;GlNMKSvjK1&V;V zt*8G9F-p0ss-AX06&407+|zwT6KV6UPbju2_5Jg;(AdZQi5R$VpilYnC$^GSJ{c&J z>ZxGza(%{Vg+*L-*A+gC&vy3OJ8QnbI^%=+si|mDU8$8lik>q?xg9zrtEJ5&F209z zQELORw7mHU(;R_Wb)R zN=zqO^WN7l5$>p-h?7lLw0z|2vI>ATteUSz2@~M;0M6s9`iF-2S=rb;pz;a&`0a3$mFC9^&dIh1KyhiawmI&_C#$xuY9Iul!|i z0g59S8?P69@X+z&zk|_c=m+AW3%l+1f`9Hn?%H8xrRt47q}t`oc%$7k@d)ECRXSy0^&9gF6QmvV7?hlZ5Goyl0RczoARMNpT9^SRY z5r{>90*513+f4Q+>;69vq?lkVA_E{Cjmx_IRmNzJg%?w?kFk@-*yXSNe~Bkg9JgTN zR*%l(hUHK<1TnWkgfzT)<<&yN2=D|?MKobvqV83*pH|_js?_$zfuUMJKtNwZT|K2v zaBO76uM|%Iz1l@jc5fZVKN4Ffn1I}IgyBZGJQ;27&D<-g;MnDxtL;UtxRBMFLP5EX=uRc zM5|bR1SXD_pkE)@!clS#4!TDOq-OkFma8MdQzde!d{kMT#DL7v;WYc{uci2uB`vDB zKL9=35tg~J#UHZ)Ubxlw9ZZl^Uu#-QyBU=%aY@+>|%dqg%Uz5)E2{S2Dy5Ur~d_JiJv|I literal 0 HcmV?d00001 diff --git a/aidial_interceptors_sdk/chat_completion/__init__.py b/aidial_interceptors_sdk/chat_completion/__init__.py new file mode 100644 index 0000000..700a5b0 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/__init__.py @@ -0,0 +1,3 @@ +from .adapter import interceptor_to_chat_completion +from .base import ChatCompletionInterceptor, ChatCompletionNoOpInterceptor +from .element_path import ElementPath diff --git a/aidial_interceptors_sdk/chat_completion/adapter.py b/aidial_interceptors_sdk/chat_completion/adapter.py new file mode 100644 index 0000000..363aa81 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/adapter.py @@ -0,0 +1,106 @@ +import json +import logging +from typing import Any, AsyncIterator, Type, cast + +from aidial_sdk.chat_completion import ChatCompletion as DialChatCompletion +from aidial_sdk.chat_completion import Request as DialRequest +from aidial_sdk.chat_completion import Response as DialResponse +from openai import AsyncStream +from openai.types.chat.chat_completion import ChatCompletion +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.dial_client import DialClient +from aidial_interceptors_sdk.utils.debug import debug_logging +from aidial_interceptors_sdk.utils.exceptions import ( + EarlyStreamExit, + dial_exception_decorator, +) +from aidial_interceptors_sdk.utils.reflection import call_with_extra_body +from aidial_interceptors_sdk.utils.streaming import ( + block_to_chunk, + handle_streaming_errors, + map_stream, + singleton_stream, +) + +_log = logging.getLogger(__name__) +_debug = _log.isEnabledFor(logging.DEBUG) + + +def interceptor_to_chat_completion( + cls: Type[ChatCompletionInterceptor], +) -> DialChatCompletion: + class Impl(DialChatCompletion): + @dial_exception_decorator + async def chat_completion( + self, request: DialRequest, response: DialResponse + ) -> None: + + dial_client = await DialClient.create( + api_key=request.api_key, + api_version=request.api_version, + ) + + interceptor = cls( + dial_client=dial_client, + response=response, + **request.original_request.path_params, + ) + + request_body = await request.original_request.json() + request_body = await debug_logging("request")( + interceptor.traverse_request + )(request_body) + + async def call_upstream( + request: dict, call_context: Any | None + ) -> AsyncIterator[dict]: + upstream_response = cast( + AsyncStream[ChatCompletionChunk] | ChatCompletion, + await call_with_extra_body( + dial_client.client.chat.completions.create, request + ), + ) + + if isinstance(upstream_response, ChatCompletion): + resp = upstream_response.to_dict() + if _debug: + _log.debug( + f"upstream response[{call_context}]: {json.dumps(resp)}" + ) + + chunk = block_to_chunk(resp) + stream = singleton_stream(chunk) + else: + + def on_upstream_chunk(chunk: ChatCompletionChunk) -> dict: + d = chunk.to_dict() + if _debug: + _log.debug( + f"upstream chunk[{call_context}]: {json.dumps(d)}" + ) + return d + + stream = map_stream(on_upstream_chunk, upstream_response) + + return handle_streaming_errors(stream) + + try: + await interceptor.on_stream_start() + + async for chunk in await interceptor.call_upstreams( + request_body, call_upstream + ): + if "error" in chunk.chunk: + await interceptor.on_stream_error(chunk) + else: + await interceptor.traverse_response_chunk(chunk) + + await interceptor.on_stream_end() + except EarlyStreamExit: + pass + + return Impl() diff --git a/aidial_interceptors_sdk/chat_completion/annotated_chunk.py b/aidial_interceptors_sdk/chat_completion/annotated_chunk.py new file mode 100644 index 0000000..5717eb7 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/annotated_chunk.py @@ -0,0 +1,8 @@ +from typing import Any + +from openai import BaseModel + + +class AnnotatedChunk(BaseModel): + chunk: dict + annotation: Any | None = None diff --git a/aidial_interceptors_sdk/chat_completion/base.py b/aidial_interceptors_sdk/chat_completion/base.py new file mode 100644 index 0000000..ed90cd2 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/base.py @@ -0,0 +1,50 @@ +from typing import Any, AsyncIterator, Callable, Coroutine + +from aidial_interceptors_sdk.chat_completion.annotated_chunk import ( + AnnotatedChunk, +) +from aidial_interceptors_sdk.chat_completion.request_handler import ( + RequestHandler, +) +from aidial_interceptors_sdk.chat_completion.response_handler import ( + ResponseHandler, +) +from aidial_interceptors_sdk.dial_client import DialClient + + +class ChatCompletionInterceptor(RequestHandler, ResponseHandler): + dial_client: DialClient + + async def call_upstreams( + self, + request: dict, + call_upstream: Callable[ + [dict, Any | None], Coroutine[Any, Any, AsyncIterator[dict]] + ], + ) -> AsyncIterator[AnnotatedChunk]: + async def iterator(): + call_context = None + async for chunk in await call_upstream(request, call_context): + yield AnnotatedChunk(chunk=chunk, annotation=call_context) + + return iterator() + + async def on_stream_start(self) -> None: + # TODO: it's probably worth to put all the chunks + # generated by this method into a separate list. + # And then merge them all with the first incoming chunk. + # Otherwise, we may end up with choice being open *before* + # its "assistant" role is reported. + pass + + async def on_stream_error(self, error: AnnotatedChunk) -> None: + self.send_chunk(error.chunk) + + async def on_stream_end(self) -> None: + # TODO: it's probably worth to withhold the last chunk generated by + # on_stream_chunk and merge it with all the chunks reported by on_stream_end. + pass + + +class ChatCompletionNoOpInterceptor(ChatCompletionInterceptor): + pass diff --git a/aidial_interceptors_sdk/chat_completion/element_path.py b/aidial_interceptors_sdk/chat_completion/element_path.py new file mode 100644 index 0000000..8403c21 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/element_path.py @@ -0,0 +1,46 @@ +from typing import Any, Self + +from aidial_sdk.pydantic_v1 import BaseModel + +from aidial_interceptors_sdk.chat_completion.index_mapper import IndexMapper + + +class ChoiceContext(BaseModel): + index: int + stage_index_mapper: IndexMapper[int] + + +class ElementPath(BaseModel): + # Only for responses + response_ctx: Any | None = None + choice_ctx: ChoiceContext | None = None + + # Only for requests + message_idx: int | None = None + + # Both for requests and responses + stage_idx: int | None = None + attachment_idx: int | None = None + + def with_response_ctx(self, response_ctx: Any) -> Self: + return self.copy(update={"response_ctx": response_ctx}) + + def with_choice_ctx(self, choice_ctx: ChoiceContext) -> Self: + return self.copy(update={"choice_ctx": choice_ctx}) + + def with_message_idx(self, message_idx: int) -> Self: + return self.copy(update={"message_idx": message_idx}) + + def with_stage_idx(self, stage_idx: int) -> Self: + return self.copy(update={"stage_idx": stage_idx}) + + def with_attachment_idx(self, attachment_idx: int) -> Self: + return self.copy(update={"attachment_idx": attachment_idx}) + + @property + def choice_idx(self) -> int | None: + return self.choice_ctx.index if self.choice_ctx else None + + @property + def choice_stage_index_mapper(self) -> IndexMapper[int] | None: + return self.choice_ctx.stage_index_mapper if self.choice_ctx else None diff --git a/aidial_interceptors_sdk/chat_completion/helpers.py b/aidial_interceptors_sdk/chat_completion/helpers.py new file mode 100644 index 0000000..ecb85eb --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/helpers.py @@ -0,0 +1,155 @@ +from typing import Any, Callable, Coroutine, List, TypeVar, overload + +from aidial_interceptors_sdk.utils.not_given import NOT_GIVEN, NotGiven + +T = TypeVar("T") +P = TypeVar("P") + + +@overload +async def traverse_dict_value( + path: P, + d: dict, + key: str, + on_value: Callable[ + [P, T | NotGiven | None], + Coroutine[Any, Any, T | NotGiven | None], + ], +) -> dict: ... + + +@overload +async def traverse_dict_value( + path: P, + d: NotGiven, + key: str, + on_value: Callable[ + [P, T | NotGiven | None], + Coroutine[Any, Any, T | NotGiven | None], + ], +) -> NotGiven: ... + + +@overload +async def traverse_dict_value( + path: P, + d: None, + key: str, + on_value: Callable[ + [P, T | NotGiven | None], + Coroutine[Any, Any, T | NotGiven | None], + ], +) -> None: ... + + +async def traverse_dict_value( + path: P, + d: dict | NotGiven | None, + key: str, + on_value: Callable[ + [P, T | NotGiven | None], + Coroutine[Any, Any, T | NotGiven | None], + ], +) -> dict | NotGiven | None: + if d is None or isinstance(d, NotGiven): + return d + + old_value = d.get(key, NOT_GIVEN) + new_value = await on_value(path, old_value) + + if new_value is NOT_GIVEN: + if old_value is NOT_GIVEN: + return d + else: + return {k: v for k, v in d.items() if k != key} + else: + return {**d, key: new_value} + + +@overload +async def traverse_required_dict_value( + path: P, + d: None, + key: str, + on_value: Callable[[P, T], Coroutine[Any, Any, T]], +) -> None: ... + + +@overload +async def traverse_required_dict_value( + path: P, + d: NotGiven, + key: str, + on_value: Callable[[P, T], Coroutine[Any, Any, T]], +) -> NotGiven: ... + + +@overload +async def traverse_required_dict_value( + path: P, + d: dict, + key: str, + on_value: Callable[[P, T], Coroutine[Any, Any, T]], +) -> dict: ... + + +async def traverse_required_dict_value( + path: P, + d: dict | NotGiven | None, + key: str, + on_value: Callable[[P, T], Coroutine[Any, Any, T]], +) -> dict | NotGiven | None: + if d is None or isinstance(d, NotGiven): + return d + + old_value = d.get(key) + + if old_value is None: + raise ValueError(f"Missing required key {key!r} in a dictionary") + + new_value = await on_value(path, old_value) + return {**d, key: new_value} + + +@overload +async def traverse_list( + create_elem_path: Callable[[int], P], + lst: NotGiven, + on_elem: Callable[[P, T], Coroutine[Any, Any, List[T] | T]], +) -> NotGiven: ... + + +@overload +async def traverse_list( + create_elem_path: Callable[[int], P], + lst: None, + on_elem: Callable[[P, T], Coroutine[Any, Any, List[T] | T]], +) -> None: ... + + +@overload +async def traverse_list( + create_elem_path: Callable[[int], P], + lst: List[T], + on_elem: Callable[[P, T], Coroutine[Any, Any, List[T] | T]], +) -> List[T]: ... + + +async def traverse_list( + create_elem_path: Callable[[int], P], + lst: List[T] | NotGiven | None, + on_elem: Callable[[P, T], Coroutine[Any, Any, List[T] | T]], +) -> List[T] | NotGiven | None: + if lst is None or isinstance(lst, NotGiven): + return lst + + ret: List[T] = [] + for idx, elem in enumerate(lst): + idx = elem.get("index", idx) if isinstance(elem, dict) else idx + elem = await on_elem(create_elem_path(idx), elem) + if isinstance(elem, list): + ret.extend(elem) + else: + ret.append(elem) + + return ret diff --git a/aidial_interceptors_sdk/chat_completion/index_mapper.py b/aidial_interceptors_sdk/chat_completion/index_mapper.py new file mode 100644 index 0000000..11def73 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/index_mapper.py @@ -0,0 +1,37 @@ +from typing import Dict, Generic, Hashable, Set, TypeVar + +from aidial_sdk.pydantic_v1 import BaseModel + +_Index = TypeVar("_Index", bound=Hashable) + + +class IndexMapper(BaseModel, Generic[_Index]): + """ + Used to maintain consistent mapping between indexed values in the incoming and outgoing streams, given that outgoing stream may include additional elements at fixed indices. + """ + + migrated: Dict[_Index, int] = {} + used_indices: Set[int] = set() + + fresh_index: int = 0 + + def reserve(self, index: int | None = None) -> int: + if index is None: + return self._get_fresh_index() + + if index in self.used_indices: + raise ValueError(f"Index {index} is already taken") + + self.used_indices.add(index) + return index + + def __call__(self, index: _Index) -> int: + if index not in self.migrated: + self.migrated[index] = self._get_fresh_index() + return self.migrated[index] + + def _get_fresh_index(self) -> int: + while self.fresh_index in self.used_indices: + self.fresh_index += 1 + self.used_indices.add(self.fresh_index) + return self.fresh_index diff --git a/aidial_interceptors_sdk/chat_completion/request_handler.py b/aidial_interceptors_sdk/chat_completion/request_handler.py new file mode 100644 index 0000000..945ea32 --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/request_handler.py @@ -0,0 +1,46 @@ +from typing import List + +from aidial_interceptors_sdk.chat_completion.element_path import ElementPath +from aidial_interceptors_sdk.chat_completion.helpers import ( + traverse_list, + traverse_required_dict_value, +) +from aidial_interceptors_sdk.chat_completion.request_message_handler import ( + RequestMessageHandler, +) + + +class RequestHandler(RequestMessageHandler): + async def on_request_message( + self, path: ElementPath, message: dict + ) -> List[dict]: + return [message] + + async def on_request_messages(self, messages: List[dict]) -> List[dict]: + return messages + + async def on_request(self, request: dict) -> dict: + return request + + async def traverse_request(self, r: dict) -> dict: + async def traverse_message( + path: ElementPath, message: dict + ) -> List[dict]: + message = await self.traverse_request_message(path, message) + return await self.on_request_message(path, message) + + async def traverse_messages( + path: ElementPath, messages: List[dict] + ) -> List[dict]: + messages = await traverse_list( + path.with_message_idx, messages, traverse_message + ) + return await self.on_request_messages(messages) + + path = ElementPath() + r = await traverse_required_dict_value( + path, r, "messages", traverse_messages + ) + r = await self.on_request(r) + + return r diff --git a/aidial_interceptors_sdk/chat_completion/request_message_handler.py b/aidial_interceptors_sdk/chat_completion/request_message_handler.py new file mode 100644 index 0000000..543f42b --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/request_message_handler.py @@ -0,0 +1,108 @@ +""" +Callbacks to handle messages in the chat completion request. +""" + +from abc import ABC +from typing import List + +from aidial_sdk.pydantic_v1 import BaseModel + +from aidial_interceptors_sdk.chat_completion.element_path import ElementPath +from aidial_interceptors_sdk.chat_completion.helpers import ( + traverse_dict_value, + traverse_list, +) +from aidial_interceptors_sdk.utils.not_given import NotGiven + + +class RequestMessageHandler(ABC, BaseModel): + class Config: + arbitrary_types_allowed = True + + async def on_request_stage( + self, path: ElementPath, stage: dict + ) -> List[dict] | dict: + return stage + + async def on_request_stages( + self, path: ElementPath, stages: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + return stages + + async def on_request_attachment( + self, path: ElementPath, attachment: dict + ) -> List[dict] | dict: + """ + Applied to individual attachments: every message and stage attachment + """ + + return attachment + + async def on_request_attachments( + self, path: ElementPath, attachments: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + """ + Applied to list of attachments: message attachments and stage attachments + """ + return attachments + + async def on_request_state( + self, path: ElementPath, state: dict | NotGiven | None + ) -> dict | NotGiven | None: + return state + + async def on_request_custom_content( + self, path: ElementPath, custom_content: dict | NotGiven | None + ) -> dict | NotGiven | None: + return custom_content + + async def traverse_request_message( + self, path: ElementPath, message: dict + ) -> dict: + + async def apply_on_attachments( + path: ElementPath, attachments: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + attachments = await traverse_list( + path.with_attachment_idx, + attachments, + self.on_request_attachment, + ) + return await self.on_request_attachments(path, attachments) + + async def apply_on_stage( + path: ElementPath, stage: dict + ) -> List[dict] | dict: + stage = await traverse_dict_value( + path, stage, "attachments", apply_on_attachments + ) + + if path.stage_idx is not None and path.choice_ctx is not None: + mapper = path.choice_ctx.stage_index_mapper + stage["index"] = mapper(path.stage_idx) + + return await self.on_request_stage(path, stage) + + async def apply_on_stages( + path: ElementPath, stages: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + stages = await traverse_list( + path.with_stage_idx, stages, apply_on_stage + ) + return await self.on_request_stages(path, stages) + + async def apply_on_custom_content( + path: ElementPath, cc: dict | NotGiven | None + ) -> dict | NotGiven | None: + cc = await traverse_dict_value( + path, cc, "state", self.on_request_state + ) + cc = await traverse_dict_value( + path, cc, "attachments", apply_on_attachments + ) + cc = await traverse_dict_value(path, cc, "stages", apply_on_stages) + return await self.on_request_custom_content(path, cc) + + return await traverse_dict_value( + path, message, "custom_content", apply_on_custom_content + ) diff --git a/aidial_interceptors_sdk/chat_completion/response_handler.py b/aidial_interceptors_sdk/chat_completion/response_handler.py new file mode 100644 index 0000000..7c657eb --- /dev/null +++ b/aidial_interceptors_sdk/chat_completion/response_handler.py @@ -0,0 +1,145 @@ +from typing import Dict, List + +from aidial_sdk.chat_completion import Response +from aidial_sdk.chat_completion.chunks import BaseChunk +from aidial_sdk.pydantic_v1 import PrivateAttr + +from aidial_interceptors_sdk.chat_completion.annotated_chunk import ( + AnnotatedChunk, +) +from aidial_interceptors_sdk.chat_completion.element_path import ( + ChoiceContext, + ElementPath, +) +from aidial_interceptors_sdk.chat_completion.helpers import ( + traverse_dict_value, + traverse_list, +) +from aidial_interceptors_sdk.chat_completion.index_mapper import IndexMapper +from aidial_interceptors_sdk.chat_completion.response_message_handler import ( + ResponseMessageHandler, +) +from aidial_interceptors_sdk.utils.dial_sdk import send_chunk_to_response +from aidial_interceptors_sdk.utils.not_given import NotGiven + + +class ResponseHandler(ResponseMessageHandler): + """ + Callbacks for handling chat completion responses. + + 1. the callbacks may mutate the response in place, + 2. the callbacks are applied in bottom-up order (the order of callbacks in the class definition reflects the order of application), + 3. a callback for a list element return a list of objects, which allows for adding or removing elements. Such callbacks have the type signature: `T -> List[T]`, + 4. a callback for a dictionary value returns an optional dictionary, which allows for adding or removing keys. Such callbacks have the type signature: `(T | NotGiven | None) -> (T | NotGiven | None)`. + 5. `on_response*` methods are expected to be overridden by subclasses. + 6. `traverse_response*` methods are not expected to be overridden by subclasses. + + The callbacks are intended to be used for: + 1. collecting information about the response, + 2. inplace modifying the response. + + Avoid it creating new entities in the response, creation of deeply nested + entities is hard if possible. + + E.g. adding a new stage in a response which doesn't have custom content is impossible, + because one would have to create "custom_content" field _before_ + creating the "stages" field. Bottom-up application order of callbacks + doesn't allow for that. + """ + + response: Response + + # NOTE: `_stage_indices = {}` isn't going to work, since + # the underscored field `_stage_indices` will be shared across + # all instances of the class. + _stage_indices: Dict[int, IndexMapper[int]] = PrivateAttr({}) + + def _get_stage_index_mapper(self, choice_idx: int) -> IndexMapper[int]: + if choice_idx not in self._stage_indices: + self._stage_indices[choice_idx] = IndexMapper() + return self._stage_indices[choice_idx] + + def reserve_stage_index(self, choice_idx: int) -> int: + return self._get_stage_index_mapper(choice_idx).reserve() + + def send_chunk(self, chunk: BaseChunk | dict): + return send_chunk_to_response(self.response, chunk) + + async def on_response_message( + self, path: ElementPath, message: dict | NotGiven | None + ) -> dict | NotGiven | None: + return message + + async def on_response_finish_reason( + self, path: ElementPath, finish_reason: str | NotGiven | None + ) -> str | NotGiven | None: + return finish_reason + + async def on_response_choice( + self, path: ElementPath, choice: dict + ) -> List[dict] | dict: + return choice + + async def on_response_choices( + self, choices: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + return choices + + # TODO: add path to the signature and to the rest of similar methods + async def on_response_usage( + self, usage: dict | NotGiven | None + ) -> dict | NotGiven | None: + return usage + + async def on_stream_chunk(self, chunk: dict) -> None: + self.send_chunk(chunk) + + async def traverse_response_chunk(self, ann_chunk: AnnotatedChunk) -> None: + r = ann_chunk.chunk + + async def traverse_message( + path: ElementPath, message: dict | NotGiven | None + ) -> dict | NotGiven | None: + if message is not None and not isinstance(message, NotGiven): + message = await self.traverse_response_message(path, message) + return await self.on_response_message(path, message) + + async def traverse_choice( + path: ElementPath, choice: dict + ) -> List[dict] | dict: + choice = await traverse_dict_value( + path, choice, "finish_reason", self.on_response_finish_reason + ) + choice = await traverse_dict_value( + path, choice, "delta", traverse_message + ) + return await self.on_response_choice(path, choice) + + async def traverse_choices( + path: ElementPath, choices: List[dict] | NotGiven | None + ) -> List[dict] | NotGiven | None: + def with_choice_ctx(choice_idx: int) -> ElementPath: + return path.with_choice_ctx( + ChoiceContext( + index=choice_idx, + stage_index_mapper=self._get_stage_index_mapper( + choice_idx + ), + ) + ) + + choices = await traverse_list( + with_choice_ctx, choices, traverse_choice + ) + return await self.on_response_choices(choices) + + async def traverse_response_usage( + path: ElementPath, usage: dict | NotGiven | None + ) -> dict | NotGiven | None: + return await self.on_response_usage(usage) + + path = ElementPath(response_ctx=ann_chunk.annotation) + r = await traverse_dict_value(path, r, "usage", traverse_response_usage) + r = await traverse_dict_value(path, r, "choices", traverse_choices) + + await self.on_stream_chunk(r) diff --git a/aidial_interceptors_sdk/dial_client.py b/aidial_interceptors_sdk/dial_client.py new file mode 100644 index 0000000..d1965bb --- /dev/null +++ b/aidial_interceptors_sdk/dial_client.py @@ -0,0 +1,51 @@ +from aidial_sdk.exceptions import invalid_request_error +from aidial_sdk.pydantic_v1 import BaseModel +from openai import AsyncAzureOpenAI + +from aidial_interceptors_sdk.utils.env import get_env +from aidial_interceptors_sdk.utils.http_client import get_http_client +from aidial_interceptors_sdk.utils.storage import FileStorage + +DIAL_URL = get_env("DIAL_URL") + + +class DialClient(BaseModel): + client: AsyncAzureOpenAI + storage: FileStorage + + class Config: + arbitrary_types_allowed = True + + @property + def dial_url(self) -> str: + return self.storage.dial_url + + @classmethod + async def create( + cls, api_key: str | None, api_version: str | None + ) -> "DialClient": + if not api_key: + raise invalid_request_error( + "The 'api-key' request header is missing" + ) + + client = AsyncAzureOpenAI( + azure_endpoint=DIAL_URL, + azure_deployment="interceptor", + api_key=api_key, + # NOTE: defaulting missing api-version to an empty string, because + # 1. openai library doesn't allow for a missing api-version parameter. + # A workaround for it would be a recreation of AsyncAzureOpenAI with the check disabled: + # https://github.com/openai/openai-python/blob/9850c169c4126fd04dc6796e4685f1b9e4924aa4/src/openai/lib/azure.py#L174-L177 + # which is really not worth it. + # 2. OpenAI adapter treats a missing api-version in the same way as an empty string and + # that's the only place where api-version has any meaning, so the query param modification is safe. + # https://github.com/epam/ai-dial-adapter-openai/blob/b462d1c26ce8f9d569b9c085a849206aad91becf/aidial_adapter_openai/app.py#L93 + api_version=api_version or "", + max_retries=0, + http_client=get_http_client(), + ) + + storage = FileStorage(dial_url=DIAL_URL, api_key=api_key) + + return cls(client=client, storage=storage) diff --git a/aidial_interceptors_sdk/embeddings/__init__.py b/aidial_interceptors_sdk/embeddings/__init__.py new file mode 100644 index 0000000..2e19a3d --- /dev/null +++ b/aidial_interceptors_sdk/embeddings/__init__.py @@ -0,0 +1,2 @@ +from .adapter import interceptor_to_embeddings_handler +from .base import EmbeddingsInterceptor, EmbeddingsNoOpInterceptor diff --git a/aidial_interceptors_sdk/embeddings/adapter.py b/aidial_interceptors_sdk/embeddings/adapter.py new file mode 100644 index 0000000..6b2c179 --- /dev/null +++ b/aidial_interceptors_sdk/embeddings/adapter.py @@ -0,0 +1,37 @@ +from typing import Type + +from fastapi import Request +from openai.types import CreateEmbeddingResponse + +from aidial_interceptors_sdk.dial_client import DialClient +from aidial_interceptors_sdk.embeddings.base import EmbeddingsInterceptor +from aidial_interceptors_sdk.utils.debug import debug_logging +from aidial_interceptors_sdk.utils.exceptions import dial_exception_decorator +from aidial_interceptors_sdk.utils.reflection import call_with_extra_body + + +def interceptor_to_embeddings_handler(cls: Type[EmbeddingsInterceptor]): + @dial_exception_decorator + async def _handler(request: Request) -> dict: + + dial_client = await DialClient.create( + api_key=request.headers.get("api-key", None), + api_version=request.query_params.get("api-version"), + ) + + interceptor = cls(dial_client=dial_client, **request.path_params) + + body = await request.json() + body = await debug_logging("request")(interceptor.modify_request)(body) + + response: CreateEmbeddingResponse = await call_with_extra_body( + dial_client.client.embeddings.create, body + ) + + resp = response.to_dict() + resp = await debug_logging("response")(interceptor.modify_response)( + resp + ) + return resp + + return _handler diff --git a/aidial_interceptors_sdk/embeddings/base.py b/aidial_interceptors_sdk/embeddings/base.py new file mode 100644 index 0000000..1f671ce --- /dev/null +++ b/aidial_interceptors_sdk/embeddings/base.py @@ -0,0 +1,48 @@ +import logging +from abc import ABC +from typing import List + +from aidial_sdk.pydantic_v1 import BaseModel + +from aidial_interceptors_sdk.dial_client import DialClient + +_log = logging.getLogger(__name__) + + +class EmbeddingsInterceptor(ABC, BaseModel): + class Config: + arbitrary_types_allowed = True + + dial_client: DialClient + + async def modify_input(self, input: str) -> str: + return input + + async def modify_embedding( + self, embedding: str | List[float] + ) -> str | List[float]: + return embedding + + async def modify_request(self, request: dict) -> dict: + if "input" in request: + input = request["input"] + if isinstance(input, str): + request["input"] = await self.modify_input(input) + elif isinstance(input, list): + if all(isinstance(item, str) for item in input): + request["input"] = [ + await self.modify_input(item) for item in input + ] + else: + _log.warning("Tokenized input isn't yet supported") + + return request + + async def modify_response(self, response: dict) -> dict: + for item in response.get("data") or []: + item["embedding"] = await self.modify_embedding(item["embedding"]) + return response + + +class EmbeddingsNoOpInterceptor(EmbeddingsInterceptor): + pass diff --git a/aidial_interceptors_sdk/utils/debug.py b/aidial_interceptors_sdk/utils/debug.py new file mode 100644 index 0000000..798ade8 --- /dev/null +++ b/aidial_interceptors_sdk/utils/debug.py @@ -0,0 +1,32 @@ +import json +import logging +from typing import Callable, Coroutine, TypeVar + +_log = logging.getLogger(__name__) +_debug = _log.isEnabledFor(logging.DEBUG) + +A = TypeVar("A") +B = TypeVar("B") + + +def debug_logging( + title: str, +) -> Callable[ + [Callable[[A], Coroutine[None, None, B]]], + Callable[[A], Coroutine[None, None, B]], +]: + def decorator( + fn: Callable[[A], Coroutine[None, None, B]] + ) -> Callable[[A], Coroutine[None, None, B]]: + if not _debug: + return fn + + async def _fn(a: A) -> B: + _log.debug(f"{title} old: {json.dumps(a)}") + b = await fn(a) + _log.debug(f"{title} new: {json.dumps(b)}") + return b + + return _fn + + return decorator diff --git a/aidial_interceptors_sdk/utils/dial_sdk.py b/aidial_interceptors_sdk/utils/dial_sdk.py new file mode 100644 index 0000000..45384a2 --- /dev/null +++ b/aidial_interceptors_sdk/utils/dial_sdk.py @@ -0,0 +1,31 @@ +""" +All kinds of logic which is a good candidate +to be moved eventually to the SDK itself. +""" + +from typing import Any, Dict + +from aidial_sdk.chat_completion import Response +from aidial_sdk.chat_completion.chunks import BaseChunk + + +class _UnstructuredChunk(BaseChunk): + data: Dict[str, Any] + + def __init__(self, data: Dict[str, Any]): + self.data = data + + def to_dict(self): + return self.data + + +def send_chunk_to_response(response: Response, chunk: BaseChunk | dict): + if isinstance(chunk, dict): + for choice in chunk.get("choices") or []: + if (index := choice.get("index")) is not None: + response._last_choice_index = max( + response._last_choice_index, index + 1 + ) + response._queue.put_nowait(_UnstructuredChunk(data=chunk)) + else: + response._queue.put_nowait(chunk) diff --git a/aidial_interceptors_sdk/utils/dict.py b/aidial_interceptors_sdk/utils/dict.py new file mode 100644 index 0000000..c9af0d3 --- /dev/null +++ b/aidial_interceptors_sdk/utils/dict.py @@ -0,0 +1,173 @@ +from typing import Any, Callable, Iterable, List, Mapping, TypeVar + + +def censor_ci_dict(d: Mapping[str, str], keys: List[str]) -> dict: + key_set = {k.lower() for k in keys} + return { + k: v if k.lower() not in key_set else "**********" for k, v in d.items() + } + + +def get_keys(d: dict, keys: Iterable[str]) -> dict: + return {k: d[k] for k in keys if k in d} + + +Path = List[str | int] +PathLike = Path | str + + +def _parse_path(path: str) -> Path: + ret: Path = [] + for segment in path.split("."): + if not segment: + raise ValueError("Invalid empty path segment.") + if segment[0] == "!": + ret.append(int(segment[1:])) + else: + ret.append(segment) + + return ret + + +def _to_path(path: Path | str) -> Path: + if isinstance(path, str): + return _parse_path(path) + else: + return path + + +def _update_at_path( + obj: Any, + path: Path, + acc_path: Path, + fn: Callable[[Path, Any], Any], + **kwargs: Any, +) -> Any: + """ + Immutable modification of a nested value in a dictionary or list. + + If a dict key is missing, then it's created, unless drill_down is False. + If list/tuple index is missing, then an error is raised. + Catch-all "*" index is supported for lists. + """ + + drill_down = kwargs.get("drill_down", False) + strict = kwargs.get("strict", True) + + if path == []: + return fn(acc_path, obj) + + key = path[0] + rest = path[1:] + + if isinstance(obj, dict): + value = obj.get(key, None) + if value is None and not drill_down: + return obj + else: + return { + **obj, + key: _update_at_path( + value, rest, acc_path + [key], fn, **kwargs + ), + } + + if isinstance(obj, list): + if key == "*": + indices = set(range(len(obj))) + else: + if isinstance(key, int): + index = key + elif strict: + raise ValueError(f"Invalid list index: {key!r}") + else: + return obj + + if index < 0 or index >= len(obj) and strict: + raise ValueError( + f"Invalid index ({index}) for a list of length {len(obj)}" + ) + indices = {index} + + return [ + ( + _update_at_path(elem, rest, acc_path + [i], fn, **kwargs) + if i in indices + else elem + ) + for i, elem in enumerate(obj) + ] + + if isinstance(obj, tuple): + if isinstance(key, int): + index = key + elif strict: + raise ValueError(f"Invalid tuple index: {key!r}") + else: + return obj + + if index < 0 or index >= len(obj) and strict: + raise ValueError( + f"Invalid index ({index}) for a tuple of length {len(obj)}" + ) + + return tuple( + ( + _update_at_path(elem, rest, acc_path + [i], fn, **kwargs) + if i == index + else elem + ) + for i, elem in enumerate(obj) + ) + + if obj is None: + if isinstance(key, int): + if strict: + raise ValueError("Cannot index into None") + else: + return obj + else: + if drill_down: + return { + key: _update_at_path( + obj, rest, acc_path + [key], fn, **kwargs + ) + } + else: + return obj + + if strict: + raise ValueError( + f"Cannot get a value at path {path} in a scalar value of type {type(obj).__name__}" + ) + else: + return obj + + +def update_at_path( + obj: Any, path: PathLike, fn: Callable[[Path, Any], Any], **kwargs +) -> Any: + return _update_at_path(obj, _to_path(path), [], fn, **kwargs) + + +def set_at_path(obj: dict, path: PathLike, value: Any, **kwargs) -> dict: + return update_at_path(obj, path, lambda path, _value: value, **kwargs) + + +T = TypeVar("T") + + +def get_at_path(obj: Any, path: PathLike) -> Any: + values = collect_at_path(obj, path) + return None if values == [] else values[-1] + + +def collect_at_path(obj: Any, path: PathLike) -> List[Any]: + ret = [] + + def fn(path, value): + ret.append(value) + return value + + update_at_path(obj, path, fn) + return ret diff --git a/aidial_interceptors_sdk/utils/env.py b/aidial_interceptors_sdk/utils/env.py new file mode 100644 index 0000000..57d821a --- /dev/null +++ b/aidial_interceptors_sdk/utils/env.py @@ -0,0 +1,22 @@ +import os +from typing import List, Optional + + +def get_env(name: str, err_msg: Optional[str] = None) -> str: + if name in os.environ: + val = os.environ.get(name) + if val is not None: + return val + + raise Exception(err_msg or f"{name} env variable is not set") + + +def get_env_bool(name: str, default: bool = False) -> bool: + return os.getenv(name, str(default)).lower() == "true" + + +def get_env_list(name: str, default: List[str] = []) -> List[str]: + value = os.getenv(name) + if value is None: + return default + return value.split(",") diff --git a/aidial_interceptors_sdk/utils/exceptions.py b/aidial_interceptors_sdk/utils/exceptions.py new file mode 100644 index 0000000..ac2df75 --- /dev/null +++ b/aidial_interceptors_sdk/utils/exceptions.py @@ -0,0 +1,78 @@ +import logging +from functools import wraps + +import openai +from aidial_sdk.exceptions import HTTPException as DialException +from aidial_sdk.exceptions import internal_server_error +from fastapi.responses import Response + +_log = logging.getLogger(__name__) + + +class EarlyStreamExit(Exception): + """ + Thrown when one needs to end the stream prematurely. + """ + + +def to_dial_exception(e: Exception) -> Exception: + """ + Converting certain interceptor-specific exceptions into DialException. + + The rest of the exceptions will be handled by the DIAL SDK. + """ + + if isinstance(e, openai.APIStatusError): + r = e.response + # FIXME: the headers aren't propagated, + # so we may miss something useful like Retry-After. + return DialException(r.text, r.status_code) + + if isinstance(e, openai.APITimeoutError): + return DialException("Request timed out", 504, "timeout") + + if isinstance(e, openai.APIConnectionError): + return DialException( + "Error communicating with OpenAI", 502, "connection" + ) + + return e + + +def to_error_response(e: Exception) -> Response: + if isinstance(e, DialException): + return e.to_fastapi_response() + else: + return internal_server_error(str(e)).to_fastapi_response() + + +def dial_exception_decorator_response(func): + @wraps(func) + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except Exception as e: + _log.exception( + f"caught exception: {type(e).__module__}.{type(e).__name__}" + ) + + dial_exception = to_dial_exception(e) + error_response = to_error_response(dial_exception) + _log.debug(f"error response: {error_response}") + return error_response + + return wrapper + + +def dial_exception_decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except Exception as e: + _log.exception( + f"caught exception: {type(e).__module__}.{type(e).__name__}" + ) + raise to_dial_exception(e) from e + + return wrapper diff --git a/aidial_interceptors_sdk/utils/http_client.py b/aidial_interceptors_sdk/utils/http_client.py new file mode 100644 index 0000000..7ccc2ca --- /dev/null +++ b/aidial_interceptors_sdk/utils/http_client.py @@ -0,0 +1,20 @@ +import functools + +import httpx + +# connect timeout and total timeout +DEFAULT_TIMEOUT = httpx.Timeout(600, connect=10) + +# Borrowed from openai._constants.DEFAULT_CONNECTION_LIMITS +DEFAULT_CONNECTION_LIMITS = httpx.Limits( + max_connections=1000, max_keepalive_connections=100 +) + + +@functools.cache +def get_http_client() -> httpx.AsyncClient: + return httpx.AsyncClient( + timeout=DEFAULT_TIMEOUT, + limits=DEFAULT_CONNECTION_LIMITS, + follow_redirects=True, + ) diff --git a/aidial_interceptors_sdk/utils/not_given.py b/aidial_interceptors_sdk/utils/not_given.py new file mode 100644 index 0000000..3cff9ef --- /dev/null +++ b/aidial_interceptors_sdk/utils/not_given.py @@ -0,0 +1,22 @@ +from typing import Literal + +from typing_extensions import override + + +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + Copied from https://github.com/openai/openai-python/blob/40f4cdb52a7494472c32e26c70f54bb41bb2bb57/src/openai/_types.py#L107-L133 + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NOT_GIVEN = NotGiven() diff --git a/aidial_interceptors_sdk/utils/reflection.py b/aidial_interceptors_sdk/utils/reflection.py new file mode 100644 index 0000000..2ec3b59 --- /dev/null +++ b/aidial_interceptors_sdk/utils/reflection.py @@ -0,0 +1,43 @@ +import inspect +from typing import Any, Callable, Coroutine, TypeVar + +from aidial_sdk.exceptions import invalid_request_error + +T = TypeVar("T") + + +async def call_with_extra_body( + func: Callable[..., Coroutine[Any, Any, T]], arg: dict +) -> T: + if has_kwargs_argument(func): + return await func(**arg) + + expected_args = set(inspect.signature(func).parameters.keys()) + actual_args = set(arg.keys()) + + extra_args = actual_args - expected_args + + if extra_args and "extra_body" not in expected_args: + raise invalid_request_error( + f"Unrecognized request argument supplied: {extra_args}." + ) + + arg["extra_body"] = arg.get("extra_body") or {} + + for extra_arg in extra_args: + arg["extra_body"][extra_arg] = arg[extra_arg] + del arg[extra_arg] + + return await func(**arg) + + +def has_kwargs_argument(func: Callable[..., Coroutine[Any, Any, Any]]) -> bool: + """ + Determines if the given function accepts a variable keyword argument (**kwargs). + """ + signature = inspect.signature(func) + for param in signature.parameters.values(): + if param.kind == inspect.Parameter.VAR_KEYWORD: + print(param) + return True + return False diff --git a/aidial_interceptors_sdk/utils/storage.py b/aidial_interceptors_sdk/utils/storage.py new file mode 100644 index 0000000..edb4070 --- /dev/null +++ b/aidial_interceptors_sdk/utils/storage.py @@ -0,0 +1,165 @@ +import io +import logging +import mimetypes +from typing import Mapping, Optional, TypedDict +from urllib.parse import urljoin + +import aiohttp +from aidial_sdk.pydantic_v1 import BaseModel + +_log = logging.getLogger(__name__) + + +class FileMetadata(TypedDict): + name: str + parentPath: str + bucket: str + url: str + + +class Bucket(TypedDict): + bucket: str + appdata: str | None # Users do not have appdata + + +class AccessDeniedError(Exception): + def __init__(self, url: str): + super().__init__(f"Access denied: {url!r}") + self.url = url + + +class FileStorage(BaseModel): + dial_url: str + api_key: str + + bucket: Optional[Bucket] = None + + @property + def headers(self) -> Mapping[str, str]: + return {"api-key": self.api_key} + + async def get_bucket(self, session: aiohttp.ClientSession) -> Bucket: + if self.bucket is not None: + return self.bucket + + _log.debug(f"retrieving bucket for {self.dial_url!r}") + + async with session.get( + f"{self.dial_url}/v1/bucket", + headers=self.headers, + ) as response: + response.raise_for_status() + bucket = await response.json() + self.bucket = bucket + _log.debug(f"bucket: {self.bucket}") + return bucket + + @staticmethod + def _to_form_data( + filename: str, content_type: str | None, content: bytes + ) -> aiohttp.FormData: + data = aiohttp.FormData() + data.add_field( + "file", + io.BytesIO(content), + filename=filename, + content_type=content_type, + ) + return data + + async def is_accessible( + self, url: str, session: aiohttp.ClientSession + ) -> bool: + try: + await self._get_metadata(url, session) + _log.debug(f"file is accessible: url={url!r}") + return True + except AccessDeniedError: + _log.debug(f"file isn't accessible: url={url!r}") + return False + + def to_metadata_url(self, url: str) -> str: + """ + The file metadata URL given url like: files/BUCKET/foo/baz/document.pdf + """ + metadata_url = f"{self.dial_url}/v1/metadata/" + return urljoin(metadata_url, url, allow_fragments=True) + + async def _get_metadata( + self, + url: str, + session: aiohttp.ClientSession, + ) -> dict: + metadata_url = self.to_metadata_url(url) + + _log.debug(f"retrieving metadata: file={url!r}, url={metadata_url!r}") + + async with session.get(metadata_url, headers=self.headers) as response: + if not response.ok: + match response.status: + case 403: + raise AccessDeniedError(url) + case _: + response.raise_for_status() + + metadata = await response.json() + + _log.debug( + f"retrieved metadata: file={url!r}, url={metadata_url!r}, metadata={metadata}" + ) + + return metadata + + async def upload_file( + self, + filename: str, + content_type: str | None, + content: bytes, + session: aiohttp.ClientSession, + ) -> FileMetadata: + bucket = await self.get_bucket(session) + appdata = bucket["appdata"] + ext = (content_type and mimetypes.guess_extension(content_type)) or "" + url = f"{self.dial_url}/v1/files/{appdata}/{filename}{ext}" + return await self.upload(url, content_type, content) + + async def upload( + self, url: str, content_type: str | None, content: bytes + ) -> FileMetadata: + if self.to_dial_url(url) is None: + raise ValueError(f"URL isn't DIAL url: {url!r}") + url = self.to_abs_url(url) + + data = FileStorage._to_form_data(url, content_type, content) + async with aiohttp.ClientSession() as session: + async with session.put( + url=url, + data=data, + headers=self.headers, + ) as response: + response.raise_for_status() + meta = await response.json() + _log.debug(f"uploaded file: url={url!r}, metadata={meta}") + return meta + + def to_dial_url(self, link: str) -> str | None: + url = self.to_abs_url(link) + base_url = f"{self.dial_url}/v1/" + if url.startswith(base_url): + return url.removeprefix(base_url) + return None + + def to_abs_url(self, link: str) -> str: + base_url = f"{self.dial_url}/v1/" + ret = urljoin(base_url, link) + return ret + + async def download(self, url: str) -> bytes: + if self.to_dial_url(url) is None: + raise ValueError(f"URL isn't DIAL url: {url!r}") + url = self.to_abs_url(url) + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=self.headers) as response: + response.raise_for_status() + return await response.read() diff --git a/aidial_interceptors_sdk/utils/streaming.py b/aidial_interceptors_sdk/utils/streaming.py new file mode 100644 index 0000000..113fc5d --- /dev/null +++ b/aidial_interceptors_sdk/utils/streaming.py @@ -0,0 +1,142 @@ +import logging +from typing import ( + Any, + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, + List, + Optional, + TypeVar, + assert_never, +) + +import aiostream +import openai +from aidial_sdk.exceptions import HTTPException as DialException + +_log = logging.getLogger(__name__) + +_T = TypeVar("_T") +_V = TypeVar("_V") + + +async def handle_streaming_errors( + stream: AsyncIterator[dict], +) -> AsyncIterator[dict]: + + try: + async for chunk in stream: + yield chunk + except openai.APIError as e: + _log.error(f"error during steaming: {e.body}") + + display_message = None + if e.body is not None and isinstance(e.body, dict): + display_message = e.body.get("display_message", None) + + yield DialException( + message=e.message, + type=e.type, + param=e.param, + code=e.code, + display_message=display_message, + ).json_error() + + +# TODO: add to SDK as a inverse of cleanup_indices +def _add_indices(chunk: Any) -> Any: + if isinstance(chunk, list): + ret = [] + for idx, elem in enumerate(chunk, start=1): + if isinstance(elem, dict) and "index" not in elem: + elem = {**elem, "index": idx} + ret.append(_add_indices(elem)) + return ret + + if isinstance(chunk, dict): + return {key: _add_indices(value) for key, value in chunk.items()} + + return chunk + + +# TODO: add to SDK as an inverse of merge_chunks +def block_to_chunk(response: dict) -> dict: + for choice in response["choices"]: + choice["delta"] = choice["message"] + del choice["message"] + _add_indices(choice["delta"]) + return response + + +async def async_iterator_to_generator( + iterator: AsyncIterator[_T], +) -> AsyncGenerator[_T, None]: + async for item in iterator: + yield item + + +async def map_stream( + func: Callable[[_T], Optional[_V]], iterator: AsyncIterator[_T] +) -> AsyncIterator[_V]: + async for item in iterator: + new_item = func(item) + if new_item is not None: + yield new_item + + +async def aflatmap_stream( + func: Callable[ + [_T], + Awaitable[ + Iterable[_V] | Iterator[_V] | AsyncIterable[_V] | AsyncIterator[_V] + ] + | AsyncIterable[_V] + | AsyncIterator[_V], + ], + iterator: AsyncIterator[_T], +) -> AsyncIterator[_V]: + async for item in iterator: + iter = func(item) + if isinstance(iter, Awaitable): + iter = await iter + if isinstance(iter, (Iterable, Iterator)): + for item in iter: + yield item + elif isinstance(iter, (AsyncIterable, AsyncIterator)): + async for item in iter: + yield item + else: + assert_never(iter) + + +async def aextend_stream( + iterator: AsyncIterator[_T], func: Callable[[], AsyncIterator[_T]] +) -> AsyncIterator[_T]: + async for item in iterator: + yield item + async for item in func(): + yield item + + +async def amap_stream( + func: Callable[[_T], Awaitable[Optional[_V]]], iterator: AsyncIterator[_T] +) -> AsyncIterator[_V]: + async for item in iterator: + new_item = await func(item) + if new_item is not None: + yield new_item + + +async def singleton_stream(item: _T) -> AsyncIterator[_T]: + yield item + + +async def join_iterators(iters: List[AsyncIterator[_T]]) -> AsyncIterator[_T]: + combine = aiostream.stream.merge(*iters) + # FIXME: UserWarning: Streamer is iterated outside of its context + async for item in combine: + yield item diff --git a/examples/app.py b/examples/app.py new file mode 100644 index 0000000..77d7b7e --- /dev/null +++ b/examples/app.py @@ -0,0 +1,28 @@ +from aidial_sdk import DIALApp +from aidial_sdk.telemetry.types import TelemetryConfig + +from aidial_interceptors_sdk.chat_completion import ( + interceptor_to_chat_completion, +) +from aidial_interceptors_sdk.embeddings import interceptor_to_embeddings_handler +from examples.interceptor.registry import ( + chat_completion_interceptors, + embeddings_interceptors, +) +from examples.utils.log_config import configure_loggers + +app = DIALApp( + description="Examples of DIAL interceptors", + telemetry_config=TelemetryConfig(), + add_healthcheck=True, +) + +configure_loggers() + +for id, cls in embeddings_interceptors.items(): + app.post(f"/openai/deployments/{id}/embeddings")( + interceptor_to_embeddings_handler(cls) + ) + +for id, cls in chat_completion_interceptors.items(): + app.add_chat_completion(id, interceptor_to_chat_completion(cls)) diff --git a/examples/interceptor/chat_completion/__init__.py b/examples/interceptor/chat_completion/__init__.py new file mode 100644 index 0000000..ed07329 --- /dev/null +++ b/examples/interceptor/chat_completion/__init__.py @@ -0,0 +1,20 @@ +from examples.interceptor.chat_completion.blacklisted_words import ( + BlacklistedWordsInterceptor, +) +from examples.interceptor.chat_completion.cache import CachingInterceptor +from examples.interceptor.chat_completion.image_watermark import ( + ImageWatermarkInterceptor, +) +from examples.interceptor.chat_completion.pii_anonymiser import ( + PIIAnonymizerInterceptor, +) +from examples.interceptor.chat_completion.pirate import PirateInterceptor +from examples.interceptor.chat_completion.reject_external_links import ( + RejectExternalLinksInterceptor, +) +from examples.interceptor.chat_completion.replicator import ( + ReplicatorInterceptor, +) +from examples.interceptor.chat_completion.statistics_reporter import ( + StatisticsReporterInterceptor, +) diff --git a/examples/interceptor/chat_completion/blacklisted_words.py b/examples/interceptor/chat_completion/blacklisted_words.py new file mode 100644 index 0000000..20a2ca4 --- /dev/null +++ b/examples/interceptor/chat_completion/blacklisted_words.py @@ -0,0 +1,42 @@ +from typing import List + +from aidial_sdk.exceptions import invalid_request_error +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.utils.not_given import NotGiven + + +class BlacklistedWordsInterceptor(ChatCompletionInterceptor): + BLACKLIST = {"hello", "world"} + + total_content: str = "" + + def _validate_content_in(self, entity: str, content: str): + for word in self.BLACKLIST: + if word.lower() in content.lower(): + error_message = ( + f"The chat completion {entity} contains a blacklisted word." + ) + raise invalid_request_error( + message=error_message, + display_message=error_message, + ) + + @override + async def on_request_message(self, path, message: dict) -> List[dict]: + content = message.get("content") or "" + self._validate_content_in("request", content) + return [message] + + @override + async def on_response_message( + self, path, message: dict | NotGiven | None + ) -> dict | NotGiven | None: + if isinstance(message, dict): + content = message.get("content") or "" + self.total_content += content + self._validate_content_in("response", self.total_content) + return message diff --git a/examples/interceptor/chat_completion/cache.py b/examples/interceptor/chat_completion/cache.py new file mode 100644 index 0000000..956ec21 --- /dev/null +++ b/examples/interceptor/chat_completion/cache.py @@ -0,0 +1,67 @@ +import json +import logging +import time +import uuid + +from aidial_sdk.utils.merge_chunks import merge +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.utils.exceptions import EarlyStreamExit +from examples.utils.lru_cache import LRUCache + +_log = logging.getLogger(__name__) + +_MAX_CACHE_SIZE = 1000 +_LRU_CACHE = LRUCache[str, dict](maxsize=_MAX_CACHE_SIZE) + + +def _request_to_key(request: dict) -> str: + return json.dumps(request, sort_keys=True, separators=(",", ":")) + + +def _merge_chunks(chunk1: dict, chunk2: dict) -> dict: + # Avoid merging top-level keys + for key in ["id", "created", "model", "object", "system_fingerprint"]: + chunk1[key] = chunk1.get(key) or chunk2.get(key) + if key in chunk2: + del chunk2[key] + + return merge(chunk1, chunk2) + + +class CachingInterceptor(ChatCompletionInterceptor): + request_key: str = "" + response_merged: dict = {} + + @override + async def on_request(self, request: dict) -> dict: + self.request_key = _request_to_key(request) + return request + + @override + async def on_stream_start(self) -> None: + cached_response = _LRU_CACHE.lookup(self.request_key) + if cached_response is not None: + cached_response["id"] = "chatcmpl-" + str(uuid.uuid4()) + cached_response["created"] = int(time.time()) + self.send_chunk(cached_response) + _log.debug("Cache hit") + raise EarlyStreamExit("Cache hit") + + _log.debug("Cache miss") + + @override + async def on_stream_chunk(self, chunk: dict) -> None: + self.response_merged = _merge_chunks(self.response_merged, chunk) + self.send_chunk(chunk) + + @override + async def on_stream_end(self) -> None: + del self.response_merged["id"] + del self.response_merged["created"] + + _LRU_CACHE.save(self.request_key, self.response_merged) + _log.debug("Saved to cache") diff --git a/examples/interceptor/chat_completion/image_watermark.py b/examples/interceptor/chat_completion/image_watermark.py new file mode 100644 index 0000000..a427e32 --- /dev/null +++ b/examples/interceptor/chat_completion/image_watermark.py @@ -0,0 +1,52 @@ +import base64 + +from aidial_sdk.exceptions import invalid_request_error +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from examples.utils.watermark import stamp_watermark + + +class ImageWatermarkInterceptor(ChatCompletionInterceptor): + + @override + async def on_response_attachment(self, path, attachment: dict) -> dict: + ty = attachment.get("type") + + if ty == "image/jpeg": + format = "JPEG" + elif ty == "image/png": + format = "PNG" + else: + return attachment + + url = attachment.get("url") + if url is not None: + dial_url = self.dial_client.storage.to_dial_url(url) + if dial_url is None: + return attachment + + data = await self.dial_client.storage.download(url) + data = stamp_watermark(data, format) + + # overwrite the original image + await self.dial_client.storage.upload(url, ty, data) + + data = attachment.get("data") + if data is not None: + try: + bytes = base64.b64decode(data) + except Exception: + raise invalid_request_error( + "Attachment data isn't base64 encoded", + ) + + bytes = stamp_watermark(bytes, format) + attachment = { + **attachment, + "data": base64.b64encode(bytes).decode("utf-8"), + } + + return attachment diff --git a/examples/interceptor/chat_completion/pii_anonymiser/__init__.py b/examples/interceptor/chat_completion/pii_anonymiser/__init__.py new file mode 100644 index 0000000..02adf2c --- /dev/null +++ b/examples/interceptor/chat_completion/pii_anonymiser/__init__.py @@ -0,0 +1,3 @@ +from examples.interceptor.chat_completion.pii_anonymiser.impl import ( + PIIAnonymizerInterceptor, +) diff --git a/examples/interceptor/chat_completion/pii_anonymiser/impl.py b/examples/interceptor/chat_completion/pii_anonymiser/impl.py new file mode 100644 index 0000000..d8b0889 --- /dev/null +++ b/examples/interceptor/chat_completion/pii_anonymiser/impl.py @@ -0,0 +1,130 @@ +from collections import defaultdict +from typing import Dict, List + +from aidial_sdk.chat_completion import Stage +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.utils.env import get_env_list +from examples.interceptor.chat_completion.pii_anonymiser.spacy_anonymizer import ( + DEFAULT_LABELS_TO_REDACT, + SpacyAnonymizer, +) +from examples.utils.markdown import MarkdownTable + +PII_ANONYMIZER_LABELS_TO_REDACT = get_env_list( + "PII_ANONYMIZER_LABELS_TO_REDACT", DEFAULT_LABELS_TO_REDACT +) + + +class PIIAnonymizerInterceptor(ChatCompletionInterceptor): + anonymizer: SpacyAnonymizer = SpacyAnonymizer( + labels_to_redact=PII_ANONYMIZER_LABELS_TO_REDACT + ) + + # Request data + request_n: int = 0 + anonymized_request: str = "" + + # Per-choice response data + original_response_stages: Dict[int, Stage] = {} + content_buffers: Dict[int, str] = defaultdict(str) + + @override + async def on_request_messages(self, messages: List[dict]) -> List[dict]: + # Collect replacement dictionary first across all messages + for message in messages: + self.anonymizer.anonymize(message.get("content") or "") + + # Then apply the replacements + if not self.anonymizer.is_empty(): + chat_table = MarkdownTable( + title="Anonymized chat history", headers=["Role", "Content"] + ) + + for message in messages: + if message.get("content"): + message["content"] = self.anonymizer.anonymize( + message["content"] + ) + + content = self.anonymizer.highlight_anonymized_entities( + message["content"] + ) + + role = (message["role"] or "").upper() + chat_table.add_row([role, content]) + + self.anonymized_request += ( + chat_table.to_markdown() + + self.anonymizer.replacements_to_markdown_table() + ) + + return messages + + @override + async def on_request(self, request: dict) -> dict: + self.request_n = request.get("n") or 1 + return request + + @override + async def on_stream_start(self) -> None: + for choice_idx in range(self.request_n): + with Stage( + self.response._queue, + choice_idx, + self.reserve_stage_index(choice_idx), + "Anonymized request", + ) as stage: + stage.append_content(self.anonymized_request) + + self.original_response_stages[choice_idx] = Stage( + self.response._queue, + choice_idx, + self.reserve_stage_index(choice_idx), + "Original response", + ) + self.original_response_stages[choice_idx].open() + + @override + async def on_stream_chunk(self, chunk: dict) -> None: + # NOTE: re-chunking invalidates streaming usage reported by the upstream model + + assert self.original_response_stages is not None + + for choice in chunk.get("choices") or []: + choice_idx = choice.get("index") or 0 + + content = (choice.get("delta") or {}).get("content") or "" + finish_reason = choice.get("finish_reason") + + if content: + self.original_response_stages[choice_idx].append_content( + content + ) + + buffer = self.content_buffers[choice_idx] + content + + br_open = buffer.count("[") + br_closed = buffer.count("]") + + if br_open <= br_closed or finish_reason: + choice["delta"]["content"] = self.anonymizer.deanonymize( + buffer + ) + buffer = "" + else: + choice["delta"]["content"] = "" + + self.content_buffers[choice_idx] = buffer + + self.send_chunk(chunk) + + async def on_stream_end(self) -> None: + for choice_idx in range(self.request_n): + if stage := self.original_response_stages.get(choice_idx): + if content := self.content_buffers[choice_idx]: + stage.append_content(content) + stage.close() diff --git a/examples/interceptor/chat_completion/pii_anonymiser/spacy_anonymizer.py b/examples/interceptor/chat_completion/pii_anonymiser/spacy_anonymizer.py new file mode 100644 index 0000000..a8cfd3a --- /dev/null +++ b/examples/interceptor/chat_completion/pii_anonymiser/spacy_anonymizer.py @@ -0,0 +1,111 @@ +import re +from collections import defaultdict +from typing import Dict, List, Optional + +from aidial_sdk.pydantic_v1 import BaseModel +from spacy import load + +from examples.utils.markdown import MarkdownTable + +_PIPELINE = load("en_core_web_sm") + +DEFAULT_LABELS_TO_REDACT = [ + "PERSON", + "ORG", + "GPE", # Geo-political entity + "PRODUCT", +] + + +class Replacement(BaseModel): + entity_type: str + idx: int + + def print(self): + return f"[{self.entity_type}-{self.idx}]" + + @classmethod + def parse(cls, text: str) -> Optional["Replacement"]: + regexp = r"\[(\w+)-(\d+)\]" + match = re.match(regexp, text) + if match: + return cls(entity_type=match.group(1), idx=int(match.group(2))) + return None + + +class SpacyAnonymizer(BaseModel): + labels_to_redact: List[str] = DEFAULT_LABELS_TO_REDACT + + replacements: Dict[str, str] = {} + """ + Mapping from original text to anonymized text, e.g. + {"John Doe": "[PERSON-1]"} + """ + + types: Dict[str, int] = defaultdict(int) + """ + Counting the number of replacements for each type, e.g. + {"PERSON": 2, "ORG": 1} + """ + + def _get_replacement(self, text: str, text_type: str) -> str: + if text not in self.replacements: + self.types[text_type] += 1 + text_idx = self.types[text_type] + self.replacements[text] = f"[{text_type}-{text_idx}]" + + return self.replacements[text] + + def _is_replacement(self, text: str, start: int, end: int) -> bool: + return bool( + Replacement.parse(text[start:end]) + or Replacement.parse( + text[max(0, start - 1) : min(end + 1, len(text))] + ) + ) + + def anonymize(self, text: str) -> str: + doc = _PIPELINE(text) + redacted = [] + idx = 0 + + for ent in doc.ents: + redacted.append(doc.text[idx : ent.start_char]) + if ( + ent.label_ in self.labels_to_redact + and not self._is_replacement( + doc.text, ent.start_char, ent.end_char + ) + ): + redacted += self._get_replacement(ent.text, ent.label_) + else: + redacted.append(doc.text[ent.start_char : ent.end_char]) + idx = ent.end_char + + redacted.append(doc.text[idx:]) + + return "".join(redacted) + + def is_empty(self) -> bool: + return not bool(self.replacements) + + def replacements_to_markdown_table(self) -> str: + table = MarkdownTable( + title="Anonymized entities", + headers=["Original", "Anonymized"], + ) + for k, v in self.replacements.items(): + table.add_row([k, v]) + return table.to_markdown() + + def highlight_anonymized_entities(self, text: str) -> str: + for v in self.replacements.values(): + text = text.replace(v, f"**{v}**") + return text + + def deanonymize(self, text: str) -> str: + for k, v in self.replacements.items(): + text = text.replace(v, k) + # For cases when the model doesn't respect brackets + text = text.replace(v[1:-1], k) + return text diff --git a/examples/interceptor/chat_completion/pirate.py b/examples/interceptor/chat_completion/pirate.py new file mode 100644 index 0000000..dd0faae --- /dev/null +++ b/examples/interceptor/chat_completion/pirate.py @@ -0,0 +1,18 @@ +from typing import List + +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) + + +class PirateInterceptor(ChatCompletionInterceptor): + @override + async def on_request_messages(self, messages: List[dict]) -> List[dict]: + instruction = "Reply as a pirate." + if len(messages) > 0 and messages[0]["role"] == "system": + messages[0]["content"] = messages[0]["content"] + f"\n{instruction}" + return messages + else: + return [{"role": "system", "content": instruction}, *messages] diff --git a/examples/interceptor/chat_completion/reject_external_links.py b/examples/interceptor/chat_completion/reject_external_links.py new file mode 100644 index 0000000..d81e89a --- /dev/null +++ b/examples/interceptor/chat_completion/reject_external_links.py @@ -0,0 +1,28 @@ +from typing import List +from urllib.parse import urljoin + +from aidial_sdk.exceptions import invalid_request_error +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) + + +class RejectExternalLinksInterceptor(ChatCompletionInterceptor): + @override + async def on_request_attachment(self, path, attachment: dict) -> List[dict]: + url = attachment.get("url") + if url is None: + return [attachment] + + dial_url = self.dial_client.dial_url + abs_url = urljoin(dial_url, url) + if not abs_url.startswith(dial_url): + message = f"External links are not allowed: {url!r}" + raise invalid_request_error( + message=message, + display_message=message, + ) + + return [attachment] diff --git a/examples/interceptor/chat_completion/replicator.py b/examples/interceptor/chat_completion/replicator.py new file mode 100644 index 0000000..fc8e1ce --- /dev/null +++ b/examples/interceptor/chat_completion/replicator.py @@ -0,0 +1,183 @@ +from typing import Any, AsyncIterator, Callable, Coroutine, Dict, List, Tuple + +from aidial_sdk.chat_completion import Stage +from aidial_sdk.chat_completion.chunks import ( + ContentChunk, + EndChoiceChunk, + FinishReason, + UsageChunk, +) +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.annotated_chunk import ( + AnnotatedChunk, +) +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.chat_completion.element_path import ElementPath +from aidial_interceptors_sdk.chat_completion.index_mapper import IndexMapper +from aidial_interceptors_sdk.utils.not_given import NotGiven +from aidial_interceptors_sdk.utils.streaming import join_iterators + + +class ReplicatorInterceptor(ChatCompletionInterceptor): + """ + Call the upstream model N time and combine the results into a single response. + """ + + # Interceptor parameter + n: int + + # Response data + content_stages: Dict[int, Stage] = {} + stage_index_mapper: IndexMapper[Tuple[int, int]] = IndexMapper() + + total_usage: UsageChunk = UsageChunk(0, 0) + finish_reasons: Dict[int, str] = {} + role_sent: bool = False + chunk_template: dict = {} + + def _accumulate_usage(self, usage: Dict) -> None: + self.total_usage.prompt_tokens += usage.get("prompt_tokens") or 0 + self.total_usage.completion_tokens += ( + usage.get("completion_tokens") or 0 + ) + + def _get_content_stage(self, path: ElementPath) -> Stage: + response_idx = path.response_ctx + assert isinstance(response_idx, int) + + if response_idx not in self.content_stages: + stage = Stage( + self.response._queue, + 0, + self.stage_index_mapper((response_idx, -1)), + f"{response_idx+1} | CONTENT", + ) + stage.open() + self.content_stages[response_idx] = stage + + return self.content_stages[response_idx] + + @override + async def call_upstreams( + self, + request: dict, + call_upstream: Callable[ + [dict, Any | None], Coroutine[Any, Any, AsyncIterator[dict]] + ], + ) -> AsyncIterator[AnnotatedChunk]: + request["n"] = 1 + + async def get_iterator(idx: int) -> AsyncIterator[AnnotatedChunk]: + call_context = idx + async for chunk in await call_upstream(request, call_context): + yield AnnotatedChunk(chunk=chunk, annotation=call_context) + + iterators = [get_iterator(idx) for idx in range(self.n)] + # TODO: create tasks + return join_iterators(iterators) + + @override + async def on_response_stage( + self, path: ElementPath, stage: dict + ) -> List[dict] | dict: + assert path.stage_idx is not None + assert isinstance(path.response_ctx, int) + + if name := stage.get("name"): + stage["name"] = f"{path.response_ctx+1} | {name}" + + stage["index"] = self.stage_index_mapper( + (path.response_ctx, path.stage_idx) + ) + + return [stage] + + @override + async def on_response_message( + self, path: ElementPath, message: dict | NotGiven | None + ) -> dict | NotGiven | None: + if message: + # Avoid sending role multiple times + if message.get("role"): + if self.role_sent: + del message["role"] + else: + self.role_sent = True + + # Rerouting message content to a dedicated content stage + if content := message.get("content"): + self._get_content_stage(path).append_content(content) + message["content"] = "" + + if cc := message.get("custom_content"): + if attachments := cc.get("attachments"): + for attachment in attachments: + self._get_content_stage(path).add_attachment( + **attachment + ) + del cc["attachments"] + + return message + + @override + async def on_response_finish_reason( + self, path: ElementPath, finish_reason: str | NotGiven | None + ) -> str | NotGiven | None: + assert isinstance(path.response_ctx, int) + + if finish_reason: + self.finish_reasons[path.response_ctx + 1] = finish_reason + + return None + + @override + async def on_response_usage( + self, usage: Dict | NotGiven | None + ) -> Dict | NotGiven | None: + + # TODO: combine statistics.usage_per_model + if usage: + self._accumulate_usage(usage) + + return None + + @override + async def on_stream_chunk(self, chunk: dict) -> None: + if not self.chunk_template: + self.chunk_template = { + k: v + for k, v in chunk.items() + if k in ["id", "created", "system_fingerprint"] + } + + self.send_chunk(self.chunk_template | chunk) + + # TODO: support streaming errors + + @override + async def on_stream_end(self) -> None: + content = "\n\n".join( + [ + "Total usage:", + f" * Prompt tokens: {self.total_usage.prompt_tokens}", + f" * Completion tokens: {self.total_usage.completion_tokens}", + "Finish reasons:", + f" * {str(self.finish_reasons)}", + ] + ) + self.send_chunk(ContentChunk(content, 0)) + + self.send_chunk(self.total_usage) + + for stage in self.content_stages.values(): + stage.close() + + finish_reason = ( + FinishReason.STOP + if not self.finish_reasons + else FinishReason(self.finish_reasons[1]) + ) + self.send_chunk(EndChoiceChunk(finish_reason, 0)) diff --git a/examples/interceptor/chat_completion/statistics_reporter.py b/examples/interceptor/chat_completion/statistics_reporter.py new file mode 100644 index 0000000..49ec32d --- /dev/null +++ b/examples/interceptor/chat_completion/statistics_reporter.py @@ -0,0 +1,174 @@ +import time +from collections import defaultdict +from typing import Dict, List + +from aidial_sdk.chat_completion import Stage +from aidial_sdk.pydantic_v1 import BaseModel +from typing_extensions import override + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, +) +from aidial_interceptors_sdk.chat_completion.element_path import ElementPath +from aidial_interceptors_sdk.utils.dict import collect_at_path +from aidial_interceptors_sdk.utils.not_given import NotGiven +from examples.utils.markdown import MarkdownTable + + +class UsagePerModel(BaseModel): + index: int + model: str + prompt_tokens: int + completion_tokens: int + total_tokens: int + + +class StatisticsReporterInterceptor(ChatCompletionInterceptor): + """ + Reports token usage, finish reason and generation speed in the first stage. + """ + + # Request data + request_n: int = -1 + + # Response data + request_end_time: float = -1 + + response_start_time: float = -1 + response_end_time: float = -1 + + prompt_tokens: int = 0 + completion_tokens: int = 0 + + usage_per_model: List[UsagePerModel] = [] + + # Per-choice response data + finish_reasons: Dict[int, str] = {} + statistics_stages: Dict[int, Stage] = {} + content_lengths: Dict[int, int] = defaultdict(int) + + @override + async def on_response_usage( + self, usage: dict | NotGiven | None + ) -> dict | NotGiven | None: + if usage: + self.prompt_tokens += usage["prompt_tokens"] + self.completion_tokens += usage["completion_tokens"] + return usage + + @override + async def on_response_finish_reason( + self, path: ElementPath, finish_reason: str | NotGiven | None + ) -> str | NotGiven | None: + if finish_reason and path.choice_idx is not None: + self.finish_reasons[path.choice_idx] = finish_reason + return finish_reason + + @override + async def on_response_message( + self, path: ElementPath, message: dict | NotGiven | None + ) -> dict | NotGiven | None: + if ( + message + and path.choice_idx is not None + and (stage := self.statistics_stages.get(path.choice_idx)) + is not None + ): + if (content := message.get("content")) is not None: + stage.append_content(f"`{content or '∅'}`║") + self.content_lengths[path.choice_idx] += len(content) + return message + + def _collect_data_points(self, chunk: dict) -> None: + self.response_end_time = time.time() + + for usage in collect_at_path(chunk, "statistics.usage_per_model.*"): + self.usage_per_model.append(UsagePerModel.parse_obj(usage)) + + def _get_choice_metrics(self, choice_idx: int) -> str: + # NOTE: each metric but content_length and finish_reason + # is shared across all choices. + # So they will be repeated for each choice. + + metrics = MarkdownTable(headers=["Metric", "Value"]) + + metrics.add_rows( + ["Finish reason", self.finish_reasons.get(choice_idx) or "NA"], + ["Prompt tokens", self.prompt_tokens], + ["Completion tokens", self.completion_tokens], + ) + + response_duration = self.response_end_time - self.response_start_time + latency = self.response_start_time - self.request_end_time + + metrics.add_rows(["Latency, sec", f"{latency:.2f}"]) + metrics.add_rows(["Stream duration, sec", f"{response_duration:.2f}"]) + + if response_duration > 0: + metrics.add_rows( + [ + "Chars/sec", + f"{self.content_lengths[choice_idx] / response_duration:.2f}", + ], + [ + "Tokens/sec", + f"{self.completion_tokens / response_duration:.2f}", + ], + ) + + usages = MarkdownTable( + title="Per model usage", + headers=["Idx", "Model", "Prompt tokens", "Completion tokens"], + ) + + for idx, usage in enumerate(self.usage_per_model, start=1): + usages.add_row( + [ + idx, + usage.model, + usage.prompt_tokens, + usage.completion_tokens, + ] + ) + + return "\n\n" + metrics.to_markdown() + usages.to_markdown_opt() + + @override + async def on_request(self, request: dict) -> dict: + self.request_end_time = time.time() + self.request_n = request.get("n") or 1 + return request + + @override + async def on_stream_start(self) -> None: + # NOTE: one could create new stages inside `on_response_choice` callback. + # However, this will affect the stage indices mapping in case if the + # first chunk already contains stages. Those will be pinned and new stages + # will come after them. + # This is not what we want - we want these stages to *always* come first. + for choice_idx in range(self.request_n): + stage = Stage( + self.response._queue, + choice_idx, + self.reserve_stage_index(choice_idx), + "Statistics", + ) + + stage.open() + stage.append_content("### Stream\n\n") + self.statistics_stages[choice_idx] = stage + + @override + async def on_stream_chunk(self, chunk: dict) -> None: + if self.response_start_time == -1: + self.response_start_time = time.time() + + self._collect_data_points(chunk) + self.send_chunk(chunk) + + @override + async def on_stream_end(self) -> None: + for choice_idx in range(self.request_n): + if stage := self.statistics_stages.get(choice_idx): + stage.append_content(self._get_choice_metrics(choice_idx)) + stage.close() diff --git a/examples/interceptor/embeddings/__init__.py b/examples/interceptor/embeddings/__init__.py new file mode 100644 index 0000000..59b46a1 --- /dev/null +++ b/examples/interceptor/embeddings/__init__.py @@ -0,0 +1,9 @@ +from examples.interceptor.embeddings.blacklisted_words import ( + BlacklistedWordsInterceptor, +) +from examples.interceptor.embeddings.normalize_vector import ( + NormalizeVectorInterceptor, +) +from examples.interceptor.embeddings.project_vector import ( + ProjectVectorInterceptor, +) diff --git a/examples/interceptor/embeddings/blacklisted_words.py b/examples/interceptor/embeddings/blacklisted_words.py new file mode 100644 index 0000000..13a4122 --- /dev/null +++ b/examples/interceptor/embeddings/blacklisted_words.py @@ -0,0 +1,19 @@ +from aidial_sdk.exceptions import invalid_request_error +from typing_extensions import override + +from aidial_interceptors_sdk.embeddings.base import EmbeddingsInterceptor + + +class BlacklistedWordsInterceptor(EmbeddingsInterceptor): + BLACKLIST = {"hello", "world"} + + @override + async def modify_input(self, input: str) -> str: + for word in self.BLACKLIST: + if word.lower() in input.lower(): + message = "The embedding input contains a blacklisted word." + raise invalid_request_error( + message=message, + display_message=message, + ) + return input diff --git a/examples/interceptor/embeddings/normalize_vector.py b/examples/interceptor/embeddings/normalize_vector.py new file mode 100644 index 0000000..30d4d21 --- /dev/null +++ b/examples/interceptor/embeddings/normalize_vector.py @@ -0,0 +1,24 @@ +from typing import List + +from typing_extensions import override + +from aidial_interceptors_sdk.embeddings.base import EmbeddingsInterceptor +from examples.utils.embedding_encoding import base64_to_vector, vector_to_base64 + + +def normalize(vec: List[float]) -> List[float]: + norm = sum(x**2 for x in vec) ** 0.5 + return [x / norm for x in vec] + + +class NormalizeVectorInterceptor(EmbeddingsInterceptor): + @override + async def modify_embedding( + self, embedding: str | List[float] + ) -> str | List[float]: + if isinstance(embedding, str): + vec = base64_to_vector(embedding) + vec = normalize(vec) + return vector_to_base64(vec) + else: + return normalize(embedding) diff --git a/examples/interceptor/embeddings/project_vector.py b/examples/interceptor/embeddings/project_vector.py new file mode 100644 index 0000000..b34939d --- /dev/null +++ b/examples/interceptor/embeddings/project_vector.py @@ -0,0 +1,31 @@ +from typing import List + +from typing_extensions import override + +from aidial_interceptors_sdk.embeddings.base import EmbeddingsInterceptor +from examples.utils.embedding_encoding import base64_to_vector, vector_to_base64 + + +def project(vec: List[float], dim: int) -> List[float]: + diff = dim - len(vec) + if diff == 0: + return vec + elif diff > 0: + return vec + [0.0] * diff + else: + return vec[:dim] + + +class ProjectVectorInterceptor(EmbeddingsInterceptor): + dim: int + + @override + async def modify_embedding( + self, embedding: str | List[float] + ) -> str | List[float]: + if isinstance(embedding, str): + vec = base64_to_vector(embedding) + vec = project(vec, self.dim) + return vector_to_base64(vec) + else: + return project(embedding, self.dim) diff --git a/examples/interceptor/registry.py b/examples/interceptor/registry.py new file mode 100644 index 0000000..124c62b --- /dev/null +++ b/examples/interceptor/registry.py @@ -0,0 +1,50 @@ +from typing import Type + +from aidial_interceptors_sdk.chat_completion.base import ( + ChatCompletionInterceptor, + ChatCompletionNoOpInterceptor, +) +from aidial_interceptors_sdk.embeddings.base import ( + EmbeddingsInterceptor, + EmbeddingsNoOpInterceptor, +) +from examples.interceptor.chat_completion import ( + BlacklistedWordsInterceptor as ChatBlacklistedWordsInterceptor, +) +from examples.interceptor.chat_completion import ( + CachingInterceptor as ChatCachingInterceptor, +) +from examples.interceptor.chat_completion import ( + ImageWatermarkInterceptor, + PIIAnonymizerInterceptor, + PirateInterceptor, + RejectExternalLinksInterceptor, + ReplicatorInterceptor, + StatisticsReporterInterceptor, +) +from examples.interceptor.embeddings import ( + BlacklistedWordsInterceptor as EmbeddingsBlacklistedWordsInterceptor, +) +from examples.interceptor.embeddings import ( + NormalizeVectorInterceptor, + ProjectVectorInterceptor, +) + +chat_completion_interceptors: dict[str, Type[ChatCompletionInterceptor]] = { + "reply-as-pirate": PirateInterceptor, + "reject-external-links": RejectExternalLinksInterceptor, + "image-watermark": ImageWatermarkInterceptor, + "statistics-reporter": StatisticsReporterInterceptor, + "pii-anonymizer": PIIAnonymizerInterceptor, + "replicator:{n:int}": ReplicatorInterceptor, + "reject-blacklisted-words": ChatBlacklistedWordsInterceptor, + "cache": ChatCachingInterceptor, + "no-op": ChatCompletionNoOpInterceptor, +} + +embeddings_interceptors: dict[str, Type[EmbeddingsInterceptor]] = { + "reject-blacklisted-words": EmbeddingsBlacklistedWordsInterceptor, + "normalize-vector": NormalizeVectorInterceptor, + "project-vector:{dim:int}": ProjectVectorInterceptor, + "no-op": EmbeddingsNoOpInterceptor, +} diff --git a/examples/utils/embedding_encoding.py b/examples/utils/embedding_encoding.py new file mode 100644 index 0000000..ccb893e --- /dev/null +++ b/examples/utils/embedding_encoding.py @@ -0,0 +1,15 @@ +import base64 +from typing import List + +import numpy as np + + +def vector_to_base64(vector: List[float]) -> str: + array = np.array(vector, dtype="float32") + byte_data = array.tobytes() + base64_encoded = base64.b64encode(byte_data).decode("utf-8") + return base64_encoded + + +def base64_to_vector(data: str) -> List[float]: + return np.frombuffer(base64.b64decode(data), dtype="float32").tolist() diff --git a/examples/utils/log_config.py b/examples/utils/log_config.py new file mode 100644 index 0000000..c1e25ed --- /dev/null +++ b/examples/utils/log_config.py @@ -0,0 +1,40 @@ +import logging +import os +import sys + +from uvicorn.logging import DefaultFormatter + +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + + +def configure_loggers(): + # Making the uvicorn logger delegate logging to the root logger + uvicorn_logger = logging.getLogger("uvicorn") + uvicorn_logger.handlers = [] + uvicorn_logger.propagate = True + + # Setting up log levels + for name in ["aidial_interceptors_sdk", "uvicorn"]: + logging.getLogger(name).setLevel(LOG_LEVEL) + + # Configuring the root logger + root = logging.getLogger() + root.setLevel(LOG_LEVEL) + + root_has_stderr_handler = any( + isinstance(handler, logging.StreamHandler) + and handler.stream == sys.stderr + for handler in root.handlers + ) + + # If stderr handler is already set, then no need to add another one + if not root_has_stderr_handler: + formatter = DefaultFormatter( + fmt="%(levelprefix)s | %(asctime)s | %(name)s | %(process)d | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + use_colors=True, + ) + + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(formatter) + root.addHandler(handler) diff --git a/examples/utils/lru_cache.py b/examples/utils/lru_cache.py new file mode 100644 index 0000000..b821694 --- /dev/null +++ b/examples/utils/lru_cache.py @@ -0,0 +1,25 @@ +from collections import OrderedDict +from typing import Generic, Hashable, Optional, TypeVar + +_K = TypeVar("_K", bound=Hashable) +_V = TypeVar("_V") + + +class LRUCache(Generic[_K, _V]): + def __init__(self, maxsize: int = 128) -> None: + self.cache: OrderedDict[_K, _V] = OrderedDict() + self.maxsize: int = maxsize + + def lookup(self, key: _K) -> Optional[_V]: + if key in self.cache: + self.cache.move_to_end(key) + return self.cache[key] + else: + return None + + def save(self, key: _K, value: _V) -> None: + if key in self.cache: + self.cache.move_to_end(key) + self.cache[key] = value + if len(self.cache) > self.maxsize: + self.cache.popitem(last=False) diff --git a/examples/utils/markdown.py b/examples/utils/markdown.py new file mode 100644 index 0000000..f113aa2 --- /dev/null +++ b/examples/utils/markdown.py @@ -0,0 +1,66 @@ +import re +from typing import Any, List + +from aidial_sdk.pydantic_v1 import BaseModel + + +class MarkdownTable(BaseModel): + title: str | None = None + headers: List[Any] + rows: List[List[Any]] = [] + + def add_row(self, row: List[Any]) -> None: + if len(row) != len(self.headers): + raise ValueError( + f"Number of headers ({len(self.headers)}) does not match number of cells in a row ({len(row)})" + ) + self.rows.append(row) + + def add_rows(self, *rows: List[Any]) -> None: + for row in rows: + self.add_row(row) + + def is_empty(self) -> bool: + return not self.rows + + def to_markdown(self) -> str: + rows = [self.headers, ["---"] * len(self.headers), *self.rows] + ret = "\n".join( + f"|{'|'.join(map(escape_table_cell, row))}|" for row in rows + ) + if self.title: + ret = f"\n\n### {self.title}\n\n" + ret + return ret + + def to_markdown_opt(self) -> str: + if self.is_empty(): + return "" + return self.to_markdown() + + +def escape_table_cell(value: Any) -> str: + return _escape_new_lines(_escape_code_blocks(str(value))) + + +def _escape_new_lines(text: str) -> str: + return text.replace("\n", "
") + + +def _escape_code_blocks(text: str) -> str: + in_code_block = False + ret = [] + + open_regexp = re.compile(r"^```([^\s`]+)?\s*$") + close_regexp = re.compile(r"^```\s*$") + + for line in text.splitlines(keepends=True): + if open_regexp.match(line) and not in_code_block: + in_code_block = True + ret.append("
")
+        elif close_regexp.match(line) and in_code_block:
+            in_code_block = False
+            ret.append("
") + else: + ret.append(line) + + return "".join(ret) diff --git a/examples/utils/path.py b/examples/utils/path.py new file mode 100644 index 0000000..12adfc4 --- /dev/null +++ b/examples/utils/path.py @@ -0,0 +1,5 @@ +from pathlib import Path + + +def package_root_dir(): + return Path(__file__).parent.parent diff --git a/examples/utils/watermark.py b/examples/utils/watermark.py new file mode 100644 index 0000000..46f2d08 --- /dev/null +++ b/examples/utils/watermark.py @@ -0,0 +1,55 @@ +import io +from functools import cache +from typing import Literal + +from PIL import Image +from PIL.Image import Image as ImageObject + +from examples.utils.path import package_root_dir + + +def resize_tiling(image: ImageObject, box: tuple[int, int]) -> ImageObject: + width, height = box + tiled_image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + + img_width, img_height = image.size + + for x in range(0, width + img_width, img_width): + for y in range(0, height + img_height, img_height): + tiled_image.paste(image, (x, y)) + + return tiled_image + + +def stamp_watermark_image( + image: ImageObject, watermark: ImageObject +) -> ImageObject: + tiled_watermark = resize_tiling(watermark, image.size) + + if image.mode != "RGBA": + image = image.convert("RGBA") + if tiled_watermark.mode != "RGBA": + tiled_watermark = tiled_watermark.convert("RGBA") + + watermarked_image = Image.alpha_composite(image, tiled_watermark) + + return watermarked_image + + +@cache +def watermark_image() -> ImageObject: + return Image.open(package_root_dir() / "assets" / "watermark.png") + + +def stamp_watermark( + image_bytes: bytes, output_format: Literal["JPEG", "PNG"] +) -> bytes: + + image = Image.open(io.BytesIO(image_bytes)) + + watermarked_image = stamp_watermark_image(image, watermark_image()) + + output_bytes = io.BytesIO() + watermarked_image.save(output_bytes, format=output_format) + + return output_bytes.getvalue() diff --git a/helm/development.yaml b/helm/development.yaml new file mode 100644 index 0000000..a48ffd8 --- /dev/null +++ b/helm/development.yaml @@ -0,0 +1,28 @@ +image: + pullPolicy: Always + registry: registry-dev.deltixhub.com + repository: ai/dial/application/dial-interceptor-example + tag: development + pullSecrets: + - epm-rtc-registry-dev + +fullnameOverride: "dial-interceptor-example" + +podAnnotations: + autorestart: '{{ dateInZone "2006-01-02 15:04:05Z" (now) "UTC" }}' + +containerSecurityContext: + runAsUser: 1001 + +podSecurityContext: + fsGroup: 1001 + +ingress: + enabled: false + +serviceAccount: + create: true + +env: + DIAL_URL: "http://core.dial-development" + LOG_LEVEL: "DEBUG" \ No newline at end of file diff --git a/helm/review.yaml b/helm/review.yaml new file mode 100644 index 0000000..f7ad8b4 --- /dev/null +++ b/helm/review.yaml @@ -0,0 +1,44 @@ +image: + pullPolicy: Always + registry: registry-test.deltixhub.com + repository: ai/dial/application/dial-interceptor-example + # tag: development + pullSecrets: + - epm-rtc-registry-test + +podAnnotations: + autorestart: '{{ dateInZone "2006-01-02 15:04:05Z" (now) "UTC" }}' + +containerSecurityContext: + runAsUser: 1001 + +podSecurityContext: + fsGroup: 1001 + +ingress: + enabled: true + ingressClassName: alb + annotations: + alb.ingress.kubernetes.io/scheme: "internet-facing" + alb.ingress.kubernetes.io/target-type: "ip" + alb.ingress.kubernetes.io/backend-protocol: "HTTP" + alb.ingress.kubernetes.io/healthcheck-path: "/health" + alb.ingress.kubernetes.io/healthcheck-protocol: "HTTP" + alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "5" + alb.ingress.kubernetes.io/healthy-threshold-count: "2" + alb.ingress.kubernetes.io/target-group-attributes: "stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400" + alb.ingress.kubernetes.io/load-balancer-attributes: "routing.http2.enabled=true" + alb.ingress.kubernetes.io/listen-ports: '[{ "HTTP" : 80, "HTTPS" : 443 }]' + alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS-1-2-Ext-2018-06" + alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:eu-north-1:725751206603:certificate/373e8fd1-088e-4022-adf1-5f3e7820fb4a" + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/group.name: "allow-epam" + hosts: + - dial-interceptor-example.staging.deltixhub.io + +serviceAccount: + create: true + +env: + DIAL_URL: "http://core.dial-development" + LOG_LEVEL: "DEBUG" \ No newline at end of file diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..e8b0aeb --- /dev/null +++ b/noxfile.py @@ -0,0 +1,45 @@ +import nox + +nox.options.reuse_existing_virtualenvs = True + +SRC = "." + + +def format_with_args(session: nox.Session, *args): + session.run("autoflake", *args) + session.run("isort", *args) + session.run("black", *args) + + +@nox.session +def lint(session: nox.Session): + """Runs linters and fixers""" + try: + session.run("poetry", "install", external=True) + session.run("poetry", "check", "--lock", external=True) + session.run("pyright", SRC) + session.run("flake8", SRC) + format_with_args(session, SRC, "--check") + except Exception: + session.error( + "linting has failed. Run 'make format' to fix formatting and fix other errors manually" + ) + + +@nox.session +def format(session: nox.Session): + """Runs linters and fixers""" + session.run("poetry", "install", external=True) + format_with_args(session, SRC) + + +@nox.session(python=["3.11", "3.12"]) +# Testing against earliest and latest supported versions of the dependencies +@nox.parametrize("pydantic", ["1.10.17", "2.8.2"]) +@nox.parametrize("httpx", ["0.25.0", "0.27.0"]) +def test(session: nox.Session, pydantic: str, httpx: str) -> None: + """Runs tests""" + session.run("poetry", "install", external=True) + session.install(f"pydantic=={pydantic}") + session.install(f"httpx=={httpx}") + session.run("pytest") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..918bc03 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2886 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aidial-sdk" +version = "0.12.0" +description = "Framework to create applications and model adapters for AI DIAL" +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "aidial_sdk-0.12.0-py3-none-any.whl", hash = "sha256:12ac2e38b2e1e645ab65ababfddbdd2ba242f3301d16c0b57649bb15eda142b6"}, + {file = "aidial_sdk-0.12.0.tar.gz", hash = "sha256:dc78fdfde26a86cee749d67cab6a9ef8d2d5f3c59153a76eb8ca0951604c5598"}, +] + +[package.dependencies] +aiohttp = ">=3.8.3,<4.0.0" +fastapi = ">=0.51,<1.0" +httpx = ">=0.25.0,<1.0" +opentelemetry-api = {version = "1.20.0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-distro = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-exporter-otlp-proto-grpc = {version = "1.20.0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-exporter-prometheus = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-aiohttp-client = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-fastapi = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-httpx = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-logging = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-requests = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-system-metrics = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-instrumentation-urllib = {version = "0.41b0", optional = true, markers = "extra == \"telemetry\""} +opentelemetry-sdk = {version = "1.20.0", optional = true, markers = "extra == \"telemetry\""} +prometheus-client = {version = "0.17.1", optional = true, markers = "extra == \"telemetry\""} +pydantic = ">=1.10,<3" +requests = ">=2.19,<3.0" +uvicorn = ">=0.19,<1.0" +wrapt = ">=1.14,<2.0" + +[package.extras] +telemetry = ["opentelemetry-api (==1.20.0)", "opentelemetry-distro (==0.41b0)", "opentelemetry-exporter-otlp-proto-grpc (==1.20.0)", "opentelemetry-exporter-prometheus (==0.41b0)", "opentelemetry-instrumentation (==0.41b0)", "opentelemetry-instrumentation-aiohttp-client (==0.41b0)", "opentelemetry-instrumentation-fastapi (==0.41b0)", "opentelemetry-instrumentation-httpx (==0.41b0)", "opentelemetry-instrumentation-logging (==0.41b0)", "opentelemetry-instrumentation-requests (==0.41b0)", "opentelemetry-instrumentation-system-metrics (==0.41b0)", "opentelemetry-instrumentation-urllib (==0.41b0)", "opentelemetry-sdk (==1.20.0)", "prometheus-client (==0.17.1)"] + +[[package]] +name = "aiohttp" +version = "3.9.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "aiostream" +version = "0.6.2" +description = "Generator-based operators for asynchronous iteration" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiostream-0.6.2-py3-none-any.whl", hash = "sha256:e771bfb0a8d6f5e04359992025315fa4cc3b908ddb02822f73bb57d6ed7e9125"}, + {file = "aiostream-0.6.2.tar.gz", hash = "sha256:481e58c7f94b98f37a81384411ee39336dffb933784753b1cfa0a26f3681cc2c"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["pytest", "pytest-asyncio", "pytest-cov"] + +[[package]] +name = "anyio" +version = "4.0.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] + +[[package]] +name = "argcomplete" +version = "3.1.2" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argcomplete-3.1.2-py3-none-any.whl", hash = "sha256:d97c036d12a752d1079f190bc1521c545b941fda89ad85d15afa909b4d1b9a99"}, + {file = "argcomplete-3.1.2.tar.gz", hash = "sha256:d5d1e5efd41435260b8f85673b74ea2e883affcbec9f4230c582689e8e78251b"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "autoflake" +version = "2.2.0" +description = "Removes unused imports and unused variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "autoflake-2.2.0-py3-none-any.whl", hash = "sha256:de409b009a34c1c2a7cc2aae84c4c05047f9773594317c6a6968bd497600d4a0"}, + {file = "autoflake-2.2.0.tar.gz", hash = "sha256:62e1f74a0fdad898a96fee6f99fe8241af90ad99c7110c884b35855778412251"}, +] + +[package.dependencies] +pyflakes = ">=3.0.0" + +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + +[[package]] +name = "black" +version = "24.3.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "blis" +version = "0.7.11" +description = "The Blis BLAS-like linear algebra library, as a self-contained C-extension." +optional = false +python-versions = "*" +files = [ + {file = "blis-0.7.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd5fba34c5775e4c440d80e4dea8acb40e2d3855b546e07c4e21fad8f972404c"}, + {file = "blis-0.7.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:31273d9086cab9c56986d478e3ed6da6752fa4cdd0f7b5e8e5db30827912d90d"}, + {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06883f83d4c8de8264154f7c4a420b4af323050ed07398c1ff201c34c25c0d2"}, + {file = "blis-0.7.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee493683e3043650d4413d531e79e580d28a3c7bdd184f1b9cfa565497bda1e7"}, + {file = "blis-0.7.11-cp310-cp310-win_amd64.whl", hash = "sha256:a73945a9d635eea528bccfdfcaa59dd35bd5f82a4a40d5ca31f08f507f3a6f81"}, + {file = "blis-0.7.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b68df4d01d62f9adaef3dad6f96418787265a6878891fc4e0fabafd6d02afba"}, + {file = "blis-0.7.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:162e60d941a8151418d558a94ee5547cb1bbeed9f26b3b6f89ec9243f111a201"}, + {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686a7d0111d5ba727cd62f374748952fd6eb74701b18177f525b16209a253c01"}, + {file = "blis-0.7.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0421d6e44cda202b113a34761f9a062b53f8c2ae8e4ec8325a76e709fca93b6e"}, + {file = "blis-0.7.11-cp311-cp311-win_amd64.whl", hash = "sha256:0dc9dcb3843045b6b8b00432409fd5ee96b8344a324e031bfec7303838c41a1a"}, + {file = "blis-0.7.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dadf8713ea51d91444d14ad4104a5493fa7ecc401bbb5f4a203ff6448fadb113"}, + {file = "blis-0.7.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcdaf370f03adaf4171d6405a89fa66cb3c09399d75fc02e1230a78cd2759e4"}, + {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7de19264b1d49a178bf8035406d0ae77831f3bfaa3ce02942964a81a202abb03"}, + {file = "blis-0.7.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea55c6a4a60fcbf6a0fdce40df6e254451ce636988323a34b9c94b583fc11e5"}, + {file = "blis-0.7.11-cp312-cp312-win_amd64.whl", hash = "sha256:5a305dbfc96d202a20d0edd6edf74a406b7e1404f4fa4397d24c68454e60b1b4"}, + {file = "blis-0.7.11-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:68544a1cbc3564db7ba54d2bf8988356b8c7acd025966e8e9313561b19f0fe2e"}, + {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075431b13b9dd7b411894d4afbd4212acf4d0f56c5a20628f4b34902e90225f1"}, + {file = "blis-0.7.11-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:324fdf62af9075831aa62b51481960e8465674b7723f977684e32af708bb7448"}, + {file = "blis-0.7.11-cp36-cp36m-win_amd64.whl", hash = "sha256:afebdb02d2dcf9059f23ce1244585d3ce7e95c02a77fd45a500e4a55b7b23583"}, + {file = "blis-0.7.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2e62cd14b20e960f21547fee01f3a0b2ac201034d819842865a667c969c355d1"}, + {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b01c05a5754edc0b9a3b69be52cbee03f645b2ec69651d12216ea83b8122f0"}, + {file = "blis-0.7.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfee5ec52ba1e9002311d9191f7129d7b0ecdff211e88536fb24c865d102b50d"}, + {file = "blis-0.7.11-cp37-cp37m-win_amd64.whl", hash = "sha256:844b6377e3e7f3a2e92e7333cc644095386548ad5a027fdc150122703c009956"}, + {file = "blis-0.7.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6df00c24128e323174cde5d80ebe3657df39615322098ce06613845433057614"}, + {file = "blis-0.7.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:809d1da1331108935bf06e22f3cf07ef73a41a572ecd81575bdedb67defe3465"}, + {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfabd5272bbbe504702b8dfe30093653d278057656126716ff500d9c184b35a6"}, + {file = "blis-0.7.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca684f5c2f05269f17aefe7812360286e9a1cee3afb96d416485efd825dbcf19"}, + {file = "blis-0.7.11-cp38-cp38-win_amd64.whl", hash = "sha256:688a8b21d2521c2124ee8dfcbaf2c385981ccc27e313e052113d5db113e27d3b"}, + {file = "blis-0.7.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2ff7abd784033836b284ff9f4d0d7cb0737b7684daebb01a4c9fe145ffa5a31e"}, + {file = "blis-0.7.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9caffcd14795bfe52add95a0dd8426d44e737b55fcb69e2b797816f4da0b1d2"}, + {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fb36989ed61233cfd48915896802ee6d3d87882190000f8cfe0cf4a3819f9a8"}, + {file = "blis-0.7.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ea09f961871f880d5dc622dce6c370e4859559f0ead897ae9b20ddafd6b07a2"}, + {file = "blis-0.7.11-cp39-cp39-win_amd64.whl", hash = "sha256:5bb38adabbb22f69f22c74bad025a010ae3b14de711bf5c715353980869d491d"}, + {file = "blis-0.7.11.tar.gz", hash = "sha256:cec6d48f75f7ac328ae1b6fbb372dde8c8a57c89559172277f66e01ff08d4d42"}, +] + +[package.dependencies] +numpy = {version = ">=1.19.0", markers = "python_version >= \"3.9\""} + +[[package]] +name = "catalogue" +version = "2.0.10" +description = "Super lightweight function registries for your library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f"}, + {file = "catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.0" +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.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, + {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, +] + +[[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 = "cloudpathlib" +version = "0.18.1" +description = "pathlib-style classes for cloud storage services." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cloudpathlib-0.18.1-py3-none-any.whl", hash = "sha256:20efd5d772c75df91bb2ac52e053be53fd9000f5e9755fd92375a2a9fe6005e0"}, + {file = "cloudpathlib-0.18.1.tar.gz", hash = "sha256:ffd22f324bfbf9c3f2bc1bec6e8372cb372a0feef17c7f2b48030cd6810ea859"}, +] + +[package.extras] +all = ["cloudpathlib[azure]", "cloudpathlib[gs]", "cloudpathlib[s3]"] +azure = ["azure-storage-blob (>=12)"] +gs = ["google-cloud-storage"] +s3 = ["boto3"] + +[[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 = "colorlog" +version = "6.7.0" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, + {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "confection" +version = "0.1.5" +description = "The sweetest config system for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14"}, + {file = "confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +srsly = ">=2.4.0,<3.0.0" + +[[package]] +name = "cymem" +version = "2.0.8" +description = "Manage calls to calloc/free through Cython" +optional = false +python-versions = "*" +files = [ + {file = "cymem-2.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77b5d3a73c41a394efd5913ab7e48512054cd2dabb9582d489535456641c7666"}, + {file = "cymem-2.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd33da892fb560ba85ea14b1528c381ff474048e861accc3366c8b491035a378"}, + {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a551eda23eebd6d076b855f77a5ed14a1d1cae5946f7b3cb5de502e21b39b0"}, + {file = "cymem-2.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8260445652ae5ab19fff6851f32969a7b774f309162e83367dd0f69aac5dbf7"}, + {file = "cymem-2.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:a63a2bef4c7e0aec7c9908bca0a503bf91ac7ec18d41dd50dc7dff5d994e4387"}, + {file = "cymem-2.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b84b780d52cb2db53d4494fe0083c4c5ee1f7b5380ceaea5b824569009ee5bd"}, + {file = "cymem-2.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d5f83dc3cb5a39f0e32653cceb7c8ce0183d82f1162ca418356f4a8ed9e203e"}, + {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac218cf8a43a761dc6b2f14ae8d183aca2bbb85b60fe316fd6613693b2a7914"}, + {file = "cymem-2.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c993589d1811ec665d37437d5677b8757f53afadd927bf8516ac8ce2d3a50c"}, + {file = "cymem-2.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:ab3cf20e0eabee9b6025ceb0245dadd534a96710d43fb7a91a35e0b9e672ee44"}, + {file = "cymem-2.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cb51fddf1b920abb1f2742d1d385469bc7b4b8083e1cfa60255e19bc0900ccb5"}, + {file = "cymem-2.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9235957f8c6bc2574a6a506a1687164ad629d0b4451ded89d49ebfc61b52660c"}, + {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2cc38930ff5409f8d61f69a01e39ecb185c175785a1c9bec13bcd3ac8a614ba"}, + {file = "cymem-2.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bf49e3ea2c441f7b7848d5c61b50803e8cbd49541a70bb41ad22fce76d87603"}, + {file = "cymem-2.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:ecd12e3bacf3eed5486e4cd8ede3c12da66ee0e0a9d0ae046962bc2bb503acef"}, + {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:167d8019db3b40308aabf8183fd3fbbc256323b645e0cbf2035301058c439cd0"}, + {file = "cymem-2.0.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cd2c2791c8f6b52f269a756ba7463f75bf7265785388a2592623b84bb02bf8"}, + {file = "cymem-2.0.8-cp36-cp36m-win_amd64.whl", hash = "sha256:6204f0a3307bf45d109bf698ba37997ce765f21e359284328e4306c7500fcde8"}, + {file = "cymem-2.0.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9c05db55ea338648f8e5f51dd596568c7f62c5ae32bf3fa5b1460117910ebae"}, + {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ce641f7ba0489bd1b42a4335a36f38c8507daffc29a512681afaba94a0257d2"}, + {file = "cymem-2.0.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b83a5972a64f62796118da79dfeed71f4e1e770b2b7455e889c909504c2358"}, + {file = "cymem-2.0.8-cp37-cp37m-win_amd64.whl", hash = "sha256:ada6eb022e4a0f4f11e6356a5d804ceaa917174e6cf33c0b3e371dbea4dd2601"}, + {file = "cymem-2.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e593cd57e2e19eb50c7ddaf7e230b73c890227834425b9dadcd4a86834ef2ab"}, + {file = "cymem-2.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d513f0d5c6d76facdc605e42aa42c8d50bb7dedca3144ec2b47526381764deb0"}, + {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e370dd54359101b125bfb191aca0542718077b4edb90ccccba1a28116640fed"}, + {file = "cymem-2.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f8c58cde71b8fc7024883031a4eec66c0a9a4d36b7850c3065493652695156"}, + {file = "cymem-2.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:6a6edddb30dd000a27987fcbc6f3c23b7fe1d74f539656952cb086288c0e4e29"}, + {file = "cymem-2.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b896c83c08dadafe8102a521f83b7369a9c5cc3e7768eca35875764f56703f4c"}, + {file = "cymem-2.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f8f2bfee34f6f38b206997727d29976666c89843c071a968add7d61a1e8024"}, + {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7372e2820fa66fd47d3b135f3eb574ab015f90780c3a21cfd4809b54f23a4723"}, + {file = "cymem-2.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e57bee56d35b90fc2cba93e75b2ce76feaca05251936e28a96cf812a1f5dda"}, + {file = "cymem-2.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ceeab3ce2a92c7f3b2d90854efb32cb203e78cb24c836a5a9a2cac221930303b"}, + {file = "cymem-2.0.8.tar.gz", hash = "sha256:8fb09d222e21dcf1c7e907dc85cf74501d4cea6c4ed4ac6c9e016f98fb59cbbf"}, +] + +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + +[[package]] +name = "en-core-web-sm" +version = "3.7.0" +description = "English pipeline optimized for CPU. Components: tok2vec, tagger, parser, senter, ner, attribute_ruler, lemmatizer." +optional = false +python-versions = "*" +files = [ + {file = "en_core_web_sm-3.7.0-py3-none-any.whl", hash = "sha256:6215d71a3212690e9aec49408a27e3fe6ad7cd6c715476e93d70dc784041e93e"}, +] + +[package.dependencies] +spacy = ">=3.7.0,<3.8.0" + +[package.source] +type = "url" +url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0-py3-none-any.whl" + +[[package]] +name = "fastapi" +version = "0.109.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, +] + +[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.36.3,<0.37.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.7)", "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 = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "flake8" +version = "6.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, + {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.10.0,<2.11.0" +pyflakes = ">=3.0.0,<3.1.0" + +[[package]] +name = "frozenlist" +version = "1.4.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, + {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, + {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, + {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, + {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, + {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, + {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, + {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, + {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, + {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.63.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, + {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, +] + +[package.dependencies] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + +[[package]] +name = "grpcio" +version = "1.62.1" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.62.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:179bee6f5ed7b5f618844f760b6acf7e910988de77a4f75b95bbfaa8106f3c1e"}, + {file = "grpcio-1.62.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:48611e4fa010e823ba2de8fd3f77c1322dd60cb0d180dc6630a7e157b205f7ea"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b2a0e71b0a2158aa4bce48be9f8f9eb45cbd17c78c7443616d00abbe2a509f6d"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbe80577c7880911d3ad65e5ecc997416c98f354efeba2f8d0f9112a67ed65a5"}, + {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f6c693d446964e3292425e1d16e21a97a48ba9172f2d0df9d7b640acb99243"}, + {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:77c339403db5a20ef4fed02e4d1a9a3d9866bf9c0afc77a42234677313ea22f3"}, + {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b5a4ea906db7dec694098435d84bf2854fe158eb3cd51e1107e571246d4d1d70"}, + {file = "grpcio-1.62.1-cp310-cp310-win32.whl", hash = "sha256:4187201a53f8561c015bc745b81a1b2d278967b8de35f3399b84b0695e281d5f"}, + {file = "grpcio-1.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:844d1f3fb11bd1ed362d3fdc495d0770cfab75761836193af166fee113421d66"}, + {file = "grpcio-1.62.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:833379943d1728a005e44103f17ecd73d058d37d95783eb8f0b28ddc1f54d7b2"}, + {file = "grpcio-1.62.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:c7fcc6a32e7b7b58f5a7d27530669337a5d587d4066060bcb9dee7a8c833dfb7"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:fa7d28eb4d50b7cbe75bb8b45ed0da9a1dc5b219a0af59449676a29c2eed9698"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48f7135c3de2f298b833be8b4ae20cafe37091634e91f61f5a7eb3d61ec6f660"}, + {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f11fd63365ade276c9d4a7b7df5c136f9030e3457107e1791b3737a9b9ed6a"}, + {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b49fd8fe9f9ac23b78437da94c54aa7e9996fbb220bac024a67469ce5d0825f"}, + {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:482ae2ae78679ba9ed5752099b32e5fe580443b4f798e1b71df412abf43375db"}, + {file = "grpcio-1.62.1-cp311-cp311-win32.whl", hash = "sha256:1faa02530b6c7426404372515fe5ddf66e199c2ee613f88f025c6f3bd816450c"}, + {file = "grpcio-1.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bd90b8c395f39bc82a5fb32a0173e220e3f401ff697840f4003e15b96d1befc"}, + {file = "grpcio-1.62.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b134d5d71b4e0837fff574c00e49176051a1c532d26c052a1e43231f252d813b"}, + {file = "grpcio-1.62.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d1f6c96573dc09d50dbcbd91dbf71d5cf97640c9427c32584010fbbd4c0e0037"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:359f821d4578f80f41909b9ee9b76fb249a21035a061a327f91c953493782c31"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a485f0c2010c696be269184bdb5ae72781344cb4e60db976c59d84dd6354fac9"}, + {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b50b09b4dc01767163d67e1532f948264167cd27f49e9377e3556c3cba1268e1"}, + {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3227c667dccbe38f2c4d943238b887bac588d97c104815aecc62d2fd976e014b"}, + {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3952b581eb121324853ce2b191dae08badb75cd493cb4e0243368aa9e61cfd41"}, + {file = "grpcio-1.62.1-cp312-cp312-win32.whl", hash = "sha256:83a17b303425104d6329c10eb34bba186ffa67161e63fa6cdae7776ff76df73f"}, + {file = "grpcio-1.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:6696ffe440333a19d8d128e88d440f91fb92c75a80ce4b44d55800e656a3ef1d"}, + {file = "grpcio-1.62.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:e3393b0823f938253370ebef033c9fd23d27f3eae8eb9a8f6264900c7ea3fb5a"}, + {file = "grpcio-1.62.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:83e7ccb85a74beaeae2634f10eb858a0ed1a63081172649ff4261f929bacfd22"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:882020c87999d54667a284c7ddf065b359bd00251fcd70279ac486776dbf84ec"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a10383035e864f386fe096fed5c47d27a2bf7173c56a6e26cffaaa5a361addb1"}, + {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:960edebedc6b9ada1ef58e1c71156f28689978188cd8cff3b646b57288a927d9"}, + {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:23e2e04b83f347d0aadde0c9b616f4726c3d76db04b438fd3904b289a725267f"}, + {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978121758711916d34fe57c1f75b79cdfc73952f1481bb9583399331682d36f7"}, + {file = "grpcio-1.62.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9084086190cc6d628f282e5615f987288b95457292e969b9205e45b442276407"}, + {file = "grpcio-1.62.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:22bccdd7b23c420a27fd28540fb5dcbc97dc6be105f7698cb0e7d7a420d0e362"}, + {file = "grpcio-1.62.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:8999bf1b57172dbc7c3e4bb3c732658e918f5c333b2942243f10d0d653953ba9"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d9e52558b8b8c2f4ac05ac86344a7417ccdd2b460a59616de49eb6933b07a0bd"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1714e7bc935780bc3de1b3fcbc7674209adf5208ff825799d579ffd6cd0bd505"}, + {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8842ccbd8c0e253c1f189088228f9b433f7a93b7196b9e5b6f87dba393f5d5d"}, + {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f1e7b36bdff50103af95a80923bf1853f6823dd62f2d2a2524b66ed74103e49"}, + {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bba97b8e8883a8038606480d6b6772289f4c907f6ba780fa1f7b7da7dfd76f06"}, + {file = "grpcio-1.62.1-cp38-cp38-win32.whl", hash = "sha256:a7f615270fe534548112a74e790cd9d4f5509d744dd718cd442bf016626c22e4"}, + {file = "grpcio-1.62.1-cp38-cp38-win_amd64.whl", hash = "sha256:e6c8c8693df718c5ecbc7babb12c69a4e3677fd11de8886f05ab22d4e6b1c43b"}, + {file = "grpcio-1.62.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:73db2dc1b201d20ab7083e7041946910bb991e7e9761a0394bbc3c2632326483"}, + {file = "grpcio-1.62.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:407b26b7f7bbd4f4751dbc9767a1f0716f9fe72d3d7e96bb3ccfc4aace07c8de"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f8de7c8cef9261a2d0a62edf2ccea3d741a523c6b8a6477a340a1f2e417658de"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd5c8a1af40ec305d001c60236308a67e25419003e9bb3ebfab5695a8d0b369"}, + {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0477cb31da67846a33b1a75c611f88bfbcd427fe17701b6317aefceee1b96f"}, + {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:60dcd824df166ba266ee0cfaf35a31406cd16ef602b49f5d4dfb21f014b0dedd"}, + {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:973c49086cabab773525f6077f95e5a993bfc03ba8fc32e32f2c279497780585"}, + {file = "grpcio-1.62.1-cp39-cp39-win32.whl", hash = "sha256:12859468e8918d3bd243d213cd6fd6ab07208195dc140763c00dfe901ce1e1b4"}, + {file = "grpcio-1.62.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7209117bbeebdfa5d898205cc55153a51285757902dd73c47de498ad4d11332"}, + {file = "grpcio-1.62.1.tar.gz", hash = "sha256:6c455e008fa86d9e9a9d85bb76da4277c0d7d9668a3bfa70dbe86e9f3c759947"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.62.1)"] + +[[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 = "httpcore" +version = "0.18.0" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, + {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.25.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, + {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.18.0,<0.19.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.11.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b"}, + {file = "importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "langcodes" +version = "3.4.0" +description = "Tools for labeling human languages with IETF language tags" +optional = false +python-versions = ">=3.8" +files = [ + {file = "langcodes-3.4.0-py3-none-any.whl", hash = "sha256:10a4cc078b8e8937d8485d3352312a0a89a3125190db9f2bb2074250eef654e9"}, + {file = "langcodes-3.4.0.tar.gz", hash = "sha256:ae5a77d1a01d0d1e91854a671890892b7ce9abb601ab7327fc5c874f899e1979"}, +] + +[package.dependencies] +language-data = ">=1.2" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "language-data" +version = "1.2.0" +description = "Supplementary data about languages used by the langcodes module" +optional = false +python-versions = "*" +files = [ + {file = "language_data-1.2.0-py3-none-any.whl", hash = "sha256:77d5cab917f91ee0b2f1aa7018443e911cf8985ef734ca2ba3940770f6a3816b"}, + {file = "language_data-1.2.0.tar.gz", hash = "sha256:82a86050bbd677bfde87d97885b17566cfe75dad3ac4f5ce44b52c28f752e773"}, +] + +[package.dependencies] +marisa-trie = ">=0.7.7" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "marisa-trie" +version = "1.2.0" +description = "Static memory-efficient and fast Trie-like structures for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "marisa_trie-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:61fab91fef677f0af0e818e61595f2334f7e0b3e122b24ec65889aae69ba468d"}, + {file = "marisa_trie-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f5b3080316de735bd2b07265de5eea3ae176fa2fc60f9871aeaa9cdcddfc8f7"}, + {file = "marisa_trie-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:77bfde3287314e91e28d3a882c7b87519ef0ee104c921df72c7819987d5e4863"}, + {file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4fbb1ec1d9e891060a0aee9f9c243acec63de1e197097a14850ba38ec8a4013"}, + {file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e04e9c86fe8908b61c2aebb8444217cacaed15b93d2dccaac3849e36a6dc660"}, + {file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a7c75a508f44e40f7af8448d466045a97534adcbb026e63989407cefb9ebfa6"}, + {file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5321211647609869907e81b0230ad2dfdfa7e19fe1ee469b46304a622391e6a1"}, + {file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:88660e6ee0f821872aaf63ba4b9a7513428b9cab20c69cc013c368bd72c3a4fe"}, + {file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e4535fc5458de2b59789e574cdd55923d63de5612dc159d33941af79cd62786"}, + {file = "marisa_trie-1.2.0-cp310-cp310-win32.whl", hash = "sha256:bdd1d4d430e33abbe558971d1bd57da2d44ca129fa8a86924c51437dba5cb345"}, + {file = "marisa_trie-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:c729e2b8f9699874b1372b5a01515b340eda1292f5e08a3fe4633b745f80ad7a"}, + {file = "marisa_trie-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d62985a0e6f2cfeb36cd6afa0460063bbe83ef4bfd9afe189a99103487547210"}, + {file = "marisa_trie-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1890cc993149db4aa8242973526589e8133c3f92949b0ac74c2c9a6596707ae3"}, + {file = "marisa_trie-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26177cd0dadb7b44f47c17c40e16ac157c4d22ac7ed83b5a47f44713239e10d1"}, + {file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3425dc81d49a374be49e3a063cb6ccdf57973201b0a30127082acea50562a85e"}, + {file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:525b8df41a1a7337ed7f982eb63b704d7d75f047e30970fcfbe9cf6fc22c5991"}, + {file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c643c66bbde6a115e4ec8713c087a9fe9cb7b7c684e6af4cf448c120fa427ea4"}, + {file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a83fe83e0eab9154a2dc7c556898c86584b7779ddf4214c606fce4ceff07c13"}, + {file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:49701db6bb8f1ec0133abd95f0a4891cfd6f84f3bd019e343037e31a5a5b0210"}, + {file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3f0562863deaad58c5dc3a51f706da92582bc9084189148a45f7a12fe261a51"}, + {file = "marisa_trie-1.2.0-cp311-cp311-win32.whl", hash = "sha256:b08968ccad00f54f31e38516e4452fae59dd15a3fcee56aea3101ba2304680b3"}, + {file = "marisa_trie-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3ef375491e7dd71a0a7e7bf288c88750942bd1ee0c379dcd6ad43e31af67d00"}, + {file = "marisa_trie-1.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:39b88f126988ea83e8458259297d2b2f9391bfba8f4dc5d7a246813aae1c1def"}, + {file = "marisa_trie-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ec167b006884a90d130ee30518a9aa44cb40211f702bf07031b2d7d4d1db569b"}, + {file = "marisa_trie-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b855e6286faef5411386bf9d676dfb545c09f7d109f197f347c9366aeb12f07"}, + {file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd287ff323224d87c2b739cba39614aac3737c95a254e0ff70e77d9b8df226d"}, + {file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8a1c0361165231f4fb915237470afc8cc4803c535f535f4fc42ca72855b124"}, + {file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3267f438d258d7d85ee3dde363c4f96c3196ca9cd9e63fe429a59543cc544b15"}, + {file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c87a0c2cccce12b07bfcb70708637c0816970282d966a1531ecda1a24bd1cc8"}, + {file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d3c0e38f0501951e2322f7274a39b8e2344bbd91ceaa9da439f46022570ddc9d"}, + {file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd88a338c87e6dc130b6cea7b697580c21f0c83a8a8b46671cfecbb713d3fe24"}, + {file = "marisa_trie-1.2.0-cp312-cp312-win32.whl", hash = "sha256:5cea60975184f03fbcff51339df0eb44d2abe106a1693983cc64415eb87b897b"}, + {file = "marisa_trie-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b04a07b99b62b9bdf3eaf1d44571a3293ce249ce8971944e780c9c709593462f"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c11af35d9304de420b359741e12b885d04f11403697efcbbe8cb50f834261ebc"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db8e74493c3bffb480c54afaa88890a39bf90063ff5b322acf64bf076e4b36e"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcc6613bc873136dc62609b66aaa27363e2bd46c03fdab62d638f7cf69d5f82"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5cb731581effb3e05258f3ddc2a155475de74bb00f61eb280f991e13b48f783"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:eba1061bbeaeec4149282beab2ae163631606f119f549a10246b014e13f9047b"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:015594427360c6ad0fa94d51ee3d50fb83b0f7278996497fd2d69f877c3de9bd"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:36d65bcbf22a70cdd0202bd8608c2feecc58bdb9e5dd9a2f5a723b651fcab287"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:bc138625b383998f5cd0cbf6cd38d66d414f3786ae6d7b4e4a6fc970140ef4e9"}, + {file = "marisa_trie-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:27d270a64eb655754dfb4e352c60a084b16ab999b3a97a0cdc7dbecbca3c0e35"}, + {file = "marisa_trie-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fa1fa7f67d317a921315a65e266b9e156ce5a956076ec2b6dbf72d67c7df8216"}, + {file = "marisa_trie-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dccef41d4af11a03558c1d101de58bd723b3039a5bc4e064250008c118037ec"}, + {file = "marisa_trie-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:873efd212dfef2b736ff2ff43e10b348c428d5dbac7b8cb8aa777004bc8c7b0e"}, + {file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8af7a21ac2ba6dc23e4257fc3a40b3070e776275d3d0b5b2ef44473ad92caf3a"}, + {file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7202ba0ca1db5245feaebbeb3d0c776b2da1fffb0abc3500dd505f679686aa1"}, + {file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d90be28c083323909d23ff8e9b4a2764b9e75520d1bae1a277e9fa7ca20d15"}, + {file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40e2a374026492ac84232897f1f1d8f92a4a1f8bcf3f0ded1f2b8b708d1acfff"}, + {file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7c6e6506bd24a5799b9b4b9cf1e8d6fa281f136396ba018a95d95d4d74715227"}, + {file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:437bf6c0d7ba4cf17656a0e3bdd0b3c2c92c01fedfa670904177eef3116a4f45"}, + {file = "marisa_trie-1.2.0-cp38-cp38-win32.whl", hash = "sha256:6aeef7b364fb3b34dbba1cc57b79f1668fad0c3f039738d65a5b0d5ddce15f47"}, + {file = "marisa_trie-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:02f773e85cc566a24c0e0e28c744052db7691c4f13d02e4257bc657a49b9ab14"}, + {file = "marisa_trie-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ff705cb3b907bdeacb8c4b3bf0541691f52b101014d189a707ca41ebfacad59"}, + {file = "marisa_trie-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006419c59866979188906babc42ae0918081c18cabc2bdabca027f68c081c127"}, + {file = "marisa_trie-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7196691681ecb8a12629fb6277c33bafdb27cf2b6c18c28bc48fa42a15eab8f"}, + {file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaf052c0a1f4531ee12fd4c637212e77ad2af8c3b38a0d3096622abd01a22212"}, + {file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb95f3ab95ba933f6a2fa2629185e9deb9da45ff2aa4ba8cc8f722528c038ef"}, + {file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7459b1e1937e33daed65a6d55f8b95f9a8601f4f8749d01641cf548ecac03840"}, + {file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:902ea948677421093651ca98df62d255383f865f7c353f956ef666e92500e79f"}, + {file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf7a2d066907816726f3bf241b8cb05b698d6ffaa3c5ea2658d4ba69e87ec57"}, + {file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3540bb85b38dfc17060263e061c95a0a435681b04543d1ae7e8d7441a9790593"}, + {file = "marisa_trie-1.2.0-cp39-cp39-win32.whl", hash = "sha256:fe1394e1f262e5b45d22d30bd1ef75174d1f2772e86716b5f93f9c29dfc1a779"}, + {file = "marisa_trie-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:84c44cb13803723f0f76aa2ba1a657f762a0bb9d8a9b80dfff249bb1c3218dd6"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:035c4c8f3b313b4d7b7451ddd539da811a11077a9e359c6a0345f816b1bdccb3"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d4f05c2ee218a5ab09d269b640d06f9708b0cf37c842344cbdffb0661c74c472"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92ac63e1519598de946c7d9346df3bb52ed96968eb3021b4e89b51d79bc72a86"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:045f32eaeb5dcdb5beadb571ba616d7a34141764b616eebb4decce71b366f5fa"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb60c2f9897ce2bfc31a69ac25a040de4f8643ab2a339bb0ff1185e1a9dedaf8"}, + {file = "marisa_trie-1.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f19c5fcf23c02f1303deb69c67603ee37ed8f01de2d8b19f1716a6cf5afd5455"}, + {file = "marisa_trie-1.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a06a77075240eb83a47b780902322e66c968a06a2b6318cab06757c65ea64190"}, + {file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:125016400449e46ec0e5fabd14c8314959c4dfa02ffc2861195c99efa2b5b011"}, + {file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c57647dd9f9ba16fc5bb4679c915d7d48d5c0b25134fb10f095ccd839686a027"}, + {file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6601e74338fb31e1b20674257706150113463182a01d3a1310df6b8840720b17"}, + {file = "marisa_trie-1.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce2f68e1000c4c72820c5b2c9d037f326fcf75f036453a5e629f225f99b92cfc"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:069ac10a133d96b3f3ed1cc071b973a3f28490345e7941c778a1d81cf176f04a"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:de9911480ce2a0513582cb84ee4484e5ee8791e692276c7f5cd7378e114d1988"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfec001cf233e8853a29e1c2bb74031c217aa61e7bd19389007e04861855731"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd1f3ef8de89684fbdd6aaead09d53b82e718bad4375d2beb938cbd24b48c51a"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f5d8c1ecc85283b5b03a1475a5da723b94b3beda752c895b2f748477d8f1b1"}, + {file = "marisa_trie-1.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2e7540f844c1de493a90ad7d0f5bffc6a2cba19fe312d6db7b97aceff11d97f8"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2fb9243f66563285677079c9dccc697d35985287bacb36c8e685305687b0e025"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:58e2b84cbb6394f9c567f1f4351fc2995a094e1b684da9b577d4139b145401d6"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b4a8d3ed1f1b8f551b52e11a1265eaf0718f06bb206654b2c529cecda0913dd"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97652c5fbc92f52100afe1c4583625015611000fa81606ad17f1b3bbb9f3bfa"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7183d84da20c89b2a366bf581f0d79d1e248909678f164e8536f291120432e8"}, + {file = "marisa_trie-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c7f4df4163202b0aa5dad3eeddf088ecb61e9101986c8b31f1e052ebd6df9292"}, + {file = "marisa_trie-1.2.0.tar.gz", hash = "sha256:fedfc67497f8aa2757756b5cf493759f245d321fb78914ce125b6d75daa89b5f"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +test = ["hypothesis", "pytest", "readme-renderer"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[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 = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "murmurhash" +version = "1.0.10" +description = "Cython bindings for MurmurHash" +optional = false +python-versions = ">=3.6" +files = [ + {file = "murmurhash-1.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e90eef568adca5e17a91f96975e9a782ace3a617bbb3f8c8c2d917096e9bfeb"}, + {file = "murmurhash-1.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f8ecb00cc1ab57e4b065f9fb3ea923b55160c402d959c69a0b6dbbe8bc73efc3"}, + {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3310101004d9e2e0530c2fed30174448d998ffd1b50dcbfb7677e95db101aa4b"}, + {file = "murmurhash-1.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65401a6f1778676253cbf89c1f45a8a7feb7d73038e483925df7d5943c08ed9"}, + {file = "murmurhash-1.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:f23f2dfc7174de2cdc5007c0771ab8376a2a3f48247f32cac4a5563e40c6adcc"}, + {file = "murmurhash-1.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90ed37ee2cace9381b83d56068334f77e3e30bc521169a1f886a2a2800e965d6"}, + {file = "murmurhash-1.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22e9926fdbec9d24ced9b0a42f0fee68c730438be3cfb00c2499fd495caec226"}, + {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bfbfd68baa99717239b8844600db627f336a08b1caf4df89762999f681cdd1"}, + {file = "murmurhash-1.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b9d200a09d48ef67f6840b77c14f151f2b6c48fd69661eb75c7276ebdb146c"}, + {file = "murmurhash-1.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:e5d7cfe392c0a28129226271008e61e77bf307afc24abf34f386771daa7b28b0"}, + {file = "murmurhash-1.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:96f0a070344d4802ea76a160e0d4c88b7dc10454d2426f48814482ba60b38b9e"}, + {file = "murmurhash-1.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f61862060d677c84556610ac0300a0776cb13cb3155f5075ed97e80f86e55d9"}, + {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3b6d2d877d8881a08be66d906856d05944be0faf22b9a0390338bcf45299989"}, + {file = "murmurhash-1.0.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f54b0031d8696fed17ed6e9628f339cdea0ba2367ca051e18ff59193f52687"}, + {file = "murmurhash-1.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:97e09d675de2359e586f09de1d0de1ab39f9911edffc65c9255fb5e04f7c1f85"}, + {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b64e5332932993fef598e78d633b1ba664789ab73032ed511f3dc615a631a1a"}, + {file = "murmurhash-1.0.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2a38437a8497e082408aa015c6d90554b9e00c2c221fdfa79728a2d99a739e"}, + {file = "murmurhash-1.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:55f4e4f9291a53c36070330950b472d72ba7d331e4ce3ce1ab349a4f458f7bc4"}, + {file = "murmurhash-1.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:16ef9f0855952493fe08929d23865425906a8c0c40607ac8a949a378652ba6a9"}, + {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cc3351ae92b89c2fcdc6e41ac6f17176dbd9b3554c96109fd0713695d8663e7"}, + {file = "murmurhash-1.0.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6559fef7c2e7349a42a63549067709b656d6d1580752bd76be1541d8b2d65718"}, + {file = "murmurhash-1.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:8bf49e3bb33febb7057ae3a5d284ef81243a1e55eaa62bdcd79007cddbdc0461"}, + {file = "murmurhash-1.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f1605fde07030516eb63d77a598dd164fb9bf217fd937dbac588fe7e47a28c40"}, + {file = "murmurhash-1.0.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4904f7e68674a64eb2b08823c72015a5e14653e0b4b109ea00c652a005a59bad"}, + {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0438f0cb44cf1cd26251f72c1428213c4197d40a4e3f48b1efc3aea12ce18517"}, + {file = "murmurhash-1.0.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1171a3f9a10571931764cdbfaa5371f4cf5c23c680639762125cb075b833a5"}, + {file = "murmurhash-1.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:1c9fbcd7646ad8ba67b895f71d361d232c6765754370ecea473dd97d77afe99f"}, + {file = "murmurhash-1.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7024ab3498434f22f8e642ae31448322ad8228c65c8d9e5dc2d563d57c14c9b8"}, + {file = "murmurhash-1.0.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a99dedfb7f0cc5a4cd76eb409ee98d3d50eba024f934e705914f6f4d765aef2c"}, + {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b580b8503647de5dd7972746b7613ea586270f17ac92a44872a9b1b52c36d68"}, + {file = "murmurhash-1.0.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75840212bf75eb1352c946c3cf1622dacddd6d6bdda34368237d1eb3568f23a"}, + {file = "murmurhash-1.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:a4209962b9f85de397c3203ea4b3a554da01ae9fd220fdab38757d4e9eba8d1a"}, + {file = "murmurhash-1.0.10.tar.gz", hash = "sha256:5282aab1317804c6ebd6dd7f69f15ba9075aee671c44a34be2bde0f1b11ef88a"}, +] + +[[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 = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "nox" +version = "2023.4.22" +description = "Flexible test automation." +optional = false +python-versions = ">=3.7" +files = [ + {file = "nox-2023.4.22-py3-none-any.whl", hash = "sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891"}, + {file = "nox-2023.4.22.tar.gz", hash = "sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4,<4.0" +colorlog = ">=2.6.1,<7.0.0" +packaging = ">=20.9" +virtualenv = ">=14" + +[package.extras] +tox-to-nox = ["jinja2", "tox (<4)"] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "openai" +version = "1.32.0" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.32.0-py3-none-any.whl", hash = "sha256:953d57669f309002044fd2f678aba9f07a43256d74b3b00cd04afb5b185568ea"}, + {file = "openai-1.32.0.tar.gz", hash = "sha256:a6df15a7ab9344b1bc2bc8d83639f68b7a7e2453c0f5e50c1666547eee86f0bd"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.7,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + +[[package]] +name = "opentelemetry-api" +version = "1.20.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_api-1.20.0-py3-none-any.whl", hash = "sha256:982b76036fec0fdaf490ae3dfd9f28c81442a33414f737abc687a32758cdcba5"}, + {file = "opentelemetry_api-1.20.0.tar.gz", hash = "sha256:06abe351db7572f8afdd0fb889ce53f3c992dbf6f6262507b385cc1963e06983"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<7.0" + +[[package]] +name = "opentelemetry-distro" +version = "0.41b0" +description = "OpenTelemetry Python Distro" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_distro-0.41b0-py3-none-any.whl", hash = "sha256:61a028dc8c1418b8634a5bf71e15ad85427cb55d97a0cd6a58dd135e456cc027"}, + {file = "opentelemetry_distro-0.41b0.tar.gz", hash = "sha256:8ce05f9499a09c99d9c5f550ff2ed6d229444cae17ae36baf705b0ccb647a959"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-sdk = ">=1.13,<2.0" + +[package.extras] +otlp = ["opentelemetry-exporter-otlp (==1.20.0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.20.0" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.20.0-py3-none-any.whl", hash = "sha256:dd63209b40702636ab6ae76a06b401b646ad7b008a906ecb41222d4af24fbdef"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.20.0.tar.gz", hash = "sha256:df60c681bd61812e50b3a39a7a1afeeb6d4066117583249fcc262269374e7a49"}, +] + +[package.dependencies] +backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} +opentelemetry-proto = "1.20.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.20.0" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.20.0-py3-none-any.whl", hash = "sha256:7c3f066065891b56348ba2c7f9df6ec635a712841cae0a36f2f6a81642ae7dec"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.20.0.tar.gz", hash = "sha256:6c06d43c3771bda1795226e327722b4b980fa1ca1ec9e985f2ef3e29795bdd52"}, +] + +[package.dependencies] +backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +grpcio = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.20.0" +opentelemetry-proto = "1.20.0" +opentelemetry-sdk = ">=1.20.0,<1.21.0" + +[package.extras] +test = ["pytest-grpc"] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.41b0" +description = "Prometheus Metric Exporter for OpenTelemetry" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_exporter_prometheus-0.41b0-py3-none-any.whl", hash = "sha256:ca996f3bc15b0cbf3abd798e786095a202650202a5c0edd9e34bb9186a247b79"}, + {file = "opentelemetry_exporter_prometheus-0.41b0.tar.gz", hash = "sha256:0cc58d5d10040e69090637803b97e120f558467037c88988742c80a627e7f1ed"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-sdk = ">=1.12,<2.0" +prometheus-client = ">=0.5.0,<1.0.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.41b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation-0.41b0-py3-none-any.whl", hash = "sha256:0ef9e5705ceca0205992a4a845ae4251ce6ec15a1206ca07c2b00afb0c5bd386"}, + {file = "opentelemetry_instrumentation-0.41b0.tar.gz", hash = "sha256:214382ba10dfd29d4e24898a4c7ef18b7368178a6277a1aec95cdb75cabf4612"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +setuptools = ">=16.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-aiohttp-client" +version = "0.41b0" +description = "OpenTelemetry aiohttp client instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_aiohttp_client-0.41b0-py3-none-any.whl", hash = "sha256:a1d0d18dee5e57cf9187d1a561f9d4ce56d16433231208405458358ff6399a6f"}, + {file = "opentelemetry_instrumentation_aiohttp_client-0.41b0.tar.gz", hash = "sha256:56fd35e90c2534b2647e7cdd85f34383eddaa300ee51e989c3763dcdb205ca91"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" +opentelemetry-util-http = "0.41b0" +wrapt = ">=1.0.0,<2.0.0" + +[package.extras] +instruments = ["aiohttp (>=3.0,<4.0)"] +test = ["http-server-mock", "opentelemetry-instrumentation-aiohttp-client[instruments]"] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.41b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.41b0-py3-none-any.whl", hash = "sha256:46084195fb9c50507abbe1dd490ae4c31c8658c5790f1ddf7af95c417dbe6422"}, + {file = "opentelemetry_instrumentation_asgi-0.41b0.tar.gz", hash = "sha256:921244138b37a9a25edf2153f1c248f16f98610ee8d840b25fd7bf6b165e4d72"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" +opentelemetry-util-http = "0.41b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] +test = ["opentelemetry-instrumentation-asgi[instruments]", "opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.41b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.41b0-py3-none-any.whl", hash = "sha256:5990368e99ecc989df0a248a0b9b8e85d8b3eb7c1dbf5131c36982ba7f4a43b7"}, + {file = "opentelemetry_instrumentation_fastapi-0.41b0.tar.gz", hash = "sha256:eb4ceefe8b944fc9ea5e61fa558b99afd1285431b563f3f0104ac177cde4dfe5"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-instrumentation-asgi = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" +opentelemetry-util-http = "0.41b0" + +[package.extras] +instruments = ["fastapi (>=0.58,<1.0)"] +test = ["httpx (>=0.22,<1.0)", "opentelemetry-instrumentation-fastapi[instruments]", "opentelemetry-test-utils (==0.41b0)", "requests (>=2.23,<3.0)"] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.41b0" +description = "OpenTelemetry HTTPX Instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_httpx-0.41b0-py3-none-any.whl", hash = "sha256:6ada84b7caa95a2889b2d883c089a977546b0102c815658b88f1c2dae713e9b2"}, + {file = "opentelemetry_instrumentation_httpx-0.41b0.tar.gz", hash = "sha256:96ebc54f3f41bfcd2fc043349c8cee4b11737602512383d437e24c39a1e4adff"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" + +[package.extras] +instruments = ["httpx (>=0.18.0)"] +test = ["opentelemetry-instrumentation-httpx[instruments]", "opentelemetry-sdk (>=1.12,<2.0)", "opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.41b0" +description = "OpenTelemetry Logging instrumentation" +optional = false +python-versions = "*" +files = [ + {file = "opentelemetry_instrumentation_logging-0.41b0-py2.py3-none-any.whl", hash = "sha256:ab7117886695c32eb30d7a59199292283c5e652e2b9f2d11874fe4359eacc16a"}, + {file = "opentelemetry_instrumentation_logging-0.41b0.tar.gz", hash = "sha256:8ad46e011a99df726323428f0d0a09bf68159ab776b8184ba6d83a7c44f7de81"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" + +[package.extras] +test = ["opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.41b0" +description = "OpenTelemetry requests instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_requests-0.41b0-py3-none-any.whl", hash = "sha256:687fde31111669e729054e64d246c96b0b9d4d8702bd0e3569b7660bdb528d71"}, + {file = "opentelemetry_instrumentation_requests-0.41b0.tar.gz", hash = "sha256:bdc5515ae7533e620b312fd989941b7c2c92d492a2d4418f6ef8db5d7422fa64"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" +opentelemetry-util-http = "0.41b0" + +[package.extras] +instruments = ["requests (>=2.0,<3.0)"] +test = ["httpretty (>=1.0,<2.0)", "opentelemetry-instrumentation-requests[instruments]", "opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-instrumentation-system-metrics" +version = "0.41b0" +description = "OpenTelemetry System Metrics Instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_system_metrics-0.41b0-py3-none-any.whl", hash = "sha256:4f2106cf4b77664eb9096727eaba4ccffe28ebf426068b19aa7289644d4b9680"}, + {file = "opentelemetry_instrumentation_system_metrics-0.41b0.tar.gz", hash = "sha256:727193655d81d31a89e118d905a2691e80d967993ae62bac96979a373f59485a"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.11,<2.0" +opentelemetry-sdk = ">=1.11,<2.0" +psutil = ">=5.9,<6.0" + +[package.extras] +instruments = ["psutil (>=5)"] +test = ["opentelemetry-instrumentation-system-metrics[instruments]", "opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-instrumentation-urllib" +version = "0.41b0" +description = "OpenTelemetry urllib instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_urllib-0.41b0-py3-none-any.whl", hash = "sha256:cee9e95f55a73480df0915358ce8668bbeda53324c9426847e2ccaea0cac1a87"}, + {file = "opentelemetry_instrumentation_urllib-0.41b0.tar.gz", hash = "sha256:113416b8bd9c2d5c890cb6f86737886e209a3776c2ecdc023887bd78634d5ef3"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.41b0" +opentelemetry-semantic-conventions = "0.41b0" +opentelemetry-util-http = "0.41b0" + +[package.extras] +test = ["httpretty (>=1.0,<2.0)", "opentelemetry-test-utils (==0.41b0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.20.0" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_proto-1.20.0-py3-none-any.whl", hash = "sha256:512c3d2c6864fb7547a69577c3907348e6c985b7a204533563cb4c4c5046203b"}, + {file = "opentelemetry_proto-1.20.0.tar.gz", hash = "sha256:cf01f49b3072ee57468bccb1a4f93bdb55411f4512d0ac3f97c5c04c0040b5a2"}, +] + +[package.dependencies] +protobuf = ">=3.19,<5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.20.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_sdk-1.20.0-py3-none-any.whl", hash = "sha256:f2230c276ff4c63ea09b3cb2e2ac6b1265f90af64e8d16bbf275c81a9ce8e804"}, + {file = "opentelemetry_sdk-1.20.0.tar.gz", hash = "sha256:702e432a457fa717fd2ddfd30640180e69938f85bb7fec3e479f85f61c1843f8"}, +] + +[package.dependencies] +opentelemetry-api = "1.20.0" +opentelemetry-semantic-conventions = "0.41b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.41b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_semantic_conventions-0.41b0-py3-none-any.whl", hash = "sha256:45404391ed9e50998183a4925ad1b497c01c143f06500c3b9c3d0013492bb0f2"}, + {file = "opentelemetry_semantic_conventions-0.41b0.tar.gz", hash = "sha256:0ce5b040b8a3fc816ea5879a743b3d6fe5db61f6485e4def94c6ee4d402e1eb7"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.41b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_util_http-0.41b0-py3-none-any.whl", hash = "sha256:6a167fd1e0e8b0f629530d971165b5d82ed0be2154b7f29498499c3a517edee5"}, + {file = "opentelemetry_util_http-0.41b0.tar.gz", hash = "sha256:16d5bd04a380dc1079e766562d1e1626cbb47720f197f67010c45f090fffdfb3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[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 = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "preshed" +version = "3.0.9" +description = "Cython hash table that trusts the keys are pre-hashed" +optional = false +python-versions = ">=3.6" +files = [ + {file = "preshed-3.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f96ef4caf9847b2bb9868574dcbe2496f974e41c2b83d6621c24fb4c3fc57e3"}, + {file = "preshed-3.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a61302cf8bd30568631adcdaf9e6b21d40491bd89ba8ebf67324f98b6c2a2c05"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99499e8a58f58949d3f591295a97bca4e197066049c96f5d34944dd21a497193"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea6b6566997dc3acd8c6ee11a89539ac85c77275b4dcefb2dc746d11053a5af8"}, + {file = "preshed-3.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:bfd523085a84b1338ff18f61538e1cfcdedc4b9e76002589a301c364d19a2e36"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7c2364da27f2875524ce1ca754dc071515a9ad26eb5def4c7e69129a13c9a59"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182138033c0730c683a6d97e567ceb8a3e83f3bff5704f300d582238dbd384b3"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345a10be3b86bcc6c0591d343a6dc2bfd86aa6838c30ced4256dfcfa836c3a64"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51d0192274aa061699b284f9fd08416065348edbafd64840c3889617ee1609de"}, + {file = "preshed-3.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:96b857d7a62cbccc3845ac8c41fd23addf052821be4eb987f2eb0da3d8745aa1"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4fe6720012c62e6d550d6a5c1c7ad88cacef8388d186dad4bafea4140d9d198"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e04f05758875be9751e483bd3c519c22b00d3b07f5a64441ec328bb9e3c03700"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a55091d0e395f1fdb62ab43401bb9f8b46c7d7794d5b071813c29dc1ab22fd0"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7de8f5138bcac7870424e09684dc3dd33c8e30e81b269f6c9ede3d8c7bb8e257"}, + {file = "preshed-3.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:24229c77364628743bc29c5620c5d6607ed104f0e02ae31f8a030f99a78a5ceb"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73b0f7ecc58095ebbc6ca26ec806008ef780190fe685ce471b550e7eef58dc2"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb90ecd5bec71c21d95962db1a7922364d6db2abe284a8c4b196df8bbcc871e"}, + {file = "preshed-3.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:e304a0a8c9d625b70ba850c59d4e67082a6be9c16c4517b97850a17a282ebee6"}, + {file = "preshed-3.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1fa6d3d5529b08296ff9b7b4da1485c080311fd8744bbf3a86019ff88007b382"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e5173809d85edd420fc79563b286b88b4049746b797845ba672cf9435c0e7"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fe81eb21c7d99e8b9a802cc313b998c5f791bda592903c732b607f78a6b7dc4"}, + {file = "preshed-3.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:78590a4a952747c3766e605ce8b747741005bdb1a5aa691a18aae67b09ece0e6"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3452b64d97ce630e200c415073040aa494ceec6b7038f7a2a3400cbd7858e952"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ac970d97b905e9e817ec13d31befd5b07c9cfec046de73b551d11a6375834b79"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eebaa96ece6641cd981491cba995b68c249e0b6877c84af74971eacf8990aa19"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d473c5f6856e07a88d41fe00bb6c206ecf7b34c381d30de0b818ba2ebaf9406"}, + {file = "preshed-3.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:0de63a560f10107a3f0a9e252cc3183b8fdedcb5f81a86938fd9f1dcf8a64adf"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a9ad9f738084e048a7c94c90f40f727217387115b2c9a95c77f0ce943879fcd"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a671dfa30b67baa09391faf90408b69c8a9a7f81cb9d83d16c39a182355fbfce"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23906d114fc97c17c5f8433342495d7562e96ecfd871289c2bb2ed9a9df57c3f"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778cf71f82cedd2719b256f3980d556d6fb56ec552334ba79b49d16e26e854a0"}, + {file = "preshed-3.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:a6e579439b329eb93f32219ff27cb358b55fbb52a4862c31a915a098c8a22ac2"}, + {file = "preshed-3.0.9.tar.gz", hash = "sha256:721863c5244ffcd2651ad0928951a2c7c77b102f4e11a251ad85d37ee7621660"}, +] + +[package.dependencies] +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=0.28.0,<1.1.0" + +[[package]] +name = "prometheus-client" +version = "0.17.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, + {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "pycodestyle" +version = "2.10.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, +] + +[[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 = "3.0.1" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, + {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyright" +version = "1.1.324" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.324-py3-none-any.whl", hash = "sha256:0edb712afbbad474e347de12ca1bd9368aa85d3365a1c7b795012e48e6a65111"}, + {file = "pyright-1.1.324.tar.gz", hash = "sha256:0c48e3bca3d081bba0dddd0c1f075aaa965c59bba691f7b9bd9d73a98e44e0cf"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "requests" +version = "2.32.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, +] + +[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 = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "70.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "smart-open" +version = "7.0.4" +description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "smart_open-7.0.4-py3-none-any.whl", hash = "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47"}, + {file = "smart_open-7.0.4.tar.gz", hash = "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524"}, +] + +[package.dependencies] +wrapt = "*" + +[package.extras] +all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests", "zstandard"] +azure = ["azure-common", "azure-core", "azure-storage-blob"] +gcs = ["google-cloud-storage (>=2.6.0)"] +http = ["requests"] +s3 = ["boto3"] +ssh = ["paramiko"] +test = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "paramiko", "pytest", "pytest-rerunfailures", "requests", "responses", "zstandard"] +webhdfs = ["requests"] +zst = ["zstandard"] + +[[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 = "spacy" +version = "3.7.5" +description = "Industrial-strength Natural Language Processing (NLP) in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "spacy-3.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8002897701429ee2ab5ff6921ae43560f4cd17184cb1e10dad761901c12dcb85"}, + {file = "spacy-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43acd19efc845e9126b61a05ed7508a0aff509e96e15563f30f810c19e636b7c"}, + {file = "spacy-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f044522b1271ea54718dc43b6f593b5dad349cd31b3827764c501529b599e09a"}, + {file = "spacy-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a7dbfbca42c1c128fefa6832631fe49e11c850e963af99229f14e2d0ae94f34"}, + {file = "spacy-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:2a21b2a1e1e5d10d15c6f75990b7341d0fc9b454083dfd4222fdd75b9164831c"}, + {file = "spacy-3.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd93c34bf2a02bbed7df73d42aed8df5e3eb9688c4ea84ec576f740ba939cce5"}, + {file = "spacy-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:190ba0032a5efdb138487c587c0ebb7a98f86adb917f464b252ee8766b8eec4a"}, + {file = "spacy-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38de1c9bbb73b8cdfea2dd6e57450f093c1a1af47515870c1c8640b85b35ab16"}, + {file = "spacy-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dad4853950a2fe6c7a0bdfd791a762d1f8cedd2915c4ae41b2e0ca3a850eefc"}, + {file = "spacy-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:4e00d076871af784c2e43185a71ee676b58893853a05c5b81717b8af2b666c07"}, + {file = "spacy-3.7.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf54c3c2425428b328b53a65913d47eb4cb27a1429aa4e8ed979ffc97d4663e0"}, + {file = "spacy-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4145cea7f9814fa7d86b2028c2dd83e02f13f80d5ac604a400b2f7d7b26a0e8c"}, + {file = "spacy-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:262f8ebb71f7ed5ffe8e4f384b2594b7a296be50241ce9fbd9277b5da2f46f38"}, + {file = "spacy-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faa1e2b6234ae33c0b1f8dfa5a8dcb66fb891f19231725dfcff4b2666125c250"}, + {file = "spacy-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:07677e270a6d729453cc04b5e2247a96a86320b8845e6428d9f90f217eff0f56"}, + {file = "spacy-3.7.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e207dda0639818e2ef8f12e3df82a526de118cc09082b0eee3053ebcd9f8332"}, + {file = "spacy-3.7.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5694dd3b2f6414c18e2a3f31111cd41ffd597e1d614b51c5779f85ff07f08f6c"}, + {file = "spacy-3.7.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d211920ff73d68b8febb1d293f10accbd54f2b2228ecd3530548227b750252b1"}, + {file = "spacy-3.7.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1171bf4d8541c18a83441be01feb6c735ffc02e9308810cd691c8900a6678cd5"}, + {file = "spacy-3.7.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9108f67675fb2078ed77cda61fd4cfc197f9256c28d35cfd946dcb080190ddc"}, + {file = "spacy-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:12fdc01a4391299a47f16915505cc515fd059e71c7239904e216523354eeb9d9"}, + {file = "spacy-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f8fbe9f6b9de1bf05d163a9dd88108b8f20b138986e6ed36f960832e3fcab33"}, + {file = "spacy-3.7.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d244d524ab5a33530ac5c50fc92c9a41da6c3980f452048b9fc29e1ff1bdd03e"}, + {file = "spacy-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:8b493a8b79a7f3754102fa5ef7e2615568a390fec7ea20db49af55e5f0841fcf"}, + {file = "spacy-3.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdbb667792d6ca93899645774d1db3fccc327088a92072029be1e4bc25d7cf15"}, + {file = "spacy-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4cfb85309e11a39681c9d4941aebb95c1f5e2e3b77a61a5451e2c3849da4b92e"}, + {file = "spacy-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b0bf1788ca397eef8e67e9c07cfd9287adac438512dd191e6e6ca0f36357201"}, + {file = "spacy-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:591d90d8504e9bd5be5b482be7c6d6a974afbaeb62c3181e966f4e407e0ab300"}, + {file = "spacy-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:713b56fe008c79df01617f3602a0b7e523292211337eb999bdffb910ea1f4825"}, + {file = "spacy-3.7.5.tar.gz", hash = "sha256:a648c6cbf2acc7a55a69ee9e7fa4f22bdf69aa828a587a1bc5cfff08cf3c2dd3"}, +] + +[package.dependencies] +catalogue = ">=2.0.6,<2.1.0" +cymem = ">=2.0.2,<2.1.0" +jinja2 = "*" +langcodes = ">=3.2.0,<4.0.0" +murmurhash = ">=0.28.0,<1.1.0" +numpy = {version = ">=1.19.0", markers = "python_version >= \"3.9\""} +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +setuptools = "*" +spacy-legacy = ">=3.0.11,<3.1.0" +spacy-loggers = ">=1.0.0,<2.0.0" +srsly = ">=2.4.3,<3.0.0" +thinc = ">=8.2.2,<8.3.0" +tqdm = ">=4.38.0,<5.0.0" +typer = ">=0.3.0,<1.0.0" +wasabi = ">=0.9.1,<1.2.0" +weasel = ">=0.1.0,<0.5.0" + +[package.extras] +apple = ["thinc-apple-ops (>=0.1.0.dev0,<1.0.0)"] +cuda = ["cupy (>=5.0.0b4,<13.0.0)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0,<13.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4,<13.0.0)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4,<13.0.0)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4,<13.0.0)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4,<13.0.0)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4,<13.0.0)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4,<13.0.0)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4,<13.0.0)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4,<13.0.0)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4,<13.0.0)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4,<13.0.0)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4,<13.0.0)"] +cuda11x = ["cupy-cuda11x (>=11.0.0,<13.0.0)"] +cuda12x = ["cupy-cuda12x (>=11.5.0,<13.0.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4,<13.0.0)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4,<13.0.0)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4,<13.0.0)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4,<13.0.0)"] +ja = ["sudachidict-core (>=20211220)", "sudachipy (>=0.5.2,!=0.6.1)"] +ko = ["natto-py (>=0.9.0)"] +lookups = ["spacy-lookups-data (>=1.0.3,<1.1.0)"] +th = ["pythainlp (>=2.0)"] +transformers = ["spacy-transformers (>=1.1.2,<1.4.0)"] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +description = "Legacy registered functions for spaCy backwards compatibility" +optional = false +python-versions = ">=3.6" +files = [ + {file = "spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774"}, + {file = "spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f"}, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +description = "Logging utilities for SpaCy" +optional = false +python-versions = ">=3.6" +files = [ + {file = "spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24"}, + {file = "spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645"}, +] + +[[package]] +name = "srsly" +version = "2.4.8" +description = "Modern high-performance serialization utilities for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "srsly-2.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:17f3bcb418bb4cf443ed3d4dcb210e491bd9c1b7b0185e6ab10b6af3271e63b2"}, + {file = "srsly-2.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b070a58e21ab0e878fd949f932385abb4c53dd0acb6d3a7ee75d95d447bc609"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98286d20014ed2067ad02b0be1e17c7e522255b188346e79ff266af51a54eb33"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18685084e2e0cc47c25158cbbf3e44690e494ef77d6418c2aae0598c893f35b0"}, + {file = "srsly-2.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:980a179cbf4eb5bc56f7507e53f76720d031bcf0cef52cd53c815720eb2fc30c"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5472ed9f581e10c32e79424c996cf54c46c42237759f4224806a0cd4bb770993"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50f10afe9230072c5aad9f6636115ea99b32c102f4c61e8236d8642c73ec7a13"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c994a89ba247a4d4f63ef9fdefb93aa3e1f98740e4800d5351ebd56992ac75e3"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7ed4a0c20fa54d90032be32f9c656b6d75445168da78d14fe9080a0c208ad"}, + {file = "srsly-2.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:7a919236a090fb93081fbd1cec030f675910f3863825b34a9afbcae71f643127"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7583c03d114b4478b7a357a1915305163e9eac2dfe080da900555c975cca2a11"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ccdd2f6db824c31266aaf93e0f31c1c43b8bc531cd2b3a1d924e3c26a4f294"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72d2974f91aee652d606c7def98744ca6b899bd7dd3009fd75ebe0b5a51034"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a60c905fd2c15e848ce1fc315fd34d8a9cc72c1dee022a0d8f4c62991131307"}, + {file = "srsly-2.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:e0b8d5722057000694edf105b8f492e7eb2f3aa6247a5f0c9170d1e0d074151c"}, + {file = "srsly-2.4.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:196b4261f9d6372d1d3d16d1216b90c7e370b4141471322777b7b3c39afd1210"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4750017e6d78590b02b12653e97edd25aefa4734281386cc27501d59b7481e4e"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa034cd582ba9e4a120c8f19efa263fcad0f10fc481e73fb8c0d603085f941c4"}, + {file = "srsly-2.4.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5a78ab9e9d177ee8731e950feb48c57380036d462b49e3fb61a67ce529ff5f60"}, + {file = "srsly-2.4.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:087e36439af517e259843df93eb34bb9e2d2881c34fa0f541589bcfbc757be97"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad141d8a130cb085a0ed3a6638b643e2b591cb98a4591996780597a632acfe20"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d05367b2571c0d08d00459636b951e3ca2a1e9216318c157331f09c33489d3"}, + {file = "srsly-2.4.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3fd661a1c4848deea2849b78f432a70c75d10968e902ca83c07c89c9b7050ab8"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec37233fe39af97b00bf20dc2ceda04d39b9ea19ce0ee605e16ece9785e11f65"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2fd4bc081f1d6a6063396b6d97b00d98e86d9d3a3ac2949dba574a84e148080"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7347cff1eb4ef3fc335d9d4acc89588051b2df43799e5d944696ef43da79c873"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9dc1da5cc94d77056b91ba38365c72ae08556b6345bef06257c7e9eccabafe"}, + {file = "srsly-2.4.8-cp38-cp38-win_amd64.whl", hash = "sha256:dc0bf7b6f23c9ecb49ec0924dc645620276b41e160e9b283ed44ca004c060d79"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff8df21d00d73c371bead542cefef365ee87ca3a5660de292444021ff84e3b8c"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ac3e340e65a9fe265105705586aa56054dc3902789fcb9a8f860a218d6c0a00"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d1733f4275eff4448e96521cc7dcd8fdabd68ba9b54ca012dcfa2690db2644"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be5b751ad88fdb58fb73871d456248c88204f213aaa3c9aab49b6a1802b3fa8d"}, + {file = "srsly-2.4.8-cp39-cp39-win_amd64.whl", hash = "sha256:822a38b8cf112348f3accbc73274a94b7bf82515cb14a85ba586d126a5a72851"}, + {file = "srsly-2.4.8.tar.gz", hash = "sha256:b24d95a65009c2447e0b49cda043ac53fecf4f09e358d87a57446458f91b8a91"}, +] + +[package.dependencies] +catalogue = ">=2.0.3,<2.1.0" + +[[package]] +name = "starlette" +version = "0.36.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "thinc" +version = "8.2.4" +description = "A refreshing functional take on deep learning, compatible with your favorite libraries" +optional = false +python-versions = ">=3.6" +files = [ + {file = "thinc-8.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aaae34c28ebc7a0ba980e6c774f148e272375ff7d4ef6ae2977698edae052e52"}, + {file = "thinc-8.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a6c807cb75a22a17e5333377aff203dabf10daa457ce9e78b19f499a44dd816"}, + {file = "thinc-8.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b53c092ab30abb9a3b46ef72a8a6af76db42822b550eff778b0decf95af572c4"}, + {file = "thinc-8.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5724ea71dbdbb0d0168471884b9b6909bedaccfda01524c5e775a6fbc39d1bc7"}, + {file = "thinc-8.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:bb14e49ddb15770201682eda8381db6393f76580c1eb72d45e77e1202598116e"}, + {file = "thinc-8.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccc58e47bc285e9afbf92ed6104f555abfa285a4b92198d955d344c4c1942607"}, + {file = "thinc-8.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:baa4af044bfcaf9df6a02d6c6d6e96c960da540478a522daabfbde8923df3554"}, + {file = "thinc-8.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0e34bf322516a039e45c1da72eb82bcc97eb1f7c232b66b88f0c86f15a1202"}, + {file = "thinc-8.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc8ab48d19cd69ad9a0de2bbe49b7c20a91150faeb119638bea4c502c52b77f"}, + {file = "thinc-8.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9f8c6c006b7cbe3ebb543c224159b004b52a8ff8922615577656e1458ee4bbf0"}, + {file = "thinc-8.2.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:997a1a399af074b34e25695f37ad48f8437e7c150705891f4db89aeb430df35a"}, + {file = "thinc-8.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e28ba753cdf925ac25b52ea6c4baf5104c6ed6874d9e3dfe768ff98d5118db1"}, + {file = "thinc-8.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c3874361a1d3c469dd7c9054d4d16b7afcf791e9c47705766d690685fe702d"}, + {file = "thinc-8.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4a22e76e4651fb6b209cfba2e1c167e8537286ae35fb87769a17af491f995b5"}, + {file = "thinc-8.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:ebfd9d79d2bdadec551cb9ca8c7fdeacb56db642158c56cdb039de47e9aad995"}, + {file = "thinc-8.2.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f46f0f58f3bc02beeee5977a991335b845cb15bf1836ee8d8d15b613805c0016"}, + {file = "thinc-8.2.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d879df0997959f9d7087b0e392e72e120bde5613eb8a7c1c453370c48284e7f"}, + {file = "thinc-8.2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:795a7a1a03767a40d1e2a19fcf25c552a8d8765c78c1837514cabf5fe98817b9"}, + {file = "thinc-8.2.4-cp36-cp36m-win_amd64.whl", hash = "sha256:a276287e55b0ec50c7e8f1acef28f5353c59234af1efc54a19516328f50a6f68"}, + {file = "thinc-8.2.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21a5cb6633b4af8b49a18a3088cdcbc59756ce6a4202574f4151dd4df18bab49"}, + {file = "thinc-8.2.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca9fcddc3852b733e4754f37bb4d20693192171f1e3e9714b00abe5d74fffeb"}, + {file = "thinc-8.2.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd67e210a4a67781c9864ef45e27ec009c1f4234c737c4a2d0964aeebd3d39a1"}, + {file = "thinc-8.2.4-cp37-cp37m-win_amd64.whl", hash = "sha256:37ea70230bc4a149905e21ba620ad78ec5362b3cf8f9d1233523d6ec03a3b8e5"}, + {file = "thinc-8.2.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5586115b2f3c1c9ecc8f9dbed4a26a46d44c40e8f6be0e58e63fb673271cd0d9"}, + {file = "thinc-8.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:334097a26742ff6552a2b1ff347207b8ce4048a70756e33849bab07122f13403"}, + {file = "thinc-8.2.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a124684987554170c043ae9c497c5ebbd619a9cf2053462ff6b7e359541fbafd"}, + {file = "thinc-8.2.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f9e147b41a1e02c232d5cc4b2a333b47a245d9e87dbcd1b3f11636332a1ae5"}, + {file = "thinc-8.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:58b172d3546ecd14d257e2f37e7b9784941caa919544604137810a5477f87c99"}, + {file = "thinc-8.2.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03ab79a1734db519bd355d1c7eb41a2425d4d5c1ad4f416ea4e09cd42b8854a8"}, + {file = "thinc-8.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c466289b6fb40f477e32f99647e03256d0b1d775707778dac07973fd352c5220"}, + {file = "thinc-8.2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbffb3d284c9a54cf8bfee606e47028a27a2d11b0b1e2b83137cc03169e8a8f1"}, + {file = "thinc-8.2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abeb742352a106359ac76f048c0f4af745c59c75e02b68cc4d62cde20fcca0c5"}, + {file = "thinc-8.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:ed514b3edb185c5531fcfd77a7b0a43c196a269f1e157a025d8138076ba6328d"}, + {file = "thinc-8.2.4.tar.gz", hash = "sha256:9383b39f286291519ebbb6454bab76404992599b0cbdfaec56b2f985023186a7"}, +] + +[package.dependencies] +blis = ">=0.7.8,<0.8.0" +catalogue = ">=2.0.4,<2.1.0" +confection = ">=0.0.1,<1.0.0" +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=1.0.2,<1.1.0" +numpy = {version = ">=1.19.0", markers = "python_version >= \"3.9\""} +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +setuptools = "*" +srsly = ">=2.4.0,<3.0.0" +wasabi = ">=0.8.1,<1.2.0" + +[package.extras] +cuda = ["cupy (>=5.0.0b4)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4)"] +cuda11x = ["cupy-cuda11x (>=11.0.0)"] +cuda12x = ["cupy-cuda12x (>=11.5.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4)"] +datasets = ["ml-datasets (>=0.2.0,<0.3.0)"] +mxnet = ["mxnet (>=1.5.1,<1.6.0)"] +tensorflow = ["tensorflow (>=2.0.0,<2.6.0)"] +torch = ["torch (>=1.6.0)"] + +[[package]] +name = "tqdm" +version = "4.66.4" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typer" +version = "0.12.3" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.30.6" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "virtualenv" +version = "20.24.5" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wasabi" +version = "1.1.3" +description = "A lightweight console printing and formatting toolkit" +optional = false +python-versions = ">=3.6" +files = [ + {file = "wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c"}, + {file = "wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\" and python_version >= \"3.7\""} + +[[package]] +name = "weasel" +version = "0.4.1" +description = "Weasel: A small and easy workflow system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "weasel-0.4.1-py3-none-any.whl", hash = "sha256:24140a090ea1ac512a2b2f479cc64192fd1d527a7f3627671268d08ed5ac418c"}, + {file = "weasel-0.4.1.tar.gz", hash = "sha256:aabc210f072e13f6744e5c3a28037f93702433405cd35673f7c6279147085aa9"}, +] + +[package.dependencies] +cloudpathlib = ">=0.7.0,<1.0.0" +confection = ">=0.0.4,<0.2.0" +packaging = ">=20.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +smart-open = ">=5.2.1,<8.0.0" +srsly = ">=2.4.3,<3.0.0" +typer = ">=0.3.0,<1.0.0" +wasabi = ">=0.9.1,<1.2.0" + +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, + {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, + {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zipp" +version = "3.19.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<4.0" +content-hash = "c9d3d5c2f7f1694d25ef03ccb86d96fc67341897767dd1946ec6142922e95b9d" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b1a19d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,102 @@ +[tool.poetry] +name = "aidial-interceptors-sdk" +version = "0.1.0" +description = "Framework for creating interceptors for AI DIAL" +authors = ["EPAM RAIL "] +homepage = "https://epam-rail.com" +documentation = "https://epam-rail.com/dial_api" +license = "Apache-2.0" +readme = "README.md" +keywords = ["ai"] +classifiers = [ + "Topic :: Software Development :: Libraries :: Python Modules" +] +repository = "https://github.com/epam/ai-dial-interceptors-sdk" + +[tool.poetry.scripts] +clean = "scripts.clean:main" +codegen = "scripts.codegen:main" +gen-watermark = "scripts.gen_watermark:main" + +[pytest] +env_files = [".env"] + +[tool.poetry.dependencies] +python = ">=3.11,<4.0" +fastapi = ">=0.51,<1.0" +httpx = ">=0.25.0,<1.0" +aiohttp = "^3.8.3" +openai = "^1.32.0" +aiostream = "^0.6.2" +aidial-sdk = {version = "0.12.0", extras = ["telemetry"]} + +[tool.poetry.group.examples.dependencies] +uvicorn = "^0.30.6" +# Spacy pipeline: https://spacy.io/models/ +en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.0/en_core_web_sm-3.7.0-py3-none-any.whl" } +pillow = "^10.4.0" +numpy = "^1.26.1" +spacy = "^3.7.5" + +[tool.poetry.group.test.dependencies] +pytest = "7.4.0" +pytest-asyncio = "0.21.1" +python-dotenv = "1.0.0" + +[tool.poetry.group.lint.dependencies] +pyright = "1.1.324" +black = "24.3.0" +isort = "5.12.0" +autoflake = "2.2.0" +flake8 = "6.0.0" + +[tool.poetry.group.dev.dependencies] +nox = "^2023.4.22" + +[tool.pytest.ini_options] +testpaths = [ + "tests" +] +# muting warnings coming from opentelemetry package +filterwarnings = [ + "ignore::DeprecationWarning:opentelemetry.instrumentation.dependencies" +] + +[tool.pyright] +typeCheckingMode = "basic" +reportUnusedVariable = "error" +reportIncompatibleMethodOverride = "error" +exclude = [ + ".git", + ".venv", + ".nox", + "**/__pycache__", + "docker-compose" +] + +[tool.black] +line-length = 80 +exclude = ''' +/( + \.git + | \.venv + | \.nox + | \.__pycache__ + | docker-compose\/local\/core-data +)/ +''' + +[tool.isort] +line_length = 80 +profile = "black" + +[tool.autoflake] +ignore_init_module_imports = true +remove_all_unused_imports = true +in_place = true +recursive = true +quiet = true +exclude = [ + "\\.venv", + "\\.nox", +] diff --git a/scripts/clean.py b/scripts/clean.py new file mode 100644 index 0000000..1e3ed33 --- /dev/null +++ b/scripts/clean.py @@ -0,0 +1,32 @@ +import glob +import os +import shutil +from typing import List + + +def remove_dir(directory_path: str): + if os.path.exists(directory_path): + shutil.rmtree(directory_path) + print("Removed: " + directory_path) + + +def remove_recursively(pattern: str, exclude_dirs: List[str] = []): + files = glob.glob(f"**/{pattern}", recursive=True) + + def is_excluded(file: str) -> bool: + return any(dir in file for dir in exclude_dirs) + + for file in files: + if not is_excluded(file): + remove_dir(file) + + +def main(): + remove_dir(".nox") + remove_dir("dist") + remove_recursively("__pycache__", exclude_dirs=[".venv"]) + remove_recursively(".pytest_cache", exclude_dirs=[".venv"]) + + +if __name__ == "__main__": + main() diff --git a/scripts/codegen.py b/scripts/codegen.py new file mode 100644 index 0000000..b3dccfa --- /dev/null +++ b/scripts/codegen.py @@ -0,0 +1,43 @@ +from pathlib import Path + + +def generate_response_message_handler(): + base_path = Path("aidial_interceptors_sdk/chat_completion/") + + source = base_path / "request_message_handler.py" + target = base_path / "response_message_handler.py" + + if not source.exists(): + raise RuntimeError(f"{source!r} does not exist") + + source_content = source.read_text() + target_content = ( + source_content.replace("request", "response") + .replace("Request", "Response") + .lstrip() + ) + + prelude = ( + '"""\n' + f"CODE-GENERATED from {source} module.\n" + "DO NOT MODIFY THIS FILE.\n" + ) + + if target_content.startswith('"""'): + target_content = prelude + target_content[len('"""') :] + else: + target_content = prelude + '"""\n\n' + target_content + + target.write_text(target_content) + + print(f"Generated : {target}") + print(f"From source: {source}") + + +def main(): + print("Running code generation...") + generate_response_message_handler() + + +if __name__ == "__main__": + main() diff --git a/scripts/docker_entrypoint.sh b/scripts/docker_entrypoint.sh new file mode 100644 index 0000000..a41a774 --- /dev/null +++ b/scripts/docker_entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e +. ./.venv/bin/activate +exec "$@" diff --git a/scripts/gen_watermark.py b/scripts/gen_watermark.py new file mode 100644 index 0000000..58f19ca --- /dev/null +++ b/scripts/gen_watermark.py @@ -0,0 +1,102 @@ +import math + +import numpy as np +from PIL import Image, ImageDraw, ImageFont +from PIL.Image import Image as ImageObject + +from examples.utils.path import package_root_dir + + +def find_seamless_crop(image: ImageObject) -> tuple[int, int, int, int]: + width, height = image.size + + image_gray = image.convert("L") + pixels = np.array(image_gray) + + min_diff = np.inf + min_x = -1 + x_diffs = [] + + for x in range(10, width): + diff = np.abs(pixels[:, 0] - pixels[:, x]).sum() + x_diffs.append(int(diff)) + if diff < min_diff: + min_diff = diff + min_x = x + + vertical_seam = min_x + + min_diff = np.inf + min_y = -1 + y_diffs = [] + + for y in range(10, height): + diff = np.abs(pixels[0, :] - pixels[y, :]).sum() + y_diffs.append(int(diff)) + if diff < min_diff: + min_diff = diff + min_y = y + + horizontal_seam = min_y + + return (0, 0, vertical_seam, horizontal_seam) + + +def create_watermark_texture( + *, + size: int = 800, + font_size: int = 60, + angle: int = 45, + text: str, +) -> ImageObject: + + padded_size = int(size * math.sqrt(2)) + image_size = (padded_size, padded_size) + background_color = (255, 255, 255, 0) + image = Image.new("RGBA", image_size, background_color) + + draw = ImageDraw.Draw(image) + + font_size = 60 + font = ImageFont.load_default(font_size) + + text = "EPAM DIAL" + text_color = (0, 0, 0, 128) # Semi-transparent black + + text_width = int(draw.textlength(text, font=font)) + text_height = font_size + + text_layer = Image.new("RGBA", image_size, (255, 255, 255, 0)) + text_draw = ImageDraw.Draw(text_layer) + + for i in range(-50, 50): + for j in range(-50, 50): + text_position = ( + i * (text_width + 5), + j * (text_height + 0), + ) + + text_draw.text(text_position, text, font=font, fill=text_color) + + text_layer = text_layer.rotate(angle, expand=0) + + image.paste(text_layer, (0, 0), text_layer) + + margin = (padded_size - size) // 2 + return image.crop( + (margin, margin, padded_size - margin, padded_size - margin) + ) + + +def main(): + image = create_watermark_texture(text="EPAM DIAL") + crop_box = find_seamless_crop(image) + image = image.crop(crop_box) + + path = package_root_dir() / "assets" + path.mkdir(exist_ok=True) + image.save(path / "watermark.png", "PNG") + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..dc276e8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import os + +import httpx +import pytest_asyncio + + +def pytest_configure(): + os.environ["DIAL_URL"] = "dummy" + + +@pytest_asyncio.fixture +async def test_app(): + from examples.app import app + + async with httpx.AsyncClient( + app=app, base_url="http://test-app.com" + ) as client: + yield client diff --git a/tests/test_dict.py b/tests/test_dict.py new file mode 100644 index 0000000..a535378 --- /dev/null +++ b/tests/test_dict.py @@ -0,0 +1,244 @@ +from typing import Any, Callable + +import pytest + +from aidial_interceptors_sdk.utils.dict import ( + Path, + PathLike, + get_at_path, + set_at_path, + update_at_path, +) + + +class TestCase: + __test__ = False + + def __init__( + self, + obj: Any, + path: PathLike, + fn: Callable[[Path, Any], Any], + expected: Any, + drill_down: bool = False, + strict: bool = True, + ): + self.obj = obj + self.path = path + self.fn = fn + self.drill_down = drill_down + self.expected = expected + self.strict = strict + + +@pytest.mark.parametrize( + "case, expected_error_message", + [ + ( + TestCase( + obj={"a": 1}, + path="b", + fn=lambda p, v: 2, + drill_down=True, + expected={"a": 1, "b": 2}, + ), + None, + ), + ( + TestCase( + obj={"a": 1}, + path="a", + fn=lambda p, v: v + 1, + drill_down=True, + expected={"a": 2}, + ), + None, + ), + ( + TestCase( + obj=[1, 2, 3], + path="!1", + fn=lambda p, v: v * 2, + drill_down=True, + expected=[1, 4, 3], + ), + None, + ), + ( + TestCase( + obj=[1, 2, 3], + path="*", + fn=lambda p, v: v * 2, + drill_down=True, + expected=[2, 4, 6], + ), + None, + ), + ( + TestCase( + obj=(1, 2, 3), + path="!1", + fn=lambda p, v: v * 2, + drill_down=True, + expected=(1, 4, 3), + ), + None, + ), + ( + TestCase( + obj={"a": {"b": 2}}, + path="a.b", + fn=lambda p, v: v * 2, + drill_down=True, + expected={"a": {"b": 4}}, + ), + None, + ), + ( + TestCase( + obj={"a": {}}, + path="a.b", + fn=lambda p, v: 3, + drill_down=True, + expected={"a": {"b": 3}}, + ), + None, + ), + ( + TestCase( + obj=[[1, 2], [3, 4]], + path="!1.!0", + fn=lambda p, v: v + 1, + drill_down=True, + expected=[[1, 2], [4, 4]], + ), + None, + ), + ( + TestCase( + obj=None, + path="a", + fn=lambda p, v: 1, + drill_down=True, + expected={"a": 1}, + ), + None, + ), + pytest.param( + TestCase( + obj=[1, 2], + path="!2", + fn=lambda p, v: v, + drill_down=True, + expected=None, + ), + "Invalid index (2) for a list of length 2", + ), + pytest.param( + TestCase( + obj=(1, 2), + path="!2", + fn=lambda p, v: v, + drill_down=True, + expected=None, + ), + "Invalid index (2) for a tuple of length 2", + ), + pytest.param( + TestCase( + obj=None, + path="!0", + fn=lambda p, v: v, + drill_down=True, + expected=None, + ), + "Cannot index into None", + ), + pytest.param( + TestCase( + obj=[{"a": 1}, [], 1, {}, None, {"b": 3}, {"a": 3, "b": 4}], + path="*.a", + fn=lambda p, v: 2 * v, + drill_down=False, + strict=False, + expected=[ + {"a": 2}, + [], + 1, + {}, + None, + {"b": 3}, + {"a": 6, "b": 4}, + ], + ), + None, + ), + ], +) +def test_modify_at_path(case: TestCase, expected_error_message: str | None): + if expected_error_message is not None: + with pytest.raises(ValueError) as exc_info: + update_at_path( + case.obj, + case.path, + case.fn, + drill_down=case.drill_down, + strict=case.strict, + ) + assert str(exc_info.value) == expected_error_message + else: + assert ( + update_at_path( + case.obj, + case.path, + case.fn, + drill_down=case.drill_down, + strict=case.strict, + ) + == case.expected + ) + + +@pytest.mark.parametrize( + "obj, path, value, expected", + [ + ({"a": 1}, "a", 2, {"a": 2}), + ({"a": 1}, "b", 3, {"a": 1, "b": 3}), + ({"a": {"b": 2}}, "a.b", 4, {"a": {"b": 4}}), + ([1, 2, 3], "!1", 4, [1, 4, 3]), + ([1, 2, 3], "*", 5, [5, 5, 5]), + ((1, 2, 3), "!1", 4, (1, 4, 3)), + (None, "a", 1, {"a": 1}), + ], +) +def test_set_at_path(obj, path, value, expected): + assert ( + set_at_path(obj, path, value, drill_down=True, strict=False) == expected + ) + + +@pytest.mark.parametrize( + "obj, path, expected, expected_error_message", + [ + ({"a": 1}, "a", 1, None), + ({"a": {"b": 2}}, "a.b", 2, None), + ([1, 2, 3], "!1", 2, None), + ((1, 2, 3), "!1", 2, None), + ({"a": 1}, "b", None, None), + (None, "a", None, None), + ( + 1.0, + "a", + None, + "Cannot get a value at path ['a'] in a scalar value of type float", + ), + ([1.0], "a", None, "Invalid list index: 'a'"), + ], +) +def test_get_at_path(obj, path, expected, expected_error_message): + if expected_error_message is not None: + with pytest.raises(ValueError) as exc_info: + get_at_path(obj, path) + assert str(exc_info.value) == expected_error_message + else: + assert get_at_path(obj, path) == expected diff --git a/tests/test_embedding_encoding.py b/tests/test_embedding_encoding.py new file mode 100644 index 0000000..1c6c13e --- /dev/null +++ b/tests/test_embedding_encoding.py @@ -0,0 +1,39 @@ +import base64 +from typing import List + +import numpy as np +import pytest + +from examples.utils.embedding_encoding import base64_to_vector, vector_to_base64 + +vectors = [ + [], + [0.0], + [1.0, -1.0, 3.5, 2.2], + [ + float("inf"), + float("-inf"), + float("nan"), + ], + [1.123456789] * 1000, +] + + +@pytest.mark.parametrize("vector", vectors) +def test_to_base64_and_back(vector: List[float]): + actual_vector = base64_to_vector(vector_to_base64(vector)) + + assert np.allclose( + vector, actual_vector, equal_nan=True + ), f"Expected: {vector}, Actual: {actual_vector}" + + +@pytest.mark.parametrize("vector", vectors) +def test_to_vector_and_back(vector: List[float]): + str = base64.b64encode(np.array(vector, dtype="float32").tobytes()).decode( + "utf-8" + ) + + actual_str = vector_to_base64(base64_to_vector(str)) + + assert str == actual_str, f"Expected: {str}, Actual: {actual_str}" diff --git a/tests/test_markdown.py b/tests/test_markdown.py new file mode 100644 index 0000000..3c741ac --- /dev/null +++ b/tests/test_markdown.py @@ -0,0 +1,50 @@ +import pytest + +from examples.utils.markdown import escape_table_cell + +test_cases = [ + ("Simple text", "Simple text"), + ("Text with\nnew line", "Text with
new line"), + ( + "```python\ndef foo():\n pass\n```", + "
def foo():
pass
", + ), + ( + "```\ndef foo():\n pass\n```", + "
def foo():
pass
", + ), + ( + "Text before\n```\ndef foo():\n pass\n```\nText after", + "Text before
def foo():
pass
Text after", + ), + ("Text with inline ```code```", "Text with inline ```code```"), + ( + "```\nmultiline code block\nwith text\n```", + "
multiline code block
with text
", + ), + ( + "Text with\n```\ncode block\n```\nand text after", + "Text with
code block
and text after", + ), + ( + "````\ncode block\nwith different backticks\n````", + "````
code block
with different backticks
````", + ), # Incorrectly formatted code block + ( + "Text with mixed ```code```\nand regular text", + "Text with mixed ```code```
and regular text", + ), + ( + "```markdown\n# Heading\nThis is markdown code\n```", + "
# Heading
This is markdown code
", + ), + ( + "```\nUnclosed code block\n", + "
Unclosed code block
", + ), # Unclosed code block +] + + +@pytest.mark.parametrize("input_text, expected_output", test_cases) +def test_escape_table_cell(input_text, expected_output): + assert escape_table_cell(input_text) == expected_output diff --git a/tests/test_response_handler.py b/tests/test_response_handler.py new file mode 100644 index 0000000..f88dc71 --- /dev/null +++ b/tests/test_response_handler.py @@ -0,0 +1,164 @@ +import fastapi +import pytest +from aidial_sdk.chat_completion import Request, Response +from aidial_sdk.pydantic_v1 import SecretStr +from aidial_sdk.utils.streaming import merge_chunks + +from aidial_interceptors_sdk.chat_completion.annotated_chunk import ( + AnnotatedChunk, +) +from aidial_interceptors_sdk.chat_completion.element_path import ElementPath +from aidial_interceptors_sdk.chat_completion.response_handler import ( + ResponseHandler, +) +from aidial_interceptors_sdk.utils.not_given import NOT_GIVEN, NotGiven + +RESPONSE_CHUNK = { + "id": "chatcmpl-123", + "object": "chat.completion,chunk", + "created": 1677652288, + "model": "gpt-4o-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "index": 0, + "delta": { + "role": "assistant", + "content": "assistant response", + "custom_content": { + "attachments": [{}, {}], + "stages": [ + { + "index": 0, + "name": "Stage name", + "content": "hello", + "attachments": [{"title": "Attachment title"}], + } + ], + }, + }, + "logprobs": None, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21, + }, +} + + +async def get_response(cls: type[ResponseHandler], **kwargs) -> dict: + dummy_request = Request( + messages=[], + stream=False, + api_key_secret=SecretStr("dummy"), + deployment_id="dummy", + headers={}, + original_request=fastapi.Request(scope={"type": "http"}), + **kwargs + ) + + response = Response(request=dummy_request) + + async def producer(request: Request, response: Response): + handler = cls(response=response) + ann_chunk = AnnotatedChunk(chunk=RESPONSE_CHUNK) + await handler.traverse_response_chunk(ann_chunk) + + first_chunk = await response._generator(producer, dummy_request) + return await merge_chunks(response._generate_stream(first_chunk)) + + +# Test 1: Add a second choice, which is same as the first one, but with index=1 +class AddSecondChoice(ResponseHandler): + async def on_response_choice(self, path: ElementPath, choice: dict): + return [choice, {**choice, "index": 1}] + + +@pytest.mark.asyncio +async def test_add_second_choice(): + response = await get_response(AddSecondChoice, n=2) + assert len(response["choices"]) == 2 + assert response["choices"][1]["index"] == 1 + + +# Test 2: Remove all attachments in the message returning NOT_GIVEN +class RemoveAttachmentsNotGiven(ResponseHandler): + async def on_response_attachments(self, path: ElementPath, attachments): + return NOT_GIVEN + + +@pytest.mark.asyncio +async def test_remove_attachments_not_given(): + response = await get_response(RemoveAttachmentsNotGiven) + assert ( + "attachments" not in response["choices"][0]["message"]["custom_content"] + ) + + +# Test 3: Remove all attachments in the message by returning None +class RemoveAttachmentsNone(ResponseHandler): + async def on_response_attachments(self, path: ElementPath, attachments): + return None + + +@pytest.mark.asyncio +async def test_remove_attachments_none(): + response = await get_response(RemoveAttachmentsNone) + assert ( + response["choices"][0]["message"]["custom_content"]["attachments"] + is None + ) + + +# Test 4: Set the title of the first stage attachment to "New title" +class SetStageAttachmentTitle(ResponseHandler): + async def on_response_attachment(self, path: ElementPath, attachment): + if path.attachment_idx == 0 and path.stage_idx == 0: + return [{**attachment, "title": "New " + attachment["title"]}] + return [attachment] + + +@pytest.mark.asyncio +async def test_set_stage_attachment_title(): + response = await get_response(SetStageAttachmentTitle) + assert ( + response["choices"][0]["message"]["custom_content"]["stages"][0][ + "attachments" + ][0]["title"] + == "New Attachment title" + ) + + +# Test 5: Multiply usage numbers by 2 +class MultiplyUsageNumbers(ResponseHandler): + async def on_response_usage(self, usage: dict | NotGiven | None): + if usage: + usage["prompt_tokens"] *= 2 + usage["completion_tokens"] *= 2 + usage["total_tokens"] *= 2 + return usage + + +@pytest.mark.asyncio +async def test_multiply_usage_numbers(): + response = await get_response(MultiplyUsageNumbers) + assert response["usage"]["prompt_tokens"] == 18 + assert response["usage"]["completion_tokens"] == 24 + assert response["usage"]["total_tokens"] == 42 + + +# Test 6: Add a state equals to "{'status': 'done'}" +class AddState(ResponseHandler): + async def on_response_state(self, path: ElementPath, state): + return {"status": "done"} + + +@pytest.mark.asyncio +async def test_add_state(): + response = await get_response(AddState) + assert response["choices"][0]["message"]["custom_content"]["state"] == { + "status": "done" + } diff --git a/tests/test_spacy_anonymizer.py b/tests/test_spacy_anonymizer.py new file mode 100644 index 0000000..a99e095 --- /dev/null +++ b/tests/test_spacy_anonymizer.py @@ -0,0 +1,59 @@ +from typing import Dict, List, Tuple + +import pytest + +from examples.interceptor.chat_completion.pii_anonymiser.spacy_anonymizer import ( + SpacyAnonymizer, +) + +Replacements = Dict[str, str] +TestCase = Tuple[str, Replacements] + +test_cases: List[TestCase] = [ + ( + "My name is Adam. Paul is your name. My friend's name is Paul too.", + {"Adam": "[PERSON-1]", "Paul": "[PERSON-2]"}, + ), + ( + "London is a capital of the United Kingdom", + {"London": "[GPE-1]", "the United Kingdom": "[GPE-2]"}, + ), + ( + "Landon is a capital of the Younited Kingdom", + {"Landon": "[ORG-1]", "the Younited Kingdom": "[GPE-1]"}, + ), + ( + "The iPhone is a smartphone made by Apple", + {"iPhone": "[ORG-1]", "Apple": "[ORG-2]"}, + ), +] + + +@pytest.mark.parametrize("test_case", test_cases) +def test_anonymize_deanonymize(test_case: TestCase): + text = test_case[0] + expected_replacements = test_case[1] + + anon = SpacyAnonymizer() + anonymized = anon.anonymize(text) + deanonymized = anon.deanonymize(anonymized) + + assert anon.replacements == expected_replacements + + for key in anon.replacements: + assert key not in anonymized + assert key in text + assert key in deanonymized + + assert text == deanonymized + + +@pytest.mark.parametrize("test_case", test_cases) +def test_anonymize_idempotent(test_case: TestCase): + text = test_case[0] + + anon = SpacyAnonymizer() + anonymized1 = anon.anonymize(text) + anonymized2 = anon.anonymize(anonymized1) + + assert anonymized1 == anonymized2