diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2cad5fca..9c303644 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.3 -ARG UV_VERSION=0.5.10 +ARG UV_VERSION=latest ARG CORE_IMAGE=ubuntu:noble FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS astral-uv-source diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2ead9957..d5eeaafa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,14 @@ "name": "aymurai", // "initializeCommand": "make core-build", "dockerComposeFile": "docker-compose.yml", - "service": "aymurai-devcontainer-gpu", + "service": "aymurai-devcontainer", "runServices": [ - "aymurai-devcontainer-gpu" + "aymurai-devcontainer" ], "workspaceFolder": "/workspace", + "mounts": [ + "source=codex-data,target=/home/ubuntu/.codex,type=volume" + ], "customizations": { "vscode": { "settings": { @@ -47,9 +50,11 @@ "cweijan.vscode-database-client2", "christian-kohler.path-intellisense", "github.vscode-github-actions", - "seatonjiang.gitmoji-vscode" + "seatonjiang.gitmoji-vscode", + "openai.chatgpt" ] } }, - "postCreateCommand": "bash /home/ubuntu/entrypoint.sh" + "postCreateCommand": "bash /home/ubuntu/entrypoint.sh", + "postStartCommand": "sudo chown -R ubuntu:ubuntu /home/ubuntu/.codex && sudo chmod -R u+rwX /home/ubuntu/.codex" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5e12b20d..c434079c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,7 +5,7 @@ x-template: &template - ..:/workspace:cached - ../notebooks/:/notebooks - ../resources/:/resources - - ../test/:/test + - ../tests/:/tests - $HOME/.ssh/:/home/ubuntu/.ssh - /var/run/docker.sock:/var/run/docker.sock env_file: @@ -27,7 +27,9 @@ services: resources: reservations: devices: - - capabilities: [ gpu ] + - driver: nvidia + count: all + capabilities: [ gpu ] aymurai-devcontainer: <<: *template diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 000c3360..5eddef91 100644 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh # install dependencies -uv sync --frozen --all-extras +uv sync --frozen --all-extras --all-groups # configure precommit uv run pre-commit install diff --git a/.env.common b/.env.common index 5b5d5640..493770c9 100644 --- a/.env.common +++ b/.env.common @@ -1,31 +1,8 @@ -DATASETS_BASEPATH=/resources/datasets -MODELS_BASEPATH=/resources/models - +CACHE_PATH=/resources/cache AYMURAI_CACHE_BASEPATH=/resources/cache/aymurai -CACHE_PATH=/resources/cache -HF_DATASETS_CACHE=/resources/cache/huggingface/cache -TRANSFORMERS_CACHE=/resources/cache/huggingface/transformers +HF_HOME=/resources/cache/huggingface TOKENIZERS_PARALLELISM=1 -TESSDATA_PREFIX=/usr/local/share/tessdata - AYMURAI_RESTRICTED_DOCUMENT_PDFS_PATH="/resources/data/restricted/ar-juz-pcyf-10/RESOLUCIONES DEL JUZGADO-pdf" AYMURAI_RESTRICTED_DOCUMENT_DOCS_PATH="/resources/data/restricted/ar-juz-pcyf-10/RESOLUCIONES DEL JUZGADO" - -TF_CPP_MIN_LOG_LEVEL=3 -TFHUB_CACHE_DIR=/resources/cache/tfhub_modules - -TORCH_VERSION=2.0.1 -CUDA_VERSION=cu118 - -CORE_IMAGE_CUDA=registry.gitlab.com/collective.ai/datagenero-public/aymurai-core -CORE_IMAGE_CPU=registry.gitlab.com/collective.ai/datagenero-public/aymurai-core-cpu -API_IMAGE=registry.gitlab.com/collective.ai/datagenero-public/aymurai-api - -API_HOST=0.0.0.0 -API_PORT=8899 - -SRC_VOLUME_MOUNT=src:/src -RESOURCES_VOLUME_MOUNT=resources:/resources -NOTEBOOKS_VOLUME_MOUNT=notebooks:/notebooks diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..319cd7f0 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,86 @@ +name: pytest + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pr-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + pytest: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + name: pytest (${{ matrix.os }}, py${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + python-version: + - "3.10" + # - "3.11" + # - "3.12" + # - "3.13" + # - "3.14" + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: latest + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Configure es_AR locale (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes locales + sudo locale-gen es_AR.UTF-8 + sudo update-locale LANG=es_AR.UTF-8 LC_ALL=es_AR.UTF-8 + echo "LANG=es_AR.UTF-8" >> "$GITHUB_ENV" + echo "LC_ALL=es_AR.UTF-8" >> "$GITHUB_ENV" + locale -a + + - name: Install dependencies + run: | + uv sync --frozen --python python --no-dev --no-python-downloads --group tests + + - name: Run api tests + env: + DISKCACHE_ROOT: /tmp + run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/api + + - name: Download pipelines data + env: + DISKCACHE_ROOT: /tmp + RESOURCES_BASEPATH: resources + AYMURAI_CACHE_BASEPATH: resources/cache/aymurai + run: uv run --no-sync pipeline-download + + - name: Run pipeline tests + env: + DISKCACHE_ROOT: /tmp + RESOURCES_BASEPATH: resources + AYMURAI_CACHE_BASEPATH: resources/cache/aymurai + run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/integration/pipelines diff --git a/.gitignore b/.gitignore index 5f22f5f3..2dad2bac 100755 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,10 @@ resources .venv aymurai/version.py + +.agents +.opencode.json +.sisyphus +notebooks/** +!notebooks/**/ +!notebooks/**/*.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2aeb0810..8f7ef35f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,11 @@ repos: - - repo: https://github.com/ambv/black - rev: 22.6.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.2 hooks: - - id: black - language_version: python3.10 + - id: ruff + - id: ruff-format - repo: https://github.com/kynan/nbstripout rev: 0.6.0 hooks: - - id: nbstripout - - # - repo: https://github.com/pre-commit/pre-commit-hooks - # rev: v2.20.0 - # hooks: - # - id: flake8 \ No newline at end of file + - id: nbstripout \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..68007232 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [], + "files.associations": { + ".csv": "csv", + ".env*": "dotenv", + ".json": "json", + ".jsonc": "jsonc", + ".jsonl": "jsonl", + ".md": "markdown" + }, + "github.copilot.enable": { + "*": true, + "dotenv": false, + "csv": false, + "json": false, + "jsonc": false, + "jsonl": false, + "markdown": false, + "plaintext": false, + "scminput": false + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index 64e27084..816c1c5b 100644 --- a/Makefile +++ b/Makefile @@ -3,20 +3,37 @@ export $(shell sed 's/=.*//' .env) include .env.common export $(shell sed 's/=.*//' .env.common) +# Select which API service to control (override with API_SERVICE=aymurai-api-gpu) +API_SERVICE ?= aymurai-api +# Select which full API service to control (override with API_FULL_SERVICE=aymurai-api-full-gpu) +API_FULL_SERVICE ?= aymurai-api-full + api-build: - docker compose build aymurai-api + docker compose build $(API_SERVICE) api-run: - docker compose run --service-ports aymurai-api + docker compose run --service-ports $(API_SERVICE) +api-up: + docker compose up -d $(API_SERVICE) +api-stop: + docker compose stop $(API_SERVICE) +api-logs: + docker compose logs -f $(API_SERVICE) api-pull: - docker compose pull aymurai-api + docker compose pull $(API_SERVICE) api-full-build: - docker compose build aymurai-api-full + docker compose build $(API_FULL_SERVICE) api-full-run: - docker compose run --service-ports aymurai-api-full + docker compose run --service-ports $(API_FULL_SERVICE) +api-full-up: + docker compose up -d $(API_FULL_SERVICE) +api-full-stop: + docker compose stop $(API_FULL_SERVICE) +api-full-logs: + docker compose logs -f $(API_FULL_SERVICE) api-full-pull: - docker compose pull aymurai-api-full + docker compose pull $(API_FULL_SERVICE) stress-test: locust -f locustfile.py --host http://localhost:8899 diff --git a/README.es.md b/README.es.md new file mode 100644 index 00000000..90e939c0 --- /dev/null +++ b/README.es.md @@ -0,0 +1,161 @@ +# AymurAI Backend +Idioma: [English](README.md) | **Español** + +AymurAI Backend provee la API y los pipelines de ML usados para procesar resoluciones judiciales en dos flujos principales: + +- `anonymizer`: extracción de entidades y generación de documentos anonimizados. +- `data-public`: extracción de información estructurada para curación de dataset público. + +Este repositorio contiene el servicio FastAPI, configuraciones de pipelines de producción y persistencia en base de datos para ambos flujos. + +## Qué es AymurAI +AymurAI es un proyecto orientado a facilitar la generación de datos judiciales anonimizados y estructurados para casos de violencia de género (VG) en América Latina. El backend orquesta la ingesta de documentos, la inferencia de modelos, la persistencia de validaciones y la exportación de resultados para usos operativos y de investigación. + +Este repositorio está enfocado en el backend: expone APIs consumidas por el frontend y ejecuta los pipelines de producción de `anonymizer` y `data-public`. + +## Documentación +- Índice técnico: [docs/es/README.md](docs/es/README.md) +- Referencia de API: [docs/es/api/README.md](docs/es/api/README.md) +- Índice de pipelines: [docs/es/pipelines/README.md](docs/es/pipelines/README.md) +- Flujo anonymizer: [docs/es/pipelines/anonymizer/README.md](docs/es/pipelines/anonymizer/README.md) +- Flujo datapublic: [docs/es/pipelines/datapublic/README.md](docs/es/pipelines/datapublic/README.md) +- Esquema de base de datos interna: [docs/es/database/README.md](docs/es/database/README.md) + +## Inicio Rápido (imagen Docker) +Ejecutar la imagen full de la API (incluye recursos de producción): + +```bash +docker run -d --name aymurai-backend -p 8899:8899 ghcr.io/aymurai/api:full +``` + +Opcional: persistir DB/cache fuera del container (volumen host montado en `/resources/cache`): + +```bash +mkdir -p ./aymurai-cache + +docker run -d --name aymurai-backend -p 8899:8899 \ + -v "$(pwd)/aymurai-cache:/resources/cache" \ + ghcr.io/aymurai/api:full +``` + +Opcional: runtime con GPU (requiere NVIDIA Container Toolkit): + +```bash +docker run -d --name aymurai-backend-gpu --gpus all \ + -e TORCH_DEVICE=cuda \ + -p 8899:8899 \ + ghcr.io/aymurai/api:full +``` + +Abrir Swagger UI: + +```text +http://localhost:8899/docs +``` + +## Inicio Rápido (Docker Compose) +Usar los servicios definidos en `docker-compose.yml`: + +```bash +# CPU, perfil liviano +make api-up + +# CPU, perfil full +make api-full-up + +# GPU, perfil liviano +API_SERVICE=aymurai-api-gpu make api-up + +# GPU, perfil full +API_FULL_SERVICE=aymurai-api-full-gpu make api-full-up +``` + +Ver logs: + +```bash +make api-logs +# o make api-full-logs +``` + +## Resumen de runtime +- Framework: `FastAPI` +- Puerto por defecto: `8899` +- Motor de DB: `SQLModel` + migraciones Alembic al iniciar +- URI de DB por defecto: `sqlite:////resources/cache/sqlite/database.db` +- Configs de pipeline de producción: + - `resources/pipelines/production/flair-anonymizer/pipeline.json` + - `resources/pipelines/production/datapublic/pipeline.json` + +## Endpoints públicos principales +- `GET /server/healthcheck` +- `GET /server/stats/summary` +- `POST /misc/document-extract` (y alias deprecado `POST /document-extract`) +- `POST /anonymizer/predict` +- `POST /anonymizer/disambiguate` +- `POST /anonymizer/validation` +- `POST /anonymizer/anonymize-document` +- `POST /datapublic/predict/{document_id}` +- `GET /datapublic/validation/document/{document_id}` +- `POST /datapublic/validation/document/{document_id}` + +Para contratos request/response y ejemplos completos, ver [docs/es/api/README.md](docs/es/api/README.md). + +## Despliegue en red cerrada +Para mover una imagen a un entorno sin internet: + +```bash +docker image save ghcr.io/aymurai/api:full -o aymurai-api-full.tar +docker load -i aymurai-api-full.tar +``` + +## Contribución +Las contribuciones son bienvenidas en documentación, API y mejoras de pipelines. + +- Guía de contribución: [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) +- Seguridad y ética: [docs/SECURITY.md](docs/SECURITY.md) +- Código de conducta: [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md) + +## Contribuidores +- **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) +- **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) +- **Sofía del Pozo** - [@sofiadelpozo](https://github.com/sofiadelpozo) at [collective.ai](https://collectiveai.io) ([email](mailto:sofia.delpozo@collectiveai.io)) +- **Paolo Donizetti** - [@padonizetti](https://github.com/padonizetti) at [collective.ai](https://collectiveai.io) ([email](mailto:paolo@collectiveai.io)) +- **Conrado Beatriz** - [@conrabeatriz](https://github.com/conrabeatriz) at [collective.ai](https://collectiveai.io) ([email](mailto:conrado@collectiveai.io)) + +## Citar AymurAI +Si usás AymurAI en investigación o publicaciones, por favor citá: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` + +``` +@inproceedings{10.1145/3706598.3713681, + author = {Ciolfi Felice, Marianela and Feldfeber, Ivana and Glasserman Apicella, Carolina and Quiroga, Yasm\'{\i}n Bel\'{e}n and Ansaldo, Juli\'{a}n and Lapenna, Luciano and Bezchinsky, Santiago and Barriga Rubio, Ra\'{u}l and Garc\'{\i}a, Mail\'{e}n}, + title = {Doing the Feminist Work in AI: Reflections from an AI Project in Latin America}, + booktitle = {Proceedings of the 2025 CHI Conference on Human Factors in Computing Systems}, + series = {CHI '25}, + year = {2025}, + isbn = {9798400713941}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + doi = {10.1145/3706598.3713681}, + url = {https://doi.org/10.1145/3706598.3713681}, + abstract = {The contemporary AI development landscape is dominated by big corporations, lacks diversity, and mostly centres the Global North, or applies extractivist logics in the South. This paper showcases a feminist process of AI development from Latin America, where we created an interactive, AI-powered tool that helps criminal court officers open justice data, addressing a data gap on gender-based violence. Through a collaborative autoethnography, drawing from Latin American feminisms, we unpack and visibilize the feminist work that was required, as a crucial step to counter hegemonic narratives. Foregrounding the subjugated knowledges of our experiences, we offer a concrete example of a feminist approach to AI development grounded in practice. With this, we aim to critically inspire those who consider building technology in service of social justice causes, or who choose to build AI systems otherwise.}, + articleno = {998}, + numpages = {18}, + keywords = {Global South, NGO, activism, critical HCI, critical computing, duoethnography, feminist AI, feminist research}, + location = {}, +} +``` + +Para usar la cita más actualizada, consulta la referencia del proyecto en el repositorio de la organización: [github.com/aymurai](https://github.com/aymurai). + +## Licencia +AymurAI es software de código abierto bajo licencia [MIT](LICENSE.md). Esta licencia permite modificar, distribuir y usar de forma privada el software, siempre que se mantenga el crédito correspondiente a la autoría original. diff --git a/README.md b/README.md index d140e5ad..fae091de 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,162 @@ # AymurAI Backend -This repository contains the backend API and machine learning models for [AymurAI](https://www.aymurai.info), a tool designed to generate anonymized datasets from judicial rulings related to gender-based violence (GBV). +Language: **English** | [Español](README.es.md) -AymurAI's backend is responsible for managing the interaction between the frontend and the machine learning models. It provides an API that handles data input, automates the extraction of information from court rulings, and document edition for anonymization purposes. +AymurAI Backend provides the API and ML pipelines used to process judicial rulings for two main workflows: +- `anonymizer`: extract named entities and produce anonymized documents. +- `data-public`: extract structured information for public dataset curation. -## Table of Contents -* [About AymurAI, its Uses and Limitations](#about-aymurai-its-uses-and-limitations) -* [Deployment](#deployment) -* [Pipeline](#pipeline) -* [Tutorials](#tutorials) -* [Contributing](#contributing) -* [Contributors](#contributors) -* [Citing AymurAI](#citing-aymurai) -* [License](#license) +This repository contains the FastAPI service, production pipeline configs, and database persistence used by both workflows. +## About AymurAI +AymurAI is a project focused on supporting the generation of anonymized and structured judicial data for gender-based violence (GBV) cases in Latin America. The backend service orchestrates document ingestion, ML inference, validation persistence, and document export for downstream operational and research uses. -## About AymurAI, its Uses and Limitations -AymurAI is a tool designed to address the lack of available data in the judicial system regarding gender-based violence (GBV) rulings in Latin America. Its goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI generates and maintains anonymized datasets from legal rulings to better understand GBV and support policy-making, while also contributing to campaigns run by feminist collectives. +This repository is backend-focused: it exposes APIs consumed by the frontend and runs the production pipelines for `anonymizer` and `data-public`. -AymurAI is still a prototype and is currently only implemented in Criminal Court N°10 in the City of Buenos Aires, Argentina. Its capabilities are limited to semi-automated data collection and analysis. The quality, consistency, and availability of the data, as well as cooperation from court officials and the broader cultural and political context, may affect its results. +## Documentation +- Technical docs index: [docs/README.md](docs/README.md) +- API reference: [docs/api/README.md](docs/api/README.md) +- Pipelines index: [docs/pipelines/README.md](docs/pipelines/README.md) +- Anonymizer flow: [docs/pipelines/anonymizer/README.md](docs/pipelines/anonymizer/README.md) +- Datapublic flow: [docs/pipelines/datapublic/README.md](docs/pipelines/datapublic/README.md) +- Internal database schema: [docs/database/README.md](docs/database/README.md) -The models were trained using closed datasets from an Argentine criminal court and are specifically tailored to extract relevant information from GBV-related rulings. The domain-specific training ensures the models' accuracy within this legal and cultural context, though they may not be applicable to other regions with different legal systems or cultural norms. +## Quick Start (Docker Image) +Run the full API image (includes production resources): +```bash +docker run -d --name aymurai-backend -p 8899:8899 ghcr.io/aymurai/api:full +``` -## Deployment -AymurAI's backend is deployed using [Docker](https://www.docker.com/). The Docker images are available at the following registry: +Optional: persist DB/cache outside the container (host volume mounted at `/resources/cache`): ```bash -ghcr.io/aymurai/api:full +mkdir -p ./aymurai-cache + +docker run -d --name aymurai-backend -p 8899:8899 \ + -v "$(pwd)/aymurai-cache:/resources/cache" \ + ghcr.io/aymurai/api:full ``` -### Quick Start -To deploy a production-ready instance of the API, run: +Optional: GPU runtime (requires NVIDIA Container Toolkit): ```bash -docker run -d -p 8899:8899 ghcr.io/aymurai/api:full +docker run -d --name aymurai-backend-gpu --gpus all \ + -e TORCH_DEVICE=cuda \ + -p 8899:8899 \ + ghcr.io/aymurai/api:full ``` -This command will start the API on port `8899` on your local machine. You can access the API documentation through OpenAPI at: +Open Swagger UI: -``` +```text http://localhost:8899/docs ``` -Once it is deployed, it doesn't require an internet connection to work. - -### Running on a Closed Network -If you need to deploy in an environment without internet access, export the Docker image by running: +## Quick Start (Docker Compose) +Use the services defined in `docker-compose.yml`: ```bash -docker image save ghcr.io/aymurai/api:full -o aymurai-api.tar -``` - -Transfer the image to the target machine and load it: +# CPU, lightweight API profile +make api-up -```bash -docker load -i aymurai-api.tar -``` +# CPU, full API profile +make api-full-up -For more information on Docker deployment, refer to the [Docker documentation](https://docs.docker.com/). If you need further assistance, feel free to contact us at [aymurai@datagenero.org](mailto:aymurai@datagenero.org). +# GPU, lightweight API profile +API_SERVICE=aymurai-api-gpu make api-up +# GPU, full API profile +API_FULL_SERVICE=aymurai-api-full-gpu make api-full-up +``` -## Pipeline -AymurAI’s backend utilizes a structured data processing pipeline to handle anonymized legal rulings and extract relevant information. This data is processed and made accessible via the API. For more details, please refer to the [pipeline documentation](docs/pipeline/README.md). +Check logs: +```bash +make api-logs +# or make api-full-logs +``` -## Tutorials -To get started with AymurAI, refer to our [tutorials](tutorials/GET_STARTED.md). These guides provide step-by-step instructions on setting up and using the AymurAI backend, including configuration, example queries, and more. +## Runtime Overview +- Framework: `FastAPI` +- Default API port: `8899` +- DB engine: `SQLModel` + Alembic migrations on startup +- Default DB URI: `sqlite:////resources/cache/sqlite/database.db` +- Production pipeline configs: + - `resources/pipelines/production/flair-anonymizer/pipeline.json` + - `resources/pipelines/production/datapublic/pipeline.json` + +## Main Public Endpoints +- `GET /server/healthcheck` +- `GET /server/stats/summary` +- `POST /misc/document-extract` (and deprecated alias `POST /document-extract`) +- `POST /anonymizer/predict` +- `POST /anonymizer/disambiguate` +- `POST /anonymizer/validation` +- `POST /anonymizer/anonymize-document` +- `POST /datapublic/predict/{document_id}` +- `GET /datapublic/validation/document/{document_id}` +- `POST /datapublic/validation/document/{document_id}` + +For full request/response contracts and examples, see [docs/api/README.md](docs/api/README.md). + +## Closed-Network Deployment +To move an image into a closed environment: +```bash +docker image save ghcr.io/aymurai/api:full -o aymurai-api-full.tar +docker load -i aymurai-api-full.tar +``` ## Contributing -Thank you for your interest in contributing to AymurAI! There are many ways to get involved, from improving documentation to enhancing the codebase. To get started, please review our [contributor guidelines](docs/CONTRIBUTING.md) and our [code of conduct](docs/CODE_OF_CONDUCT.md). We welcome contributions in areas such as expanding the API and improving the data processing pipeline. +Contributions are welcome across documentation, API, and pipeline improvements. +- Contributing guide: [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) +- Security and ethics: [docs/SECURITY.md](docs/SECURITY.md) +- Code of conduct: [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md) ## Contributors -* **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) -* **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) +- **Julián Ansaldo** - [@jansaldo](https://github.com/jansaldo) at [collective.ai](https://collectiveai.io) ([email](mailto:juli@collectiveai.io)) +- **Raúl Barriga** - [@jedzill4](https://github.com/jedzill4) at [collective.ai](https://collectiveai.io) ([email](mailto:r@collectiveai.io)) +- **Sofía del Pozo** - [@sofiadelpozo](https://github.com/sofiadelpozo) at [collective.ai](https://collectiveai.io) ([email](mailto:sofia.delpozo@collectiveai.io)) +- **Paolo Donizetti** - [@padonizetti](https://github.com/padonizetti) at [collective.ai](https://collectiveai.io) ([email](mailto:paolo@collectiveai.io)) +- **Conrado Beatriz** - [@conrabeatriz](https://github.com/conrabeatriz) at [collective.ai](https://collectiveai.io) ([email](mailto:conrado@collectiveai.io)) ## Citing AymurAI -If you use AymurAI in your research or any publication, please cite the following paper to acknowledge our work: +If you use AymurAI in research or publications, please cite: ```bibtex @techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` + +``` +@inproceedings{10.1145/3706598.3713681, + author = {Ciolfi Felice, Marianela and Feldfeber, Ivana and Glasserman Apicella, Carolina and Quiroga, Yasm\'{\i}n Bel\'{e}n and Ansaldo, Juli\'{a}n and Lapenna, Luciano and Bezchinsky, Santiago and Barriga Rubio, Ra\'{u}l and Garc\'{\i}a, Mail\'{e}n}, + title = {Doing the Feminist Work in AI: Reflections from an AI Project in Latin America}, + booktitle = {Proceedings of the 2025 CHI Conference on Human Factors in Computing Systems}, + series = {CHI '25}, + year = {2025}, + isbn = {9798400713941}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + doi = {10.1145/3706598.3713681}, + url = {https://doi.org/10.1145/3706598.3713681}, + abstract = {The contemporary AI development landscape is dominated by big corporations, lacks diversity, and mostly centres the Global North, or applies extractivist logics in the South. This paper showcases a feminist process of AI development from Latin America, where we created an interactive, AI-powered tool that helps criminal court officers open justice data, addressing a data gap on gender-based violence. Through a collaborative autoethnography, drawing from Latin American feminisms, we unpack and visibilize the feminist work that was required, as a crucial step to counter hegemonic narratives. Foregrounding the subjugated knowledges of our experiences, we offer a concrete example of a feminist approach to AI development grounded in practice. With this, we aim to critically inspire those who consider building technology in service of social justice causes, or who choose to build AI systems otherwise.}, + articleno = {998}, + numpages = {18}, + keywords = {Global South, NGO, activism, critical HCI, critical computing, duoethnography, feminist AI, feminist research}, + location = {}, } ``` -Proper citation helps us continue developing AymurAI and supporting the community. +For the most up-to-date citation, please use the project-level reference in the organization repository: [github.com/aymurai](https://github.com/aymurai). ## License -AymurAI is open-source software licensed under the [MIT License](LICENSE.md). This license allows for modification, distribution, and private use, provided that appropriate credit is given to the original authors. +AymurAI is open-source software licensed under the [MIT License](LICENSE.md). This license allows modification, distribution, and private use, provided that appropriate credit is given to the original authors. diff --git a/aymurai/alembic.ini b/aymurai/alembic.ini index b9a6bd2c..87f2df84 100644 --- a/aymurai/alembic.ini +++ b/aymurai/alembic.ini @@ -68,11 +68,11 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # on newly generated revision scripts. See the documentation for further # detail and examples -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME +# format using "ruff format" - use the exec runner, execute a binary +# hooks = ruff_format +# ruff_format.type = exec +# ruff_format.executable = ruff +# ruff_format.options = format REVISION_SCRIPT_FILENAME # lint with attempts to fix using "ruff" - use the exec runner, execute a binary # hooks = ruff diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py index 00a562dd..6fde1db7 100644 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py @@ -2,10 +2,11 @@ import os import subprocess import tempfile +from collections.abc import Iterable from threading import Lock import torch -from fastapi import Body, Depends, Form, Query, UploadFile +from fastapi import Body, Depends, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from fastapi.routing import APIRouter from sqlmodel import Session @@ -18,7 +19,7 @@ anonymization_paragraph_create, anonymization_paragraph_read, ) -from aymurai.database.schema import AnonymizationParagraph +from aymurai.database.schema import AnonymizationParagraph, AnonymizationParagraphCreate from aymurai.database.session import get_session from aymurai.database.utils import data_to_uuid, text_to_uuid from aymurai.logger import get_logger @@ -26,11 +27,21 @@ DocLabel, DocumentAnnotations, DocumentInformation, + LabelPolicy, + RenderPolicy, TextRequest, ) from aymurai.settings import settings -from aymurai.text.anonymization import DocAnonymizer +from aymurai.text.anonymization import ( + InvalidDocumentAnonymizer, + get_anonymizer, +) from aymurai.text.extraction import MIMETYPE_EXTENSION_MAPPER +from aymurai.utils.entity_disambiguation import ( + build_canonical_entities, + get_canonical_dates, + map_canonical_entities_ner_preds, +) from aymurai.utils.misc import get_element logger = get_logger(__name__) @@ -44,6 +55,284 @@ router = APIRouter() +def _entities_to_doclabels(entities: list[dict]) -> list[DocLabel]: + """ + Convert raw entities to DocLabel objects. + + Args: + entities (list[dict]): List of entity dictionaries. + + Returns: + list[DocLabel]: List of DocLabel objects. + """ + doclabels: list[DocLabel] = [] + + for ent in entities: + try: + if isinstance(ent, DocLabel): + doclabel = ent + else: + doclabel = DocLabel.model_validate( + { + "text": ent.get("text", ""), + "start_char": ent.get("start_char"), + "end_char": ent.get("end_char"), + "attrs": ent.get("attrs", {}), + } + ) + doclabels.append(doclabel) + except Exception as exc: # keep going if a single entity is malformed + logger.warning(f"Skipping invalid entity for DocLabel: {exc}") + + return _dedupe_doclabels(doclabels) + + +def _dedupe_doclabels(labels: Iterable[DocLabel]) -> list[DocLabel]: + """ + Merge duplicate labels for the same span and AymurAI label. + + Args: + labels (Iterable[DocLabel]): An iterable of DocLabel objects, + potentially containing duplicates. + + Returns: + list[DocLabel]: A list of DocLabel objects with duplicates merged, + preserving the order of first occurrence. + """ + deduped: list[DocLabel] = [] + index_by_key: dict[tuple[int, int, str], int] = {} + + for label in labels: + key = ( + label.start_char, + label.end_char, + label.attrs.aymurai_label if label.attrs else "", + ) + existing_index = index_by_key.get(key) + if existing_index is None: + index_by_key[key] = len(deduped) + deduped.append(label) + continue + + existing = deduped[existing_index] + existing_data = existing.model_dump(mode="json") + incoming_data = label.model_dump(mode="json") + existing_attrs = existing_data.get("attrs") or {} + incoming_attrs = incoming_data.get("attrs") or {} + + for attr_key, incoming_value in incoming_attrs.items(): + existing_value = existing_attrs.get(attr_key) + if attr_key == "aymurai_label_subclass": + merged = list(existing_value or []) + for subclass in incoming_value or []: + if subclass not in merged: + merged.append(subclass) + existing_attrs[attr_key] = merged + elif existing_value in (None, [], "") and incoming_value not in ( + None, + [], + "", + ): + existing_attrs[attr_key] = incoming_value + + existing_data["attrs"] = existing_attrs + deduped[existing_index] = DocLabel.model_validate(existing_data) + + return deduped + + +def _merge_label_policies( + request_policies: dict[str, LabelPolicy] | None, +) -> dict[str, LabelPolicy]: + """ + Merges label policies from settings and request, with request policies taking precedence. + + Args: + request_policies (dict[str, LabelPolicy] | None): Per-label policies provided in the request body. + + Returns: + dict[str, LabelPolicy]: Effective per-label policies after merging settings and request policies. + """ + policies: dict[str, LabelPolicy] = {} + + if settings.DISAMBIGUATION_LABEL_POLICIES: + for label, policy in settings.DISAMBIGUATION_LABEL_POLICIES.items(): + incoming = LabelPolicy.model_validate(policy) + current = policies.get(label, LabelPolicy()) + if incoming.disambiguation is not None: + current.disambiguation = incoming.disambiguation + if incoming.anonymize is not None: + current.anonymize = incoming.anonymize + if incoming.use_subclass_when_available is not None: + current.use_subclass_when_available = ( + incoming.use_subclass_when_available + ) + policies[label] = current + + if request_policies: + for label, policy in request_policies.items(): + incoming = LabelPolicy.model_validate(policy) + current = policies.get(label, LabelPolicy()) + if incoming.disambiguation is not None: + current.disambiguation = incoming.disambiguation + if incoming.anonymize is not None: + current.anonymize = incoming.anonymize + if incoming.use_subclass_when_available is not None: + current.use_subclass_when_available = ( + incoming.use_subclass_when_available + ) + policies[label] = current + + return policies + + +def _merge_render_policy( + request_policy: RenderPolicy | None, +) -> RenderPolicy: + """ + Merges render policies from settings and request, with request policy taking precedence. + + Args: + request_policy (RenderPolicy | None): Render policy from request. + + Returns: + RenderPolicy: Effective render policy. + """ + policy = RenderPolicy( + suffix_mode="auto", + suffix_threshold=1, + ) + + def apply(incoming: RenderPolicy) -> None: + nonlocal policy + if incoming.suffix_mode is not None: + policy.suffix_mode = incoming.suffix_mode + if incoming.suffix_threshold is not None: + policy.suffix_threshold = incoming.suffix_threshold + + if settings.RENDER_POLICY: + apply(RenderPolicy.model_validate(settings.RENDER_POLICY)) + + if request_policy: + apply(RenderPolicy.model_validate(request_policy)) + + return policy + + +def _resolve_token_base( + label: DocLabel, + label_policy: LabelPolicy, +) -> str: + """ + Resolves the base token label for rendering. + + Args: + label (DocLabel): Label to render. + label_policy (LabelPolicy): Label policy rules. + + Returns: + str: Base token label (e.g., PER, DENUNCIANTE). + """ + attrs = label.attrs + if attrs is None: + return label.text + + subclass = None + if attrs.aymurai_label_subclass: + subclass = attrs.aymurai_label_subclass[0] + + if label_policy.use_subclass_when_available and subclass: + return subclass.upper() + + return attrs.aymurai_label + + +def _build_render_context( + annotations: list[DocumentInformation], + render_policy: RenderPolicy, + label_policies: dict[str, LabelPolicy], +) -> dict: + """ + Builds render context with per-entity indices and counts. + + Args: + annotations (list[DocumentInformation]): Document annotations. + render_policy (RenderPolicy): Render policy rules. + label_policies (dict[str, LabelPolicy]): Per-label policies. + + Returns: + dict: Render context with policy, indices, and counts. + """ + occurrences: list[tuple[int, int, str, str]] = [] + + for p_idx, paragraph in enumerate(annotations): + for label in paragraph.labels or []: + label_policy = label_policies.get( + label.attrs.aymurai_label + if label.attrs and label.attrs.aymurai_label + else None, + LabelPolicy(), + ) + base = _resolve_token_base(label, label_policy) + entity_id = ( + str(label.attrs.canonical_entity_id) + if label.attrs and label.attrs.canonical_entity_id + else label.text + ) + occurrences.append((p_idx, label.start_char, base, entity_id)) + + occurrences.sort(key=lambda item: (item[0], item[1])) + + index_by_entity: dict[tuple[str, str], int] = {} + next_index_by_base: dict[str, int] = {} + + for _, _, base, entity_id in occurrences: + key = (base, entity_id) + if key not in index_by_entity: + next_index_by_base[base] = next_index_by_base.get(base, 0) + 1 + index_by_entity[key] = next_index_by_base[base] + + count_by_base = {base: count for base, count in next_index_by_base.items()} + + return { + "render_policy": render_policy, + "label_policies": label_policies, + "index_by_entity": index_by_entity, + "count_by_base": count_by_base, + } + + +def _should_anonymize_label( + label: DocLabel, + label_policies: dict[str, LabelPolicy], +) -> bool: + """ + Determines whether a given label should be anonymized based on its attributes and the effective label policies. + + Args: + label (DocLabel): The document label to evaluate for anonymization. + label_policies (dict[str, LabelPolicy]): Effective per-label policies that may override default anonymization behavior. + + Returns: + bool: True if the label should be anonymized, False otherwise. + """ + if label.attrs and label.attrs.aymurai_anonymize is not None: + return bool(label.attrs.aymurai_anonymize) + + policy = ( + label_policies.get(str(label.attrs.aymurai_label).strip().upper()) + if label.attrs and label.attrs.aymurai_label + else None + ) + if policy is None: + return True + + if policy.anonymize is None: + return True + + return bool(policy.anonymize) + + # MARK: Predict @router.post("/predict", response_model=DocumentInformation) async def anonymizer_paragraph_predict( @@ -78,8 +367,8 @@ async def anonymizer_paragraph_predict( logger.info(f"cache loaded from key: {paragraph_id}") logger.debug(f"{cached_prediction}") - labels = cached_prediction.prediction - return DocumentInformation(document=cached_prediction.text, labels=labels or []) + labels = _entities_to_doclabels(cached_prediction.prediction or []) + return DocumentInformation(document=cached_prediction.text, labels=labels) logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": text_request.text}}] @@ -93,18 +382,153 @@ async def anonymizer_paragraph_predict( processed = pipeline.postprocess([processed]) text = get_element(processed[0], ["data", "doc.text"]) or "" - labels = get_element(processed[0], ["predictions", "entities"]) or [] + raw_entities = get_element(processed[0], ["predictions", "entities"]) or [] + labels = _entities_to_doclabels(raw_entities) if use_cache: logger.info(f"saving in cache: {paragraph_id}") - paragraph = AnonymizationParagraph( - id=paragraph_id, + paragraph = AnonymizationParagraphCreate( text=text, prediction=labels, ) paragraph = anonymization_paragraph_create(paragraph, session=session) - return DocumentInformation(document=text, labels=paragraph.prediction) + return DocumentInformation(document=text, labels=labels) + + +# MARK: Disambiguate +@router.post("/disambiguate", response_model=DocumentAnnotations) +async def anonymizer_disambiguate( + paragraphs: list[DocumentInformation] = Body( + ..., + description=( + "List of per-paragraph predictions returned by /anonymizer/predict." + ), + ), + label_policies: dict[str, LabelPolicy] | None = Body( + None, + description=( + "Optional per-label policy overrides for disambiguation/anonymization." + ), + ), + session: Session = Depends(get_session), +) -> DocumentAnnotations: + """ + Performs canonical entity disambiguation using fuzzy matching. + + Args: + paragraphs: A list of DocumentInformation objects containing the NER + predictions per paragraph that need to be disambiguated. + label_policies: Optional per-label disambiguation/anonymization policies. + session: Database session dependency for caching results. + Returns: + DocumentAnnotations: The original annotations enriched with + 'canonical_entity_id' and 'role' fields for each resolved mention. + """ + logger.info( + "disambiguation start: paragraphs=%d", + len(paragraphs), + ) + + labels = [label for paragraph in paragraphs for label in (paragraph.labels or [])] + effective_label_policies = _merge_label_policies(label_policies) + logger.info("disambiguation labels: %d", len(labels)) + + all_detected_labels = { + label.attrs.aymurai_label + for label in labels + if label.attrs + and label.attrs.aymurai_label + and effective_label_policies.get(label.attrs.aymurai_label) + and effective_label_policies.get(label.attrs.aymurai_label).anonymize + } + + fuzzy_labels: set[str] = set() + + for label in all_detected_labels: + policy = effective_label_policies.get(label) + if policy and policy.disambiguation == "fuzzy": + fuzzy_labels.add(label) + continue + if policy and policy.disambiguation == "none": + continue + + fuzzy_labels.add(label) + + effective_disambiguation_by_label: dict[str, str] = {} + for label in all_detected_labels: + if label in fuzzy_labels: + effective_disambiguation_by_label[label] = "fuzzy" + else: + effective_disambiguation_by_label[label] = "none" + logger.info( + "disambiguation targets: detected=%s fuzzy=%s", + sorted(all_detected_labels), + sorted(fuzzy_labels), + ) + + canonical_entities = ( + build_canonical_entities( + labels, + target_labels=[label for label in fuzzy_labels if label != "FECHA"] + if fuzzy_labels + else None, + threshold=settings.THRESHOLD, + ) + if fuzzy_labels + else [] + ) + + logger.info("canonical entities detected were: %s", canonical_entities) + + if "FECHA" in fuzzy_labels: + canonical_entities += get_canonical_dates(labels) + + logger.info("canonical entities after adding dates: %s", canonical_entities) + + logger.info( + "fuzzy clustering produced %d canonical entities", len(canonical_entities) + ) + + logger.info( + "disambiguation merge: total=%d", + len(canonical_entities), + ) + + predictions = map_canonical_entities_ner_preds( + predictions=paragraphs, + canonical_entities=canonical_entities, + ) + + for document in predictions: + document.labels = _dedupe_doclabels(document.labels or []) + for label in document.labels or []: + label.attrs.aymurai_disambiguation = effective_disambiguation_by_label.get( + label.attrs.aymurai_label, "fuzzy" + ) + + policy = effective_label_policies.get(label.attrs.aymurai_label) + label.attrs.aymurai_anonymize = ( + policy.anonymize if policy and policy.anonymize is not None else True + ) + + paragraph_updates = [ + AnonymizationParagraphCreate( + text=paragraph.document, + prediction=paragraph.labels or [], + ) + for paragraph in predictions + ] + anonymization_paragraph_batch_create_update(paragraph_updates, session=session) + logger.info( + "disambiguation persisted predictions for %d paragraphs", + len(paragraph_updates), + ) + + return DocumentAnnotations( + data=predictions, + label_policies=effective_label_policies if effective_label_policies else None, + ) # MARK: Validate @@ -155,11 +579,21 @@ async def anonymizer_compile_document( """ logger.info(f"receiving => {file.filename}") extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) - logger.info(f"detection extension: {extension} ({file.content_type})") + file_suffix = os.path.splitext(file.filename or "")[1].lower() + + if extension is None and file_suffix: + extension = file_suffix.lstrip(".") + + if extension not in {"docx", "pdf"}: + raise HTTPException( + status_code=400, + detail=f"Unsupported format for anonymization: {extension or 'unknown'}", + ) + + logger.info(f"detected extension: {extension} ({file.content_type})") # Create a temporary file - _, suffix = os.path.splitext(file.filename) - suffix = suffix if suffix == ".docx" else ".txt" + suffix = f".{extension}" tmp_dir = tempfile.gettempdir() # Use delete=False to avoid the file being deleted when the NamedTemporaryFile object is closed @@ -178,13 +612,14 @@ async def anonymizer_compile_document( annots_json = json.loads(annotations) annots = DocumentAnnotations.model_validate(annots_json) - logger.info(f"processing annotations => {annots}") + + effective_label_policies = _merge_label_policies(annots.label_policies) + effective_render_policy = _merge_render_policy(annots.render_policy) # Add paragraphs to the database # validation MUST be at least an empty list, to remember user feedback paragraphs = [ - AnonymizationParagraph( - id=text_to_uuid(paragraph.document), + AnonymizationParagraphCreate( text=paragraph.document, validation=paragraph.labels or [], ) @@ -202,35 +637,54 @@ async def anonymizer_compile_document( override=False, ) - # Anonymize the document - doc_anonymizer = DocAnonymizer() + filtered_annotations = [] + for paragraph in annots.data: + filtered_labels = [ + label + for label in (paragraph.labels or []) + if _should_anonymize_label(label, effective_label_policies) + ] + filtered_annotations.append( + DocumentInformation( + document=paragraph.document, + labels=filtered_labels, + ) + ) + + render_context = _build_render_context( + filtered_annotations, effective_render_policy, effective_label_policies + ) + + preds = [ + document_information.model_dump(mode="json", exclude_none=True) + for document_information in filtered_annotations + ] - if suffix == ".docx": - item = {"path": tmp_filename} - doc_anonymizer( - item, - [document_information.model_dump() for document_information in annots.data], + try: + anonymizer = get_anonymizer(extension) + anonymized_path = anonymizer( + {"path": tmp_filename}, + preds, tmp_dir, + render_context=render_context, + ) + except (ValueError, InvalidDocumentAnonymizer) as exc: + if os.path.exists(tmp_filename): + os.remove(tmp_filename) + raise HTTPException(status_code=400, detail=str(exc)) from exc + + if extension == "pdf": + if os.path.exists(tmp_filename): + os.remove(tmp_filename) + + return FileResponse( + anonymized_path, + background=BackgroundTask(os.remove, anonymized_path), + media_type="application/pdf", + filename=f"{os.path.splitext(file.filename)[0]}.pdf", ) - logger.info(f"saved temp file on local storage => {tmp_filename}") - - else: - # Export as raw document - anonymized_doc = [ - doc_anonymizer.replace_labels_in_text(document_information.model_dump()) - .replace("<", "<") - .replace(">", ">") - for document_information in annots.data - ] - with open(tmp_filename, "w") as f: - f.write("\n".join(anonymized_doc)) - - # Add watermark to the end of the document - f.write( - "\n\nDocumento anonimizado por AymurAI\n\nhttps://www.aymurai.info/" - ) - # Convert to ODT + # DOCX flow keeps ODT output cmd = [ settings.LIBREOFFICE_BIN, "--headless", @@ -238,9 +692,8 @@ async def anonymizer_compile_document( "odt", "--outdir", tmp_dir, - tmp_filename, + anonymized_path, ] - logger.info(f"Executing: {' '.join(cmd)}") try: @@ -248,20 +701,20 @@ async def anonymizer_compile_document( cmd, shell=False, encoding="utf-8", errors="ignore" ) logger.info(f"LibreOffice output: {output}") - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError as exc: raise RuntimeError( - f"LibreOffice conversion failed: {e.output.decode('utf-8', errors='ignore')}" - ) + f"LibreOffice conversion failed: {exc.output.decode('utf-8', errors='ignore')}" + ) from exc + finally: + if os.path.exists(tmp_filename): + os.remove(tmp_filename) - odt = tmp_filename.replace(suffix, ".odt") + odt = f"{os.path.splitext(anonymized_path)[0]}.odt" logger.info(f"Expected output file path: {odt}") if not os.path.exists(odt): raise RuntimeError(f"File at path {odt} does not exist.") - # Ensure the temporary file is deleted - os.remove(tmp_filename) - return FileResponse( odt, background=BackgroundTask(os.remove, odt), diff --git a/aymurai/api/endpoints/routers/datapublic/datapublic.py b/aymurai/api/endpoints/routers/datapublic/datapublic.py index b83ae0d0..91e1f0d5 100644 --- a/aymurai/api/endpoints/routers/datapublic/datapublic.py +++ b/aymurai/api/endpoints/routers/datapublic/datapublic.py @@ -2,16 +2,16 @@ from threading import Lock import torch -from fastapi import Body, Depends, Query, HTTPException +from fastapi import Body, Depends, HTTPException, Query from fastapi.routing import APIRouter from pydantic import UUID5 from sqlmodel import Session from aymurai.api.utils import load_pipeline from aymurai.database.schema import ( - DataPublicParagraph, DataPublicDocument, DataPublicDocumentParagraph, + DataPublicParagraph, ) from aymurai.database.session import get_session from aymurai.database.utils import text_to_uuid @@ -62,7 +62,7 @@ async def predict_over_text( logger.info("Running prediction") item = [{"path": "empty", "data": {"doc.text": text_request.text}}] pipeline = load_pipeline( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") ) with pipeline_lock: @@ -72,6 +72,7 @@ async def predict_over_text( text = get_element(processed[0], ["data", "doc.text"]) or "" labels = get_element(processed[0], ["predictions", "entities"]) or [] + paragraph: DataPublicParagraph | None = None if use_cache: logger.info(f"saving in cache: {paragraph_id}") @@ -92,7 +93,9 @@ async def predict_over_text( # paragraph = datapublic_paragraph_create(paragraph, session=session) - return DocumentInformation(document=text, labels=paragraph.prediction) + return DocumentInformation( + document=text, labels=paragraph.prediction if paragraph else labels + ) # MARK: Validate Paragraph diff --git a/aymurai/api/endpoints/routers/misc/document_extract.py b/aymurai/api/endpoints/routers/misc/document_extract.py index be5d4d63..ab1c61b3 100644 --- a/aymurai/api/endpoints/routers/misc/document_extract.py +++ b/aymurai/api/endpoints/routers/misc/document_extract.py @@ -23,19 +23,26 @@ def extraction(path: str) -> str: """ Wrapper function to call the extract_document function. This is necessary to ensure that the function can be pickled and run in a separate process. + + Args: + path (str): Path to the file to be processed. + + Returns: + str: Extracted text from the document. """ text = extract_document(path) - return document_normalize(text) if text else "" + return document_normalize(text, preserve_paragraphs=True) if text else "" -def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: +def run_safe_text_extraction(path: str, timeout_s: float | None = 300) -> str: """ Runs the text extraction in a separate process to avoid blocking the main thread. This is useful for long-running tasks or when the extraction might hang. Args: path (str): Path to the file to be processed. - timeout_s (float): Timeout in seconds for the extraction process. Defaults to 30 seconds. + timeout_s (float | None): Timeout in seconds for the extraction process. + If None, waits indefinitely. Defaults to 300. Returns: str: Extracted text from the document. @@ -43,7 +50,6 @@ def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: Raises: TimeoutError: If the extraction process exceeds the specified timeout. """ - with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(extraction, path) try: @@ -54,8 +60,31 @@ def run_safe_text_extraction(path: str, timeout_s: float = 30) -> str: raise +def _split_document_paragraphs(document: str) -> list[str]: + if re.search(r"\n\s*\n+", document): + raw_paragraphs = re.split(r"\n\s*\n+", document) + else: + raw_paragraphs = document.splitlines() + + paragraphs = [ + re.sub(r"[ \t]{2,}", " ", paragraph.strip()) + for paragraph in raw_paragraphs + if paragraph.strip() + ] + return list(unique_justseen(paragraphs)) + + @router.post("/document-extract", response_model=Document) def plain_text_extractor(file: UploadFile) -> Document: + """ + Extract plain text from an uploaded document. + + Args: + file (UploadFile): Incoming document upload. + + Returns: + Document: Extracted and normalized document payload. + """ logger.info(f"receiving => {file.filename}") extension = MIMETYPE_EXTENSION_MAPPER.get(file.content_type) logger.info(f"detected extension: {extension} ({file.content_type})") @@ -93,9 +122,6 @@ def plain_text_extractor(file: UploadFile) -> Document: logger.info(f"removed temp file from local storage => {tmp_filename}") document_id = data_to_uuid(data) - - paragraphs = [line.strip() for line in document.split("\n") if line.strip()] - paragraphs = [re.sub(r"\s{2,}", " ", line) for line in paragraphs] - paragraphs = list(unique_justseen(paragraphs)) + paragraphs = _split_document_paragraphs(document) return Document(document=paragraphs, document_id=document_id) diff --git a/aymurai/api/main.py b/aymurai/api/main.py index d814a3f6..4bfd9370 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -8,6 +8,7 @@ from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware +from starlette.formparsers import MultiPartParser from aymurai.api import core from aymurai.logger import get_logger @@ -27,6 +28,22 @@ RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH +MULTIPART_MAX_PART_SIZE = 10 * 1024 * 1024 # 10 MB + + +def _set_kwdefault(func, name: str, value: int) -> None: + kwdefaults = getattr(func, "__kwdefaults__", None) + if kwdefaults and name in kwdefaults: + kwdefaults[name] = value + + +# FastAPI parses Form(...) before endpoint execution. Starlette defaults each +# non-file multipart field to 1MB, which is too small for annotations JSON. +MultiPartParser.max_part_size = MULTIPART_MAX_PART_SIZE +_set_kwdefault(MultiPartParser.__init__, "max_part_size", MULTIPART_MAX_PART_SIZE) +_set_kwdefault(Request.form, "max_part_size", MULTIPART_MAX_PART_SIZE) +_set_kwdefault(Request._get_form, "max_part_size", MULTIPART_MAX_PART_SIZE) + @asynccontextmanager async def lifespan(app: FastAPI): @@ -109,5 +126,5 @@ def healthcheck(): os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") ) AymurAIPipeline.load( - os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "full-paragraph") + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") ) diff --git a/aymurai/database/crud/anonymization/paragraph.py b/aymurai/database/crud/anonymization/paragraph.py index 593ebbdc..cf1e30e5 100644 --- a/aymurai/database/crud/anonymization/paragraph.py +++ b/aymurai/database/crud/anonymization/paragraph.py @@ -1,5 +1,7 @@ +import json import uuid +from pydantic import TypeAdapter from sqlmodel import Session from aymurai.database.schema import ( @@ -8,9 +10,54 @@ AnonymizationParagraphUpdate, ) from aymurai.database.utils import text_to_uuid -from aymurai.logger import get_logger +from aymurai.meta.api_interfaces import DocLabel -logger = get_logger(__name__) +_DOC_LABELS_ADAPTER = TypeAdapter(list[DocLabel]) + + +def _serialize_doclabels(value: list[DocLabel] | None): + """ + Serializes DocLabel objects into JSON-compatible data structures. + + Args: + value (list[DocLabel] | None): DocLabel list to serialize. + + Returns: + list[dict] | None: JSON-safe list of labels, or None if input is None. + """ + if value is None: + return None + + labels = _DOC_LABELS_ADAPTER.dump_python(value, mode="json", exclude_none=True) + deduped = [] + seen = set() + + for label in labels: + key = json.dumps(label, sort_keys=True, separators=(",", ":")) + if key in seen: + continue + + seen.add(key) + deduped.append(label) + + return deduped + + +def _normalize_paragraph_payload(payload: dict) -> dict: + """ + Normalizes paragraph payload fields for JSON storage. + + Args: + payload (dict): Paragraph payload possibly containing DocLabel objects. + + Returns: + dict: Payload with JSON-serializable prediction/validation fields. + """ + if "prediction" in payload: + payload["prediction"] = _serialize_doclabels(payload.get("prediction")) + if "validation" in payload: + payload["validation"] = _serialize_doclabels(payload.get("validation")) + return payload def anonymization_paragraph_create( @@ -18,7 +65,19 @@ def anonymization_paragraph_create( session: Session, override: bool = False, ) -> AnonymizationParagraph: - new_paragraph = AnonymizationParagraph(**paragraph_in.model_dump()) + """ + Creates a new anonymization paragraph record. + + Args: + paragraph_in (AnonymizationParagraphCreate): Paragraph creation payload. + session (Session): Database session. + override (bool): If True, delete any existing paragraph with the same ID. + + Returns: + AnonymizationParagraph: The persisted paragraph record. + """ + payload = _normalize_paragraph_payload(paragraph_in.model_dump(exclude_none=True)) + new_paragraph = AnonymizationParagraph(**payload) if override: existing = session.get(AnonymizationParagraph, new_paragraph.id) @@ -36,6 +95,16 @@ def anonymization_paragraph_read( paragraph_id: uuid.UUID, session: Session, ) -> AnonymizationParagraph | None: + """ + Reads a paragraph record by ID. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID. + session (Session): Database session. + + Returns: + AnonymizationParagraph | None: Paragraph record if found. + """ return session.get(AnonymizationParagraph, paragraph_id) @@ -44,12 +113,26 @@ def anonymization_paragraph_update( paragraph_in: AnonymizationParagraphUpdate, session: Session, ) -> AnonymizationParagraph: + """ + Updates an existing paragraph record. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID to update. + paragraph_in (AnonymizationParagraphUpdate): Update payload. + session (Session): Database session. + + Returns: + AnonymizationParagraph: The updated paragraph record. + """ paragraph = session.get(AnonymizationParagraph, paragraph_id) if not paragraph: raise ValueError(f"Paragraph not found: {paragraph_id}") - for field, value in paragraph_in.model_dump(exclude_none=True).items(): + payload = _normalize_paragraph_payload( + paragraph_in.model_dump(exclude_none=True, mode="json") + ) + for field, value in payload.items(): setattr(paragraph, field, value) session.add(paragraph) @@ -59,6 +142,16 @@ def anonymization_paragraph_update( def anonymization_paragraph_delete(paragraph_id: uuid.UUID, session: Session): + """ + Deletes a paragraph record by ID. + + Args: + paragraph_id (uuid.UUID): Paragraph UUID to delete. + session (Session): Database session. + + Returns: + None + """ paragraph = session.get(AnonymizationParagraph, paragraph_id) if not paragraph: @@ -74,6 +167,16 @@ def anonymization_paragraph_delete(paragraph_id: uuid.UUID, session: Session): def anonymization_paragraph_batch_create_update( paragraphs_in: list[AnonymizationParagraphCreate], session: Session ) -> list[AnonymizationParagraph]: + """ + Creates or updates a batch of paragraph records. + + Args: + paragraphs_in (list[AnonymizationParagraphCreate]): Paragraph payloads. + session (Session): Database session. + + Returns: + list[AnonymizationParagraph]: Persisted paragraph records. + """ paragraphs = [] for p_in in paragraphs_in: @@ -81,13 +184,15 @@ def anonymization_paragraph_batch_create_update( paragraph = session.get(AnonymizationParagraph, paragraph_id) if paragraph: - update = AnonymizationParagraphUpdate(**p_in.model_dump()) - - for field, value in update.model_dump(exclude_none=True).items(): - setattr(paragraph, field, value) + payload = _normalize_paragraph_payload(p_in.model_dump(exclude_none=True)) + payload.pop("id", None) + for field, value in payload.items(): + if value is not None: + setattr(paragraph, field, value) else: - paragraph = AnonymizationParagraph(**p_in.model_dump()) + payload = _normalize_paragraph_payload(p_in.model_dump(exclude_none=True)) + paragraph = AnonymizationParagraph(**payload) session.add(paragraph) session.commit() diff --git a/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py b/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py index 94e896db..4cecf591 100644 --- a/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py +++ b/aymurai/datasets/ar_juz_pcyf_10/labelstudio/utils.py @@ -1,13 +1,13 @@ import re -from glob import glob from copy import deepcopy +from glob import glob from itertools import groupby +from more_itertools import collapse, unzip from numpy import cumsum -from more_itertools import unzip, collapse -from aymurai.meta.types import DataItem from aymurai.meta.entities import Entity +from aymurai.meta.types import DataItem from aymurai.utils.json_data import load_json @@ -43,6 +43,8 @@ def reformat_entity(text: str, span: dict) -> dict: end=span["end"], label=span["labels"][0], text=text[span["start"] : span["end"]], + start_char=span["start"], + end_char=span["end"], context_pre=text[soffset : span["start"]], context_post=text[span["end"] : eoffset], attrs={"aymurai_label": span["labels"][0]}, diff --git a/aymurai/datasets/ar_juz_pcyf_10/public.py b/aymurai/datasets/ar_juz_pcyf_10/public.py index 09b9870e..5c7b3cc6 100644 --- a/aymurai/datasets/ar_juz_pcyf_10/public.py +++ b/aymurai/datasets/ar_juz_pcyf_10/public.py @@ -1,17 +1,17 @@ -import sys import logging -import subprocess +import sys +from collections import UserList +from datetime import date, time from pathlib import Path from typing import Any, Union -from collections import UserList -from datetime import date, time, datetime import datasets import pandas as pd -from aymurai.text.extraction import get_extension from aymurai.datasets.ar_juz_pcyf_10.common import BASE, FIELDS +from aymurai.text.extraction import get_extension from aymurai.utils.cache import cache_load, cache_save, get_cache_key +from aymurai.utils.download import download logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger(__name__) @@ -38,10 +38,7 @@ def to_datetime(value): @staticmethod def get_file(source: str, dest: str) -> str: - # gdown have to much verbosity and cant be quiet (at least in this version) - # as workaround we use subprocess to get all output (even errors) - cmd = f"gdown --fuzzy -q --continue -O {dest} {source}" - subprocess.getoutput(cmd) + download(source, dest) if not Path(dest).exists(): Path(dest).touch() @@ -118,7 +115,6 @@ def __init__(self, use_cache: Union[str, bool] = True): data = [] GROUPBY_COLUMNS = ["nro_registro", "tomo", "path"] for keys, group in annotations.groupby(GROUPBY_COLUMNS): - data_ = {} data_["path"] = keys[2] data_["metadata"] = { diff --git a/aymurai/models/peft/__init__.py b/aymurai/evaluation/__init__.py similarity index 100% rename from aymurai/models/peft/__init__.py rename to aymurai/evaluation/__init__.py diff --git a/aymurai/evaluation/metrics.py b/aymurai/evaluation/metrics.py new file mode 100644 index 00000000..ced35d99 --- /dev/null +++ b/aymurai/evaluation/metrics.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import json +import unicodedata +from pathlib import Path +from typing import Any + +from aymurai.logger import get_logger +from aymurai.utils.json_data import load_json + +logger = get_logger(__name__) + + +def normalize_text(text: str) -> str: + """ + Normalize text by lowercasing, stripping, and removing accents. + + Args: + text (str): Input text. + + Returns: + str: Normalized text. + """ + if not text: + return "" + + # Lowercase and strip + text = text.strip().lower() + + # Remove accents + text = unicodedata.normalize("NFD", text) + text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn") + + return text + + +def get_alias_set(entity: dict[str, Any], normalize: bool = True) -> set[str]: + """ + Returns the set of normalized aliases for an entity, ensuring that the + canonical_text is included as an alias. + + Args: + entity (dict[str, Any]): A dictionary containing entity information with + optional 'aliases' (list of strings) and 'canonical_text' (string) keys. + normalize (bool, optional): If True, normalizes the alias texts using + normalize_text function. Defaults to True. + + Returns: + set[str]: A set of alias strings (normalized if normalize=True) including + both explicit aliases and the canonical text. + """ + aliases = set() + + # Get aliases from the entity + for alias in entity.get("aliases", []): + text = normalize_text(alias) if normalize else alias + if text: + aliases.add(text) + + # Get canonical text from the entity + canonical = ( + normalize_text(entity.get("canonical_text", "")) + if normalize + else entity.get("canonical_text", "") + ) + if canonical: + aliases.add(canonical) + + return aliases + + +def alias_jaccard(aliases_gold: set[str], aliases_pred: set[str]) -> float: + """ + Jaccard similarity between two sets of aliases. + + Args: + aliases_gold (set[str]): Set of gold aliases. + aliases_pred (set[str]): Set of predicted aliases. + + Returns: + float: Jaccard similarity score. + """ + # If both sets are empty, define similarity as 1.0 + if not aliases_gold and not aliases_pred: + return 1.0 + + # Calculate Jaccard similarity + union = aliases_gold | aliases_pred + inter = aliases_gold & aliases_pred + + return len(inter) / len(union) + + +def greedy_matching( + sim_matrix: list[list[float]], sim_threshold: float +) -> list[tuple[int, int, float]]: + """ + Perform greedy maximum matching based on similarity scores. + + Args: + sim_matrix (list[list[float]]): Similarity matrix where sim_matrix[i][j] + represents the similarity between the gold entity i and predicted + entity j. + sim_threshold (float): Minimum similarity required to consider a match + between two entities. + + Returns: + list[tuple[int, int, float]]: Tuples containing the matched gold index, + predicted index, and their similarity score. + """ + matches: list[tuple[int, int, float]] = [] + num_gold = len(sim_matrix) + num_pred = len(sim_matrix[0]) if num_gold > 0 else 0 + + # Build the list of candidate matches above the threshold + candidates: list[tuple[float, int, int]] = [] + for i in range(num_gold): + for j in range(num_pred): + sim = sim_matrix[i][j] + if sim >= sim_threshold: + candidates.append((sim, i, j)) + + # Sort descending by similarity so higher matches are selected first + candidates.sort(reverse=True, key=lambda x: x[0]) + + used_gold = set() + used_pred = set() + + for sim, i, j in candidates: + if i in used_gold or j in used_pred: + continue + used_gold.add(i) + used_pred.add(j) + matches.append((i, j, sim)) + + return matches + + +def compute_metrics_components( + gold_entities: list[dict[str, Any]], + pred_entities: list[dict[str, Any]], + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, +) -> dict[str, float]: + """ + Compute each component of the disambiguation metric by label if provided. + + The metric includes entity-level F1, macro average alias F1, and accuracy + for labels and roles on matched entities. + + Args: + gold_entities (list[dict[str, Any]]): Ground-truth entities. + pred_entities (list[dict[str, Any]]): Predicted entities. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + + Returns: + dict[str, float]: Mapping with the keys F1_ent, AliasF1_macro, Acc_label, and Acc_role. + """ + # Filter entities by target label if provided + if target_label: + gold_entities = [ + e for e in gold_entities if e.get("aymurai_label") == target_label + ] + pred_entities = [ + e for e in pred_entities if e.get("aymurai_label") == target_label + ] + + # Handle the trivial case where both sets are empty + if not gold_entities and not pred_entities: + return { + "F1_ent": 1.0, + "AliasF1_macro": 1.0, + "Acc_label": 1.0, + "Acc_role": 1.0, + } + + # Preprocess aliases, labels and roles + gold_aliases = [get_alias_set(e, normalize=normalize) for e in gold_entities] + pred_aliases = [get_alias_set(e, normalize=normalize) for e in pred_entities] + + gold_labels = [e.get("aymurai_label") for e in gold_entities] + pred_labels = [e.get("aymurai_label") for e in pred_entities] + + gold_roles = [e.get("attributes", {}).get("role") for e in gold_entities] + pred_roles = [e.get("attributes", {}).get("role") for e in pred_entities] + + num_gold = len(gold_entities) + num_pred = len(pred_entities) + + # Build the alias similarity matrix + sim_matrix = [] + for i in range(num_gold): + row = [] + for j in range(num_pred): + sim = alias_jaccard(gold_aliases[i], pred_aliases[j]) + row.append(sim) + sim_matrix.append(row) + + # Perform greedy matching + matches = greedy_matching(sim_matrix, sim_threshold=sim_threshold) + + TP = len(matches) + FN = num_gold - TP + FP = num_pred - TP + + # Entity-level precision/recall/F1 + if TP + FP > 0: + P_ent = TP / (TP + FP) + else: + P_ent = 0.0 + + if TP + FN > 0: + R_ent = TP / (TP + FN) + else: + R_ent = 0.0 + + if P_ent + R_ent > 0: + F1_ent = 2 * P_ent * R_ent / (P_ent + R_ent) + else: + F1_ent = 0.0 + + # Alias-level F1 macro + if TP > 0: + alias_f1_sum = 0.0 + label_correct = 0 + role_correct = 0 + + for i, j, _sim in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + + inter = len(A_g & A_p) + P_alias = inter / len(A_p) if len(A_p) > 0 else 0.0 + R_alias = inter / len(A_g) if len(A_g) > 0 else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + + alias_f1_sum += F1_alias + + # Label accuracy on matched entities + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + + # Role accuracy on matched entities + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + + AliasF1_macro = alias_f1_sum / TP + Acc_label = label_correct / TP + Acc_role = role_correct / TP + else: + AliasF1_macro = 0.0 + Acc_label = 0.0 + Acc_role = 0.0 + + return { + "F1_ent": F1_ent, + "AliasF1_macro": AliasF1_macro, + "Acc_label": Acc_label, + "Acc_role": Acc_role, + } + + +def evaluate_disambiguation( + gold_json: Any, + pred_json: Any, + w_ent: float = 0.4, + w_alias: float = 0.35, + w_label: float = 0.2, + w_role: float = 0.05, + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, +) -> tuple[float, dict[str, float]]: + """ + Evaluate disambiguation predictions against ground truth entities by label if provided. + + Args: + gold_json (Any): Ground-truth entities as a parsed list or JSON string. + pred_json (Any): Predicted entities as a parsed list or JSON string. + w_ent (float): Weight assigned to entity-level F1. Defaults to 0.4. + w_alias (float): Weight assigned to alias macro F1. Defaults to 0.35. + w_label (float): Weight assigned to label accuracy. Defaults to 0.2. + w_role (float): Weight assigned to role accuracy. Defaults to 0.05. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + + Returns: + tuple[float, dict[str, float]]: Overall disambiguation score and detailed metrics components. + """ + + # Parse JSON strings if necessary + if isinstance(gold_json, str): + gold_entities = json.loads(gold_json) + else: + gold_entities = gold_json + + if isinstance(pred_json, str): + pred_entities = json.loads(pred_json) + else: + pred_entities = pred_json + + metrics = compute_metrics_components( + gold_entities, + pred_entities, + sim_threshold=sim_threshold, + normalize=normalize, + target_label=target_label, + ) + + score = ( + w_ent * metrics["F1_ent"] + + w_alias * metrics["AliasF1_macro"] + + w_label * metrics["Acc_label"] + + w_role * metrics["Acc_role"] + ) + + return score, metrics + + +def evaluate_prediction_directories( + gold_dir: Path, + preds_dir: Path, + *, + sim_threshold: float = 0.3, + normalize: bool = True, + target_label: str = None, + gold_json_suffix: str = None, + pred_json_suffix: str = None, +) -> tuple[list[tuple[str, float, dict[str, float]]], float]: + """ + Evaluate predictions stored on disk against the gold standard set by label if provided. + It has the feature of comparing files in two directories based on their core names, + ignoring predefined suffixes. + + Args: + gold_dir (Path): Directory containing gold json files. + preds_dir (Path): Directory containing predicted json files. + sim_threshold (float): Minimum alias similarity to consider entities a match. + Defaults to 0.3. + normalize (bool): If True normalizes aliases before comparison. + Defaults to True. + target_label (str, optional): If provided, evaluates only entities with this label. + Defaults to None. + gold_json_suffix (str, optional): Suffix to remove from gold json filenames + when matching with predictions. Defaults to None. + pred_json_suffix (str, optional): Suffix to remove from predicted json filenames + when matching with gold files. Defaults to None. + + Returns: + tuple[list[tuple[str, float, dict[str, float]]], float]: Detailed results + per document and the average score across all evaluated documents. + """ + gold_dir = Path(gold_dir) + preds_dir = Path(preds_dir) + + results = [] + + pred_map = {} + for p in preds_dir.glob("*.json"): + core_name = p.stem.replace(pred_json_suffix, "") + pred_map[core_name] = p + + for gold_path in sorted(gold_dir.glob("*.json")): + core_id = gold_path.stem.replace(gold_json_suffix, "") + pred_path = pred_map.get(core_id) + + if not pred_path or not pred_path.exists(): + logger.warning( + f"Skipping: No prediction found for '{core_id}'. " + f"Expected something like '{core_id}{pred_json_suffix}.json'" + ) + continue + + gold_data = load_json(gold_path) + pred_data = load_json(pred_path) + + score, metrics = evaluate_disambiguation( + gold_data, + pred_data, + sim_threshold=sim_threshold, + normalize=normalize, + target_label=target_label, + ) + results.append((core_id, score, metrics)) + + average_score = ( + sum(score for _, score, _ in results) / len(results) + if results + else float("nan") + ) + + return results, average_score diff --git a/aymurai/meta/api_interfaces.py b/aymurai/meta/api_interfaces.py index 1ff51443..ab1e8845 100644 --- a/aymurai/meta/api_interfaces.py +++ b/aymurai/meta/api_interfaces.py @@ -1,4 +1,5 @@ import uuid +from typing import Literal from pydantic import UUID5, BaseModel, Field, RootModel @@ -42,10 +43,27 @@ class DocumentInformation(BaseModel): labels: list[DocLabel] = Field(default_factory=list) +class LabelPolicy(BaseModel): + """Per-label policy for disambiguation and anonymization.""" + + anonymize: bool | None = None + disambiguation: Literal["none", "fuzzy"] | None = None + use_subclass_when_available: bool | None = None + + +class RenderPolicy(BaseModel): + """Render policy for anonymized tokens.""" + + suffix_mode: Literal["auto", "always", "never"] | None = None + suffix_threshold: int | None = None + + class DocumentAnnotations(BaseModel): """Datatype for document annotations""" data: list[DocumentInformation] + label_policies: dict[str, LabelPolicy] | None = None + render_policy: RenderPolicy | None = None class DataPublicDocumentAnnotations(RootModel): diff --git a/aymurai/meta/entities.py b/aymurai/meta/entities.py index d16c0b68..fe36b762 100644 --- a/aymurai/meta/entities.py +++ b/aymurai/meta/entities.py @@ -1,4 +1,10 @@ -from pydantic import BaseModel, Field +from __future__ import annotations + +import json +from typing import Any +from uuid import NAMESPACE_URL, UUID, uuid5 + +from pydantic import BaseModel, Field, model_validator class EntityAttributes(BaseModel): @@ -26,6 +32,21 @@ class EntityAttributes(BaseModel): description="Method used on the prediction label", ) aymurai_score: float | None = Field(None, description="Score for prediction") + aymurai_label_instance: int | None = Field( + None, + description="Label instance index assigned by order of appearance (e.g., 1, 2, 3).", + ) + aymurai_disambiguation: str | None = Field( + None, + description="Override disambiguation mode for this entity (none, fuzzy).", + ) + aymurai_anonymize: bool | None = Field( + None, + description="Whether this entity should be anonymized in output.", + ) + canonical_entity_id: UUID | None = Field( + None, description="Reference to the canonical entity ID" + ) class Entity(BaseModel): @@ -38,3 +59,49 @@ class Entity(BaseModel): context_pre: str = "" context_post: str = "" attrs: EntityAttributes | None = None + + +class CanonicalEntity(BaseModel): + """Canonical representation of an entity cluster.""" + + entity_id: UUID | None = Field( + None, description="Unique identifier for the canonical entity" + ) + aymurai_label: str = Field(title="AymurAI label") + canonical_text: str = Field(description="Preferred textual form for the entity") + aliases: list[str] = Field( + default_factory=list, + description="Alternative surface forms observed for the entity", + ) + attributes: dict[str, Any] = Field( + default_factory=dict, description="Additional metadata for the entity" + ) + + @model_validator(mode="after") + def validate_entity_id(self) -> CanonicalEntity: + """ + Ensure the canonical entity has an entity_id; if missing, create a surrogate UUID and set it. + + Returns: + CanonicalEntity: the validated model instance (self). + """ + # If entity_id is already set, nothing to do. + if self.entity_id is not None: + return self + + payload = { + "aymurai_label": self.aymurai_label, + "canonical_text": self.canonical_text, + "aliases": sorted(self.aliases), + "attributes": self.attributes, + } + seed = json.dumps(payload, sort_keys=True, separators=(",", ":")) + object.__setattr__(self, "entity_id", uuid5(NAMESPACE_URL, seed)) + + return self + + +class CanonicalEntities(BaseModel): + """Collection of canonical entities.""" + + canonical_entities: list[CanonicalEntity] diff --git a/aymurai/models/decision/binregex.py b/aymurai/models/decision/binregex.py index a3b77a01..bc67e2be 100644 --- a/aymurai/models/decision/binregex.py +++ b/aymurai/models/decision/binregex.py @@ -1,96 +1,114 @@ +from __future__ import annotations + import os -import shutil from copy import deepcopy import regex import torch -from unidecode import unidecode from aymurai.logger import get_logger from aymurai.meta.api_interfaces import DocLabel, EntityAttributes from aymurai.meta.pipeline_interfaces import TrainModule from aymurai.meta.types import DataBlock, DataItem -from aymurai.models.decision.conv1d import Conv1dTextClassifier -from aymurai.models.decision.tokenizer import Tokenizer +from aymurai.models.decision.embeddingbag import ( + TinyEmbeddingBagClassifier, + encode_text, + make_offsets, +) +from aymurai.settings import settings from aymurai.utils.download import download from aymurai.utils.misc import get_element, is_url logger = get_logger(__name__) -class DecisionConv1dBinRegex(TrainModule): +class DecisionEmbeddingBagBinRegex(TrainModule): def __init__( self, - tokenizer_path: str, model_checkpoint: str, device: str = "cpu", threshold: float = 0.88, return_only_with_detalle: bool = True, ): self._device = device - self._tokenizer_path = tokenizer_path self._model_path = model_checkpoint self.threshold = threshold self.return_only_with_detalle = return_only_with_detalle - # download if needed - # tokenizer - basepath = os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai") - if is_url(url := self._tokenizer_path): - output = f"{basepath}/{self.__name__}/tokenizer.pth" - logger.info(f"downloading tokenizer on {output}") - os.makedirs(os.path.dirname(output), exist_ok=True) - self._tokenizer_path = download(url, output=output) - # model + basepath = settings.CACHE_BASEPATH if is_url(url := self._model_path): - output = f"{basepath}/{self.__name__}/model.ckpt" + # Determine file extension from URL or default to safetensors + if url.endswith(".pt"): + ext = ".pt" + else: + ext = ".safetensors" + output = f"{basepath}/{self.__class__.__name__}/model{ext}" logger.info(f"downloading model on {output}") os.makedirs(os.path.dirname(output), exist_ok=True) self._model_path = download(url, output=output) - - self.tokenizer = Tokenizer.load(self._tokenizer_path) - self.model = Conv1dTextClassifier.load_from_checkpoint( + # If downloading safetensors, also try to download config + if ext == ".safetensors": + config_url = url.rsplit(".safetensors", 1)[0] + ".json" + config_output = output.rsplit(".safetensors", 1)[0] + ".json" + try: + download(config_url, output=config_output) + except Exception as e: + logger.warning(f"Could not download config file: {e}") + + self.model, self.cfg = TinyEmbeddingBagClassifier.from_checkpoint( self._model_path, - map_location=self._device, + device=self._device, ) self.model = self.model.eval() - def save(self, basepath: str) -> dict | None: - # save tokenizer - os.makedirs(basepath, exist_ok=True) - self._tokenizer_path = f"{basepath}/tokenizer.pth" - self.tokenizer.save(self._tokenizer_path) - logger.info(f"tokenizer saved on: {self._tokenizer_path}") - - # save model - new_model_path = f"{basepath}/model.ckpt" - shutil.copy(self._model_path, new_model_path) - self._model_path = new_model_path - logger.info(f"model saved on: {self._model_path}") - return { - "tokenizer_path": self._tokenizer_path, - "model_checkpoint": self._model_path, - "device": self._device, - } - - @classmethod - def load(cls, path: str, **kwargs): - return cls( - tokenizer_path=f"{path}/tokenizer.pth", - model_checkpoint=f"{path}/model.ckpt", - **kwargs, - ) + def fit(self, train: DataBlock, val: DataBlock) -> None: + """ + Fit the model on training data. Currently not implemented. - def fit(self, train: DataBlock, val: DataBlock): + Args: + train (DataBlock): Training data block. + val (DataBlock): Validation data block. + """ logger.warning("fit routine not implemented") pass def predict(self, data: DataBlock) -> DataBlock: + """ + Predict on a data block. + + Args: + data (DataBlock): Input data block. + + Returns: + DataBlock: Predicted data block. + """ # FIXME: optimize - logger.warn("predict not optimized") + logger.warning("predict not optimized") return [self.predict_single(item) for item in data] - def get_subcategory(self, text): + def model_input_from_text(self, text: str) -> tuple[torch.Tensor, torch.Tensor]: + """ + Convert text to model input tensors. + + Args: + text (str): Input text. + + Returns: + tuple[torch.Tensor, torch.Tensor]: (flat_tokens, offsets) for model input. + """ + token_ids = encode_text(text, self.cfg).to(self._device) + return make_offsets([token_ids]) + + def get_subcategory(self, text: str) -> list[str]: + """ + Determine the subcategory of a decision based on its text. + + Args: + text (str): Text of the decision. + + Returns: + list[str]: List of subcategories for the decision. + """ pattern_no_hace_lugar = regex.compile( r"(?i)(no hacer? lugar|rechaz[ao]r?|no admitir|no convalidar|no autorizar|declarar inadmisible)" ) @@ -100,8 +118,19 @@ def get_subcategory(self, text): else: return ["hace_lugar"] - def gen_aymurai_entity(self, text: str, category: int, score: float): + def gen_aymurai_entity(self, text: str, score: float) -> dict: + """ + Generate an Aymurai entity dictionary for a decision. + + Args: + text (str): Text of the decision. + score (float): Confidence score of the decision. + + Returns: + dict: Aymurai entity dictionary. + """ subcategory = self.get_subcategory(text) + attrs = EntityAttributes( aymurai_label="DECISION", aymurai_label_subclass=subcategory, @@ -119,18 +148,27 @@ def gen_aymurai_entity(self, text: str, category: int, score: float): ent["label"] = "DECISION" ent["context_pre"] = "" ent["context_post"] = "" + return ent def predict_single(self, item: DataItem) -> DataItem: + """ + Predict a single data item. + + Args: + item (DataItem): The data item to predict. + + Returns: + DataItem: The predicted data item with added entities if applicable. + """ item = deepcopy(item) text = item["data"]["doc.text"] - text = unidecode(text) - input_ids = self.tokenizer.encode_batch([text]).to(self.model.device) + flat_tokens, offsets = self.model_input_from_text(text) with torch.no_grad(): - log_prob = self.model(input_ids).exp() - # using category 1 as global score (binary) - prob = log_prob.detach().numpy()[0, 1] + logits = self.model(flat_tokens, offsets) + probs = logits.softmax(dim=1).cpu() + prob = float(probs[0, 1]) category = int(prob > self.threshold) score = prob @@ -143,7 +181,7 @@ def predict_single(self, item: DataItem) -> DataItem: if self.return_only_with_detalle and not detalles: return item - ent = self.gen_aymurai_entity(text=text, category=category, score=score) + ent = self.gen_aymurai_entity(text=text, score=score) ents.append(ent) if "predictions" not in item: @@ -152,3 +190,53 @@ def predict_single(self, item: DataItem) -> DataItem: item["predictions"]["entities"] = ents return item + + def save(self, basepath: str) -> dict: + """ + Save the model to a directory. + + Args: + basepath (str): Directory path to save the model. + + Returns: + dict: A dictionary containing metadata about the saved model. + """ + os.makedirs(basepath, exist_ok=True) + + # Use safetensors as the main format + new_model_path = f"{basepath}/model.safetensors" + self.model.save_checkpoint(new_model_path, use_safetensors=True) + self._model_path = new_model_path + logger.info(f"model saved on: {self._model_path}") + + return { + "model_checkpoint": self._model_path, + "device": self._device, + "threshold": self.threshold, + "return_only_with_detalle": self.return_only_with_detalle, + } + + @classmethod + def load(cls, path: str, **kwargs) -> DecisionEmbeddingBagBinRegex: + """ + Load a DecisionEmbeddingBagBinRegex model from a directory. + + Args: + path (str): Path to the directory containing the model files. + + Returns: + DecisionEmbeddingBagBinRegex: The loaded model instance. + """ + # Try safetensors first, then .pt + safetensors_path = f"{path}/model.safetensors" + pt_path = f"{path}/model.pt" + + if os.path.exists(safetensors_path): + model_checkpoint = safetensors_path + elif os.path.exists(pt_path): + model_checkpoint = pt_path + else: + # Fallback to .pt for backward compatibility + model_checkpoint = pt_path + + return cls(model_checkpoint=model_checkpoint, **kwargs) diff --git a/aymurai/models/decision/conv1d.py b/aymurai/models/decision/conv1d.py deleted file mode 100644 index 9c93dfad..00000000 --- a/aymurai/models/decision/conv1d.py +++ /dev/null @@ -1,138 +0,0 @@ -import pytorch_lightning as pl -import torch -import torch.nn.functional as F -import torchmetrics -from torch import nn -from torch.optim.lr_scheduler import ReduceLROnPlateau - - -class Conv1dTextClassifier(pl.LightningModule): - def __init__( - self, - vocab_size: int, - max_tokens: int = 128, - embed_len: int = 128, - nfeatures: int = 64, - num_classes: int = 2, - lr_scheduler_patience: int = 2, - class_weights: list = [], - ): - self.vocab_size = vocab_size - self.max_tokens = max_tokens - self.embed_len = embed_len - self.nfeatures = nfeatures - self.num_classes = num_classes - self.lr = 1e-3 - self.lr_scheduler_patience = lr_scheduler_patience - self.class_weights = class_weights - - super().__init__() - self.save_hyperparameters() - - # layers - self.embedding_layer = nn.Embedding( - num_embeddings=self.vocab_size, - embedding_dim=self.embed_len, - ) - self.conv1 = nn.Conv1d(self.embed_len, 64, kernel_size=7, padding="same") - self.conv2 = nn.Conv1d(64, 32, kernel_size=7, padding="same") - self.pooling = nn.MaxPool1d(2) - - self.linear1 = nn.Linear(32, 32) - self.linear2 = nn.Linear(32, self.num_classes) - # self.linear = nn.Linear(self.nfeatures, self.num_classes) - - if len(self.class_weights): - self.class_weights = torch.tensor(class_weights, dtype=torch.float32) - - self.logsoftmax = nn.LogSoftmax(dim=1) - self.loss = nn.NLLLoss(weight=self.class_weights) - - # metrics - self.accuracy = torchmetrics.Accuracy( - task="multiclass", - num_classes=self.num_classes, - ) - self.f1score = torchmetrics.F1Score( - task="multiclass", - num_classes=self.num_classes, - ) - - def forward(self, X_batch): - x = self.embedding_layer(X_batch) - x = x.reshape( - len(x), self.embed_len, self.max_tokens - ) ## Embedding Length needs to be treated as channel dimension - x = F.relu(self.conv1(x)) - x = self.pooling(x) - # x = F.dropout(x, 0.5) - x = F.relu(self.conv2(x)) - x, _ = x.max(dim=-1) - - x = self.linear2(x) - x = self.logsoftmax(x) - - return x - - def predict_step(self, batch, batch_idx): - return self(batch) - - def training_step(self, batch, batch_idx): - # training_step defines the train loop. - x, y = batch - - y_pred = self.forward(x) - - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - return loss - - def validation_step(self, batch, batch_idx): - x, y = batch - - y_pred = self.forward(x) - - # loss = F.cross_entropy(y_pred, y) - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("val_loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("val_acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("val_f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - - def test_step(self, batch, batch_idx): - x, y = batch - - y_pred = self.forward(x) - - # loss = F.cross_entropy(y_pred, y) - loss = self.loss(y_pred, y) - acc = self.accuracy(y_pred, y) - f1score = self.f1score(y_pred, y) - - self.log("test_loss", loss, on_epoch=True, prog_bar=True, logger=True) - self.log("test_acc", acc, on_epoch=True, prog_bar=True, logger=True) - self.log("test_f1score", f1score, on_epoch=True, prog_bar=True, logger=True) - - def configure_optimizers(self): - # optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) - optimizer = torch.optim.AdamW(self.parameters(), lr=self.lr) - return { - "optimizer": optimizer, - "lr_scheduler": { - "scheduler": ReduceLROnPlateau( - optimizer, - patience=self.lr_scheduler_patience, - ), - "monitor": "val_loss", - "frequency": 1, - # If "monitor" references validation metrics, then "frequency" should be set to a - # multiple of "trainer.check_val_every_n_epoch". - }, - } diff --git a/aymurai/models/decision/embeddingbag.py b/aymurai/models/decision/embeddingbag.py new file mode 100644 index 00000000..19e29d46 --- /dev/null +++ b/aymurai/models/decision/embeddingbag.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import hashlib +import json +import os +import re +from dataclasses import dataclass, field + +import torch +from safetensors.torch import load_file as safe_load_file +from safetensors.torch import save_file as safe_save_file +from torch import nn +from unidecode import unidecode + + +def _normalize_text(text: str) -> str: + """ + Normalize text by removing accents, converting to lowercase, and collapsing whitespace. + + Args: + text (str): The input text to normalize. + + Returns: + str: The normalized text. + """ + text = unidecode(str(text)).lower() + text = re.sub(r"\s+", " ", text).strip() + return text + + +def _tokenize(text: str) -> list[str]: + """ + Tokenize text by splitting on spaces after normalization. + + Args: + text (str): The input text to tokenize. + + Returns: + list[str]: The list of tokens. + """ + text = _normalize_text(text) + return [token for token in text.split(" ") if token] + + +def _hash_token(token: str, vocab_size: int) -> int: + """ + Hash a token string into an integer in the range [0, vocab_size). + + Args: + token (str): The token string to hash. + vocab_size (int): The size of the vocabulary. + + Returns: + int: The hashed token id. + """ + digest = hashlib.blake2b(token.encode("utf-8"), digest_size=4).digest() + return int.from_bytes(digest, "little") % vocab_size + + +@dataclass +class EmbeddingBagConfig: + vocab_size: int = 20000 + embed_dim: int = 64 + max_tokens: int = 128 + dropout: float = 0.1 + num_classes: int = 2 + # allow passthrough of unknown config keys when loading from checkpoints + extra: dict = field(default_factory=dict) + + +def _merge_config(ckpt_cfg: dict | None) -> EmbeddingBagConfig: + """ + Merge checkpoint config with defaults, stashing unknown keys in extra. + + Args: + ckpt_cfg (dict | None): The config dictionary from checkpoint. + + Returns: + EmbeddingBagConfig: The merged configuration object. + """ + ckpt_cfg = ckpt_cfg or {} + cfg_defaults = EmbeddingBagConfig().__dict__ + merged = {**cfg_defaults} + extra = {} + + for k, v in ckpt_cfg.items(): + if k in cfg_defaults: + merged[k] = v + else: + extra[k] = v + + merged["extra"] = extra + + return EmbeddingBagConfig(**merged) + + +def encode_text(text: str, cfg: EmbeddingBagConfig) -> torch.Tensor: + """ + Encode text into token ids using hashing; truncates to cfg.max_tokens. + + Args: + text (str): The input text to encode. + cfg (EmbeddingBagConfig): The configuration with vocab size and max tokens. + + Returns: + torch.Tensor: The tensor of token ids. + """ + tokens = _tokenize(text) + token_ids = [_hash_token(tok, cfg.vocab_size) for tok in tokens[: cfg.max_tokens]] + + if not token_ids: + token_ids = [0] + + return torch.tensor(token_ids, dtype=torch.long) + + +def make_offsets(token_seqs: list[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]: + """ + Flatten variable-length token sequences for EmbeddingBag. + + Args: + token_seqs (list[torch.Tensor]): List of 1D tensors of token ids. + + Returns: + tuple[torch.Tensor, torch.Tensor]: (flat_tokens, offsets) where flat_tokens is a 1D tensor of concatenated tokens, + and offsets is a 1D tensor indicating start indices of each sequence. + """ + device = token_seqs[0].device if token_seqs else None + offsets = torch.zeros(len(token_seqs), dtype=torch.long, device=device) + flat = [] + total = 0 + + for i, seq in enumerate(token_seqs): + offsets[i] = total + flat.append(seq) + total += len(seq) + flat_tokens = torch.cat(flat) if flat else torch.tensor([], device=device) + + return flat_tokens, offsets + + +class TinyEmbeddingBagClassifier(nn.Module): + def __init__(self, cfg: EmbeddingBagConfig): + super().__init__() + self.cfg = cfg + self.embedding = nn.EmbeddingBag(cfg.vocab_size, cfg.embed_dim, mode="mean") + self.dropout = nn.Dropout(cfg.dropout) + self.head = nn.Linear(cfg.embed_dim, cfg.num_classes) + + def forward(self, tokens: torch.Tensor, offsets: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the TinyEmbeddingBagClassifier. + + Args: + tokens (torch.Tensor): Tensor of token ids. + offsets (torch.Tensor): Tensor of offsets for EmbeddingBag. + + Returns: + torch.Tensor: The output logits for each class. + """ + x = self.embedding(tokens, offsets) + x = self.dropout(x) + return self.head(x) + + @classmethod + def from_checkpoint( + cls, checkpoint_path: str, device: str = "cpu" + ) -> tuple[TinyEmbeddingBagClassifier, EmbeddingBagConfig]: + """ + Load a TinyEmbeddingBagClassifier from a checkpoint file. + + Raises: + ImportError: If safetensors is required but not installed. + + Returns: + tuple[TinyEmbeddingBagClassifier, EmbeddingBagConfig]: The loaded model and its configuration. + """ + if checkpoint_path.endswith(".safetensors"): + if safe_load_file is None: + raise ImportError( + "safetensors is not installed; cannot load .safetensors checkpoint" + ) + state = safe_load_file(checkpoint_path, device=device) + cfg_path = os.path.splitext(checkpoint_path)[0] + ".json" + cfg_data = {} + if os.path.exists(cfg_path): + with open(cfg_path, "r") as f: + cfg_data = json.load(f) + cfg = _merge_config(cfg_data) + else: + checkpoint = torch.load(checkpoint_path, map_location=device) + cfg = _merge_config(checkpoint.get("config", {})) + state = checkpoint.get("state_dict", checkpoint) + + model = cls(cfg) + model.load_state_dict(state) + model.to(device) + model.eval() + return model, cfg + + def save_checkpoint( + self, + save_path: str, + use_safetensors: bool = True, + ) -> str: + """ + Save model checkpoint. + + Args: + save_path (str): Path to save checkpoint. Extension determines format. + If no extension, .safetensors or .pt will be added based on use_safetensors. + use_safetensors (bool): If True and no extension provided, save as .safetensors. + If False, save as .pt format. Defaults to True. + + Returns: + str: The actual path where the checkpoint was saved. + """ + # Determine save format based on extension or flag + if not save_path.endswith((".safetensors", ".pt")): + ext = ".safetensors" if use_safetensors else ".pt" + save_path = save_path + ext + + os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) + + if save_path.endswith(".safetensors"): + if safe_save_file is None: + raise ImportError( + "safetensors is not installed; cannot save .safetensors checkpoint. " + "Install with: pip install safetensors" + ) + # Save model weights as safetensors + safe_save_file(self.state_dict(), save_path) + + # Save config as JSON + cfg_path = os.path.splitext(save_path)[0] + ".json" + + with open(cfg_path, "w") as f: + json.dump( + { + "vocab_size": self.cfg.vocab_size, + "embed_dim": self.cfg.embed_dim, + "max_tokens": self.cfg.max_tokens, + "dropout": self.cfg.dropout, + "num_classes": self.cfg.num_classes, + **self.cfg.extra, + }, + f, + indent=2, + ) + else: + # Save as PyTorch checkpoint with config embedded + torch.save( + { + "state_dict": self.state_dict(), + "config": { + "vocab_size": self.cfg.vocab_size, + "embed_dim": self.cfg.embed_dim, + "max_tokens": self.cfg.max_tokens, + "dropout": self.cfg.dropout, + "num_classes": self.cfg.num_classes, + **self.cfg.extra, + }, + }, + save_path, + ) + + return save_path diff --git a/aymurai/models/decision/tokenizer.py b/aymurai/models/decision/tokenizer.py deleted file mode 100644 index bbb21eae..00000000 --- a/aymurai/models/decision/tokenizer.py +++ /dev/null @@ -1,36 +0,0 @@ -import torch -from unidecode import unidecode - -from aymurai.text.tokenizers.spanish import SpanishTokenizer - - -class Tokenizer(object): - def __init__(self, vocab): - self.max_len = 128 - self.tokenizer = SpanishTokenizer() - self.vocab = vocab - - def save(self, path: str): - torch.save(self.vocab, path) - - @classmethod - def load(cls, path: str): - vocab = torch.load(path) - return cls(vocab=vocab) - - def __call__(self, text: str): - text = text.lower() - text = unidecode(text) - return self.tokenizer(text) - - def encode(self, text: int): - tokens = self(text)[: self.max_len] - indices = self.vocab(tokens) - indices = torch.tensor(indices, dtype=torch.int64) - indices = torch.nn.functional.pad(indices, (0, self.max_len - len(indices))) - return indices - - def encode_batch(self, texts): - indices = [self.encode(text) for text in texts] - indices = torch.stack(indices) - return indices diff --git a/aymurai/models/flair/core.py b/aymurai/models/flair/core.py index a3a5ad87..6fae5daf 100644 --- a/aymurai/models/flair/core.py +++ b/aymurai/models/flair/core.py @@ -1,20 +1,21 @@ +import logging import os import re -import logging from copy import deepcopy import flair import numpy as np from flair.data import Sentence -from more_itertools import collapse from flair.models import SequenceTagger +from more_itertools import collapse from aymurai.logger import get_logger -from aymurai.utils.misc import is_url -from aymurai.utils.download import download -from aymurai.meta.types import DataItem, DataBlock -from aymurai.meta.pipeline_interfaces import TrainModule from aymurai.meta.entities import Entity, EntityAttributes +from aymurai.meta.pipeline_interfaces import TrainModule +from aymurai.meta.types import DataBlock, DataItem +from aymurai.settings import settings +from aymurai.utils.download import download +from aymurai.utils.misc import is_url flair.logger.setLevel(logging.ERROR) @@ -46,7 +47,7 @@ def __init__( # load model if is_url(url := basepath): - basepath = os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai") + basepath = settings.CACHE_BASEPATH model_path = f"{basepath}/{self.__name__}/model.pt" logger.info(f"downloading model on {model_path}") os.makedirs(os.path.dirname(model_path), exist_ok=True) diff --git a/aymurai/models/sentence_encoder/__init__.py b/aymurai/models/sentence_encoder/__init__.py new file mode 100644 index 00000000..c3714e12 --- /dev/null +++ b/aymurai/models/sentence_encoder/__init__.py @@ -0,0 +1,45 @@ +"""Sentence encoder module with sentence-transformers backends. + +Implementations are lazily loaded to avoid importing unavailable dependencies. +Use the factory function `create_encoder()` for automatic backend selection, +or import specific implementations directly from their modules: + + from aymurai.models.sentence_encoder.sentence_transformers_encoder import DistilUSEEncoder +""" + +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder +from aymurai.models.sentence_encoder.factory import ( + EncoderType, + create_encoder, +) + + +def __getattr__(name: str): + """Lazy loading of encoder implementations.""" + if name == "DistilUSEEncoder": + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + DistilUSEEncoder, + ) + + return DistilUSEEncoder + + if name == "MultilingualMiniLMEncoder": + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + MultilingualMiniLMEncoder, + ) + + return MultilingualMiniLMEncoder + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + # Base interface + "BaseSentenceEncoder", + # Factory + "EncoderType", + "create_encoder", + # Implementations (lazy loaded) + "DistilUSEEncoder", + "MultilingualMiniLMEncoder", +] diff --git a/aymurai/models/sentence_encoder/base.py b/aymurai/models/sentence_encoder/base.py new file mode 100644 index 00000000..daf25c69 --- /dev/null +++ b/aymurai/models/sentence_encoder/base.py @@ -0,0 +1,66 @@ +""" +Abstract base class for sentence encoders. +""" + +import unicodedata +from abc import ABC, abstractmethod +from typing import Iterable, Optional + +import numpy as np + + +class BaseSentenceEncoder(ABC): + """ + Abstract base class defining the interface for sentence encoders. + All encoder implementations must inherit from this class. + """ + + def normalize_text(self, text: str) -> str: + """Normalize text by lowercasing and removing non-alphanumeric characters.""" + text = text.lower() + text = "".join( + char + for char in unicodedata.normalize("NFKD", text) + if char.isalnum() or char.isspace() + ) + return text + + @abstractmethod + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode a list of texts into embeddings. + + Args: + text_array: List of texts to encode. + encoder_type: Type of encoder to use ('question_encoder' or 'response_encoder'). + context_array: Optional context for response encoding. + + Returns: + numpy array of embeddings. + """ + pass + + @abstractmethod + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """ + Encode texts in batches. + + Args: + text_array: Iterable of texts to encode. + encoder_type: Type of encoder to use. + batch_size: Number of texts per batch. + + Returns: + numpy array of embeddings. + """ + pass diff --git a/aymurai/models/sentence_encoder/factory.py b/aymurai/models/sentence_encoder/factory.py new file mode 100644 index 00000000..a5d5b541 --- /dev/null +++ b/aymurai/models/sentence_encoder/factory.py @@ -0,0 +1,102 @@ +""" +Factory for creating sentence encoder instances based on configuration. +""" + +import os +from enum import Enum +from typing import Optional + +from aymurai.logger import get_logger +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder + +logger = get_logger(__name__) + + +class EncoderType(str, Enum): + """Available encoder implementations.""" + + DISTILUSE = "distiluse" + MINILM = "minilm" + + +def _get_encoder_type_from_env() -> EncoderType: + """Get encoder type from environment variable.""" + env_value = os.getenv("SENTENCE_ENCODER_TYPE", "distiluse").lower() + try: + return EncoderType(env_value) + except ValueError: + logger.warning( + f"Invalid SENTENCE_ENCODER_TYPE '{env_value}', falling back to 'distiluse'" + ) + return EncoderType.DISTILUSE + + +def _coerce_encoder_type(value: EncoderType | str | None) -> EncoderType: + """ + Normalize encoder type values into EncoderType. + + Args: + value: Encoder type as EncoderType, string, or None. If None, reads from env var. + + Returns: + EncoderType: Normalized encoder type. + """ + if value is None: + return _get_encoder_type_from_env() + + if isinstance(value, EncoderType): + return value + + if isinstance(value, str): + try: + return EncoderType(value.lower()) + except ValueError as exc: + raise ValueError(f"Unknown encoder type: {value}") from exc + + raise TypeError(f"Unsupported encoder type value: {type(value)!r}") + + +def create_encoder( + encoder_type: EncoderType | str | None = None, + device: Optional[str] = None, +) -> BaseSentenceEncoder: + """ + Factory function to create the appropriate sentence encoder. + + Args: + encoder_type: Type of encoder to create. Accepts EncoderType or string. + If None, reads from SENTENCE_ENCODER_TYPE env var + (defaults to 'distiluse'). + device: Device for sentence-transformers models. + + Returns: + An instance of BaseSentenceEncoder. + + Environment Variables: + SENTENCE_ENCODER_TYPE: One of 'distiluse', 'minilm'. + - 'distiluse': Use distiluse-base-multilingual-cased-v2 + - 'minilm': Use paraphrase-multilingual-MiniLM-L12-v2 + + Raises: + ImportError: If required dependencies are not available. + ValueError: If an invalid encoder type is specified. + """ + encoder_type = _coerce_encoder_type(encoder_type) + + # Create the appropriate encoder + if encoder_type == EncoderType.DISTILUSE: + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + DistilUSEEncoder, + ) + + return DistilUSEEncoder(device=device) + + elif encoder_type == EncoderType.MINILM: + from aymurai.models.sentence_encoder.sentence_transformers_encoder import ( + MultilingualMiniLMEncoder, + ) + + return MultilingualMiniLMEncoder(device=device) + + else: + raise ValueError(f"Unknown encoder type: {encoder_type}") diff --git a/aymurai/models/sentence_encoder/sentence_transformers_encoder.py b/aymurai/models/sentence_encoder/sentence_transformers_encoder.py new file mode 100644 index 00000000..53668f01 --- /dev/null +++ b/aymurai/models/sentence_encoder/sentence_transformers_encoder.py @@ -0,0 +1,124 @@ +""" +Sentence Transformers encoder implementations. +Compatible with Apple Silicon (M1/M2/M3) via PyTorch MPS backend. +""" + +from typing import Iterable, Optional + +import numpy as np +from sentence_transformers import SentenceTransformer + +from aymurai.logger import get_logger +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder + +logger = get_logger(__name__) + + +class DistilUSEEncoder(BaseSentenceEncoder): + """ + Knowledge-distilled Universal Sentence Encoder using sentence-transformers. + + This is a PyTorch-based distillation of the original USE multilingual model. + Supports 50+ languages and produces 512-dimensional embeddings. + + Compatible with Apple Silicon via MPS backend. + """ + + MODEL_NAME = "sentence-transformers/distiluse-base-multilingual-cased-v2" + + def __init__(self, device: Optional[str] = None): + """ + Initialize the DistilUSE encoder. + + Args: + device: Device to run the model on. If None, auto-detects + (will use 'mps' on Apple Silicon, 'cuda' if available, else 'cpu'). + """ + self.model = SentenceTransformer(self.MODEL_NAME, device=device) + logger.info(f"Loaded {self.MODEL_NAME} on device: {self.model.device}") + + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode texts into embeddings. + + Note: encoder_type and context_array are kept for API compatibility + but are not used since sentence-transformers uses a single encoder. + """ + input_array = [self.normalize_text(text) for text in text_array] + return self.model.encode(input_array, convert_to_numpy=True) + + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """Encode texts in batches with progress bar.""" + text_list = list(text_array) + input_array = [self.normalize_text(text) for text in text_list] + return self.model.encode( + input_array, + batch_size=batch_size, + show_progress_bar=True, + convert_to_numpy=True, + ) + + +class MultilingualMiniLMEncoder(BaseSentenceEncoder): + """ + Multilingual MiniLM encoder using sentence-transformers. + + Higher quality embeddings than DistilUSE with faster inference. + Supports 50+ languages and produces 384-dimensional embeddings. + + Compatible with Apple Silicon via MPS backend. + """ + + MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" + + def __init__(self, device: Optional[str] = None): + """ + Initialize the Multilingual MiniLM encoder. + + Args: + device: Device to run the model on. If None, auto-detects + (will use 'mps' on Apple Silicon, 'cuda' if available, else 'cpu'). + """ + self.model = SentenceTransformer(self.MODEL_NAME, device=device) + logger.info(f"Loaded {self.MODEL_NAME} on device: {self.model.device}") + + def encode( + self, + text_array: list[str], + encoder_type: str, + context_array: Optional[list[str]] = None, + ) -> np.ndarray: + """ + Encode texts into embeddings. + + Note: encoder_type and context_array are kept for API compatibility + but are not used since sentence-transformers uses a single encoder. + """ + input_array = [self.normalize_text(text) for text in text_array] + return self.model.encode(input_array, convert_to_numpy=True) + + def batch_encode( + self, + text_array: Iterable[str], + encoder_type: str, + batch_size: int = 256, + ) -> np.ndarray: + """Encode texts in batches with progress bar.""" + text_list = list(text_array) + input_array = [self.normalize_text(text) for text in text_list] + return self.model.encode( + input_array, + batch_size=batch_size, + show_progress_bar=True, + convert_to_numpy=True, + ) diff --git a/aymurai/models/usem/core.py b/aymurai/models/usem/core.py deleted file mode 100644 index 4fbd6746..00000000 --- a/aymurai/models/usem/core.py +++ /dev/null @@ -1,109 +0,0 @@ -import unicodedata -from multiprocessing import cpu_count -from typing import Iterable, Optional - -import numpy as np -import tensorflow as tf -import tensorflow_hub as hub -import tensorflow_text # noqa -from more_itertools import chunked -from tqdm.auto import tqdm - -from aymurai.logger import get_logger - -logger = get_logger(__name__) - -N_JOBS = cpu_count() - - -class USEMQA: - """ - Base class for USEM (Universal Sentence Encoder Multilingual QA) - """ - - def __init__( - self, - usem_qa_url: str = "https://tfhub.dev/google/universal-sentence-encoder-multilingual-qa/3", - ): - self.embed = hub.load(usem_qa_url) - - def normalize_text(self, text: str) -> str: - text = text.lower() - text = "".join( - char - for char in unicodedata.normalize("NFKD", text) - if char.isalnum() or char.isspace() - ) - return text - - def encode( - self, - text_array: list[str], - encoder_type: str, - context_array: Optional[list[str]] = None, - ) -> np.ndarray: - input_array = [self.normalize_text(text) for text in text_array] - - if encoder_type == "response_encoder": - encoder_params = { - "input": tf.constant(input_array), - "context": tf.constant(context_array), - } - - if encoder_type == "question_encoder": - encoder_params = { - "input": tf.constant(input_array), - } - - encoded = self.embed.signatures[encoder_type](**encoder_params) - encoded = encoded["outputs"] - - return encoded - - def batch_encode( - self, - text_array: Iterable[str], - encoder_type: str, - batch_size: int = 256, - ) -> np.ndarray: - text_chunks = chunked(text_array, batch_size) - - encoded = [ - self.encode(text_chunk, encoder_type) - for text_chunk in tqdm( - text_chunks, - desc="creating USEM vectors...", - total=len(text_array) // batch_size, - ) - ] - - encoded = np.vstack(encoded) - - return encoded - - -class SentenceRetrieval: - def __init__( - self, - categories: list[str], - response_embeddings_path: str, - ): - self.usem = USEMQA() - self.categories = categories - self.usem_vectors = self.load_usem_vectors(response_embeddings_path) - - def load_usem_vectors(self, file_path): - usem_vectors = np.load(file_path) - return usem_vectors - - def retrieve(self, text: str, top_k: int = 10) -> list[str]: - query_vector = self.usem.encode( - [text], - encoder_type="question_encoder", - ) - - products = np.inner(query_vector, self.usem_vectors)[0] - similar_idx = np.flip(products.argsort())[:top_k] - similar_sentences = [self.categories[idx] for idx in similar_idx] - - return similar_sentences diff --git a/aymurai/scripts/pipelines_download.py b/aymurai/scripts/pipelines_download.py new file mode 100644 index 00000000..f797b613 --- /dev/null +++ b/aymurai/scripts/pipelines_download.py @@ -0,0 +1,19 @@ +import os + +from aymurai.logger import get_logger +from aymurai.pipeline.pipeline import AymurAIPipeline +from aymurai.settings import settings + +logger = get_logger(__name__) + +RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH + + +def pipelines_download(): + logger.info("Loading pipelines and exit.") + AymurAIPipeline.load( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "flair-anonymizer") + ) + AymurAIPipeline.load( + os.path.join(RESOURCES_BASEPATH, "pipelines", "production", "datapublic") + ) diff --git a/aymurai/settings.py b/aymurai/settings.py index 9fdd5331..2079f561 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -1,9 +1,10 @@ +import json import os from pathlib import Path from dotenv import load_dotenv +from pydantic import AliasChoices, ConfigDict, Field, FilePath, field_validator from pydantic_settings import BaseSettings -from pydantic import FilePath, ConfigDict, field_validator import aymurai @@ -22,7 +23,7 @@ def load_env(): class Settings(BaseSettings): - model_config = ConfigDict(case_sensitive=True) + model_config = ConfigDict(case_sensitive=False) CORS_ORIGINS: list[str] | str = ",".join( [ @@ -49,6 +50,10 @@ def assemble_cors_origins(cls, v) -> list[str]: SQLALCHEMY_DATABASE_URI: str = "sqlite:////resources/cache/sqlite/database.db" RESOURCES_BASEPATH: str = "/resources" + CACHE_BASEPATH: str = Field( + default="/resources/cache", + validation_alias=AliasChoices("AYMURAI_CACHE_BASEPATH", "CACHE_BASEPATH"), + ) # Alembic Config for running migrations ALEMBIC_INI_PATH: FilePath = PARENT / "alembic.ini" @@ -60,6 +65,39 @@ def assemble_cors_origins(cls, v) -> list[str]: MEMORY_CACHE_TTL: int = 60 LIBREOFFICE_BIN: str = "libreoffice" + PDF_WATERMARK_FONT_REGULAR: str | None = None + PDF_WATERMARK_FONT_BOLD: str | None = None + ANONYMIZATION_METADATA_CREATOR: str = "AymurAI" + ANONYMIZATION_METADATA_PRODUCER: str = "AymurAI" + + # Disambiguation Config + + # Fuzzy Matching + THRESHOLD: int = 70 + + # Label policies (JSON dict: label -> {disambiguation, anonymize}) + DISAMBIGUATION_LABEL_POLICIES: dict | None = None + + @field_validator("DISAMBIGUATION_LABEL_POLICIES", mode="before") + @classmethod + def parse_label_policies(cls, v): + if v is None or v == "": + return None + if isinstance(v, str): + return json.loads(v) + return v + + # Render policy (JSON dict) + RENDER_POLICY: dict | None = None + + @field_validator("RENDER_POLICY", mode="before") + @classmethod + def parse_render_policy(cls, v): + if v is None or v == "": + return None + if isinstance(v, str): + return json.loads(v) + return v load_env() diff --git a/aymurai/text/anonymization.py b/aymurai/text/anonymization.py deleted file mode 100644 index fe6fb14f..00000000 --- a/aymurai/text/anonymization.py +++ /dev/null @@ -1,766 +0,0 @@ -import os -import re -import tempfile -import xml.sax.saxutils -import zipfile -from copy import deepcopy -from glob import glob -from unicodedata import normalize - -import numpy as np -import pandas as pd -from docx import Document -from docx.enum.text import WD_ALIGN_PARAGRAPH -from docx.opc.constants import RELATIONSHIP_TYPE -from docx.oxml.shared import OxmlElement, qn -from docx.shared import Inches, Pt, RGBColor -from jiwer import cer -from joblib import hash -from lxml import etree -from more_itertools import flatten - -from aymurai.logger import get_logger -from aymurai.meta.pipeline_interfaces import Transform -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.utils.alignment.core import align_text, tokenize -from aymurai.utils.cache import cache_load, cache_save, get_cache_key - -logger = get_logger(__file__) - - -REGEX_PARAGRAPH = r"((?.*?)(\/w:p\b)" -REGEX_FRAGMENT = r"(?(?P.*?)(<.*?\/w:t)" - - -class DocAnonymizer(Transform): - """ - Anonymize document by replacing sensitive data with label tokens - """ - - def __init__(self, use_cache: bool = False, **kwargs): - self.use_cache = use_cache - self.kwargs = kwargs - - def unzip_document(self, doc_path: str, output_dir: str) -> None: - """ - Unzips the document file to the specified output directory. - - Args: - doc_path (str): The path to the document file. - output_dir (str): The directory where the contents of the document - file will be extracted. - """ - # Ensure the output directory exists - os.makedirs(output_dir, exist_ok=True) - - # Open the doc file as a zip file - with zipfile.ZipFile(doc_path, "r") as doc_zip: - # Extract all the contents to the output directory - doc_zip.extractall(output_dir) - logger.info(f"unzipped {doc_path} to {output_dir}") - - def index_paragraphs(self, file: str) -> list[dict]: - """ - Indexes the paragraphs of an XML file. - - Args: - file (str): The path to the XML file to be indexed. - - Returns: - list[dict]: A list of dictionaries representing the indexed paragraphs. - """ - # Read the XML file - with open(file) as f: - xml = f.read() - - paragraphs = [] - paragraph_index = 0 - - # Find all paragraphs in the XML file - for match in re.finditer(REGEX_PARAGRAPH, xml): - paragraph = match.group("paragraph") - paragraph_start = match.start("paragraph") - paragraph_end = match.end("paragraph") - fragments = [] - fragment_index = 0 - - # Find all text fragments in the paragraph - for fragment in re.finditer(REGEX_FRAGMENT, paragraph): - text = fragment.group("text") - start = fragment.start("text") - end = fragment.end("text") - - fragment_dict = { - "text": text, - "normalized_text": FlairTextNormalize.normalize_text(text), - "start": start, - "end": end, - "fragment_index": fragment_index, - "paragraph_index": paragraph_index, - } - fragments.append(fragment_dict) - fragment_index += 1 - - # Join all fragments as plain text - plain_text = "".join( - [fragment["normalized_text"] for fragment in fragments] - ) - - paragraphs.append( - { - "plain_text": plain_text, - "metadata": { - "start": paragraph_start, - "end": paragraph_end, - "fragments": fragments, - "xml_file": os.path.basename(file), - }, - } - ) - paragraph_index += 1 - - return paragraphs - - def match_paragraphs_with_predictions( - self, - paragraphs: list[dict], - predictions: list[dict], - ) -> list[dict]: - """ - Matches each paragraph with its corresponding predictions. - - Args: - paragraphs (list[dict]): A list of dictionaries representing the paragraphs. - predictions (list[dict]): A list of dictionaries representing - the predictions. - - Returns: - list[dict]: A list of dictionaries representing - the matched paragraphs with predictions. - """ - - paragraphs = deepcopy(paragraphs) - - # Hash prediction documents - pred_hashes = [hash(prediction["document"]) for prediction in predictions] - idx2hash = {i: _hash for i, _hash in enumerate(pred_hashes)} - - # Hash paragraphs - paragraphs = [ - paragraph - | {"hash": hash(normalize("NFKC", paragraph["plain_text"].strip()))} - for paragraph in paragraphs - ] - - # Assign prediction indices to each paragraph by hash - hash2idx = { - paragraph["hash"]: np.where(np.array(pred_hashes) == paragraph["hash"])[ - 0 - ].tolist() - for paragraph in paragraphs - } - - paragraphs = [ - paragraph | {"pred_indices": hash2idx[paragraph["hash"]]} - for paragraph in paragraphs - ] - - # Identify missing indices - missing_indices = list( - set(idx2hash.keys()) - set(flatten(list(hash2idx.values()))) - ) - - if missing_indices: - # Assign prediction indices to each paragraph by lowest CER - target_texts = np.array( - [prediction["document"] for prediction in predictions] - )[missing_indices] - - missing_paragraphs = [ - paragraph for paragraph in paragraphs if not paragraph["pred_indices"] - ] - - for missing_paragraph in missing_paragraphs: - source_text = missing_paragraph["plain_text"] - min_cer_idx = np.argmin( - [cer(source_text, target_text) for target_text in target_texts] - ) - missing_paragraph["pred_indices"] = [missing_indices[min_cer_idx]] - - # Assign document text and labels - paragraphs = [ - paragraph - | { - "document": predictions[paragraph["pred_indices"][0]]["document"], - "labels": predictions[paragraph["pred_indices"][0]]["labels"], - } - for paragraph in paragraphs - ] - - return paragraphs - - def unify_consecutive_labels( - self, sample: dict, text_key: str = "document" - ) -> list[dict]: - """ - Unifies consecutive labels in a sample. - - Args: - sample (dict): A dictionary representing the sample. - text_key (str, optional): The key for the text in the sample dictionary. - Defaults to "document". - - Returns: - list[dict]: A list of dictionaries representing the unified labels. - """ - sample = deepcopy(sample) - - # Extract labels and document text - labels = sample["labels"] - document = sample[text_key] - - # Reorder labels based on start indices - labels = sorted(labels, key=lambda x: x["start_char"]) - - unified_labels = [] - current_group = None - - # Iterate over labels - for label in labels: - # Get attributes - text = label["attrs"]["aymurai_alt_text"] or label["text"] - start_char = label["attrs"]["aymurai_alt_start_char"] or label["start_char"] - end_char = label["attrs"]["aymurai_alt_end_char"] or label["end_char"] - aymurai_label = label["attrs"]["aymurai_label"] - - if current_group is None: - # Start a new group with the current label - current_group = { - "text": text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - elif ( - current_group["aymurai_label"] == aymurai_label - and (start_char - current_group["end_char"]) <= 1 - ): - # Extend the current group with the current label - current_group["end_char"] = end_char - else: - # Finish the current group and start a new one - current_group["text"] = document[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - current_group = { - "text": text, - "start_char": start_char, - "end_char": end_char, - "aymurai_label": aymurai_label, - } - - # Finish the last group - if current_group is not None: - current_group["text"] = document[ - current_group["start_char"] : current_group["end_char"] + 1 - ] - unified_labels.append(current_group) - - return unified_labels - - def replace_labels_in_text(self, pred: dict, text_key: str = "document") -> str: - """ - Replaces labels in the text with anonymized tokens. - - Args: - pred (dict): A dictionary representing the prediction. - text_key (str, optional): The key for the text in the prediction dictionary. - Defaults to "document". - - Returns: - str: The text with replaced labels. - """ - pred = deepcopy(pred) - doc = pred[text_key] - - # Unify consecutive labels - unified_labels = self.unify_consecutive_labels(pred, text_key) - - # Initialize the offset - offset = 0 - - # Replace labels in the text - for unified_label in unified_labels: - # Adjust start and end character indices of the label - start_char = unified_label["start_char"] + offset - end_char = unified_label["end_char"] + offset - len_text_to_replace = end_char - start_char - - # Replace the text with the anonymized token - aymurai_label = f" <{unified_label['aymurai_label']}>" - len_aymurai_label = len(aymurai_label) - - doc = doc[:start_char] + aymurai_label + doc[end_char:] - - # Update the offset - offset += len_aymurai_label - len_text_to_replace - - return re.sub(r" +", " ", doc).strip() - - def erase_duplicates_justseen(self, series: pd.Series) -> pd.Series: - """ - Erases duplicates that were just seen between two consecutive rows. - - Args: - series (pd.Series): The pandas Series to be processed. - - Returns: - pd.Series: The processed pandas Series. - """ - return pd.Series( - [ - ( - "" - if (i > 0 and series.iloc[i] == series.iloc[i - 1]) - else series.iloc[i] - ) - for i in range(len(series)) - ] - ) - - def parse_token_indices(self, sample: dict) -> pd.DataFrame: - """ - Parses the token indices from a sample. - - Args: - sample (dict): A dictionary representing the sample. - - Returns: - pd.DataFrame: A pandas DataFrame representing the parsed token indices. - """ - original_text = " ".join( - [fragment["text"] for fragment in sample["metadata"]["fragments"]] - ) - anonymized_text = self.replace_labels_in_text(sample) - - aligned = align_text( - " " + original_text + " ", - " " + anonymized_text + " ", - ) - aligned["target"] = self.erase_duplicates_justseen(aligned["target"]) - - xml_file = sample["metadata"]["xml_file"] - - tokens = [] - for i, fragment in enumerate(sample["metadata"]["fragments"]): - text = fragment["text"] - tokenized_text = tokenize(text) - paragraph_index = fragment["paragraph_index"] - - # Use re.finditer to locate each instance of tokens in the text - token_matches = list( - re.finditer(r"\S+", text) - ) # \S+ matches any non-whitespace sequence - - # Loop over tokenized text and token_matches in parallel - for j, (token, match) in enumerate(zip(tokenized_text, token_matches)): - start = sample["metadata"]["start"] + fragment["start"] + match.start() - end = start + len(token) - - tokens.append((xml_file, paragraph_index, i, j, token, start, end)) - - tokens = pd.DataFrame( - tokens, - columns=[ - "xml_file", - "paragraph_index", - "fragment_index", - "token_index", - "token", - "start_char", - "end_char", - ], - ) - - tokens = pd.concat( - [tokens, aligned["target"].iloc[1:-1].reset_index(drop=True)], axis=1 - ) - - tokens["target"] = tokens["target"].fillna("") - - return tokens - - def normalize_document(self, xml_content: str) -> str: - """ - Normalizes the XML document by removing extra spaces, preserving line breaks, - and removing hyperlinks while preserving text content. - - Args: - xml_content (str): The XML content to be normalized. - - Returns: - str: The normalized XML content. - """ - # Parse the XML content - parser = etree.XMLParser(ns_clean=True) - root = etree.fromstring(xml_content.encode("utf-8"), parser) - - # Extract namespaces - namespaces = {k: v for k, v in root.nsmap.items() if k} - - # Remove hyperlinks but preserve the text content - for hyperlink in root.xpath("//w:hyperlink", namespaces=namespaces): - parent = hyperlink.getparent() - index = list(parent).index(hyperlink) - - # Move all text-containing children (e.g., w:r elements) outside the hyperlink tag - for child in hyperlink: - parent.insert(index, child) - index += 1 - parent.remove(hyperlink) # Remove the element itself - - # Process each paragraph - for wp in root.xpath("//w:p", namespaces=namespaces): - first = True - - # Find all w:r elements containing w:t elements - for wr in wp.xpath(".//w:r", namespaces=namespaces): - wt = wr.find(".//w:t", namespaces) - if wt is not None and wt.text: - # Normalize spaces within the text, preserving new line breaks - wt.text = re.sub(r"[^\S\r\n]+", " ", wt.text) - - # Add a leading space if not the first fragment in the paragraph - if not first: - wt.text = " " + wt.text.lstrip() - else: - wt.text = wt.text.lstrip() - first = False - - # Remove trailing spaces from all fragments - wt.text = wt.text.rstrip() - - # Set the xml:space attribute to preserve - wt.set( - "{http://www.w3.org/XML/1998/namespace}space", - "preserve", - ) - - # Check if the text is empty after normalization - if not wt.text or wt.text.strip() == "": - # Remove the w:r element from its parent - wr.getparent().remove(wr) - - # Write back the XML content to a string - xml_str = etree.tostring(root, encoding="unicode", pretty_print=True) - - return xml_str - - def replace_text_in_xml(self, paragraphs: list[dict], base_dir: str) -> None: - """ - Replaces text in XML files based on the provided paragraphs - and saves the modified files. - - Args: - paragraphs (list[dict]): A list of dictionaries representing - the paragraphs to be replaced. - base_dir (str): The base directory where the XML files are located. - """ - tokens = pd.concat( - [self.parse_token_indices(sample) for sample in paragraphs], - ignore_index=True, - ) - - fragments = ( - tokens.groupby(["xml_file", "paragraph_index", "fragment_index"]) - .agg({"target": " ".join, "start_char": "min", "end_char": "max"}) - .reset_index() - ) - - for xml_file, group in fragments.groupby("xml_file"): - group = group.sort_values("end_char", ascending=False) - - with open(f"{base_dir}/word/{xml_file}", "r+") as file: - content = file.read() - - for _, r in group.iterrows(): - start_char = r["start_char"] - end_char = r["end_char"] - - target = r["target"] - target = re.sub(r"[^\S\r\n]+", " ", target) - - # Escape XML special characters - target = xml.sax.saxutils.escape(target) - - content = content[:start_char] + target + content[end_char:] - - # MUST be at the end to dont screw up the indexes - content = self.normalize_document(content) - - file.seek(0) - file.write(content) - file.truncate() - - def add_files_to_zip(self, zip_file: zipfile.ZipFile, directory: str) -> None: - """ - Adds all files in the specified directory to a zip file. - - Args: - zip_file (zipfile.ZipFile): The zip file to add the files to. - directory (str): The directory containing the files to be added. - """ - for root, _, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - zip_file.write(file_path, os.path.relpath(file_path, directory)) - - def add_hyperlink( - self, - paragraph, - text, - url, - font_name="Archivo", - size=10, - color=RGBColor(115, 190, 250), - italic=False, - bold=True, - underline=True, - ) -> None: - """ - Adds a formatted hyperlink to a given paragraph in a Word document. - - Notes: - - This method directly manipulates the underlying XML of the paragraph to insert a hyperlink, - as python-docx does not natively support hyperlinks. - - The hyperlink will be appended to the end of the given paragraph. - - Formatting options (font, size, color, italic, bold, underline) are applied to the hyperlink text. - - Args: - paragraph: The python-docx paragraph object to which the hyperlink will be added. - text (str): The display text for the hyperlink. - url (str): The URL that the hyperlink points to. - font_name (str, optional): The font name to use for the hyperlink text. Defaults to "Archivo". - size (int, optional): The font size (in points) for the hyperlink text. Defaults to 10. - color (RGBColor, optional): The font color as an RGBColor tuple. Defaults to RGBColor(115, 190, 250). - italic (bool, optional): Whether the hyperlink text should be italicized. Defaults to False. - bold (bool, optional): Whether the hyperlink text should be bold. Defaults to True. - underline (bool, optional): Whether the hyperlink text should be underlined. Defaults to True. - - Raises: - ValueError: If the paragraph is not a valid python-docx paragraph object. - """ # noqa: E501 - # Create the hyperlink relationship - part = paragraph.part - r_id = part.relate_to(url, RELATIONSHIP_TYPE.HYPERLINK, is_external=True) - - # Create the hyperlink element - hyperlink = OxmlElement("w:hyperlink") - hyperlink.set(qn("r:id"), r_id) - - # Create a new run - new_run = OxmlElement("w:r") - - # Set run properties (formatting) - rPr = OxmlElement("w:rPr") - - # Set font - if font_name: - font = OxmlElement("w:rFonts") - font.set(qn("w:ascii"), font_name) - font.set(qn("w:hAnsi"), font_name) - rPr.append(font) - - # Set color - if color: - c = OxmlElement("w:color") - c.set(qn("w:val"), f"{color[0]:02x}{color[1]:02x}{color[2]:02x}") - rPr.append(c) - - # Set italic - if italic: - i = OxmlElement("w:i") - rPr.append(i) - - # Set bold - if bold: - b = OxmlElement("w:b") - rPr.append(b) - - # Set underline - added for links - if underline: - u = OxmlElement("w:u") - u.set(qn("w:val"), "single") # single underline - rPr.append(u) - - # Set size - sz = OxmlElement("w:sz") - sz.set(qn("w:val"), str(size * 2)) # Word uses half-points - rPr.append(sz) - - # Add properties to run - new_run.append(rPr) - - # Set text - t = OxmlElement("w:t") - t.text = text - new_run.append(t) - - # Add run to hyperlink - hyperlink.append(new_run) - - # Add hyperlink to paragraph - paragraph._p.append(hyperlink) - - def _add_watermark_to_footer( - self, - footer, - alignment, - font_name="Archivo", - hyperlink_text="AymurAI", - hyperlink_url="https://www.aymurai.info/", - watermark_text="Documento anonimizado por AymurAI", - ) -> None: - """ - Adds a watermark text to the footer of a document. - - Args: - footer: The footer object to which the watermark will be added. - alignment: The alignment setting for the paragraph (e.g., left, center, right). - font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". - hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". - hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". - watermark_text (str): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". - """ # noqa: E501 - paragraph = footer.add_paragraph() - paragraph.alignment = alignment - - if hyperlink_url and hyperlink_text in watermark_text: - parts = watermark_text.split(hyperlink_text, 1) - before_text = parts[0] - after_text = parts[1] if len(parts) > 1 else "" - - # Add text before the hyperlink - if before_text: - run = paragraph.add_run(before_text) - run.font.name = "Archivo" - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - # Add hyperlink - self.add_hyperlink(paragraph, hyperlink_text, hyperlink_url) - - # Add text after the hyperlink - if after_text: - run = paragraph.add_run(after_text) - run.font.name = font_name - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - else: - # Just add the full text without a hyperlink - run = paragraph.add_run(watermark_text) - run.font.name = font_name - run.font.color.rgb = RGBColor(192, 192, 192) - run.font.size = Pt(10) - - def add_footer_watermark(self, doc_path) -> None: - """ - Adds a watermark to the footer of each section in a Word document. - - Args: - doc_path: Path to the document - """ - document = Document(doc_path) - processed_footers = set() - - for section in document.sections: - section.footer_distance = Inches(0.1) - - # List of (footer_obj, alignment) tuples - footers = [(section.footer, WD_ALIGN_PARAGRAPH.RIGHT)] # Odd/default - if section.even_page_footer is not None: - footers.append((section.even_page_footer, WD_ALIGN_PARAGRAPH.LEFT)) - if section.different_first_page_header_footer: - footers.append((section.first_page_footer, WD_ALIGN_PARAGRAPH.RIGHT)) - - for footer, alignment in footers: - if id(footer) not in processed_footers: - self._add_watermark_to_footer(footer, alignment) - processed_footers.add(id(footer)) - - document.save(doc_path) - - def create_docx(self, xml_directory, output_file) -> None: - """ - Creates a new DOCX file by adding XML components from the specified directory. - - Args: - xml_directory (str): The directory containing the XML components. - output_file (str): The path to the output DOCX file. - """ - # Create a new zip file - with zipfile.ZipFile(output_file, "w") as docx: - # Add XML components - self.add_files_to_zip(docx, xml_directory) - - def __call__(self, item: dict, preds: list[dict], output_dir: str = ".") -> None: - """ - Performs the anonymization process on a document. - - Args: - item (dict): The document item to be anonymized. - preds (list[dict]): The list of predictions for the document. - output_dir (str, optional): The directory to save the anonymized document. - Defaults to ".". - - Raises: - ValueError: If the document has an extension other than `.docx`. - """ - item_path = item["path"] - - if not os.path.splitext(item_path)[-1] == ".docx": - raise ValueError("Only `.docx` extension is allowed.") - - if not item.get("data"): - item["data"] = {} - - cache_key = get_cache_key(item_path, self.__name__) - if self.use_cache and (cache_data := cache_load(key=cache_key)): - paragraphs = cache_data - else: - # Unzip document into a temporary directory - with tempfile.TemporaryDirectory() as tempdir: - self.unzip_document(item_path, tempdir) - - # Parse XML files - xml_files = glob(f"{tempdir}/**/*.xml", recursive=True) - paragraphs = (self.index_paragraphs(file) for file in xml_files) - paragraphs = list(flatten(paragraphs)) - - # Filter out empty paragraphs - paragraphs = [ - paragraph - for paragraph in paragraphs - if paragraph["plain_text"].strip() - ] - - # Matching - paragraphs = self.match_paragraphs_with_predictions(paragraphs, preds) - - # Edit XML filess - self.replace_text_in_xml(paragraphs, tempdir) - - # Recreate anonymized document - os.makedirs(output_dir, exist_ok=True) - self.create_docx( - tempdir, - f"{output_dir}/{os.path.basename(item_path)}", - ) - - # Add watermark to the footer - self.add_footer_watermark(f"{output_dir}/{os.path.basename(item_path)}") - - if self.use_cache: - cache_save(paragraphs, key=cache_key) diff --git a/aymurai/text/anonymization/__init__.py b/aymurai/text/anonymization/__init__.py new file mode 100644 index 00000000..51f3a65b --- /dev/null +++ b/aymurai/text/anonymization/__init__.py @@ -0,0 +1,21 @@ +from aymurai.text.anonymization.alignment import replace_labels_in_text +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + get_anonymizer, + register_anonymizer, + supported_extensions, +) +from aymurai.text.anonymization.docx import DocxAnonymizer +from aymurai.text.anonymization.pdf import PdfAnonymizer + +__all__ = [ + "BaseAnonymizer", + "DocxAnonymizer", + "PdfAnonymizer", + "InvalidDocumentAnonymizer", + "get_anonymizer", + "register_anonymizer", + "supported_extensions", + "replace_labels_in_text", +] diff --git a/aymurai/text/anonymization/alignment.py b/aymurai/text/anonymization/alignment.py new file mode 100644 index 00000000..e4f2547e --- /dev/null +++ b/aymurai/text/anonymization/alignment.py @@ -0,0 +1,463 @@ +import os +import re +from copy import deepcopy +from unicodedata import normalize + +import numpy as np +import pandas as pd +from jiwer import cer +from joblib import hash +from more_itertools import flatten + +from aymurai.meta.api_interfaces import LabelPolicy +from aymurai.models.flair.utils import FlairTextNormalize +from aymurai.utils.alignment.core import align_text, tokenize + +REGEX_PARAGRAPH = r"((?.*?)(\/w:p\b)" +REGEX_FRAGMENT = r"(?(?P.*?)(<.*?\/w:t)" + + +def resolve_render_token(label: dict, render_context: dict | None = None) -> str: + """ + Resolves the render token for a label using the current render context. + + Args: + label (dict): Label dictionary with attrs. + + Returns: + str: Render token to insert in the document. + """ + if not render_context: + return label["attrs"]["aymurai_label"] + + render_policy = render_context["render_policy"] + label_policies = render_context["label_policies"] + count_by_base = render_context["count_by_base"] + index_by_entity = render_context["index_by_entity"] + + attrs = label.get("attrs") or {} + base = attrs.get("aymurai_label") + + label_policy = label_policies.get(base.upper(), LabelPolicy()) + + subclasses = attrs.get("aymurai_label_subclass") or [] + if label_policy.use_subclass_when_available and subclasses: + base = subclasses[0].upper() + + if not base: + base = label.get("label") or label.get("text") or "ENT" + + entity_id = attrs.get("canonical_entity_id") or label.get("text") + key = (base, str(entity_id)) + index = index_by_entity.get(key) + + if render_policy.suffix_mode == "never" or index is None: + return base + + if render_policy.suffix_mode == "auto": + if count_by_base.get(base, 0) <= render_policy.suffix_threshold: + return base + + return f"{base}_{index}" + + +def _label_replacement_start(label: dict) -> int: + """ + Determines the start character index for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative start character attributes. + + Returns: + int: The start character index for the label. + """ + attrs = label.get("attrs") or {} + alt_start = attrs.get("aymurai_alt_start_char") + start_char = label.get("start_char") + return int(alt_start if alt_start is not None else (start_char or 0)) + + +def _label_replacement_end(label: dict) -> int: + """ + Determines the end character index for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative end character attributes. + + Returns: + int: The end character index for the label. + """ + attrs = label.get("attrs") or {} + alt_end = attrs.get("aymurai_alt_end_char") + end_char = label.get("end_char") + return int(alt_end if alt_end is not None else (end_char or 0)) + + +def _label_replacement_text(label: dict, document: str) -> str: + """ + Determines the replacement text for a label, considering possible alternative attributes. + + Args: + label (dict): Label dictionary which may contain alternative text attributes. + document (str): The document text from which to extract the label text. + + Returns: + str: The text for the label, considering possible alternative attributes. + """ + attrs = label.get("attrs") or {} + + alt_text = attrs.get("aymurai_alt_text") + if alt_text is not None: + return str(alt_text) if alt_text else "" + + alt_start = attrs.get("aymurai_alt_start_char") + alt_end = attrs.get("aymurai_alt_end_char") + if alt_start is not None and alt_end is not None: + start_char, end_char = int(alt_start), int(alt_end) + if 0 <= start_char < end_char <= len(document): + return document[start_char:end_char] + + start_char = int(label.get("start_char") or 0) + end_char = int(label.get("end_char") or 0) + if 0 <= start_char < end_char <= len(document): + return document[start_char:end_char] + + text = label.get("text") + return str(text) if text else "" + + +def unify_consecutive_labels( + sample: dict, + text_key: str = "document", + render_context: dict | None = None, +) -> list[dict]: + """ + Unifies consecutive labels in a sample. + + Args: + sample (dict): A dictionary representing the sample. + text_key (str, optional): The key for the text in the sample dictionary. + Defaults to "document". + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + + Returns: + list[dict]: A list of dictionaries representing the unified labels. + """ + sample = deepcopy(sample) + + # Extract labels and document text + labels = sample["labels"] + document = sample[text_key] + + # Reorder labels based on start indices + labels = sorted(labels, key=lambda x: x["start_char"]) + + unified_labels = [] + current_group = None + + # Iterate over labels + for label in labels: + # Get attributes + text = _label_replacement_text(label, document) + start_char = _label_replacement_start(label) + end_char = _label_replacement_end(label) + if not text or end_char <= start_char: + continue + aymurai_label = resolve_render_token(label, render_context) + + if current_group is None: + # Start a new group with the current label + current_group = { + "text": text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + elif ( + current_group["aymurai_label"] == aymurai_label + and (start_char - current_group["end_char"]) <= 1 + ): + # Extend the current group with the current label + current_group["end_char"] = end_char + else: + # Finish the current group and start a new one + current_group["text"] = document[ + current_group["start_char"] : current_group["end_char"] + ] + unified_labels.append(current_group) + current_group = { + "text": text, + "start_char": start_char, + "end_char": end_char, + "aymurai_label": aymurai_label, + } + + # Finish the last group + if current_group is not None: + current_group["text"] = document[ + current_group["start_char"] : current_group["end_char"] + ] + unified_labels.append(current_group) + + return unified_labels + + +def replace_labels_in_text( + pred: dict, + text_key: str = "document", + render_context: dict | None = None, +) -> str: + """ + Replaces labels in the text with anonymized tokens. + + Args: + pred (dict): A dictionary representing the prediction. + text_key (str, optional): The key for the text in the prediction dictionary. + Defaults to "document". + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + Returns: + str: The text with replaced labels. + """ + pred = deepcopy(pred) + doc = pred[text_key] + + # Unify consecutive labels + unified_labels = unify_consecutive_labels( + pred, render_context=render_context, text_key=text_key + ) + + # Initialize the offset + offset = 0 + + # Replace labels in the text + for unified_label in unified_labels: + # Adjust start and end character indices of the label + start_char = unified_label["start_char"] + offset + end_char = unified_label["end_char"] + offset + len_text_to_replace = end_char - start_char + + # Replace the text with the anonymized token + aymurai_label = f" <{unified_label['aymurai_label']}>" + len_aymurai_label = len(aymurai_label) + + doc = doc[:start_char] + aymurai_label + doc[end_char:] + + # Update the offset + offset += len_aymurai_label - len_text_to_replace + + return re.sub(r" +", " ", doc).strip() + + +def erase_duplicates_justseen(series: pd.Series) -> pd.Series: + """ + Replaces consecutive duplicate values in a pandas Series with an empty string, keeping only the first occurrence. + + Args: + series (pd.Series): The input pandas Series. + + Returns: + pd.Series: A pandas Series with consecutive duplicates replaced by an empty string. + """ + return series.where(series.ne(series.shift(), fill_value=None), "") + + +def parse_token_indices( + sample: dict, render_context: dict | None = None +) -> pd.DataFrame: + """ + Parses the token indices from a sample. + + Args: + sample (dict): A dictionary representing the sample. + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + + Returns: + pd.DataFrame: A pandas DataFrame representing the parsed token indices. + """ + original_text = " ".join( + [fragment["text"] for fragment in sample["metadata"]["fragments"]] + ) + anonymized_text = replace_labels_in_text(sample, render_context=render_context) + + aligned = align_text( + " " + original_text + " ", + " " + anonymized_text + " ", + ) + aligned["target"] = erase_duplicates_justseen(aligned["target"]) + + xml_file = sample["metadata"]["xml_file"] + + tokens = [] + for i, fragment in enumerate(sample["metadata"]["fragments"]): + text = fragment["text"] + tokenized_text = tokenize(text) + paragraph_index = fragment["paragraph_index"] + + # Use re.finditer to locate each instance of tokens in the text + token_matches = list( + re.finditer(r"\S+", text) + ) # \S+ matches any non-whitespace sequence + + # Loop over tokenized text and token_matches in parallel + for j, (token, match) in enumerate(zip(tokenized_text, token_matches)): + start = sample["metadata"]["start"] + fragment["start"] + match.start() + end = start + len(token) + + tokens.append((xml_file, paragraph_index, i, j, token, start, end)) + + tokens = pd.DataFrame( + tokens, + columns=[ + "xml_file", + "paragraph_index", + "fragment_index", + "token_index", + "token", + "start_char", + "end_char", + ], + ) + + tokens = pd.concat( + [tokens, aligned["target"].iloc[1:-1].reset_index(drop=True)], axis=1 + ) + + tokens["target"] = tokens["target"].fillna("") + + return tokens + + +def index_paragraphs(file: str) -> list[dict]: + """ + Indexes the paragraphs of an XML file. + + Args: + file (str): The path to the XML file to be indexed. + + Returns: + list[dict]: A list of dictionaries representing the indexed paragraphs. + """ + # Read the XML file + with open(file, encoding="utf-8-sig") as f: + xml = f.read() + + paragraphs = [] + paragraph_index = 0 + + # Find all paragraphs in the XML file + for match in re.finditer(REGEX_PARAGRAPH, xml): + paragraph = match.group("paragraph") + paragraph_start = match.start("paragraph") + paragraph_end = match.end("paragraph") + fragments = [] + fragment_index = 0 + + # Find all text fragments in the paragraph + for fragment in re.finditer(REGEX_FRAGMENT, paragraph): + text = fragment.group("text") + start = fragment.start("text") + end = fragment.end("text") + + fragment_dict = { + "text": text, + "normalized_text": FlairTextNormalize.normalize_text(text), + "start": start, + "end": end, + "fragment_index": fragment_index, + "paragraph_index": paragraph_index, + } + fragments.append(fragment_dict) + fragment_index += 1 + + # Join all fragments as plain text + plain_text = "".join([fragment["normalized_text"] for fragment in fragments]) + + paragraphs.append( + { + "plain_text": plain_text, + "metadata": { + "start": paragraph_start, + "end": paragraph_end, + "fragments": fragments, + "xml_file": os.path.basename(file), + }, + } + ) + paragraph_index += 1 + + return paragraphs + + +def match_paragraphs_with_predictions( + paragraphs: list[dict], + predictions: list[dict], +) -> list[dict]: + """ + Matches each paragraph with its corresponding predictions. + + Args: + paragraphs (list[dict]): A list of dictionaries representing the paragraphs. + predictions (list[dict]): A list of dictionaries representing + the predictions. + + Returns: + list[dict]: A list of dictionaries representing + the matched paragraphs with predictions. + """ + + paragraphs = deepcopy(paragraphs) + + # Hash prediction documents + pred_hashes = [hash(prediction["document"]) for prediction in predictions] + idx2hash = {i: _hash for i, _hash in enumerate(pred_hashes)} + + # Hash paragraphs + paragraphs = [ + paragraph | {"hash": hash(normalize("NFKC", paragraph["plain_text"].strip()))} + for paragraph in paragraphs + ] + + # Assign prediction indices to each paragraph by hash + hash2idx = { + paragraph["hash"]: np.where(np.array(pred_hashes) == paragraph["hash"])[ + 0 + ].tolist() + for paragraph in paragraphs + } + + paragraphs = [ + paragraph | {"pred_indices": hash2idx[paragraph["hash"]]} + for paragraph in paragraphs + ] + + # Identify missing indices + missing_indices = list(set(idx2hash.keys()) - set(flatten(list(hash2idx.values())))) + + if missing_indices: + # Assign prediction indices to each paragraph by lowest CER + target_texts = np.array([prediction["document"] for prediction in predictions])[ + missing_indices + ] + + missing_paragraphs = [ + paragraph for paragraph in paragraphs if not paragraph["pred_indices"] + ] + + for missing_paragraph in missing_paragraphs: + source_text = missing_paragraph["plain_text"] + min_cer_idx = np.argmin( + [cer(source_text, target_text) for target_text in target_texts] + ) + missing_paragraph["pred_indices"] = [missing_indices[min_cer_idx]] + + # Assign document text and labels + paragraphs = [ + paragraph + | { + "document": predictions[paragraph["pred_indices"][0]]["document"], + "labels": predictions[paragraph["pred_indices"][0]]["labels"], + } + for paragraph in paragraphs + ] + + return paragraphs diff --git a/aymurai/text/anonymization/base.py b/aymurai/text/anonymization/base.py new file mode 100644 index 00000000..a1631159 --- /dev/null +++ b/aymurai/text/anonymization/base.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any + + +class InvalidDocumentAnonymizer(Exception): + """Raised when an anonymizer receives an invalid or unsupported document.""" + + +class BaseAnonymizer(ABC): + """Common interface shared by all document anonymizers.""" + + extension: str + + @property + def __name__(self) -> str: + return self.__class__.__name__ + + def ensure_file(self, path: Path) -> Path: + if not path.exists(): + raise InvalidDocumentAnonymizer(f"Invalid path: {path}") + return path + + def __call__( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + return self.anonymize(item, preds, output_dir, render_context=render_context) + + @abstractmethod + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """Anonymize a document and return the output path.""" + + +_REGISTRY: dict[str, type[BaseAnonymizer]] = {} + + +def register_anonymizer(cls: type[BaseAnonymizer]) -> type[BaseAnonymizer]: + extension = getattr(cls, "extension", None) + if not extension: + raise ValueError( + f"Anonymizer {cls.__name__} must define an 'extension' attribute" + ) + + _REGISTRY[extension.lower()] = cls + return cls + + +def get_anonymizer(extension: str) -> BaseAnonymizer: + normalized = extension.lower() + try: + anonymizer_cls = _REGISTRY[normalized] + except KeyError as exc: + raise ValueError(f"Unsupported extension: {extension}") from exc + return anonymizer_cls() + + +def supported_extensions() -> set[str]: + return set(_REGISTRY.keys()) + + +__all__ = [ + "BaseAnonymizer", + "InvalidDocumentAnonymizer", + "get_anonymizer", + "register_anonymizer", + "supported_extensions", +] diff --git a/aymurai/text/anonymization/docx/__init__.py b/aymurai/text/anonymization/docx/__init__.py new file mode 100644 index 00000000..5d5d0aca --- /dev/null +++ b/aymurai/text/anonymization/docx/__init__.py @@ -0,0 +1,3 @@ +from aymurai.text.anonymization.docx.anonymizer import DocxAnonymizer + +__all__ = ["DocxAnonymizer"] diff --git a/aymurai/text/anonymization/docx/anonymizer.py b/aymurai/text/anonymization/docx/anonymizer.py new file mode 100644 index 00000000..73c43487 --- /dev/null +++ b/aymurai/text/anonymization/docx/anonymizer.py @@ -0,0 +1,122 @@ +import os +import tempfile +from datetime import datetime, timezone +from glob import glob +from pathlib import Path +from typing import Any + +from docx import Document +from more_itertools import flatten + +from aymurai.text.anonymization.alignment import ( + index_paragraphs, + match_paragraphs_with_predictions, +) +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + register_anonymizer, +) +from aymurai.text.anonymization.docx.watermark import add_footer_watermark +from aymurai.settings import settings +from aymurai.text.anonymization.docx.xml import ( + create_docx, + replace_text_in_xml, + unzip_document, +) +from aymurai.utils.cache import cache_load, cache_save, get_cache_key + + +def _set_aymurai_core_properties(doc_path: str) -> None: + """ + Applies the configured AymurAI tooling metadata fields to the DOCX core properties. + + Args: + doc_path (str): The path to the DOCX document to update. + """ + document = Document(doc_path) + core_properties = document.core_properties + core_properties.author = "" + core_properties.last_modified_by = settings.ANONYMIZATION_METADATA_CREATOR + core_properties.modified = datetime.now(timezone.utc) + document.save(doc_path) + + +@register_anonymizer +class DocxAnonymizer(BaseAnonymizer): + """ + Anonymize DOCX documents by replacing sensitive data with label tokens. + """ + + extension = "docx" + + def __init__(self, use_cache: bool = False): + self.use_cache = use_cache + + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """ + Anonymizes a DOCX document using the matched paragraph predictions. + + Args: + item (dict): The item dictionary containing the input DOCX path. + preds (list[dict]): The predictions to apply to the document. + output_dir (str, optional): The directory where the anonymized document should be written. Defaults to '.'. + render_context (dict[str, Any] | None, optional): The rendering context used to resolve replacement tokens. + Defaults to None. + + Returns: + str: The path to the anonymized DOCX output file. + """ + item_path = Path(item["path"]) + file_path = self.ensure_file(item_path) + + if file_path.suffix.lower() != ".docx": + raise InvalidDocumentAnonymizer("Only `.docx` extension is allowed.") + + if not item.get("data"): + item["data"] = {} + + cache_key = get_cache_key(str(file_path), self.__name__) + if self.use_cache and (cache_data := cache_load(key=cache_key)): + paragraphs = cache_data + else: + # Unzip document into a temporary directory + with tempfile.TemporaryDirectory() as tempdir: + unzip_document(str(file_path), tempdir) + + # Parse XML files + xml_files = glob(f"{tempdir}/**/*.xml", recursive=True) + paragraphs = (index_paragraphs(file) for file in xml_files) + paragraphs = list(flatten(paragraphs)) + + # Filter out empty paragraphs + paragraphs = [ + paragraph + for paragraph in paragraphs + if paragraph["plain_text"].strip() + ] + # Matching + paragraphs = match_paragraphs_with_predictions(paragraphs, preds) + + # Edit XML files + replace_text_in_xml(paragraphs, tempdir, render_context) + + # Recreate anonymized document + os.makedirs(output_dir, exist_ok=True) + output_path = f"{output_dir}/{os.path.basename(str(file_path))}" + create_docx(tempdir, output_path) + + # Add metadata branding and the footer watermark + _set_aymurai_core_properties(output_path) + add_footer_watermark(output_path) + + if self.use_cache: + cache_save(paragraphs, key=cache_key) + + return f"{output_dir}/{os.path.basename(str(file_path))}" diff --git a/aymurai/text/anonymization/docx/watermark.py b/aymurai/text/anonymization/docx/watermark.py new file mode 100644 index 00000000..3199bb9e --- /dev/null +++ b/aymurai/text/anonymization/docx/watermark.py @@ -0,0 +1,197 @@ +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.opc.constants import RELATIONSHIP_TYPE +from docx.oxml.shared import OxmlElement, qn +from docx.shared import Inches, Pt, RGBColor + + +def _add_hyperlink( + paragraph, + text: str, + url: str, + font_name: str = "Archivo", + size: int = 10, + color: RGBColor = RGBColor(115, 190, 250), + italic: bool = False, + bold: bool = True, + underline: bool = True, +) -> None: + """ + Adds a formatted hyperlink to a given paragraph in a Word document. + + Notes: + - This method directly manipulates the underlying XML of the paragraph to insert a hyperlink, + as python-docx does not natively support hyperlinks. + - The hyperlink will be appended to the end of the given paragraph. + - Formatting options (font, size, color, italic, bold, underline) are applied to the hyperlink text. + + Args: + paragraph: The python-docx paragraph object to which the hyperlink will be added. + text (str): The display text for the hyperlink. + url (str): The URL that the hyperlink points to. + font_name (str, optional): The font name to use for the hyperlink text. Defaults to "Archivo". + size (int, optional): The font size (in points) for the hyperlink text. Defaults to 10. + color (RGBColor, optional): The font color as an RGBColor tuple. Defaults to RGBColor(115, 190, 250). + italic (bool, optional): Whether the hyperlink text should be italicized. Defaults to False. + bold (bool, optional): Whether the hyperlink text should be bold. Defaults to True. + underline (bool, optional): Whether the hyperlink text should be underlined. Defaults to True. + + Raises: + ValueError: If the paragraph is not a valid python-docx paragraph object. + """ + # Create the hyperlink relationship + part = paragraph.part + r_id = part.relate_to(url, RELATIONSHIP_TYPE.HYPERLINK, is_external=True) + + # Create the hyperlink element + hyperlink = OxmlElement("w:hyperlink") + hyperlink.set(qn("r:id"), r_id) + + # Create a new run + new_run = OxmlElement("w:r") + + # Set run properties (formatting) + r_pr = OxmlElement("w:rPr") + + # Set font + if font_name: + font = OxmlElement("w:rFonts") + font.set(qn("w:ascii"), font_name) + font.set(qn("w:hAnsi"), font_name) + r_pr.append(font) + + # Set color + if color: + color_el = OxmlElement("w:color") + color_el.set(qn("w:val"), f"{color[0]:02x}{color[1]:02x}{color[2]:02x}") + r_pr.append(color_el) + + # Set italic + if italic: + r_pr.append(OxmlElement("w:i")) + + # Set bold + if bold: + r_pr.append(OxmlElement("w:b")) + + # Set underline - added for links + if underline: + underline_el = OxmlElement("w:u") + underline_el.set(qn("w:val"), "single") # single underline + r_pr.append(underline_el) + + # Set size + sz = OxmlElement("w:sz") + sz.set(qn("w:val"), str(size * 2)) # Word uses half-points + r_pr.append(sz) + + # Add properties to run + new_run.append(r_pr) + + # Set text + text_el = OxmlElement("w:t") + text_el.text = text + new_run.append(text_el) + + # Add run to hyperlink + hyperlink.append(new_run) + + # Add hyperlink to paragraph + paragraph._p.append(hyperlink) + + +def _add_watermark_to_footer( + footer, + alignment, + font_name: str = "Archivo", + hyperlink_text: str = "AymurAI", + hyperlink_url: str = "https://www.aymurai.info/", + watermark_text: str = "Documento anonimizado por AymurAI", +) -> None: + """ + Adds a watermark text to the footer of a document. + + Args: + footer: The footer object to which the watermark will be added. + alignment: The alignment setting for the paragraph (e.g., left, center, right). + font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". + hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". + hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". + watermark_text (str): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". + """ + paragraph = footer.add_paragraph() + paragraph.alignment = alignment + + if hyperlink_url and hyperlink_text in watermark_text: + parts = watermark_text.split(hyperlink_text, 1) + before_text = parts[0] + after_text = parts[1] if len(parts) > 1 else "" + + # Add text before the hyperlink + if before_text: + run = paragraph.add_run(before_text) + run.font.name = "Archivo" + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + # Add hyperlink + _add_hyperlink(paragraph, hyperlink_text, hyperlink_url) + + # Add text after the hyperlink + if after_text: + run = paragraph.add_run(after_text) + run.font.name = font_name + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + else: + # Just add the full text without a hyperlink + run = paragraph.add_run(watermark_text) + run.font.name = font_name + run.font.color.rgb = RGBColor(192, 192, 192) + run.font.size = Pt(10) + + +def add_footer_watermark( + doc_path: str, + font_name: str = "Archivo", + hyperlink_text: str = "AymurAI", + hyperlink_url: str = "https://www.aymurai.info/", + watermark_text: str = "Documento anonimizado por AymurAI", +) -> None: + """ + Adds a watermark to the footer of each section in a Word document. + + Args: + doc_path (str): Path to the document. + font_name (str, optional): The font name to use for the watermark text. Defaults to "Archivo". + hyperlink_text (str, optional): The text to be hyperlinked. Defaults to "AymurAI". + hyperlink_url (str, optional): The URL to link "AymurAI" to. Defaults to "https://www.aymurai.info/". + watermark_text (str, optional): The text to be used as the watermark. Defaults to "Documento anonimizado por AymurAI". + """ + document = Document(doc_path) + processed_footers = set() + + for section in document.sections: + section.footer_distance = Inches(0.1) + + # List of (footer_obj, alignment) tuples + footers = [(section.footer, WD_ALIGN_PARAGRAPH.RIGHT)] # Odd/default + if section.even_page_footer is not None: + footers.append((section.even_page_footer, WD_ALIGN_PARAGRAPH.LEFT)) + if section.different_first_page_header_footer: + footers.append((section.first_page_footer, WD_ALIGN_PARAGRAPH.RIGHT)) + + for footer, alignment in footers: + if id(footer) not in processed_footers: + _add_watermark_to_footer( + footer, + alignment, + font_name=font_name, + hyperlink_text=hyperlink_text, + hyperlink_url=hyperlink_url, + watermark_text=watermark_text, + ) + processed_footers.add(id(footer)) + + document.save(doc_path) diff --git a/aymurai/text/anonymization/docx/xml.py b/aymurai/text/anonymization/docx/xml.py new file mode 100644 index 00000000..1ae9f50e --- /dev/null +++ b/aymurai/text/anonymization/docx/xml.py @@ -0,0 +1,172 @@ +import os +import re +import xml.sax.saxutils +import zipfile + +import pandas as pd +from lxml import etree + +from aymurai.text.anonymization.alignment import parse_token_indices + + +def unzip_document(doc_path: str, output_dir: str) -> None: + """ + Unzips the document file to the specified output directory. + + Args: + doc_path (str): The path to the document file. + output_dir (str): The directory where the contents of the document + file will be extracted. + """ + # Ensure the output directory exists + os.makedirs(output_dir, exist_ok=True) + + # Open the doc file as a zip file + with zipfile.ZipFile(doc_path, "r") as doc_zip: + # Extract all the contents to the output directory + doc_zip.extractall(output_dir) + + +def normalize_document(xml_content: str) -> str: + """ + Normalizes the XML document by removing extra spaces, preserving line breaks, + and removing hyperlinks while preserving text content. + + Args: + xml_content (str): The XML content to be normalized. + + Returns: + str: The normalized XML content. + """ + # Parse the XML content + parser = etree.XMLParser(ns_clean=True) + root = etree.fromstring(xml_content.encode("utf-8"), parser) + + # Extract namespaces + namespaces = {k: v for k, v in root.nsmap.items() if k} + + # Remove hyperlinks but preserve the text content + for hyperlink in root.xpath("//w:hyperlink", namespaces=namespaces): + parent = hyperlink.getparent() + index = list(parent).index(hyperlink) + + # Move all text-containing children (e.g., w:r elements) outside the hyperlink tag + for child in hyperlink: + parent.insert(index, child) + index += 1 + parent.remove(hyperlink) # Remove the element itself + + # Process each paragraph + for wp in root.xpath("//w:p", namespaces=namespaces): + first = True + + # Find all w:r elements containing w:t elements + for wr in wp.xpath(".//w:r", namespaces=namespaces): + wt = wr.find(".//w:t", namespaces) + if wt is not None and wt.text: + # Normalize spaces within the text, preserving new line breaks + wt.text = re.sub(r"[^\S\r\n]+", " ", wt.text) + + # Add a leading space if not the first fragment in the paragraph + if not first: + wt.text = " " + wt.text.lstrip() + else: + wt.text = wt.text.lstrip() + first = False + + # Remove trailing spaces from all fragments + wt.text = wt.text.rstrip() + + # Set the xml:space attribute to preserve + wt.set( + "{http://www.w3.org/XML/1998/namespace}space", + "preserve", + ) + + # Check if the text is empty after normalization + if not wt.text or wt.text.strip() == "": + # Remove the w:r element from its parent + wr.getparent().remove(wr) + + # Write back the XML content to a string + xml_str = etree.tostring(root, encoding="unicode", pretty_print=True) + + return xml_str + + +def replace_text_in_xml( + paragraphs: list[dict], base_dir: str, render_context: dict | None = None +) -> None: + """ + Replaces text in XML files based on the provided paragraphs + and saves the modified files. + + Args: + paragraphs (list[dict]): A list of dictionaries representing + the paragraphs to be replaced. + base_dir (str): The base directory where the XML files are located. + render_context (dict | None, optional): The context for rendering labels. Defaults to None. + """ + tokens = pd.concat( + [parse_token_indices(sample, render_context) for sample in paragraphs], + ignore_index=True, + ) + + fragments = ( + tokens.groupby(["xml_file", "paragraph_index", "fragment_index"]) + .agg({"target": " ".join, "start_char": "min", "end_char": "max"}) + .reset_index() + ) + + for xml_file, group in fragments.groupby("xml_file"): + group = group.sort_values("end_char", ascending=False) + + with open(f"{base_dir}/word/{xml_file}", "r+") as file: + content = file.read() + + for _, r in group.iterrows(): + start_char = r["start_char"] + end_char = r["end_char"] + + target = r["target"] + target = re.sub(r"[^\S\r\n]+", " ", target) + + # Escape XML special characters + target = xml.sax.saxutils.escape(target) + + content = content[:start_char] + target + content[end_char:] + + # MUST be at the end to dont screw up the indexes + content = normalize_document(content) + + file.seek(0) + file.write(content) + file.truncate() + + +def add_files_to_zip(zip_file: zipfile.ZipFile, directory: str) -> None: + """ + Adds all files in the specified directory to a zip file. + + Args: + zip_file (zipfile.ZipFile): The zip file to add the files to. + directory (str): The directory containing the files to be added. + """ + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + zip_file.write(file_path, os.path.relpath(file_path, directory)) + + +def create_docx(xml_directory: str, output_file: str) -> None: + """ + Creates a new DOCX file by adding XML components from the specified directory. + + Args: + xml_directory (str): The directory containing the XML components. + output_file (str): The path to the output DOCX file. + """ + # Create a new zip file + with zipfile.ZipFile(output_file, "w") as docx: + # Add XML components + add_files_to_zip(docx, xml_directory) diff --git a/aymurai/text/anonymization/pdf/__init__.py b/aymurai/text/anonymization/pdf/__init__.py new file mode 100644 index 00000000..21271aae --- /dev/null +++ b/aymurai/text/anonymization/pdf/__init__.py @@ -0,0 +1,3 @@ +from aymurai.text.anonymization.pdf.anonymizer import PdfAnonymizer + +__all__ = ["PdfAnonymizer"] diff --git a/aymurai/text/anonymization/pdf/anonymizer.py b/aymurai/text/anonymization/pdf/anonymizer.py new file mode 100644 index 00000000..0030c24b --- /dev/null +++ b/aymurai/text/anonymization/pdf/anonymizer.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import pymupdf +import pymupdf.layout # noqa: F401 # activates layout support +from pymupdf4llm.helpers import document_layout as pymupdf4llm_document_layout + +from aymurai.text.anonymization.base import ( + BaseAnonymizer, + InvalidDocumentAnonymizer, + register_anonymizer, +) +from aymurai.text.anonymization.pdf.layout import ( + _apply_minimal_boundary_merge, + _build_layout_paragraphs, + _match_predictions_to_layout, +) +from aymurai.text.anonymization.pdf.ops import ( + _apply_redactions, + _collect_page_redactions, +) +from aymurai.text.anonymization.pdf.sanitize import ( + _collect_link_cleanup_rects, + _sanitize_document, +) +from aymurai.text.anonymization.pdf.watermark import add_pdf_footer_watermark + + +@register_anonymizer +class PdfAnonymizer(BaseAnonymizer): + """ + Anonymize PDF documents by replacing sensitive data with label tokens. + """ + + extension = "pdf" + + def anonymize( + self, + item: dict, + preds: list[dict], + output_dir: str = ".", + render_context: dict[str, Any] | None = None, + ) -> str: + """ + Anonymizes a PDF document using the matched paragraph predictions. + + Args: + item (dict): The item dictionary containing the input PDF path. + preds (list[dict]): The predictions to apply to the document. + output_dir (str, optional): The directory where the anonymized document should be written. Defaults to '.'. + render_context (dict[str, Any] | None, optional): The rendering context used to resolve replacement tokens. Defaults to None. + + Returns: + str: The path to the anonymized PDF output file. + """ + item_path = Path(item["path"]) + file_path = self.ensure_file(item_path) + + if file_path.suffix.lower() != ".pdf": + raise InvalidDocumentAnonymizer("Only `.pdf` extension is allowed.") + + with pymupdf.open(str(file_path)) as doc: + parsed_doc = pymupdf4llm_document_layout.parse_document( + doc, + filename=str(file_path), + show_progress=False, + force_text=True, + use_ocr=False, + force_ocr=False, + ) + + layout_paragraphs = _build_layout_paragraphs(parsed_doc) + matched_paragraphs = _match_predictions_to_layout( + layout_paragraphs, + preds, + ) + + _apply_minimal_boundary_merge(matched_paragraphs, render_context) + page_ops, widget_ops, signature_widget_ops = _collect_page_redactions( + doc, + matched_paragraphs, + render_context, + ) + _apply_redactions(doc, page_ops, widget_ops, signature_widget_ops) + cleanup_rects = _collect_link_cleanup_rects( + page_ops, + widget_ops, + signature_widget_ops, + ) + _sanitize_document(doc, cleanup_rects) + add_pdf_footer_watermark(doc) + + os.makedirs(output_dir, exist_ok=True) + output_path = Path(output_dir) / f"{file_path.stem}.anonymized.pdf" + doc.save(str(output_path), garbage=4, clean=1, deflate=1) + + return str(output_path) diff --git a/aymurai/text/anonymization/pdf/common.py b/aymurai/text/anonymization/pdf/common.py new file mode 100644 index 00000000..b17c22c2 --- /dev/null +++ b/aymurai/text/anonymization/pdf/common.py @@ -0,0 +1,620 @@ +from __future__ import annotations + +import re +from functools import lru_cache +from typing import Any +from unicodedata import normalize + +import pymupdf + +TEXT_FLAG_ITALIC = 2 +TEXT_FLAG_SERIF = 4 +TEXT_FLAG_MONOSPACED = 8 +TEXT_FLAG_BOLD = 16 +PDF_TAG_MIN_FONT_SIZE = 7.0 +PDF_TAG_FONT_STEP = 0.5 +PDF_TAG_MAX_ABBREVIATION = 3 +PDF_TOKEN_ALIAS_MAP: dict[str, tuple[str, ...]] = { + "CORREO_ELECTRONICO": ("CORREO", "MAIL"), + "CUIT_CUIL": ("CUIT", "CUIL"), + "DIRECCION": ("DIR",), + "ESTUDIOS": ("EDU",), + "MARCA_AUTOMOVIL": ("VEHICULO", "AUTO"), + "NACIONALIDAD": ("PAIS", "NAC"), + "NOMBRE_ARCHIVO": ("ARCHIVO", "FILE"), + "NUM_ACTUACION": ("ACTUACION", "ACT"), + "NUM_CAJA_AHORRO": ("CAJA_AHORRO", "CAJA"), + "NUM_EXPEDIENTE": ("EXPEDIENTE", "EXPTE"), + "NUM_MATRICULA": ("MATRICULA", "MAT"), + "PATENTE_DOMINIO": ("PATENTE", "DOMINIO"), + "TELEFONO": ("TEL",), + "TEXTO_ANONIMIZAR": ("ANONIMIZAR", "TEXTO"), + "USUARIX": ("USER",), +} +PDF_TAG_RECT_X_PADDING = 0.5 +PDF_TAG_RECT_Y_PADDING = 0.0 +PDF_TAG_RECT_INSET = 0.5 +PDF_TAG_RECT_GAP_FACTOR = 0.5 +PDF_TAG_RECT_GAP_MIN = 3.0 +PDF_TAG_RECT_GAP_MAX = 8.0 + + +def _line_text(line: dict) -> str: + """ + Builds the plain text content for a parsed PDF line. + + Args: + line (dict): The parsed line metadata being processed. + + Returns: + str: The concatenated text content for the line. + """ + return "".join(span.get("text", "") for span in line.get("spans", [])) + + +def _rect_tuple(value: Any) -> tuple[float, float, float, float]: + """ + Normalizes a rectangle-like value into a coordinate tuple. + + Args: + value (Any): The rectangle-like value to normalize. + + Returns: + tuple[float, float, float, float]: The normalized rectangle coordinates. + """ + if isinstance(value, pymupdf.Rect): + return (float(value.x0), float(value.y0), float(value.x1), float(value.y1)) + if isinstance(value, (list, tuple)) and len(value) == 4: + return (float(value[0]), float(value[1]), float(value[2]), float(value[3])) + raise ValueError(f"Invalid rectangle value: {value}") + + +def _default_style(fallback_size: float = 10.0) -> dict[str, Any]: + """ + Builds a default text style dictionary for PDF rendering helpers. + + Args: + fallback_size (float, optional): The fallback font size used when no style data is available. Defaults to 10.0. + + Returns: + dict[str, Any]: The default style dictionary. + """ + return { + "font": "", + "flags": 0, + "color": (0.0, 0.0, 0.0), + "size": fallback_size, + "ascender": 0.8, + "descender": -0.2, + } + + +def _span_text_weight(span: dict) -> tuple[int, float]: + """ + Computes a sorting weight for a span based on text length and size. + + Args: + span (dict): The span metadata being evaluated. + + Returns: + tuple[int, float]: The text-length and size weight for the span. + """ + text = str(span.get("text") or "").strip() + return (len(text), float(span.get("size") or 0.0)) + + +def _pdf_color_from_span(span: dict) -> tuple[float, float, float]: + """ + Converts a span color value into PDF RGB components. + + Args: + span (dict): The span metadata being evaluated. + + Returns: + tuple[float, float, float]: The PDF RGB color components for the span. + """ + try: + return tuple( + float(value) for value in pymupdf.sRGB_to_pdf(int(span.get("color") or 0)) + ) + except Exception: + return (0.0, 0.0, 0.0) + + +def _line_style(line: dict, fallback_size: float = 10.0) -> dict[str, Any]: + """ + Determines the dominant text style for a parsed PDF line. + + Args: + line (dict): The parsed line metadata being processed. + fallback_size (float, optional): The fallback font size used when no style data is available. Defaults to 10.0. + + Returns: + dict[str, Any]: The dominant style dictionary for the line. + """ + spans = [ + span for span in line.get("spans") or [] if str(span.get("text") or "").strip() + ] + if not spans: + return _default_style(fallback_size) + + dominant = max(spans, key=_span_text_weight) + return { + "font": str(dominant.get("font") or ""), + "flags": int(dominant.get("flags") or 0), + "color": _pdf_color_from_span(dominant), + "size": float(dominant.get("size") or fallback_size), + "ascender": float(dominant.get("ascender") or 0.8), + "descender": float(dominant.get("descender") or -0.2), + } + + +def _build_spans_detail(line: dict) -> tuple[list[dict], int]: + """ + Builds per-span style metadata and character offsets for a line. + + Args: + line (dict): The parsed line metadata being processed. + + Returns: + tuple[list[dict], int]: The span detail list and left-strip offset. + """ + raw_text = normalize("NFKC", _line_text(line)) + strip_offset = len(raw_text) - len(raw_text.lstrip()) + + spans_detail: list[dict] = [] + cursor = 0 + for span in line.get("spans", []): + span_text = normalize("NFKC", span.get("text", "")) + span_start = cursor + cursor += len(span_text) + spans_detail.append( + { + "start": span_start, + "end": cursor, + "style": { + "font": str(span.get("font") or ""), + "flags": int(span.get("flags") or 0), + "color": _pdf_color_from_span(span), + "size": float(span.get("size") or 10.0), + "ascender": float(span.get("ascender") or 0.8), + "descender": float(span.get("descender") or -0.2), + }, + } + ) + return spans_detail, strip_offset + + +def _entity_style_from_spans( + line_entry: dict, + offset_in_stripped_text: int, +) -> dict[str, Any]: + """ + Resolves the style for the entity offset inside a line entry. + + Args: + line_entry (dict): The `line_entry` value used by this helper. + offset_in_stripped_text (int): The entity offset inside the stripped line text. + + Returns: + dict[str, Any]: The resolved style dictionary for the entity offset. + """ + spans_detail = line_entry.get("spans_detail") + if not spans_detail: + return line_entry.get("style") or _default_style() + + strip_offset = line_entry.get("strip_offset", 0) + raw_offset = offset_in_stripped_text + strip_offset + + for span_info in spans_detail: + if span_info["start"] <= raw_offset < span_info["end"]: + return span_info["style"] + + return line_entry.get("style") or _default_style() + + +def _font_size(line: dict, fallback: float = 10.0) -> float: + """ + Calculates a representative font size for a parsed line. + + Args: + line (dict): The parsed line metadata being processed. + fallback (float, optional): The fallback font size to use when the line has no span sizes. Defaults to 10.0. + + Returns: + float: The representative font size for the line. + """ + spans = line.get("spans") or [] + sizes = [float(span.get("size")) for span in spans if span.get("size")] + if not sizes: + return fallback + size = sum(sizes) / len(sizes) + return max(size * 0.9, PDF_TAG_MIN_FONT_SIZE) + + +def _style_flags(style: dict[str, Any]) -> tuple[bool, bool, bool, bool]: + """ + Extracts boolean style flags from a style dictionary. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + tuple[bool, bool, bool, bool]: The bold, italic, monospace, and serif flags. + """ + flags = int(style.get("flags") or 0) + font_label = str(style.get("font") or "").lower() + + is_bold = bool(flags & TEXT_FLAG_BOLD) or "bold" in font_label + is_italic = bool(flags & TEXT_FLAG_ITALIC) or any( + token in font_label for token in ("italic", "oblique") + ) + is_mono = bool(flags & TEXT_FLAG_MONOSPACED) or any( + token in font_label for token in ("courier", "mono", "console") + ) + is_serif = bool(flags & TEXT_FLAG_SERIF) or any( + token in font_label + for token in ("times", "serif", "georgia", "garamond", "mistral") + ) + return is_bold, is_italic, is_mono, is_serif + + +def _base14_fontname_for_style(style: dict[str, Any]) -> str: + """ + Maps a style dictionary to the closest Base-14 font name. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + str: The Base-14 font name that best matches the style. + """ + is_bold, is_italic, is_mono, is_serif = _style_flags(style) + + if is_mono: + family = "Courier" + elif is_serif: + family = "Times" + else: + family = "Helvetica" + + variants = { + ("Helvetica", False, False): "Helvetica", + ("Helvetica", True, False): "Helvetica-Bold", + ("Helvetica", False, True): "Helvetica-Oblique", + ("Helvetica", True, True): "Helvetica-BoldOblique", + ("Times", False, False): "Times-Roman", + ("Times", True, False): "Times-Bold", + ("Times", False, True): "Times-Italic", + ("Times", True, True): "Times-BoldItalic", + ("Courier", False, False): "Courier", + ("Courier", True, False): "Courier-Bold", + ("Courier", False, True): "Courier-Oblique", + ("Courier", True, True): "Courier-BoldOblique", + } + return variants[(family, is_bold, is_italic)] + + +def _build_flexible_pattern(text: str) -> str: + """ + Builds a whitespace-tolerant regex pattern for the given text. + + Args: + text (str): The text value being normalized or searched. + + Returns: + str: The whitespace-tolerant regex pattern. + """ + tokens = [re.escape(tok) for tok in re.split(r"\s+", text.strip()) if tok] + return r"\s+".join(tokens) + + +def _find_flexible( + haystack: str, + needle: str, + start: int = 0, +) -> tuple[int, int] | None: + """ + Finds a text span using exact and whitespace-tolerant matching. + + Args: + haystack (str): The source text to search within. + needle (str): The target text to search for. + start (int, optional): The preferred start offset for the search. Defaults to 0. + + Returns: + tuple[int, int] | None: The start and end offsets of the match, if found. + """ + if not needle: + return None + + idx = haystack.find(needle, start) + if idx >= 0: + return idx, idx + len(needle) + + pattern = _build_flexible_pattern(needle) + if not pattern: + return None + + match = re.search(pattern, haystack[start:]) + if match: + return start + match.start(), start + match.end() + + if start > 0: + match = re.search(pattern, haystack) + if match: + return match.start(), match.end() + + return None + + +def _token_parts(token: str) -> tuple[str, str | None]: + """ + Splits a logical token into its base label and numeric suffix. + + Args: + token (str): The logical replacement token being processed. + + Returns: + tuple[str, str | None]: The token base and optional numeric suffix. + """ + match = re.match(r"^(.*?)(?:_(\d+))?$", token) + if not match: + normalized = token.strip() or "ENT" + return normalized, None + + base = match.group(1).strip() or "ENT" + suffix = match.group(2) + return base, suffix + + +def _abbreviate_token(base: str, length: int) -> str: + """ + Builds an abbreviated token label with the requested length. + + Args: + base (str): The token base label to abbreviate or alias. + length (int): The target abbreviation length. + + Returns: + str: The abbreviated token label. + """ + normalized = "".join(char for char in base.upper() if char.isalnum()) + if not normalized: + normalized = "ENT" + return normalized[:length] or normalized[:1] or "E" + + +def _token_aliases(base: str) -> tuple[str, ...]: + """ + Returns configured alias labels for a token base. + + Args: + base (str): The token base label to abbreviate or alias. + + Returns: + tuple[str, ...]: The configured aliases for the token base. + """ + aliases = PDF_TOKEN_ALIAS_MAP.get(base.upper(), ()) + normalized_aliases: list[str] = [] + + for alias in aliases: + normalized = re.sub(r"[^A-Z0-9_]", "", str(alias).upper()) + if ( + normalized + and normalized != base.upper() + and normalized not in normalized_aliases + ): + normalized_aliases.append(normalized) + + return tuple(normalized_aliases) + + +def _build_display_token_candidates(token: str) -> list[str]: + """ + Builds the list of token display candidates to try when rendering. + + Args: + token (str): The logical replacement token being processed. + + Returns: + list[str]: The candidate display tokens to try when rendering. + """ + base, suffix = _token_parts(token.upper()) + candidates: list[str] = [] + + def add(value: str) -> None: + """ + Appends a token display candidate when it has not been added yet. + + Args: + value (str): The rectangle-like value to normalize. + """ + if value and value not in candidates: + candidates.append(value) + + def add_base_variants(label: str) -> None: + """ + Appends the base token variants for the current label candidate. + + Args: + label (str): The label metadata being processed. + """ + if suffix: + add(f"<{label}_{suffix}>") + add(f"<{label}>") + + add_base_variants(base) + + for alias in _token_aliases(base): + add_base_variants(alias) + + abbreviated = _abbreviate_token(base, PDF_TAG_MAX_ABBREVIATION) + add_base_variants(abbreviated) + + return candidates + + +def _iter_font_sizes(start_size: float) -> list[float]: + """ + Builds the descending font sizes to try when fitting a token. + + Args: + start_size (float): The `start_size` value used by this helper. + + Returns: + list[float]: The font sizes to try in descending order. + """ + if start_size <= 0: + return [] + + sizes: list[float] = [start_size] + current = start_size + while current - PDF_TAG_FONT_STEP >= PDF_TAG_MIN_FONT_SIZE - 1e-6: + current = round(current - PDF_TAG_FONT_STEP, 2) + if current not in sizes: + sizes.append(current) + + return sizes + + +def _fit_display_token( + token: str, + rect: pymupdf.Rect, + fontname: str, + base_font_size: float, + font_obj: pymupdf.Font | None = None, +) -> tuple[str | None, float | None]: + """ + Finds a token rendering variant and font size that fit inside a rectangle. + + Args: + token (str): The logical replacement token being processed. + rect (pymupdf.Rect): The rectangle used by the helper. + fontname (str): The font name to use for measurement or rendering. + base_font_size (float): The initial font size to try when fitting text. + font_obj (pymupdf.Font | None, optional): The font object used for measurement. Defaults to None. + + Returns: + tuple[str | None, float | None]: The fitted token text and font size. + """ + if rect.width <= 0 or rect.height <= 0: + return None, None + + available_width = max(rect.width - (2 * PDF_TAG_RECT_INSET), 1.0) + start_size = min(base_font_size, max(rect.height - 1.0, 1.0)) + if start_size < 1.0: + return None, None + + def _measure(text: str, size: float) -> float: + """ + Measures the width of a candidate token at the given font size. + + Args: + text (str): The text value being normalized or searched. + size (float): The font size used for the current measurement. + + Returns: + float: The measured width of the candidate text. + """ + if font_obj is not None: + try: + return font_obj.text_length(text, fontsize=size) + except Exception: + pass + return pymupdf.get_text_length(text, fontname=fontname, fontsize=size) + + for size in _iter_font_sizes(start_size): + for candidate in _build_display_token_candidates(token): + if _measure(candidate, size) <= available_width + 0.1: + return candidate, size + + return None, None + + +_BASE14_FONT_CACHE: dict[str, pymupdf.Font] = {} + + +@lru_cache(maxsize=None) +def _cached_base14_font(name: str) -> pymupdf.Font: + """ + Loads and caches a Base-14 font by name. + + Args: + name (str): The Base-14 font name to load. + + Returns: + pymupdf.Font: The cached Base-14 font object. + """ + return pymupdf.Font(name) + + +def _get_base14_font(style: dict[str, Any]) -> pymupdf.Font: + """ + Returns the cached Base-14 font object for a style dictionary. + + Args: + style (dict[str, Any]): The style dictionary being analyzed. + + Returns: + pymupdf.Font: The cached Base-14 font for the style. + """ + name = _base14_fontname_for_style(style) + font = _BASE14_FONT_CACHE.get(name) + if font is None: + font = _cached_base14_font(name) + _BASE14_FONT_CACHE[name] = font + return font + + +def _rect_vertical_overlap(left: pymupdf.Rect, right: pymupdf.Rect) -> float: + """ + Calculates the vertical overlap ratio between two rectangles. + + Args: + left (pymupdf.Rect): The left rectangle or label to compare. + right (pymupdf.Rect): The right rectangle or label to compare. + + Returns: + float: The vertical overlap ratio between the rectangles. + """ + overlap = max(0.0, min(left.y1, right.y1) - max(left.y0, right.y0)) + min_height = max(min(left.height, right.height), 1e-6) + return overlap / min_height + + +def _group_adjacent_rects( + rects: list[pymupdf.Rect], max_gap: float +) -> list[pymupdf.Rect]: + """ + Merges horizontally adjacent rectangles that belong to the same segment. + + Args: + rects (list[pymupdf.Rect]): The `rects` value used by this helper. + max_gap (float): The `max_gap` value used by this helper. + + Returns: + list[pymupdf.Rect]: The merged rectangle groups. + """ + if not rects: + return [] + + ordered = sorted(rects, key=lambda rect: (rect.y0, rect.x0, rect.x1)) + groups: list[list[pymupdf.Rect]] = [[ordered[0]]] + + for rect in ordered[1:]: + previous = groups[-1][-1] + gap = rect.x0 - previous.x1 + if _rect_vertical_overlap(previous, rect) >= 0.5 and gap <= max_gap: + groups[-1].append(rect) + else: + groups.append([rect]) + + merged_rects: list[pymupdf.Rect] = [] + for group in groups: + merged = pymupdf.Rect(group[0]) + for rect in group[1:]: + merged.include_rect(rect) + merged_rects.append(merged) + + return merged_rects diff --git a/aymurai/text/anonymization/pdf/layout.py b/aymurai/text/anonymization/pdf/layout.py new file mode 100644 index 00000000..50ce529a --- /dev/null +++ b/aymurai/text/anonymization/pdf/layout.py @@ -0,0 +1,510 @@ +from __future__ import annotations + +import re +from copy import deepcopy +from typing import Any +from unicodedata import normalize + +import pymupdf +from jiwer import cer + +from aymurai.logger import get_logger +from aymurai.text.anonymization.alignment import ( + _label_replacement_end as _label_end, +) +from aymurai.text.anonymization.alignment import ( + _label_replacement_start as _label_start, +) +from aymurai.text.anonymization.alignment import ( + resolve_render_token, +) +from aymurai.text.anonymization.pdf.common import ( + PDF_TAG_RECT_GAP_FACTOR, + PDF_TAG_RECT_GAP_MAX, + PDF_TAG_RECT_GAP_MIN, + _build_flexible_pattern, + _build_spans_detail, + _font_size, + _group_adjacent_rects, + _line_style, + _line_text, + _rect_tuple, + _rect_vertical_overlap, +) + +logger = get_logger(__name__) + + +def _same_boundary_candidate(left: dict, right: dict) -> bool: + """ + Checks whether two labels can share a merged boundary token. + + Args: + left (dict): The left rectangle or label to compare. + right (dict): The right rectangle or label to compare. + + Returns: + bool: Whether the labels can share a boundary token. + """ + left_attrs = left.get("attrs") or {} + right_attrs = right.get("attrs") or {} + + if left_attrs.get("aymurai_label") != right_attrs.get("aymurai_label"): + return False + + left_cid = left_attrs.get("canonical_entity_id") + right_cid = right_attrs.get("canonical_entity_id") + if left_cid and right_cid and str(left_cid) != str(right_cid): + return False + + left_text = str(left.get("text") or "").strip() + right_text = str(right.get("text") or "").strip() + return bool(left_text and right_text) + + +def _resolve_token(label: dict, render_context: dict[str, Any] | None) -> str: + """ + Resolves the logical replacement token for a label. + + Args: + label (dict): The label metadata being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + + Returns: + str: The logical token that should replace the label. + """ + boundary_token = label.get("_boundary_token") + if boundary_token: + return boundary_token + + token = resolve_render_token(label, render_context) + return token or "ENT" + + +def _apply_minimal_boundary_merge( + paragraphs: list[dict], + render_context: dict[str, Any] | None, +) -> None: + """ + Propagates a shared token across paragraph-boundary label pairs. + + Args: + paragraphs (list[dict]): The paragraph collection being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + """ + for left_par, right_par in zip(paragraphs, paragraphs[1:]): + left_doc = left_par.get("document") or "" + right_doc = right_par.get("document") or "" + left_labels = left_par.get("labels") or [] + right_labels = right_par.get("labels") or [] + + if not left_doc or not right_doc or not left_labels or not right_labels: + continue + + left_candidates = [ + label + for label in left_labels + if _label_end(label) >= max(0, len(left_doc) - 2) + ] + right_candidates = [label for label in right_labels if _label_start(label) <= 2] + + if not left_candidates or not right_candidates: + continue + + for left_label in left_candidates: + for right_label in right_candidates: + if not _same_boundary_candidate(left_label, right_label): + continue + + shared_token = _resolve_token(left_label, render_context) + if not shared_token: + shared_token = _resolve_token(right_label, render_context) + if shared_token: + left_label["_boundary_token"] = shared_token + right_label["_boundary_token"] = shared_token + break + + +def _build_layout_paragraphs(parsed_doc: Any) -> list[dict]: + """ + Builds normalized paragraph metadata from the parsed PDF layout. + + Args: + parsed_doc (Any): The parsed PDF layout document. + + Returns: + list[dict]: The normalized layout paragraphs extracted from the parsed document. + """ + chunks = parsed_doc.to_text( + page_chunks=True, + header=True, + footer=True, + show_progress=False, + ) + + paragraphs: list[dict] = [] + layout_index = 0 + for page_idx, (page, chunk) in enumerate(zip(parsed_doc.pages, chunks)): + page_text = chunk.get("text") or "" + page_boxes = chunk.get("page_boxes") or [] + + for box_meta in page_boxes: + box_idx = int(box_meta["index"]) + if box_idx >= len(page.boxes): + continue + + start, stop = box_meta.get("pos", (0, 0)) + box_text = normalize("NFKC", page_text[start:stop]).strip() + if not box_text: + continue + + box = page.boxes[box_idx] + line_entries: list[dict] = [] + line_text_chunks: list[str] = [] + line_cursor = 0 + + for line_idx, line in enumerate(box.textlines or []): + text = normalize("NFKC", _line_text(line)).strip() + if not text: + continue + + if line_text_chunks: + line_text_chunks.append("\n") + line_cursor += 1 + + line_start = line_cursor + line_text_chunks.append(text) + line_cursor += len(text) + line_end = line_cursor + style = _line_style(line) + spans_detail, strip_offset = _build_spans_detail(line) + + line_entries.append( + { + "page_index": page_idx, + "box_index": box_idx, + "line_index": line_idx, + "bbox": _rect_tuple(line["bbox"]), + "font_size": _font_size(line, float(style.get("size") or 10.0)), + "start": line_start, + "end": line_end, + "text": text, + "style": style, + "spans_detail": spans_detail, + "strip_offset": strip_offset, + } + ) + + line_text = "".join(line_text_chunks) + if not line_text: + continue + + paragraphs.append( + { + "plain_text": box_text, + "metadata": { + "layout_index": layout_index, + "page_index": page_idx, + "page_number": page.page_number, + "box_index": box_idx, + "boxclass": box.boxclass, + "box_bbox": ( + float(box.x0), + float(box.y0), + float(box.x1), + float(box.y1), + ), + "line_text": line_text, + "lines": line_entries, + }, + } + ) + layout_index += 1 + + return paragraphs + + +def _match_predictions_to_layout( + layout_paragraphs: list[dict], + preds: list[dict], +) -> list[dict]: + """ + Matches model predictions to the closest layout paragraphs. + + Args: + layout_paragraphs (list[dict]): The `layout_paragraphs` value used by this helper. + preds (list[dict]): The predictions to apply to the document. + + Returns: + list[dict]: The predictions annotated with their matched layout metadata. + """ + if not layout_paragraphs or not preds: + return [] + + available_indices = list(range(len(layout_paragraphs))) + all_indices = list(range(len(layout_paragraphs))) + matched: list[dict] = [] + + normalized_layout_texts = [ + normalize("NFKC", paragraph["plain_text"]).strip() + for paragraph in layout_paragraphs + ] + + for pred_idx, pred in enumerate(preds): + pred_text = normalize("NFKC", str(pred.get("document") or "")).strip() + if not pred_text: + continue + + candidate_pool = available_indices if available_indices else all_indices + exact_idx = next( + ( + idx + for idx in candidate_pool + if normalized_layout_texts[idx] == pred_text + ), + None, + ) + + if exact_idx is None: + exact_idx = min( + candidate_pool, + key=lambda idx: cer(pred_text, normalized_layout_texts[idx]), + ) + + paragraph = deepcopy(layout_paragraphs[exact_idx]) + paragraph["document"] = pred.get("document") or "" + paragraph["labels"] = pred.get("labels") or [] + paragraph["pred_index"] = pred_idx + matched.append(paragraph) + + if exact_idx in available_indices: + available_indices.remove(exact_idx) + + matched.sort(key=lambda paragraph: paragraph["metadata"]["layout_index"]) + return matched + + +def _pick_rect_group_for_segment( + page: pymupdf.Page, + line: dict, + text: str, + line_x_cursor: dict[tuple[int, int, int], float], +) -> pymupdf.Rect: + """ + Chooses the best rectangle group for a text segment on the page. + + Args: + page (pymupdf.Page): The PDF page being processed. + line (dict): The parsed line metadata being processed. + text (str): The text value being normalized or searched. + line_x_cursor (dict[tuple[int, int, int], float]): The per-line cursor used to keep page searches stable. + + Returns: + pymupdf.Rect | None: The chosen rectangle group for the segment, if found. + """ + clip = pymupdf.Rect(line["bbox"]) + rects = [rect for rect in page.search_for(text, clip=clip) if rect.intersects(clip)] + if not rects: + return clip + + max_gap = min( + max(clip.height * PDF_TAG_RECT_GAP_FACTOR, PDF_TAG_RECT_GAP_MIN), + PDF_TAG_RECT_GAP_MAX, + ) + grouped_rects = _group_adjacent_rects(rects, max_gap=max_gap) + + line_key = (line["page_index"], line["box_index"], line["line_index"]) + min_x = line_x_cursor.get(line_key, clip.x0 - 1) + + for rect in grouped_rects: + if rect.x0 >= min_x - 0.5: + line_x_cursor[line_key] = rect.x1 + return rect + + chosen = grouped_rects[0] + line_x_cursor[line_key] = chosen.x1 + return chosen + + +def _normalize_line_chars(spans: list[dict]) -> list[dict[str, Any]]: + """ + Normalizes per-character span data into searchable character entries. + + Args: + spans (list[dict]): The span collection to normalize into character entries. + + Returns: + list[dict[str, Any]]: The normalized character entries for the line. + """ + chars: list[dict[str, Any]] = [] + for span in spans: + for char in span.get("chars") or []: + norm_text = normalize("NFKC", str(char.get("c") or "")) + if not norm_text: + continue + bbox = pymupdf.Rect(char["bbox"]) + for norm_char in norm_text: + chars.append({"char": norm_char, "bbox": bbox}) + return chars + + +def _line_chars_from_page(page: pymupdf.Page, line: dict) -> list[dict[str, Any]]: + """ + Extracts character-level geometry for a parsed line from the page text. + + Args: + page (pymupdf.Page): The PDF page being processed. + line (dict): The parsed line metadata being processed. + + Returns: + list[dict[str, Any]]: The character entries extracted from the page. + """ + clip = pymupdf.Rect(line["bbox"]) + raw = page.get_text("rawdict", clip=clip) + target_text = normalize("NFKC", str(line.get("text") or "")).strip() + + best_chars: list[dict[str, Any]] = [] + best_score: tuple[float, float, float] | None = None + + for block in raw.get("blocks") or []: + if block.get("type", 0) != 0: + continue + for raw_line in block.get("lines") or []: + chars = _normalize_line_chars(raw_line.get("spans") or []) + if not chars: + continue + + candidate_rect = pymupdf.Rect(raw_line["bbox"]) + candidate_text = "".join(entry["char"] for entry in chars).strip() + overlap = ( + _rect_vertical_overlap(candidate_rect, clip) + if candidate_rect.intersects(clip) + else 0.0 + ) + text_score = 0.0 + if target_text or candidate_text: + text_score = ( + 0.0 + if target_text == candidate_text + else cer(target_text, candidate_text) + ) + bbox_score = ( + abs(candidate_rect.x0 - clip.x0) + + abs(candidate_rect.y0 - clip.y0) + + abs(candidate_rect.x1 - clip.x1) + + abs(candidate_rect.y1 - clip.y1) + ) / 100.0 + score = (1.0 - overlap, text_score, bbox_score) + if best_score is None or score < best_score: + best_score = score + best_chars = chars + + return best_chars + + +def _line_chars_text(chars: list[dict[str, Any]]) -> str: + """ + Builds the searchable text for a character entry list. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + + Returns: + str: The concatenated character text. + """ + return "".join(str(entry.get("char") or "") for entry in chars) + + +def _find_line_char_span( + chars: list[dict[str, Any]], + text: str, + *, + start: int = 0, + raw_text: str | None = None, +) -> tuple[int, int] | None: + """ + Finds the character span for a text fragment inside a line. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + text (str): The text value being normalized or searched. + start (int, optional): The preferred start offset for the search. Defaults to 0. + raw_text (str | None, optional): The raw line text used as a fallback search surface. Defaults to None. + + Returns: + tuple[int, int] | None: The start and end character offsets, if found. + """ + if not chars or not text: + return None + + haystack = raw_text if raw_text is not None else _line_chars_text(chars) + pattern = _build_flexible_pattern(text) + + def _search(offset: int) -> tuple[int, int] | None: + """ + Searches for the candidate span from the provided offset. + + Args: + offset (int): The search offset used by the nested helper. + + Returns: + tuple[int, int] | None: The matching span for the current offset, if found. + """ + exact_idx = haystack.find(text, offset) + flexible_span = None + if pattern: + match = re.search(pattern, haystack[offset:]) + if match is not None: + flexible_span = (offset + match.start(), offset + match.end()) + + if exact_idx < 0: + return flexible_span + exact_span = (exact_idx, exact_idx + len(text)) + if flexible_span is None: + return exact_span + return min(exact_span, flexible_span, key=lambda span: span[0]) + + span = _search(start) + if span is None and start > 0: + span = _search(0) + return span + + +def _rect_from_char_slice( + chars: list[dict[str, Any]], + start: int, + end: int, +) -> pymupdf.Rect | None: + """ + Builds a rectangle covering the requested character slice. + + Args: + chars (list[dict[str, Any]]): The character entry list being processed. + start (int): The preferred start offset for the search. + end (int): The `end` value used by this helper. + + Returns: + pymupdf.Rect | None: The rectangle covering the requested character slice. + """ + if not chars: + return None + + slice_start = max(int(start), 0) + slice_end = min(int(end), len(chars)) + if slice_end <= slice_start: + return None + + segment = chars[slice_start:slice_end] + if not segment: + return None + + boxes = [entry["bbox"] for entry in segment if str(entry["char"]).strip()] + if not boxes: + boxes = [entry["bbox"] for entry in segment] + if not boxes: + return None + + rect = pymupdf.Rect(boxes[0]) + for bbox in boxes[1:]: + rect.include_rect(bbox) + return rect diff --git a/aymurai/text/anonymization/pdf/ops.py b/aymurai/text/anonymization/pdf/ops.py new file mode 100644 index 00000000..fb5a324f --- /dev/null +++ b/aymurai/text/anonymization/pdf/ops.py @@ -0,0 +1,967 @@ +from __future__ import annotations + +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.text.anonymization.alignment import ( + _label_replacement_start as _label_start, +) +from aymurai.text.anonymization.alignment import ( + _label_replacement_text as _label_surface_text, +) +from aymurai.text.anonymization.pdf.common import ( + PDF_TAG_RECT_GAP_MAX, + PDF_TAG_RECT_INSET, + PDF_TAG_RECT_X_PADDING, + PDF_TAG_RECT_Y_PADDING, + _base14_fontname_for_style, + _default_style, + _entity_style_from_spans, + _find_flexible, + _fit_display_token, + _get_base14_font, + _group_adjacent_rects, + _rect_vertical_overlap, +) +from aymurai.text.anonymization.pdf.layout import ( + _find_line_char_span, + _line_chars_from_page, + _line_chars_text, + _pick_rect_group_for_segment, + _rect_from_char_slice, + _resolve_token, +) +from aymurai.text.anonymization.pdf.widgets import ( + _apply_widget_ops, + _entity_overlaps_widget, + _page_widget_infos, + _prepare_signature_widget_ops, +) + +logger = get_logger(__name__) + +_IMAGE_OVERLAP_THRESHOLD = 0.3 + + +def _padded_rect(rect: pymupdf.Rect, clip: pymupdf.Rect) -> pymupdf.Rect: + """ + Pads a rectangle within the provided clipping bounds. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + + Returns: + pymupdf.Rect: The padded rectangle clipped to the provided bounds. + """ + padded = pymupdf.Rect(rect) + padded.x0 = max(clip.x0, padded.x0 - PDF_TAG_RECT_X_PADDING) + padded.y0 = max(clip.y0, padded.y0 - PDF_TAG_RECT_Y_PADDING) + padded.x1 = min(clip.x1, padded.x1 + PDF_TAG_RECT_X_PADDING) + padded.y1 = min(clip.y1, padded.y1 + PDF_TAG_RECT_Y_PADDING) + return padded + + +def _render_rect(rect: pymupdf.Rect) -> pymupdf.Rect: + """ + Builds the token rendering rectangle from the padded canvas rectangle. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + + Returns: + pymupdf.Rect: The rectangle used to render the replacement token. + """ + render_rect = pymupdf.Rect(rect) + inset = min(PDF_TAG_RECT_INSET, max(render_rect.height * 0.1, 0.0)) + render_rect.x0 += inset + render_rect.x1 -= inset + if render_rect.x1 <= render_rect.x0: + render_rect = pymupdf.Rect(rect) + return render_rect + + +def _text_redact_rect(rect: pymupdf.Rect) -> pymupdf.Rect: + """ + Builds the redaction rectangle used to remove original text. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + + Returns: + pymupdf.Rect: The rectangle used for text redaction. + """ + redact_rect = pymupdf.Rect(rect) + edge_inset = min(0.25, max(redact_rect.width * 0.01, 0.05)) + if redact_rect.width > (2 * edge_inset): + redact_rect.x0 += edge_inset + redact_rect.x1 -= edge_inset + return redact_rect + + +def _build_page_op( + rect: pymupdf.Rect, + line: dict | None, + token: str, + is_image: bool = False, + entity_style: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Builds the rendering operation metadata for a matched page segment. + + Args: + rect (pymupdf.Rect): The rectangle used by the helper. + line (dict | None): The parsed line metadata being processed. + token (str): The logical replacement token being processed. + is_image (bool, optional): Whether the operation is intended for image-backed content. Defaults to False. + entity_style (dict[str, Any] | None, optional): The resolved style dictionary for the entity text. Defaults to None. + + Returns: + dict[str, Any]: The rendering operation metadata for the segment. + """ + line_clip = pymupdf.Rect(line["bbox"]) if line else pymupdf.Rect(rect) + canvas_rect = _padded_rect(rect, line_clip) + render_rect = _render_rect(canvas_rect) + style = entity_style or (line or {}).get("style") or _default_style() + base_font_size = float((line or {}).get("font_size") or style.get("size") or 10.0) + + # Always use Base-14 fonts: they carry correct bold/italic weight and + # contain all glyphs needed for tags (<, >, _, digits, letters). + # Subset font buffers extracted from the PDF lack many of these glyphs. + fontname = _base14_fontname_for_style(style) + font_obj = _get_base14_font(style) + + display_token, fitted_size = _fit_display_token( + token, + render_rect, + fontname, + base_font_size, + font_obj=font_obj, + ) + + if not display_token or fitted_size is None: + logger.warning( + "Could not fit PDF token '%s' inside rect=%s", + token, + tuple(round(value, 2) for value in canvas_rect), + ) + + return { + "redact_rect": _text_redact_rect(rect), + "background_rect": canvas_rect, + "canvas_rect": canvas_rect, + "render_rect": render_rect, + "line_rect": line_clip, + "text": display_token, + "logical_token": token, + "fontname": fontname, + "fontsize": fitted_size, + "text_align": pymupdf.TEXT_ALIGN_LEFT, + "text_color": style.get("color") or (0.0, 0.0, 0.0), + "style": style, + } + + +def _image_rects_for_clip( + page: pymupdf.Page, + clip: pymupdf.Rect, +) -> list[pymupdf.Rect]: + """ + Collects image rectangles that overlap the given page region. + + Args: + page (pymupdf.Page): The PDF page being processed. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + + Returns: + list[pymupdf.Rect]: The image rectangles that overlap the clip region. + """ + rects: list[pymupdf.Rect] = [] + for img_info in page.get_image_info(): + bbox = img_info.get("bbox") + if bbox is None: + continue + img_rect = pymupdf.Rect(bbox) + if img_rect.intersects(clip) and img_rect.get_area() > 0: + rects.append(img_rect) + return rects + + +def _squared_distance_between_rect_centers( + left: pymupdf.Rect, + right: pymupdf.Rect, +) -> float: + """ + Computes the squared distance between two rectangle centers. + + Args: + left (pymupdf.Rect): The first rectangle. + right (pymupdf.Rect): The second rectangle. + + Returns: + float: The squared distance between rectangle centers. + """ + left_center = ((left.x0 + left.x1) / 2.0, (left.y0 + left.y1) / 2.0) + right_center = ((right.x0 + right.x1) / 2.0, (right.y0 + right.y1) / 2.0) + return (left_center[0] - right_center[0]) ** 2 + ( + left_center[1] - right_center[1] + ) ** 2 + + +def _refine_signature_text_rect( + page: pymupdf.Page, + entity_text: str, + widget_rect: pymupdf.Rect, + current_rect: pymupdf.Rect, +) -> pymupdf.Rect: + """ + Finds a tighter text rectangle for signer names inside signature widgets. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The entity text being mapped. + widget_rect (pymupdf.Rect): The signature widget rectangle. + current_rect (pymupdf.Rect): The currently resolved entity rectangle. + + Returns: + pymupdf.Rect: The refined rectangle when available, otherwise current_rect. + """ + widget_clip = pymupdf.Rect(widget_rect) + hits = [ + pymupdf.Rect(hit) + for hit in page.search_for(entity_text, clip=widget_clip) + if pymupdf.Rect(hit).intersects(widget_clip) + ] + if not hits: + return pymupdf.Rect(current_rect) + + target = pymupdf.Rect(current_rect) + intersecting_hits = [hit for hit in hits if hit.intersects(target)] + candidates = intersecting_hits or hits + return pymupdf.Rect( + min( + candidates, + key=lambda hit: _squared_distance_between_rect_centers(hit, target), + ) + ) + + +def _build_signature_page_op( + page: pymupdf.Page, + entity_text: str, + widget_info: dict[str, Any], + current_rect: pymupdf.Rect, + token: str, + entity_style: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Builds a signature-specific operation scoped to the sensitive text only. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The sensitive text being replaced. + widget_info (dict[str, Any]): The signature widget metadata. + current_rect (pymupdf.Rect): The initially resolved text rectangle. + token (str): The logical replacement token. + entity_style (dict[str, Any] | None): The text style to render with. + + Returns: + dict[str, Any]: The signature replacement operation. + """ + refined_rect = _refine_signature_text_rect( + page, + entity_text, + widget_info["rect"], + current_rect, + ) + op = _build_page_op( + refined_rect, + None, + token, + entity_style=entity_style or widget_info.get("style") or None, + ) + op["widget_xref"] = widget_info["xref"] + op["widget_rect"] = widget_info["rect"] + return op + + +def _entity_overlaps_image( + page: pymupdf.Page, + entity_rect: pymupdf.Rect, + image_rects: list[pymupdf.Rect], +) -> pymupdf.Rect | None: + """ + Checks whether an entity rectangle overlaps a detected image. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_rect (pymupdf.Rect): The rectangle representing the entity on the page. + image_rects (list[pymupdf.Rect]): The image rectangles available for overlap checks. + + Returns: + pymupdf.Rect | None: The overlapping image rectangle, if one exists. + """ + for img_rect in image_rects: + overlap = _rect_vertical_overlap(entity_rect, img_rect) + if overlap >= _IMAGE_OVERLAP_THRESHOLD and entity_rect.intersects(img_rect): + return img_rect + return None + + +def _collect_page_redactions( + doc: pymupdf.Document, + paragraphs: list[dict], + render_context: dict[str, Any] | None, +) -> dict[int, list[dict]]: + """ + Collects text, widget, and signature redaction operations for a document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + paragraphs (list[dict]): The paragraph collection being processed. + render_context (dict[str, Any] | None): The rendering context used to resolve replacement tokens. + + Returns: + tuple[dict[int, list[dict]], dict[int, list[dict]], dict[int, list[dict]]]: The page, text-widget, and signature-widget operations. + """ + page_ops: dict[int, list[dict]] = {} + widget_ops: dict[int, list[dict]] = {} + signature_widget_ops: dict[int, list[dict]] = {} + line_x_cursor: dict[tuple[int, int, int], float] = {} + line_char_cache: dict[tuple[int, int, int], list[dict[str, Any]]] = {} + line_char_text_cache: dict[tuple[int, int, int], str] = {} + line_char_cursor: dict[tuple[int, int, int], int] = {} + + # Pre-compute image rects and widgets per page + page_image_rects: dict[int, list[pymupdf.Rect]] = {} + page_widgets: dict[int, list[dict[str, Any]]] = {} + + for paragraph in paragraphs: + metadata = paragraph.get("metadata") or {} + lines = metadata.get("lines") or [] + if not lines: + continue + + page_index = int(metadata["page_index"]) + page = doc[page_index] + line_text = metadata.get("line_text") or "" + box_clip = pymupdf.Rect(metadata.get("box_bbox") or page.rect) + document = paragraph.get("document") or "" + labels = sorted(paragraph.get("labels") or [], key=_label_start) + search_cursor = 0 + + # Lazy-load image rects and widget infos for this page + if page_index not in page_image_rects: + page_image_rects[page_index] = _image_rects_for_clip(page, page.rect) + if page_index not in page_widgets: + page_widgets[page_index] = _page_widget_infos(page) + + for label in labels: + entity_text = _label_surface_text(label, document).strip() + if not entity_text: + continue + + token = _resolve_token(label, render_context) + + span = _find_flexible(line_text, entity_text, start=search_cursor) + if span is None: + span = _find_flexible(line_text, entity_text, start=0) + if span is None: + # -- Fallback: direct page search -- + fallback_rects = [ + rect + for rect in page.search_for(entity_text, clip=box_clip) + if rect.intersects(box_clip) + ] + + # Check if this is a widget-backed entity before falling back to images + if fallback_rects: + fallback_widget = _entity_overlaps_widget( + fallback_rects[0], + page_widgets[page_index], + ) + if fallback_widget is not None: + if ( + fallback_widget["field_type"] + == pymupdf.PDF_WIDGET_TYPE_TEXT + ): + widget_ops.setdefault(page_index, []).append( + { + "widget_xref": fallback_widget["xref"], + "field_name": fallback_widget["field_name"], + "widget_info": fallback_widget, + "entity_text": entity_text, + "logical_token": token, + } + ) + continue + if ( + fallback_widget["field_type"] + == pymupdf.PDF_WIDGET_TYPE_SIGNATURE + ): + op = _build_signature_page_op( + page, + entity_text, + fallback_widget, + fallback_rects[0], + token, + entity_style=fallback_widget.get("style") or None, + ) + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + # Check if this is an image-based entity + if not fallback_rects: + img_match = _try_image_entity( + page, + entity_text, + box_clip, + page_image_rects[page_index], + ) + if img_match is not None: + op = _build_page_op( + img_match, + lines[0] if lines else None, + token, + is_image=True, + ) + op["image_rect"] = img_match + page_ops.setdefault(page_index, []).append(op) + continue + + if fallback_rects: + grouped_rects = _group_adjacent_rects( + fallback_rects, max_gap=PDF_TAG_RECT_GAP_MAX + ) + fallback_line = lines[0] if lines else None + + # Check if any of these rects overlap an image + for rect in grouped_rects: + img_rect = _entity_overlaps_image( + page, + rect, + page_image_rects[page_index], + ) + op = _build_page_op( + rect, + fallback_line, + token, + is_image=(img_rect is not None), + ) + if img_rect is not None: + op["image_rect"] = img_rect + page_ops.setdefault(page_index, []).append(op) + continue + + logger.warning( + "Could not map label '%s' on page=%s box=%s", + entity_text, + metadata.get("page_number"), + metadata.get("box_index"), + ) + continue + + search_cursor = span[1] + + # Collect line segments this entity spans + segments: list[ + tuple[ + dict, + str, + pymupdf.Rect, + pymupdf.Rect | None, + dict, + dict[str, Any] | None, + ] + ] = [] + for line in lines: + overlap_start = max(span[0], line["start"]) + overlap_end = min(span[1], line["end"]) + if overlap_end <= overlap_start: + continue + + segment_text = line_text[overlap_start:overlap_end].strip() + if not segment_text: + continue + + line_key = ( + line["page_index"], + line["box_index"], + line["line_index"], + ) + line_chars = line_char_cache.get(line_key) + if line_chars is None: + line_chars = _line_chars_from_page(page, line) + line_char_cache[line_key] = line_chars + + line_char_text = line_char_text_cache.get(line_key) + if line_char_text is None: + line_char_text = _line_chars_text(line_chars) + line_char_text_cache[line_key] = line_char_text + + raw_span = _find_line_char_span( + line_chars, + segment_text, + start=line_char_cursor.get(line_key, 0), + raw_text=line_char_text, + ) + rect = None + if raw_span is not None: + line_char_cursor[line_key] = raw_span[1] + rect = _rect_from_char_slice(line_chars, raw_span[0], raw_span[1]) + + if rect is None: + raw_start = ( + overlap_start - line["start"] + int(line.get("strip_offset", 0)) + ) + raw_end = ( + overlap_end - line["start"] + int(line.get("strip_offset", 0)) + ) + rect = _rect_from_char_slice(line_chars, raw_start, raw_end) + if rect is None: + rect = _pick_rect_group_for_segment( + page, + line, + segment_text, + line_x_cursor, + ) + + widget_info = _entity_overlaps_widget( + rect, + page_widgets[page_index], + ) + + # Check for image overlap + img_rect = _entity_overlaps_image( + page, + rect, + page_image_rects[page_index], + ) + + # Determine entity-specific style from the span that + # actually contains this text (not the line's dominant style) + offset_in_line = overlap_start - line["start"] + ent_style = _entity_style_from_spans(line, offset_in_line) + + segments.append( + (line, segment_text, rect, img_rect, ent_style, widget_info) + ) + + if not segments: + continue + + if len(segments) == 1: + # Single-line entity: route widget-backed content through the widget path. + line, _seg_text, rect, img_rect, ent_style, widget_info = segments[0] + if widget_info is not None: + if widget_info["field_type"] == pymupdf.PDF_WIDGET_TYPE_TEXT: + widget_ops.setdefault(page_index, []).append( + { + "widget_xref": widget_info["xref"], + "field_name": widget_info["field_name"], + "widget_info": widget_info, + "entity_text": entity_text, + "logical_token": token, + } + ) + continue + if widget_info["field_type"] == pymupdf.PDF_WIDGET_TYPE_SIGNATURE: + op = _build_signature_page_op( + page, + entity_text, + widget_info, + rect, + token, + entity_style=ent_style, + ) + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + op = _build_page_op( + rect, + line, + token, + is_image=(img_rect is not None), + entity_style=ent_style, + ) + if img_rect is not None: + op["image_rect"] = img_rect + page_ops.setdefault(page_index, []).append(op) + else: + # Multi-line entity: write the token on the widest segment only; blank the others. + widest_idx = max( + range(len(segments)), + key=lambda i: segments[i][2].width, + ) + any_image = any(seg[3] is not None for seg in segments) + shared_image_rect = next( + (seg[3] for seg in segments if seg[3] is not None), + None, + ) + + signature_widget = None + if all(seg[5] is not None for seg in segments): + widget_xrefs = {int(seg[5]["xref"]) for seg in segments} + widget_types = {int(seg[5]["field_type"]) for seg in segments} + if len(widget_xrefs) == 1 and widget_types == { + pymupdf.PDF_WIDGET_TYPE_SIGNATURE + }: + signature_widget = segments[0][5] + + for seg_idx, ( + seg_line, + seg_text, + seg_rect, + seg_img, + seg_style, + seg_widget, + ) in enumerate(segments): + if signature_widget is not None: + op = _build_signature_page_op( + page, + seg_text, + signature_widget, + seg_rect, + token, + entity_style=seg_style, + ) + if seg_idx != widest_idx: + op["text"] = None + op["fontsize"] = None + signature_widget_ops.setdefault(page_index, []).append(op) + continue + + if seg_idx == widest_idx: + op = _build_page_op( + seg_rect, + seg_line, + token, + is_image=any_image, + entity_style=seg_style, + ) + if shared_image_rect is not None: + op["image_rect"] = shared_image_rect + else: + op = _build_page_op( + seg_rect, + seg_line, + token, + is_image=(seg_img is not None), + entity_style=seg_style, + ) + op["text"] = None + op["fontsize"] = None + if seg_img is not None: + op["image_rect"] = seg_img + + page_ops.setdefault(page_index, []).append(op) + + return page_ops, widget_ops, signature_widget_ops + + +def _try_image_entity( + page: pymupdf.Page, + entity_text: str, + clip: pymupdf.Rect, + image_rects: list[pymupdf.Rect], +) -> pymupdf.Rect | None: + """ + Finds the best image rectangle for an entity when text search fails. + + Args: + page (pymupdf.Page): The PDF page being processed. + entity_text (str): The entity text being mapped. + clip (pymupdf.Rect): The clipping rectangle to constrain the operation. + image_rects (list[pymupdf.Rect]): The image rectangles available for overlap checks. + + Returns: + pymupdf.Rect | None: The best image rectangle for the entity, if found. + """ + if not image_rects: + return None + + # Try unclipped text search — the entity might be rendered as real text + # on top of (or near) an image. + text_hits = page.search_for(entity_text) + if text_hits: + for hit_rect in text_hits: + for img_rect in image_rects: + if hit_rect.intersects(img_rect): + return img_rect + + # Fallback: pick the image whose intersection with *clip* is largest + best: pymupdf.Rect | None = None + best_area = 0.0 + for img_rect in image_rects: + if not img_rect.intersects(clip) or img_rect.get_area() <= 0: + continue + intersection = img_rect & clip + area = intersection.get_area() + if area > best_area: + best_area = area + best = img_rect + + return best + + +def _render_text_op(page: pymupdf.Page, op: dict) -> None: + """ + Renders a single anonymization token back onto a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + op (dict): The operation dictionary being processed. + """ + canvas = pymupdf.Rect(op.get("background_rect") or op["canvas_rect"]) + if not op.get("skip_background_fill"): + page.draw_rect( + canvas, + color=(1, 1, 1), + fill=(1, 1, 1), + width=0, + overlay=True, + ) + + if not op.get("text") or not op.get("fontsize"): + return + + render = op["render_rect"] + line_rect = pymupdf.Rect(op.get("line_rect") or render) + style = op.get("style") or {} + base14_name = _base14_fontname_for_style(style) + font_obj = _get_base14_font(style) + + fontsize = float(op["fontsize"]) + descender = float(style.get("descender") or -0.2) + baseline_y = line_rect.y1 + (descender * fontsize) + baseline_y = min( + max(baseline_y, line_rect.y0 + (fontsize * 0.65)), + line_rect.y1 - 0.1, + ) + + text_width = font_obj.text_length(op["text"], fontsize=fontsize) + x_start = render.x0 + max((render.width - text_width) / 2.0, 0.0) + + try: + page.insert_text( + (x_start, baseline_y), + op["text"], + fontname=base14_name, + fontsize=fontsize, + color=op["text_color"], + overlay=True, + ) + return + except Exception as exc: + logger.debug("insert_text failed for '%s': %s", op["text"], exc) + + try: + tw = pymupdf.TextWriter(page.rect, color=op["text_color"]) + tw.fill_textbox( + render, + op["text"], + font=font_obj, + fontsize=fontsize, + align=op.get("text_align", pymupdf.TEXT_ALIGN_CENTER), + ) + tw.write_text(page, overlay=True) + return + except Exception as exc: + logger.debug("TextWriter failed for '%s': %s", op["text"], exc) + + try: + page.insert_textbox( + render, + op["text"], + fontname=base14_name, + fontsize=fontsize, + color=op["text_color"], + align=op.get("text_align", pymupdf.TEXT_ALIGN_CENTER), + overlay=True, + ) + except Exception as exc: + logger.warning( + "All text insertion methods failed for '%s': %s", + op["text"], + exc, + ) + + +def _page_asset_rect(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Resolves the asset rectangle associated with a page operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The asset rectangle associated with the operation, if any. + """ + asset_rect = op.get("asset_rect") or op.get("image_rect") + if asset_rect is None: + return None + return pymupdf.Rect(asset_rect) + + +def _partition_page_ops( + page_ops: dict[int, list[dict]], +) -> tuple[dict[int, list[dict]], dict[int, list[dict]]]: + """ + Splits page operations into text-only and asset-backed groups. + + Args: + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + + Returns: + tuple[dict[int, list[dict]], dict[int, list[dict]]]: The text-only and asset-backed operations. + """ + text_ops: dict[int, list[dict]] = {} + asset_ops: dict[int, list[dict]] = {} + + for page_idx, ops in page_ops.items(): + for op in ops: + if _page_asset_rect(op) is None: + text_ops.setdefault(page_idx, []).append(op) + else: + asset_ops.setdefault(page_idx, []).append(op) + + return text_ops, asset_ops + + +def _apply_text_redactions( + doc: pymupdf.Document, + text_page_ops: dict[int, list[dict]], +) -> None: + """ + Applies text-only redactions and re-renders replacement tokens. + + Args: + doc (pymupdf.Document): The PDF document being processed. + text_page_ops (dict[int, list[dict]]): The text-only page operations grouped by page index. + """ + for page_idx, ops in text_page_ops.items(): + if not ops: + continue + + page = doc[page_idx] + for op in ops: + page.add_redact_annot( + op["redact_rect"], + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_NONE, + graphics=pymupdf.PDF_REDACT_LINE_ART_NONE, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_asset_redactions( + doc: pymupdf.Document, + asset_page_ops: dict[int, list[dict]], +) -> None: + """ + Applies asset-backed redactions and re-renders replacement tokens. + + Args: + doc (pymupdf.Document): The PDF document being processed. + asset_page_ops (dict[int, list[dict]]): The asset-backed page operations grouped by page index. + """ + for page_idx, ops in asset_page_ops.items(): + if not ops: + continue + + page = doc[page_idx] + graphics_mode = pymupdf.PDF_REDACT_LINE_ART_NONE + + for op in ops: + asset_rect = _page_asset_rect(op) + if asset_rect is None: + continue + + page.add_redact_annot( + asset_rect, + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + graphics_mode = max( + graphics_mode, + int(op.get("graphics_mode") or pymupdf.PDF_REDACT_LINE_ART_NONE), + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_REMOVE, + graphics=graphics_mode, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_signature_redactions( + doc: pymupdf.Document, + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies signer-name redactions without removing the full signature appearance. + + Args: + doc (pymupdf.Document): The PDF document being processed. + signature_widget_ops (dict[int, list[dict]]): The signature operations grouped by page index. + """ + for page_idx, ops in signature_widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + for op in ops: + page.add_redact_annot( + op["redact_rect"], + text=None, + fill=(1, 1, 1), + cross_out=False, + ) + + page.apply_redactions( + images=pymupdf.PDF_REDACT_IMAGE_PIXELS, + graphics=pymupdf.PDF_REDACT_LINE_ART_NONE, + text=pymupdf.PDF_REDACT_TEXT_REMOVE, + ) + + for op in ops: + _render_text_op(page, op) + + +def _apply_redactions( + doc: pymupdf.Document, + page_ops: dict[int, list[dict]], + widget_ops: dict[int, list[dict]], + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies all collected PDF redactions in the correct order. + + Args: + doc (pymupdf.Document): The PDF document being processed. + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + """ + _apply_widget_ops(doc, widget_ops) + _prepare_signature_widget_ops(doc, signature_widget_ops) + + text_page_ops, asset_page_ops = _partition_page_ops(page_ops) + + _apply_text_redactions(doc, text_page_ops) + _apply_asset_redactions(doc, asset_page_ops) + _apply_signature_redactions(doc, signature_widget_ops) diff --git a/aymurai/text/anonymization/pdf/sanitize.py b/aymurai/text/anonymization/pdf/sanitize.py new file mode 100644 index 00000000..ab1bf344 --- /dev/null +++ b/aymurai/text/anonymization/pdf/sanitize.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) + + +def _pdf_metadata_mod_date() -> str: + """ + Builds the PDF metadata modification timestamp in UTC. + + Returns: + str: The PDF-formatted UTC modification timestamp. + """ + timestamp = datetime.now(timezone.utc) + return timestamp.strftime("D:%Y%m%d%H%M%S+00'00'") + + +def _append_cleanup_rect( + cleanup_rects: dict[int, list[pymupdf.Rect]], + page_idx: int, + rect: pymupdf.Rect | tuple[float, float, float, float] | None, +) -> None: + """ + Appends a cleanup rectangle for later document sanitization. + + Args: + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + page_idx (int): The page index associated with the operation. + rect (pymupdf.Rect | tuple[float, float, float, float] | None): The rectangle used by the helper. + """ + if rect is None: + return + + cleanup_rect = pymupdf.Rect(rect) + if cleanup_rect.get_area() <= 0: + return + cleanup_rects.setdefault(page_idx, []).append(cleanup_rect) + + +def _cleanup_rect_for_page_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a standard page operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the page operation, if available. + """ + if op.get("image_rect") is not None: + cleanup_rect = pymupdf.Rect(op["image_rect"]) + redact_rect = op.get("redact_rect") + if redact_rect is not None: + cleanup_rect.include_rect(pymupdf.Rect(redact_rect)) + return cleanup_rect + + cleanup_source = ( + op.get("redact_rect") or op.get("background_rect") or op.get("canvas_rect") + ) + if cleanup_source is None: + return None + return pymupdf.Rect(cleanup_source) + + +def _cleanup_rect_for_widget_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a text widget operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the widget operation, if available. + """ + widget_info = op.get("widget_info") or {} + widget_rect = widget_info.get("rect") + if widget_rect is None: + return None + return pymupdf.Rect(widget_rect) + + +def _cleanup_rect_for_signature_widget_op(op: dict[str, Any]) -> pymupdf.Rect | None: + """ + Builds the cleanup rectangle for a signature widget operation. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + + Returns: + pymupdf.Rect | None: The cleanup rectangle for the signature widget operation, if available. + """ + cleanup_source = ( + op.get("redact_rect") or op.get("background_rect") or op.get("canvas_rect") + ) + if cleanup_source is None: + return None + return pymupdf.Rect(cleanup_source) + + +def _collect_link_cleanup_rects( + page_ops: dict[int, list[dict]], + widget_ops: dict[int, list[dict]], + signature_widget_ops: dict[int, list[dict]], +) -> dict[int, list[pymupdf.Rect]]: + """ + Collects cleanup rectangles used to prune overlapping links. + + Args: + page_ops (dict[int, list[dict]]): The collected page operations grouped by page index. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + + Returns: + dict[int, list[pymupdf.Rect]]: The cleanup rectangles grouped by page index. + """ + cleanup_rects: dict[int, list[pymupdf.Rect]] = {} + + for page_idx, ops in page_ops.items(): + for op in ops: + _append_cleanup_rect(cleanup_rects, page_idx, _cleanup_rect_for_page_op(op)) + + for page_idx, ops in widget_ops.items(): + for op in ops: + _append_cleanup_rect( + cleanup_rects, + page_idx, + _cleanup_rect_for_widget_op(op), + ) + + for page_idx, ops in signature_widget_ops.items(): + for op in ops: + _append_cleanup_rect( + cleanup_rects, + page_idx, + _cleanup_rect_for_signature_widget_op(op), + ) + + return cleanup_rects + + +def _remove_overlapping_page_links( + doc: pymupdf.Document, + cleanup_rects: dict[int, list[pymupdf.Rect]], +) -> None: + """ + Deletes page links that overlap anonymized regions. + + Args: + doc (pymupdf.Document): The PDF document being processed. + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + """ + for page_idx, page_rects in cleanup_rects.items(): + if not page_rects: + continue + + page = doc[page_idx] + for link in list(page.get_links()): + link_rect = link.get("from") + if link_rect is None: + continue + link_rect = pymupdf.Rect(link_rect) + if not any(link_rect.intersects(rect) for rect in page_rects): + continue + try: + page.delete_link(link) + except Exception as exc: + logger.warning( + "Failed to delete PDF link on page=%s rect=%s: %s", + page_idx, + tuple(round(value, 2) for value in link_rect), + exc, + ) + + +def _remove_remaining_annotations(doc: pymupdf.Document) -> None: + """ + Deletes residual page annotations after sanitization. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + for page_idx, page in enumerate(doc): + for annot in list(page.annots() or []): + try: + page.delete_annot(annot) + except Exception as exc: + logger.warning( + "Failed to delete residual PDF annotation on page=%s: %s", + page_idx, + exc, + ) + + +def _clear_standard_metadata(doc: pymupdf.Document) -> None: + """ + Clears the standard PDF metadata fields on a document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + doc.set_metadata( + { + "title": "", + "author": "", + "subject": "", + "keywords": "", + "creator": "", + "producer": "", + "creationDate": "", + "modDate": "", + "trapped": "", + } + ) + + +def _apply_aymurai_metadata(doc: pymupdf.Document) -> None: + """ + Applies the configured AymurAI tooling metadata fields to the PDF document. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + metadata = dict(doc.metadata or {}) + metadata.update( + { + "title": metadata.get("title") or "", + "author": "", + "subject": metadata.get("subject") or "", + "keywords": metadata.get("keywords") or "", + "creator": settings.ANONYMIZATION_METADATA_CREATOR, + "producer": settings.ANONYMIZATION_METADATA_PRODUCER, + "creationDate": metadata.get("creationDate") or "", + "modDate": _pdf_metadata_mod_date(), + "trapped": metadata.get("trapped") or "", + } + ) + doc.set_metadata(metadata) + + +def _sanitize_document( + doc: pymupdf.Document, + cleanup_rects: dict[int, list[pymupdf.Rect]], +) -> None: + """ + Sanitizes document-level PDF metadata, attachments, and annotations. + + Args: + doc (pymupdf.Document): The PDF document being processed. + cleanup_rects (dict[int, list[pymupdf.Rect]]): The cleanup rectangles grouped by page index. + """ + _remove_overlapping_page_links(doc, cleanup_rects) + doc.scrub( + metadata=True, + xml_metadata=True, + javascript=True, + attached_files=True, + embedded_files=True, + thumbnails=True, + reset_responses=True, + hidden_text=True, + clean_pages=True, + remove_links=False, + reset_fields=False, + redactions=False, + ) + _remove_remaining_annotations(doc) + _clear_standard_metadata(doc) + _apply_aymurai_metadata(doc) + + get_xml_metadata = getattr(doc, "get_xml_metadata", None) + del_xml_metadata = getattr(doc, "del_xml_metadata", None) + if callable(get_xml_metadata) and callable(del_xml_metadata): + try: + xml_metadata = get_xml_metadata() + except Exception as exc: + logger.warning("Failed to read PDF XML metadata after scrub: %s", exc) + else: + if xml_metadata: + try: + del_xml_metadata() + except Exception as exc: + logger.warning( + "Failed to delete residual PDF XML metadata: %s", + exc, + ) diff --git a/aymurai/text/anonymization/pdf/watermark.py b/aymurai/text/anonymization/pdf/watermark.py new file mode 100644 index 00000000..c15d9aef --- /dev/null +++ b/aymurai/text/anonymization/pdf/watermark.py @@ -0,0 +1,522 @@ +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.settings import settings + +logger = get_logger(__name__) + +WATERMARK_PREFIX_TEXT = "Documento anonimizado por " +WATERMARK_LINK_TEXT = "AymurAI" +WATERMARK_TEXT = f"{WATERMARK_PREFIX_TEXT}{WATERMARK_LINK_TEXT}" +WATERMARK_URL = "https://www.aymurai.info/" +WATERMARK_FONT_SIZE = 10.0 +WATERMARK_MARGIN_X = 24.0 +WATERMARK_BASELINE_MARGIN = 12.0 +WATERMARK_TOP_BASELINE = 22.0 +WATERMARK_RECT_PADDING_X = 4.0 +WATERMARK_RECT_PADDING_Y = 4.0 +WATERMARK_COLLISION_PADDING = 12.0 +WATERMARK_TEXT_COLOR = tuple(channel / 255 for channel in (192, 192, 192)) +WATERMARK_LINK_COLOR = tuple(channel / 255 for channel in (115, 190, 250)) + + +def _candidate_font_paths() -> tuple[list[Path], list[Path]]: + """ + Builds the ordered list of candidate font paths for the PDF watermark. + + Returns: + tuple[list[Path], list[Path]]: The regular and bold watermark font candidates. + """ + override_regular = ( + os.getenv("PDF_WATERMARK_FONT_REGULAR") or settings.PDF_WATERMARK_FONT_REGULAR + ) + override_bold = ( + os.getenv("PDF_WATERMARK_FONT_BOLD") or settings.PDF_WATERMARK_FONT_BOLD + ) + + regular_candidates: list[Path] = [] + bold_candidates: list[Path] = [] + + if override_regular: + regular_candidates.append(Path(override_regular).expanduser()) + if override_bold: + bold_candidates.append(Path(override_bold).expanduser()) + + resource_roots: list[Path] = [] + resources_base = Path(settings.RESOURCES_BASEPATH) + if resources_base.is_absolute(): + resource_roots.append(resources_base) + else: + resource_roots.append((Path("/workspace") / resources_base).resolve()) + resource_roots.append(resources_base) + + font_roots: list[Path] = [] + for root in resource_roots: + font_roots.extend([root / "fonts", root / "fonts" / "archivo"]) + + for root in font_roots: + regular_candidates.extend( + [ + root / "Archivo-Regular.ttf", + root / "Archivo-Regular.otf", + root / "Archivo[wdth,wght].ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + ] + ) + bold_candidates.extend( + [ + root / "Archivo-Bold.ttf", + root / "Archivo-Bold.otf", + root / "Archivo-BoldItalic.ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + root / "Archivo[wdth,wght].ttf", + ] + ) + + system_roots = [ + Path("/usr/share/fonts/truetype/archivo"), + Path("/usr/share/fonts/opentype/archivo"), + Path("/usr/local/share/fonts/archivo"), + Path.home() / ".local/share/fonts", + Path.home() / ".local/share/fonts/archivo", + ] + for root in system_roots: + regular_candidates.extend( + [ + root / "Archivo-Regular.ttf", + root / "Archivo-Regular.otf", + root / "Archivo[wdth,wght].ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + ] + ) + bold_candidates.extend( + [ + root / "Archivo-Bold.ttf", + root / "Archivo-Bold.otf", + root / "Archivo-BoldItalic.ttf", + root / "Archivo-VariableFont_wdth,wght.ttf", + root / "Archivo[wdth,wght].ttf", + ] + ) + + return regular_candidates, bold_candidates + + +def _first_existing_path(paths: list[Path]) -> str | None: + """ + Returns the first existing file path from the provided candidates. + + Args: + paths (list[Path]): The candidate paths to inspect. + + Returns: + str | None: The first existing file path, if one is found. + """ + seen: set[str] = set() + for path in paths: + expanded = path.expanduser() + resolved = str(expanded) + if resolved in seen: + continue + seen.add(resolved) + if expanded.exists() and expanded.is_file(): + return str(expanded) + return None + + +@lru_cache(maxsize=1) +def _watermark_font_paths() -> tuple[str | None, str | None]: + """ + Resolves the font paths used by the PDF watermark. + + Returns: + tuple[str | None, str | None]: The resolved regular and bold watermark font paths. + """ + regular_candidates, bold_candidates = _candidate_font_paths() + regular_path = _first_existing_path(regular_candidates) + bold_path = _first_existing_path(bold_candidates) + if regular_path is None and bold_path is not None: + regular_path = bold_path + if bold_path is None: + bold_path = regular_path + return regular_path, bold_path + + +@lru_cache(maxsize=1) +def _watermark_font_config() -> dict[str, Any]: + """ + Builds the font configuration used to render the PDF watermark. + + Returns: + dict[str, Any]: The watermark font configuration dictionary. + """ + regular_path, bold_path = _watermark_font_paths() + if regular_path: + try: + return { + "text_fontname": "archivo-watermark", + "text_fontfile": regular_path, + "text_font": pymupdf.Font(fontfile=regular_path), + "link_fontname": "archivo-watermark-bold", + "link_fontfile": bold_path or regular_path, + "link_font": pymupdf.Font(fontfile=bold_path or regular_path), + } + except Exception as exc: + logger.warning( + "Could not load Archivo font for PDF watermark, falling back to Base-14 fonts: %s", + exc, + ) + + return { + "text_fontname": "Helvetica", + "text_fontfile": None, + "text_font": pymupdf.Font("Helvetica"), + "link_fontname": "Helvetica-Bold", + "link_fontfile": None, + "link_font": pymupdf.Font("Helvetica-Bold"), + } + + +def _watermark_text_length( + text: str, + *, + font_obj: pymupdf.Font, + fontname: str, + fontsize: float, +) -> float: + """ + Measures the rendered width of watermark text. + + Args: + text (str): The text value being normalized or searched. + font_obj (pymupdf.Font): The font object used for measurement. + fontname (str): The font name to use for measurement or rendering. + fontsize (float): The font size used for measurement or rendering. + + Returns: + float: The rendered width of the watermark text. + """ + try: + return float(font_obj.text_length(text, fontsize=fontsize)) + except Exception: + return float( + pymupdf.get_text_length(text, fontname=fontname, fontsize=fontsize) + ) + + +def _insert_watermark_text( + page: pymupdf.Page, + point: tuple[float, float], + text: str, + *, + fontname: str, + fontsize: float, + color: tuple[float, float, float], + fontfile: str | None = None, +) -> None: + """ + Inserts watermark text onto a page using the resolved font settings. + + Args: + page (pymupdf.Page): The PDF page being processed. + point (tuple[float, float]): The insertion point on the page. + text (str): The text value being normalized or searched. + fontname (str): The font name to use for measurement or rendering. + fontsize (float): The font size used for measurement or rendering. + color (tuple[float, float, float]): The PDF RGB color used to render the text. + fontfile (str | None, optional): The optional font file path to embed for rendering. Defaults to None. + """ + kwargs: dict[str, Any] = { + "fontsize": fontsize, + "fontname": fontname, + "color": color, + "overlay": True, + } + if fontfile: + kwargs["fontfile"] = fontfile + page.insert_text(point, text, **kwargs) + + +def _expanded_rect(rect: pymupdf.Rect, padding: float) -> pymupdf.Rect: + """ + Expands a rectangle by a uniform padding in every direction. + + Args: + rect (pymupdf.Rect): The rectangle to expand. + padding (float): The amount of padding to apply on every side. + + Returns: + pymupdf.Rect: The expanded rectangle. + """ + return pymupdf.Rect( + rect.x0 - padding, + rect.y0 - padding, + rect.x1 + padding, + rect.y1 + padding, + ) + + +def _watermark_corner_order(page_index: int) -> list[str]: + """ + Builds the preferred watermark corner order for a page. + + Args: + page_index (int): The page index being processed. + + Returns: + list[str]: The ordered watermark corner candidates for the page. + """ + if page_index % 2 == 0: + return ["bottom-right", "bottom-left", "top-left", "top-right"] + return ["bottom-left", "top-left", "top-right", "bottom-right"] + + +def _watermark_layout_for_corner( + page: pymupdf.Page, + corner: str, + *, + prefix_width: float, + link_width: float, + total_width: float, +) -> dict[str, Any]: + """ + Builds the watermark geometry for a specific page corner. + + Args: + page (pymupdf.Page): The PDF page being processed. + corner (str): The corner identifier used to position the watermark. + prefix_width (float): The rendered width of the watermark prefix text. + link_width (float): The rendered width of the watermark link text. + total_width (float): The total rendered width of the watermark text. + + Returns: + dict[str, Any]: The watermark layout data for the corner. + """ + if corner.endswith("right"): + x_start = max( + WATERMARK_MARGIN_X, + page.rect.width - total_width - WATERMARK_MARGIN_X, + ) + else: + x_start = WATERMARK_MARGIN_X + + if corner.startswith("bottom"): + baseline_y = page.rect.height - WATERMARK_BASELINE_MARGIN + else: + baseline_y = WATERMARK_TOP_BASELINE + + link_x = x_start + prefix_width + text_top = baseline_y - WATERMARK_FONT_SIZE + banner_rect = pymupdf.Rect( + x_start - WATERMARK_RECT_PADDING_X, + text_top - WATERMARK_RECT_PADDING_Y, + x_start + total_width + WATERMARK_RECT_PADDING_X, + baseline_y + WATERMARK_RECT_PADDING_Y, + ) + link_rect = pymupdf.Rect( + link_x, + text_top, + link_x + link_width, + baseline_y + 2.0, + ) + + return { + "corner": corner, + "x_start": x_start, + "baseline_y": baseline_y, + "link_x": link_x, + "banner_rect": banner_rect, + "link_rect": link_rect, + } + + +def _occupied_page_rects(page: pymupdf.Page) -> list[pymupdf.Rect]: + """ + Collects page rectangles already occupied by visible content. + + Args: + page (pymupdf.Page): The PDF page being processed. + + Returns: + list[pymupdf.Rect]: The occupied rectangles found on the page. + """ + occupied: list[pymupdf.Rect] = [] + + text_data = page.get_text("dict") + for block in text_data.get("blocks", []): + bbox = block.get("bbox") + if bbox is None: + continue + rect = pymupdf.Rect(bbox) + if rect.get_area() <= 0: + continue + occupied.append(_expanded_rect(rect, WATERMARK_COLLISION_PADDING)) + + for drawing in page.get_drawings(): + rect = drawing.get("rect") + if rect is None: + continue + rect = pymupdf.Rect(rect) + if rect.get_area() <= 0: + continue + occupied.append(_expanded_rect(rect, WATERMARK_COLLISION_PADDING)) + + return occupied + + +def _watermark_overlap_score( + banner_rect: pymupdf.Rect, + occupied_rects: list[pymupdf.Rect], +) -> tuple[float, float, int]: + """ + Scores a watermark placement by the amount of page content it overlaps. + + Args: + banner_rect (pymupdf.Rect): The watermark banner rectangle being scored. + occupied_rects (list[pymupdf.Rect]): The occupied page rectangles used for overlap checks. + + Returns: + tuple[float, float, int]: The overlap ratio, overlap area, and overlap count for the placement. + """ + overlap_area = 0.0 + overlap_count = 0 + banner_area = max(banner_rect.get_area(), 1.0) + + for rect in occupied_rects: + if not banner_rect.intersects(rect): + continue + intersection = banner_rect & rect + area = intersection.get_area() + if area <= 0: + continue + overlap_area += area + overlap_count += 1 + + return overlap_area / banner_area, overlap_area, overlap_count + + +def _choose_watermark_layout( + page: pymupdf.Page, + page_index: int, + *, + prefix_width: float, + link_width: float, + total_width: float, +) -> dict[str, Any]: + """ + Selects the watermark placement with the least overlap on a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + page_index (int): The page index being processed. + prefix_width (float): The rendered width of the watermark prefix text. + link_width (float): The rendered width of the watermark link text. + total_width (float): The total rendered width of the watermark text. + + Returns: + dict[str, Any]: The chosen watermark layout data. + """ + occupied_rects = _occupied_page_rects(page) + candidate_layouts = [ + _watermark_layout_for_corner( + page, + corner, + prefix_width=prefix_width, + link_width=link_width, + total_width=total_width, + ) + for corner in _watermark_corner_order(page_index) + ] + + best_layout = candidate_layouts[0] + best_score: tuple[float, float, int] | None = None + + for layout in candidate_layouts: + score = _watermark_overlap_score(layout["banner_rect"], occupied_rects) + if score[0] == 0.0 and score[1] == 0.0: + return layout + if best_score is None or score < best_score: + best_layout = layout + best_score = score + + return best_layout + + +def add_pdf_footer_watermark(doc: pymupdf.Document) -> None: + """ + Adds the anonymization watermark to the least crowded corner of each PDF page. + + Args: + doc (pymupdf.Document): The PDF document being processed. + """ + font_config = _watermark_font_config() + prefix_width = _watermark_text_length( + WATERMARK_PREFIX_TEXT, + font_obj=font_config["text_font"], + fontname=font_config["text_fontname"], + fontsize=WATERMARK_FONT_SIZE, + ) + link_width = _watermark_text_length( + WATERMARK_LINK_TEXT, + font_obj=font_config["link_font"], + fontname=font_config["link_fontname"], + fontsize=WATERMARK_FONT_SIZE, + ) + total_width = prefix_width + link_width + + for page_index, page in enumerate(doc): + layout = _choose_watermark_layout( + page, + page_index, + prefix_width=prefix_width, + link_width=link_width, + total_width=total_width, + ) + baseline_y = layout["baseline_y"] + x_start = layout["x_start"] + link_x = layout["link_x"] + + _insert_watermark_text( + page, + (x_start, baseline_y), + WATERMARK_PREFIX_TEXT, + fontname=font_config["text_fontname"], + fontsize=WATERMARK_FONT_SIZE, + color=WATERMARK_TEXT_COLOR, + fontfile=font_config["text_fontfile"], + ) + _insert_watermark_text( + page, + (link_x, baseline_y), + WATERMARK_LINK_TEXT, + fontname=font_config["link_fontname"], + fontsize=WATERMARK_FONT_SIZE, + color=WATERMARK_LINK_COLOR, + fontfile=font_config["link_fontfile"], + ) + + if layout["corner"].startswith("bottom"): + underline_y = min(page.rect.height - 1.0, baseline_y + 1.0) + else: + underline_y = baseline_y + 1.0 + page.draw_line( + (link_x, underline_y), + (link_x + link_width, underline_y), + color=WATERMARK_LINK_COLOR, + width=0.8, + overlay=True, + ) + page.insert_link( + { + "kind": pymupdf.LINK_URI, + "from": layout["link_rect"], + "uri": WATERMARK_URL, + } + ) diff --git a/aymurai/text/anonymization/pdf/widgets.py b/aymurai/text/anonymization/pdf/widgets.py new file mode 100644 index 00000000..266a912f --- /dev/null +++ b/aymurai/text/anonymization/pdf/widgets.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +from typing import Any + +import pymupdf + +from aymurai.logger import get_logger +from aymurai.text.anonymization.pdf.common import ( + _build_display_token_candidates, + _default_style, + _find_flexible, + _get_base14_font, +) + +logger = get_logger(__name__) + + +def _signature_background_rect( + op: dict[str, Any], + widget_rect: pymupdf.Rect, +) -> pymupdf.Rect: + """ + Builds the background rectangle used for a signature replacement. + + Args: + op (dict[str, Any]): The operation dictionary being processed. + widget_rect (pymupdf.Rect): The rectangle occupied by the widget. + + Returns: + pymupdf.Rect: The background rectangle for the signature replacement. + """ + background_source = ( + op.get("canvas_rect") or op.get("redact_rect") or op.get("line_rect") + ) + background = pymupdf.Rect(background_source or widget_rect) + + redact_rect = op.get("redact_rect") + if redact_rect is not None: + background.include_rect(pymupdf.Rect(redact_rect)) + + # Keep the repaint area on the sensitive text line so the replacement + # background does not visually cover adjacent non-sensitive content + pad_x = max(background.height * 0.2, 0.5) + widget_clip = pymupdf.Rect(widget_rect) + + background.x0 = max(widget_clip.x0, background.x0 - pad_x) + background.y0 = max(widget_clip.y0, background.y0) + background.x1 = min(widget_clip.x1, background.x1 + pad_x) + background.y1 = min(widget_clip.y1, background.y1) + return background + + +def _widget_text_color(widget: pymupdf.Widget) -> tuple[float, float, float]: + """ + Extracts the text color configured on a PDF widget. + + Args: + widget (pymupdf.Widget): The widget being processed. + + Returns: + tuple[float, float, float]: The widget text color in PDF RGB components. + """ + values = list(widget.text_color or []) + if not values: + return (0.0, 0.0, 0.0) + if len(values) == 1: + shade = float(values[0]) + return (shade, shade, shade) + if len(values) >= 3: + return tuple(float(value) for value in values[:3]) + return (0.0, 0.0, 0.0) + + +def _style_from_widget(widget: pymupdf.Widget) -> dict[str, Any]: + """ + Builds a text style dictionary from a widget definition. + + Args: + widget (pymupdf.Widget): The widget being processed. + + Returns: + dict[str, Any]: The style dictionary derived from the widget. + """ + return { + "font": str(widget.text_font or ""), + "flags": 0, + "color": _widget_text_color(widget), + "size": float(widget.text_fontsize or 10.0), + "ascender": 0.8, + "descender": -0.2, + } + + +def _page_widget_infos(page: pymupdf.Page) -> list[dict[str, Any]]: + """ + Collects text and signature widget metadata for a page. + + Args: + page (pymupdf.Page): The PDF page being processed. + + Returns: + list[dict[str, Any]]: The widget metadata collected for the page. + """ + infos: list[dict[str, Any]] = [] + for widget in page.widgets() or []: + if widget.field_type not in ( + pymupdf.PDF_WIDGET_TYPE_TEXT, + pymupdf.PDF_WIDGET_TYPE_SIGNATURE, + ): + continue + infos.append( + { + "xref": int(widget.xref), + "field_type": int(widget.field_type), + "field_name": str(widget.field_name or ""), + "field_value": str(widget.field_value or ""), + "rect": pymupdf.Rect(widget.rect), + "style": _style_from_widget(widget), + } + ) + return infos + + +def _entity_overlaps_widget( + entity_rect: pymupdf.Rect, + widget_infos: list[dict[str, Any]], +) -> dict[str, Any] | None: + """ + Finds the widget that most overlaps the given entity rectangle. + + Args: + entity_rect (pymupdf.Rect): The rectangle representing the entity on the page. + widget_infos (list[dict[str, Any]]): The widget metadata available for overlap checks. + + Returns: + dict[str, Any] | None: The best overlapping widget info, if one exists. + """ + best_widget: dict[str, Any] | None = None + best_area = 0.0 + for widget_info in widget_infos: + widget_rect = widget_info["rect"] + if not entity_rect.intersects(widget_rect): + continue + area = (entity_rect & widget_rect).get_area() + if area > best_area: + best_area = area + best_widget = widget_info + return best_widget + + +def _fit_widget_token( + widget_info: dict[str, Any], + current_text: str, + entity_span: tuple[int, int], + token: str, +) -> str: + """ + Finds a token variant that fits inside a widget value. + + Args: + widget_info (dict[str, Any]): The widget metadata being processed. + current_text (str): The current widget text value. + entity_span (tuple[int, int]): The span of the entity inside the widget text. + token (str): The logical replacement token being processed. + + Returns: + str: The token variant that fits in the widget value. + """ + style = widget_info.get("style") or _default_style() + rect = pymupdf.Rect(widget_info["rect"]) + font_obj = _get_base14_font(style) + max_width = max(rect.width - 1.0, 1.0) + + prefix = current_text[: entity_span[0]] + suffix = current_text[entity_span[1] :] + + for candidate in _build_display_token_candidates(token): + candidate_text = f"{prefix}{candidate}{suffix}" + if ( + font_obj.text_length( + candidate_text, fontsize=float(style.get("size") or 10.0) + ) + <= max_width + 0.1 + ): + return candidate + + candidates = _build_display_token_candidates(token) + return candidates[0] if candidates else f"<{token}>" + + +def _apply_widget_ops( + doc: pymupdf.Document, + widget_ops: dict[int, list[dict]], +) -> None: + """ + Applies collected replacements to editable text widgets. + + Args: + doc (pymupdf.Document): The PDF document being processed. + widget_ops (dict[int, list[dict]]): The collected text widget operations grouped by page index. + """ + for page_idx, ops in widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + widgets = { + int(widget.xref): widget + for widget in (page.widgets() or []) + if widget.field_type == pymupdf.PDF_WIDGET_TYPE_TEXT + } + grouped: dict[int, list[dict]] = {} + for op in ops: + grouped.setdefault(int(op["widget_xref"]), []).append(op) + + for widget_xref, replacements in grouped.items(): + widget = widgets.get(widget_xref) + if widget is None: + logger.warning( + "Could not resolve PDF widget xref=%s on page=%s", + widget_xref, + page_idx, + ) + continue + + current_text = str(widget.field_value or "") + if not current_text: + continue + + search_cursor = 0 + changed = False + for replacement in replacements: + entity_text = replacement["entity_text"] + span = _find_flexible(current_text, entity_text, start=search_cursor) + if span is None: + span = _find_flexible(current_text, entity_text, start=0) + if span is None: + logger.warning( + "Could not map widget label '%s' in widget '%s' on page=%s", + entity_text, + replacement.get("field_name") or widget.field_name, + page_idx, + ) + continue + + token_text = _fit_widget_token( + replacement["widget_info"], + current_text, + span, + replacement["logical_token"], + ) + current_text = ( + f"{current_text[: span[0]]}{token_text}{current_text[span[1] :]}" + ) + search_cursor = span[0] + len(token_text) + changed = True + + if not changed: + continue + + try: + widget.field_value = current_text + widget.update() + except Exception as exc: + logger.warning( + "Failed to update PDF widget '%s' on page=%s: %s", + widget.field_name, + page_idx, + exc, + ) + + +def _prepare_signature_widget_ops( + doc: pymupdf.Document, + signature_widget_ops: dict[int, list[dict]], +) -> None: + """ + Flattens signature widgets and prepares their replacement operations. + + PyMuPDF bakes widgets at document scope, not per widget. When a + signature widget must be flattened, all widgets are intentionally baked + before sanitization so their visible appearances survive in the static + anonymized PDF. + + Args: + doc (pymupdf.Document): The PDF document being processed. + signature_widget_ops (dict[int, list[dict]]): The collected signature widget operations grouped by page index. + """ + should_bake_widgets = False + + for page_idx, ops in signature_widget_ops.items(): + if not ops: + continue + + page = doc[page_idx] + widgets = { + int(widget.xref): widget + for widget in (page.widgets() or []) + if widget.field_type == pymupdf.PDF_WIDGET_TYPE_SIGNATURE + } + grouped: dict[int, list[dict]] = {} + for op in ops: + grouped.setdefault(int(op["widget_xref"]), []).append(op) + + for widget_xref, widget_group_ops in grouped.items(): + widget = widgets.get(widget_xref) + widget_rect = pymupdf.Rect( + widget_group_ops[0].get("widget_rect") or (0, 0, 0, 0) + ) + + if widget is not None: + widget_rect = pymupdf.Rect(widget.rect) + should_bake_widgets = True + else: + logger.warning( + "Could not resolve PDF signature widget xref=%s on page=%s", + widget_xref, + page_idx, + ) + + for op in widget_group_ops: + op["widget_rect"] = pymupdf.Rect(widget_rect) + op.pop("asset_rect", None) + op.pop("image_rect", None) + op.pop("graphics_mode", None) + op["background_rect"] = _signature_background_rect(op, widget_rect) + + if should_bake_widgets: + try: + doc.bake(annots=False, widgets=True) + except Exception as exc: + logger.warning("Failed to flatten PDF signature widgets: %s", exc) diff --git a/aymurai/text/extraction.py b/aymurai/text/extraction.py index 66cc76b0..990eab6e 100644 --- a/aymurai/text/extraction.py +++ b/aymurai/text/extraction.py @@ -1,31 +1,16 @@ -import logging import mimetypes import os -import statistics -import unicodedata import zipfile from pathlib import Path -from typing import Any from zipfile import BadZipFile -import numpy as np -import pymupdf -import textract -import xmltodict -from lxml import etree -from more_itertools import flatten -from textract.exceptions import ShellError -from textract.parsers import _get_available_extensions - from aymurai.logger import get_logger -from aymurai.utils.misc import get_element, get_recursively +from aymurai.text.extractors import SUPPORTED_EXTENSIONS, InvalidFile, get_extractor logger = get_logger(__file__) -TEXTRACT_EXTENSIONS = _get_available_extensions() MIMETYPE_EXTENSION_MAPPER = { "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", - "application/msword": "doc", "application/vnd.oasis.opendocument.text": "odt", "application/pdf": "pdf", } @@ -34,12 +19,6 @@ ERRORS = ["ignore", "coerce", "raise"] -class InvalidFile(Exception): - """Invalid File""" - - pass - - def _zip_contains(path: str, member: str) -> bool: """ Check if a zip file contains a specific member. @@ -100,133 +79,10 @@ def get_extension(path: str) -> str: return "unknown" -def _load_xml_from_odt(path: str, xmlfile: str = "styles.xml") -> str: - """ - Load xml file inside an odt. - - Args: - path (str): path to odt file. - xmlfile (str, optional): xml to open. Defaults to 'styles.xml'. - - Returns: - str: xml content. - """ - with zipfile.ZipFile(path, "r") as odt: - if xmlfile not in odt.namelist(): - return "" - with odt.open(xmlfile) as file: - content = file.read().decode("utf-8") - - return content - - -def _load_xml_from_docx(path: str, xmlfile: str = "word/footnotes.xml") -> Any | None: - """Extract XML content from a specific file inside a .docx.""" - with zipfile.ZipFile(path, "r") as docx: - if xmlfile not in docx.namelist(): - return - with docx.open(xmlfile) as f: - return etree.parse(f) - - -def get_header(path: str) -> list[str]: - """ - Extract header from styles.xml inside a ODT file. - - Args: - path (str): path to odt file. - - Returns: - list[str]: header lines. - """ - styles_xml_content = _load_xml_from_odt(path) - styles_dict = xmltodict.parse(styles_xml_content) - - header_root = get_element( - styles_dict, - levels=[ - "office:document-styles", - "office:master-styles", - "style:master-page", - ], - ) - - if not isinstance(header_root, list): - header_root = [header_root] - - style_header = [ - get_recursively(item, "style:header") - for item in header_root - if get_recursively(item, "style:header") - ] - style_header = list(flatten(style_header)) - - texts = [ - get_recursively(item, "#text") - for item in style_header - if get_recursively(item, "#text") - ] - texts = list(flatten(texts)) - - if not texts: - return [] - - return texts - - -def get_footnotes(path: str) -> list[str] | None: - """ - Extract footnotes from footnotes.xml inside a DOCX file. - - Args: - path (str): Path to the DOCX file. - - Returns: - list[str]: Footnote texts. - """ - footnotes_tree = _load_xml_from_docx(path) - if not footnotes_tree: - return - - footnotes_root = footnotes_tree.getroot() - - # Define the namespace map - ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} - - # Extract footnote texts in order - footnotes_texts = [] - for footnote in footnotes_root.findall("w:footnote", namespaces=ns): - texts = footnote.xpath(".//w:t/text()", namespaces=ns) - if texts: - footnotes_texts.append("".join(texts)) - - return footnotes_texts - - -def pdf_to_text(filename: str, y_tolerance: float | None = None) -> str: - """ - Extract text from a PDF file. - - Args: - filename (str): Path to the PDF file. - y_tolerance (float, optional): - Maximum vertical gap (in points) to consider blocks part of the same paragraph. - - Returns: - str: Extracted text. - """ - if y_tolerance is None: - y_tolerance = compute_median_margin_between_blocks(filename) - - paragraphs = extract_and_merge_paragraphs(filename, np.ceil(y_tolerance)) - docu = "\n\n".join(paragraphs) - docu = unicodedata.normalize("NFKC", docu) - return docu - - def extract_document( filename: str | Path, errors: str = "ignore", + use_cache: bool = True, **kwargs, ) -> str | None: """ @@ -240,7 +96,8 @@ def extract_document( and warn. - If :const:`'ignore'`, then invalid parsing will be set as :const:`NaN` but not warn. - **kwargs: keyword arguments for textract. + use_cache (bool, optional): Toggle extractor-level caching. Defaults to True. + **kwargs: keyword arguments for text extractors. Raises: ValueError: Invalid argument. @@ -249,135 +106,41 @@ def extract_document( Returns: str: extracted document text. """ - filename = str(filename) # patch for pathlib + filename = str(filename) if errors not in ERRORS: raise ValueError(f"errors argument must be in {ERRORS}") ext = get_extension(filename) - kwargs["extension"] = kwargs.get("extension", ext) - kwargs["output_encoding"] = kwargs.get("output_encoding", "utf-8") - - logger = get_logger(f"{__file__}.{__name__}") - - if errors == "ignore": - logger.setLevel(logging.ERROR) - - if ( - not isinstance(filename, str) - or not os.path.exists(filename) - or ext not in TEXTRACT_EXTENSIONS - ): + if (not isinstance(filename, str)) or not os.path.exists(filename): if errors == "raise": raise InvalidFile(f"Invalid path: {filename}") - logger.warn(f"skipping (invalid): {filename}") - return - - try: - if ext == "pdf": - return pdf_to_text(filename, y_tolerance=kwargs.get("y_tolerance")) + logger.warning("Skipping (missing): %s", filename) + return None - docu = textract.process(filename, **kwargs).decode("utf-8") - except (BadZipFile, KeyError, ShellError): + if ext not in SUPPORTED_EXTENSIONS: if errors == "raise": - raise - logger.warn(f"skipping (corrupted): {filename}") - return - - # patch header loading in odt files - if ext == "odt": - header = "\n".join(get_header(filename)) - docu = header + "\n\n" + docu - - # patch footnotes loading in docx files - if ext == "docx": - footnotes = get_footnotes(filename) or [] - footnotes = "\n".join(footnotes) - if footnotes.strip(): - docu = docu + "\n\n" + footnotes - - docu = unicodedata.normalize("NFKC", docu) - return docu - - -def compute_median_margin_between_blocks(pdf_path: str) -> float: - """ - Computes the median vertical margin between text blocks in a PDF. - - Args: - pdf_path (str): Path to the PDF file. - - Returns: - float: Median margin between text blocks (in points). - """ - margins = [] - - with pymupdf.open(pdf_path) as doc: - for page in doc: - # Extract all text blocks from the page - blocks = page.get_text("blocks") - - # Sort blocks by their top y-coordinate (y0) - blocks_sorted = sorted(blocks, key=lambda b: b[1]) - - # Compute vertical margins between consecutive blocks - for i in range(1, len(blocks_sorted)): - previous_block = blocks_sorted[i - 1] - current_block = blocks_sorted[i] - - # Calculate the vertical margin - previous_y1 = previous_block[3] # Bottom of the previous block - current_y0 = current_block[1] # Top of the current block - margin = current_y0 - previous_y1 - - if margin > 0: # Ignore overlapping blocks - margins.append(margin) - - # Compute and return the median margin - if margins: - return statistics.median(margins) - else: - return 0.0 # Return 0 if no margins were found + raise InvalidFile(f"Unsupported extension: {ext}") + logger.warning("Skipping (unsupported %s): %s", ext, filename) + return None + extractor = get_extractor(ext) -def extract_and_merge_paragraphs(pdf_path: str, y_tolerance=5) -> list[str]: - """ - Extracts and merges paragraphs from a PDF by grouping close text blocks. - - Args: - pdf_path (str): Path to the PDF file. - y_tolerance (float): Maximum vertical gap (in points) to consider blocks part of the same paragraph. - - Returns: - list[str]: A list of merged paragraphs as strings. - """ - paragraphs = [] - current_paragraph = [] - last_y1 = None - - with pymupdf.open(pdf_path) as doc: - for page in doc: - # Extract all text blocks from the page - blocks = page.get_text("blocks") - - # Sort blocks by their top y-coordinate (y0) - blocks_sorted = sorted(blocks, key=lambda b: b[1]) - - for block in blocks_sorted: - x0, y0, x1, y1, text, *_ = block - - if last_y1 is not None and (y0 - last_y1) > y_tolerance: - # If the gap between blocks is too large, start a new paragraph - if current_paragraph: - paragraphs.append(" ".join(current_paragraph)) - current_paragraph = [] - - current_paragraph.append(text) - last_y1 = y1 - - if current_paragraph: - paragraphs.append(" ".join(current_paragraph)) - current_paragraph = [] - - return paragraphs + try: + return extractor.extract(Path(filename), use_cache=use_cache, **kwargs) + except InvalidFile as exc: + if errors == "raise": + raise + logger.warning("Skipping (corrupted): %s (%s)", filename, exc) + return None + except BadZipFile as exc: + if errors == "raise": + raise + logger.warning("Skipping (corrupted archive): %s (%s)", filename, exc) + return None + except Exception as exc: + if errors == "raise": + raise + logger.warning("Skipping (unexpected): %s (%s)", filename, exc) + return None diff --git a/aymurai/text/extractors/__init__.py b/aymurai/text/extractors/__init__.py new file mode 100644 index 00000000..01919448 --- /dev/null +++ b/aymurai/text/extractors/__init__.py @@ -0,0 +1,19 @@ +# Import concrete extractors so they self-register. +from aymurai.text.extractors import docx, odt, pdf # noqa: F401 +from aymurai.text.extractors.base import ( + BaseExtractor, + InvalidFile, + get_extractor, + register_extractor, + supported_extensions, +) + +SUPPORTED_EXTENSIONS = supported_extensions() + +__all__ = [ + "BaseExtractor", + "InvalidFile", + "SUPPORTED_EXTENSIONS", + "get_extractor", + "register_extractor", +] diff --git a/aymurai/text/extractors/base.py b/aymurai/text/extractors/base.py new file mode 100644 index 00000000..1603519d --- /dev/null +++ b/aymurai/text/extractors/base.py @@ -0,0 +1,113 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +from aymurai.logger import get_logger + +logger = get_logger(__file__) + + +class InvalidFile(Exception): + """Raised when an extractor receives an invalid or missing file.""" + + +class BaseExtractor(ABC): + """Common interface shared by all document extractors.""" + + # Lowercase file extension handled by the extractor, without dot. + extension: str + + def ensure_file(self, path: Path) -> Path: + """ + Ensure the input file exists before extraction. + + Args: + path (Path): Candidate file path. + + Raises: + InvalidFile: If the file does not exist. + + Returns: + Path: Validated path ready for extraction. + """ + if not path.exists(): + raise InvalidFile(f"Invalid path: {path}") + return path + + @abstractmethod + def extract(self, path: Path, **kwargs) -> str: + """ + Extract normalized text from the source document. + + Args: + path (Path): Input document path. + **kwargs: Optional extractor-specific flags. + + Returns: + str: Cleaned textual content. + """ + + +_REGISTRY: dict[str, type[BaseExtractor]] = {} + + +def register_extractor(cls: type[BaseExtractor]) -> type[BaseExtractor]: + """ + Register an extractor class for a specific extension. + + Args: + cls (type[BaseExtractor]): Extractor class to register. + + Returns: + type[BaseExtractor]: Registered class for fluent decorator usage. + """ + extension = getattr(cls, "extension", None) + if not extension: + raise ValueError( + f"Extractor {cls.__name__} must define an 'extension' attribute" + ) + + normalized = extension.lower() + if normalized in _REGISTRY: + logger.warning( + "Overriding extractor for extension '%s' with %s", normalized, cls.__name__ + ) + + _REGISTRY[normalized] = cls + return cls + + +def get_extractor(extension: str) -> BaseExtractor: + """ + Retrieve an extractor instance for the desired extension. + + Args: + extension (str): File extension to resolve. + + Returns: + BaseExtractor: Ready-to-use extractor instance. + """ + normalized = extension.lower() + try: + extractor_cls = _REGISTRY[normalized] + except KeyError as exc: + raise ValueError(f"Unsupported extension: {extension}") from exc + return extractor_cls() + + +def supported_extensions() -> set[str]: + """ + List the registered document extensions. + + Returns: + set[str]: Known extensions handled by the registry. + """ + return set(_REGISTRY.keys()) + + +__all__ = [ + "BaseExtractor", + "InvalidFile", + "get_extractor", + "register_extractor", + "supported_extensions", +] diff --git a/aymurai/text/extractors/docx.py b/aymurai/text/extractors/docx.py new file mode 100644 index 00000000..52824449 --- /dev/null +++ b/aymurai/text/extractors/docx.py @@ -0,0 +1,31 @@ +from pathlib import Path +from typing import Any +from zipfile import BadZipFile + +import docx2txt + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import get_footnotes, normalize_text + + +@register_extractor +class DocxExtractor(BaseExtractor): + extension = "docx" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + document_text = docx2txt.process(str(file_path)) or "" + except (OSError, BadZipFile, KeyError) as exc: + raise InvalidFile(str(exc)) from exc + except Exception as exc: + raise InvalidFile(str(exc)) from exc + + footnotes = get_footnotes(file_path) or [] + footnotes_text = "\n".join(note for note in footnotes if note.strip()) + + if footnotes_text: + document_text = f"{document_text}\n\n{footnotes_text}" + + return normalize_text(document_text) diff --git a/aymurai/text/extractors/odt.py b/aymurai/text/extractors/odt.py new file mode 100644 index 00000000..41a55355 --- /dev/null +++ b/aymurai/text/extractors/odt.py @@ -0,0 +1,26 @@ +from pathlib import Path +from typing import Any +from xml.etree.ElementTree import ParseError +from zipfile import BadZipFile + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import get_header, normalize_text, odt_to_text + + +@register_extractor +class OdtExtractor(BaseExtractor): + extension = "odt" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + document_text = odt_to_text(file_path) + except (OSError, ValueError, BadZipFile, KeyError, ParseError) as exc: + raise InvalidFile(str(exc)) from exc + + header = "\n".join(get_header(file_path)).strip() + if header: + document_text = f"{header}\n\n{document_text}" + + return normalize_text(document_text) diff --git a/aymurai/text/extractors/pdf.py b/aymurai/text/extractors/pdf.py new file mode 100644 index 00000000..c672dfe7 --- /dev/null +++ b/aymurai/text/extractors/pdf.py @@ -0,0 +1,20 @@ +from pathlib import Path +from typing import Any + +from aymurai.text.extractors.base import BaseExtractor, InvalidFile, register_extractor +from aymurai.text.extractors.utils import pdf_to_text + + +@register_extractor +class PdfExtractor(BaseExtractor): + extension = "pdf" + + def extract(self, path: Path, **_: Any) -> str: + file_path = self.ensure_file(path) + + try: + return pdf_to_text(file_path) + except (OSError, ValueError) as exc: + raise InvalidFile(str(exc)) from exc + except Exception as exc: + raise InvalidFile(str(exc)) from exc diff --git a/aymurai/text/extractors/utils.py b/aymurai/text/extractors/utils.py new file mode 100644 index 00000000..8db4c661 --- /dev/null +++ b/aymurai/text/extractors/utils.py @@ -0,0 +1,280 @@ +import re +import unicodedata +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path +from typing import AbstractSet, Any + +import pymupdf +import pymupdf.layout # noqa: F401 # activates layout support +import pymupdf4llm +import xmltodict +from lxml import etree +from more_itertools import flatten + +from aymurai.logger import get_logger +from aymurai.utils.misc import get_element, get_recursively + +logger = get_logger(__file__) + + +ODT_NS = {"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0"} +PDF_SKIP_BOX_CLASSES = frozenset({"picture", "formula", "table"}) + + +def normalize_text(text: str) -> str: + """ + Normalize Unicode output consistently across extractors. + + Args: + text (str): Raw text extracted from a document. + + Returns: + str: Normalized string in NFKC form. + """ + return unicodedata.normalize("NFKC", text) + + +def _clean_pdf_box_text(text: str, box_class: str) -> str: + """ + Clean box-level PDF text while preserving the original layout content. + + Args: + text (str): Raw text sliced from a page box. + box_class (str): Box class emitted by ``pymupdf4llm``. + + Returns: + str: Cleaned, normalized box text. + """ + text = normalize_text(text).strip() + if box_class == "footnote": + text = re.sub(r"(?m)^>\s?", "", text) + return text + + +def pdf_to_paragraphs( + file_path: Path | str, + *, + include_headers: bool = True, + include_footers: bool = True, + skip_box_classes: AbstractSet[str] = PDF_SKIP_BOX_CLASSES, +) -> list[str]: + """ + Extract paragraph-like layout units from a PDF using PyMuPDF layout parsing. + + Args: + file_path (Path | str): Path to the PDF document. + include_headers (bool): Whether to keep header boxes. Defaults to True. + include_footers (bool): Whether to keep footer boxes. Defaults to True. + skip_box_classes (AbstractSet[str]): Layout box classes to ignore. Defaults to PDF_SKIP_BOX_CLASSES. + + Returns: + list[str]: Normalized paragraph strings extracted from the PDF. + """ + logger.debug("Extracting layout paragraphs from PDF: %s", file_path) + + with pymupdf.open(str(file_path)) as doc: + chunks = pymupdf4llm.to_text( + doc, + filename=str(file_path), + page_chunks=True, + header=include_headers, + footer=include_footers, + show_progress=False, + force_text=True, + use_ocr=False, + force_ocr=False, + ) + + paragraphs: list[str] = [] + for chunk in chunks: + page_text = chunk.get("text") or "" + for box in chunk.get("page_boxes") or []: + if box.get("class") in skip_box_classes: + continue + + start, stop = box.get("pos", (0, 0)) + text = _clean_pdf_box_text(page_text[start:stop], box.get("class") or "") + if text: + paragraphs.append(text) + + return paragraphs + + +def pdf_to_text(file_path: Path | str) -> str: + """ + Extract normalized plain text from a PDF using filtered layout boxes. + + Args: + file_path (Path | str): Path to the PDF document. + + Returns: + str: Cleaned textual content extracted from the PDF. + """ + return "\n\n".join(pdf_to_paragraphs(file_path)) + + +def load_xml_from_docx(path: Path, xmlfile: str = "word/footnotes.xml") -> Any | None: + """ + Extract XML content from a specific file inside a DOCX container. + + Args: + path (Path): DOCX archive path. + xmlfile (str, optional): Internal member name to inspect. Defaults to "word/footnotes.xml". + + Returns: + Any | None: Parsed XML tree or None when the member is missing. + """ + with zipfile.ZipFile(path, "r") as docx: + if xmlfile not in docx.namelist(): + return None + with docx.open(xmlfile) as handle: + return etree.parse(handle) + + +def load_xml_from_odt(path: Path, xmlfile: str = "styles.xml") -> str: + """ + Load XML content from an ODT archive member. + + Args: + path (Path): ODT archive path. + xmlfile (str, optional): Member name to open. Defaults to "styles.xml". + + Returns: + str: UTF-8 decoded XML content or an empty string when absent. + """ + with zipfile.ZipFile(path, "r") as odt: + if xmlfile not in odt.namelist(): + return "" + with odt.open(xmlfile) as file: + return file.read().decode("utf-8") + + +def get_header(path: Path) -> list[str]: + """ + Extract header text defined in an ODT stylesheet. + + Args: + path (Path): ODT document path. + + Returns: + list[str]: Header snippets collected from the stylesheet. + """ + styles_xml_content = load_xml_from_odt(path) + if not styles_xml_content: + return [] + + styles_dict = xmltodict.parse(styles_xml_content) + + header_root = get_element( + styles_dict, + levels=[ + "office:document-styles", + "office:master-styles", + "style:master-page", + ], + ) + + if not isinstance(header_root, list): + header_root = [header_root] + + style_header = [ + get_recursively(item, "style:header") + for item in header_root + if get_recursively(item, "style:header") + ] + style_header = list(flatten(style_header)) + + texts = [ + get_recursively(item, "#text") + for item in style_header + if get_recursively(item, "#text") + ] + texts = list(flatten(texts)) + + return [text for text in texts if isinstance(text, str)] + + +def get_footnotes(path: Path) -> list[str] | None: + """ + Extract footnotes from a DOCX document. + + Args: + path (Path): DOCX document path. + + Returns: + list[str] | None: Ordered footnote texts or None when absent. + """ + footnotes_tree = load_xml_from_docx(path) + if not footnotes_tree: + return None + + footnotes_root = footnotes_tree.getroot() + ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + + footnotes_texts: list[str] = [] + for footnote in footnotes_root.findall("w:footnote", namespaces=ns): + texts = footnote.xpath(".//w:t/text()", namespaces=ns) + if texts: + footnotes_texts.append("".join(texts)) + + return footnotes_texts + + +def _odt_qn(name: str) -> str: + """ + Resolve a qualified ODT tag into the ElementTree namespace form. + + Args: + name (str): Namespaced tag in the ``prefix:local`` format. + + Returns: + str: Expanded tag with namespace URI. + """ + prefix, local = name.split(":", 1) + return f"{{{ODT_NS[prefix]}}}{local}" + + +def _odt_text_to_string(elem) -> str: + """ + Flatten an ODT Element into a plain-text string, preserving special tokens. + + Args: + elem (Element): XML element to flatten. + + Returns: + str: Concatenated text content for the element subtree. + """ + out = elem.text or "" + for child in elem: + if child.tag == _odt_qn("text:tab"): + out += "\t" + elif child.tag == _odt_qn("text:s"): + out += " " * int(child.get(_odt_qn("text:c"), 1)) + else: + out += _odt_text_to_string(child) + if child.tail: + out += child.tail + return out + + +def odt_to_text(path: Path) -> str: + """ + Extract text content, preserving paragraphs and headings, from an ODT file. + + Args: + path (Path): ODT document path. + + Returns: + str: Plain text version of the document content. + """ + with zipfile.ZipFile(path, "r") as archive: + content_xml = archive.read("content.xml") + + content = ET.fromstring(content_xml) + + lines = [] + for child in content.iter(): + if child.tag in (_odt_qn("text:p"), _odt_qn("text:h")): + lines.append(_odt_text_to_string(child)) + return "\n".join(lines) diff --git a/aymurai/text/normalize.py b/aymurai/text/normalize.py index 9027a0d8..4154533b 100644 --- a/aymurai/text/normalize.py +++ b/aymurai/text/normalize.py @@ -2,45 +2,72 @@ import unicodedata -def document_normalize(text: str) -> str: - """Normalize extracted text from documents - * join invalid newlines - * remove continous whitespaces +def _normalize_document_characters(text: str) -> str: + """ + Apply character-level normalization without changing document structure. Args: - text (str): document + text (str): Raw extracted document text. Returns: - str: normalized + str: Character-normalized text. """ - - # normalize character encodings - # text = unicodedata.normalize("NFKD", text) + text = text.replace("\r\n", "\n").replace("\r", "\n") text = unicodedata.normalize("NFKC", text) + text = re.sub(r"(“|”)", '"', text) + text = text.replace("\\/", "/") + text = re.sub(r"[ \t]{2,}", " ", text) + return text + - # remove continous whitespace - text = re.sub(r" {2,}", r" ", text) +def _normalize_paragraph_text(text: str) -> str: + """ + Normalize text inside a single paragraph while preserving paragraph borders. + + Args: + text (str): Paragraph text. + + Returns: + str: Normalized paragraph content. + """ + text = re.sub(r"[ \t]*\n[ \t]*", "\n", text.strip()) # delete newline if NEXT char is: # - lower character or a number - # - punctuanion + # - punctuation text = re.sub(r"\n([a-z0-9;:,\.])", r" \g<1>", text) # delete newline if PREVIOUS char is: # - quote mark - # - punctuanions (except '.' because possible ambiguity) + # - punctuations (except '.' because possible ambiguity) text = re.sub(r"([\w,\"-])\n", r"\g<1> ", text) # cleanup some junk - # - multiple newlines, hyphens - text = re.sub(r"\n{2,}", "\n", text) text = re.sub(r"[-]{2,}", "-", text) text = re.sub(r"\.-", ".", text) + text = re.sub(r" {2,}", " ", text) + return text.strip() - # quotation marks - text = re.sub(r"(“|”)", '"', text) - # scaped slashes - text = text.replace("\/", "/") +def document_normalize(text: str, *, preserve_paragraphs: bool = False) -> str: + """Normalize extracted text from documents. - return text + Args: + text (str): Document text. + preserve_paragraphs (bool): Preserve blank-line paragraph boundaries. Defaults to False. + + Returns: + str: Normalized document text. + """ + text = _normalize_document_characters(text) + + if preserve_paragraphs: + paragraphs = [ + _normalize_paragraph_text(paragraph) + for paragraph in re.split(r"\n\s*\n+", text) + if paragraph.strip() + ] + return "\n\n".join(paragraphs) + + text = _normalize_paragraph_text(text) + return re.sub(r"\n{2,}", "\n", text) diff --git a/aymurai/text/tokenizers/spanish.py b/aymurai/text/tokenizers/spanish.py deleted file mode 100644 index fa11b0b4..00000000 --- a/aymurai/text/tokenizers/spanish.py +++ /dev/null @@ -1,372 +0,0 @@ -""" -A simplified clone of spaCy’s Spanish tokenizer (and related components) -without any dependency on spaCy. This module provides: - • TOKENIZER_EXCEPTIONS – a mapping for known exceptions. - • STOP_WORDS – Spanish stop words. - • TOKENIZER_SUFFIXES and TOKENIZER_INFIXES – basic regex rules. - • Lexical attribute getter for “like_num”. - • SpanishTokenizer – a class that tokenizes text using the above rules. - -Note: This is a “simple but close” clone. spaCy’s internal rules are very -comprehensive; here we implement only some of the core ideas. -""" - -import re - -# --- Definitions used in spaCy’s internals, simplified --- -ORTH = "ORTH" -NORM = "NORM" - - -def update_exc(base, new_exc): - base.update(new_exc) - return base - - -# A base (empty) exceptions dict -BASE_EXCEPTIONS = {} - -# --- Tokenizer Exceptions (from spacy.lang.es.tokenizer_exceptions) --- -_exc = { - "pal": [{ORTH: "pa"}, {ORTH: "l", NORM: "el"}], -} - -for exc_data in [ - {ORTH: "n°"}, - {ORTH: "°C"}, - {ORTH: "aprox."}, - {ORTH: "dna."}, - {ORTH: "dpto."}, - {ORTH: "ej."}, - {ORTH: "esq."}, - {ORTH: "pág."}, - {ORTH: "p.ej."}, - {ORTH: "Ud.", NORM: "usted"}, - {ORTH: "Vd.", NORM: "usted"}, - {ORTH: "Uds.", NORM: "ustedes"}, - {ORTH: "Vds.", NORM: "ustedes"}, - {ORTH: "vol.", NORM: "volúmen"}, -]: - _exc[exc_data[ORTH]] = [exc_data] - -# Times exceptions: “12m.” splits into “12” and “m.”, and also the h+a.m./p.m. forms. -_exc["12m."] = [{ORTH: "12"}, {ORTH: "m."}] - -for h in range(1, 13): - for period in ["a.m.", "am"]: - _exc[f"{h}{period}"] = [{ORTH: f"{h}"}, {ORTH: period}] - for period in ["p.m.", "pm"]: - _exc[f"{h}{period}"] = [{ORTH: f"{h}"}, {ORTH: period}] - -for orth in [ - "a.C.", - "a.J.C.", - "d.C.", - "d.J.C.", - "apdo.", - "Av.", - "Avda.", - "Cía.", - "Dr.", - "Dra.", - "EE.UU.", - "Ee.Uu.", - "EE. UU.", - "Ee. Uu.", - "etc.", - "fig.", - "Gob.", - "Gral.", - "Ing.", - "J.C.", - "km/h", - "Lic.", - "m.n.", - "núm.", - "P.D.", - "Prof.", - "Profa.", - "q.e.p.d.", - "Q.E.P.D.", - "S.A.", - "S.L.", - "S.R.L.", - "s.s.s.", - "Sr.", - "Sra.", - "Srta.", -]: - _exc[orth] = [{ORTH: orth}] - -TOKENIZER_EXCEPTIONS = update_exc(BASE_EXCEPTIONS.copy(), _exc) - -# --- Stop words (from spacy.lang.es.stop_words) --- -STOP_WORDS = set( - """ -a acuerdo adelante ademas además afirmó agregó ahi ahora ahí al algo alguna -algunas alguno algunos algún alli allí alrededor ambos ante anterior antes -apenas aproximadamente aquel aquella aquellas aquello aquellos aqui aquél -aquélla aquéllas aquéllos aquí arriba aseguró asi así atras aun aunque añadió -aún - -bajo bastante bien breve buen buena buenas bueno buenos - -cada casi cierta ciertas cierto ciertos cinco claro comentó como con conmigo -conocer conseguimos conseguir considera consideró consigo consigue consiguen -consigues contigo contra creo cual cuales cualquier cuando cuanta cuantas -cuanto cuantos cuatro cuenta cuál cuáles cuándo cuánta cuántas cuánto cuántos -cómo - -da dado dan dar de debajo debe deben debido decir dejó del delante demasiado -demás dentro deprisa desde despacio despues después detras detrás dia dias dice -dicen dicho dieron diez diferente diferentes dijeron dijo dio doce donde dos -durante día días dónde - -e el ella ellas ello ellos embargo en encima encuentra enfrente enseguida -entonces entre era eramos eran eras eres es esa esas ese eso esos esta estaba -estaban estado estados estais estamos estan estar estará estas este esto estos -estoy estuvo está están excepto existe existen explicó expresó él ésa ésas ése -ésos ésta éstas éste éstos - -fin final fue fuera fueron fui fuimos - -gran grande grandes - -ha haber habia habla hablan habrá había habían hace haceis hacemos hacen hacer -hacerlo haces hacia haciendo hago han hasta hay haya he hecho hemos hicieron -hizo hoy hubo - -igual incluso indicó informo informó ir - -junto - -la lado largo las le les llegó lleva llevar lo los luego - -mal manera manifestó mas mayor me mediante medio mejor mencionó menos menudo mi -mia mias mientras mio mios mis misma mismas mismo mismos modo mucha muchas -mucho muchos muy más mí mía mías mío míos - -nada nadie ni ninguna ningunas ninguno ningunos ningún no nos nosotras nosotros -nuestra nuestras nuestro nuestros nueva nuevas nueve nuevo nuevos nunca - -o ocho once os otra otras otro otros - -para parece parte partir pasada pasado paìs peor pero pesar poca pocas poco -pocos podeis podemos poder podria podriais podriamos podrian podrias podrá -podrán podría podrían poner por porque posible primer primera primero primeros -pronto propia propias propio propios proximo próximo próximos pudo pueda puede -pueden puedo pues - -qeu que quedó queremos quien quienes quiere quiza quizas quizá quizás quién -quiénes qué - -realizado realizar realizó repente respecto - -sabe sabeis sabemos saben saber sabes salvo se sea sean segun segunda segundo -según seis ser sera será serán sería señaló si sido siempre siendo siete sigue -siguiente sin sino sobre sois sola solamente solas solo solos somos son soy su -supuesto sus suya suyas suyo suyos sé sí sólo - -tal tambien también tampoco tan tanto tarde te temprano tendrá tendrán teneis -tenemos tener tenga tengo tenido tenía tercera tercero ti tiene tienen toda -todas todavia todavía todo todos total tras trata través tres tu tus tuvo tuya -tuyas tuyo tuyos tú - -u ultimo un una unas uno unos usa usais usamos usan usar usas uso usted ustedes -última últimas último últimos - -va vais vamos van varias varios vaya veces ver verdad verdadera verdadero vez -vosotras vosotros voy vuestra vuestras vuestro vuestros - -y ya yo -""".split() -) - -# --- Punctuation-related rules (a simplified version of spacy.lang.es.punctuation) --- -# Here we provide basic suffix and infix patterns. -TOKENIZER_SUFFIXES = [ - r"[—–]", # dashes - r"[.,!?;:%]", # common punctuation - r"['\"“”‘’]", # quotes -] -# Infixes that split on, for example, hyphens between digits. -TOKENIZER_INFIXES = [ - r"(?<=[0-9])[-/](?=[0-9])", -] - -# --- Lexical attribute getter (from spacy.lang.es.lex_attrs) --- -_num_words = { - "cero", - "uno", - "dos", - "tres", - "cuatro", - "cinco", - "seis", - "siete", - "ocho", - "nueve", - "diez", - "once", - "doce", - "trece", - "catorce", - "quince", - "dieciséis", - "diecisiete", - "dieciocho", - "diecinueve", - "veinte", - "veintiuno", - "veintidós", - "veintitrés", - "veinticuatro", - "veinticinco", - "veintiséis", - "veintisiete", - "veintiocho", - "veintinueve", - "treinta", - "cuarenta", - "cincuenta", - "sesenta", - "setenta", - "ochenta", - "noventa", - "cien", - "mil", - "millón", - "billón", - "trillón", -} -_ordinal_words = { - "primero", - "segundo", - "tercero", - "cuarto", - "quinto", - "sexto", - "séptimo", - "octavo", - "noveno", - "décimo", - "undécimo", - "duodécimo", - "decimotercero", - "decimocuarto", - "decimoquinto", - "decimosexto", - "decimoséptimo", - "decimoctavo", - "decimonoveno", - "vigésimo", - "trigésimo", - "cuadragésimo", - "quincuagésimo", - "sexagésimo", - "septuagésimo", - "octogésima", - "nonagésima", - "centésima", - "milésima", - "millonésima", - "billonésima", -} - - -def like_num(text): - # Remove a leading +, -, ±, or ~ if present. - if text.startswith(("+", "-", "±", "~")): - text = text[1:] - # Remove commas and periods for a digit check. - text_clean = text.replace(",", "").replace(".", "") - if text_clean.isdigit(): - return True - if text.count("/") == 1: - num, denom = text.split("/") - if num.isdigit() and denom.isdigit(): - return True - text_lower = text.lower() - if text_lower in _num_words: - return True - if text_lower in _ordinal_words: - return True - return False - - -LEX_ATTRS = {"LIKE_NUM": like_num} - - -# --- Spanish Tokenizer Implementation --- -class SpanishTokenizer: - """ - Spacy spanish tokenizer clone - Source: https://github.com/explosion/spaCy/tree/b3c46c315eb16ce644bddd106d31c3dd349f6bb2/spacy/lang/es - """ - - def __init__(self): - # A basic regex for “words” and “non-space” characters. - self.word_re = re.compile(r"\w+|[^\w\s]", re.UNICODE) - # Combine infix patterns into one regex. - self.infix_re = re.compile("|".join(TOKENIZER_INFIXES)) - - def apply_exceptions(self, text: str): - """ - If the given text exactly matches a tokenizer exception, - return the corresponding token list. - """ - if text in TOKENIZER_EXCEPTIONS: - # For each exception item (a dict), return the ORTH value, - # falling back to NORM if available. - return [ - item.get(ORTH, item.get(NORM, item)) - for item in TOKENIZER_EXCEPTIONS[text] - ] - return None - - def tokenize(self, text: str): - """ - Tokenizes the input text into a list of tokens. - The algorithm first splits on whitespace, checks for exceptions, - and otherwise applies regex-based splitting and infix splitting. - """ - tokens = [] - for word in text.split(): - exc = self.apply_exceptions(word) - if exc is not None: - tokens.extend(exc) - else: - # First, use regex to split word into basic tokens. - sub_tokens = self.word_re.findall(word) - final_tokens = [] - for token in sub_tokens: - # Apply infix splitting if applicable. - splits = self.infix_re.split(token) - if len(splits) > 1: - last_end = 0 - # Re-find infix matches to capture separators. - for m in self.infix_re.finditer(token): - pre = token[last_end : m.start()] - sep = token[m.start() : m.end()] - if pre: - final_tokens.append(pre) - final_tokens.append(sep) - last_end = m.end() - remainder = token[last_end:] - if remainder: - final_tokens.append(remainder) - else: - final_tokens.append(token) - tokens.extend(final_tokens) - return tokens - - def __call__(self, text: str): - return self.tokenize(text) - - -# --- Example Usage --- -if __name__ == "__main__": - sample_text = "El Sr. habló con 12a.m. y 3pm en la reunión. ¡Increíble, verdad?" - tokenizer = SpanishTokenizer() - tokens = tokenizer(sample_text) - print("Tokens:", tokens) diff --git a/aymurai/transforms/anonymization_postprocess/core.py b/aymurai/transforms/anonymization_postprocess/core.py index 977fb109..af5185e4 100644 --- a/aymurai/transforms/anonymization_postprocess/core.py +++ b/aymurai/transforms/anonymization_postprocess/core.py @@ -1,10 +1,10 @@ import re from copy import deepcopy -from string import punctuation +from aymurai.meta.pipeline_interfaces import Transform from aymurai.meta.types import DataItem from aymurai.utils.misc import get_element -from aymurai.meta.pipeline_interfaces import Transform +from aymurai.transforms.anonymization_postprocess.exact_labels import EXACT_LABELS class AnonymizationEntityCleaner(Transform): @@ -33,6 +33,7 @@ def process(self, ent: dict) -> dict: original_text = ent["text"] start_char = ent["start_char"] end_char = ent["end_char"] + label = ent["attrs"]["aymurai_label"] # Match leading and trailing non-alphanumeric characters leading_match = re.match(r"^\W+", original_text) @@ -45,10 +46,27 @@ def process(self, ent: dict) -> dict: # Clean the text cleaned_text = pattern.sub("", original_text) + if not cleaned_text: + return None + + raw_subclass = ent["attrs"]["aymurai_label_subclass"] + if isinstance(raw_subclass, list): + aymurai_label_subclass = raw_subclass.copy() + elif raw_subclass: + aymurai_label_subclass = [raw_subclass] + else: + aymurai_label_subclass = [] + + if label in EXACT_LABELS: + flattened_text = re.sub(r"[^a-zA-Z0-9]", "", cleaned_text) + if flattened_text and flattened_text not in aymurai_label_subclass: + aymurai_label_subclass.append(flattened_text) + # Update the entity's alt text and indices ent["attrs"]["aymurai_alt_text"] = cleaned_text ent["attrs"]["aymurai_alt_start_char"] = start_char + leading_chars_removed ent["attrs"]["aymurai_alt_end_char"] = end_char - trailing_chars_removed + ent["attrs"]["aymurai_label_subclass"] = aymurai_label_subclass return ent @@ -61,11 +79,11 @@ def __call__(self, item: DataItem) -> DataItem: DataItem: processed item """ item = deepcopy(item) - ents = get_element(item, [self.field, "entities"]) or [] - # Filter out predictions that are punctuation marks only - ents = [ent for ent in ents if ent["text"] not in punctuation] - ents = [self.process(ent) for ent in ents] + # Filter out predictions with empty alt text and update the rest + item[self.field]["entities"] = [ + out for ent in ents if (out := self.process(ent)) is not None + ] return item diff --git a/aymurai/transforms/anonymization_postprocess/exact_labels.py b/aymurai/transforms/anonymization_postprocess/exact_labels.py new file mode 100644 index 00000000..afe345da --- /dev/null +++ b/aymurai/transforms/anonymization_postprocess/exact_labels.py @@ -0,0 +1,10 @@ +EXACT_LABELS = { + "DNI", + "CUIT_CUIL", + "TELEFONO", + "PATENTE_DOMINIO", + "IP", + "NUM_CAJA_AHORRO", + "CBU", + "NUM_MATRICULA", +} diff --git a/aymurai/transforms/datetime_formatter/__init__.py b/aymurai/transforms/datetime_formatter/__init__.py index e69de29b..822a1d04 100644 --- a/aymurai/transforms/datetime_formatter/__init__.py +++ b/aymurai/transforms/datetime_formatter/__init__.py @@ -0,0 +1 @@ +from .core import DatetimeFormatter diff --git a/aymurai/transforms/datetime_formatter/core.py b/aymurai/transforms/datetime_formatter/core.py index 8f3c49c9..e98c0d76 100644 --- a/aymurai/transforms/datetime_formatter/core.py +++ b/aymurai/transforms/datetime_formatter/core.py @@ -2,9 +2,9 @@ from datetime_matcher import DatetimeMatcher +from aymurai.meta.pipeline_interfaces import Transform from aymurai.meta.types import DataItem from aymurai.utils.misc import get_element -from aymurai.meta.pipeline_interfaces import Transform from .patterns import patterns @@ -55,7 +55,9 @@ def process(self, ent): text_repr = datetime.strftime("%d/%m/%Y") suggestions.append(text_repr) - ent["attrs"]["aymurai_label_subclass"] = suggestions + ent["attrs"]["aymurai_label_subclass"] = ( + [max(suggestions)] if suggestions else [] + ) return ent diff --git a/aymurai/transforms/datetime_formatter/patterns.py b/aymurai/transforms/datetime_formatter/patterns.py index 77bf1d9d..242b04e4 100644 --- a/aymurai/transforms/datetime_formatter/patterns.py +++ b/aymurai/transforms/datetime_formatter/patterns.py @@ -10,7 +10,13 @@ r"%d/%m/%y", r"(?i)%-d de %B del? %Y", r"(?i)%-d de %B %Y", + r"(?i)%-d %B de %Y", + r"(?i)%-d de %B %Y", + r"%d-%m-%Y", + r"%Y-%m-%d", + r"(?i)%-d de %B", ] + HOURS = [ r"%H[\.:]%M", r"(?i)%-H[\.:]%M horas", @@ -19,6 +25,7 @@ patterns = { "FECHA_RESOLUCION": DATES, + "FECHA": DATES, "HORA_DE_INICIO": HOURS, "HORA_DE_CIERRE": HOURS, } diff --git a/aymurai/transforms/entity_subcategories/bm25.py b/aymurai/transforms/entity_subcategories/bm25.py new file mode 100644 index 00000000..cb88d145 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/bm25.py @@ -0,0 +1,101 @@ +import re +from typing import Callable + +import numpy as np + + +class BM25Scorer: + """Lightweight BM25 scorer over subcategory strings.""" + + def __init__(self, subcategories: list[str], normalize_fn: Callable[[str], str]): + """ + Initialize the BM25Scorer with subcategories and a normalization function. + + Args: + subcategories (list[str]): List of subcategory strings to be scored. + normalize_fn (Callable[[str], str]): Function to normalize text before tokenization. + """ + self.subcategories = subcategories + self.token_pattern = re.compile(r"\w+", re.UNICODE) + self.normalize_fn = normalize_fn + + tokenized = [self._tokenize(sub) for sub in subcategories] + self.doc_len = [len(toks) for toks in tokenized] + self.avgdl = sum(self.doc_len) / len(self.doc_len) if self.doc_len else 0.0 + self.N = len(tokenized) + self.doc_freqs = [self._to_counter(toks) for toks in tokenized] + corpus_tokens = set().union(*self.doc_freqs) if self.doc_freqs else set() + self.df = { + token: sum(1 for doc in self.doc_freqs if token in doc) + for token in corpus_tokens + } + # Using BM25+ style IDF smoothing + self.idf = { + token: np.log(1 + (self.N - freq + 0.5) / (freq + 0.5)) + for token, freq in self.df.items() + } + + def _tokenize(self, text: str) -> list[str]: + """ + Tokenize the input text after normalization. + + Args: + text (str): Input text to tokenize. + + Returns: + list[str]: List of tokens. + """ + normalized = self.normalize_fn(text) + return self.token_pattern.findall(normalized) + + def _to_counter(self, tokens: list[str]) -> dict[str, int]: + """ + Convert a list of tokens into a frequency counter. + + Args: + tokens (list[str]): List of tokens. + + Returns: + dict[str, int]: Frequency counter of tokens. + """ + counts = {} + + for t in tokens: + counts[t] = counts.get(t, 0) + 1 + + return counts + + def score_vector(self, text: str, k1: float = 1.2, b: float = 0.75) -> np.ndarray: + """ + Compute BM25 scores for the input text against all subcategories. + + Args: + text (str): Input text to score. + k1 (float, optional): BM25 k1 parameter. Defaults to 1.2. + b (float, optional): BM25 b parameter. Defaults to 0.75. + + Returns: + np.ndarray: + """ + scores = np.zeros(self.N, dtype=np.float64) + tokens = self._tokenize(text) + + for token in tokens: + idf = self.idf.get(token) + if idf is None: + continue + + for idx, freq_map in enumerate(self.doc_freqs): + freq = freq_map.get(token, 0) + if freq == 0: + continue + + # BM25 scoring formula + denom = freq + k1 * ( + 1 - b + b * self.doc_len[idx] / (self.avgdl + 1e-9) + ) + + # Update score + scores[idx] += idf * freq * (k1 + 1) / denom + + return scores diff --git a/aymurai/transforms/entity_subcategories/sentence_transformer.py b/aymurai/transforms/entity_subcategories/sentence_transformer.py new file mode 100644 index 00000000..790127f2 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/sentence_transformer.py @@ -0,0 +1,268 @@ +from copy import deepcopy +from pathlib import Path +from typing import Iterable + +import numpy as np + +from aymurai.logger import get_logger +from aymurai.meta.pipeline_interfaces import Transform +from aymurai.meta.types import DataItem +from aymurai.models.sentence_encoder.base import BaseSentenceEncoder +from aymurai.models.sentence_encoder.factory import create_encoder +from aymurai.settings import settings +from aymurai.transforms.entity_subcategories.bm25 import BM25Scorer +from aymurai.transforms.entity_subcategories.subcategories import SUBCATEGORIES +from aymurai.transforms.entity_subcategories.utils import filter_by_category +from aymurai.utils.misc import get_element + +logger = get_logger(__name__) + + +class SentenceTransformerSubcategorizer(Transform): + """ + Hybrid sentence-transformer + optional BM25 subcategorizer. + + - When ``bm25_weight > 0`` mixes BM25 and encoder cosine scores. + - When ``bm25_weight <= 0`` it falls back to encoder-only retrieval. + - Subcategories are normalized (underscore -> space, then encoder.normalize_text) before encoding. + """ + + def __init__( + self, + category: str, + embeddings_path: str, + encoder_name: str = "distiluse", + bm25_weight: float = 0.5, + device: str | None = None, + batch_size: int = 256, + rebuild_embeddings: bool = False, + encoder: BaseSentenceEncoder | None = None, + ): + """ + SentenceTransformerSubcategorizer constructor. + + Args: + category (str): Category to subcategorize. + embeddings_path (str): Path to store/load subcategory embeddings. + encoder_name (str, optional): Name of the encoder to use. Defaults to "distiluse". + bm25_weight (float, optional): Weight for BM25 scoring. Defaults to 0.5. + device (str | None, optional): Device to run the encoder on. Defaults to None. + batch_size (int, optional): Batch size for encoding. Defaults to 256. + rebuild_embeddings (bool, optional): Whether to rebuild embeddings even if cached ones exist. Defaults to False. + encoder (BaseSentenceEncoder | None, optional): Encoder instance to use. Defaults to None. + """ + self.category = category + self.bm25_weight = float(bm25_weight) + self.batch_size = batch_size + + cache_root = Path(settings.CACHE_BASEPATH) + self.cache_path = cache_root / self.__class__.__name__ + self.cache_path.mkdir(parents=True, exist_ok=True) + + self.subcategories = self._load_subcategories(category) + + self.encoder_name = encoder_name + self.encoder = encoder or create_encoder( + encoder_type=encoder_name, device=device + ) + + embeddings_path = Path(embeddings_path) + if not embeddings_path.is_absolute(): + embeddings_path = self.cache_path / embeddings_path + embeddings_path.parent.mkdir(parents=True, exist_ok=True) + self.embeddings_path = embeddings_path + + self.response_vectors = self._load_or_build_embeddings(rebuild_embeddings) + self.bm25 = ( + BM25Scorer(self.subcategories, normalize_fn=self._normalize_subcategory) + if self.bm25_weight > 0 + else None + ) + + def _load_subcategories(self, category: str) -> list[str]: + """ + Load subcategories for a given category. + + Args: + category (str): Category name. + + Raises: + ValueError: If no subcategories are found for the given category. + + Returns: + list[str]: List of subcategories. + """ + key = category.lower().replace(" ", "_") + try: + return SUBCATEGORIES[key] + except KeyError as exc: + raise ValueError( + f"No subcategories found for category '{category}'" + ) from exc + + def _load_or_build_embeddings(self, rebuild: bool) -> np.ndarray: + """ + Load or build subcategory response embeddings. + + Args: + rebuild (bool): Whether to rebuild embeddings even if cached ones exist. + + Raises: + ValueError: If the embeddings file is missing expected data. + + Returns: + np.ndarray: Array of subcategory embeddings. + """ + if self.embeddings_path.exists() and not rebuild: + data = np.load(self.embeddings_path, allow_pickle=True) + if isinstance(data, np.lib.npyio.NpzFile): + vectors = data.get("vectors") + subs = data.get("subcategories") + if vectors is None: + raise ValueError("Embeddings file missing 'vectors'") + if subs is not None and len(subs) != len(self.subcategories): + logger.warning("Subcategory count mismatch; rebuilding embeddings") + else: + return vectors + else: + return data + + logger.info("Building response embeddings for subcategories") + + # Apply the same normalization used in notebooks: replace underscores, then encoder normalize. + normalized = [self._normalize_subcategory(sub) for sub in self.subcategories] + vectors = self.encoder.batch_encode( + normalized, + encoder_type="response_encoder", + batch_size=self.batch_size, + ) + + np.savez( + self.embeddings_path, + vectors=vectors, + subcategories=self.subcategories, + ) + logger.info(f"Saved response embeddings to {self.embeddings_path}") + + return vectors + + def _normalize_subcategory(self, name: str) -> str: + """ + Normalize subcategory strings for stable embeddings. + + Args: + name (str): Subcategory name. + + Returns: + str: Normalized subcategory name. + """ + + name = name.replace("_", " ") + + return self.encoder.normalize_text(name) + + def _combine_scores( + self, sim_scores: np.ndarray, bm25_scores: np.ndarray + ) -> np.ndarray: + """ + Combine similarity and BM25 scores. + + Args: + sim_scores (np.ndarray): Similarity scores. + bm25_scores (np.ndarray): BM25 scores. + + Returns: + np.ndarray: Combined scores. + """ + if self.bm25_weight <= 0: + return sim_scores + + bm25_max = bm25_scores.max() if bm25_scores.size else 0.0 + sim_max = sim_scores.max() if sim_scores.size else 0.0 + + bm25_norm = ( + bm25_scores / (bm25_max + 1e-9) + if bm25_max > 0 + else np.zeros_like(bm25_scores) + ) + + sim_norm = ( + sim_scores / (sim_max + 1e-9) if sim_max > 0 else np.zeros_like(sim_scores) + ) + + return self.bm25_weight * bm25_norm + (1 - self.bm25_weight) * sim_norm + + def retrieve(self, text: str, top_k: int = 10) -> list[str]: + """ + Retrieve top-k subcategories for a given text. + + Args: + text (str): Input text. + top_k (int, optional): Number of top subcategories to retrieve. Defaults to 10. + + Returns: + list[str]: List of top-k subcategories. + """ + return self.batch_retrieve([text], top_k=top_k)[0] + + def batch_retrieve(self, texts: Iterable[str], top_k: int = 10) -> list[list[str]]: + """ + Batch retrieve top-k subcategories for given texts. + + Args: + texts (Iterable[str]): Input texts. + top_k (int, optional): Number of top subcategories to retrieve. Defaults to 10. + + Returns: + list[list[str]]: List of lists of top-k subcategories for each input text. + """ + texts = list(texts) + if not texts: + return [] + + query_vectors = self.encoder.batch_encode( + texts, encoder_type="question_encoder", batch_size=self.batch_size + ) + similarity = np.inner(query_vectors, self.response_vectors) + k = min(top_k, similarity.shape[1]) + + results = [] + for idx, text in enumerate(texts): + sim_scores = similarity[idx] + + if self.bm25 and self.bm25_weight > 0: + bm25_scores = self.bm25.score_vector(text) + combined = self._combine_scores(sim_scores, bm25_scores) + else: + combined = sim_scores + + top_indices = np.argsort(-combined)[:k] + results.append([self.subcategories[i] for i in top_indices]) + + return results + + def __call__(self, item: DataItem) -> DataItem: + """ + Apply subcategorization to entities in the data item. + + Args: + item (DataItem): Input data item. + + Returns: + DataItem: Data item with added subclass labels. + """ + item = deepcopy(item) + ents = get_element(item, levels=["predictions", "entities"]) or [] + texts = [ent["text"] for ent in ents] + filtered_ents = filter_by_category(ents, self.category) + retrieved = self.batch_retrieve(texts, top_k=5) + + for ent, retrieved_ in zip(ents, retrieved): + if ent in filtered_ents and not get_element( + ent, levels=["attrs", "aymurai_label_subclass"] + ): + ent.setdefault("attrs", {})["aymurai_label_subclass"] = retrieved_ + + item.setdefault("predictions", {})["entities"] = ents + + return item diff --git a/aymurai/transforms/entity_subcategories/subcategories.py b/aymurai/transforms/entity_subcategories/subcategories.py new file mode 100644 index 00000000..ad4fd5a9 --- /dev/null +++ b/aymurai/transforms/entity_subcategories/subcategories.py @@ -0,0 +1,638 @@ +SUBCATEGORIES = { + "conducta": [ + "abandonar_animal_domestico", + "abandono_de_personas", + "abuso_de_armas", + "abuso_de_autoridad_e_incumplimiento_deberes_funcionario_publico", + "abuso_sexual", + "acceder_a_lugares_distintos_segun_entrada", + "acceder_lugares_distintos_segun_entrada", + "acceso_a_sistema_restringido", + "acoso_sexual_callejero", + "actividades_lucrativas_sin_autorizacion", + "actividades_lucrativas_sin_habilitacion", + "actos_contenido_sexual_con_menores", + "administracion_fraudulenta", + "afectar_desarrollo_espectaculo", + "afectar_el_desarrollo_del_espectaculo", + "afectar_funcionamiento_servicios_publicos", + "afectar_servicios_de_emergencia_o_seguridad", + "afectar_servicios_emergencia", + "afectar_señalizacion", + "allanamiento_autonomo", + "alterar_programa", + "alterar_sepulturas", + "amenazas", + "amparo", + "apariencia_falsa", + "apariencia_falsa_para_entrar_a_domicilio_o_lugar_privado", + "apropiacion_indebida_de_tributos", + "arrojar_cosas_que_puedan_causar_lesiones", + "arrojar_cosas_sustancias", + "arrojar_sustancias_insalubres_en_lugares_publicos", + "asociacion_ilicita", + "asuncion_falsa_de_contravencion", + "atentado_contra_la_autoridad", + "ausencia_de_habilitacion", + "banderas", + "carteles_afiches_volantes", + "cierre_defectuoso", + "circular_por_carriles_exclusivos", + "circular_por_lugar_prohibido", + "cohecho_activo", + "cohecho_pasivo", + "coimas", + "competencia_desleal", + "conducir_con_mayor_cantidad_de_alcohol_en_sangre_del_permitido", + "conducir_con_mayor_grado_de_alcohol_o_bajo_efectos_de_estupefacientes", + "conducir_sin_licencia_que_lo_habilite_por_categoria_de_vehiculo", + "conducir_usando_aparato_electronico", + "connivencia_policial", + "contactar_menor_por_medio_de_tecnologias_para_cometer_delitos_contra_su_integridad_sexual", + "cruce_de_semaforo_en_rojo", + "cuida_coche", + "cuidar_coches_sin_autorizacion", + "daños", + "daños_informaticos", + "defraudacion", + "defraudacion_contra_la_administracion_publica", + "delito_contra_seguridad_transito", + "denegacion_de_justicia", + "derecho_admision", + "desarmado_automotor", + "desinfeccion_y_desratizacion", + "desobediencia_a_cargas_procesales", + "desobediencia_a_la_autoridad", + "difusion_no_autorizada_de_imagenes", + "difusion_no_autorizada_de_imagenes_intimas", + "discriminar", + "documentacion_sanitaria", + "ejecucion_de_multa", + "ejercer_ilegitimamente_actividad", + "ejercicio_ilegal_de_la_medicina", + "elementos_de_prevencion_contra_incendio", + "encubrimiento", + "encubrimiento_actividades_baile", + "ensuciar_bienes", + "entregar_indebidamente_armas_explosivos", + "espantar_animales", + "espejos_retrovisores", + "estacionamiento_prohibido", + "estacionamiento_sobre_senda_peatonal", + "estafa", + "estupefacientes", + "exceso_capacidad_ingreso", + "exceso_de_velocidad", + "exhibicion_de_documentacion_obligatoria", + "exhibiciones_obscenas", + "exhortos_de_otras_jurisdicciones", + "explotacion_trabajo_infantil", + "extorsion", + "fabricar_transportar_artefactos_pirotecnicos", + "falsa_denuncia", + "falsedad_documental_de_certificado_de_aptitud_ambiental", + "falsificacion_certificado_medico", + "falsificacion_de_documento", + "falsificacion_de_marcas_firmas_señas_oficiales", + "falsificacion_de_numeracion_de_objeto_registrada_de_acuerdo_a_ley", + "falta_de_poliza_de_seguros", + "favorecimiento_evasion_persona_detenida", + "frustrar_subasta_publica", + "guardar_artefactos_pirotecnicos", + "guardar_elementos_aptos_violencia", + "habeas_corpus", + "habilitacion_en_infraccion", + "homicidio", + "hostigamiento", + "hostigamiento_digital", + "hurto", + "impedimento_de_contacto_de_menor_con_padre_no_conviviente", + "incendio_explosion_inundacion_con_peligro_para_bienes", + "incendios_y_otros_estragos", + "incitar_al_desorden", + "incitar_desorden", + "incumplimiento_de_plazos", + "incumplimiento_deberes_familiares", + "incumplimiento_deberes_funcionario_publico", + "incumplimiento_medidas_prevencion_sanidad", + "incumplimiento_perimetro_profundidad", + "incumplir_clausura", + "incumplir_obligaciones_legales", + "inducir_menor_a_mendigar", + "infraccion_reglamentos_seguridad", + "ingresar_artefactos_pirotecnicos", + "ingresar_consumir_bebidas_alcoholicas", + "ingresar_contra_derecho_admision", + "ingresar_sin_autorizacion_lugares_reservados", + "ingresar_sin_entrada", + "ingresar_sin_entrada_autorizacion", + "ingreso_a_domicilio_sin_autorizacion", + "inhumar_exhumar_profanar", + "intimidacion", + "juego_sin_autorizacion", + "juegos_de_azar_sin_autorizacion", + "lesiones", + "ley_451", + "licencia_vencida", + "maltrato", + "mantener_animal_domestico_espacios_inadecuados", + "mantenimiento_de_cercas_y_aceras", + "menoscabar_integridad_animal_domestico", + "mesnna_masnna", + "no_realizar_grabado_de_autopartes", + "no_respetar_carriles", + "no_respetar_senda_peatonal", + "obligacion_de_exhibir_cartel", + "obligacion_de_informar_prohibicion_de_fumar", + "obligacion_de_conservar", + "obstaculizar_ingreso_o_salida", + "obstruccion_de_inspeccion", + "obstruccion_de_via", + "obstruccion_de_via_publica", + "obstruccion_via_publica", + "obstruir_salida", + "ocupar_via_publica", + "oferta_demanda_sexo_espacio_publico", + "omitir_cuidados_animal_domestico", + "omitir_recaudos_de_cuidado_animal_domestico", + "omitir_recaudos_de_organizacion", + "omitir_recaudos_espectaculo_masivo", + "omitir_recaudos_organizacion_seguridad", + "organizar_explotar_juego", + "organizar_sin_autorizacion_juego", + "participar_competencias_velocidad_via_publica", + "pelear_en_lugar_publico", + "permiso_y_planos_de_obra", + "persona_no_habilitada", + "perturbar_ceremonias_religiosas_funebres", + "perturbar_filas_ingreso_no_respetar_vallado", + "placas_de_dominio", + "pornografia_infantil", + "portacion_de_arma", + "portar_armas_no_convencionales", + "presencia_menores_lugar_no_autorizado", + "presunta_comision_delito", + "presunta_contravencion", + "privacion_ilegitima_de_la_libertad", + "producir_avalanchas", + "promocion_o_facilitacion_de_prostitucion_de_personas", + "promover_comerciar_ofertar_juego", + "propaganda_discriminatoria", + "proteccion_animal", + "provocar_parcialidad", + "publicidad_en_lugares_no_habilitados_via_publica", + "regentear_o_administrar_casas_de_tolerancia", + "residuos_peligrosos", + "retencion_indebida", + "revender_entradas", + "robo", + "ruidos_molestos", + "ruidos_y_vibraciones", + "sancion_generica", + "servicios_de_seguridad_prohibiciones_incumplidas", + "servicios_de_seguridad_requisitos_incumplidos", + "suministrar_alcohol_menor", + "suministrar_bebidas_alcoholicas", + "suministrar_elementos_aptos_agresion", + "suministrar_material_pornografico", + "suministrar_objetos_peligrosos", + "suministrar_productos_farmaceuticos", + "suministro_de_medicamentos", + "suplantacion_identidad", + "suplantacion_digital_de_identidad", + "taxis_transportes_escolares_remises_sin_autorizacion", + "tenencia_de_arma", + "transporte_de_pasajeros", + "transporte_de_pasajeros_sin_habilitacion", + "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "usar_indebidamente_credencial", + "usar_indebidamente_espacio_publico", + "usar_indebidamente_armas", + "uso_de_documento_falso_o_adulterado", + "uso_indebido_del_espacio_publico", + "uso_o_exhibicion_de_franquicias", + "usurpacion", + "vehiculo_abandonado", + "vender_entradas_o_permitir_ingreso_exceso", + "vender_sustancias_medicinales_sin_receta", + "venta_o_consumo_de_bebidas_alcoholicas_fuera_del_horario", + "verificacion_tecnica", + "violacion_de_peaje", + "violacion_de_domicilio", + "violacion_de_secretos_y_de_la_privacidad", + "violar_clausura", + "violar_inhabilitacion_para_conducir", + "violar_reglamentacion_juego", + "zanjas_y_pozos_en_via_publica", + ], + "conducta_descripcion": [ + "agravada", + "agravadas_por_edad", + "agravadas_por_el_uso_de_armas", + "agravadas_por_uso_de_armas", + "agravadas_por_uso_de_armas_impropia", + "agravado", + "agravado_abuso_funcion", + "agravado_alevosia", + "agravado_causar_sufrimiento_otra_persona", + "agravado_concurso", + "agravado_familiar", + "agravado_insertar_datos_en_datos_personales", + "agravado_lugar_publico", + "agravado_mas_personas", + "agravado_medio_idoneo", + "agravado_menor_de_edad", + "agravado_miembro_fuerzas", + "agravado_monumentos", + "agravado_otro_delito", + "agravado_peligro_de_muerte", + "agravado_por_el_vinculo", + "agravado_por_espacio", + "agravado_por_ser_fuerza_de_seguridad", + "agravado_precio", + "agravado_relacion_de_pareja", + "agravado_superior_militar", + "agravado_violar_informacion_banco_de_datos", + "agravado_violar_sistemas_seguridad", + "agravado_violencia_de_genero", + "agravado_violencia_mujer", + "agravados", + "coactivas", + "coactivas_agravadas", + "coactivas_agravadas_por_uso_de_armas", + "comercio_de_plantas_para_producir_estupefacientes", + "con_escalamiento", + "conduccion_imprudente", + "culposas", + "culposo", + "de_fuego_uso_civil", + "de_guerra", + "destinado_a_acreditar_identidad_de_personas_habilitacion_o_titularidad", + "digital", + "digital_agravado_familiar", + "digital_agravado_jefe", + "digital_agravado_mas_de_una_persona", + "digital_agravado_menor_de_edad", + "digital_agravado_relacion_de_pareja", + "durante_espectaculo_deportivo", + "en_grandes_parques_o_espectaculos_masivos", + "en_riña", + "entrega_suministro_facilitacion", + "funcionario_publico", + "ganzua_llave", + "graves", + "graves_tentativa", + "informatica", + "ingreso_a_domicilio_sin_autorizacion", + "intimadacion_o_invocando_orden_superior", + "leves", + "leves_agravadas", + "muebles_transportables", + "no_denuncia_delito", + "ocasion_de_incendio", + "organismo_publico", + "para_produccion_o_tenencia_con_fines_de_comercializacion", + "por_despojo", + "por_obligacion_de_devolver", + "por_turbacion_de_la_posesion", + "productos_separados_suelo", + "publico_o_privado", + "seguido_de_muerte", + "simple", + "simples", + "sin_autorizacion", + "tarjeta_credito_debito", + "tenencia", + "tentativa", + "tentativa_agravado_alevosia", + "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "uso_de_tarjeta_o_datos", + "vehiculos", + ], + "detalle": [ + "audiencia_victima_ley26485", + "30_dias", + "45_dias", + "60_dias", + "90_dias", + "180_dias", + "abandono_vehiculo", + "absolucion", + "absolucion_desistimiento_fiscal", + "accesorias_legales", + "acepta_competencia_inhibitoria", + "acepta_cuestion_turno", + "aclaratoria", + "acta_de_comprobacion", + "acta_de_intimacion", + "acuerdo_de_pago_levanta_embargo", + "acumulacion_por_conexidad_y_ampliacion", + "admisibilidad", + "agente_encubierto", + "agente_revelador", + "agotamiento_de_la_pena", + "alimentos_provisorios", + "allanamiento", + "apelacion", + "apertura_extraccion_examen_datos", + "aprehension", + "arma", + "armas", + "arresto_domiciliario_deja_sin_efecto", + "asesor_tutelar", + "atipicidad_sobreseimiento", + "audiencia_acusado_ley26485", + "audiencia_acusado_difiere_sentencia", + "audiencia_preliminar", + "audiencia_victima_11bis_ley24660", + "bloqueo_acceso_al_dominio", + "cadena_de_custodia_de_elementos_secuestrados", + "camara_gesell", + "cambio_domicilio_prision_domiciliaria", + "captura", + "captura_deja_sin_efecto", + "caucion_real", + "cedula_de_notificacion", + "cese_de_actos_de_perturbacion_o_intimidacion", + "cese_de_medidas", + "cese_de_medidas_parcial", + "cierre_de_instancia", + "clausura", + "clausura_deja_sin_efecto", + "competencia_penal_juvenil", + "concede", + "concede_parcialmente", + "conocimiento_personal", + "consigna_policial", + "contienda_negativa", + "control", + "convalida_fallecimiento", + "convalida_inimputabilidad", + "convalidacion_otras_causales", + "copia_forense_celular", + "copias_de_capturas_de_pantalla_mensajes_y_llamados", + "cosa_juzgada_sobreseimiento", + "cuarto_intermedio", + "cuarto_intermedio_para_resolver", + "cumplimiento_de_regla_de_conducta_en_complejo_penitenciario", + "cumplimiento_de_reglas_de_conducta", + "declaracion_testimonial", + "declina_competencia", + "defensa", + "defensa_particular", + "defensa_querella", + "deja_sin_efecto_inhabilitacion_y_devuelve_licencia", + "deja_sin_efecto_revocacion", + "delitos_de_accion_publica", + "desistimiento_accion_instancia_privada_sobreseimiento", + "desistimiento_fiscal", + "destruccion_de_arma", + "detencion", + "detencion_captura_y/o_traslado", + "detencion_captura_y/o_traslado_deja_sin_efecto", + "detencion_secuestro_y_requisa", + "devolucion_acarreo_y_estadia", + "dictamen_fiscal", + "difiere_decision", + "difiere_fundamentos", + "difiere_resolucion", + "dispositivo_de_geoposicionamiento_mantiene_hasta_juicio", + "domiciliaria_con_vigilancia_electronica", + "egreso_del_pais", + "ejecucion_de_honorarios", + "embargo", + "embargo_deja_sin_efecto", + "estimulo_educativo", + "estupefacientes", + "estupefacientes_dinero", + "estupefacientes_telefono_dinero", + "estupefacientes_telefono_dinero_arma", + "estupefacientes_telefono_vehiculo", + "estupefacientes_vehiculo_dinero", + "excarcelacion", + "exclusion_del_hogar", + "exhorto", + "exhorto_diplomatico", + "exime_pago_multa", + "exime_reparacion_del_daño_y_pago_multa", + "eximicion_pago", + "eximicion_pauta_de_conducta", + "expulsion_art_64_a_ley_25871", + "extincion_accion_por_autocomposicion", + "extincion_accion_por_cumplimiento", + "extincion_accion_sobreseimiento", + "extincion_accion_sobreseimiento_por_fallecimiento", + "extincion_de_la_pena", + "extincion_sancion_por_cumplimiento", + "extrañamiento", + "falta_de_accion_sobreseimiento", + "falta_de_legitimacion_pasiva", + "falta_de_participacion", + "falta_de_presentacion", + "falta_de_requisitos_procedencia", + "fija_nueva_audiencia", + "fondo_de_reserva", + "habeas_corpus", + "hasta_agotar_investigacion", + "identidad_objeto_y_sujeto", + "imagenes_videos", + "incompetencia_material", + "incompetencia_por_conexidad", + "incompetencia_por_conexidad_subjetiva", + "incompetencia_territorial", + "indeterminacion_imputacion", + "informacion_y_apertura_de_antenas_llamadas", + "informe_cij", + "informe_mercado_libre", + "informe_ncmec", + "informe_previo", + "informes_llamadas_y_deteccion_de_celdas", + "inhabilitacion_para_conducir", + "inhabilitacion_para_conducir_deja_sin_efecto", + "inmovilizacion", + "interes_en_el_caso", + "internacion_involuntaria", + "interprete", + "interprete_y_defensa", + "interrogatorio_policial", + "intervencion_equipo_especializado", + "intervencion_previa", + "intervencion_red_social", + "intervencion_telefonica", + "intervencion_telefonica_con_ubicacion_de_celda_de_apertura", + "levantamiento_clausura_administrativa", + "levantamiento_secreto_bancario", + "levantamiento_secreto_fiscal", + "liberacion_aves", + "libertad_asistida_incorporacion", + "libertad_condicional_otorga", + "libertad_condicional_revoca", + "litispendencia", + "mantiene", + "mediacion", + "medidas_restrictivas", + "modifica_pauta_de_conducta", + "morigeracion_domiciliaria", + "no_convalida", + "no_homologa_absolucion", + "notificacion_via_mail", + "nulidad_incorporacion_prueba", + "otorga_plazo", + "otras_vias_idoneas", + "pago_de_multa_en_cuotas", + "pago_minimo_multa_sobreseimiento", + "pago_minimo_multa_y_reparacion_daño_64cp", + "paradero", + "paradero_deja_sin_efecto", + "parcial_requerimiento_de_juicio", + "parcial_requerimiento_de_juicio_falta_de_circunscripcion_temporal", + "parcial_requerimiento_de_juicio_falta_de_fundamentacion", + "parcial_requerimiento_de_juicio_por_introduccion_de_agravante", + "pedido_de_informe_banco", + "pedido_de_informe_facebook", + "pedido_de_informe_facebook_google", + "pedido_de_informe_facebook_microsoft", + "pedido_de_informe_google", + "pedido_de_informe_google_microsoft", + "pedido_de_informe_imgur_llc", + "pedido_de_informe_instagram", + "pedido_de_informe_microsoft", + "pedido_de_informe_skype", + "pedido_de_informe_telefonia", + "pedido_de_informe_whatsapp", + "pedido_informe_tiktok", + "pedido_prision_domiciliaria", + "pena_de_arresto_efectivo_cumplimiento", + "pena_de_arresto_efectivo_cumplimiento_compurgada", + "pena_de_arresto_en_suspenso", + "pena_de_inhabilitacion_especial", + "pena_de_multa_efectivo_cumplimiento", + "pena_de_multa_en_suspenso", + "pena_de_multa_sustituida_por_amonestacion", + "pena_de_multa_sustituida_por_clausura_se_tiene_por_cumplida", + "pena_de_multa_sustituida_por_trabajos_comunitarios", + "pena_de_prision_domiciliaria", + "pena_de_prision_efectivo_cumplimiento", + "pena_de_prision_efectivo_cumplimiento_compurgada", + "pena_de_prision_efectivo_cumplimiento_domiciliaria", + "pena_de_prision_en_suspenso", + "pena_de_tareas_comunitarias", + "pena_de_tareas_de_utilidad_publica", + "pericia", + "pericia_arma", + "pericia_examen_veterinario", + "peritaje_de_telefono", + "peritaje_ordenadores_y_o_telefonos", + "peritaje_telefono", + "perito", + "permiso_de_viaje", + "plazo_razonable_sobreseimiento", + "plazo_razonable_sobresimiento", + "pleito_con_una_parte", + "por_quince_dias_hasta_tomar_contacto_con_victima", + "prescripcion_sobreseimiento", + "prision_domiciliaria_mantiene", + "prision_domiciliaria_prorroga", + "procedimiento", + "procedimiento_administrativo_pendiente", + "prohibicion_compra_tenencia_registracion_de_armas", + "prohibicion_de_acercamiento", + "prohibicion_de_acercamiento_y_contacto", + "prohibicion_de_publicar_en_redes", + "prohibicion_de_publicar_en_redes_sobre", + "promocion_a_periodo_de_prueba_art_7_ley_24660", + "prorroga", + "prueba_ofrecida", + "publicacion_de_edictos", + "queja", + "querella", + "quita_puntos_de_licencia_de_conducir", + "realizacion", + "rebeldia", + "rebeldia_deja_sin_efecto", + "rebeldia_y_captura", + "rebeldia_y_paradero", + "rechaza_acuerdo", + "rechaza_competencia", + "regimen_de_comunicacion_provisoria", + "reincidencia", + "reintegra_puntos_de_licencia_de_conducir", + "remate_vehiculo", + "remision_a_justicia_civil", + "remision_camara_para_sorteo", + "reparacion_integral_del_daño_sobreseimiento", + "replica_de_arma_de_plastico", + "reposicion", + "reposicion_apelacion_subsidio", + "reprogramacion", + "requerido_por_infractor", + "requerimiento_de_juicio", + "requerimiento_de_juicio_falta_de_fundamentacion", + "requisa", + "restitucion_de_inmueble", + "revision_psicofisica_capacidad", + "revision_psiquiatrica_y_psicologica", + "revoca", + "revoca_condicionalidad", + "revocacion", + "rueda_de_reconocimiento", + "salidas_extraordinarias", + "salidas_transitorias", + "sancion_sustitutiva_prorroga", + "secuestro", + "sin_indicacion_plazo", + "sobreseimiento", + "solicita", + "solicitud_destruccion_de_estupefacientes", + "tareas_de_investigacion", + "telefono", + "temor_parcialidad_generico", + "tiene_por_desistido", + "tiene_por_no_computado_plazo", + "tiene_presente", + "tiene_presente_acuerdo", + "tobillera_refractaria", + "traslado_por_fuerza_publica", + "unifica_solicita_inhibitoria", + "unificacion_de_pena", + "vehiculo", + "vencimiento_plazo_ipp", + ], + "objeto_de_la_resolucion": [ + "acumulacion_de_caso", + "admisibilidad_amparo", + "admisibilidad_habeas_corpus", + "admisibilidad_prueba", + "apartamiento", + "archivo_fiscal", + "audiencia_de_conciliacion", + "beneficio_litigar_sin_gastos", + "caducidad_de_instancia", + "cuestion_de_competencia", + "decomiso", + "desistimiento_faltas", + "ejecucion_de_la_pena", + "excepcion", + "excusacion", + "extraccion_testimonios", + "juicio_abreviado", + "juicio_oral", + "juzgamiento_faltas", + "mediacion", + "medida_cautelar", + "medida_probatoria", + "mesa_de_dialogo", + "nulidad", + "prision_preventiva", + "prorroga_investigacion_penal_preparatoria", + "recurso", + "recusacion", + "regulacion_de_honorarios", + "remision_controladora", + "restablecimiento_de_contacto", + "restitucion_de_efectos", + "restitucion_de_inmueble", + "restitucion_efectos", + "suspension_del_proceso_a_prueba", + ], +} diff --git a/aymurai/transforms/entity_subcategories/usem.py b/aymurai/transforms/entity_subcategories/usem.py deleted file mode 100644 index c70df207..00000000 --- a/aymurai/transforms/entity_subcategories/usem.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -from hashlib import md5 -from copy import deepcopy - -import numpy as np -import tensorflow as tf - -from aymurai.logger import get_logger -from aymurai.meta.types import DataItem -from aymurai.models.usem.core import USEMQA -from aymurai.utils.download import download -from aymurai.utils.misc import is_url, get_element -from aymurai.meta.pipeline_interfaces import Transform - -from .utils import filter_by_category - -logger = get_logger(__name__) - - -class USEMSubcategorizer(Transform): - """ - Use USEM to retrieve subcategories - """ - - usem = USEMQA() - - def __init__( - self, - category: str, - subcategories_path: list[str], - response_embeddings_path: str, - device: str = "/cpu:0", - ): - """ - Sentence Similarity using USEM (Universal Sentence Encoder Multilingual QA) - - Args: - category (str): category to filter - subcategories_path (list[str]): path to subcategories - response_embeddings_path (str): path to response embeddings - device (str, optional): device to use. Defaults to "/cpu:0". - """ - self.category = category - - CACHE_PATH = os.path.join( - os.getenv("AYMURAI_CACHE_BASEPATH", "/resources/cache/aymurai"), - self.__name__, - ) - - logger.info(f"load usem options from {subcategories_path}") - if is_url(url := subcategories_path): - fname = md5(url.encode("utf-8")).hexdigest() - subcategories_path = f"{CACHE_PATH}/{fname}" - logger.info(f"downloading options on {subcategories_path}") - os.makedirs(CACHE_PATH, exist_ok=True) - subcategories_path = download(url, output=subcategories_path) - - with open(subcategories_path, "r") as file: - self.subcategories = file.read().splitlines() - logger.info(f"options head: {self.subcategories[:5]}") - - # download embeddings - if is_url(url := response_embeddings_path): - fname = md5(url.encode("utf-8")).hexdigest() - model_path = f"{CACHE_PATH}/{fname}" - logger.info(f"downloading embeddings on {model_path}") - os.makedirs(CACHE_PATH, exist_ok=True) - response_embeddings_path = download(url, output=model_path) - logger.info(f"load usem embeddings from {response_embeddings_path}") - self.usem_vectors = self.load_usem_vectors(response_embeddings_path) - self.device = device - - def load_usem_vectors(self, file_path): - """ - Load USEM vectors - """ - usem_vectors = np.load(file_path) - return usem_vectors.copy() - - def retrieve(self, text: str, top_k: int = 10) -> list[str]: - """ - Retrieve similar sentences using USEM - """ - with tf.device(self.device): - query_vector = self.usem.encode( - [text], - encoder_type="question_encoder", - ) - - products = np.inner(query_vector, self.usem_vectors)[0] - similar_idx = np.flip(products.argsort())[:top_k] - similar_sentences = [self.subcategories[idx] for idx in similar_idx] - - return similar_sentences - - def __call__(self, item: DataItem) -> DataItem: - """ - Retrieve subcategories for entities. If the entity is not in the category of interest, - the subcategory is not retrieved. - - Args: - item (DataItem): input data item - - Returns: - DataItem: output data item - """ - item = deepcopy(item) - - ents = get_element(item, levels=["predictions", "entities"]) or [] - - texts = list(map(lambda x: x["text"], ents)) - filtered_ents = filter_by_category(ents, self.category) - retrieved = list(map(self.retrieve, texts)) - - for ent, retrieved_ in zip(ents, retrieved): - if ent in filtered_ents and not get_element( - ent, levels=["attrs", "aymurai_label_subclass"] - ): - ent["attrs"]["aymurai_label_subclass"] = retrieved_ - - item["predictions"]["entities"] = ents - - return item diff --git a/aymurai/utils/alignment/core.py b/aymurai/utils/alignment/core.py index c8eeeed2..65a3004c 100644 --- a/aymurai/utils/alignment/core.py +++ b/aymurai/utils/alignment/core.py @@ -11,6 +11,15 @@ def tokenize(text: str) -> list[str]: + """ + Split multi-line text into whitespace-delimited tokens. + + Args: + text (str): Input text to tokenize. + + Returns: + list[str]: Tokens extracted from the text. + """ tokens = map(str.split, text.splitlines()) tokens = flatten(tokens) return list(tokens) @@ -21,15 +30,17 @@ def align_text( target_text: str, columns: tuple[str, str] = ("source", "target"), ) -> pd.DataFrame: - """align source and target text into a table + """ + Align source and target text into a token-level mapping table. Args: - source_text (str): reference text - target_text (str): second text to align with - columns (tuple[str, str]): names of columns on output + source_text (str): Reference text. + target_text (str): Text to align against `source_text`. + columns (tuple[str, str], optional): Output column names. + Defaults to ("source", "target"). Returns: - pd.DataFrame: alignment table + pd.DataFrame: Alignment table. """ source_tokens = [t.strip() for t in tokenize(source_text)] target_tokens = [t.strip() for t in tokenize(target_text)] @@ -78,15 +89,22 @@ def align_docs( columns: tuple[str, str] = ("source", "target"), source_preprocess: Callable[[str], str] | None = None, target_preprocess: Callable[[str], str] | None = None, -): - """align two documents word to word +) -> pd.DataFrame: + """ + Align two documents word by word. Args: - source_path (str | Path): source document path (reference) - target_path (str | Path): target document path (target) + source_path (str | Path): Source document path (reference). + target_path (str | Path): Target document path. + columns (tuple[str, str], optional): Output column names. + Defaults to ("source", "target"). + source_preprocess (Callable[[str], str] | None, optional): Preprocessing + function applied to extracted source text. Defaults to None. + target_preprocess (Callable[[str], str] | None, optional): Preprocessing + function applied to extracted target text. Defaults to None. Returns: - pd.DataFrame: alignment table + pd.DataFrame: Alignment table. """ source: str = extract_document(source_path, errors="raise") # type: ignore target: str = extract_document(target_path, errors="raise") # type: ignore @@ -140,6 +158,16 @@ def add_empty_lines_between_paragraphs( reference: str, mapping: pd.DataFrame, ) -> pd.DataFrame: + """ + Insert empty rows in an alignment table at paragraph break boundaries. + + Args: + reference (str): Reference text used to infer paragraph boundaries. + mapping (pd.DataFrame): Alignment table to augment. + + Returns: + pd.DataFrame: Alignment table with inserted empty rows. + """ mapping = mapping.copy() reference = re.sub(r"\n+", "\n", reference) splitted = reference.splitlines() diff --git a/aymurai/utils/alignment/ia2.py b/aymurai/utils/alignment/ia2.py index f94af1a3..68f7755a 100644 --- a/aymurai/utils/alignment/ia2.py +++ b/aymurai/utils/alignment/ia2.py @@ -1,5 +1,6 @@ import os import re +from typing import Any import numpy as np import pandas as pd @@ -7,12 +8,31 @@ def normalize(text: str) -> str: + """ + Normalize text by removing accents and non-word characters. + + Args: + text (str): Input text. + + Returns: + str: Normalized text. + """ text = unidecode(text) text = re.sub(r"\W", "", text) return text -def norm_ia2_label(text, labels: list[str]) -> str | float: +def norm_ia2_label(text: Any, labels: list[str]) -> str | float: + """ + Extract a single matching IA2 label from text. + + Args: + text (Any): Input text containing IA2 label content. + labels (list[str]): Valid label patterns to match. + + Returns: + str | float: The matched label when unique, otherwise `np.nan`. + """ if not isinstance(text, str): return np.nan @@ -27,6 +47,15 @@ def norm_ia2_label(text, labels: list[str]) -> str | float: def label_to_conll_format(labels: pd.Series) -> pd.Series: + """ + Convert label sequences to CONLL BIO tags. + + Args: + labels (pd.Series): Sequence of labels. + + Returns: + pd.Series: Labels converted to BIO format. + """ labels = labels.copy() if len(labels.dropna()) == 0: @@ -43,6 +72,15 @@ def label_to_conll_format(labels: pd.Series) -> pd.Series: def ia2_text_preprocess(text: str) -> str: + """ + Insert whitespace after IA2 XML-like tags when missing. + + Args: + text (str): Input text. + + Returns: + str: Preprocessed text. + """ text = re.sub(r"(<\w+>)(\S)", r"\g<1> \g<2>", text) return text @@ -52,7 +90,18 @@ def mapping2conll( filename: str, text_column: str = "original", label_column: str = "label", -): +) -> None: + """ + Write token-label mappings to a CONLL-style file. + + Args: + df (pd.DataFrame): DataFrame containing token and label columns. + filename (str): Output file path. + text_column (str, optional): Column name containing tokens. + Defaults to "original". + label_column (str, optional): Column name containing labels. + Defaults to "label". + """ dir = os.path.dirname(filename) os.makedirs(dir, exist_ok=True) with open(filename, "w") as file: diff --git a/aymurai/utils/cache.py b/aymurai/utils/cache.py index 48a4ae75..6d1928eb 100644 --- a/aymurai/utils/cache.py +++ b/aymurai/utils/cache.py @@ -1,13 +1,12 @@ -import os import json +import os import pickle -from typing import Any, Optional +from typing import Any -import joblib import diskcache +import joblib from aymurai.logger import get_logger -from aymurai.meta.types import DataItem from aymurai.utils.json_encoding import EnhancedJSONEncoder logger = get_logger(__name__) @@ -18,15 +17,15 @@ def flatten_dict(current: dict, key: str = "", result: dict = {}) -> dict: """ - Flatten a dict + Flatten nested dictionaries into a dotted-key mapping. Args: - current (dict): dict to be flattened - key (str, optional): key to be used. Defaults to "". - result (dict, optional): result dict. Defaults to {}. + current (dict): Source dictionary to flatten. + key (str, optional): Parent key prefix. Defaults to "". + result (dict, optional): Accumulator reused across recursion. Defaults to {}. Returns: - dict: flattened dict + dict: Mapping of flattened keys to terminal values. """ if type(current) is dict: for k in current: @@ -37,12 +36,12 @@ def flatten_dict(current: dict, key: str = "", result: dict = {}) -> dict: return result -def cache_clear(keys: list[str]): +def cache_clear(keys: list[str]) -> None: """ - Clear cache + Remove the provided keys from the disk-backed cache. Args: - keys (list[str]): keys to be cleared + keys (list[str]): Cache keys to delete. """ for key in keys: cache.pop(key) @@ -50,14 +49,15 @@ def cache_clear(keys: list[str]): def get_cache_key(item: Any, context: Any = "") -> str: """ - Get cache key + Build a stable cache key combining the item payload and context. Args: - item (Any): Data to hash - context (Any): context object to create hash + item (Any): Value to hash into the key namespace. + context (Any, optional): Additional context used to scope the key. + Defaults to "". Returns: - str: hash + str: Deterministic hash suitable for cache lookups. """ if type(item) in [dict]: @@ -72,20 +72,26 @@ def get_cache_key(item: Any, context: Any = "") -> str: return cache_key -def is_cached(key: str): +def is_cached(key: str) -> bool: + """ + Determine whether a cache entry exists for the given key. + + Args: + key (str): Cache key to inspect. + + Returns: + bool: True when the key is present, otherwise False. + """ return key in cache -def cache_save( - data_item: DataItem, - key: str, -): +def cache_save(data_item: Any, key: str) -> None: """ - save data on cache + Persist a Python object in the disk-backed cache. Args: - data (Data): data to be cached - key (str): key to store + data_item (Any): Serializable payload to store. + key (str): Cache key under which the payload is saved. """ data = pickle.dumps(data_item) @@ -94,18 +100,16 @@ def cache_save( cache.set(key, data) -def cache_load(key: str) -> Optional[DataItem]: +def cache_load(key: str) -> Any | None: """ - load data from cache + Retrieve and deserialize a cached payload when present. Args: - key (str): key to load + key (str): Cache key to resolve. Returns: - Optional[Data]: loaded data - + Any | None: Cached payload on hit, otherwise ``None``. """ - if key in cache: # Retrieve the serialized object from cache data = cache.get(key) diff --git a/aymurai/utils/display/pandas.py b/aymurai/utils/display/pandas.py index 2f83742b..d2abc054 100644 --- a/aymurai/utils/display/pandas.py +++ b/aymurai/utils/display/pandas.py @@ -1,7 +1,18 @@ +from typing import ContextManager + import pandas as pd from more_itertools import flatten -def pandas_context(**kwargs): +def pandas_context(**kwargs) -> ContextManager[None]: + """ + Build a pandas option context manager from keyword options. + + Args: + **kwargs: Pandas option names mapped to values. + + Returns: + ContextManager[None]: Context manager that applies the provided options. + """ options = flatten(kwargs.items()) return pd.option_context(*options) # type: ignore diff --git a/aymurai/utils/download.py b/aymurai/utils/download.py index 17151f32..52d2a7f3 100644 --- a/aymurai/utils/download.py +++ b/aymurai/utils/download.py @@ -1,6 +1,9 @@ import os +import shutil +import tempfile +from pathlib import Path -import gdown +import requests from aymurai.logger import get_logger @@ -9,30 +12,39 @@ logger = get_logger(__name__) -def download(url, output): +def download(url: str, output: str) -> str: """ - Download file from url - skip if file exists and environment variable NO_DOWNLOAD_IF_EXISTS is set to True + Stream download to a file. + + Skips download when the target exists and NO_DOWNLOAD_IF_EXISTS is truthy. Args: - url (str): url to download - output (str): output path + url (str): URL to download. + output (str): Path to save the downloaded file. Returns: - str: output path + str: Path to the downloaded file. + + Raises: + requests.HTTPError: If the download request returns an HTTP error status. """ - if os.path.exists(output) and bool(NO_DOWNLOAD_IF_EXISTS): - logger.warn( + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if output_path.exists() and NO_DOWNLOAD_IF_EXISTS: + logger.warning( "File found and skipping. Set NO_DOWNLOAD_IF_EXISTS environment to false to force download." ) - return output - - gdown.download( - url, - quiet=False, - fuzzy=True, - resume=True, - output=output, - ) - return output + return str(output_path) + + logger.info(f"Downloading {url} -> {output_path}") + with requests.get(url, stream=True, allow_redirects=True, timeout=60) as resp: + resp.raise_for_status() + with tempfile.NamedTemporaryFile(delete=False) as tmp: + for chunk in resp.iter_content(chunk_size=8192): + if chunk: + tmp.write(chunk) + tmp_path = Path(tmp.name) + shutil.move(tmp_path, output_path) + return str(output_path) diff --git a/aymurai/utils/entity_disambiguation/__init__.py b/aymurai/utils/entity_disambiguation/__init__.py new file mode 100644 index 00000000..1cfebd3d --- /dev/null +++ b/aymurai/utils/entity_disambiguation/__init__.py @@ -0,0 +1,16 @@ +from aymurai.utils.entity_disambiguation.core import ( + assign_label_instances, + map_canonical_entities_ner_preds, +) +from aymurai.utils.entity_disambiguation.fuzzy import ( + build_canonical_entities, +) + +from aymurai.utils.entity_disambiguation.date_formatter import get_canonical_dates + +__all__ = [ + "assign_label_instances", + "build_canonical_entities", + "map_canonical_entities_ner_preds", + "get_canonical_dates", +] diff --git a/aymurai/utils/entity_disambiguation/core.py b/aymurai/utils/entity_disambiguation/core.py new file mode 100644 index 00000000..ffa156fc --- /dev/null +++ b/aymurai/utils/entity_disambiguation/core.py @@ -0,0 +1,108 @@ +import copy +import uuid + +from aymurai.meta.api_interfaces import DocumentAnnotations +from aymurai.meta.entities import CanonicalEntities + + +def assign_label_instances( + predictions: DocumentAnnotations, +) -> DocumentAnnotations: + """ + Assigns ordered label instance indices (e.g., 1, 2) by appearance. + + Args: + predictions (DocumentAnnotations): Predictions with canonical IDs assigned. + + Returns: + DocumentAnnotations: Updated predictions with `aymurai_label_instance`. + """ + predictions_with_instances = copy.deepcopy(predictions) + + next_index_by_label: dict[str, int] = {} + instance_by_label_and_id: dict[tuple[str, str], int] = {} + + for document in predictions_with_instances: + if not document.labels: + continue + + for label in document.labels: + label_name = label.attrs.aymurai_label + entity_id = label.attrs.canonical_entity_id + + if not label_name or entity_id is None: + continue + + key = (label_name, str(entity_id)) + if key not in instance_by_label_and_id: + next_index_by_label[label_name] = ( + next_index_by_label.get(label_name, 0) + 1 + ) + instance_by_label_and_id[key] = next_index_by_label[label_name] + + label.attrs.aymurai_label_instance = instance_by_label_and_id[key] + + return predictions_with_instances + + +def map_canonical_entities_ner_preds( + predictions: DocumentAnnotations, + canonical_entities: CanonicalEntities, + *, + include_label_instances: bool = True, +) -> DocumentAnnotations: + """ + Applies canonical entity IDs and roles back onto NER predictions. + + Args: + predictions (DocumentAnnotations): Original predictions to update. + canonical_entities (CanonicalEntities): Canonical entities with IDs/roles. + include_label_instances (bool, optional): Whether to assign ordered label + instance indices (e.g., 1, 2). Defaults to True. + + Returns: + DocumentAnnotations: Updated predictions with canonical IDs, roles, and + optionally `aymurai_label_instance`. + """ + predictions_mapped = copy.deepcopy(predictions) + + new_ids_map = {} + + for document in predictions_mapped: + if not document.labels: + continue + + for label in document.labels: + if not label.attrs: + continue + + for ce in canonical_entities: + if label.attrs.aymurai_label == ce.aymurai_label: + entity_id = ce.entity_id + aliases = ce.aliases + + clean_aliases = [str(a).strip().lower() for a in aliases] + label_text = ( + str(label.attrs.aymurai_alt_text or label.text).strip().lower() + ) + + if label_text in clean_aliases: + label.attrs.canonical_entity_id = entity_id + break + + if label.attrs.canonical_entity_id is not None: + continue + + key = ( + label.attrs.aymurai_label, + str(label.attrs.aymurai_alt_text).strip(), + ) + if key not in new_ids_map: + new_ids_map[key] = uuid.uuid4() + + label.attrs.canonical_entity_id = new_ids_map[key] + + if include_label_instances: + return assign_label_instances(predictions_mapped) + + return predictions_mapped diff --git a/aymurai/utils/entity_disambiguation/date_formatter.py b/aymurai/utils/entity_disambiguation/date_formatter.py new file mode 100644 index 00000000..23196409 --- /dev/null +++ b/aymurai/utils/entity_disambiguation/date_formatter.py @@ -0,0 +1,55 @@ +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import CanonicalEntity +import uuid + + +def get_canonical_dates(labels: list[DocLabel]) -> list[CanonicalEntity]: + """ + Groups date labels by their normalized day and month (if available) to create canonical entities. + + Args: + labels (list[DocLabel]): A list of document labels to process. + + Returns: + list[CanonicalEntity]: A list of canonical entities representing unique dates, + each with its aliases and attributes. + """ + groups = {} + + for label in labels: + if label.attrs.aymurai_label != "FECHA": + continue + + raw_date = label.attrs.aymurai_alt_text or label.text + norm_date = ( + max(label.attrs.aymurai_label_subclass) + if label.attrs.aymurai_label_subclass + else None + ) + + if norm_date: + parts = norm_date.split("/") + day = parts[0] + month = parts[1] + year = parts[2] if len(parts) > 2 else "1900" + if year == "1900": + day_month_key = f"{day}/{month}" + else: + day_month_key = f"{day}/{month}/{year}" + else: + day_month_key = uuid.uuid4() + + if day_month_key not in groups: + groups[day_month_key] = CanonicalEntity( + aymurai_label="FECHA", + canonical_text=raw_date, + aliases=[], + attributes={}, + ) + + if raw_date not in groups[day_month_key].aliases: + groups[day_month_key].aliases.append(raw_date) + + label.attrs.canonical_entity_id = groups[day_month_key].entity_id + + return list(groups.values()) diff --git a/aymurai/utils/entity_disambiguation/fuzzy.py b/aymurai/utils/entity_disambiguation/fuzzy.py new file mode 100644 index 00000000..e55850d1 --- /dev/null +++ b/aymurai/utils/entity_disambiguation/fuzzy.py @@ -0,0 +1,227 @@ +from collections import Counter +from typing import Iterable + +from rapidfuzz import process +from rapidfuzz.fuzz import token_set_ratio + +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import CanonicalEntity + +from aymurai.transforms.anonymization_postprocess.exact_labels import EXACT_LABELS + + +def _find_parent(parent: list[int], idx: int) -> int: + """ + Finds the root representative of a union-find set with path compression. + + Args: + parent (list[int]): Parent pointers for the union-find structure. + idx (int): Index to resolve to its root. + + Returns: + int: The root index for the provided `idx`. + """ + if parent[idx] == idx: + return idx + parent[idx] = _find_parent(parent, parent[idx]) + return parent[idx] + + +def _union_parent(parent: list[int], left: int, right: int) -> None: + """ + Unions two indices in a union-find structure. + + Args: + parent (list[int]): Parent pointers for the union-find structure. + left (int): First index to union. + right (int): Second index to union. + """ + root_left, root_right = _find_parent(parent, left), _find_parent(parent, right) + if root_left != root_right: + parent[root_right] = root_left + + +def _cluster_aliases_with_cdist( + *, + items: list[dict[str, str]], + threshold: int, +) -> list[list[tuple[str, str, str]]]: + """ + Clusters alias items using RapidFuzz pairwise similarity with union-find. + + Args: + items (list[dict[str, str]]): Items containing "text" and "aymurai_label". + threshold (int): Minimum similarity threshold for clustering. + + Returns: + list[list[tuple[str, str, str]]]: Clusters of (original, normalized, label). + """ + if not items: + return [] + + entities = [item.get("text", "") for item in items] + labels = [item.get("aymurai_label", "UNKNOWN") for item in items] + + normed = [str(e).lower().strip() if e else "" for e in entities] + + sim = process.cdist(normed, normed, scorer=token_set_ratio, score_cutoff=threshold) + + parent = list(range(len(normed))) + + n = len(normed) + for i in range(n): + for j in range(i + 1, n): + if sim[i][j] >= threshold: + _union_parent(parent, i, j) + + clusters_map: dict[int, list[tuple[str, str, str]]] = {} + for idx in range(n): + root = _find_parent(parent, idx) + clusters_map.setdefault(root, []).append( + (entities[idx], normed[idx], labels[idx]) + ) + + return list(clusters_map.values()) + + +def _parse_cluster_item(item: tuple[str, str, str]) -> tuple[str, str, str]: + """ + Reorders a tuple from (original, normalized, label) to (label, original, normalized). + + Args: + item (tuple[str, str, str]): Tuple in the form (original, normalized, label). + + Returns: + tuple[str, str, str]: Tuple in the form (label, original, normalized). + """ + orig, norm, label = item + return label, orig, norm + + +def _pick_cluster_label(parsed_items: list[tuple[str, str, str]]) -> str: + """ + Selects the most frequent label from a cluster. + + Args: + parsed_items (list[tuple[str, str, str]]): Items in (label, original, normalized). + + Returns: + str: The most common label in the cluster. + """ + labels = [label for label, _, _ in parsed_items] + return Counter(labels).most_common(1)[0][0] + + +def _pick_longest_alias(parsed_items: list[tuple[str, str, str]]) -> str: + """ + Picks a canonical text representation from a cluster. + + Args: + parsed_items (list[tuple[str, str, str]]): Items in (label, original, normalized). + + Returns: + str: The longest alias text in the cluster. + """ + return max(parsed_items, key=lambda item: len(item[1]))[1] + + +def _clusters_to_canonical_entities( + clusters: list[list[tuple[str, str, str]]], +) -> list[CanonicalEntity]: + """ + Converts clustered alias groups into CanonicalEntity objects. + + Args: + clusters (list[list[tuple[str, str, str]]]): Clusters of + (original, normalized, label). + + Returns: + list[CanonicalEntity]: Canonical entities derived from clusters. + """ + canonical_entities = [] + for cluster in clusters: + parsed = [_parse_cluster_item(item) for item in cluster] + label = _pick_cluster_label(parsed) + canonical_text = _pick_longest_alias(parsed) + aliases = sorted({orig for _, orig, _ in parsed}) + canonical_entities.append( + CanonicalEntity( + aymurai_label=label, + canonical_text=canonical_text, + aliases=aliases, + attributes={}, + ) + ) + return canonical_entities + + +def build_canonical_entities( + labels: Iterable[DocLabel], + *, + target_labels: set[str] | None = None, + threshold: int, +) -> list[CanonicalEntity]: + """ + Builds canonical entities by clustering label aliases per entity type. + + Args: + labels (Iterable[DocLabel]): NER labels to cluster. + target_labels (set[str] | None, optional): Label filter; if provided, + only these labels are clustered. Defaults to None. + threshold (int): Minimum similarity threshold for clustering. + + Returns: + list[CanonicalEntity]: Canonical entities built from clustered aliases. + """ + grouped: dict[str, list[dict[str, str]]] = {} + for label in labels: + attrs = label.attrs + if not attrs or not attrs.aymurai_label: + continue + if target_labels and attrs.aymurai_label not in target_labels: + continue + alias = attrs.aymurai_alt_text or label.text + + subclass_val = getattr(attrs, "aymurai_label_subclass", None) + + if isinstance(subclass_val, list): + exact_alias = subclass_val[-1] if subclass_val else alias + else: + exact_alias = subclass_val or alias + + grouped.setdefault(attrs.aymurai_label, []).append( + { + "text": alias, + "aymurai_label": attrs.aymurai_label, + "exact_alias": exact_alias, + } + ) + + canonical_entities: list[CanonicalEntity] = [] + for label_type, items in grouped.items(): + if label_type in EXACT_LABELS: + exact_groups = {} + for item in items: + exact_groups.setdefault(item["exact_alias"], []).append(item) + clusters = [ + [ + ( + item["text"], + str(item["exact_alias"]).lower().strip(), + item["aymurai_label"], + ) + for item in group_items + ] + for group_items in exact_groups.values() + ] + else: + clusters = _cluster_aliases_with_cdist( + items=items, + threshold=threshold, + ) + + canonical_entities.extend(_clusters_to_canonical_entities(clusters)) + + canonical_entities = sorted(canonical_entities, key=lambda x: x.canonical_text) + + return canonical_entities diff --git a/aymurai/utils/json_data.py b/aymurai/utils/json_data.py index a28db543..b69bf5d6 100644 --- a/aymurai/utils/json_data.py +++ b/aymurai/utils/json_data.py @@ -1,20 +1,80 @@ import json +from datetime import date, datetime from itertools import groupby -from typing import Union, Iterable, Iterator +from typing import Any, Iterable, Iterator -def save_json(json_data: Union[dict, list[dict]], file_path: str): +def json_serial(obj: Any) -> str: + """ + JSON serializer for objects not serializable by default JSON encoder. + + Args: + obj (Any): The object to serialize. This function currently supports + datetime.date and datetime.datetime objects. + + Returns: + str: The ISO 8601 formatted string representation of the date or datetime object. + + Raises: + TypeError: If the object is not of type datetime.date or datetime.datetime. + """ + if isinstance(obj, (datetime, date)): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + +def get_pretty(obj: dict | list[Any]) -> str: + """ + Returns a pretty json string. + + Args: + obj (dict | list[Any]): The object to be converted to JSON. + + Returns: + str: The pretty JSON string. + """ + return json.dumps(obj, indent=4, ensure_ascii=False, default=json_serial) + + +def save_json(json_data: dict | list[dict], file_path: str) -> None: + """ + Saves a JSON object to a file. + + Args: + json_data (dict | list[dict]): The JSON data to be saved. + file_path (str): The path to the file where the JSON data will be saved. + """ with open(file_path, "w") as f: - f.write(json.dumps(json_data, indent=4, ensure_ascii=False)) + f.write( + json.dumps(json_data, indent=4, ensure_ascii=False, default=json_serial) + ) + + +def load_json(json_file_path: str) -> dict | list[dict]: + """ + Loads a JSON object from a file. + Args: + json_file_path (str): The path to the JSON file. -def load_json(json_file_path: str) -> Union[dict, list[dict]]: + Returns: + dict | list[dict]: The loaded JSON object. + """ with open(json_file_path, "r") as f: content = json.loads(f.read()) return content def get_unique(json_list: list[dict]) -> Iterator[dict]: + """ + Get unique json objects from a list of json objects. + + Args: + json_list (list[dict]): The list of JSON objects. + + Returns: + Iterator[dict]: An iterator of unique JSON objects. + """ unique = set(map(json.dumps, json_list)) unique = map(json.loads, unique) return unique @@ -23,7 +83,18 @@ def get_unique(json_list: list[dict]) -> Iterator[dict]: def group_by_key( json_iter: Iterable[dict], group_key: str, sort_key: str = "" ) -> Iterator[list[dict]]: + """ + Groups a list of JSON objects by a specified key. + + Args: + json_iter (Iterable[dict]): An iterable of JSON objects to be grouped. + group_key (str): The key to group the JSON objects by. + sort_key (str, optional): An optional key to sort the JSON objects before grouping. + Defaults to "". + Returns: + Iterator[list[dict]]: An iterator of lists of JSON objects grouped by the specified key. + """ if sort_key: json_iter = sorted(json_iter, key=lambda x: x[sort_key]) diff --git a/aymurai/utils/json_encoding.py b/aymurai/utils/json_encoding.py index c4b2e930..5ff367a9 100644 --- a/aymurai/utils/json_encoding.py +++ b/aymurai/utils/json_encoding.py @@ -3,6 +3,7 @@ import json import decimal import datetime +from typing import Any import pandas as pd @@ -12,7 +13,22 @@ class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, obj): + """JSON encoder with support for datetime, timedelta, decimal, and NA values.""" + + def default(self, obj: Any) -> Any: + """ + Encode unsupported Python objects into JSON-serializable payloads. + + Args: + obj (Any): Object to encode. + + Returns: + Any: JSON-serializable representation of `obj`. + + Raises: + TypeError: If `obj` cannot be encoded by the custom logic or parent + JSON encoder. + """ if isinstance(obj, datetime.datetime): ARGS = ("year", "month", "day", "hour", "minute", "second", "microsecond") return { @@ -55,10 +71,28 @@ def default(self, obj): class EnhancedJSONDecoder(json.JSONDecoder): - def __init__(self, *args, **kwargs): + """JSON decoder that reconstructs objects serialized by `EnhancedJSONEncoder`.""" + + def __init__(self, *args, **kwargs) -> None: + """ + Initialize decoder with a custom object hook. + + Args: + *args: Positional arguments forwarded to `json.JSONDecoder`. + **kwargs: Keyword arguments forwarded to `json.JSONDecoder`. + """ super().__init__(*args, object_hook=self.object_hook, **kwargs) - def object_hook(self, d): + def object_hook(self, d: dict) -> Any: + """ + Decode typed payloads into native Python objects. + + Args: + d (dict): Decoded dictionary from JSON. + + Returns: + Any: Reconstructed object when `__type__` is present, else `d`. + """ if "__type__" not in d: return d o = sys.modules[__name__] diff --git a/aymurai/utils/misc.py b/aymurai/utils/misc.py index 286d30f4..6d665e80 100644 --- a/aymurai/utils/misc.py +++ b/aymurai/utils/misc.py @@ -1,25 +1,31 @@ import re -from typing import Any, Union +from typing import Any def get_element( - obj, - levels: Union[list, Any] = [], + obj: Any, + levels: list[Any] | Any = [], default: Any = None, *, ignore_errors: bool = True, -): +) -> Any: """ - retrieve element hierarchically + Retrieve an element from a nested object using hierarchical keys. Args: - obj (object): parent object to retrieve to - levels (Union[list, Any], optional): hierarchy levels. Defaults to []. - default (Any, optional): default value to return - ignore_errors (str, optional): raise errors or ignore them. Defaults to True. + obj (Any): Parent object to traverse. + levels (list[Any] | Any, optional): Hierarchy levels to access. Defaults to []. + default (Any, optional): Value returned when traversal fails and + `ignore_errors` is True. Defaults to None. + ignore_errors (bool, optional): Whether to suppress lookup errors. + Defaults to True. Returns: - _type_: element or None (in case child element doesnt exist and `ignore_errors=True`) + Any: Retrieved element, or `default` when traversal fails and + `ignore_errors` is True. + + Raises: + Exception: Propagates the underlying error when `ignore_errors` is False. """ # if levels not a list handle it has a key @@ -40,7 +46,16 @@ def get_element( raise -def is_url(text: str): +def is_url(text: str) -> bool: + """ + Check whether a string contains a URL-like pattern. + + Args: + text (str): Text to evaluate. + + Returns: + bool: True when a URL-like pattern is found, otherwise False. + """ match = re.findall( r"(http(s)?:\/\/.)(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)", text, @@ -49,11 +64,16 @@ def is_url(text: str): # Taken from https://stackoverflow.com/a/20254842 -def get_recursively(search_dict: dict, field: str) -> list: +def get_recursively(search_dict: dict, field: str) -> list[Any]: """ - Takes a dict with nested lists and dicts, - and searches all dicts for a key of the field - provided. + Search nested dictionaries and lists for values under a target key. + + Args: + search_dict (dict): Dictionary to search recursively. + field (str): Key name to collect. + + Returns: + list[Any]: Values found for `field` across the nested structure. """ fields_found = [] diff --git a/aymurai/utils/pickle_data.py b/aymurai/utils/pickle_data.py index 4d5ceebb..09169ab1 100644 --- a/aymurai/utils/pickle_data.py +++ b/aymurai/utils/pickle_data.py @@ -3,12 +3,28 @@ from typing import Any -def save_pickle(object_: Any, output_path: str): +def save_pickle(object_: Any, output_path: str) -> None: + """ + Serialize and save a Python object to a pickle file. + + Args: + object_ (Any): Object to serialize. + output_path (str): Output file path. + """ with open(output_path, "wb") as f: pickle.dump(object_, f) def load_pickle(input_path: str) -> Any: + """ + Load and deserialize a Python object from a pickle file. + + Args: + input_path (str): Input file path. + + Returns: + Any: Deserialized Python object. + """ with (open(input_path, "rb")) as f: object_ = pickle.load(f) return object_ diff --git a/aymurai/utils/yaml_data.py b/aymurai/utils/yaml_data.py new file mode 100644 index 00000000..97d30fbb --- /dev/null +++ b/aymurai/utils/yaml_data.py @@ -0,0 +1,28 @@ +import yaml + + +def load_yaml(file_path: str) -> dict: + """ + Loads a yaml file and returns its content as a dictionary. + + Args: + file_path (str): The path to the yaml file. + + Returns: + dict: The yaml file content. + """ + with open(file_path, "r") as f: + content = yaml.safe_load(f) + return content + + +def save_yaml(dict_data: dict, file_path: str) -> None: + """ + Saves a dictionary as a yaml file. + + Args: + dict_data (dict): The dictionary to be saved. + file_path (str): The path to the file where the yaml will be saved. + """ + with open(file_path, "w") as f: + f.write(yaml.dump(dict_data)) diff --git a/docker-compose.yml b/docker-compose.yml index 3ce7c536..0f7775ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,72 @@ +x-api-lite: &api-lite + image: ghcr.io/aymurai/api:latest + build: + context: . + dockerfile: ./docker/api/Dockerfile + target: aymurai-api + shm_size: "2gb" + ports: + - "8899:8899" + volumes: + - ./resources/cache:/resources/cache + env_file: + - .env + - .env.common + +x-api-full: &api-full + image: ghcr.io/aymurai/api:full + build: + context: . + dockerfile: ./docker/api/Dockerfile + target: aymurai-api-full + shm_size: "2gb" + ports: + - "8899:8899" + restart: always + deploy: + resources: + limits: + memory: 4g + cpus: "4.0" + services: aymurai-api: - image: ghcr.io/aymurai/api:latest - ports: - - "8899:8899" - build: - context: . - dockerfile: ./docker/api/Dockerfile - target: aymurai-api - env_file: - - .env - - .env.common - volumes: - - ./resources/cache:/resources/cache + <<: *api-lite + container_name: aymurai-api + environment: + TORCH_DEVICE: cpu + + aymurai-api-gpu: + <<: *api-lite + container_name: aymurai-api-gpu + environment: + TORCH_DEVICE: cuda + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] aymurai-api-full: - image: ghcr.io/aymurai/api:full - ports: - - "8899:8899" - build: - context: . - dockerfile: ./docker/api/Dockerfile - target: aymurai-api-full - restart: always + <<: *api-full + container_name: aymurai-api-full + environment: + TORCH_DEVICE: cpu + + aymurai-api-full-gpu: + <<: *api-full + container_name: aymurai-api-full-gpu + environment: + TORCH_DEVICE: cuda deploy: resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] limits: memory: 4g cpus: "4.0" diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 660e5a7e..bd492f09 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -19,7 +19,7 @@ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --extra runtime + uv sync --frozen --no-install-project # Copy application code COPY aymurai /app/aymurai @@ -73,7 +73,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=builder --chown=app:app /app/.venv /app/.venv # Copy static resources -COPY resources/api/static /resources/api/static COPY resources/pipelines /resources/pipelines # Set working directory @@ -94,7 +93,6 @@ ENV TF_CPP_MIN_LOG_LEVEL=3 \ RESOURCES_BASEPATH=resources # Copy additional resources -COPY ./resources/api resources/api COPY ./resources/pipelines/production resources/pipelines/production # Initialize application to download models diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6d7efff6..c9b462da 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,66 +1,78 @@ # Contributing to AymurAI -We are happy to accept your contributions to make `AymurAI` better and more awesome! To avoid unnecessary work on either -side, please stick to the following process: +We are happy to accept contributions that help make `AymurAI` more useful, more robust, and easier to maintain. +To avoid unnecessary work on either side, please use the following flow. -1. Check if there is already [an issue](https://github.com/AymurAI/dev/issues) for your concern. -2. If there is not, open a new one to start a discussion. We hate to close finished PRs! -3. If we decide your concern needs code changes, we would be happy to accept a pull request. Please consider the -commit guidelines below. +## Contribution flow +1. Check whether there is already an issue for your topic: +2. If not, open a new issue with context, motivation, and expected outcome. +3. Once the scope is clear, submit a pull request tied to that issue. -In case you just want to help out and don't know where to start, -[issues with "help wanted" label](https://github.com/AymurAI/dev/labels/help%20wanted) are good for -first-time contributors. +If you want to help and do not know where to start, small documentation fixes, test coverage improvements, and cleanup PRs are all welcome. +## Local development +If you want to get deeper into the API, we recommend cloning the repository and running the stack locally. +The codebase is fairly navigable, and most of the important modules are documented or organized by workflow. +### Option A: Docker (recommended) +You can use the provided compose services directly: -## Developing locally +```bash +make api-up +# or make api-full-up +``` -For contributors looking to get deeper into the API we suggest cloning the repository. -Nearly all classes and methods are documented, so finding your way around -the code should hopefully be easy. +Swagger UI: `http://localhost:8899/docs` -### Setup +If you prefer working from VS Code, the repository also includes a `.devcontainer/` setup. -#### Using Docker and devcontainer (recommended) -You can use the provided `devcontainer` load all the tools and packages needed. This can be done directly from Visual Studio Code. -You can check the [devcontainer documentation](https://code.visualstudio.com/docs/remote/containers) for more information. +### Option B: Local Python environment +Repository requires Python `3.10`. -#### Using jupyterlab image -If you want just check the notebooks and tutorials you can use the `jupyterlab` docker image. To run the image in `gpu` mode run: -```bash -make jupyter-run -``` -alternatively you can run in `cpu` mode with: ```bash -make jupyter-run-cpu -``` - +# if using uv +uv sync --all-groups -#### Install direclty on a your python environment -create a python environment of your preference and run: -```bash -pip install src/aymurai +# fallback with pip +pip install -e . ``` -You may need to install redis or run it in a docker container. You can use the following command to run it in a docker container: -```bash -make redis-run -``` +For most contributors, Docker is the easiest way to get a working API with the expected runtime dependencies. + +## Pre-commit hooks +After installing dependencies, enable the hooks: -### Git pre-commit Hooks -After installing the dependencies, install `pre-commit` hooks via: ```bash pre-commit install ``` -This will automatically run code formatters black and isort for each git commit. Also it will clear all outputs from the notebooks. If you want to more information about why we do this, please refer to the [data security](docs/DATA_SECURITY.md) section. +Configured hooks currently include: +- `ruff` +- `ruff-format` +- `nbstripout` +This helps keep code formatting consistent and prevents notebook output from leaking into commits. -### Code Formatting +## Formatting +If needed, you can run the formatter manually before committing: -To ensure a standardized code style we use the formatter [black](https://github.com/ambv/black) and for standardizing imports we use [isort](https://github.com/PyCQA/isort). -If your code is not formatted properly, the tests will fail. +```bash +ruff format aymurai/ +``` -If you set up pre-commit hooks, every git commit will automatically run these formatters. Otherwise you can also manually run them, or let your IDE run them on every file save. -Running from the command line works via `black src/aymurai/ && isort src/aymurai/` in the repository root folder. +## Documentation policy +When behavior changes in API, pipelines, or DB persistence, update the corresponding docs in the same PR: +- `README.md` +- `docs/api/README.md` +- `docs/pipelines/README.md` +- `docs/pipelines/anonymizer/README.md` +- `docs/pipelines/datapublic/README.md` +- `docs/database/README.md` +- `docs/entities/README.md` +- `docs/models/README.md` + +If the change is user-facing, documentation should land together with the code. + +## Community and security +- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) +- Security and ethics: [SECURITY.md](SECURITY.md) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..5a05e53e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# Documentation Index +Language: **English** | [Español](es/README.md) + +This index covers operational documentation for `v1.5.0`. + +## Core docs +- Repository entrypoint: [../README.md](../README.md) +- API reference: [api/README.md](api/README.md) +- Pipelines index: [pipelines/README.md](pipelines/README.md) +- Anonymizer flow: [pipelines/anonymizer/README.md](pipelines/anonymizer/README.md) +- Datapublic flow: [pipelines/datapublic/README.md](pipelines/datapublic/README.md) +- Internal database schema: [database/README.md](database/README.md) + +## Entities +- Entities index: [entities/README.md](entities/README.md) +- Datapublic entities: [entities/datapublic/README.md](entities/datapublic/README.md) +- Anonymizer entities: [entities/anonymizer/README.md](entities/anonymizer/README.md) + +## Models +- Models index: [models/README.md](models/README.md) +- Anonymizer NER model card: [models/anonymizer-model-card.md](models/anonymizer-model-card.md) +- Flair NER model card: [models/flair-model-card.md](models/flair-model-card.md) +- Decision model card: [models/decision-model-card.md](models/decision-model-card.md) + +## Governance +- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) +- Security and ethics: [SECURITY.md](SECURITY.md) +- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 7e95a465..72aa9d8a 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -11,4 +11,4 @@ AymurAI is guided by a strong ethical position that considers the complexities o We acknowledge that every line of code that we write may potentially contain security issues. We are trying to deal with it responsibly and provide patches as quickly as possible. ## Reporting a Vulnerability -If you have found a security vulnerability, please open a new [issue](https://github.com/AymurAI/dev/issues/new) or report it to us by sending an [email](aymurai@datagenero.org). We will respond to you as soon as possible. \ No newline at end of file +If you have found a security vulnerability, please open a new [issue](https://github.com/AymurAI/backend/issues/new) or report it to us by sending an [email](mailto:aymurai@datagenero.org). We will respond to you as soon as possible. diff --git a/docs/anonymization/pipeline.excalidraw b/docs/anonymization/pipeline.excalidraw new file mode 100644 index 00000000..358c538e --- /dev/null +++ b/docs/anonymization/pipeline.excalidraw @@ -0,0 +1,3892 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "mprOUEQImobIiF7TwmSu5", + "type": "rectangle", + "x": 323.33333333333337, + "y": 212.14179104477614, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1252894395, + "version": 114, + "versionNonce": 929231579, + "isDeleted": false, + "boundElements": [ + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow" + } + ], + "updated": 1764873895187, + "link": null, + "locked": false + }, + { + "id": "j4e8O-DqInLIe6FPmm6uf", + "type": "line", + "x": 366.7885572139304, + "y": 252.37810945273628, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a1", + "roundness": { + "type": 2 + }, + "seed": 1993017845, + "version": 82, + "versionNonce": 612513819, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "VvUrRcwOayuSCWD_BEIZ-", + "type": "line", + "x": 362.4084692677094, + "y": 279.52565900841427, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a2", + "roundness": { + "type": 2 + }, + "seed": 1917498107, + "version": 185, + "versionNonce": 1834646715, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "XF0GuSC8OHCflsYFqZIj-", + "type": "line", + "x": 362.95503103842134, + "y": 309.29823770215256, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a3", + "roundness": { + "type": 2 + }, + "seed": 954781173, + "version": 215, + "versionNonce": 1208036699, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ePQ9UG1U9o78Pyz1DIpJG", + "type": "text", + "x": 376.4452736318408, + "y": 538.8606965174129, + "width": 82.85454631444826, + "height": 80.47263681592038, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a4", + "roundness": null, + "seed": 899224181, + "version": 96, + "versionNonce": 1204341243, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "text": ".docx\n.pdf", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "50Qya96m5ryhBOeNuo5sd", + "type": "line", + "x": 361.96019900497515, + "y": 337.67910447761193, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a5", + "roundness": { + "type": 2 + }, + "seed": 1599168565, + "version": 113, + "versionNonce": 1907610267, + "isDeleted": false, + "boundElements": null, + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "euPm7o0scLf0nKXvH2Mi-", + "type": "line", + "x": 369.12473982912934, + "y": 364.5779783514023, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a6", + "roundness": { + "type": 2 + }, + "seed": 1599660891, + "version": 147, + "versionNonce": 108316475, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "RTz0Ti8oU16rW7TBEfaWk", + "type": "line", + "x": 369.4772828409415, + "y": 390.08391000778613, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a7", + "roundness": { + "type": 2 + }, + "seed": 1240160981, + "version": 253, + "versionNonce": 208545755, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ovlQj2YD-86eShmKhZsbR", + "type": "line", + "x": 368.4824508074953, + "y": 418.4647767832455, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a8", + "roundness": { + "type": 2 + }, + "seed": 1975728181, + "version": 151, + "versionNonce": 431125627, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EtWsq0p_WPVrVIpbECqaR", + "type": "line", + "x": 375.6469916316496, + "y": 445.36365065703586, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6mIBkNFpjb5VS1uhyvbNS" + ], + "frameId": null, + "index": "a9", + "roundness": { + "type": 2 + }, + "seed": 158886293, + "version": 185, + "versionNonce": 1577331995, + "isDeleted": false, + "boundElements": [], + "updated": 1764873895188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow", + "x": 544.801292623903, + "y": 335.6285710087246, + "width": 276.37331055069967, + "height": 0.9929830550302654, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": { + "type": 2 + }, + "seed": 910188603, + "version": 458, + "versionNonce": 376827547, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "2tskJJoi5g4IGXcOp3rfQ" + } + ], + "updated": 1764875808259, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 276.37331055069967, + -0.9929830550302654 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "mprOUEQImobIiF7TwmSu5", + "focus": -0.11485898447026521, + "gap": 5.801292623903009 + }, + "endBinding": { + "elementId": "R8DpmX_gFC_B_zQb3gs_d", + "focus": 0.058585250919652596, + "gap": 26.273809523809632 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "2tskJJoi5g4IGXcOp3rfQ", + "type": "text", + "x": 600.4880623401708, + "y": 310.1320794812094, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": null, + "seed": 773447163, + "version": 42, + "versionNonce": 491638229, + "isDeleted": false, + "boundElements": null, + "updated": 1764875807159, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "WOvaiSIhXDUuFwvEHM9bo", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "R8DpmX_gFC_B_zQb3gs_d", + "type": "text", + "x": 847.4484126984123, + "y": 224.08730158730157, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aE", + "roundness": null, + "seed": 890582459, + "version": 272, + "versionNonce": 1768355637, + "isDeleted": false, + "boundElements": [ + { + "id": "WOvaiSIhXDUuFwvEHM9bo", + "type": "arrow" + } + ], + "updated": 1764874730850, + "link": null, + "locked": false, + "text": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ]\n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ]\n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "9wZT6gadfEtFJsnK6a38y", + "type": "arrow", + "x": 1160.80487286459, + "y": 452.14285714285705, + "width": 348.62970232164366, + "height": 56.216370515816834, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aF", + "roundness": { + "type": 2 + }, + "seed": 1750164379, + "version": 998, + "versionNonce": 411091643, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "1kzq54A47HhfpjGS19GQs" + } + ], + "updated": 1764876692331, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 151.88163507191734, + 49.64285714285717 + ], + [ + 348.62970232164366, + 56.216370515816834 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.20762051337520257, + "gap": 22.16146671210572 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "fixedSegments": [ + { + "index": 2, + "start": [ + 0, + 123.33333333333314 + ], + "end": [ + 111.76666666666688, + 123.33333333333314 + ] + }, + { + "index": 3, + "start": [ + 111.76666666666688, + 123.33333333333314 + ], + "end": [ + 111.76666666666688, + 110.84286406975184 + ] + } + ], + "startIsSpecial": false, + "endIsSpecial": false + }, + { + "id": "1kzq54A47HhfpjGS19GQs", + "type": "text", + "x": 1208.1866528950034, + "y": 489.2857142857142, + "width": 208.9997100830078, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aFV", + "roundness": null, + "seed": 1033291995, + "version": 30, + "versionNonce": 40809307, + "isDeleted": false, + "boundElements": null, + "updated": 1764876692331, + "link": null, + "locked": false, + "text": "/anonymizer/predict", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9wZT6gadfEtFJsnK6a38y", + "originalText": "/anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XNFeqRFFVQOQ2Amv9KiKf", + "type": "rectangle", + "x": 1526.4365079365075, + "y": 256.86507936507934, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aH", + "roundness": { + "type": 3 + }, + "seed": 477388725, + "version": 215, + "versionNonce": 1294039099, + "isDeleted": false, + "boundElements": [ + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow" + }, + { + "id": "X1lDBhgBqAfbBo2Wh3tA1", + "type": "arrow" + }, + { + "id": "9wZT6gadfEtFJsnK6a38y", + "type": "arrow" + } + ], + "updated": 1764874713403, + "link": null, + "locked": false + }, + { + "id": "DDpm2VgzVTuFYgzzSeDoT", + "type": "text", + "x": 1698.3106526268848, + "y": 697.4206349206348, + "width": 127.13986206054688, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aI", + "roundness": null, + "seed": 781671541, + "version": 183, + "versionNonce": 198505051, + "isDeleted": false, + "boundElements": [ + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow" + } + ], + "updated": 1764875154944, + "link": null, + "locked": false, + "text": "NER outputs", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "NER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow", + "x": 1759.2849840986328, + "y": 751.4206349206347, + "width": 5.191199117050928, + "height": 178.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": { + "type": 2 + }, + "seed": 1942697813, + "version": 481, + "versionNonce": 1156232859, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "i_EvQstHW-Ge2wvbLPeQ_" + } + ], + "updated": 1764875159518, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 5.191199117050928, + 178.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "DDpm2VgzVTuFYgzzSeDoT", + "focus": 0.05947616440110493, + "gap": 14 + }, + "endBinding": { + "elementId": "QMg1bW0iJLThqV1wPLnRu", + "focus": -0.39612694313383184, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "i_EvQstHW-Ge2wvbLPeQ_", + "type": "text", + "x": 1654.8938758278896, + "y": 786.9206349206347, + "width": 204.59686279296875, + "height": 70, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJG", + "roundness": null, + "seed": 1379805179, + "version": 123, + "versionNonce": 1293571995, + "isDeleted": false, + "boundElements": null, + "updated": 1764875154945, + "link": null, + "locked": false, + "text": "fuzzy matching\napproach", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "qJLbJGOfzrt9cwONItkng", + "originalText": "fuzzy matching approach", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "QMg1bW0iJLThqV1wPLnRu", + "type": "text", + "x": 1498.6190476190475, + "y": 929.9206349206347, + "width": 905.5525030525026, + "height": 372.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 101534011, + "version": 624, + "versionNonce": 869890651, + "isDeleted": false, + "boundElements": [ + { + "id": "qJLbJGOfzrt9cwONItkng", + "type": "arrow" + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + } + ], + "updated": 1764876718291, + "link": null, + "locked": false, + "text": "[\n {\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {}\n \n }\n]", + "fontSize": 22.923076923076923, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {}\n \n }\n]", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow", + "x": 1779.442822764371, + "y": 1253.9206349206347, + "width": 1.335206841328045, + "height": 212.22319297821105, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aM", + "roundness": { + "type": 2 + }, + "seed": 1658264629, + "version": 582, + "versionNonce": 1501063611, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "cPRb_-k-P_xgsphynZa-B" + } + ], + "updated": 1764876718291, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.335206841328045, + 212.22319297821105 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "QMg1bW0iJLThqV1wPLnRu", + "focus": 0.37688413952517746, + "gap": 14 + }, + "endBinding": { + "elementId": "HT4dCz_WP0No85IzXVlCJ", + "focus": 0.01747376853034287, + "gap": 14 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "cPRb_-k-P_xgsphynZa-B", + "type": "text", + "x": 1680.8807133568653, + "y": 1277.5159805629846, + "width": 186.99974060058594, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aMV", + "roundness": null, + "seed": 1547919451, + "version": 55, + "versionNonce": 979131387, + "isDeleted": false, + "boundElements": null, + "updated": 1764875143575, + "link": null, + "locked": false, + "text": "CanonicalEntities\n", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ZMKT-Hy2-Yjk8g0ssbD0q", + "originalText": "CanonicalEntities\n", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "HT4dCz_WP0No85IzXVlCJ", + "type": "diamond", + "x": 1572.2377265143011, + "y": 1491.9307081807083, + "width": 404.2857142857141, + "height": 299.9999999999999, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": { + "type": 2 + }, + "seed": 474391803, + "version": 219, + "versionNonce": 917939349, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "6BbvmS0QuJA92jEgUAM7-" + }, + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + }, + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow" + } + ], + "updated": 1764876727910, + "link": null, + "locked": false + }, + { + "id": "6BbvmS0QuJA92jEgUAM7-", + "type": "text", + "x": 1748.381054499792, + "y": 1624.4307081807083, + "width": 51.856201171875, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 827091643, + "version": 146, + "versionNonce": 459352021, + "isDeleted": false, + "boundElements": null, + "updated": 1764875170814, + "link": null, + "locked": false, + "text": "LLM", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "HT4dCz_WP0No85IzXVlCJ", + "originalText": "LLM", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "dHelCwKVUG5VTmm5vyDz_", + "type": "arrow", + "x": 2063.285201930389, + "y": 694.3135258107342, + "width": 474.8611910477862, + "height": 844.0773032721003, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aR", + "roundness": { + "type": 2 + }, + "seed": 2016671099, + "version": 642, + "versionNonce": 611520693, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GTYZaUFv22_vAQroP4Ots" + } + ], + "updated": 1764876728139, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 290.8894012442138, + 434.06705471007433 + ], + [ + -183.9717898035724, + 844.0773032721003 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.3128704896896401, + "gap": 28.317023962699135 + }, + "endBinding": { + "elementId": "HT4dCz_WP0No85IzXVlCJ", + "focus": -0.07412509117026206, + "gap": 24.87677840035615 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GTYZaUFv22_vAQroP4Ots", + "type": "text", + "x": 2323.4000670417904, + "y": 1024.213913854142, + "width": 261.549072265625, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aRV", + "roundness": null, + "seed": 1799358171, + "version": 29, + "versionNonce": 916759547, + "isDeleted": false, + "boundElements": null, + "updated": 1764873079510, + "link": null, + "locked": false, + "text": "Entities in context", + "fontSize": 28, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "dHelCwKVUG5VTmm5vyDz_", + "originalText": "Entities in context", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "fQd08AUXM4h1r1rt1GEYO", + "type": "arrow", + "x": 1531.7321526774883, + "y": 1643.1003241404883, + "width": 262.51194713959944, + "height": 2.3392319195597793, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aT", + "roundness": { + "type": 2 + }, + "seed": 860487771, + "version": 330, + "versionNonce": 363111093, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "NX_RkmyZ4ShoFSsJ4jTLw" + } + ], + "updated": 1764875170815, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -262.51194713959944, + -2.3392319195597793 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "NX_RkmyZ4ShoFSsJ4jTLw", + "type": "text", + "x": 576.9763088073955, + "y": 1091.9307081807083, + "width": 186.99974060058594, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU", + "roundness": null, + "seed": 410991867, + "version": 62, + "versionNonce": 810030421, + "isDeleted": false, + "boundElements": [], + "updated": 1764875170815, + "link": null, + "locked": false, + "text": "CanonicalEntities\n", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fQd08AUXM4h1r1rt1GEYO", + "originalText": "CanonicalEntities\n", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "IEM1W5vPfKwT3lgRgCi-R", + "type": "text", + "x": 694.7031440781441, + "y": 1395.8730158730161, + "width": 905.5525030525026, + "height": 487.1153846153846, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aW", + "roundness": null, + "seed": 202961365, + "version": 797, + "versionNonce": 518433403, + "isDeleted": false, + "boundElements": [ + { + "id": "ZMKT-Hy2-Yjk8g0ssbD0q", + "type": "arrow" + } + ], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "[\n {\n \"entity_id\": ,\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {\n \"role\": \"Acusado/a \n }\n \n },\n ...\n]", + "fontSize": 22.923076923076923, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"entity_id\": ,\n \"aymurai_label\": \"PER\",\n \"canonical_text\": \"MATIAS EZEQUIEL R.\",\n \"aliases\": [\n \"MATIAS EZEQUIEL R.\",\n \"R., MATÍAS EZEQUIEL\",\n \"Matías Ezequiel R.\"\n ],\n \"attributes\": {\n \"role\": \"Acusado/a \n }\n \n },\n ...\n]", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "X1lDBhgBqAfbBo2Wh3tA1", + "type": "arrow", + "x": 1522.927714874569, + "y": 690.6888562602353, + "width": 1016.253111699966, + "height": 1419.4587535518317, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aY", + "roundness": { + "type": 2 + }, + "seed": 1525774261, + "version": 379, + "versionNonce": 916502267, + "isDeleted": false, + "boundElements": null, + "updated": 1764874776673, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1016.253111699966, + 794.7674929461141 + ], + [ + -678.7729798846242, + 1419.4587535518317 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "XNFeqRFFVQOQ2Amv9KiKf", + "focus": -0.03022916805647842, + "gap": 17.297044401543584 + }, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "bLpxUpm3l_5AdE-wefxGf", + "type": "arrow", + "x": 1033.2850735335269, + "y": 1775.4563492063494, + "width": 0.08162462097448042, + "height": 270.3395345008357, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aZ", + "roundness": { + "type": 2 + }, + "seed": 1621637429, + "version": 264, + "versionNonce": 980985435, + "isDeleted": false, + "boundElements": null, + "updated": 1764876700324, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.08162462097448042, + 270.3395345008357 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "-PgdE-XG4rOV6rnyozIFX", + "focus": -0.010707089043139571, + "gap": 30.459273173000824 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "-PgdE-XG4rOV6rnyozIFX", + "type": "diamond", + "x": 762.8365384615385, + "y": 2065.4563492063494, + "width": 544.2857142857139, + "height": 342.49999999999994, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aa", + "roundness": { + "type": 2 + }, + "seed": 987028059, + "version": 397, + "versionNonce": 1724613083, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Xwf273IrzwAMLQrLCkCOu" + }, + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + }, + { + "id": "bLpxUpm3l_5AdE-wefxGf", + "type": "arrow" + } + ], + "updated": 1764876697912, + "link": null, + "locked": false + }, + { + "id": "Xwf273IrzwAMLQrLCkCOu", + "type": "text", + "x": 919.4074406047444, + "y": 2184.0813492063494, + "width": 231.0010528564453, + "height": 105, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ab", + "roundness": null, + "seed": 1570690811, + "version": 369, + "versionNonce": 1402964917, + "isDeleted": false, + "boundElements": [], + "updated": 1764875219055, + "link": null, + "locked": false, + "text": "Entity to\nCanonicalEntity\nmapping", + "fontSize": 28, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "-PgdE-XG4rOV6rnyozIFX", + "originalText": "Entity to CanonicalEntity mapping", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "YWHB-Nw2VxgFSkL-pcYdK", + "type": "text", + "x": 1035.5431163075204, + "y": 3213.0399522268453, + "width": 223.87255859375, + "height": 80.21844660194176, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "al", + "roundness": null, + "seed": 1428684437, + "version": 332, + "versionNonce": 1630622267, + "isDeleted": false, + "boundElements": [ + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow" + } + ], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "Disambiguated\nNER outputs", + "fontSize": 32.087378640776706, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Disambiguated\nNER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow", + "x": 1049.3582234833063, + "y": 2398.7390090918116, + "width": 56.8350216132967, + "height": 206.2823298539638, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "am", + "roundness": { + "type": 2 + }, + "seed": 479819349, + "version": 456, + "versionNonce": 528370965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875219055, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 56.8350216132967, + 206.2823298539638 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "-PgdE-XG4rOV6rnyozIFX", + "focus": 0.11433848854056756, + "gap": 3.6805732632926023 + }, + "endBinding": { + "elementId": "RCTW7-Twmc0vzPbjJZlva", + "focus": 0.012854192325188162, + "gap": 17.9562212017031 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow", + "x": 1149.7230307134546, + "y": 3304.34173216212, + "width": 0.0530047510710574, + "height": 179.03128371089633, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "an", + "roundness": { + "type": 2 + }, + "seed": 1544894267, + "version": 234, + "versionNonce": 63933787, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0530047510710574, + 179.03128371089633 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "YWHB-Nw2VxgFSkL-pcYdK", + "focus": -0.12996340741750428, + "gap": 11.08333333333303 + }, + "endBinding": { + "elementId": "htYeX7jml0k3AqG6W3Q_x", + "focus": -0.1090092112744266, + "gap": 22.666666666666515 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "htYeX7jml0k3AqG6W3Q_x", + "type": "image", + "x": 635.4793956043954, + "y": 3506.039682539683, + "width": 1024, + "height": 473, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ao", + "roundness": null, + "seed": 1128147605, + "version": 156, + "versionNonce": 748760533, + "isDeleted": false, + "boundElements": [ + { + "id": "j-ZCuk6tMAZF7uiWTtIk4", + "type": "arrow" + } + ], + "updated": 1764879496527, + "link": null, + "locked": false, + "status": "pending", + "fileId": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "a9YgxexRbVJcxPwYgreZn", + "type": "arrow", + "x": 1147.7941516061107, + "y": 4019.4978280481223, + "width": 2.5974080402759228, + "height": 247.43565838777295, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01", + "roundness": { + "type": 2 + }, + "seed": 1067879003, + "version": 921, + "versionNonce": 230655093, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "elej2TV9eJ1qTHpBpx-2H" + } + ], + "updated": 1764879496527, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.3147560017152955, + 139.37773758213234 + ], + [ + 2.2826520385606273, + 247.43565838777295 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "elej2TV9eJ1qTHpBpx-2H", + "type": "text", + "x": 1036.3792216542001, + "y": 4122.625565630255, + "width": 167.20034790039062, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01G", + "roundness": null, + "seed": 553249339, + "version": 35, + "versionNonce": 359281307, + "isDeleted": false, + "boundElements": null, + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "/anonymizer/anonymi\nze-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "a9YgxexRbVJcxPwYgreZn", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "h9PsSXNcnOUBkMSzBjg6z", + "type": "rectangle", + "x": 1052.940182148664, + "y": 4308.408197109689, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b08", + "roundness": { + "type": 3 + }, + "seed": 1300012667, + "version": 142, + "versionNonce": 1041008603, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false + }, + { + "id": "UieOrbihjMKgv6n7yxaDj", + "type": "line", + "x": 1096.3954060292608, + "y": 4348.64451551765, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b09", + "roundness": { + "type": 2 + }, + "seed": 720967451, + "version": 110, + "versionNonce": 1216664699, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "10owweBOlc7qCyP6aRJJV", + "type": "line", + "x": 1092.0153180830398, + "y": 4375.792065073328, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0A", + "roundness": { + "type": 2 + }, + "seed": 1604156347, + "version": 213, + "versionNonce": 1133153563, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "BQmhv6TG0JxjRxuwDfSMw", + "type": "line", + "x": 1092.561879853752, + "y": 4405.564643767066, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0B", + "roundness": { + "type": 2 + }, + "seed": 395070555, + "version": 243, + "versionNonce": 311439803, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "4Hm0xXA1DdypSLAR9BoaN", + "type": "text", + "x": 929.1926219227548, + "y": 4635.127102582326, + "width": 436.57354736328125, + "height": 40.2363184079602, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0C", + "roundness": null, + "seed": 1452804347, + "version": 162, + "versionNonce": 112738907, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "text": "documento_anonimizado.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "documento_anonimizado.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "5LyI3ky8p3RebAfVu1tA_", + "type": "line", + "x": 1091.5670478203056, + "y": 4433.945510542525, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0D", + "roundness": { + "type": 2 + }, + "seed": 37958043, + "version": 141, + "versionNonce": 685063931, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "OSeMl_IH1Pq_pZxGzfw_4", + "type": "line", + "x": 1098.7315886444599, + "y": 4460.844384416316, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0E", + "roundness": { + "type": 2 + }, + "seed": 734021179, + "version": 175, + "versionNonce": 1130997659, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "b4ihNsBPXcvY1lhr6-Ohh", + "type": "line", + "x": 1099.084131656272, + "y": 4486.3503160727, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0F", + "roundness": { + "type": 2 + }, + "seed": 435275483, + "version": 281, + "versionNonce": 749206587, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "mq_eSKsQgMs7MFsq2AJNk", + "type": "line", + "x": 1098.0892996228258, + "y": 4514.731182848159, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0G", + "roundness": { + "type": 2 + }, + "seed": 325727099, + "version": 179, + "versionNonce": 1349696731, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YN6yT1TNVtadNJ1X5Melr", + "type": "line", + "x": 1105.2538404469801, + "y": 4541.630056721949, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "SQGLLXLdvVhMNPoAROpAt" + ], + "frameId": null, + "index": "b0H", + "roundness": { + "type": 2 + }, + "seed": 1751911451, + "version": 213, + "versionNonce": 2052876667, + "isDeleted": false, + "boundElements": [], + "updated": 1764875186373, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "9G3g0qab8TQWTTo68WKEc", + "type": "text", + "x": 1536.304106606377, + "y": 329.64285714285705, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0L", + "roundness": null, + "seed": 915516597, + "version": 11, + "versionNonce": 672956187, + "isDeleted": false, + "boundElements": null, + "updated": 1764875143575, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "RCTW7-Twmc0vzPbjJZlva", + "type": "rectangle", + "x": 629.1746031746031, + "y": 2613.596856218215, + "width": 1090.833333333333, + "height": 576.5587918015109, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0N", + "roundness": { + "type": 3 + }, + "seed": 2097245493, + "version": 450, + "versionNonce": 492327899, + "isDeleted": false, + "boundElements": [ + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + } + ], + "updated": 1764875205688, + "link": null, + "locked": false + }, + { + "id": "Hx_F-eFq9Elf3olyEAh5H", + "type": "text", + "x": 791.7764842274424, + "y": 2644.4357759284935, + "width": 711.4058227539062, + "height": 516.4285714285714, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0O", + "roundness": null, + "seed": 1957523003, + "version": 210, + "versionNonce": 1767565467, + "isDeleted": false, + "boundElements": [ + { + "id": "3VriZd7FuEOwMpdPbnIiX", + "type": "arrow" + } + ], + "updated": 1764875208055, + "link": null, + "locked": false, + "text": "[\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_suffix\": 1,\n \"canonical_entity_id\": ,\n ...\n }\n }\n ]\n }\n]", + "fontSize": 22.952380952380953, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_suffix\": 1,\n \"canonical_entity_id\": ,\n ...\n }\n }\n ]\n }\n]", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "M3_9cSeevpvgvuGOJmtLu", + "type": "rectangle", + "x": 341.0753968253973, + "y": -946.5918028903099, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0P", + "roundness": { + "type": 3 + }, + "seed": 630737269, + "version": 231, + "versionNonce": 857423573, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875361089, + "link": null, + "locked": false + }, + { + "id": "e0VYlO1HBUE6Y61hMmjQS", + "type": "line", + "x": 384.5306207059946, + "y": -906.3554844823498, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Q", + "roundness": { + "type": 2 + }, + "seed": 924406485, + "version": 197, + "versionNonce": 1258245525, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "zz4q6sGFYLSQEGm4_Z_pZ", + "type": "line", + "x": 380.15053275977357, + "y": -879.2079349266717, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0R", + "roundness": { + "type": 2 + }, + "seed": 2054303797, + "version": 300, + "versionNonce": 2102055669, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "S9t8FU85SfKbOpC8RR_gM", + "type": "line", + "x": 380.6970945304854, + "y": -849.4353562329334, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0S", + "roundness": { + "type": 2 + }, + "seed": 227041685, + "version": 330, + "versionNonce": 1522445397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DL-iPq6d9xxkjVSnghr83", + "type": "text", + "x": 394.1873371239051, + "y": -619.8728974176731, + "width": 82.85454631444826, + "height": 80.47263681592038, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0T", + "roundness": null, + "seed": 898721525, + "version": 211, + "versionNonce": 53500341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "text": ".docx\n.pdf", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eb7-zegRUYIJqYobn0GTg", + "type": "line", + "x": 379.7022624970393, + "y": -821.0544894574741, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0U", + "roundness": { + "type": 2 + }, + "seed": 1964163157, + "version": 228, + "versionNonce": 2143640341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "76_13jw7FNEf9CD3vN4K7", + "type": "line", + "x": 386.86680332119363, + "y": -794.1556155836837, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0V", + "roundness": { + "type": 2 + }, + "seed": 1642196405, + "version": 262, + "versionNonce": 418590837, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "7bpvHSILjOEGoxBVuVvmL", + "type": "line", + "x": 387.21934633300566, + "y": -768.6496839272999, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0W", + "roundness": { + "type": 2 + }, + "seed": 992243477, + "version": 368, + "versionNonce": 1820285397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hunvhsGsscndVNFWE5PbJ", + "type": "line", + "x": 386.2245142995596, + "y": -740.2688171518405, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0X", + "roundness": { + "type": 2 + }, + "seed": 1062192245, + "version": 266, + "versionNonce": 1132418869, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "T8wwR2IwhMmplgY6InC65", + "type": "line", + "x": 393.3890551237139, + "y": -713.3699432780502, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Y", + "roundness": { + "type": 2 + }, + "seed": 1110011349, + "version": 300, + "versionNonce": 676898965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow", + "x": 560.3079384765335, + "y": -801.9441674997673, + "width": 311.1087281901333, + "height": 3.0858479987831515, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": { + "type": 2 + }, + "seed": 46590773, + "version": 818, + "versionNonce": 1436837659, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "AQOsTPNecUVIP5dcYG-CL" + } + ], + "updated": 1764875651472, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 311.1087281901333, + 3.0858479987831515 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "M3_9cSeevpvgvuGOJmtLu", + "focus": 0.024877658056798795, + "gap": 5.80129262390335 + }, + "endBinding": { + "elementId": "vWBP3oXNVHZGwRqV71dye", + "focus": 0.16647678467610177, + "gap": 26.273809523809405 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "AQOsTPNecUVIP5dcYG-CL", + "type": "text", + "x": 617.1124170125182, + "y": -797.1021624155233, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": null, + "seed": 1099807893, + "version": 47, + "versionNonce": 156055285, + "isDeleted": false, + "boundElements": [], + "updated": 1764875337505, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EOtxLYz_c5dBLBZTsf6bP", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vWBP3oXNVHZGwRqV71dye", + "type": "text", + "x": 897.6904761904764, + "y": -891.8596681096677, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": null, + "seed": 372466165, + "version": 414, + "versionNonce": 1404522139, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875606006, + "link": null, + "locked": false, + "text": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ]\n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document_id\": ,\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ]\n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "AJupGiIgf295eDXNF1NTl", + "type": "rectangle", + "x": 1911.6785714285718, + "y": -959.9404761904758, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0e", + "roundness": { + "type": 3 + }, + "seed": 807684629, + "version": 367, + "versionNonce": 1386791739, + "isDeleted": false, + "boundElements": [ + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow" + } + ], + "updated": 1764875606006, + "link": null, + "locked": false + }, + { + "id": "uIFK9kOYNJE4xD48qvRxR", + "type": "text", + "x": 2083.5527161189484, + "y": -757.9960317460313, + "width": 127.13986206054688, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0f", + "roundness": null, + "seed": 1570520949, + "version": 331, + "versionNonce": 2091546331, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "text": "NER outputs", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "NER outputs", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "InHz3D1JmEtzd_JC59uqm", + "type": "text", + "x": 1934.4354277111238, + "y": -887.162698412698, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0g", + "roundness": null, + "seed": 1999538389, + "version": 169, + "versionNonce": 660586261, + "isDeleted": false, + "boundElements": [], + "updated": 1764875581978, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "OKZB0eDL-ABRXZHWhREn2", + "type": "image", + "x": 2663.960317460318, + "y": -976.9960317460313, + "width": 1024, + "height": 473, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0h", + "roundness": null, + "seed": 1317474869, + "version": 341, + "versionNonce": 543979707, + "isDeleted": false, + "boundElements": [ + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow" + } + ], + "updated": 1764875603696, + "link": null, + "locked": false, + "status": "pending", + "fileId": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "lhZO6MPRRTWy3V55I0vZM", + "type": "arrow", + "x": 3756.6202534941517, + "y": -758.447755452392, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0i", + "roundness": { + "type": 2 + }, + "seed": 280036245, + "version": 1548, + "versionNonce": 1898165275, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GHolMdYEXJBOSjiLEJF7i" + } + ], + "updated": 1764875603696, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GHolMdYEXJBOSjiLEJF7i", + "type": "text", + "x": 3760.5369624941827, + "y": -745.4960317460313, + "width": 264.00054931640625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0j", + "roundness": null, + "seed": 1331022069, + "version": 47, + "versionNonce": 2111430683, + "isDeleted": false, + "boundElements": [], + "updated": 1764875595533, + "link": null, + "locked": false, + "text": "/anonymizer/anonymize-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lhZO6MPRRTWy3V55I0vZM", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "H45OT-nnyrYBIdX_fHnB-", + "type": "rectangle", + "x": 4251.421104004587, + "y": -923.9736436863299, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0k", + "roundness": { + "type": 3 + }, + "seed": 1543168597, + "version": 364, + "versionNonce": 1947781947, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false + }, + { + "id": "HkLFBBIeoT2NmdHolQogn", + "type": "line", + "x": 4294.876327885184, + "y": -883.7373252783694, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0l", + "roundness": { + "type": 2 + }, + "seed": 997576629, + "version": 330, + "versionNonce": 787590107, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "dP23wQQAJGa8EbTko6TLk", + "type": "line", + "x": 4290.496239938962, + "y": -856.5897757226916, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0m", + "roundness": { + "type": 2 + }, + "seed": 1005326613, + "version": 433, + "versionNonce": 690321531, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "AzAxanw4nR0bo9R-OtYUW", + "type": "line", + "x": 4291.042801709675, + "y": -826.8171970289535, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0n", + "roundness": { + "type": 2 + }, + "seed": 1255351925, + "version": 463, + "versionNonce": 276214043, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "cPvNsqjYFBYhf1dvqCfUM", + "type": "text", + "x": 4127.673543778677, + "y": -597.254738213693, + "width": 436.57354736328125, + "height": 40.2363184079602, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0o", + "roundness": null, + "seed": 183537621, + "version": 382, + "versionNonce": 911726011, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "text": "documento_anonimizado.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "documento_anonimizado.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "FZMsE2ftxJlqlXy5y1Kot", + "type": "line", + "x": 4290.047969676229, + "y": -798.4363302534942, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0p", + "roundness": { + "type": 2 + }, + "seed": 710528309, + "version": 361, + "versionNonce": 434857563, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "c5LayQF2_U6-IqmzQigBO", + "type": "line", + "x": 4297.212510500382, + "y": -771.5374563797035, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0q", + "roundness": { + "type": 2 + }, + "seed": 2020892309, + "version": 395, + "versionNonce": 839115515, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "CZP0a-w2xzB-aIeBIdTfj", + "type": "line", + "x": 4297.565053512195, + "y": -746.0315247233196, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0r", + "roundness": { + "type": 2 + }, + "seed": 1005962229, + "version": 501, + "versionNonce": 1998834587, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YGpIXuZRgXAXrbeyTS72v", + "type": "line", + "x": 4296.570221478749, + "y": -717.6506579478604, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0s", + "roundness": { + "type": 2 + }, + "seed": 1222868309, + "version": 399, + "versionNonce": 763207739, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ABZSKgAIt_rm_gemO06m9", + "type": "line", + "x": 4303.734762302903, + "y": -690.7517840740705, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "heHx6O1UXsEtwJkRBikSl" + ], + "frameId": null, + "index": "b0t", + "roundness": { + "type": 2 + }, + "seed": 1452477109, + "version": 433, + "versionNonce": 1861476571, + "isDeleted": false, + "boundElements": [], + "updated": 1764875579057, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow", + "x": 2444.591377430666, + "y": -762.2243671485597, + "width": 196.3689400296521, + "height": 3.692569021604527, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0u", + "roundness": { + "type": 2 + }, + "seed": 755633851, + "version": 323, + "versionNonce": 1071716059, + "isDeleted": false, + "boundElements": null, + "updated": 1764875655966, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 196.3689400296521, + -3.692569021604527 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "AJupGiIgf295eDXNF1NTl", + "focus": 0.03289811846175117, + "gap": 14.626683917308128 + }, + "endBinding": { + "elementId": "OKZB0eDL-ABRXZHWhREn2", + "focus": 0.06765961386393643, + "gap": 23 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "gXsFYxTEKv14jMJGLJI8O", + "type": "arrow", + "x": 1452.931012958739, + "y": -762.3018491588567, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0v", + "roundness": { + "type": 2 + }, + "seed": 1974610101, + "version": 1579, + "versionNonce": 919034907, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Q4IQFcmT8i1SnTx5DuzTS" + } + ], + "updated": 1764875655966, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Q4IQFcmT8i1SnTx5DuzTS", + "type": "text", + "x": 1566.9144893334446, + "y": -809.3501254524961, + "width": 167.20034790039062, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": null, + "seed": 2134198805, + "version": 55, + "versionNonce": 1592755067, + "isDeleted": false, + "boundElements": [], + "updated": 1764875655966, + "link": null, + "locked": false, + "text": "/anonymizer/predict", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "gXsFYxTEKv14jMJGLJI8O", + "originalText": "/anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "jFnC8PtgWHvjc9weWuFWX", + "type": "text", + "x": 326.69940776522253, + "y": -1119.4404761904757, + "width": 654.3769721137143, + "height": 76.99999999999989, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0z", + "roundness": null, + "seed": 29900859, + "version": 85, + "versionNonce": 988358325, + "isDeleted": false, + "boundElements": null, + "updated": 1764875700204, + "link": null, + "locked": false, + "text": "Anonymization pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymization pipeline", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hdfluh6k2tthpQT1_UP4m", + "type": "text", + "x": 326.69940776522253, + "y": -3.9404761904756924, + "width": 1170.5897216796875, + "height": 76.99999999999986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b10", + "roundness": null, + "seed": 2091673109, + "version": 149, + "versionNonce": 725184187, + "isDeleted": false, + "boundElements": [], + "updated": 1764875707681, + "link": null, + "locked": false, + "text": "Anonymization + disambiguation pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymization + disambiguation pipeline", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "7828d57068931cdd86458a8cbc891ed6ee122d38": { + "mimeType": "image/png", + "id": "7828d57068931cdd86458a8cbc891ed6ee122d38", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAHZCAYAAAAYFEnDAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQd4FcXXxt+9JZUUQhJC6BhaKCqgVAUEQSxIL6KoIKAiKEqRooQiRRAUQSlW9K8IAgICChYEAREsIERASuiQhJDe7r2733cmd8Nm2ZvckISWs8/DE5K7d3fmN7Ozc94554wEPpgAE2ACTIAJMAEmwASYABNgAkyACTCBW56AdMvXkCvIBJgAE2ACTIAJMAEmwASYABNgAkyACYAFAO4ETIAJMAEmwASYABNgAkyACTABJsAESgEBFgBKQSNzFZkAE2ACTIAJMAEmwASYABNgAkyACbAAwH2ACTABJsAEmAATYAJMgAkwASbABJhAKSDAAkApaGSuIhNgAkyACTABJsAEmAATYAJMgAkwARYAuA8wASbABJgAE2ACTIAJMAEmwASYABMoBQRYACgFjcxVZAJMgAkwASbABJgAE2ACTIAJMAEmwAIA9wEmwASYABNgAkyACTABJsAEmAATYAKlgAALAKWgkbmKTIAJMAEmwASYABNgAkyACTABJsAEWADgPsAEmAATYAJMgAkwASbABJgAE2ACTKAUEGABoBQ0MleRCTABJsAEmAATYAJMgAkwASbABJgACwDcB5gAE2ACTIAJMAEmwASYABNgAkyACZQCAiwAlIJG5ioyASbABJgAE2ACTIAJMAEmwASYABNgAYD7ABNgAkyACTABJsAEmAATYAJMgAkwgVJAgAWAUtDIXEUmwASYABNgAkyACTABJsAEmAATYAIsAHAfYAJMgAkwASbABJgAE2ACTIAJMAEmUAoIsABQChqZq8gEmAATYAJMgAkwASbABJgAE2ACTIAFAO4DTIAJMAEmwASYABNgAkyACTABJsAESgEBFgBKQSNzFZkAE2ACTIAJMAEmwASYABNgAkyACbAAwH2ACTABJsAEmAATYAJMgAkwASbABJhAKSDAAkApaGSuIhNgAkyACTABJsAEmAATYAJMgAkwARYAuA8wASbABJgAE2ACTIAJMAEmwASYABMoBQRYACgFjcxVZAJMgAkwASbABJgAE2ACTIAJMAEmcDMIACajZlIUBZJ0ufj0Ox3q3wr6nM7Vf0d/H/UaUVFRmDhx4hXX1t9DvSaVQb32pEmT8nxXf462Dtr7G5WfrkVl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", + "created": 1764873715847, + "lastRetrieved": 1764873715847 + } + } +} \ No newline at end of file diff --git a/docs/api/README.md b/docs/api/README.md index f5d1b028..212e215e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1,35 +1,321 @@ -# API -The connection between AymurAI's frontend and backend occurs through an Application Programming Interface (API). There are several endpoints that allow communication between the different parts of the software. Calls to these endpoints are made from the frontend to execute different subroutines. +# API Reference +Language: **English** | [Español](../es/api/README.md) -# Endpoints -* `/document-extract`: plain text extraction from .doc or .docx documents -* `/predict`: prediction over a single paragraph -* `/predict-batch`: run prediction over a batch of paragraphs +This document describes the currently mounted public API in `aymurai/api/main.py` + `aymurai/api/core.py`. + +## Base URL and OpenAPI +- Local base URL: `http://localhost:8899` +- Swagger UI: `http://localhost:8899/docs` +- OpenAPI JSON: `http://localhost:8899/openapi.json` + +## Public Endpoints (Mounted) + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/server/healthcheck` | Service liveness check | +| `GET` | `/server/stats/summary` | Runtime CPU/memory stats | +| `POST` | `/document-extract` | Deprecated alias of `/misc/document-extract` | +| `POST` | `/misc/document-extract` | Extract normalized paragraphs from uploaded document | +| `POST` | `/anonymizer/predict` | NER prediction for a paragraph | +| `POST` | `/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge | +| `POST` | `/anonymizer/validation` | Fetch paragraph-level manual validation | +| `POST` | `/anonymizer/anonymize-document` | Compile and export anonymized document | +| `POST` | `/datapublic/predict/{document_id}` | Predict entities for data-public flow | +| `GET` | `/datapublic/validation/document/{document_id}` | Read document-level validation | +| `POST` | `/datapublic/validation/document/{document_id}` | Save document-level validation | +| `POST` | `/convert/pdf/odt` | Convert PDF to ODT | +| `POST` | `/convert/pdf/docx` | Convert PDF to DOCX | +| `POST` | `/convert/docx/odt` | Convert DOCX to ODT | +| `POST` | `/convert/docx/pdf` | Convert DOCX to PDF | +| `POST` | `/convert/odt/pdf` | Convert ODT to PDF | +| `POST` | `/convert/odt/docx` | Convert ODT to DOCX | + +## Core Data Contracts + +Note: the JSON snippets below are minimal valid examples. Real payloads may include additional fields depending on the endpoint and processing stage. + +### `TextRequest` +```json +{ + "text": "Acusado: Ramiro Marrón DNI 34.555.666." +} +``` + +### `EntityAttributes` (relevant fields) +```json +{ + "aymurai_label": "PER", + "aymurai_label_subclass": [], + "aymurai_method": "flair", + "aymurai_score": 0.97, + "canonical_entity_id": "a3f8b60f-8e7e-4c8b-9de2-ec8dd3f44c12", + "aymurai_label_instance": 1, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": true +} +``` + +### `DocLabel` +```json +{ + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } +} +``` + +### `DocumentInformation` +```json +{ + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [ + { + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } + } + ] +} +``` + +### `LabelPolicy` +```json +{ + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": true +} +``` + +### `RenderPolicy` +```json +{ + "suffix_mode": "auto", + "suffix_threshold": 1 +} +``` + +### `DocumentAnnotations` +```json +{ + "data": [ + { + "document": "...", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + }, + "render_policy": { + "suffix_mode": "auto", + "suffix_threshold": 1 + } +} +``` + +## Endpoint Details and Examples + +### Server + +#### `GET /server/healthcheck` +- Response `200`: + +```json +{"status": "ok"} +``` -# Deployment -This project is deployed using [Docker](https://www.docker.com/) , and the images are available in the following registry: ```bash -registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod +curl -s http://localhost:8899/server/healthcheck ``` -You can a production ready instance of the API by running: + +#### `GET /server/stats/summary` +- Response `200` (shape): + +```json +{ + "is_docker": true, + "cpu_core_limit": 4, + "cpu_usage_percent": 0.0, + "memory_limit_mb": 4096.0, + "memory_usage_mb": 823.5 +} +``` + ```bash -docker run -d --rm -p 8899:8899 \ - registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod +curl -s http://localhost:8899/server/stats/summary ``` -this will start a container with the API running on port 8899 on your localhost. The API is documented using OpenAPI and can be accessed at `http://localhost:8899/docs` -Once it is deployed, it doesn't requere a internet connection to work. If you need to deployed in a closed network you can port the image using + +### Document Extraction + +#### `POST /misc/document-extract` +#### `POST /document-extract` (deprecated alias) +- Request: `multipart/form-data` with `file` +- Supported MIME types in extraction flow: DOCX, ODT, PDF +- Response `200`: + +```json +{ + "document": ["Paragraph 1", "Paragraph 2"], + "document_id": "f2b25507-cf88-5b11-8f2a-c0b6f940b7f8" +} +``` + ```bash -docker image save registry.gitlab.com/collective.ai/datagenero-public/aymurai-api:prod -o aymurai-api.tar +curl -s -X POST \ + -F "file=@/resources/data/sample/document-01.docx" \ + http://localhost:8899/misc/document-extract ``` -and then transfer the image to the target machine and load it using + +Common errors: +- `504` extraction timeout +- `500` extractor/internal errors + +### Anonymizer + +#### `POST /anonymizer/predict` +- Request body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Response `200`: `DocumentInformation` + ```bash -docker load -i aymurai-api.tar +curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` -For more information about docker deployment please refer to the [docker documentation](https://docs.docker.com/). -You also can contact us at aymurai@datagenero.org and we will be happy to help you. +#### `POST /anonymizer/disambiguate` +- Request body: + +```json +{ + "paragraphs": [ + { + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + } +} +``` + +- Response `200`: `DocumentAnnotations` (with `data` and effective `label_policies`) + +```bash +curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ + -H "Content-Type: application/json" \ + -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' +``` + +#### `POST /anonymizer/validation` +- Request body: `TextRequest` +- Response `200`: `list[DocLabel] | null` + +```bash +curl -s -X POST http://localhost:8899/anonymizer/validation \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /anonymizer/anonymize-document` +- Request: `multipart/form-data` + - `file`: original document (`.docx`, `.pdf`, `.odt`) + - `annotations`: JSON string serialized from `DocumentAnnotations` +- Response `200`: binary anonymized `.odt` file + +```bash +curl -X POST http://localhost:8899/anonymizer/anonymize-document \ + -F "file=@/resources/data/sample/document-01.docx" \ + -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' +``` + +Common errors: +- `400` invalid form payload +- `500` conversion/anonymization failures + +### Data-Public + +#### `POST /datapublic/predict/{document_id}` +- Path param: `document_id` (`UUID5`) +- Request body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Response `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' +``` + +#### `GET /datapublic/validation/document/{document_id}` +- Response `200`: object or `null` +- Response `404`: document not found + +```bash +curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +``` + +#### `POST /datapublic/validation/document/{document_id}` +- Request body: free-form JSON object (stored as document-level validation) +- Response `200`: empty body + +```bash +curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ + -H "Content-Type: application/json" \ + -d '{"materia":"penal","violencia_de_genero":"si"}' +``` + +### Document Conversion + +All conversion endpoints use `multipart/form-data` with a `file` field. + +| Method | Path | Input | Output | +|---|---|---|---| +| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/convert/odt/docx` | `.odt` | `.docx` | + +For PDF input endpoints, optional query param: +- `backend=libreoffice|pandoc` (default: `libreoffice`) + +Example: + +```bash +curl -X POST "http://localhost:8899/convert/pdf/docx?backend=libreoffice" \ + -F "file=@input.pdf" -o output.docx +``` + +Common errors: +- `400` unsupported input extension +- `500` conversion tool failure + +## Legacy / Not Public (Not Mounted) +The following route modules exist in code but are not included in `core.router` at runtime: -# Source -The api source code is written in top of FastAPI and can be found in [here](../../api/). +- `aymurai/api/endpoints/routers/datapublic/dataset.py` + - includes `/datapublic/dataset/*` CRUD/batch routes, but router is not mounted. +- `aymurai/api/endpoints/routers/anonymizer/database.py` + - `/anonymizer/database/*` routes exist, include is commented out. +- `aymurai/api/endpoints/routers/database/*` + - additional DB admin routes exist, but no mounting in `core.router`. -Please follow the [code of conduct](../CODE_OF_CONDUCT.md) and [contributing guidelines](../CONTRIBUTING.md). \ No newline at end of file +Treat these as legacy/internal code paths until explicitly exposed in the public router. diff --git a/docs/data/en/entities-table.md b/docs/data/en/entities-table.md deleted file mode 100644 index 1037b24d..00000000 --- a/docs/data/en/entities-table.md +++ /dev/null @@ -1,32 +0,0 @@ -# Entities -| Entity | Description | -|-----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ART_INFRINGIDO | Article(s) of the infraction(s) in the case. | -| CONDUCTA | Indicates the action relating to the crime, contravention, or misdemeanor described in the article infringed. | -| CONDUCTA_DESCRIPCION | Indicates that the contravention or offence infringed has some particularity, such as being aggravated by some causality. | -| DECISION | Fragment of text denoting a decision taken by the court regarding the case. | -| DETALLE | Specifies OBJETO_DE_LA_RESOLUCION as to what was resolved. | -| EDAD_AL_MOMENTO_DEL_HECHO | Indicates the age of the person at the time of the event. | -| FECHA_DE_NACIMIENTO | Date of birth. | -| FECHA_DEL_HECHO | Date on which the reported event occurred. | -| FECHA_RESOLUCION | Day of resolution. In the case of oral hearings, it is the day of the beginning of the hearing. | -| FRASES_AGRESION | Transcription of the phrases described by the victim as the verbal aggression suffered and that are part of the facts of the case. Applies to cases of verbal violence. | -| GENERO | Gender. | -| HIJOS_HIJAS_EN_COMUN | Indicates whether the accused person and the complainant have children in common. | -| HORA_DE_CIERRE | Time of end of court hearing. | -| HORA_DE_INICIO | Court hearing start time. | -| LUGAR_DEL_HECHO | Indicates the physical place (environment or public road) where the facts occurred or if it was committed by technological means. | -| MODALIDAD_DE_LA_VIOLENCIA | The way in which the different types of violence manifest themselves. For example: domestic, institutional, media, labor, reproductive freedom, obstetric, in public or private spaces, and political and public violence. | -| N_EXPTE_EJE | Case identifier number. | -| NACIONALIDAD | Nationality. | -| NIVEL_INSTRUCCION | Level of formal education attained by the individual. | -| NOMBRE | Name. | -| OBJETO_DE_LA_RESOLUCION | About what was resolved. | -| PERSONA_ACUSADA_NO_DETERMINADA | In case the accused person is not a natural person or is not determined. For example: legal entity, social network user, etc. | -| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | Indicates the type of relationship the accused person has with the complainant. | -| TIPO_DE_RESOLUCION | Interlocutory decisions are those that define a specific issue during the process of the proceeding, or final decisions are those that put an end to the process of the case. | -| VIOLENCIA_DE_GENERO | If the fact under investigation is within a context of gender violence. | -# Subcategories -The majority of the entities described above have subcategories acording their use in the criminal court and the type of information they contain. You can check a more complete description [here](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) (Spanish version) - -A comprehensive list of all entities and their subcategory can be found [here](https://docs.google.com/spreadsheets/d/1FTRtltYJrGJ6huQd0ih-QBZfqo5LGamVFXvXKcmX9-k/edit#gid=0) \ No newline at end of file diff --git a/docs/data/es/entities-table.md b/docs/data/es/entities-table.md deleted file mode 100644 index 1f4b9301..00000000 --- a/docs/data/es/entities-table.md +++ /dev/null @@ -1,27 +0,0 @@ -| Entidad | Descripcion | -|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ART_INFRINGIDO | Artículo/s de la/s infracción/es en el caso | -| CONDUCTA | Indica la acción relativa al delito, la contravención, o la falta que aparece descripta en el artículo infringido; | -| CONDUCTA_DESCRIPCION | Indica la contravención o la falta infringida tiene alguna particularidad, como puede ser que se encuentra agravada por alguna causal | -| DECISION | Fragmento de texto que denota una decision tomada por el juzgado respecto al caso. | -| DETALLE | Especifica OBJETO_DE_LA_RESOLUCIÓN respecto a qué se resolvió | -| EDAD_AL_MOMENTO_DEL_HECHO | Indica la edad de la persona al momento del hecho | -| FECHA_DE_NACIMIENTO | Fecha de nacimiento | -| FECHA_DEL_HECHO | Fecha en que sucedio el hecho denunciado | -| FECHA_RESOLUCION | Día de la resolución. En caso de las audiencias orales es el día de su inicio | -| FRASES_AGRESION | Transcripción de las frases descriptas por la víctima como la agresión verbal sufrida y que son parte de los hechos del caso. Aplica para los casos de violencia verbal; | -| GENERO | Genero | -| HIJOS_HIJAS_EN_COMUN | indica si la persona acusada y la denunciante tienen hijos/as en común | -| HORA_DE_CIERRE | Hora de finalización de la audiencia | -| HORA_DE_INICIO | Hora de inicio de la audiencia | -| LUGAR_DEL_HECHO | Indica lugar físico (ambiente o vía pública) donde ocurrieron los hechos o si fue cometido mediante medios tecnológicos | -| MODALIDAD_DE_LA_VIOLENCIA | Forma en que se manifiestan los distintos tipos de violencia | -| N_EXPTE_EJE | Numeroo identificador del caso | -| NACIONALIDAD | Nacioalidad | -| NIVEL_INSTRUCCION | Nivel de estudios formales alcanzados por la persona | -| NOMBRE | Nombre | -| OBJETO_DE_LA_RESOLUCION | Sobre qué se resolvió | -| PERSONA_ACUSADA_NO_DETERMINADA | En el caso de que la persona acusada no sea una persona física o no esté determinada. Por ejemplo: persona jurídica, usuario de red social, etc. | -| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | Indica el tipo de vínculo que tiene la persona acusada con la denunciante | -| TIPO_DE_RESOLUCION | Interlocutorias son aquellas que definen una cuestión concreta durante la tramitación del proceso, o definitivas son aquellas resoluciones que ponen fin al proceso de la causa | -| VIOLENCIA_DE_GENERO | Si el hecho objeto de investigación se encuentra dentro de un contexto de violencia de género. | diff --git a/docs/database/README.md b/docs/database/README.md index 387ea6ed..64cf5212 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -1,66 +1,113 @@ -# Descripción del nuevo flujo de integración en AymurAI - -En este documento, describimos brevemente el nuevo flujo de integración entre el _front-end_ y el _back-end_ de AymurAI. - -## Actualización principal del back-end - -Se implementó un motor de base de datos que permite persistir en disco tanto las predicciones generadas por los modelos como las validaciones manuales realizadas desde la interfaz gráfica. Esta base de datos no sólo funciona como un caché para las predicciones, sino que además permite a les usuaries recuperar la última validación manual en caso de necesitar reprocesar un documento específico. - -A continuación, se detalla el nuevo flujo para cada _pipeline_ de AymurAI: +# Internal Database +Language: **English** | [Español](../es/database/README.md) + +This document describes the internal persistence used by the API runtime. + +## Engine and Migrations +- ORM: `SQLModel` (`sqlalchemy` backend) +- Migration tool: Alembic +- Runtime setting: `SQLALCHEMY_DATABASE_URI` +- Default DB URI: `sqlite:////resources/cache/sqlite/database.db` +- Startup behavior: + 1. API checks DB connectivity. + 2. If SQLite file does not exist, it creates parent directories. + 3. API runs `alembic upgrade head` on startup. + +Related code: +- `aymurai/settings.py` +- `aymurai/api/startup/database.py` +- `aymurai/api/main.py` +- `aymurai/database/versions/13f78d08e925_create_database.py` + +## ER Diagram +![Database ER diagram](schema.png) + +Editable source: [`schema.mmd`](schema.mmd) + +## Tables + +### `anonymization_document` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | +| `name` | `TEXT` | no | Original filename | + +### `anonymization_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derived from paragraph text hash | +| `text` | `TEXT` | no | Normalized paragraph text | +| `prediction` | `JSON` | yes | Model predictions (`list[DocLabel]`) | +| `validation` | `JSON` | yes | Manual labels (`list[DocLabel]`) | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `anonymization_document_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Link row identifier | +| `document_id` | `UUID` | no | FK -> `anonymization_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `anonymization_paragraph.id` | +| `order` | `INTEGER` | yes | Paragraph order in source document | + +Primary key is composite over `id`, `document_id`, `paragraph_id`. + +### `datapublic_document` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key (document identifier) | +| `prediction` | `JSON` | yes | Reserved document-level prediction payload; not written by the current public router | +| `validation` | `JSON` | yes | Document-level validation payload | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `datapublic_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derived from paragraph text hash | +| `text` | `TEXT` | no | Normalized paragraph text | +| `prediction` | `JSON` | yes | Model predictions (`list[DocLabel]`) | +| `validation` | `JSON` | yes | Reserved for paragraph-level validation; public route is currently commented legacy logic | +| `created_at` | `DATETIME` | no | Server default `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | yes | Updated on row changes | + +### `datapublic_document_paragraph` +| Column | Type | Nullable | Notes | +|---|---|---|---| +| `id` | `UUID` | no | Link row identifier | +| `document_id` | `UUID` | no | FK -> `datapublic_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `datapublic_paragraph.id` | +| `order` | `INTEGER` | yes | Paragraph order in source document | + +Primary key is composite over `id`, `document_id`, `paragraph_id`. + +## Endpoint to Persistence Mapping ### Anonymizer - -1. **Consulta inicial en la base de datos (_endpoint_ `/anonymizer/validation`):** - - Tras la extracción del texto del documento, se verifica si existe una validación guardada para cada uno de sus párrafos. El identificador utilizado es un _hash_ único generado a partir del contenido textual del párrafo. - - - **Si existe una validación previa en la tabla `anonymization_paragraph`:** se retorna dicha validación. - - **Si no existe una validación previa:** debe dispararse el flujo de predicciones consumiendo el _endpoint_ correspondiente. - -2. **Consulta de predicciones (_endpoint_ `/anonymizer/predict`):** - - - **Si la predicción para el párrafo ya existe en la tabla `anonymization_paragraph`:** se retorna. - - **Si no existe:** se ejecuta el modelo de predicción, y los resultados se retornan y se persisten en la tabla `anonymization_paragraph`. - - > **Formato de etiquetas:** tanto las validaciones manuales como las predicciones se estructuran utilizando el formato actual de las etiquetas de AymurAI. - -3. **Interfaz gráfica y validación manual:** - - Las etiquetas retornadas se muestran en la interfaz gráfica, donde pueden ser validadas manualmente. Una vez finalizada la validación, se procede con la anonimización del documento (_endpoint_ `/anonymizer/anonymize-document`). - -4. **Persistencia de validaciones:** - - Al finalizar el proceso, las validaciones se escriben en la tabla `anonymization_paragraph`, ya sea creando nuevos registros para párrafos nuevos o actualizando registros existentes para párrafos previamente procesados. Esta escritura es realizada al inicio del flujo de `anonymize-document`, luego de que se indique desde la UI que el documento está listo para su anonimización. Además, en la tabla `anonymization_document` se escribe la referencia al ID del documento y el nombre del archivo correspondiente. Asimismo, el ID del documento permite recuperar los párrafos y su orden en el documento a través de la tabla `anonymization_document_paragraph`. - -### Data Public - -El flujo para el _pipeline_ de Data Public es similar al del Anonymizer, pero presenta diferencias importantes en cuanto a las validaciones, ya que estas no comparten el mismo formato que las predicciones originales del modelo. En este caso, las validaciones se realizan a nivel general del documento (no por párrafo) y se llevan a cabo manualmente en la sección lateral derecha de la UI, donde se definen los campos que serán volcados al dataset público. A diferencia del _pipeline_ de anonimización, las predicciones siempre son necesarias para resaltar los campos detectados en el texto, incluso cuando ya existen validaciones realizadas, ya que estas no son modificaciones directas sobre las entidades reconocidas por el modelo. - -1. **Consulta inicial en la base de datos (_endpoint_ `/datapublic/dataset/{document_id}`):** - - Tras la extracción del texto del documento, se verifica si existe una validación general para el documento. - - - **Si existe una validación previa para el documento en la tabla `datapublic_dataset`:** se retorna dicha validación. - -2. **Flujo de predicciones (_endpoint_ `/datapublic/predict`):** - - A su vez, se consulta si existen predicciones guardadas para cada uno de los párrafos: - - - **Si existe una predicción previa para cada párrafo en la tabla `datapublic_paragraph`:** se retorna dicha predicción. - - - **Si no existe:** se ejecuta el modelo de predicción, y los resultados se retornan y se persisten en la tabla `datapublic_paragraph`. - - > **Formato de etiquetas:** las predicciones se estructuran utilizando el formato actual de las etiquetas de AymurAI. Las validaciones, sin embargo, tienen un formato distinto, **equivalente al de las filas del dataset público**. Es necesario tener presente esto para poder integrar correctamente con el _front-end_ los campos validados persistidos en la base de datos. - -3. **Interfaz gráfica y validación manual:** - - Una vez finalizadas las consultas a la base de datos y el flujo de predicciones, las etiquetas retornadas se muestran en la interfaz gráfica, donde pueden ser revisadas y validadas manualmente. Posteriormente, las nuevas validaciones se persisten en la tabla `datapublic_dataset` haciendo un _post_ contra el _endpoint_ `/datapublic/dataset`, ya sea creando nuevos registros para documentos nuevos o actualizando registros existentes para documentos previamente procesados. Esta escritura debe realizarse una vez que desde la UI se confirma que los datos extraídos del documento son correctos. - - -#### Requerimientos a la UI - -Para la correcta persistencia de los datos (por ejemplo, para hacer evaluaciones o futuros entrenamientos de los modelos), se necesita que la UI envíe los datos corregidos a la API. Esto puede ser resumido en un _endpoint_ de "compilación" al final del proceso de validación. Este _endpoint_ sería equivalente al `anonymization/anonymize-document` - -## Esquema de la base de datos - -![schema](schema.jpg) +- `POST /anonymizer/predict` + - Reads `anonymization_paragraph` by paragraph UUID. + - Writes `anonymization_paragraph.prediction` when cache is enabled. +- `POST /anonymizer/disambiguate` + - Writes disambiguated predictions to `anonymization_paragraph.prediction`. +- `POST /anonymizer/validation` + - Reads `anonymization_paragraph.validation`. +- `POST /anonymizer/anonymize-document` + - Writes `anonymization_paragraph.validation`. + - Creates `anonymization_document` keyed by uploaded file content hash. + - Creates link rows in `anonymization_document_paragraph`. + +### Data-public +- `POST /datapublic/predict/{document_id}` + - Uses caller-provided `document_id` as the document primary key. + - Ensures `datapublic_document` exists when `use_cache=true`. + - Writes `datapublic_paragraph.prediction` when `use_cache=true`. + - Writes link row in `datapublic_document_paragraph` when `use_cache=true`. +- `GET /datapublic/validation/document/{document_id}` + - Reads `datapublic_document.validation`. +- `POST /datapublic/validation/document/{document_id}` + - Upserts `datapublic_document.validation`. + +## Legacy Note +Route modules for dataset CRUD (`/datapublic/dataset/*`) exist in code but are not mounted in the public router. Do not treat them as active public API until exposed in `core.router`. diff --git a/docs/database/schema.excalidraw b/docs/database/schema.excalidraw new file mode 100644 index 00000000..111dd9fe --- /dev/null +++ b/docs/database/schema.excalidraw @@ -0,0 +1,56 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "UnZYOKsPO14ownJjIhB0K", + "type": "image", + "x": 216.9140625, + "y": 240.5078125, + "width": 968.171875, + "height": 397.984375, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 810215012, + "version": 5, + "versionNonce": 845707748, + "isDeleted": false, + "boundElements": [], + "updated": 1772457952595, + "link": null, + "locked": false, + "status": "saved", + "fileId": "X7nK5-PoRG7EVUWSedgsz", + "scale": [ + 1, + 1 + ], + "crop": null + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "X7nK5-PoRG7EVUWSedgsz": { + "id": "X7nK5-PoRG7EVUWSedgsz", + "mimeType": "image/svg+xml", + "dataURL": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtcm9sZWRlc2NyaXB0aW9uPSJlciIgcm9sZT0iZ3JhcGhpY3MtZG9jdW1lbnQgZG9jdW1lbnQiIHZpZXdCb3g9IjAgMCA5NjguMTgxNzAxNjYwMTU2MiAzOTgiIHN0eWxlPSJtYXgtd2lkdGg6IDk2OC4xODE3MDE2NjAxNTYycHg7IiB3aWR0aD0iOTY4LjE3MTg3NSIgaWQ9Im1lcm1haWQtdG8tZXhjYWxpZHJhdyIgaGVpZ2h0PSIzOTcuOTg0Mzc1Ij48c3R5bGU+I21lcm1haWQtdG8tZXhjYWxpZHJhd3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjI1cHg7ZmlsbDojMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lcnJvci1pY29ue2ZpbGw6IzU1MjIyMjt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAuZXJyb3ItdGV4dHtmaWxsOiM1NTIyMjI7c3Ryb2tlOiM1NTIyMjI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtdGhpY2tuZXNzLW5vcm1hbHtzdHJva2Utd2lkdGg6MnB4O30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lZGdlLXRoaWNrbmVzcy10aGlja3tzdHJva2Utd2lkdGg6My41cHg7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtcGF0dGVybi1zb2xpZHtzdHJva2UtZGFzaGFycmF5OjA7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVkZ2UtcGF0dGVybi1kYXNoZWR7c3Ryb2tlLWRhc2hhcnJheTozO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5lZGdlLXBhdHRlcm4tZG90dGVke3N0cm9rZS1kYXNoYXJyYXk6Mjt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAubWFya2Vye2ZpbGw6IzMzMzMzMztzdHJva2U6IzMzMzMzMzt9I21lcm1haWQtdG8tZXhjYWxpZHJhdyAubWFya2VyLmNyb3Nze3N0cm9rZTojMzMzMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3IHN2Z3tmb250LWZhbWlseToidHJlYnVjaGV0IG1zIix2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7Zm9udC1zaXplOjI1cHg7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVudGl0eUJveHtmaWxsOiNFQ0VDRkY7c3Ryb2tlOiM5MzcwREI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmF0dHJpYnV0ZUJveE9kZHtmaWxsOiNmZmZmZmY7c3Ryb2tlOiM5MzcwREI7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmF0dHJpYnV0ZUJveEV2ZW57ZmlsbDojZjJmMmYyO3N0cm9rZTojOTM3MERCO30jbWVybWFpZC10by1leGNhbGlkcmF3IC5yZWxhdGlvbnNoaXBMYWJlbEJveHtmaWxsOmhzbCg4MCwgMTAwJSwgOTYuMjc0NTA5ODAzOSUpO29wYWNpdHk6MC43O2JhY2tncm91bmQtY29sb3I6aHNsKDgwLCAxMDAlLCA5Ni4yNzQ1MDk4MDM5JSk7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLnJlbGF0aW9uc2hpcExhYmVsQm94IHJlY3R7b3BhY2l0eTowLjU7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLnJlbGF0aW9uc2hpcExpbmV7c3Ryb2tlOiMzMzMzMzM7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgLmVudGl0eVRpdGxlVGV4dHt0ZXh0LWFuY2hvcjptaWRkbGU7Zm9udC1zaXplOjE4cHg7ZmlsbDojMzMzO30jbWVybWFpZC10by1leGNhbGlkcmF3ICNNRF9QQVJFTlRfU1RBUlR7ZmlsbDojZjVmNWY1IWltcG9ydGFudDtzdHJva2U6IzMzMzMzMyFpbXBvcnRhbnQ7c3Ryb2tlLXdpZHRoOjE7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgI01EX1BBUkVOVF9FTkR7ZmlsbDojZjVmNWY1IWltcG9ydGFudDtzdHJva2U6IzMzMzMzMyFpbXBvcnRhbnQ7c3Ryb2tlLXdpZHRoOjE7fSNtZXJtYWlkLXRvLWV4Y2FsaWRyYXcgOnJvb3R7LS1tZXJtYWlkLWZvbnQtZmFtaWx5OiJ0cmVidWNoZXQgbXMiLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT48Zy8+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMjQwIiBtYXJrZXJXaWR0aD0iMTkwIiByZWZZPSI3IiByZWZYPSIwIiBpZD0iTURfUEFSRU5UX1NUQVJUIj48cGF0aCBkPSJNIDE4LDcgTDksMTMgTDEsNyBMOSwxIFoiLz48L21hcmtlcj48L2RlZnM+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMjgiIG1hcmtlcldpZHRoPSIyMCIgcmVmWT0iNyIgcmVmWD0iMTkiIGlkPSJNRF9QQVJFTlRfRU5EIj48cGF0aCBkPSJNIDE4LDcgTDksMTMgTDEsNyBMOSwxIFoiLz48L21hcmtlcj48L2RlZnM+PGRlZnM+PG1hcmtlciBvcmllbnQ9ImF1dG8iIG1hcmtlckhlaWdodD0iMTgiIG1hcmtlcldpZHRoPSIxOCIgcmVmWT0iOSIgcmVmWD0iMCIgaWQ9Ik9OTFlfT05FX1NUQVJUIj48cGF0aCBkPSJNOSwwIEw5LDE4IE0xNSwwIEwxNSwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJncmF5Ii8+PC9tYXJrZXI+PC9kZWZzPjxkZWZzPjxtYXJrZXIgb3JpZW50PSJhdXRvIiBtYXJrZXJIZWlnaHQ9IjE4IiBtYXJrZXJXaWR0aD0iMTgiIHJlZlk9IjkiIHJlZlg9IjE4IiBpZD0iT05MWV9PTkVfRU5EIj48cGF0aCBkPSJNMywwIEwzLDE4IE05LDAgTDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIxOCIgbWFya2VyV2lkdGg9IjMwIiByZWZZPSI5IiByZWZYPSIwIiBpZD0iWkVST19PUl9PTkVfU1RBUlQiPjxjaXJjbGUgcj0iNiIgY3k9IjkiIGN4PSIyMSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iZ3JheSIvPjxwYXRoIGQ9Ik05LDAgTDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIxOCIgbWFya2VyV2lkdGg9IjMwIiByZWZZPSI5IiByZWZYPSIzMCIgaWQ9IlpFUk9fT1JfT05FX0VORCI+PGNpcmNsZSByPSI2IiBjeT0iOSIgY3g9IjkiIGZpbGw9IndoaXRlIiBzdHJva2U9ImdyYXkiLz48cGF0aCBkPSJNMjEsMCBMMjEsMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjQ1IiByZWZZPSIxOCIgcmVmWD0iMTgiIGlkPSJPTkVfT1JfTU9SRV9TVEFSVCI+PHBhdGggZD0iTTAsMTggUSAxOCwwIDM2LDE4IFEgMTgsMzYgMCwxOCBNNDIsOSBMNDIsMjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjQ1IiByZWZZPSIxOCIgcmVmWD0iMjciIGlkPSJPTkVfT1JfTU9SRV9FTkQiPjxwYXRoIGQ9Ik0zLDkgTDMsMjcgTTksMTggUTI3LDAgNDUsMTggUTI3LDM2IDksMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48ZGVmcz48bWFya2VyIG9yaWVudD0iYXV0byIgbWFya2VySGVpZ2h0PSIzNiIgbWFya2VyV2lkdGg9IjU3IiByZWZZPSIxOCIgcmVmWD0iMTgiIGlkPSJaRVJPX09SX01PUkVfU1RBUlQiPjxjaXJjbGUgcj0iNiIgY3k9IjE4IiBjeD0iNDgiIGZpbGw9IndoaXRlIiBzdHJva2U9ImdyYXkiLz48cGF0aCBkPSJNMCwxOCBRMTgsMCAzNiwxOCBRMTgsMzYgMCwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJncmF5Ii8+PC9tYXJrZXI+PC9kZWZzPjxkZWZzPjxtYXJrZXIgb3JpZW50PSJhdXRvIiBtYXJrZXJIZWlnaHQ9IjM2IiBtYXJrZXJXaWR0aD0iNTciIHJlZlk9IjE4IiByZWZYPSIzOSIgaWQ9IlpFUk9fT1JfTU9SRV9FTkQiPjxjaXJjbGUgcj0iNiIgY3k9IjE4IiBjeD0iOSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iZ3JheSIvPjxwYXRoIGQ9Ik0yMSwxOCBRMzksMCA1NywxOCBRMzksMzYgMjEsMTgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iZ3JheSIvPjwvbWFya2VyPjwvZGVmcz48cGF0aCBzdHlsZT0ic3Ryb2tlOiBncmF5OyBmaWxsOiBub25lOyIgbWFya2VyLXN0YXJ0PSJ1cmwoI09OTFlfT05FX1NUQVJUKSIgbWFya2VyLWVuZD0idXJsKCNaRVJPX09SX01PUkVfRU5EKSIgZD0iTTEwMy43MDksMTQ5TDEwMy43MDksMTYwLjgzM0MxMDMuNzA5LDE3Mi42NjcsMTAzLjcwOSwxOTYuMzMzLDExNC40NSwyMTYuNUMxMjUuMTkxLDIzNi42NjcsMTQ2LjY3MiwyNTMuMzMzLDE1Ny40MTMsMjYxLjY2N0wxNjguMTU0LDI3MCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExpbmUiLz48cGF0aCBzdHlsZT0ic3Ryb2tlOiBncmF5OyBmaWxsOiBub25lOyIgbWFya2VyLXN0YXJ0PSJ1cmwoI09OTFlfT05FX1NUQVJUKSIgbWFya2VyLWVuZD0idXJsKCNaRVJPX09SX01PUkVfRU5EKSIgZD0iTTM3MS44MDEsMTcwTDM3MS44MDEsMTc4LjMzM0MzNzEuODAxLDE4Ni42NjcsMzcxLjgwMSwyMDMuMzMzLDM2MS4wNiwyMjBDMzUwLjMxOSwyMzYuNjY3LDMyOC44MzcsMjUzLjMzMywzMTguMDk2LDI2MS42NjdMMzA3LjM1NiwyNzAiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMaW5lIi8+PHBhdGggc3R5bGU9InN0cm9rZTogZ3JheTsgZmlsbDogbm9uZTsiIG1hcmtlci1zdGFydD0idXJsKCNPTkxZX09ORV9TVEFSVCkiIG1hcmtlci1lbmQ9InVybCgjWkVST19PUl9NT1JFX0VORCkiIGQ9Ik02MjkuMTM2LDE1OS41TDYyOS4xMzYsMTY5LjU4M0M2MjkuMTM2LDE3OS42NjcsNjI5LjEzNiwxOTkuODMzLDYzOC45OTIsMjE4LjI1QzY0OC44NDcsMjM2LjY2Nyw2NjguNTU5LDI1My4zMzMsNjc4LjQxNCwyNjEuNjY3TDY4OC4yNywyNzAiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMaW5lIi8+PHBhdGggc3R5bGU9InN0cm9rZTogZ3JheTsgZmlsbDogbm9uZTsiIG1hcmtlci1zdGFydD0idXJsKCNPTkxZX09ORV9TVEFSVCkiIG1hcmtlci1lbmQ9InVybCgjWkVST19PUl9NT1JFX0VORCkiIGQ9Ik04NzUuMTM1LDE3MEw4NzUuMTM1LDE3OC4zMzNDODc1LjEzNSwxODYuNjY3LDg3NS4xMzUsMjAzLjMzMyw4NjUuMjc5LDIyMEM4NTUuNDIzLDIzNi42NjcsODM1LjcxMiwyNTMuMzMzLDgyNS44NTYsMjYxLjY2N0w4MTYsMjcwIiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGluZSIvPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwLDQxICkiIGlkPSJlbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YSI+PHJlY3QgaGVpZ2h0PSIxMDgiIHdpZHRoPSIxNjcuNDE3OTY4NzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODMuNzA4OTg0Mzc1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+YW5vbnltaXphdGlvbl9kb2N1bWVudDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMTIzNzU4OTUxODIyOTIiIHk9IjI0IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9IjI0IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjEyMzc1ODk1MTgyMjkyLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTEtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5pZDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjI0IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDEuNDUxNzg3MzEyODI1NTMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnQtOGRhYWQxMWYtMGZhNS01OGJmLWJlYTgtYmI2ODdmZWI1MDhhLWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjEyMzc1ODk1MTgyMjkyIiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9IjQ1IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0yLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjQ1IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQxLjQ1MTc4NzMxMjgyNTUzLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTIta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMTIzNzU4OTUxODIyOTIiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjMyODAyODM2MTAwMjYxIiB5PSI2NiIgeD0iNjguMTIzNzU4OTUxODIyOTIiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci0zLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dXBkYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9IjY2IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDEuNDUxNzg3MzEyODI1NTMsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnQtOGRhYWQxMWYtMGZhNS01OGJmLWJlYTgtYmI2ODdmZWI1MDhhLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC4xMjM3NTg5NTE4MjI5MiIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci00LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VEVYVDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguMzI4MDI4MzYxMDAyNjEiIHk9Ijg3IiB4PSI2OC4xMjM3NTg5NTE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3My4xMjM3NTg5NTE4MjI5Miw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudC04ZGFhZDExZi0wZmE1LTU4YmYtYmVhOC1iYjY4N2ZlYjUwOGEtYXR0ci00LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+bmFtZTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzAuOTY2MTgxNDM3MTc0NDgiIHk9Ijg3IiB4PSIxMzYuNDUxNzg3MzEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQxLjQ1MTc4NzMxMjgyNTUzLDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50LThkYWFkMTFmLTBmYTUtNThiZi1iZWE4LWJiNjg3ZmViNTA4YS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjg3LjQxNzk2ODc1LDIwICkiIGlkPSJlbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMiPjxyZWN0IGhlaWdodD0iMTUwIiB3aWR0aD0iMTY4Ljc2NTYyNSIgeT0iMCIgeD0iMCIgY2xhc3M9ImVyIGVudGl0eUJveCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4NC4zODI4MTI1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmFub255bWl6YXRpb25fcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNzc3MjQ3MTExMDAyNjEiIHk9IjI0IiB4PSI2OC41NzI5Nzc3MDE4MjI5MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjMxLjQxNTQwMDE4NzE3NDQ4IiB5PSIyNCIgeD0iMTM3LjM1MDIyNDgxMjgyNTUzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0xLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNTcyOTc3NzAxODIyOTIiIHk9IjQ1IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5URVhUPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC43NzcyNDcxMTEwMDI2MSIgeT0iNDUiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0yLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dGV4dDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzEuNDE1NDAwMTg3MTc0NDgiIHk9IjQ1IiB4PSIxMzcuMzUwMjI0ODEyODI1NTMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjU3Mjk3NzcwMTgyMjkyIiB5PSI2NiIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTMtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5KU09OPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC43NzcyNDcxMTEwMDI2MSIgeT0iNjYiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTMtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5wcmVkaWN0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIzMS40MTU0MDAxODcxNzQ0OCIgeT0iNjYiIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0Mi4zNTAyMjQ4MTI4MjU1Myw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSI4NyIgeD0iNjguNTcyOTc3NzAxODIyOTIiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTQtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj52YWxpZGF0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIzMS40MTU0MDAxODcxNzQ0OCIgeT0iODciIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDIuMzUwMjI0ODEyODI1NTMsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9ucGFyYWdyYXBoLWY1YzY1MzkzLTUwNjAtNTQzMi1iMzU3LThmOTM1YTU4YTFlMy1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNTcyOTc3NzAxODIyOTIiIHk9IjEwOCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci01LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSIxMDgiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNTcyOTc3NzAxODIyOTIsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci01LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMzEuNDE1NDAwMTg3MTc0NDgiIHk9IjEwOCIgeD0iMTM3LjM1MDIyNDgxMjgyNTUzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQyLjM1MDIyNDgxMjgyNTUzLDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC41NzI5Nzc3MDE4MjI5MiIgeT0iMTI5IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci02LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+REFURVRJTUU8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4Ljc3NzI0NzExMTAwMjYxIiB5PSIxMjkiIHg9IjY4LjU3Mjk3NzcwMTgyMjkyIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjU3Mjk3NzcwMTgyMjkyLDEzOS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25wYXJhZ3JhcGgtZjVjNjUzOTMtNTA2MC01NDMyLWIzNTctOGY5MzVhNThhMWUzLWF0dHItNi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnVwZGF0ZWRfYXQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjMxLjQxNTQwMDE4NzE3NDQ4IiB5PSIxMjkiIHg9IjEzNy4zNTAyMjQ4MTI4MjU1MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNDIuMzUwMjI0ODEyODI1NTMsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbnBhcmFncmFwaC1mNWM2NTM5My01MDYwLTU0MzItYjM1Ny04ZjkzNWE1OGExZTMtYXR0ci02LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMy42ODQ1NzAzMTI1LDI3MCApIiBpZD0iZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzkiPjxyZWN0IGhlaWdodD0iMTA4IiB3aWR0aD0iMjI4LjE0MDYyNSIgeT0iMCIgeD0iMCIgY2xhc3M9ImVyIGVudGl0eUJveCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMTQuMDcwMzEyNSwxMikiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmFub255bWl6YXRpb25fZG9jdW1lbnRfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI3Ni4yMzg3NTkzNTg3MjM5NSIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTEtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5VVUlEPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI5MS4zMTY2NzA3MzU2NzcwOCIgeT0iMjQiIHg9Ijc2LjIzODc1OTM1ODcyMzk1IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODEuMjM4NzU5MzU4NzIzOTUsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItMS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmlkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC41ODUxOTQ5MDU1OTg5NTQiIHk9IjI0IiB4PSIxNjcuNTU1NDMwMDk0NDAxMDMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNzIuNTU1NDMwMDk0NDAxMDMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9Ijc2LjIzODc1OTM1ODcyMzk1IiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTItdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5VVUlEPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI5MS4zMTY2NzA3MzU2NzcwOCIgeT0iNDUiIHg9Ijc2LjIzODc1OTM1ODcyMzk1IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgxLjIzODc1OTM1ODcyMzk1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTItbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5kb2N1bWVudF9pZDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuNTg1MTk0OTA1NTk4OTU0IiB5PSI0NSIgeD0iMTY3LjU1NTQzMDA5NDQwMTAzIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE3Mi41NTU0MzAwOTQ0MDEwMyw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNzYuMjM4NzU5MzU4NzIzOTUiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iOTEuMzE2NjcwNzM1Njc3MDgiIHk9IjY2IiB4PSI3Ni4yMzg3NTkzNTg3MjM5NSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgxLjIzODc1OTM1ODcyMzk1LDc2LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTMtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5wYXJhZ3JhcGhfaWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYwLjU4NTE5NDkwNTU5ODk1NCIgeT0iNjYiIHg9IjE2Ny41NTU0MzAwOTQ0MDEwMyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE3Mi41NTU0MzAwOTQ0MDEwMyw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWFub255bWl6YXRpb25kb2N1bWVudHBhcmFncmFwaC05MGMwMzBmMC1mNzYzLTU3NWUtOGU3Ni0yNmU1M2Y4YjRmYzktYXR0ci0zLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNzYuMjM4NzU5MzU4NzIzOTUiIHk9Ijg3IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPklOVEVHRVI8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjkxLjMxNjY3MDczNTY3NzA4IiB5PSI4NyIgeD0iNzYuMjM4NzU5MzU4NzIzOTUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODEuMjM4NzU5MzU4NzIzOTUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1hbm9ueW1pemF0aW9uZG9jdW1lbnRwYXJhZ3JhcGgtOTBjMDMwZjAtZjc2My01NzVlLThlNzYtMjZlNTNmOGI0ZmM5LWF0dHItNC1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPm9yZGVyPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC41ODUxOTQ5MDU1OTg5NTQiIHk9Ijg3IiB4PSIxNjcuNTU1NDMwMDk0NDAxMDMiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTcyLjU1NTQzMDA5NDQwMTAzLDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktYW5vbnltaXphdGlvbmRvY3VtZW50cGFyYWdyYXBoLTkwYzAzMGYwLWY3NjMtNTc1ZS04ZTc2LTI2ZTUzZjhiNGZjOS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTU2LjE4MzU5Mzc1LDMwLjUgKSIgaWQ9ImVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1Ij48cmVjdCBoZWlnaHQ9IjEyOSIgd2lkdGg9IjE0NS45MDQzNzMxNjg5NDUzIiB5PSIwIiB4PSIwIiBjbGFzcz0iZXIgZW50aXR5Qm94Ii8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IHRleHQtYW5jaG9yOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyLjk1MjE4NjU4NDQ3MjY2LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+ZGF0YXB1YmxpY19kb2N1bWVudDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9IjI0IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSIyNCIgeD0iNjAuOTUyNTYwNDI0ODA0NjkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS45NTI1NjA0MjQ4MDQ2OSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSIyNCIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTEta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPlBLPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MC45NTI1NjA0MjQ4MDQ2OSIgeT0iNDUiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+SlNPTjwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSI0NSIgeD0iNjAuOTUyNTYwNDI0ODA0NjkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnByZWRpY3Rpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSI0NSIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4xMDkzOTAyNTg3ODkwNiw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci0yLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYwLjk1MjU2MDQyNDgwNDY5IiB5PSI2NiIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMy10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjE1NjgyOTgzMzk4NDM3NSIgeT0iNjYiIHg9IjYwLjk1MjU2MDQyNDgwNDY5IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksNzYuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItMy1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnZhbGlkYXRpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjc5NDk4MjkxMDE1NjI1IiB5PSI2NiIgeD0iMTIyLjEwOTM5MDI1ODc4OTA2IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDc2LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTMta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9Ijg3IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnQtYTBiZTViOTQtOTNkNi01MTZiLTg1YWEtNTliNmM2ZTRjODY1LWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkRBVEVUSU1FPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4xNTY4Mjk4MzM5ODQzNzUiIHk9Ijg3IiB4PSI2MC45NTI1NjA0MjQ4MDQ2OSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS45NTI1NjA0MjQ4MDQ2OSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudC1hMGJlNWI5NC05M2Q2LTUxNmItODVhYS01OWI2YzZlNGM4NjUtYXR0ci00LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+Y3JlYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMjMuNzk0OTgyOTEwMTU2MjUiIHk9Ijg3IiB4PSIxMjIuMTA5MzkwMjU4Nzg5MDYiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjEwOTM5MDI1ODc4OTA2LDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTQta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjAuOTUyNTYwNDI0ODA0NjkiIHk9IjEwOCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMTU2ODI5ODMzOTg0Mzc1IiB5PSIxMDgiIHg9IjYwLjk1MjU2MDQyNDgwNDY5IiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuOTUyNTYwNDI0ODA0NjksMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj51cGRhdGVkX2F0PC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIyMy43OTQ5ODI5MTAxNTYyNSIgeT0iMTA4IiB4PSIxMjIuMTA5MzkwMjU4Nzg5MDYiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMTA5MzkwMjU4Nzg5MDYsMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50LWEwYmU1Yjk0LTkzZDYtNTE2Yi04NWFhLTU5YjZjNmU0Yzg2NS1hdHRyLTUta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODAyLjA4Nzk2NjkxODk0NTMsMjAgKSIgaWQ9ImVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4YiI+PHJlY3QgaGVpZ2h0PSIxNTAiIHdpZHRoPSIxNDYuMDkzNzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuMDQ2ODc1LDEyKSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRhdGFwdWJsaWNfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4wMTU2ODYwMzUxNTYyNSIgeT0iMjQiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0xLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjI0IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjAxNTY4NjAzNTE1NjI1LDM0LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0xLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+aWQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iMjQiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4yMzU2NDE0Nzk0OTIxOSwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMS1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEs8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSI0NSIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDU1LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VEVYVDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjQ1IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Ni4wMTU2ODYwMzUxNTYyNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnRleHQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iNDUiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTIta2V5IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiLz48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMDE1Njg2MDM1MTU2MjUiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjIxOTk1NTQ0NDMzNTk0IiB5PSI2NiIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2Ni4wMTU2ODYwMzUxNTYyNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPnByZWRpY3Rpb248L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iNjYiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyNy4yMzU2NDE0Nzk0OTIxOSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItMy1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4wMTU2ODYwMzUxNTYyNSIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNC10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkpTT048L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjIxOTk1NTQ0NDMzNTk0IiB5PSI4NyIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjYuMDE1Njg2MDM1MTU2MjUsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTQtbmFtZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj52YWxpZGF0aW9uPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSIyMy44NTgxMDg1MjA1MDc4MTIiIHk9Ijg3IiB4PSIxMjIuMjM1NjQxNDc5NDkyMTkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI3LjIzNTY0MTQ3OTQ5MjE5LDk3LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci00LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSIxMDgiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNS10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPkRBVEVUSU1FPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2MS4yMTk5NTU0NDQzMzU5NCIgeT0iMTA4IiB4PSI2MS4wMTU2ODYwMzUxNTYyNSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY2LjAxNTY4NjAzNTE1NjI1LDExOC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNwYXJhZ3JhcGgtYTdhMTY0NWUtYTNlNy01MjM3LTgzY2UtMzFhNWQ3NTA2ODhiLWF0dHItNS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmNyZWF0ZWRfYXQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjIzLjg1ODEwODUyMDUwNzgxMiIgeT0iMTA4IiB4PSIxMjIuMjM1NjQxNDc5NDkyMTkiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hPZGQiLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksMTE4LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci01LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjYxLjAxNTY4NjAzNTE1NjI1IiB5PSIxMjkiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSwxMzkuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljcGFyYWdyYXBoLWE3YTE2NDVlLWEzZTctNTIzNy04M2NlLTMxYTVkNzUwNjg4Yi1hdHRyLTYtdHlwZSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5EQVRFVElNRTwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjEuMjE5OTU1NDQ0MzM1OTQiIHk9IjEyOSIgeD0iNjEuMDE1Njg2MDM1MTU2MjUiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjYuMDE1Njg2MDM1MTU2MjUsMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci02LW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+dXBkYXRlZF9hdDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iMjMuODU4MTA4NTIwNTA3ODEyIiB5PSIxMjkiIHg9IjEyMi4yMzU2NDE0Nzk0OTIxOSIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjcuMjM1NjQxNDc5NDkyMTksMTM5LjUpIiB5PSIwIiB4PSIwIiBpZD0idGV4dC1lbnRpdHktZGF0YXB1YmxpY3BhcmFncmFwaC1hN2ExNjQ1ZS1hM2U3LTUyMzctODNjZS0zMWE1ZDc1MDY4OGItYXR0ci02LWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY0OS40MDA5MzYxMjY3MDksMjcwICkiIGlkPSJlbnRpdHktZGF0YXB1YmxpY2RvY3VtZW50cGFyYWdyYXBoLWVmMGYzODg3LTJkYmEtNTc2ZS04NjM0LTg1NjI3OGI2OGI0NiI+PHJlY3QgaGVpZ2h0PSIxMDgiIHdpZHRoPSIyMDUuNDY4NzUiIHk9IjAiIHg9IjAiIGNsYXNzPSJlciBlbnRpdHlCb3giLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgdGV4dC1hbmNob3I6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAyLjczNDM3NSwxMikiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2IiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRhdGFwdWJsaWNfZG9jdW1lbnRfcGFyYWdyYXBoPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI2OC42ODE0Njc2OTIwNTczIiB5PSIyNCIgeD0iMCIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMS10eXBlIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPlVVSUQ8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjgzLjc1OTM3OTA2OTAxMDQyIiB5PSIyNCIgeD0iNjguNjgxNDY3NjkyMDU3MyIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsMzQuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMS1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmlkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iMjQiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE1Ny40NDA4NDY3NjEwNjc3MiwzNC41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0xLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNjgxNDY3NjkyMDU3MyIgeT0iNDUiIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1NS41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0yLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9IjQ1IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMi1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPmRvY3VtZW50X2lkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iNDUiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNTcuNDQwODQ2NzYxMDY3NzIsNTUuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItMi1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+UEssRks8L3RleHQ+PHJlY3QgaGVpZ2h0PSIyMSIgd2lkdGg9IjY4LjY4MTQ2NzY5MjA1NzMiIHk9IjY2IiB4PSIwIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+VVVJRDwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9IjY2IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94T2RkIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzMuNjgxNDY3NjkyMDU3Myw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLW5hbWUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+cGFyYWdyYXBoX2lkPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iNjYiIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveE9kZCIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE1Ny40NDA4NDY3NjEwNjc3Miw3Ni41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci0zLWtleSIgY2xhc3M9ImVyIGVudGl0eUxhYmVsIj5QSyxGSzwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iNjguNjgxNDY3NjkyMDU3MyIgeT0iODciIHg9IjAiIGNsYXNzPSJlciBhdHRyaWJ1dGVCb3hFdmVuIi8+PHRleHQgc3R5bGU9ImRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTAuMnB4OyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw5Ny41KSIgeT0iMCIgeD0iMCIgaWQ9InRleHQtZW50aXR5LWRhdGFwdWJsaWNkb2N1bWVudHBhcmFncmFwaC1lZjBmMzg4Ny0yZGJhLTU3NmUtODYzNC04NTYyNzhiNjhiNDYtYXR0ci00LXR5cGUiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCI+SU5URUdFUjwvdGV4dD48cmVjdCBoZWlnaHQ9IjIxIiB3aWR0aD0iODMuNzU5Mzc5MDY5MDEwNDIiIHk9Ijg3IiB4PSI2OC42ODE0Njc2OTIwNTczIiBjbGFzcz0iZXIgYXR0cmlidXRlQm94RXZlbiIvPjx0ZXh0IHN0eWxlPSJkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEwLjJweDsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczLjY4MTQ2NzY5MjA1NzMsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItNC1uYW1lIiBjbGFzcz0iZXIgZW50aXR5TGFiZWwiPm9yZGVyPC90ZXh0PjxyZWN0IGhlaWdodD0iMjEiIHdpZHRoPSI1My4wMjc5MDMyMzg5MzIyOSIgeT0iODciIHg9IjE1Mi40NDA4NDY3NjEwNjc3MiIgY2xhc3M9ImVyIGF0dHJpYnV0ZUJveEV2ZW4iLz48dGV4dCBzdHlsZT0iZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMC4ycHg7IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNTcuNDQwODQ2NzYxMDY3NzIsOTcuNSkiIHk9IjAiIHg9IjAiIGlkPSJ0ZXh0LWVudGl0eS1kYXRhcHVibGljZG9jdW1lbnRwYXJhZ3JhcGgtZWYwZjM4ODctMmRiYS01NzZlLTg2MzQtODU2Mjc4YjY4YjQ2LWF0dHItNC1rZXkiIGNsYXNzPSJlciBlbnRpdHlMYWJlbCIvPjwvZz48cmVjdCBoZWlnaHQ9IjE0IiB3aWR0aD0iMjQuMDE1NjI1IiB5PSIyMTIuNjYzMDQwMTYxMTMyOCIgeD0iMTA0LjIyOTUwNzQ0NjI4OTA2IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWxCb3giLz48dGV4dCBzdHlsZT0idGV4dC1hbmNob3I6IG1pZGRsZTsgZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgeT0iMjE5LjY2MzA0MDE2MTEzMjgiIHg9IjExNi4yMzczMTk5NDYyODkwNiIgaWQ9InJlbDI1IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWwiPmxpbmtzPC90ZXh0PjxyZWN0IGhlaWdodD0iMTQiIHdpZHRoPSIyNC4wMTU2MjUiIHk9IjIyMS42NzgwNzAwNjgzNTkzOCIgeD0iMzQyLjU2NDE3ODQ2Njc5NjkiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbEJveCIvPjx0ZXh0IHN0eWxlPSJ0ZXh0LWFuY2hvcjogbWlkZGxlOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyBmb250LXNpemU6IDEycHg7IiB5PSIyMjguNjc4MDcwMDY4MzU5MzgiIHg9IjM1NC41NzE5OTA5NjY3OTY5IiBpZD0icmVsMjYiIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbCI+bGlua3M8L3RleHQ+PHJlY3QgaGVpZ2h0PSIxNCIgd2lkdGg9IjI0LjAxNTYyNSIgeT0iMjE2LjI1NzAxOTA0Mjk2ODc1IiB4PSI2MjkuOTIyNjA3NDIxODc1IiBjbGFzcz0iZXIgcmVsYXRpb25zaGlwTGFiZWxCb3giLz48dGV4dCBzdHlsZT0idGV4dC1hbmNob3I6IG1pZGRsZTsgZG9taW5hbnQtYmFzZWxpbmU6IG1pZGRsZTsgZm9udC1zaXplOiAxMnB4OyIgeT0iMjIzLjI1NzAxOTA0Mjk2ODc1IiB4PSI2NDEuOTMwNDE5OTIxODc1IiBpZD0icmVsMjciIGNsYXNzPSJlciByZWxhdGlvbnNoaXBMYWJlbCI+bGlua3M8L3RleHQ+PHJlY3QgaGVpZ2h0PSIxNCIgd2lkdGg9IjI0LjAxNTYyNSIgeT0iMjIwLjc4MTM1NjgxMTUyMzQ0IiB4PSI4NDguMDA0MDg5MzU1NDY4OCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExhYmVsQm94Ii8+PHRleHQgc3R5bGU9InRleHQtYW5jaG9yOiBtaWRkbGU7IGRvbWluYW50LWJhc2VsaW5lOiBtaWRkbGU7IGZvbnQtc2l6ZTogMTJweDsiIHk9IjIyNy43ODEzNTY4MTE1MjM0NCIgeD0iODYwLjAxMTkwMTg1NTQ2ODgiIGlkPSJyZWwyOCIgY2xhc3M9ImVyIHJlbGF0aW9uc2hpcExhYmVsIj5saW5rczwvdGV4dD48L3N2Zz4=", + "version": 2 + } + } +} \ No newline at end of file diff --git a/docs/database/schema.jpg b/docs/database/schema.jpg deleted file mode 100644 index 0c37c647..00000000 Binary files a/docs/database/schema.jpg and /dev/null differ diff --git a/docs/database/schema.mmd b/docs/database/schema.mmd new file mode 100644 index 00000000..c45aa17a --- /dev/null +++ b/docs/database/schema.mmd @@ -0,0 +1,53 @@ +erDiagram + anonymization_document { + UUID id PK + DATETIME created_at + DATETIME updated_at + TEXT name + } + + anonymization_paragraph { + UUID id PK + TEXT text + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + anonymization_document_paragraph { + UUID id PK + UUID document_id PK, FK + UUID paragraph_id PK, FK + INTEGER order + } + + datapublic_document { + UUID id PK + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + datapublic_paragraph { + UUID id PK + TEXT text + JSON prediction + JSON validation + DATETIME created_at + DATETIME updated_at + } + + datapublic_document_paragraph { + UUID id PK + UUID document_id PK, FK + UUID paragraph_id PK, FK + INTEGER order + } + + anonymization_document ||--o{ anonymization_document_paragraph : links + anonymization_paragraph ||--o{ anonymization_document_paragraph : links + + datapublic_document ||--o{ datapublic_document_paragraph : links + datapublic_paragraph ||--o{ datapublic_document_paragraph : links diff --git a/docs/database/schema.png b/docs/database/schema.png new file mode 100644 index 00000000..522c5e57 Binary files /dev/null and b/docs/database/schema.png differ diff --git a/docs/entities/README.md b/docs/entities/README.md new file mode 100644 index 00000000..e12055f7 --- /dev/null +++ b/docs/entities/README.md @@ -0,0 +1,17 @@ +# Entities +Language: **English** | [Español](../es/entities/README.md) + +This section documents the entity catalogs used by AymurAI workflows. + +## Entity catalogs by flow +- Datapublic: [datapublic/README.md](datapublic/README.md) +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) + +## Notes +- `datapublic` entities describe the public-data extraction taxonomy used by the production `datapublic` pipeline. +- `anonymizer` entities describe the labels currently used by the anonymization flow for disambiguation and replacement. + +## Related docs +- Documentation index: [../README.md](../README.md) +- Pipelines index: [../pipelines/README.md](../pipelines/README.md) +- Models index: [../models/README.md](../models/README.md) diff --git a/docs/entities/anonymizer/README.md b/docs/entities/anonymizer/README.md new file mode 100644 index 00000000..2ae23e12 --- /dev/null +++ b/docs/entities/anonymizer/README.md @@ -0,0 +1,45 @@ +# Anonymizer Entities +Language: **English** | [Español](../../es/entities/anonymizer/README.md) + +Catalog of labels currently used by the anonymization flow. + +## Scope +These labels represent the entity types recognized and transformed by the production `flair-anonymizer` pipeline. They are the labels on which disambiguation, anonymization policies, and render policies operate. + +## Labels +| Label | Description | +|---|---| +| `PER` | Person name or person mention. | +| `EDAD` | Age. | +| `DNI` | Argentine national identity document number. | +| `NACIONALIDAD` | Nationality. | +| `ESTUDIOS` | Education or level of studies. | +| `DIRECCION` | Postal or street address. | +| `LOC` | Geographic location or place reference. | +| `TELEFONO` | Phone number. | +| `CORREO_ELECTRONICO` | Email address. | +| `FECHA` | Calendar date. | +| `NUM_EXPEDIENTE` | Case or file number. | +| `CUIJ` | Judicial unique case identifier. | +| `NUM_ACTUACION` | Proceeding or action number. | +| `NUM_MATRICULA` | Registration or professional license number. | +| `NOMBRE_ARCHIVO` | File name. | +| `TEXTO_ANONIMIZAR` | Free-form text span explicitly marked for anonymization when it does not fit a more specific structured label. | +| `USUARIX` | Username, account handle, or user identifier. | +| `LINK` | URL or web link. | +| `IP` | IP address. | +| `CUIT_CUIL` | Argentine tax or labor identifier. | +| `BANCO` | Bank name. | +| `CBU` | Argentine bank account CBU. | +| `NUM_CAJA_AHORRO` | Savings account number. | +| `MARCA_AUTOMOVIL` | Vehicle make or model reference. | +| `PATENTE_DOMINIO` | Vehicle license plate. | + +## Notes +- This catalog reflects the current anonymization token set used by the backend replacement utilities. +- Individual labels may be configured with per-label anonymization or disambiguation policies through `LabelPolicy`. + +## Related docs +- Entities index: [../README.md](../README.md) +- Anonymizer pipeline: [../../pipelines/anonymizer/README.md](../../pipelines/anonymizer/README.md) +- API reference: [../../api/README.md](../../api/README.md) diff --git a/docs/entities/datapublic/README.md b/docs/entities/datapublic/README.md new file mode 100644 index 00000000..578e1c30 --- /dev/null +++ b/docs/entities/datapublic/README.md @@ -0,0 +1,47 @@ +# Datapublic Entities +Language: **English** | [Español](../../es/entities/datapublic/README.md) + +Catalog of entities used by the datapublic extraction flow. + +## Scope +These entities correspond to the taxonomy extracted from judicial rulings by the production `datapublic` pipeline. They are used across the Flair NER model, decision post-processing, validation UI, and related dataset tooling. + +## Entities +| Entity | Description | +|---|---| +| `ART_INFRINGIDO` | Article(s) or legal provision(s) infringed in the case. | +| `CONDUCTA` | Action associated with the offence, contravention, or misdemeanor described in the infringed article. | +| `CONDUCTA_DESCRIPCION` | Additional characterization of the conduct, such as aggravating circumstances or modality details. | +| `DECISION` | Text fragment denoting a judicial decision. In production this is a synthetic label emitted by the decision classifier. | +| `DETALLE` | Detail that specifies what was resolved within `OBJETO_DE_LA_RESOLUCION`. | +| `EDAD_AL_MOMENTO_DEL_HECHO` | Age of the person at the time of the event. | +| `FECHA_DE_NACIMIENTO` | Date of birth. | +| `FECHA_DEL_HECHO` | Date on which the reported event occurred. | +| `FECHA_RESOLUCION` | Date of the resolution; for oral hearings, the hearing start date. | +| `FRASES_AGRESION` | Quoted or paraphrased phrases described as verbal aggression within the facts of the case. | +| `GENERO` | Gender. | +| `HIJOS_HIJAS_EN_COMUN` | Whether the accused person and the complainant have children in common. | +| `HORA_DE_CIERRE` | End time of the hearing. | +| `HORA_DE_INICIO` | Start time of the hearing. | +| `LUGAR_DEL_HECHO` | Physical or mediated location where the facts occurred. | +| `MODALIDAD_DE_LA_VIOLENCIA` | Modality in which violence manifests, such as domestic, institutional, labor, media, or public-space violence. | +| `N_EXPTE_EJE` | Case or file identifier. | +| `NACIONALIDAD` | Nationality. | +| `NIVEL_INSTRUCCION` | Level of formal education attained by the person. | +| `NOMBRE` | Person name. | +| `OBJETO_DE_LA_RESOLUCION` | What the court resolved about. | +| `PERSONA_ACUSADA_NO_DETERMINADA` | Non-natural or undetermined accused party, such as a legal entity or online account. | +| `RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE` | Relationship type between the accused person and the complainant. | +| `TIPO_DE_RESOLUCION` | Type of resolution, such as interlocutory or final resolution. | +| `VIOLENCIA_DE_GENERO` | Whether the investigated facts occur within a gender-violence context. | + +## Subcategories and validation fields +- Many of these entities have subclassification or validation-specific controlled vocabularies. +- Historical validation options are captured in the Label Studio template: [../../../resources/annotations/label-studio/datapublic/label-studio-config.xml](../../../resources/annotations/label-studio/datapublic/label-studio-config.xml) +- A broader explanatory glossary is available here (Spanish): https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit + +## Related docs +- Entities index: [../README.md](../README.md) +- Datapublic pipeline: [../../pipelines/datapublic/README.md](../../pipelines/datapublic/README.md) +- Decision model card: [../../models/decision-model-card.md](../../models/decision-model-card.md) +- Flair model card: [../../models/flair-model-card.md](../../models/flair-model-card.md) diff --git a/docs/es/README.md b/docs/es/README.md new file mode 100644 index 00000000..5998d7cc --- /dev/null +++ b/docs/es/README.md @@ -0,0 +1,28 @@ +# Índice de Documentación +Idioma: [English](../README.md) | **Español** + +Este índice cubre la documentación operativa para `v1.5.0`. + +## Documentos principales +- Entrada del repositorio: [../../README.es.md](../../README.es.md) +- Referencia de API: [api/README.md](api/README.md) +- Índice de pipelines: [pipelines/README.md](pipelines/README.md) +- Flujo anonymizer: [pipelines/anonymizer/README.md](pipelines/anonymizer/README.md) +- Flujo datapublic: [pipelines/datapublic/README.md](pipelines/datapublic/README.md) +- Esquema de base de datos interna: [database/README.md](database/README.md) + +## Entidades +- Índice de entidades: [entities/README.md](entities/README.md) +- Entidades de datapublic: [entities/datapublic/README.md](entities/datapublic/README.md) +- Entidades de anonymizer: [entities/anonymizer/README.md](entities/anonymizer/README.md) + +## Modelos +- Índice de modelos: [models/README.md](models/README.md) +- Model card del NER de anonymizer: [models/anonymizer-model-card.md](models/anonymizer-model-card.md) +- Model card de Flair NER: [models/flair-model-card.md](models/flair-model-card.md) +- Model card de Decision: [models/decision-model-card.md](models/decision-model-card.md) + +## Gobernanza +- Contribución: [../CONTRIBUTING.md](../CONTRIBUTING.md) +- Seguridad y ética: [../SECURITY.md](../SECURITY.md) +- Código de conducta: [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) diff --git a/docs/es/api/README.md b/docs/es/api/README.md new file mode 100644 index 00000000..adb3db4e --- /dev/null +++ b/docs/es/api/README.md @@ -0,0 +1,321 @@ +# Referencia de API +Idioma: [English](../../api/README.md) | **Español** + +Este documento describe la API pública actualmente montada en `aymurai/api/main.py` + `aymurai/api/core.py`. + +## Base URL y OpenAPI +- Base local: `http://localhost:8899` +- Swagger UI: `http://localhost:8899/docs` +- OpenAPI JSON: `http://localhost:8899/openapi.json` + +## Endpoints públicos (montados) + +| Método | Path | Propósito | +|---|---|---| +| `GET` | `/server/healthcheck` | Liveness del servicio | +| `GET` | `/server/stats/summary` | Métricas de CPU/memoria | +| `POST` | `/document-extract` | Alias deprecado de `/misc/document-extract` | +| `POST` | `/misc/document-extract` | Extrae párrafos normalizados de un documento | +| `POST` | `/anonymizer/predict` | Predicción NER por párrafo | +| `POST` | `/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas | +| `POST` | `/anonymizer/validation` | Obtiene validación manual por párrafo | +| `POST` | `/anonymizer/anonymize-document` | Compila y exporta documento anonimizado | +| `POST` | `/datapublic/predict/{document_id}` | Predicción para flujo data-public | +| `GET` | `/datapublic/validation/document/{document_id}` | Lee validación a nivel documento | +| `POST` | `/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento | +| `POST` | `/convert/pdf/odt` | Convierte PDF a ODT | +| `POST` | `/convert/pdf/docx` | Convierte PDF a DOCX | +| `POST` | `/convert/docx/odt` | Convierte DOCX a ODT | +| `POST` | `/convert/docx/pdf` | Convierte DOCX a PDF | +| `POST` | `/convert/odt/pdf` | Convierte ODT a PDF | +| `POST` | `/convert/odt/docx` | Convierte ODT a DOCX | + +## Contratos de datos principales + +Nota: los snippets JSON de abajo son ejemplos mínimos válidos. Los payloads reales pueden incluir campos adicionales según el endpoint y la etapa de procesamiento. + +### `TextRequest` +```json +{ + "text": "Acusado: Ramiro Marrón DNI 34.555.666." +} +``` + +### `EntityAttributes` (campos relevantes) +```json +{ + "aymurai_label": "PER", + "aymurai_label_subclass": [], + "aymurai_method": "flair", + "aymurai_score": 0.97, + "canonical_entity_id": "a3f8b60f-8e7e-4c8b-9de2-ec8dd3f44c12", + "aymurai_label_instance": 1, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": true +} +``` + +### `DocLabel` +```json +{ + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } +} +``` + +### `DocumentInformation` +```json +{ + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [ + { + "text": "Ramiro Marrón", + "start_char": 9, + "end_char": 22, + "attrs": { + "aymurai_label": "PER" + } + } + ] +} +``` + +### `LabelPolicy` +```json +{ + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": true +} +``` + +### `RenderPolicy` +```json +{ + "suffix_mode": "auto", + "suffix_threshold": 1 +} +``` + +### `DocumentAnnotations` +```json +{ + "data": [ + { + "document": "...", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + }, + "render_policy": { + "suffix_mode": "auto", + "suffix_threshold": 1 + } +} +``` + +## Detalle de endpoints y ejemplos + +### Server + +#### `GET /server/healthcheck` +- Respuesta `200`: + +```json +{"status": "ok"} +``` + +```bash +curl -s http://localhost:8899/server/healthcheck +``` + +#### `GET /server/stats/summary` +- Respuesta `200` (forma): + +```json +{ + "is_docker": true, + "cpu_core_limit": 4, + "cpu_usage_percent": 0.0, + "memory_limit_mb": 4096.0, + "memory_usage_mb": 823.5 +} +``` + +```bash +curl -s http://localhost:8899/server/stats/summary +``` + +### Extracción de documentos + +#### `POST /misc/document-extract` +#### `POST /document-extract` (alias deprecado) +- Request: `multipart/form-data` con `file` +- MIME types soportados en extracción: DOCX, ODT, PDF +- Respuesta `200`: + +```json +{ + "document": ["Párrafo 1", "Párrafo 2"], + "document_id": "f2b25507-cf88-5b11-8f2a-c0b6f940b7f8" +} +``` + +```bash +curl -s -X POST \ + -F "file=@/resources/data/sample/document-01.docx" \ + http://localhost:8899/misc/document-extract +``` + +Errores comunes: +- `504` timeout de extracción +- `500` errores del extractor/internos + +### Anonymizer + +#### `POST /anonymizer/predict` +- Body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Respuesta `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /anonymizer/disambiguate` +- Body request: + +```json +{ + "paragraphs": [ + { + "document": "Acusado: Ramiro Marrón DNI 34.555.666.", + "labels": [] + } + ], + "label_policies": { + "PER": { + "anonymize": true, + "disambiguation": "fuzzy", + "use_subclass_when_available": false + } + } +} +``` + +- Respuesta `200`: `DocumentAnnotations` (incluye `data` y `label_policies` efectivas) + +```bash +curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ + -H "Content-Type: application/json" \ + -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' +``` + +#### `POST /anonymizer/validation` +- Body: `TextRequest` +- Respuesta `200`: `list[DocLabel] | null` + +```bash +curl -s -X POST http://localhost:8899/anonymizer/validation \ + -H "Content-Type: application/json" \ + -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' +``` + +#### `POST /anonymizer/anonymize-document` +- Request: `multipart/form-data` + - `file`: documento original (`.docx`, `.pdf`, `.odt`) + - `annotations`: string JSON serializado de `DocumentAnnotations` +- Respuesta `200`: archivo `.odt` anonimizado (binario) + +```bash +curl -X POST http://localhost:8899/anonymizer/anonymize-document \ + -F "file=@/resources/data/sample/document-01.docx" \ + -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' +``` + +Errores comunes: +- `400` payload multipart inválido +- `500` errores de anonimización/conversión + +### Data-Public + +#### `POST /datapublic/predict/{document_id}` +- Path param: `document_id` (`UUID5`) +- Body: `TextRequest` +- Query param: `use_cache=true|false` (default `true`) +- Respuesta `200`: `DocumentInformation` + +```bash +curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ + -H "Content-Type: application/json" \ + -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' +``` + +#### `GET /datapublic/validation/document/{document_id}` +- Respuesta `200`: objeto o `null` +- Respuesta `404`: documento inexistente + +```bash +curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +``` + +#### `POST /datapublic/validation/document/{document_id}` +- Body: objeto JSON libre (se persiste como validación a nivel documento) +- Respuesta `200`: body vacío + +```bash +curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ + -H "Content-Type: application/json" \ + -d '{"materia":"penal","violencia_de_genero":"si"}' +``` + +### Conversión de documentos + +Todos los endpoints de conversión usan `multipart/form-data` con campo `file`. + +| Método | Path | Input | Output | +|---|---|---|---| +| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/convert/odt/docx` | `.odt` | `.docx` | + +Para endpoints con input PDF, query param opcional: +- `backend=libreoffice|pandoc` (default: `libreoffice`) + +Ejemplo: + +```bash +curl -X POST "http://localhost:8899/convert/pdf/docx?backend=libreoffice" \ + -F "file=@input.pdf" -o output.docx +``` + +Errores comunes: +- `400` extensión de entrada no soportada +- `500` falla de herramienta de conversión + +## Legacy / no pública (no montada) +Las siguientes rutas existen en código pero no están incluidas en `core.router` en runtime: + +- `aymurai/api/endpoints/routers/datapublic/dataset.py` + - define `/datapublic/dataset/*`, pero ese router no está montado. +- `aymurai/api/endpoints/routers/anonymizer/database.py` + - define `/anonymizer/database/*`, pero su include está comentado. +- `aymurai/api/endpoints/routers/database/*` + - rutas administrativas de DB, sin montaje en `core.router`. + +Estas rutas deben tratarse como caminos internos/legacy hasta su exposición explícita. diff --git a/docs/es/database/README.md b/docs/es/database/README.md new file mode 100644 index 00000000..7fee93a5 --- /dev/null +++ b/docs/es/database/README.md @@ -0,0 +1,113 @@ +# Base de Datos Interna +Idioma: [English](../../database/README.md) | **Español** + +Este documento describe la persistencia interna usada por la API en runtime. + +## Motor y migraciones +- ORM: `SQLModel` (backend `sqlalchemy`) +- Herramienta de migración: Alembic +- Setting de runtime: `SQLALCHEMY_DATABASE_URI` +- URI por defecto: `sqlite:////resources/cache/sqlite/database.db` +- Comportamiento al iniciar: + 1. La API valida conectividad de DB. + 2. Si falta el archivo SQLite, crea los directorios padre. + 3. Ejecuta `alembic upgrade head` al iniciar. + +Código relacionado: +- `aymurai/settings.py` +- `aymurai/api/startup/database.py` +- `aymurai/api/main.py` +- `aymurai/database/versions/13f78d08e925_create_database.py` + +## Diagrama ER +![Diagrama ER de la base de datos](../../database/schema.png) + +Fuente editable: [../../database/schema.mmd](../../database/schema.mmd) + +## Tablas + +### `anonymization_document` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | +| `name` | `TEXT` | no | Nombre de archivo original | + +### `anonymization_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derivada del hash del texto | +| `text` | `TEXT` | no | Texto normalizado del párrafo | +| `prediction` | `JSON` | sí | Predicciones del modelo (`list[DocLabel]`) | +| `validation` | `JSON` | sí | Etiquetas validadas manualmente (`list[DocLabel]`) | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `anonymization_document_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Identificador del vínculo | +| `document_id` | `UUID` | no | FK -> `anonymization_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `anonymization_paragraph.id` | +| `order` | `INTEGER` | sí | Orden del párrafo en documento origen | + +La clave primaria es compuesta por `id`, `document_id`, `paragraph_id`. + +### `datapublic_document` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key (identificador del documento) | +| `prediction` | `JSON` | sí | Payload reservado de predicción a nivel documento; el router público actual no lo escribe | +| `validation` | `JSON` | sí | Payload de validación a nivel documento | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `datapublic_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Primary key, derivada del hash del texto | +| `text` | `TEXT` | no | Texto normalizado del párrafo | +| `prediction` | `JSON` | sí | Predicciones del modelo (`list[DocLabel]`) | +| `validation` | `JSON` | sí | Reservado para validación por párrafo; la ruta pública hoy existe solo como lógica legacy comentada | +| `created_at` | `DATETIME` | no | Default server `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME` | sí | Se actualiza ante cambios | + +### `datapublic_document_paragraph` +| Columna | Tipo | Nulo | Notas | +|---|---|---|---| +| `id` | `UUID` | no | Identificador del vínculo | +| `document_id` | `UUID` | no | FK -> `datapublic_document.id` | +| `paragraph_id` | `UUID` | no | FK -> `datapublic_paragraph.id` | +| `order` | `INTEGER` | sí | Orden del párrafo en documento origen | + +La clave primaria es compuesta por `id`, `document_id`, `paragraph_id`. + +## Mapeo endpoint -> persistencia + +### Anonymizer +- `POST /anonymizer/predict` + - Lee `anonymization_paragraph` por UUID de párrafo. + - Escribe `anonymization_paragraph.prediction` cuando el cache está activo. +- `POST /anonymizer/disambiguate` + - Escribe predicciones desambiguadas en `anonymization_paragraph.prediction`. +- `POST /anonymizer/validation` + - Lee `anonymization_paragraph.validation`. +- `POST /anonymizer/anonymize-document` + - Escribe `anonymization_paragraph.validation`. + - Crea `anonymization_document` con clave derivada del hash del contenido binario subido. + - Crea vínculos en `anonymization_document_paragraph`. + +### Data-public +- `POST /datapublic/predict/{document_id}` + - Usa el `document_id` provisto por el cliente como primary key del documento. + - Asegura existencia de `datapublic_document` cuando `use_cache=true`. + - Escribe `datapublic_paragraph.prediction` cuando `use_cache=true`. + - Escribe vínculo en `datapublic_document_paragraph` cuando `use_cache=true`. +- `GET /datapublic/validation/document/{document_id}` + - Lee `datapublic_document.validation`. +- `POST /datapublic/validation/document/{document_id}` + - Hace upsert de `datapublic_document.validation`. + +## Nota legacy +Los módulos de rutas para CRUD de dataset (`/datapublic/dataset/*`) existen en código pero no están montados en el router público. No deben tratarse como API pública activa hasta su exposición en `core.router`. diff --git a/docs/es/entities/README.md b/docs/es/entities/README.md new file mode 100644 index 00000000..59fb1247 --- /dev/null +++ b/docs/es/entities/README.md @@ -0,0 +1,17 @@ +# Entidades +Idioma: [English](../../entities/README.md) | **Español** + +Esta sección documenta los catálogos de entidades utilizados por los distintos flujos de AymurAI. + +## Catálogos de entidades por flujo +- Datapublic: [datapublic/README.md](datapublic/README.md) +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) + +## Notas +- Las entidades de `datapublic` describen la taxonomía de extracción de datos públicos usada por el pipeline de producción `datapublic`. +- Las entidades de `anonymizer` describen las labels usadas actualmente por el flujo de anonimización para desambiguación y reemplazo. + +## Documentación relacionada +- Índice de documentación: [../README.md](../README.md) +- Índice de pipelines: [../pipelines/README.md](../pipelines/README.md) +- Índice de modelos: [../models/README.md](../models/README.md) diff --git a/docs/es/entities/anonymizer/README.md b/docs/es/entities/anonymizer/README.md new file mode 100644 index 00000000..b4fee224 --- /dev/null +++ b/docs/es/entities/anonymizer/README.md @@ -0,0 +1,45 @@ +# Entidades de Anonymizer +Idioma: [English](../../../entities/anonymizer/README.md) | **Español** + +Catálogo de labels usadas actualmente por el flujo de anonimización. + +## Alcance +Estas labels representan los tipos de entidad reconocidos y transformados por el pipeline de producción `flair-anonymizer`. Son las labels sobre las que operan la desambiguación, las políticas de anonimización y las políticas de renderizado. + +## Labels +| Label | Descripción | +|---|---| +| `PER` | Nombre de persona o mención de persona. | +| `EDAD` | Edad. | +| `DNI` | Número de documento nacional de identidad argentino. | +| `NACIONALIDAD` | Nacionalidad. | +| `ESTUDIOS` | Estudios o nivel educativo. | +| `DIRECCION` | Dirección postal o calle. | +| `LOC` | Ubicación geográfica o referencia de lugar. | +| `TELEFONO` | Número de teléfono. | +| `CORREO_ELECTRONICO` | Dirección de correo electrónico. | +| `FECHA` | Fecha calendario. | +| `NUM_EXPEDIENTE` | Número de expediente o causa. | +| `CUIJ` | Identificador judicial único de causa. | +| `NUM_ACTUACION` | Número de actuación. | +| `NUM_MATRICULA` | Número de matrícula o licencia profesional. | +| `NOMBRE_ARCHIVO` | Nombre de archivo. | +| `TEXTO_ANONIMIZAR` | Fragmento de texto libre marcado explícitamente para anonimización cuando no encaja en una label estructurada más específica. | +| `USUARIX` | Nombre de usuario, handle o identificador de cuenta. | +| `LINK` | URL o enlace web. | +| `IP` | Dirección IP. | +| `CUIT_CUIL` | Identificador tributario o laboral argentino. | +| `BANCO` | Nombre de entidad bancaria. | +| `CBU` | Clave Bancaria Uniforme argentina. | +| `NUM_CAJA_AHORRO` | Número de caja de ahorro. | +| `MARCA_AUTOMOVIL` | Referencia a marca o modelo de vehículo. | +| `PATENTE_DOMINIO` | Patente o dominio vehicular. | + +## Notas +- Este catálogo refleja el conjunto actual de tokens de anonimización usado por las utilidades de reemplazo del backend. +- Cada label puede configurarse con políticas específicas de anonimización o desambiguación mediante `LabelPolicy`. + +## Documentación relacionada +- Índice de entidades: [../README.md](../README.md) +- Pipeline anonymizer: [../../pipelines/anonymizer/README.md](../../pipelines/anonymizer/README.md) +- Referencia API: [../../api/README.md](../../api/README.md) diff --git a/docs/es/entities/datapublic/README.md b/docs/es/entities/datapublic/README.md new file mode 100644 index 00000000..e645344f --- /dev/null +++ b/docs/es/entities/datapublic/README.md @@ -0,0 +1,47 @@ +# Entidades de Datapublic +Idioma: [English](../../../entities/datapublic/README.md) | **Español** + +Catálogo de entidades utilizadas por el flujo de extracción de datapublic. + +## Alcance +Estas entidades corresponden a la taxonomía extraída de resoluciones judiciales por el pipeline de producción `datapublic`. Se usan a lo largo del modelo Flair NER, el postprocesamiento de decisiones, la UI de validación y el tooling relacionado con el dataset. + +## Entidades +| Entidad | Descripción | +|---|---| +| `ART_INFRINGIDO` | Artículo(s) o norma(s) legal(es) infringida(s) en el caso. | +| `CONDUCTA` | Acción asociada al delito, contravención o falta descripta en el artículo infringido. | +| `CONDUCTA_DESCRIPCION` | Caracterización adicional de la conducta, como agravantes o detalles de modalidad. | +| `DECISION` | Fragmento de texto que denota una decisión judicial. En producción es una label sintética emitida por el clasificador de decisiones. | +| `DETALLE` | Detalle que especifica qué se resolvió dentro de `OBJETO_DE_LA_RESOLUCION`. | +| `EDAD_AL_MOMENTO_DEL_HECHO` | Edad de la persona al momento del hecho. | +| `FECHA_DE_NACIMIENTO` | Fecha de nacimiento. | +| `FECHA_DEL_HECHO` | Fecha en que ocurrió el hecho denunciado. | +| `FECHA_RESOLUCION` | Fecha de la resolución; en audiencias orales, la fecha de inicio de la audiencia. | +| `FRASES_AGRESION` | Frases citadas o parafraseadas como agresión verbal dentro de los hechos del caso. | +| `GENERO` | Género. | +| `HIJOS_HIJAS_EN_COMUN` | Si la persona acusada y la denunciante tienen hijos/as en común. | +| `HORA_DE_CIERRE` | Hora de finalización de la audiencia. | +| `HORA_DE_INICIO` | Hora de inicio de la audiencia. | +| `LUGAR_DEL_HECHO` | Lugar físico o mediado donde ocurrieron los hechos. | +| `MODALIDAD_DE_LA_VIOLENCIA` | Modalidad en la que se manifiesta la violencia, como violencia doméstica, institucional, laboral, mediática o en espacio público. | +| `N_EXPTE_EJE` | Identificador del expediente o caso. | +| `NACIONALIDAD` | Nacionalidad. | +| `NIVEL_INSTRUCCION` | Nivel de estudios formales alcanzado por la persona. | +| `NOMBRE` | Nombre de persona. | +| `OBJETO_DE_LA_RESOLUCION` | Sobre qué resolvió el juzgado. | +| `PERSONA_ACUSADA_NO_DETERMINADA` | Parte acusada no determinada o no humana, como una persona jurídica o una cuenta en línea. | +| `RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE` | Tipo de vínculo entre la persona acusada y la denunciante. | +| `TIPO_DE_RESOLUCION` | Tipo de resolución, como interlocutoria o definitiva. | +| `VIOLENCIA_DE_GENERO` | Si los hechos investigados ocurren en un contexto de violencia de género. | + +## Subcategorías y campos de validación +- Muchas de estas entidades tienen subclasificaciones o vocabularios controlados específicos para validación. +- Las opciones históricas de validación están capturadas en el template de Label Studio: [../../../../resources/annotations/label-studio/datapublic/label-studio-config.xml](../../../../resources/annotations/label-studio/datapublic/label-studio-config.xml) +- Un glosario explicativo más amplio está disponible aquí: https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit + +## Documentación relacionada +- Índice de entidades: [../README.md](../README.md) +- Pipeline datapublic: [../../pipelines/datapublic/README.md](../../pipelines/datapublic/README.md) +- Model card de Decision: [../../models/decision-model-card.md](../../models/decision-model-card.md) +- Model card de Flair: [../../models/flair-model-card.md](../../models/flair-model-card.md) diff --git a/docs/es/models/README.md b/docs/es/models/README.md new file mode 100644 index 00000000..a843aee4 --- /dev/null +++ b/docs/es/models/README.md @@ -0,0 +1,19 @@ +# Modelos +Idioma: [English](../../models/README.md) | **Español** + +Esta sección documenta los modelos individuales utilizados por el backend. + +## Documentación disponible +- Model card del NER de anonymizer: [anonymizer-model-card.md](anonymizer-model-card.md) +- Flair NER: [flair-model-card.md](flair-model-card.md) +- Clasificador de decisiones: [decision-model-card.md](decision-model-card.md) + +## Uso actual en producción +- `flair-anonymizer` usa la model card del NER de anonymizer documentada aquí. +- `datapublic` usa el modelo Flair NER y el clasificador de decisiones. + +## Documentación relacionada +- Índice de documentación: [../README.md](../README.md) +- Índice de pipelines: [../pipelines/README.md](../pipelines/README.md) +- Flujo anonymizer: [../pipelines/anonymizer/README.md](../pipelines/anonymizer/README.md) +- Flujo datapublic: [../pipelines/datapublic/README.md](../pipelines/datapublic/README.md) diff --git a/docs/es/models/anonymizer-model-card.md b/docs/es/models/anonymizer-model-card.md new file mode 100644 index 00000000..8ab878d8 --- /dev/null +++ b/docs/es/models/anonymizer-model-card.md @@ -0,0 +1,172 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +- anonymization +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- precision +- recall +- f1-score +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Idioma: [English](../../models/anonymizer-model-card.md) | **Español** + +# Descripción del modelo + +Este modelo es el componente NER usado por el pipeline de producción `flair-anonymizer`. +Detecta spans que deben anonimizarse en documentos judiciales antes de la desambiguación, la validación manual y el render final del documento. + +Siguiendo las guías de Flair para entrenamiento de NER, el modelo se entrenó sobre [embeddings BETO](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), una versión en español de BERT, con una arquitectura BiLSTM-CRF. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). + +## Usos previstos y limitaciones + +AymurAI busca ayudar a abordar la falta de datos disponibles sobre resoluciones de violencia de género (VG) en América Latina. En el flujo de anonimización, el objetivo inmediato de este modelo es identificar spans sensibles en documentos legales para que puedan ser revisados y reemplazados antes de su uso posterior. + +AymurAI sigue siendo un sistema específico de dominio. Sus capacidades se limitan a la anonimización, recolección y análisis semiautomatizados de datos judiciales, y la salida puede verse afectada por la calidad de la anotación, la heterogeneidad documental, errores de OCR o extracción y la disponibilidad de datos de entrenamiento representativos. + +Este modelo fue entrenado con un dataset cerrado de un juzgado penal argentino. Esa especificidad de dominio mejora el desempeño en el contexto objetivo, pero también implica que el modelo puede no transferir bien a otras jurisdicciones, estilos documentales o culturas jurídicas. + +## Comportamiento en producción + +En producción, este modelo se carga a través de `aymurai.models.flair.core.FlairModel` desde: + +- `resources/pipelines/production/flair-anonymizer/pipeline.json` +- model path: `aymurai/anonymizer-beto-cased-flair` + +Sus predicciones de spans se postprocesan luego con: + +- `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` +- `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +Esas predicciones alimentan el resto del flujo de anonimización: + +1. `POST /anonymizer/predict` ejecuta la extracción de spans. +2. `POST /anonymizer/disambiguate` asigna IDs canónicos y metadatos efectivos por label. +3. La revisión manual puede editar labels, `label_policies` y `render_policy`. +4. `POST /anonymizer/anonymize-document` aplica los reemplazos sobre el documento de salida. + +# Uso + +## Cómo usar el modelo en Flair + +Requiere **[Flair](https://github.com/flairNLP/flair/)**. +Instalación: `pip install flair`. + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +tagger = SequenceTagger.load("aymurai/anonymizer-beto-cased-flair") + +sentence = Sentence( + "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento " + "de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO " + "MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves " + "agravadas, amenazas simples y agravadas por el uso de armas." +) + +tagger.predict(sentence) + +for entity in sentence.get_spans("ner"): + print(entity) +``` + +Esto produce una salida similar a: + +```text +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" -> PER (0.9541) +Span[27:28]: "11.222.333" -> DNI (1.0) +``` + +## Uso del modelo en un pipeline de AymurAI + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/flair-anonymizer") + +item = { + "path": "dummy", + "data": { + "doc.text": ( + "Acusado: Ramiro Marrón DNI 34.555.666. " + "Fecha: 17 de noviembre de 2024." + ) + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entidades y métricas + +## Descripción + +Consultá el catálogo de entidades de anonymizer ([en](../../entities/anonymizer/README.md)|[es](../entities/anonymizer/README.md)). + +## Datos + +El modelo fue entrenado con un dataset de 535 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +## Métricas + +Las siguientes métricas por label provienen de la model card publicada en Hugging Face para esta familia de modelos de anonymizer y deben interpretarse como métricas NER a nivel modelo, no como calidad end-to-end de la anonimización. + +| label | precision | recall | f1-score | +|---|---:|---:|---:| +| `BANCO` | 1.00 | 0.90 | 0.95 | +| `CBU` | 0.92 | 0.92 | 0.92 | +| `CORREO_ELECTRONICO` | 1.00 | 1.00 | 1.00 | +| `CUIJ` | 1.00 | 1.00 | 1.00 | +| `CUIT_CUIL` | 1.00 | 1.00 | 1.00 | +| `DIRECCION` | 0.97 | 0.85 | 0.91 | +| `DNI` | 0.96 | 1.00 | 0.98 | +| `EDAD` | 1.00 | 0.95 | 0.97 | +| `ESTUDIOS` | 1.00 | 1.00 | 1.00 | +| `FECHA` | 1.00 | 0.99 | 1.00 | +| `LINK` | 1.00 | 0.94 | 0.97 | +| `LOC` | 0.99 | 0.72 | 0.83 | +| `MARCA_AUTOMOVIL` | 0.95 | 1.00 | 0.97 | +| `NACIONALIDAD` | 1.00 | 0.94 | 0.97 | +| `NUM_ACTUACION` | 0.84 | 0.96 | 0.90 | +| `NUM_CAJA_AHORRO` | 0.00 | 0.00 | 0.00 | +| `NUM_EXPEDIENTE` | 0.98 | 0.92 | 0.95 | +| `NUM_MATRICULA` | 0.33 | 0.50 | 0.40 | +| `PATENTE_DOMINIO` | 1.00 | 1.00 | 1.00 | +| `PER` | 0.98 | 0.97 | 0.98 | +| `TELEFONO` | 0.97 | 1.00 | 0.99 | +| `TEXTO_ANONIMIZAR` | 0.98 | 0.61 | 0.75 | +| `macro avg` | 0.91 | 0.88 | 0.89 | + +# Cita + +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/models/decision-model-card.md b/docs/es/models/decision-model-card.md new file mode 100644 index 00000000..0735c450 --- /dev/null +++ b/docs/es/models/decision-model-card.md @@ -0,0 +1,217 @@ +--- +license: mit +language: +- es +tags: +- text-classification +- embeddingbag +- binary-classification +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: torch +pipeline_tag: text-classification +--- + +Idioma: [English](../../models/decision-model-card.md) | **Español** + +# Descripción del modelo + +Este modelo es el clasificador actual de decisiones a nivel párrafo utilizado en el pipeline de producción `datapublic`. +Estima si un párrafo contiene una decisión judicial y, cuando se usa dentro del pipeline de AymurAI, emite una entidad sintética `DECISION` con una subcategoría basada en reglas. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). + +## Arquitectura + +El clasificador actual de producción es un modelo compacto de bolsa de palabras con hashing, implementado con `torch.nn.EmbeddingBag`. +Su camino de inferencia está definido en `aymurai/models/decision/binregex.py` y `aymurai/models/decision/embeddingbag.py`. + +A alto nivel, el modelo funciona así: + +1. El texto de entrada se normaliza quitando tildes, pasando a minúsculas y colapsando espacios. +2. El texto se tokeniza por espacios en blanco. +3. Los tokens se hashean con BLAKE2b sobre un vocabulario fijo. +4. Los IDs de tokens se agrupan mediante embeddings promedio con `EmbeddingBag`. +5. Una capa de dropout y una cabeza lineal producen logits binarios (`not decision`, `decision`). +6. Si el score positivo supera el umbral configurado, el pipeline agrega una etiqueta `DECISION` al párrafo. + +Configuración actual del checkpoint cargado por el modelo de producción: + +| parámetro | valor | +|---|---| +| `vocab_size` | `20000` | +| `embed_dim` | `64` | +| `max_tokens` | `128` | +| `dropout` | `0.1` | +| `num_classes` | `2` | +| `batch_size` | `512` | +| `lr` | `0.005` | +| `weight_decay` | `0.001` | +| `epochs` | `50` | + +## Usos previstos y limitaciones + +AymurAI está pensado como una herramienta para abordar la falta de transparencia en el sistema judicial en relación con casos de violencia de género (VG) en América Latina. El objetivo es aumentar los niveles de reporte, construir confianza en el sistema de justicia y mejorar el acceso a la justicia para mujeres y personas LGBTIQ+. AymurAI genera y mantiene datasets anonimizados a partir de sentencias judiciales para comprender la violencia de género y apoyar el diseño de políticas públicas, además de contribuir a campañas de colectivos feministas. + +Las capacidades de AymurAI se limitan a la recolección y análisis semiautomatizados de datos, y sus resultados pueden estar sujetos a limitaciones como la calidad y consistencia de los datos, posibles sesgos del modelo de IA y la disponibilidad de la información. Además, la efectividad de AymurAI para abordar la falta de transparencia del sistema judicial y mejorar el acceso a la justicia también puede depender de otros factores, como el nivel de cooperación de funcionarios judiciales y el contexto cultural y político más amplio. + +Este modelo fue entrenado con un dataset cerrado proveniente de un juzgado penal argentino. Está diseñado para identificar si un párrafo contiene una decisión judicial. El uso de un dataset específico de dominio, proveniente de un juzgado penal argentino, hace que el modelo esté ajustado al contexto jurídico y cultural concreto, lo que permite resultados más precisos. Sin embargo, esto también implica que el modelo puede no ser aplicable o efectivo en otros países o regiones con sistemas jurídicos o normas culturales diferentes. + +## Comportamiento en producción + +Cuando este clasificador se utiliza a través de `DecisionEmbeddingBagBinRegex`, el comportamiento en producción incluye dos reglas adicionales además de la predicción binaria cruda: + +- La clase positiva se emite solo cuando el score de decisión supera el umbral configurado (`0.5` en el pipeline de producción). +- Con `return_only_with_detalle=true`, el clasificador solo agrega una entidad `DECISION` si el párrafo ya contiene una entidad `DETALLE` proveniente de la etapa NER. + +Si un párrafo es clasificado como decisión, el modelo emite una entidad `DECISION` que cubre el texto completo del párrafo. La etiqueta emitida incluye una subcategoría basada en reglas: + +- `hace_lugar` +- `no_hace_lugar` + +Esa subcategoría se asigna mediante postprocesamiento con expresiones regulares sobre el texto del párrafo. + +# Uso + +## Cómo usar el modelo en torch + +```python +import torch + +from aymurai.models.decision.binregex import DecisionEmbeddingBagBinRegex + +model = DecisionEmbeddingBagBinRegex( + model_checkpoint="https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + device="cpu", + threshold=0.5, + return_only_with_detalle=False, +) + +text = "1. DECLARAR EXTINGUIDA LA ACCION PENAL en este caso por cumplimiento de la suspension del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + +flat_tokens, offsets = model.model_input_from_text(text) +with torch.no_grad(): + logits = model.model(flat_tokens, offsets) + probabilities = logits.softmax(dim=1).cpu().numpy() + +print(probabilities) +print(model.get_subcategory(text)) +``` + +Esto produce una salida similar a: + +```text +[[0.0010057, 0.9989943]] +['hace_lugar'] +``` + +La primera columna es la probabilidad de que el texto no sea una decisión, y la segunda columna es la probabilidad de que el texto sí sea una decisión. + +## Uso del modelo en un pipeline de AymurAI + +El pipeline actual de producción `datapublic` incluye este clasificador después de la etapa NER con Flair. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + "path": "dummy", + "data": { + "doc.text": "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +En producción, el clasificador se configura así: + +```json +{ + "aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex": { + "model_checkpoint": "https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + "device": "cpu", + "threshold": 0.5, + "return_only_with_detalle": true + } +} +``` + +# Entidades y métricas + +## Descripción + +Este modelo solo considera la clasificación de párrafos como decisiones o no decisiones. +Cuando la predicción es positiva, el pipeline emite una entidad sintética `DECISION` que cubre todo el párrafo. + +Para la lista completa de entidades usadas por el flujo datapublic, consultá el catálogo de entidades de datapublic ([en](../../entities/datapublic/README.md)|[es](../entities/datapublic/README.md)). + +Para una descripción completa de las entidades consideradas por AymurAI, ver el [Glosario para el dataset con perspectiva de género](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit), elaborado por [DataGenero](https://datagenero.org) (solo en español). + +## Datos + +El modelo fue entrenado con un dataset de 1200 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +### Lista de personas colaboradoras en la anotación + +El dataset fue anotado manualmente por: + +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Métricas + +Las siguientes métricas provienen de la notebook actual de entrenamiento/evaluación de EmbeddingBag y corresponden a la tarea binaria de clasificación de decisiones (`0 = no decisión`, `1 = decisión`). + +### Split de validación + +| métrica | clase 1 (decisión) | general | +|---|---:|---:| +| precision | 0.774 | - | +| recall | 0.896 | - | +| f1-score | 0.830 | - | +| accuracy | - | 0.962 | +| support | 336 | 3211 | + +### Split de test + +| métrica | clase 1 (decisión) | general | +|---|---:|---:| +| precision | 0.754 | - | +| recall | 0.905 | - | +| f1-score | 0.823 | - | +| accuracy | - | 0.959 | +| support | 336 | 3211 | + +# Cita + +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{'i}n Bel{'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/models/flair-model-card.md b/docs/es/models/flair-model-card.md new file mode 100644 index 00000000..5efa7a81 --- /dev/null +++ b/docs/es/models/flair-model-card.md @@ -0,0 +1,166 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Idioma: [English](../../models/flair-model-card.md) | **Español** + +# Descripción del modelo +

+schema +

+ +Siguiendo las guías de Flair para entrenar un modelo de NER, este modelo se entrenó sobre [embeddings BETO](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), una versión en español de BERT entrenada sobre un corpus en español, con una arquitectura BiLSTM-CRF. + +Este modelo fue desarrollado por [{ collective.ai }](https://collectiveai.io) como parte del proyecto [AymurAI](https://aymurai.info) de [DataGenero](https://datagenero.org). +Actualmente se usa como componente NER del pipeline de producción `datapublic`. + +# Usos previstos y limitaciones +AymurAI está pensado como una herramienta para abordar la falta de transparencia en el sistema judicial en relación con casos de violencia de género (VG) en América Latina. El objetivo es aumentar los niveles de reporte, construir confianza en el sistema de justicia y mejorar el acceso a la justicia para mujeres y personas LGBTIQ+. AymurAI genera y mantiene datasets anonimizados a partir de sentencias judiciales para comprender la violencia de género y apoyar el diseño de políticas públicas, además de contribuir a campañas de colectivos feministas. + +Las capacidades de AymurAI se limitan a la recolección y análisis semiautomatizados de datos, y sus resultados pueden estar sujetos a limitaciones como la calidad y consistencia de los datos, posibles sesgos del modelo de IA y la disponibilidad de la información. Además, la efectividad de AymurAI para abordar la falta de transparencia del sistema judicial y mejorar el acceso a la justicia también puede depender de otros factores, como el nivel de cooperación de funcionarios judiciales y el contexto cultural y político más amplio. + +Este modelo fue entrenado con un dataset cerrado proveniente de un juzgado penal argentino. Está diseñado para identificar y extraer información relevante de sentencias vinculadas con casos de violencia de género. El uso de un dataset específico de dominio, proveniente de un juzgado penal argentino, hace que el modelo esté ajustado al contexto jurídico y cultural concreto, lo que permite resultados más precisos. Sin embargo, esto también implica que el modelo puede no ser aplicable o efectivo en otros países o regiones con sistemas jurídicos o normas culturales diferentes. + +# Uso +## Cómo usar el modelo en Flair + +Requiere **[Flair](https://github.com/flairNLP/flair/)**. +Instalación: `pip install flair` + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +# load tagger +tagger = SequenceTagger.load("aymurai/flair-ner-spanish-judicial") + +# make example sentence +sentence = Sentence("1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas.") + +# predict NER tags +tagger.predict(sentence) + +# print sentence +print(sentence) + +# print predicted NER spans +print('The following NER tags are found:') +# iterate over entities and print +for entity in sentence.get_spans('ner'): + print(entity) +``` + +Esto produce una salida como la siguiente: + +```text +Span[2:11]: "EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento" → DETALLE (0.5498) +Span[13:18]: "suspensión del proceso a prueba" → OBJETO_DE_LA_RESOLUCION (0.5647) +Span[20:21]: "SOBRESEER" → DETALLE (0.7766) +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" → NOMBRE (0.6454) +Span[35:36]: "lesiones" → CONDUCTA (0.9457) +Span[36:38]: "leves agravadas" → CONDUCTA_DESCRIPCION (0.8818) +Span[39:40]: "amenazas" → CONDUCTA (0.956) +Span[40:48]: "simples y agravadas por el uso de armas" → CONDUCTA_DESCRIPCION (0.6866) +``` + +## Uso del modelo en un pipeline de AymurAI +También podés ejecutar el modelo a través de un pipeline de AymurAI. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + 'path': 'dummy', + 'data': { + 'doc.text': "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." + } +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entidades y métricas +## Descripción +Consultá el catálogo de entidades de datapublic ([en](../../entities/datapublic/README.md)|[es](../entities/datapublic/README.md)). + +Para una descripción completa de las entidades consideradas por AymurAI, ver el [Glosario para el dataset con perspectiva de género](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit), elaborado por [DataGenero](https://datagenero.org) (solo en español). + +## Datos +El modelo fue entrenado con un dataset de 1200 resoluciones judiciales de un juzgado penal argentino. + +Dada la naturaleza de los datos (datos personales, características de la denuncia y protección de víctimas), los documentos se mantienen privados. + +### Lista de personas colaboradoras en la anotación +El dataset fue anotado manualmente por: +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Métricas + +| label | precision | recall | f1-score | +|-----------------------------------------------------|-----------|--------|----------| +| FECHA_DE_NACIMIENTO | 0.98 | 0.99 | 0.99 | +| FECHA_RESOLUCION | 0.95 | 0.98 | 0.96 | +| NACIONALIDAD | 0.94 | 0.98 | 0.96 | +| GENERO | 1.00 | 0.50 | 0.67 | +| HORA_DE_INICIO | 0.98 | 0.92 | 0.95 | +| NOMBRE | 0.94 | 0.95 | 0.95 | +| FRASES_AGRESION | 0.90 | 0.98 | 0.94 | +| HORA_DE_CIERRE | 0.90 | 0.92 | 0.91 | +| NIVEL_INSTRUCCION | 0.85 | 0.94 | 0.90 | +| N_EXPTE_EJE | 0.85 | 0.93 | 0.89 | +| TIPO_DE_RESOLUCION | 0.63 | 0.93 | 0.75 | +| VIOLENCIA_DE_GENERO | 0.49 | 0.59 | 0.54 | +| RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE | 0.93 | 0.76 | 0.84 | +| HIJOS_HIJAS_EN_COMUN | 0.47 | 0.57 | 0.52 | +| MODALIDAD_DE_LA_VIOLENCIA | 0.57 | 0.56 | 0.57 | +| FECHA_DEL_HECHO | 0.83 | 0.83 | 0.83 | +| CONDUCTA | 0.79 | 0.67 | 0.73 | +| ART_INFRINGIDO | 0.76 | 0.74 | 0.75 | +| DETALLE | 0.53 | 0.37 | 0.43 | +| OBJETO_DE_LA_RESOLUCION | 0.60 | 0.78 | 0.68 | +| CONDUCTA_DESCRIPCION | 0.54 | 0.43 | 0.48 | +| LUGAR_DEL_HECHO | 0.75 | 0.47 | 0.58 | +| EDAD_AL_MOMENTO_DEL_HECHO | 0.50 | 0.20 | 0.29 | +| PERSONA_ACUSADA_NO_DETERMINADA | 0.71 | 0.19 | 0.30 | +| | | | | +| macro avg | 0.77 | 0.72 | 0.73 | + +# Cita +Por favor citá [el siguiente paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) al utilizar AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm'{\i}n Bel'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/es/pipelines/README.md b/docs/es/pipelines/README.md new file mode 100644 index 00000000..c6ff29fa --- /dev/null +++ b/docs/es/pipelines/README.md @@ -0,0 +1,25 @@ +# Pipelines +Idioma: [English](../../pipelines/README.md) | **Español** + +Esta sección documenta los pipelines de producción del backend por flujo. + +## Documentación por flujo +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) +- Datapublic: [datapublic/README.md](datapublic/README.md) + +## Modelos relacionados +- Índice de modelos: [../models/README.md](../models/README.md) +- Model card de Flair NER: [../models/flair-model-card.md](../models/flair-model-card.md) +- Model card de Decision: [../models/decision-model-card.md](../models/decision-model-card.md) + +## Fuentes de configuración en producción +- Config anonymizer: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Config datapublic: `resources/pipelines/production/datapublic/pipeline.json` + +## Mapeo con API +- `POST /anonymizer/predict` -> `flair-anonymizer` +- `POST /datapublic/predict/{document_id}` -> `datapublic` + +## Documentación relacionada +- Referencia API: [../api/README.md](../api/README.md) +- Base de datos interna: [../database/README.md](../database/README.md) diff --git a/docs/es/pipelines/anonymizer/README.md b/docs/es/pipelines/anonymizer/README.md new file mode 100644 index 00000000..f7f71857 --- /dev/null +++ b/docs/es/pipelines/anonymizer/README.md @@ -0,0 +1,68 @@ +# Pipeline de Anonymizer +Idioma: [English](../../../pipelines/anonymizer/README.md) | **Español** + +Referencia técnica orientada al flujo de anonimización. + +## Alcance +Este flujo extrae entidades del texto judicial y compila documentos anonimizados. + +## Diagrama +![Diagrama del pipeline de anonymizer](../../../pipelines/anonymizer/pipeline.png) + +Fuente editable: [../../../pipelines/anonymizer/pipeline.excalidraw](../../../pipelines/anonymizer/pipeline.excalidraw) + +## Entrypoints de runtime +- `POST /misc/document-extract` +- `POST /anonymizer/predict` +- `POST /anonymizer/disambiguate` +- `POST /anonymizer/validation` +- `POST /anonymizer/anonymize-document` + +## Flujo paso a paso +1. Extracción de texto (`/misc/document-extract`) divide el documento fuente en párrafos normalizados. +2. Predicción (`/anonymizer/predict`) ejecuta NER por párrafo. +3. Desambiguación (`/anonymizer/disambiguate`) asigna IDs canónicos y metadatos efectivos para la desambiguación/anonimización. +4. Revisión manual en UI para editar etiquetas/políticas. +5. Compilación (`/anonymizer/anonymize-document`) aplica reemplazos y exporta `.odt` anonimizado. + +## Componentes técnicos + +### Configuración del pipeline +- Fuente: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Preprocesamiento: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Modelo: + - `aymurai.models.flair.core.FlairModel` + - base model path: `aymurai/anonymizer-beto-cased-flair` +- Postprocesamiento: + - `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +### Contratos de API usados por este flujo +- `DocumentInformation` +- `DocumentAnnotations` +- `LabelPolicy` +- `RenderPolicy` +- `EntityAttributes` (en particular: `canonical_entity_id`, `aymurai_label_instance`, `aymurai_disambiguation`, `aymurai_anonymize`) + +### Módulos backend relevantes +- Router: `aymurai/api/endpoints/routers/anonymizer/anonymizer.py` +- Render/anonymize: `aymurai/text/anonymization/docx.py` and `aymurai/text/anonymization/pdf.py` +- Desambiguación canónica: `aymurai/utils/entity_disambiguation/` + +## Persistencia (DB) +Tablas usadas por este flujo: +- `anonymization_paragraph` +- `anonymization_document` +- `anonymization_document_paragraph` + +## Notas +- Las políticas por label se mergean desde entorno y request. +- `render_policy` controla el comportamiento de sufijos (`auto`, `always`, `never`). + +## Documentación relacionada +- Índice de pipelines: [../README.md](../README.md) +- Entidades de anonymizer: [../../entities/anonymizer/README.md](../../entities/anonymizer/README.md) +- Model card de anonymizer: [../../models/anonymizer-model-card.md](../../models/anonymizer-model-card.md) +- Referencia API: [../../api/README.md](../../api/README.md) +- Base interna: [../../database/README.md](../../database/README.md) diff --git a/docs/es/pipelines/datapublic/README.md b/docs/es/pipelines/datapublic/README.md new file mode 100644 index 00000000..aec3291c --- /dev/null +++ b/docs/es/pipelines/datapublic/README.md @@ -0,0 +1,76 @@ +# Pipeline de Datapublic +Idioma: [English](../../../pipelines/datapublic/README.md) | **Español** + +Referencia técnica orientada al flujo de extracción de información del datapublic. + +## Alcance +Este flujo extrae información estructurada a partir de párrafos y soporta persistencia de validación a nivel documento para curación de dataset público. + +## Diagrama +![Diagrama del pipeline de datapublic](../../../pipelines/datapublic/pipeline.png) + +## Entrypoints de runtime +- `POST /misc/document-extract` +- `POST /datapublic/predict/{document_id}` +- `GET /datapublic/validation/document/{document_id}` +- `POST /datapublic/validation/document/{document_id}` + +## Flujo paso a paso +1. Extracción de texto (`/misc/document-extract`) divide el documento fuente en párrafos normalizados. +2. Predicción (`/datapublic/predict/{document_id}`) procesa cada párrafo y devuelve predicciones; la persistencia en caché ocurre solo cuando `use_cache=true` (default). +3. Revisión en UI agrega la salida validada a nivel documento. +4. Los endpoints de validación leen/escriben payload de validación de documento. + +## Componentes técnicos + +### Configuración del pipeline +- Fuente: `resources/pipelines/production/datapublic/pipeline.json` +- Preprocesamiento: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Modelos: + - `aymurai.models.flair.core.FlairModel` (`aymurai/flair-ner-spanish-judicial`) + - `aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex` (`return_only_with_detalle=true` en producción) +- Postprocesamiento: + - `aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + - `aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer` (4 instancias configuradas en producción para `CONDUCTA`, `CONDUCTA_DESCRIPCION`, `DETALLE` y `OBJETO_DE_LA_RESOLUCION`) + - `aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer` + +### Algoritmos y procesamiento +- Extracción NER sobre párrafos judiciales. +- Clasificación/filtro de decisiones para relevancia, con gating en producción para que `DECISION` solo se emita cuando ya existe una entidad `DETALLE`. +- Subcategorización rule-based + embeddings. +- Normalización de fecha/hora y mapeo por artículo. + +### Contratos de API usados por este flujo +- `TextRequest` +- `DocumentInformation` +- `DataPublicDocumentAnnotations` (payload libre de validación a nivel documento) + +### Módulos backend relevantes +- Router: `aymurai/api/endpoints/routers/datapublic/datapublic.py` +- Carga de pipeline e inferencia: endpoint de predicción en `aymurai/api/endpoints/routers/datapublic/datapublic.py`. + +## Persistencia (DB) +Tablas usadas por este flujo: +- `datapublic_paragraph` +- `datapublic_document` +- `datapublic_document_paragraph` + +## Notas +- El directorio de pipeline en producción sigue llamándose `datapublic`. +- `document_id` es la clave de agrupamiento a nivel documento para asociar predicciones por párrafo y el payload de validación. +- La persistencia de validación es a nivel documento y acepta intencionalmente un objeto JSON libre. +- `GET /datapublic/validation/document/{document_id}` devuelve `404` cuando el documento no existe; `POST` crea o actualiza el payload de validación. +- El router público no monta actualmente `/datapublic/dataset/*`. +- La validación a nivel párrafo aparece en código como lógica legacy comentada y no forma parte del flujo público. + +## Modelos usados por este flujo +- Flair NER: [../../models/flair-model-card.md](../../models/flair-model-card.md) +- Clasificador de decisiones: [../../models/decision-model-card.md](../../models/decision-model-card.md) + +## Documentación relacionada +- Índice de pipelines: [../README.md](../README.md) +- Entidades de datapublic: [../../entities/datapublic/README.md](../../entities/datapublic/README.md) +- Referencia API: [../../api/README.md](../../api/README.md) +- Base interna: [../../database/README.md](../../database/README.md) diff --git a/docs/metrics/Desarrollo metrica.md b/docs/metrics/Desarrollo metrica.md new file mode 100644 index 00000000..13fab9b7 --- /dev/null +++ b/docs/metrics/Desarrollo metrica.md @@ -0,0 +1,500 @@ +# Métrica de evaluación para el desambiguador de entidades + +Este documento describe la métrica con la que evaluamos el módulo de **desambiguación de entidades canónicas** en documentos judiciales. La métrica es independiente del NER (que se evalúa por separado con F1 por clase) y se centra en medir la calidad de la resolución de entidades. + +La métrica: + +- recibe dos JSON: + - `gold_json`: referencia (ground truth), + - `pred_json`: salida del desambiguador; +- calcula cuatro componentes complementarios: +- $F1_{\text{ent}}$: calidad en la **detección de entidades canónicas**, +- $\text{AliasF1}_{\text{macro}}$: calidad en la **agrupación de aliases**, +- $Acc_{\text{label}}$: exactitud del **label** (`aymurai_label`), +- $Acc_{\text{rol}}$: exactitud del **rol** (`attributes["role"]`); +- devuelve un escalar final como combinación ponderada: + +$$ +\text{Score}_{\text{global}} = +w_{\text{ent}} \cdot F1_{\text{ent}} + +w_{\text{alias}} \cdot \text{AliasF1}_{\text{macro}} + +w_{\text{label}} \cdot Acc_{\text{label}} + +w_{\text{rol}} \cdot Acc_{\text{rol}}. +$$ + +Valores por defecto de los pesos: + +- $w_{\text{ent}} = 0.40$ +- $w_{\text{alias}} = 0.35$ +- $w_{\text{label}} = 0.20$ +- $w_{\text{rol}} = 0.05$ + +--- + +## 1. Modelo de datos y notación + +Tanto `gold_json` como `pred_json` se modelan como una **lista de entidades canónicas**. Cada entidad sigue la estructura: + +```json +{ + "entity_id": "", + "aymurai_label": "", + "canonical_text": "", + "aliases": [], + "attributes": {}, + "relations": [] +} +``` + +Denotamos: + +- Conjunto de entidades gold (referencia): $G = \{ g_1, g_2, \dots, g_N \}$ +- Conjunto de entidades predichas: $P = \{ p_1, p_2, \dots, p_M \}$ + +Para cada entidad $e \in G \cup P$: + +- $\text{label}(e)$ es su `aymurai_label`. +- $\text{aliases}(e)$ es el conjunto de aliases (incluyendo `canonical_text`). +- $\text{rol}(e)$ es `attributes["role"]` si existe (en caso contrario es `None`). + +La métrica está inspirada en trabajos de **entity resolution** y **coreference**, donde interesa comparar la formación de clusters más que strings exactos. + +--- + +## 2. Preprocesamiento: normalización y conjuntos de aliases + +Cada alias se normaliza para mitigar variaciones de formato. Se procesa tanto la lista `aliases` como `canonical_text`, y se construye un conjunto de strings normalizados que representa a la entidad. + +### 2.1. Normalización de texto + +Sea una función $\text{norm}(\cdot)$ que: + +1. convierte a minúsculas, +2. aplica `strip()` para remover espacios extras, +3. elimina tildes y acentos utilizando normalización Unicode. + +```python +import unicodedata + +def normalize_text(text: str) -> str: + if text is None: + return "" + text = text.strip().lower() + text = unicodedata.normalize("NFD", text) + text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn") + return text +``` + +### 2.2. Construcción del conjunto de aliases + +Para cada entidad $e$: + +$$ +\text{aliases}(e) = \{ \text{norm}(a) \mid a \in e.\text{aliases} \} \cup \{ \text{norm}(e.\text{canonical\_text}) \}. +$$ + +```python +from typing import Any, Dict, Set + +def get_alias_set(entity: Dict[str, Any]) -> Set[str]: + aliases = { + normalize_text(a) + for a in entity.get("aliases", []) + if normalize_text(a) + } + canonical = normalize_text(entity.get("canonical_text", "")) + if canonical: + aliases.add(canonical) + return aliases +``` + +--- + +## 3. Similitud entre entidades: Jaccard sobre aliases + +Para medir qué tan similares son dos entidades, usamos la similitud de **Jaccard** sobre sus conjuntos de aliases: + +$$ +\text{sim}(g_i, p_j) = +\frac{\lvert \text{aliases}(g_i) \cap \text{aliases}(p_j) \rvert} + {\lvert \text{aliases}(g_i) \cup \text{aliases}(p_j) \rvert}. +$$ + +- Valor 1: conjuntos idénticos. +- Valor 0: conjuntos disjuntos. +- Valores intermedios: superposición parcial. + +```python +from typing import Set + +def alias_jaccard(aliases_gold: Set[str], aliases_pred: Set[str]) -> float: + if not aliases_gold and not aliases_pred: + return 1.0 + union = aliases_gold | aliases_pred + if not union: + return 0.0 + inter = aliases_gold & aliases_pred + return len(inter) / len(union) +``` + +Construimos una **matriz de similitud** $S \in \mathbb{R}^{N \times M}$ donde $S_{ij} = \text{sim}(g_i, p_j)$. + +--- + +## 4. Emparejamiento de entidades (matching gold–pred) + +### 4.1. Problema + +Necesitamos decidir qué entidad predicha corresponde a cada entidad gold. Esto es un matching bipartito: + +- Si el modelo divide una entidad real en dos, solo una predicha debería emparejarse con la gold; la otra contará como falso positivo. +- Si el modelo fusiona dos entidades distintas, solo una gold se emparejará; la otra será falso negativo. + +### 4.2. Umbral de similitud + +Se define un umbral $\tau$ (por defecto $\tau = 0.3$): + +- Si $S_{ij} < \tau$, se rechaza el match. +- Si $S_{ij} \ge \tau$, el par se considera candidato. + +### 4.3. Algoritmo greedy de emparejamiento + +Usamos un matching greedy ordenado por similitud (alternativa simple al algoritmo Húngaro): + +```python +from typing import List, Tuple + +def greedy_matching( + sim_matrix: List[List[float]], + sim_threshold: float +) -> List[Tuple[int, int, float]]: + matches: List[Tuple[int, int, float]] = [] + num_gold = len(sim_matrix) + num_pred = len(sim_matrix[0]) if num_gold > 0 else 0 + + candidates = [] + for i in range(num_gold): + for j in range(num_pred): + sim = sim_matrix[i][j] + if sim >= sim_threshold: + candidates.append((sim, i, j)) + + candidates.sort(reverse=True, key=lambda x: x[0]) + + used_gold, used_pred = set(), set() + for sim, i, j in candidates: + if i in used_gold or j in used_pred: + continue + used_gold.add(i) + used_pred.add(j) + matches.append((i, j, sim)) + + return matches +``` + +El resultado es una lista de triples $(i, j, \text{sim})$ con las parejas aceptadas. + +--- + +## 5. Métrica de entidades: $F1_{\text{ent}}$ + +Sean: + +- $\text{TP}$: número de pares emparejados. +- $\text{FN} = N - \text{TP}$: entidades gold sin match (perdidas). +- $\text{FP} = M - \text{TP}$: entidades pred sin match (inventadas o splits innecesarios). + +```math +P_{\text{ent}} = \frac{\text{TP}}{\text{TP} + \text{FP}}, \qquad +R_{\text{ent}} = \frac{\text{TP}}{\text{TP} + \text{FN}} +``` + +```math +F1_{\text{ent}} = +\begin{cases} +\dfrac{2 P_{\text{ent}} R_{\text{ent}}}{P_{\text{ent}} + R_{\text{ent}}}, & \text{si } P_{\text{ent}} + R_{\text{ent}} > 0 \\ +0, & \text{en otro caso} +\end{cases} +``` + +Interpretación: penaliza entidades faltantes, inventadas y los splits/merges incorrectos. + +--- + +## 6. Métrica de aliases: $\text{AliasF1}_{\text{macro}}$ + +Para cada par emparejado $(g_i, p_j)$: + +```math +A_g = \text{aliases}(g_i), \quad +A_p = \text{aliases}(p_j), \quad +I = |A_g \cap A_p| +``` + +```math +P_{\text{alias}}^{(i)} = \frac{I}{|A_p|}, \qquad +R_{\text{alias}}^{(i)} = \frac{I}{|A_g|} +``` + +```math +F1_{\text{alias}}^{(i)} = +\begin{cases} +\dfrac{2 P_{\text{alias}}^{(i)} R_{\text{alias}}^{(i)}} + {P_{\text{alias}}^{(i)} + R_{\text{alias}}^{(i)}}, & +\text{si } P_{\text{alias}}^{(i)} + R_{\text{alias}}^{(i)} > 0 \\ +0, & \text{en otro caso} +\end{cases} +``` + +Promediamos macro sobre los matches: + +```math +\text{AliasF1}_\text{macro} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} F1_{\text{alias}}^{(i)} +``` + +- Falta de aliases relevantes → baja el recall. +- Aliases incorrectos → baja la precision. +- Splits se reflejan en menor $F1_{\text{alias}}$. + +```python +alias_f1_sum = 0.0 +for i, j, _ in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + inter = len(A_g & A_p) + + P_alias = inter / len(A_p) if A_p else 0.0 + R_alias = inter / len(A_g) if A_g else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + + alias_f1_sum += F1_alias + +AliasF1_macro = alias_f1_sum / TP if TP > 0 else 0.0 +``` + +--- + +## 7. Exactitud de label y rol + +Para cada match \((g_i, p_j)\): + +```math +\text{correctLabel}^{(i)} = +\begin{cases} +1, & \text{si } \text{label}(g_i) = \text{label}(p_j) \\ +0, & \text{en otro caso} +\end{cases} +``` + +```math +\text{correctRol}^{(i)} = +\begin{cases} +1, & \text{si } \text{rol}(g_i) = \text{rol}(p_j) \\ +0, & \text{en otro caso} +\end{cases} +``` + +```math +Acc_{\text{label}} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} \text{correctLabel}^{(i)}, \quad +Acc_{\text{rol}} = +\frac{1}{\text{TP}} \sum_{(i,j) \in M_{\text{match}}} \text{correctRol}^{(i)}. +``` + +```python +label_correct = 0 +role_correct = 0 + +for i, j, _ in matches: + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + +Acc_label = label_correct / TP if TP > 0 else 0.0 +Acc_role = role_correct / TP if TP > 0 else 0.0 +``` + +Separar la calidad de la clusterización (aliases) de la del rotulado alinea la métrica con enfoques **entity-centric** en la literatura. + +--- + +## 8. Función `compute_metrics_components` + +Integra todos los pasos anteriores y devuelve un diccionario con los cuatro componentes: + +```python +from typing import Any, Dict, List + +def compute_metrics_components( + gold_entities: List[Dict[str, Any]], + pred_entities: List[Dict[str, Any]], + sim_threshold: float = 0.3, +) -> Dict[str, float]: + if not gold_entities and not pred_entities: + return { + "F1_ent": 1.0, + "AliasF1_macro": 1.0, + "Acc_label": 1.0, + "Acc_role": 1.0, + } + + gold_aliases = [get_alias_set(e) for e in gold_entities] + pred_aliases = [get_alias_set(e) for e in pred_entities] + + gold_labels = [e.get("aymurai_label") for e in gold_entities] + pred_labels = [e.get("aymurai_label") for e in pred_entities] + + gold_roles = [e.get("attributes", {}).get("role") for e in gold_entities] + pred_roles = [e.get("attributes", {}).get("role") for e in pred_entities] + + sim_matrix = [ + [alias_jaccard(g_gold, g_pred) for g_pred in pred_aliases] + for g_gold in gold_aliases + ] + + matches = greedy_matching(sim_matrix, sim_threshold=sim_threshold) + + TP = len(matches) + FN = len(gold_entities) - TP + FP = len(pred_entities) - TP + + P_ent = TP / (TP + FP) if TP + FP > 0 else 0.0 + R_ent = TP / (TP + FN) if TP + FN > 0 else 0.0 + F1_ent = 2 * P_ent * R_ent / (P_ent + R_ent) if P_ent + R_ent > 0 else 0.0 + + alias_f1_sum = 0.0 + label_correct = 0 + role_correct = 0 + + for i, j, _ in matches: + A_g = gold_aliases[i] + A_p = pred_aliases[j] + inter = len(A_g & A_p) + + P_alias = inter / len(A_p) if A_p else 0.0 + R_alias = inter / len(A_g) if A_g else 0.0 + if P_alias + R_alias > 0: + F1_alias = 2 * P_alias * R_alias / (P_alias + R_alias) + else: + F1_alias = 0.0 + alias_f1_sum += F1_alias + + if gold_labels[i] == pred_labels[j]: + label_correct += 1 + if gold_roles[i] == pred_roles[j]: + role_correct += 1 + + AliasF1_macro = alias_f1_sum / TP if TP > 0 else 0.0 + Acc_label = label_correct / TP if TP > 0 else 0.0 + Acc_role = role_correct / TP if TP > 0 else 0.0 + + return { + "F1_ent": F1_ent, + "AliasF1_macro": AliasF1_macro, + "Acc_label": Acc_label, + "Acc_role": Acc_role, + } +``` + +--- + +## 9. Métrica escalar final: `evaluate_disambiguation` + +Función de alto nivel que combina los componentes con los pesos: + +```python +import json +from typing import Any + +def evaluate_disambiguation( + gold_json: Any, + pred_json: Any, + w_ent: float = 0.4, + w_alias: float = 0.35, + w_label: float = 0.2, + w_role: float = 0.05, + sim_threshold: float = 0.3, +) -> Tuple[float, Dict[str, float]]: + if isinstance(gold_json, str): + gold_entities = json.loads(gold_json) + else: + gold_entities = gold_json + + if isinstance(pred_json, str): + pred_entities = json.loads(pred_json) + else: + pred_entities = pred_json + + metrics = compute_metrics_components( + gold_entities, + pred_entities, + sim_threshold=sim_threshold, + ) + + return ( + w_ent * metrics["F1_ent"] + + w_alias * metrics["AliasF1_macro"] + + w_label * metrics["Acc_label"] + + w_role * metrics["Acc_role"] + ) +``` + +### Justificación de los pesos por defecto + +- **$w_{\text{ent}} = 0.40$**: prioriza detectar todas las entidades canónicas sin inventarlas. +- **$w_{\text{alias}} = 0.35$**: agrupar aliases es crucial para preservar el anonimato. +- **$w_{\text{label}} = 0.20$**: el tipo de entidad importa, pero es secundario frente a la detección. +- **$w_{\text{rol}} = 0.05$**: el rol agrega contexto, pero tiene menor peso. + +Los pesos pueden ajustarse según las necesidades del proyecto. + +--- + +## 10. Ejemplo de uso + +```python +gold = [ + { + "entity_id": "GT_1", + "aymurai_label": "PER", + "canonical_text": "Juan Pérez", + "aliases": ["Juan Pérez", "Pérez, Juan"], + "attributes": {"role": "imputado"}, + "relations": [] + } +] + +pred = [ + { + "entity_id": "P_1", + "aymurai_label": "PER", + "canonical_text": "juan perez", + "aliases": ["juan perez", "perez juan"], + "attributes": {"role": "imputado"}, + "relations": [] + } +] + +score, components = evaluate_disambiguation(gold, pred) +print("Score global:", score) +print(components) +``` + +--- + +## 11. Referencias y relación con la literatura + +- Moosavi, N. S., & Strube, M. (2016). *Which coreference evaluation metric do you trust? A proposal for a link-based entity aware metric*. ACL. +- Menestrina, D., Whang, S. E., & Garcia-Molina, H. (2010). *Evaluating entity resolution results*. PVLDB. +- Binette, O., et al. (2024). *Comprehensive evaluation frameworks for entity resolution*. + +La métrica propuesta toma conceptos de la evaluación de coreference y entity resolution y los adapta a nuestro contexto judicial: + +- priorizando no perder entidades sensibles, +- evitando crear entidades inexistentes, +- y reforzando que los aliases de una misma persona no se mezclen con los de otra. diff --git a/docs/models/README.md b/docs/models/README.md new file mode 100644 index 00000000..8893a1e4 --- /dev/null +++ b/docs/models/README.md @@ -0,0 +1,19 @@ +# Models +Language: **English** | [Español](../es/models/README.md) + +This section documents the individual models used by the backend. + +## Available model docs +- Anonymizer NER model card: [anonymizer-model-card.md](anonymizer-model-card.md) +- Flair NER: [flair-model-card.md](flair-model-card.md) +- Decision classifier: [decision-model-card.md](decision-model-card.md) + +## Current production usage +- `flair-anonymizer` uses the anonymizer NER model card documented here. +- `datapublic` uses the Flair NER model and the decision classifier. + +## Related docs +- Documentation index: [../README.md](../README.md) +- Pipelines index: [../pipelines/README.md](../pipelines/README.md) +- Anonymizer flow: [../pipelines/anonymizer/README.md](../pipelines/anonymizer/README.md) +- Datapublic flow: [../pipelines/datapublic/README.md](../pipelines/datapublic/README.md) diff --git a/docs/models/anonymizer-model-card.md b/docs/models/anonymizer-model-card.md new file mode 100644 index 00000000..53a46813 --- /dev/null +++ b/docs/models/anonymizer-model-card.md @@ -0,0 +1,172 @@ +--- +license: mit +language: +- es +tags: +- flair +- token-classification +- sequence-tagger-model +- anonymization +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- precision +- recall +- f1-score +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: flair +pipeline_tag: token-classification +--- + +Language: **English** | [Español](../es/models/anonymizer-model-card.md) + +# Model Description + +This model is the NER component used by the production `flair-anonymizer` pipeline. +It detects spans that should be anonymized in judicial documents before disambiguation, validation, and document rendering. + +Following the Flair guidelines for NER training, the model was trained on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), a Spanish BERT model, with a BiLSTM-CRF architecture. + +This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). + +## Intended uses & limitations + +AymurAI is intended to help address the lack of available data on gender-based violence (GBV) rulings in Latin America. In the anonymization workflow, the immediate purpose of this model is to identify sensitive spans in legal documents so they can be reviewed and replaced before downstream use. + +AymurAI remains a domain-specific system. Its capabilities are limited to semi-automated anonymization, collection, and analysis of judicial data, and the output may be affected by annotation quality, document heterogeneity, OCR or extraction errors, and the availability of representative training data. + +This model was trained on a closed dataset from an Argentine criminal court. That domain specificity improves performance on the target setting, but it also means the model may not transfer well to other jurisdictions, document styles, or legal cultures. + +## Production behavior + +In production, this model is loaded through `aymurai.models.flair.core.FlairModel` from: + +- `resources/pipelines/production/flair-anonymizer/pipeline.json` +- model path: `aymurai/anonymizer-beto-cased-flair` + +Its raw span predictions are then post-processed by: + +- `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` +- `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +Those predictions feed the rest of the anonymization flow: + +1. `POST /anonymizer/predict` runs span extraction. +2. `POST /anonymizer/disambiguate` assigns canonical entity IDs and effective per-label metadata. +3. Manual review may edit labels, `label_policies`, and `render_policy`. +4. `POST /anonymizer/anonymize-document` applies replacements in the output document. + +# Usage + +## How to use the model in Flair + +Requires **[Flair](https://github.com/flairNLP/flair/)**. +Install it with `pip install flair`. + +```python +from flair.data import Sentence +from flair.models import SequenceTagger + +tagger = SequenceTagger.load("aymurai/anonymizer-beto-cased-flair") + +sentence = Sentence( + "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento " + "de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO " + "MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves " + "agravadas, amenazas simples y agravadas por el uso de armas." +) + +tagger.predict(sentence) + +for entity in sentence.get_spans("ner"): + print(entity) +``` + +This yields output similar to: + +```text +Span[22:25]: "EZEQUIEL CAMILO MARCONNI" -> PER (0.9541) +Span[27:28]: "11.222.333" -> DNI (1.0) +``` + +## Using the model in an AymurAI pipeline + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/flair-anonymizer") + +item = { + "path": "dummy", + "data": { + "doc.text": ( + "Acusado: Ramiro Marrón DNI 34.555.666. " + "Fecha: 17 de noviembre de 2024." + ) + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +# Entities and metrics + +## Description + +Please refer to the anonymizer entities catalog ([en](../entities/anonymizer/README.md)|[es](../es/entities/anonymizer/README.md)). + +## Data + +The model was trained with a dataset of 535 legal rulings from an Argentine criminal court. + +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + +## Metrics + +The following per-label metrics come from the published Hugging Face card for this anonymizer model family and should be interpreted as model-level NER metrics, not end-to-end anonymization quality. + +| label | precision | recall | f1-score | +|---|---:|---:|---:| +| `BANCO` | 1.00 | 0.90 | 0.95 | +| `CBU` | 0.92 | 0.92 | 0.92 | +| `CORREO_ELECTRONICO` | 1.00 | 1.00 | 1.00 | +| `CUIJ` | 1.00 | 1.00 | 1.00 | +| `CUIT_CUIL` | 1.00 | 1.00 | 1.00 | +| `DIRECCION` | 0.97 | 0.85 | 0.91 | +| `DNI` | 0.96 | 1.00 | 0.98 | +| `EDAD` | 1.00 | 0.95 | 0.97 | +| `ESTUDIOS` | 1.00 | 1.00 | 1.00 | +| `FECHA` | 1.00 | 0.99 | 1.00 | +| `LINK` | 1.00 | 0.94 | 0.97 | +| `LOC` | 0.99 | 0.72 | 0.83 | +| `MARCA_AUTOMOVIL` | 0.95 | 1.00 | 0.97 | +| `NACIONALIDAD` | 1.00 | 0.94 | 0.97 | +| `NUM_ACTUACION` | 0.84 | 0.96 | 0.90 | +| `NUM_CAJA_AHORRO` | 0.00 | 0.00 | 0.00 | +| `NUM_EXPEDIENTE` | 0.98 | 0.92 | 0.95 | +| `NUM_MATRICULA` | 0.33 | 0.50 | 0.40 | +| `PATENTE_DOMINIO` | 1.00 | 1.00 | 1.00 | +| `PER` | 0.98 | 0.97 | 0.98 | +| `TELEFONO` | 0.97 | 1.00 | 0.99 | +| `TEXTO_ANONIMIZAR` | 0.98 | 0.61 | 0.75 | +| `macro avg` | 0.91 | 0.88 | 0.89 | + +# Citation + +Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/pipeline/assets/ner-schema.png b/docs/models/assets/ner-schema.png similarity index 100% rename from docs/pipeline/assets/ner-schema.png rename to docs/models/assets/ner-schema.png diff --git a/docs/models/decision-model-card.md b/docs/models/decision-model-card.md new file mode 100644 index 00000000..715f4354 --- /dev/null +++ b/docs/models/decision-model-card.md @@ -0,0 +1,217 @@ +--- +license: mit +language: +- es +tags: +- text-classification +- embeddingbag +- binary-classification +- judicial-text +datasets: +- ArJuzPCyF10 +metrics: +- f1 +widget: +- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. +library_name: torch +pipeline_tag: text-classification +--- + +Language: **English** | [Español](../es/models/decision-model-card.md) + +# Model Description + +This model is the current paragraph-level decision classifier used in the production `datapublic` pipeline. +It estimates whether a paragraph contains a judicial decision and, when used inside the AymurAI pipeline, emits a synthetic `DECISION` entity with a rule-based subclass. + +This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). + +## Architecture + +The current production classifier is a compact hashed bag-of-words model implemented with `torch.nn.EmbeddingBag`. +Its inference path is defined in `aymurai/models/decision/binregex.py` and `aymurai/models/decision/embeddingbag.py`. + +At a high level, the model works as follows: + +1. Input text is normalized with accent stripping, lowercasing, and whitespace collapsing. +2. Text is tokenized by whitespace. +3. Tokens are hashed with BLAKE2b into a fixed vocabulary. +4. Token IDs are pooled with mean `EmbeddingBag` embeddings. +5. A dropout layer and a linear head produce binary logits (`not decision`, `decision`). +6. If the positive score exceeds the configured threshold, the pipeline appends a `DECISION` label to the paragraph. + +Current checkpoint configuration loaded with the production model: + +| parameter | value | +|---|---| +| `vocab_size` | `20000` | +| `embed_dim` | `64` | +| `max_tokens` | `128` | +| `dropout` | `0.1` | +| `num_classes` | `2` | +| `batch_size` | `512` | +| `lr` | `0.005` | +| `weight_decay` | `0.001` | +| `epochs` | `50` | + +## Intended uses & limitations + +AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. + +AymurAI capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. + +This model was trained on a closed dataset from an Argentine criminal court. It is designed to identify whether a paragraph contains a judicial decision. The use of a domain-specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. + +## Production behavior + +When this classifier is used through `DecisionEmbeddingBagBinRegex`, the production behavior includes two additional rules beyond the raw binary prediction: + +- The positive class is emitted only when the decision score is above the configured threshold (`0.5` in the production pipeline). +- With `return_only_with_detalle=true`, the classifier only appends a `DECISION` entity if the paragraph already contains a `DETALLE` entity from the NER stage. + +If a paragraph is classified as a decision, the model emits a `DECISION` entity covering the full paragraph text. The emitted label includes a rule-based subclass: + +- `hace_lugar` +- `no_hace_lugar` + +That subclass is assigned with regex-based post-processing over the paragraph text. + +# Usage + +## How to use the model in torch + +```python +import torch + +from aymurai.models.decision.binregex import DecisionEmbeddingBagBinRegex + +model = DecisionEmbeddingBagBinRegex( + model_checkpoint="https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + device="cpu", + threshold=0.5, + return_only_with_detalle=False, +) + +text = "1. DECLARAR EXTINGUIDA LA ACCION PENAL en este caso por cumplimiento de la suspension del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + +flat_tokens, offsets = model.model_input_from_text(text) +with torch.no_grad(): + logits = model.model(flat_tokens, offsets) + probabilities = logits.softmax(dim=1).cpu().numpy() + +print(probabilities) +print(model.get_subcategory(text)) +``` + +This yields output similar to: + +```text +[[0.0010057, 0.9989943]] +['hace_lugar'] +``` + +The first column is the probability of the text not being a decision, and the second column is the probability of the text being a decision. + +## Using the model in an AymurAI pipeline + +The current production `datapublic` pipeline includes this classifier after the Flair NER stage. + +```python +from aymurai.pipeline import AymurAIPipeline + +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") + +item = { + "path": "dummy", + "data": { + "doc.text": "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas." + }, +} + +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) +``` + +In production, the classifier is configured as: + +```json +{ + "aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex": { + "model_checkpoint": "https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + "device": "cpu", + "threshold": 0.5, + "return_only_with_detalle": true + } +} +``` + +# Entities and metrics + +## Description + +This model only considers the classification of paragraphs as decisions or non-decisions. +When the prediction is positive, the pipeline emits a synthetic `DECISION` entity that spans the full paragraph. + +For the complete list of entities used by the datapublic flow, please refer to the datapublic entities catalog ([en](../entities/datapublic/README.md)|[es](../es/entities/datapublic/README.md)). + +For a complete description of the entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (Spanish only). + +## Data + +The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. + +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + +### List of annotation contributors + +The dataset was manually annotated by: + +* Diego Scopetta +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Laura Barreiro +* Matías Sosa +* Maximiliano Sosa +* Patricia Sandoval +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Zoe Rodriguez Gerzovich + +## Metrics + +The following metrics were obtained from the current EmbeddingBag training/evaluation notebook and correspond to the binary decision classification task (`0 = not decision`, `1 = decision`). + +### Validation split + +| metric | class 1 (decision) | overall | +|---|---:|---:| +| precision | 0.774 | - | +| recall | 0.896 | - | +| f1-score | 0.830 | - | +| accuracy | - | 0.962 | +| support | 336 | 3211 | + +### Test split + +| metric | class 1 (decision) | overall | +|---|---:|---:| +| precision | 0.754 | - | +| recall | 0.905 | - | +| f1-score | 0.823 | - | +| accuracy | - | 0.959 | +| support | 336 | 3211 | + +# Citation + +Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: + +```bibtex +@techreport{feldfeber2022, + author = {Feldfeber, Ivana and Quiroga, Yasm{\'i}n Bel{\'e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} +} +``` diff --git a/docs/pipeline/flair-model-card.md b/docs/models/flair-model-card.md similarity index 69% rename from docs/pipeline/flair-model-card.md rename to docs/models/flair-model-card.md index 15fd493d..54143211 100644 --- a/docs/pipeline/flair-model-card.md +++ b/docs/models/flair-model-card.md @@ -16,23 +16,24 @@ library_name: flair pipeline_tag: token-classification --- +Language: **English** | [Español](../es/models/flair-model-card.md) + # Model Description

schema

-Following the FLAIR guidelines for training a NER model, we trained a model on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased) (a spanish version of BERT trained in a spanish corpus) and a BiLSTM-CRF architecture. +Following the Flair guidelines for training a NER model, we trained this model on top of [BETO embeddings](https://huggingface.co/dccuchile/bert-base-spanish-wwm-uncased), a Spanish version of BERT trained on a Spanish corpus, with a BiLSTM-CRF architecture. This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). - - +It is currently used as the NER component of the production `datapublic` pipeline. # Intended uses & limitations AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. -AymurAI is still a prototype and is only being implemented in Argentina and Mexico. Its capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. +AymurAI capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. -This model was trained with a closed dataset from an Argentine criminal court. It's is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. +This model was trained on a closed dataset from an Argentine criminal court. It is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain-specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. # Usage ## How to use the model in Flair @@ -76,38 +77,13 @@ Span[39:40]: "amenazas" → CONDUCTA (0.956) Span[40:48]: "simples y agravadas por el uso de armas" → CONDUCTA_DESCRIPCION (0.6866) ``` -## Using the model in aymurai pipeline -You also can run the model directly in the aymurai pipeline. +## Using the model in an AymurAI pipeline +You can also run the model through an AymurAI pipeline. ```python from aymurai.pipeline import AymurAIPipeline -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.models.flair.core import FlairModel - -config = { - "preprocess": [ - [ - "aymurai.models.flair.utils.FlairTextNormalize", - {} - ] - ], - "models": [ - [ - "aymurai.models.flair.core.FlairModel", - { - "basepath": "https://drive.google.com/uc?id=1QJYkegv_P3d27FyRLabIUiw9154ur9-I&confirm=true", - "split_doc": false, - "device": "cpu" - } - ] - ], - "postprocess": [], - "use_cache": false -} -} - -pipeline = AymurAIPipeline.load("/resources/pipelines/production/full-paragraph") +pipeline = AymurAIPipeline.load("/resources/pipelines/production/datapublic") item = { 'path': 'dummy', @@ -116,7 +92,11 @@ item = { } } -pipeline.predict([item]) +processed = pipeline.preprocess([item]) +processed = pipeline.predict_single(processed[0]) +processed = pipeline.postprocess([processed]) + +print(processed[0]["predictions"]["entities"]) ``` @@ -124,24 +104,26 @@ pipeline.predict([item]) # Entities and metrics ## Description -Please refer to the entities' description table ([en](../data/en/entities-table.md)|[es](../data/es/entities-table.md)). +Please refer to the datapublic entities catalog ([en](../entities/datapublic/README.md)|[es](../es/entities/datapublic/README.md)). For a complete description about entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (spanish only) ## Data The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. -Due to the nature of the data (personal data, complaint characteristics and victim protection) the documents are kept private. +Due to the nature of the data (personal data, complaint characteristics, and victim protection), the documents are kept private. + ### List of annotation contributors The dataset was manually annotated by: * Diego Scopetta -* Franny Rodriguez Gerzovich ([email](fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) +* Franny Rodriguez Gerzovich ([email](mailto:fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) * Laura Barreiro * Matías Sosa * Maximiliano Sosa * Patricia Sandoval -* Santiago Bezchinsky ([email](santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) +* Santiago Bezchinsky ([email](mailto:santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) * Zoe Rodriguez Gerzovich + ## Metrics | label | precision | recall | f1-score | @@ -179,10 +161,10 @@ Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44F ```bibtex @techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" + author = {Feldfeber, Ivana and Quiroga, Yasm\'{\i}n Bel\'{e}n and Guevara, Clarissa and Ciolfi Felice, Marianela}, + title = {Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico}, + institution = {DataGenero}, + year = {2022}, + url = {https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view} } ``` diff --git a/docs/pipeline/README.md b/docs/pipeline/README.md deleted file mode 100644 index 22514bf3..00000000 --- a/docs/pipeline/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Pipeline -

-schema -

- -The AymurAI pipeline is written in a modular way, each step is independent and can be replaced by another one. This allows us to easily change the models or the way in which the information is processed. -It's made up of three main steps: preprocessing, inference and postprocessing. -The preprocessing step is in charge of dividing the court rulings into paragraphs and normalizing their text. The inference step is in charge of extracting the information from the paragraphs. Finally, the postprocessing step is in charge of filtering out irrelevant decisions and formatting the text. -# Preprocessing -The flow of information in the system starts with a pre-processing step for the court rulings, where the documents are divided into paragraphs and their text is normalized. This normalization process involves the unification of multiple spaces and the removal of accents and/or special characters . -In this way, the paragraphs become the minimum unit of analysis, and predictions are generated in parallel and independently from one another. - - -# Inference -

-schema -

- -The fundamental piece of the AymurAI backend is the AI models. They are in charge of extracting information from court rulings. -For their development, we use the following work frameworks: -The NER model was developed using [Flair](https://github.com/flairNLP/flair), a Python library created by Humboldt - University of Berlin focused on natural language processing models. Flair is based on PyTorch, one of the main artificial intelligence frameworks, and allows integration with Transformers, another of the main libraries of natural language processing models. -For the decision model we use PyTorch natively. - -Each paragraph is processed by these models, and the results are combined. - -Check out the [model cards](#model-cards) for more information about the models, and the [model training](#model-training-notebooks) section for more information about how the models were trained. - -# Postprocessing -Once the inference is made, the predictions are postprocessed in order to obtain the final results. -This process involves: -* Subcategorization extraction or classification -* Filtering out irrelevant decisions -* Text formatting - -## Subcategorization -* Regex -Regular expressions allow us to identify some subcategories whose texts tend to be repeated following very marked patterns. - -* Sentence Similarity -Entities with many subcategories, and less well represented, are identified using sentence similarity. The Universal Sentence Encoder Multilingual QA model, published by Google, allows us to encode the text identified as belonging to certain categories in order to then calculate a semantic similarity score with respect to each possible subcategory and thus generate a ranking of candidates, ordered from highest to lowest similarity. - - -## Text Formating -The NER model extract non-structured information from the text. Dates and times are reformatted to a standard format. - -## Filters -Many of the decisions that are extracted by the models are not relevant to the user. Relevant decisions are those that contain a specific type of information, such as presence of other relevant entities (e.i DETALLE) - -# Models -## Model cards -* [Flair NER Spanish Judicial](./flair-model-card.md) -* [Decision Text Classification](./decision-model-card.md) - -# Model Training Notebooks -* [Flair NER Spanish Judicial](../../notebooks/experiments/ner/flair/) -* [Decision Text Classification](../../notebooks/experiments/decision/) diff --git a/docs/pipeline/assets/decision-confusion-matrix.png b/docs/pipeline/assets/decision-confusion-matrix.png deleted file mode 100644 index a88291fe..00000000 Binary files a/docs/pipeline/assets/decision-confusion-matrix.png and /dev/null differ diff --git a/docs/pipeline/assets/decision-schema.png b/docs/pipeline/assets/decision-schema.png deleted file mode 100644 index 26e4366f..00000000 Binary files a/docs/pipeline/assets/decision-schema.png and /dev/null differ diff --git a/docs/pipeline/assets/inference-example.png b/docs/pipeline/assets/inference-example.png deleted file mode 100644 index bb6000dd..00000000 Binary files a/docs/pipeline/assets/inference-example.png and /dev/null differ diff --git a/docs/pipeline/assets/postprocessing-example.png b/docs/pipeline/assets/postprocessing-example.png deleted file mode 100644 index 9bd51d84..00000000 Binary files a/docs/pipeline/assets/postprocessing-example.png and /dev/null differ diff --git a/docs/pipeline/decision-model-card.md b/docs/pipeline/decision-model-card.md deleted file mode 100644 index e1e37bb3..00000000 --- a/docs/pipeline/decision-model-card.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -license: mit -language: -- es -tags: -- text-classification -datasets: -- ArJuzPCyF10 -metrics: -- f1 -widget: -- text: 1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas. -library_name: torch -pipeline_tag: text-classification ---- - -# Model Description -

-schema -

- -This model was developed by [{ collective.ai }](https://collectiveai.io) as part of the [AymurAI](https://aymurai.info) project by [DataGenero](https://datagenero.org). - - - -# Intended uses & limitations -AymurAI is intended to be used as a tool to address the lack of transparency in the judicial system regarding gender-based violence (GBV) cases in Latin America. The goal is to increase report levels, build trust in the justice system, and improve access to justice for women and LGBTIQ+ people. AymurAI will generate and maintain anonymized datasets from legal rulings to understand GBV and support policy making, and also contribute to feminist collectives' campaigns. - -AymurAI is still a prototype and is only being implemented in Argentina and Mexico. Its capabilities are limited to semi-automated data collection and analysis, and the results may be subject to limitations such as the quality and consistency of the data, potential biases in the AI model, and the availability of the data. Additionally, the effectiveness of AymurAI in addressing the lack of transparency in the judicial system and improving access to justice may also depend on other factors such as the level of cooperation from court officials and the broader cultural and political context. - -This model was trained with a closed dataset from an Argentine criminal court. It's is designed to identify and extract relevant information from court sentences related to GBV cases. The use of a domain specific dataset from an Argentine criminal court ensures that the model is tailored to the specific legal and cultural context, allowing for more accurate results. However, it also means that the model may not be applicable or effective in other countries or regions with different legal systems or cultural norms. - -# Usage -## How to use the model in torch - - -```python - -from aymurai.models.decision.binregex import DecisionConv1dBinRegex - -model = DecisionConv1dBinRegex( - tokenizer_path="https://drive.google.com/uc?id=1eljQOinpObdfBREIKxVnC5Y2g_sbhPHT&confirm=true", - model_checkpoint="https://drive.google.com/uc?id=19_YmBJnO06iS0qW8ak0zl0EIsJYin8kQ&confirm=true", - device="cpu", -) - -text = "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." - -input_ids = model.tokenizer.encode_batch([text]) -input_ids.shape - -# The model return the logsoftmax of the probabilities of the classes -probabilities = model.model(input_ids).exp().detach().numpy() -probabilities - - -``` - -This yields the following output: -``` -array([[2.2457261e-05, 9.9997759e-01]], dtype=float32) -``` -The first column is the probability of the text not being a decision, and the second column is the probability of the text being a decision. For this example, the model predicts that the text is a decision with a probability of 99.99%. - -## Using the model in aymurai pipeline -You also can run the model directly in the aymurai pipeline. - -```python -from aymurai.pipeline import AymurAIPipeline -from aymurai.models.flair.utils import FlairTextNormalize -from aymurai.models.decision.binregex import DecisionConv1dBinRegex - -config = { - "preprocess": [ - [ - "aymurai.models.flair.utils.FlairTextNormalize", - {} - ] - ], - "models": [ - - [ - "aymurai.models.decision.binregex.DecisionConv1dBinRegex", - { - "tokenizer_path": "https://drive.google.com/uc?id=1eljQOinpObdfBREIKxVnC5Y2g_sbhPHT&confirm=true", - "model_checkpoint": "https://drive.google.com/uc?id=19_YmBJnO06iS0qW8ak0zl0EIsJYin8kQ&confirm=true", - "device": "cpu", - "threshold": 0.5, - "return_only_with_detalle": true - } - ] - ], - "postprocess": [ - ], - "use_cache": false -} - - -pipeline = AymurAIPipeline.load("/resources/pipelines/production/full-paragraph") - -item = { - 'path': 'dummy', - 'data': { - 'doc.text': "1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas." - } -} - -pipeline.predict([item]) - - -``` - - - - -# Entities and metrics -## Description -This model only considers the classification of paragraphs being decisions or not. - -For the complete list of entities, please refer to the entities' description table ([en](../data/en/entities-table.md)|[es](../data/es/entities-table.md)). - -For a complete description about entities considered by AymurAI, refer to the [Glossary for the Dataset with gender perspective](https://docs.google.com/document/d/123B9T2abCEqBaxxOl5c7HBJZRdIMtKDWo6IKHIVil04/edit) written by [DataGenero](https://datagenero.org) (spanish only) - - -## Data -The model was trained with a dataset of 1200 legal rulings from an Argentine criminal court. - -Due to the nature of the data (personal data, complaint characteristics and victim protection) the documents are kept private. -### List of annotation contributors -The dataset was manually annotated by: -* Diego Scopetta -* Franny Rodriguez Gerzovich ([email](fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) -* Laura Barreiro -* Matías Sosa -* Maximiliano Sosa -* Patricia Sandoval -* Santiago Bezchinsky ([email](santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) -* Zoe Rodriguez Gerzovich -## Metrics - -

-schema -

- -Class 0: Not a decision -Class 1: Decision - - - -## Citation -Please cite [the following paper](https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view) when using AymurAI: - -```bibtex -@techreport{feldfeber2022, - author = "Feldfeber, Ivana and Quiroga, Yasmín Belén and Guevara, Clarissa and Ciolfi Felice, Marianela", - title = "Feminisms in Artificial Intelligence: Automation Tools towards a Feminist Judiciary Reform in Argentina and Mexico", - institution = "DataGenero", - year = "2022", - url = "https://drive.google.com/file/d/1P-hW0JKXWZ44Fn94fDVIxQRTExkK6m4Y/view" -} -``` diff --git a/docs/pipelines/README.md b/docs/pipelines/README.md new file mode 100644 index 00000000..e62edfe0 --- /dev/null +++ b/docs/pipelines/README.md @@ -0,0 +1,25 @@ +# Pipelines +Language: **English** | [Español](../es/pipelines/README.md) + +This section documents AymurAI backend production pipelines by workflow. + +## Flow docs +- Anonymizer: [anonymizer/README.md](anonymizer/README.md) +- Datapublic: [datapublic/README.md](datapublic/README.md) + +## Related model docs +- Models index: [../models/README.md](../models/README.md) +- Flair NER model card: [../models/flair-model-card.md](../models/flair-model-card.md) +- Decision model card: [../models/decision-model-card.md](../models/decision-model-card.md) + +## Production pipeline sources +- Anonymizer config: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Datapublic config: `resources/pipelines/production/datapublic/pipeline.json` + +## API mapping +- `POST /anonymizer/predict` -> `flair-anonymizer` +- `POST /datapublic/predict/{document_id}` -> `datapublic` + +## Related docs +- API reference: [../api/README.md](../api/README.md) +- Internal database: [../database/README.md](../database/README.md) diff --git a/docs/pipelines/anonymizer/README.md b/docs/pipelines/anonymizer/README.md new file mode 100644 index 00000000..67880ba0 --- /dev/null +++ b/docs/pipelines/anonymizer/README.md @@ -0,0 +1,68 @@ +# Anonymizer Pipeline +Language: **English** | [Español](../../es/pipelines/anonymizer/README.md) + +Workflow-oriented technical reference for anonymization. + +## Scope +This flow extracts entities from judicial text and compiles anonymized output documents. + +## Diagram +![Anonymizer pipeline diagram](pipeline.png) + +Editable source: [pipeline.excalidraw](pipeline.excalidraw) + +## Runtime entrypoints +- `POST /misc/document-extract` +- `POST /anonymizer/predict` +- `POST /anonymizer/disambiguate` +- `POST /anonymizer/validation` +- `POST /anonymizer/anonymize-document` + +## Step-by-step flow +1. Text extraction (`/misc/document-extract`) splits source document into normalized paragraphs. +2. Prediction (`/anonymizer/predict`) runs NER on each paragraph. +3. Disambiguation (`/anonymizer/disambiguate`) assigns canonical entity IDs and effective anonymization/disambiguation metadata. +4. Manual review in UI edits labels and optional policies. +5. Compilation (`/anonymizer/anonymize-document`) applies replacements and exports anonymized `.odt`. + +## Technical components + +### Pipeline configuration +- Source: `resources/pipelines/production/flair-anonymizer/pipeline.json` +- Preprocess: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Model: + - `aymurai.models.flair.core.FlairModel` + - base model path: `aymurai/anonymizer-beto-cased-flair` +- Postprocess: + - `aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + +### API contracts used by this flow +- `DocumentInformation` +- `DocumentAnnotations` +- `LabelPolicy` +- `RenderPolicy` +- `EntityAttributes` (notably: `canonical_entity_id`, `aymurai_label_instance`, `aymurai_disambiguation`, `aymurai_anonymize`) + +### Core backend modules +- Router: `aymurai/api/endpoints/routers/anonymizer/anonymizer.py` +- Rendering: `aymurai/text/anonymization/docx.py` and `aymurai/text/anonymization/pdf.py` +- Canonical entity mapping: `aymurai/utils/entity_disambiguation/` + +## Persistence (DB) +Tables touched by this flow: +- `anonymization_paragraph` +- `anonymization_document` +- `anonymization_document_paragraph` + +## Notes +- Label policies are merged from environment and request payload. +- Render policy controls suffix behavior (`auto`, `always`, `never`) during replacement. + +## Related docs +- Pipelines index: [../README.md](../README.md) +- Anonymizer entities: [../../entities/anonymizer/README.md](../../entities/anonymizer/README.md) +- Anonymizer model card: [../../models/anonymizer-model-card.md](../../models/anonymizer-model-card.md) +- API reference: [../../api/README.md](../../api/README.md) +- Internal database: [../../database/README.md](../../database/README.md) diff --git a/docs/pipelines/anonymizer/pipeline.excalidraw b/docs/pipelines/anonymizer/pipeline.excalidraw new file mode 100644 index 00000000..f7da8a73 --- /dev/null +++ b/docs/pipelines/anonymizer/pipeline.excalidraw @@ -0,0 +1,1748 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "M3_9cSeevpvgvuGOJmtLu", + "type": "rectangle", + "x": 341.0753968253973, + "y": -946.5918028903099, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0P", + "roundness": { + "type": 3 + }, + "seed": 630737269, + "version": 231, + "versionNonce": 857423573, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1764875361089, + "link": null, + "locked": false + }, + { + "id": "e0VYlO1HBUE6Y61hMmjQS", + "type": "line", + "x": 384.5306207059946, + "y": -906.3554844823498, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Q", + "roundness": { + "type": 2 + }, + "seed": 924406485, + "version": 197, + "versionNonce": 1258245525, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "zz4q6sGFYLSQEGm4_Z_pZ", + "type": "line", + "x": 380.15053275977357, + "y": -879.2079349266717, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0R", + "roundness": { + "type": 2 + }, + "seed": 2054303797, + "version": 300, + "versionNonce": 2102055669, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "S9t8FU85SfKbOpC8RR_gM", + "type": "line", + "x": 380.6970945304854, + "y": -849.4353562329334, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0S", + "roundness": { + "type": 2 + }, + "seed": 227041685, + "version": 330, + "versionNonce": 1522445397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "DL-iPq6d9xxkjVSnghr83", + "type": "text", + "x": 394.20929106726203, + "y": -619.8728974176731, + "width": 82.81063842773438, + "height": 120.7089552238806, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0T", + "roundness": null, + "seed": 898721525, + "version": 217, + "versionNonce": 1900882722, + "isDeleted": false, + "boundElements": [], + "updated": 1772204466428, + "link": null, + "locked": false, + "text": ".docx\n.pdf\n.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": ".docx\n.pdf\n.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eb7-zegRUYIJqYobn0GTg", + "type": "line", + "x": 379.7022624970393, + "y": -821.0544894574741, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0U", + "roundness": { + "type": 2 + }, + "seed": 1964163157, + "version": 228, + "versionNonce": 2143640341, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "76_13jw7FNEf9CD3vN4K7", + "type": "line", + "x": 386.86680332119363, + "y": -794.1556155836837, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0V", + "roundness": { + "type": 2 + }, + "seed": 1642196405, + "version": 262, + "versionNonce": 418590837, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "7bpvHSILjOEGoxBVuVvmL", + "type": "line", + "x": 387.21934633300566, + "y": -768.6496839272999, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0W", + "roundness": { + "type": 2 + }, + "seed": 992243477, + "version": 368, + "versionNonce": 1820285397, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hunvhsGsscndVNFWE5PbJ", + "type": "line", + "x": 386.2245142995596, + "y": -740.2688171518405, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0X", + "roundness": { + "type": 2 + }, + "seed": 1062192245, + "version": 266, + "versionNonce": 1132418869, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "T8wwR2IwhMmplgY6InC65", + "type": "line", + "x": 393.3890551237139, + "y": -713.3699432780502, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "baV9NX1y69MAosZs6e-CT" + ], + "frameId": null, + "index": "b0Y", + "roundness": { + "type": 2 + }, + "seed": 1110011349, + "version": 300, + "versionNonce": 676898965, + "isDeleted": false, + "boundElements": [], + "updated": 1764875361089, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow", + "x": 560.3079384765334, + "y": -801.8590832623923, + "width": 311.10872819013355, + "height": 3.3450992498784444, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0Z", + "roundness": { + "type": 2 + }, + "seed": 46590773, + "version": 868, + "versionNonce": 416388706, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "AQOsTPNecUVIP5dcYG-CL" + } + ], + "updated": 1772207740751, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 311.10872819013355, + 3.3450992498784444 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "M3_9cSeevpvgvuGOJmtLu", + "focus": 0.024877658056798795, + "gap": 5.80129262390335 + }, + "endBinding": { + "elementId": "vWBP3oXNVHZGwRqV71dye", + "focus": 0.16647678467610177, + "gap": 26.273809523809405 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "AQOsTPNecUVIP5dcYG-CL", + "type": "text", + "x": 633.3624170125181, + "y": -825.1557337445369, + "width": 164.99977111816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0a", + "roundness": null, + "seed": 1099807893, + "version": 58, + "versionNonce": 587560482, + "isDeleted": false, + "boundElements": [], + "updated": 1772204827339, + "link": null, + "locked": false, + "text": "/misc/document-\nextract", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EOtxLYz_c5dBLBZTsf6bP", + "originalText": "/misc/document-extract", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vWBP3oXNVHZGwRqV71dye", + "type": "text", + "x": 897.6904761904764, + "y": -891.8596681096677, + "width": 536.5656565656566, + "height": 232.7272727272728, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0b", + "roundness": null, + "seed": 372466165, + "version": 440, + "versionNonce": 1496857790, + "isDeleted": false, + "boundElements": [ + { + "id": "EOtxLYz_c5dBLBZTsf6bP", + "type": "arrow" + } + ], + "updated": 1772204827339, + "link": null, + "locked": false, + "text": "{\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE\n128 1 párr\",\n ...\n ],\n \"document_id\": \n}", + "fontSize": 23.27272727272728, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"document\": [\n \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n ...\n ],\n \"document_id\": \n}", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "uIFK9kOYNJE4xD48qvRxR", + "type": "text", + "x": 1973.8949799899103, + "y": -527.8682896843161, + "width": 351.6396893419871, + "height": 74.26086756682336, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0f", + "roundness": null, + "seed": 1570520949, + "version": 508, + "versionNonce": 1517826942, + "isDeleted": false, + "boundElements": [], + "updated": 1772207749195, + "link": null, + "locked": false, + "text": "Per-paragraph prediction\n(DocumentInformation)", + "fontSize": 29.704347026729355, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Per-paragraph prediction\n(DocumentInformation)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "cPvNsqjYFBYhf1dvqCfUM", + "type": "text", + "x": 5111.82726940021, + "y": -661.9813153131714, + "width": 335.071044921875, + "height": 80.4726368159204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0o", + "roundness": null, + "seed": 183537621, + "version": 526, + "versionNonce": 1023679678, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "text": "Anonymized document\n.odt", + "fontSize": 32.189054726368155, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymized document\n.odt", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "H45OT-nnyrYBIdX_fHnB-", + "type": "rectangle", + "x": 5171.529458527813, + "y": -988.7002207858083, + "width": 215.66666666666663, + "height": 280.0447761194029, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0o8", + "roundness": { + "type": 3 + }, + "seed": 1543168597, + "version": 491, + "versionNonce": 1377686782, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false + }, + { + "id": "HkLFBBIeoT2NmdHolQogn", + "type": "line", + "x": 5214.984682408411, + "y": -948.4639023778478, + "width": 94.95771144278605, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0oG", + "roundness": { + "type": 2 + }, + "seed": 997576629, + "version": 455, + "versionNonce": 1138132286, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 94.95771144278605, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "dP23wQQAJGa8EbTko6TLk", + "type": "line", + "x": 5210.604594462189, + "y": -921.31635282217, + "width": 48.28358208955214, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0oV", + "roundness": { + "type": 2 + }, + "seed": 1005326613, + "version": 558, + "versionNonce": 328150398, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 48.28358208955214, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "AzAxanw4nR0bo9R-OtYUW", + "type": "line", + "x": 5211.1511562329015, + "y": -891.5437741284319, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0ol", + "roundness": { + "type": 2 + }, + "seed": 1255351925, + "version": 588, + "versionNonce": 1854459326, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "FZMsE2ftxJlqlXy5y1Kot", + "type": "line", + "x": 5210.156324199455, + "y": -863.1629073529726, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0p", + "roundness": { + "type": 2 + }, + "seed": 710528309, + "version": 485, + "versionNonce": 837051902, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "c5LayQF2_U6-IqmzQigBO", + "type": "line", + "x": 5217.320865023609, + "y": -836.2640334791819, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0pG", + "roundness": { + "type": 2 + }, + "seed": 2020892309, + "version": 520, + "versionNonce": 1856173630, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "CZP0a-w2xzB-aIeBIdTfj", + "type": "line", + "x": 5217.673408035422, + "y": -810.758101822798, + "width": 131.97512437810926, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0pV", + "roundness": { + "type": 2 + }, + "seed": 1005962229, + "version": 626, + "versionNonce": 1839562366, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 131.97512437810926, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YGpIXuZRgXAXrbeyTS72v", + "type": "line", + "x": 5216.678576001976, + "y": -782.3772350473388, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0q", + "roundness": { + "type": 2 + }, + "seed": 1222868309, + "version": 524, + "versionNonce": 866975422, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ABZSKgAIt_rm_gemO06m9", + "type": "line", + "x": 5223.8431168261295, + "y": -755.4783611735489, + "width": 128.75621890547262, + "height": 1.6094527363184077, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "mnNZnb2eX5SBS_s6ArNSy" + ], + "frameId": null, + "index": "b0r", + "roundness": { + "type": 2 + }, + "seed": 1452477109, + "version": 558, + "versionNonce": 89814782, + "isDeleted": false, + "boundElements": [], + "updated": 1772207001336, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 128.75621890547262, + 1.6094527363184077 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "hotXOq7-8juh39kLJem4p", + "type": "arrow", + "x": 2454.591377430666, + "y": -806.9942535766841, + "width": 374.23286489749944, + "height": 2.3331459911660204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0u", + "roundness": { + "type": 2 + }, + "seed": 755633851, + "version": 550, + "versionNonce": 1019750946, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "XSjKuWf82l8iev5nfszlU" + } + ], + "updated": 1772206699138, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 374.23286489749944, + 2.3331459911660204 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "XSjKuWf82l8iev5nfszlU", + "type": "text", + "x": 2517.6379398842982, + "y": -818.327680581101, + "width": 248.13973999023438, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0uV", + "roundness": null, + "seed": 193864190, + "version": 43, + "versionNonce": 360400482, + "isDeleted": false, + "boundElements": null, + "updated": 1772206699138, + "link": null, + "locked": false, + "text": "/anonymizer/disambiguate", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hotXOq7-8juh39kLJem4p", + "originalText": "/anonymizer/disambiguate", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "gXsFYxTEKv14jMJGLJI8O", + "type": "arrow", + "x": 1452.931012958739, + "y": -798.134476023053, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0v", + "roundness": { + "type": 2 + }, + "seed": 1974610101, + "version": 1591, + "versionNonce": 620553314, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Q4IQFcmT8i1SnTx5DuzTS" + } + ], + "updated": 1772205508184, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Q4IQFcmT8i1SnTx5DuzTS", + "type": "text", + "x": 1547.6477860456841, + "y": -810.1827523166924, + "width": 202.40042114257812, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0w", + "roundness": null, + "seed": 2134198805, + "version": 73, + "versionNonce": 1627097790, + "isDeleted": false, + "boundElements": [], + "updated": 1772205508184, + "link": null, + "locked": false, + "text": "N x /anonymizer/predict", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "gXsFYxTEKv14jMJGLJI8O", + "originalText": "N x /anonymizer/predict", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "jFnC8PtgWHvjc9weWuFWX", + "type": "text", + "x": 326.69940776522253, + "y": -1119.4404761904757, + "width": 558.84033203125, + "height": 76.99999999999986, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b0z", + "roundness": null, + "seed": 29900859, + "version": 88, + "versionNonce": 181080958, + "isDeleted": false, + "boundElements": [], + "updated": 1772204432143, + "link": null, + "locked": false, + "text": "Anonymizer pipeline", + "fontSize": 61.59999999999989, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Anonymizer pipeline", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "LUVMyylE6hgpMsgfw7aao", + "type": "rectangle", + "x": 1888.8814913275708, + "y": -991.508812100596, + "width": 521.6666666666664, + "height": 428.88888888888886, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b13", + "roundness": { + "type": 3 + }, + "seed": 1127296034, + "version": 420, + "versionNonce": 1975633314, + "isDeleted": false, + "boundElements": [], + "updated": 1772204924667, + "link": null, + "locked": false + }, + { + "id": "1ulzr8Q7P6FFgdWlFr0cp", + "type": "text", + "x": 1911.6383476101225, + "y": -918.7310343228181, + "width": 476.1529541015625, + "height": 283.3333333333333, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b14", + "roundness": null, + "seed": 238770146, + "version": 239, + "versionNonce": 7850686, + "isDeleted": false, + "boundElements": [], + "updated": 1772206121905, + "link": null, + "locked": false, + "text": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "fontSize": 16.19047619047619, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "{\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n ...\n }\n }\n ]\n}", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vrSceZivuOFwwm43eU0mr", + "type": "text", + "x": 924.6722658756239, + "y": -606.9260633331838, + "width": 482.6020771953618, + "height": 43.38079023612419, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b15", + "roundness": null, + "seed": 1891961378, + "version": 472, + "versionNonce": 550795810, + "isDeleted": false, + "boundElements": [], + "updated": 1772207740751, + "link": null, + "locked": false, + "text": "Document extraction output", + "fontSize": 34.70463218889935, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Document extraction output", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GNTtv7Qdsx0aEF6kxRZ5a", + "type": "text", + "x": 2736.8880249454437, + "y": -1160.814903248222, + "width": 1100.7685546875, + "height": 832.0238095238095, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b17", + "roundness": null, + "seed": 882291582, + "version": 555, + "versionNonce": 362426494, + "isDeleted": false, + "boundElements": [], + "updated": 1772207723129, + "link": null, + "locked": false, + "text": "{\n \"data\": [\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_instance\": 1,\n 'aymurai_disambiguation': 'fuzzy',\n 'aymurai_anonymize': True,\n 'canonical_entity_id': \n }\n }\n ]\n },\n ...\n ],\n \"label_policies\": {\n \"PER\": {\"anonymize\": True, \"disambiguation\": \"fuzzy\", \"use_subclass_when_available\": True},\n \"FECHA\": {\"anonymize\": False, \"disambiguation\": \"none\", \"use_subclass_when_available\": False}\n },\n \"render_policy\": {\n \"suffix_mode\": \"auto\",\n \"suffix_threshold\": 1\n }", + "fontSize": 22.952380952380953, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "{\n \"data\": [\n {\n \"document\": \"R., MATIAS EZEQUIEL SOBRE 128 1 párr\",\n \"labels\": [\n {\n \"text\": \"R., MATIAS EZEQUIEL\",\n \"start_char\": 0,\n \"end_char\": 23,\n \"attrs\": {\n \"aymurai_label\": \"PER\",\n \"aymurai_label_instance\": 1,\n 'aymurai_disambiguation': 'fuzzy',\n 'aymurai_anonymize': True,\n 'canonical_entity_id': \n }\n }\n ]\n },\n ...\n ],\n \"label_policies\": {\n \"PER\": {\"anonymize\": True, \"disambiguation\": \"fuzzy\", \"use_subclass_when_available\": True},\n \"FECHA\": {\"anonymize\": False, \"disambiguation\": \"none\", \"use_subclass_when_available\": False}\n },\n \"render_policy\": {\n \"suffix_mode\": \"auto\",\n \"suffix_threshold\": 1\n }", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "GpgRoWvkFU8pY8qyanTEz", + "type": "text", + "x": 2971.7865914541294, + "y": -294.843926983973, + "width": 630.971421670128, + "height": 41.08319145660856, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b18", + "roundness": null, + "seed": 440634338, + "version": 526, + "versionNonce": 60141218, + "isDeleted": false, + "boundElements": [], + "updated": 1772207760618, + "link": null, + "locked": false, + "text": "Review payload (DocumentAnnotations)", + "fontSize": 32.86655316528685, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Review payload (DocumentAnnotations)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "t1CVOK0LIbIN3hNMusjFo", + "type": "arrow", + "x": 3486.838089935834, + "y": -810.7442535766841, + "width": 374.23286489749944, + "height": 2.3331459911660204, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b19", + "roundness": { + "type": 2 + }, + "seed": 1603482402, + "version": 639, + "versionNonce": 2029670818, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "9SlkoqV3LCMNC6VOzAR4J" + } + ], + "updated": 1772206766396, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 374.23286489749944, + 2.3331459911660204 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "9SlkoqV3LCMNC6VOzAR4J", + "type": "text", + "x": 3609.451244288067, + "y": -828.7443472477678, + "width": 122.33988952636719, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1A", + "roundness": null, + "seed": 2126806754, + "version": 83, + "versionNonce": 1522294242, + "isDeleted": false, + "boundElements": [], + "updated": 1772206766396, + "link": null, + "locked": false, + "text": "Review in UI", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "t1CVOK0LIbIN3hNMusjFo", + "originalText": "Review in UI", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1g5mwsMmOsivD02csjkTB", + "type": "text", + "x": 3879.9078976523183, + "y": -604.1107637238993, + "width": 809.6706988450118, + "height": 36.97264967907313, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1C", + "roundness": null, + "seed": 306784958, + "version": 693, + "versionNonce": 1588957730, + "isDeleted": false, + "boundElements": [], + "updated": 1772207711810, + "link": null, + "locked": false, + "text": "Manual review: edit labels, label policies and render policy", + "fontSize": 29.5781197432585, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Manual review: edit labels, label policies and render policy", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lhZO6MPRRTWy3V55I0vZM", + "type": "arrow", + "x": 4705.8560612283145, + "y": -807.5294042874617, + "width": 403.8000183105464, + "height": 5.908527289127619, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1G", + "roundness": { + "type": 2 + }, + "seed": 280036245, + "version": 1662, + "versionNonce": 1583584226, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "GHolMdYEXJBOSjiLEJF7i" + } + ], + "updated": 1772207526341, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195.91698365823413, + -2.048276293639333 + ], + [ + 403.8000183105464, + -5.908527289127619 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "GHolMdYEXJBOSjiLEJF7i", + "type": "text", + "x": 4769.7727702283455, + "y": -819.577680581101, + "width": 264.00054931640625, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1H", + "roundness": null, + "seed": 1331022069, + "version": 58, + "versionNonce": 451123582, + "isDeleted": false, + "boundElements": [], + "updated": 1772206985239, + "link": null, + "locked": false, + "text": "/anonymizer/anonymize-document", + "fontSize": 16, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lhZO6MPRRTWy3V55I0vZM", + "originalText": "/anonymizer/anonymize-document", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "oRmHjBYwEzoCu3wVivfxA", + "type": "image", + "x": 3870.418043822792, + "y": -974.1722733335696, + "width": 828.6504065040658, + "height": 351.8703703703709, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b1M", + "roundness": null, + "seed": 22438654, + "version": 258, + "versionNonce": 1815649534, + "isDeleted": false, + "boundElements": [], + "updated": 1772207295559, + "link": null, + "locked": false, + "status": "pending", + "fileId": "ac6c81549182e851ebe5aa58dd0fba44c4ab568b", + "scale": [ + 1, + 1 + ], + "crop": { + "x": 0, + "y": 0, + "width": 1440, + "height": 611.4681527412586, + "naturalWidth": 1440, + "naturalHeight": 820 + } + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": { + "ac6c81549182e851ebe5aa58dd0fba44c4ab568b": { + "mimeType": "image/png", + "id": "ac6c81549182e851ebe5aa58dd0fba44c4ab568b", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAM0CAYAAABNh4WnAAAQAElEQVR4AeydBYBUVRfH//e9mdkOurtTkRRFFAMVW1EEu7A7sD5bRMROMFAaERAklVZKurtri+2cmfe+c9/swAILoi64LP/n3Bf3nnvuub93dt65Z8bBaNu2rc1CBvQB+gB9gD5AH6AP0AfoA/QB+gB9gD5AHyjRPsC1P/Mf9AH6AH2APvCf+ICRkZEBFjKgD9AH6AP0AfoAfYA+cLJ8gOPQ1+gD9AH6AH2APkAfoA/QB+gD9IHTxwcMpRSUYlGKDJQ6zRhwvvzbpw/QB+gD9AH6AH2APkAfoA/QB+gD9AH6QMn3Ad5j3mP6wH/qAwa4kQAJkAAJkAAJkAAJkAAJkMBJIMAhSIAESIAESIAESIAETj8CTECffvecMyYBEiABEiABEiABEiABEiABEiABEiABEiABEij5BIrFDJmALha3gUaQAAmQAAmQAAmQAAmQAAmQAAmUXAKcGQmQAAmQAAmcvgSYgD597z1nTgIkQAIkQAKnHwHOmARIgARIgARIgARIgARIgARI4KQSYAL6pOLmYEECPJIACZAACZAACZAACZAACZAACZAACZR8ApwhCZAACTABTR8gARIgARIgARIgARIouQT0v3henGZ3VHsU1Im1k9pJgARIgARIgARIgARI4D8hwAT0f4Kdg5IACZy+BDhzEiABEiCBk0nA8nrh9Vsnc8hjjGXDr+2x7MNkbFh+bacfh7ccJshLEiABEiABEiABEiCBU4bAiTfU7/ejYLHtoo0mtT6tX6mDX5VQSjlj6rbjnSET0MdLinIkQAIkQAIkQAIkQAKnFgHbQHhsaZSOCoOh7EByVwJm03TBNPKDaOfaDFzLuWHKeX4xDAO6mM61AWkOzF8ZMF2m6AzoMEwTLtOAcloVDMOEKXW6GMFxACjlQlTpMogNdwMBa+SoXybCY0qjdEwkXKKkaJcNWj8LCZzmBDh9EiABEiABEihhBAzDgMvlQunSEkPml5iYGLjdbvydxDCOsWk9eozIyEh4vV6JZZVT9Lmu07GuljmGigNNxoEznpAACZAACZAACZDACSRA1SRw8ggo2FYu3JVa44GXeuOdF+9G84qh8PlsKL8XaSlJSM3Kg0TQgFynpyYjJTPX+RZyVnoaUlNSkJySiqzsHGRnZSAlORmp6VnwWYEuli8bqUnJyMzzQW856clISs2C35Yr24ecrHSkpoqO5BRkZMs4koT2eX0o0+RyPN+7L1556GpUDgN8fkDBD78qhUsfeg2vPXQ9akb5kOdXktwGNxIgARIgARIgARIgARI4goBSCpmZmdi/fz/KlClzoDRr1gxNmjRBXl4edIL6iI5/o0IpBcuyEBISgltvvRVad05ODrKzs9G8eXOnTrdpGRzHxgT0cUAqYSKcDgmQAAmQAAmQAAmUaALKsOH1RqBZhzYoh2Qku2qhw1n1EerPhqtMLVza7R5c264mlC8HVnQ1XHTNLbi+Qz1ERFVGm4sux7Vdu6H7jVehbbOGaNbmAnTt3gPXXNgK5cOB3FwLkeWb4bq7euCcOmUkMDfR6MLuuOPKtoj12LA9pdG43QW46tob0aNHV5zfoiZcubmApzxadTwT7pQ4+Mq2wHmNK8K2/XIflCShIclvP/x+CzqHLZV8kQAJkAAJkEBREKAOEiCBEkZAJ5Z1Irh169bo2bOnk3DWyeHatWtjzZo1RTZb/c1m/e3ntLQ0zJs3D9dddx0aN27sjHfttdc6denp6c63sI9nUCagj4cSZUiABEiABEiABEiABE4NAkrCW28ujIpNcW7jyti3cgamLUpE/Q7tUDPahQx/GBq2uQTXXdEO0V4foqs2xaXXX48mpUyEV26Oq3vcjC6dOqDjxVeg++134JauV+GCjhdI/Z248aKzEGnnwB1bH5fccBXOqh4rSWQDddtfiesuOAORhg+e0o1w5a09cNUlnXDhBRegdYPy8GXnIKLu2ehQ08CS36dj+U4bzc9vjfKGBf+pQZVWkgAJkAAJkAAJkAAJHAcB/bMUhZXj6HpcIkop51vIrVq1gk4Ob9iwAatXr0bLli1Rrlw56J/HOC5FxyGkk9D6Jz0WLVqEH3/8Ed26dcONN96IkSNHQtfpNi1zHKogEfrxiFGGBEiABEoAAU6BBEiABEigxBNQykZungv1WzRDhcgszB87FtMXzMdeT1N0bFkFObu3YNnSZcgoXRe1y8WifIUqCPetw+x522GZLrjNbPzxw3t49b2hSIyqjKylP+KV/72OqVvdaN6kHkpFeZCXm4205BRk5QXSx7kZKUhOy0LgJzpMhIUrrJkwAM8+/hS++mUFcjyl0aJtExgpu/HHmB/x64qNMCq0RJv6EWKrDVlHQJX4O8MJkgAJkAAJkAAJkMBJJPAfDKWTsfpnMQ4vSUlJ0D9VoVTRRHxKKehvQa9YsQI6EbxgwQLs2rULSqki/Q3oggj1P0Sov32ti88X+Bm6gu1/dc4E9F8RYjsJkAAJkAAJkAAJkMApQsCA8ufCiqmKpvXrIdzKRLlm56Ntg3LwZ1qo3aYdqruSsHbzdmTnlkazdg1RtUZdeHatwoaUHBj629MSuCvbi8yUOCSm2vC4FHJz0hCfkA5XiBs66JboHqbLhIINpZTUmTBNA86SwralFlIs+Px+5OVkwl2pCc6sVQaWDdRvfyEalQ2BbUSh4VlnINqfA0sZ4EYCJZUA50UCJEACJEACpwMBnWD2eDzo2rXrIeWGG25A9+7dERUVBZ24VcqJGP81EsMwHH3695510de5ubnYt28fXC4XdDL83w6ilHK+Ud2iRQvn288jRoxwvgmt56Pr9LetlTq++TDa/bd3g/1JgARIgARIoPgToIUkcFoQUIaNvFygfJXaqFMjBn5faZzXtQduvLgVYjw+eMo0QsuGMdixYgU2JGaiwTldcF4DD1YtXo+UbC8kjodePEBSyfpcLiRRbAQS07YFv84g69SyNwd5djjKxIbB8uUi12vj4GZLN0ukFExJXntzPajVqD4qxITCHVETl99yB65pW1PaTVSs3gR1KriQnWfJiLqfLfXgRgIkQAIkQAIkQAIkcIoRUEo5Sd+srCwcXvQ/GBhMCAeP/2Z6Sikn3rzlllvw2GOP4ZlnnkGDBg2QkpKCbdu2Fcm3oJVSToI7Ojoa5513Hn7++WesXLkS+lvX48ePd+oiIyMdmeOZCxPQx0OpyGSoiARIgARIgARIgARI4EQRUJYf/pBoNDjrPDQrm4JfB32KV55/ES+++BI+GbMYRukaOKt9O5TP3IA/lu6AHV0dFfxbsWT9TmRbHrhcHoRHRiJEEsdQBkIjohARYjrmukMiEBkeCo8LSE/ZhvXrUlHvkttE97O4pGEkLFtBKVu6uR0dYR43lCSqrVLV0LJlK1Q2NmHYR+/gpRfEnlffxOCZ21G2QTO0aFYPoT4vXDJWZHgITAVuJEACJEACJYIAJ0ECJHA6EVAq8G3hiRMnomCZPHkydMI2IyNDYk3Xv0aifwpDJ37HjRuHYcOGYebMmZg1axY++ugj6H8UMDw83EmE/9uBdKI8+K3qQYMGYdmyZQgNDUVYWBiWLFkCXef16i9wHF9q+fik/q3V7E8CJEACJEACJEACJEACJ5SA/iaIDU+IJHGt/Vj0629YtHUPUiXYz8zOwPal8zBjwTLsS1MIUfuxYv5KpNm52LVhLTbt3A93uAs5ybuwfMEibEvJg+3NxKZl87Bi234niE/csgwLVmxBtu2GnbYDU4Z9hwkLNiJxfxI2LZmJiXOWI81rwM5NxKqFf2JTXBq8tonISA9yk3di3tQZWLV3PzIy0pGRnoIN86djxpJNyEUYIkNysH3VIixZsw0ZXgVDktA2uJEACZAACZAACZAACZxKBJRSiI2NPaTExMQ41zqZq5O6RTEfrUsnmzdv3gxdNm3ahD179jgxa1GNoe1UKvAt6LS0tAPfqtb63W638w8g+nw+KCWBqxb+i2L8RTubSYAESKBICFAJCZAACZAACZxYApKyNUyo3GTMGvIe3vnuVyTkGAgNccHlDkNI7haM/ORdfDN1Pepdcgduvf5clPMmYMmixYjLDUFYqELSlj/w1dv98OvmDNjZcRj35VsYMGUtLNuPlRM+x1tfT8LeDEt0GkjdvRJjBn6ODz/8CF/0/xajZ65EpiSncxOXY2Dvvhj95zbYoZHwJ2zEqC/fxqdjlsDrCoHH7ZIkeSh88YvQv08fDJq2CtnIxe8/fIAPBk/G7kwTHsOWBQS4kQAJkAAJkAAJkMApSeB0Nlr/nFthpaiZmKbpfCNZfytZF/3700U9htanlILrsN+Utm3bqVPq+JLPWg8T0JoCCwmQAAmQAAmQAAmQQMkgoAyERcaiVHS483MWOkDWBYYbERHRKFe1PjpcfjUuahqJRZNGY9bq/fCEumD5bZiSqI4pFYtwtwTTyoWImFKIDvdAb56IGGid+tc5JOaG4fIgIjrG+UZLTEw0IsI8ULChTA+iRUdEiBtOFtl0I1L0xESEONfaFl1ghiCmVClEiX4lPcOiYxEbGQ5HP7iRQJEQoBISIAESIAESIIESTEDHlAXLiZqqHuNw3YXVHS5T8JoJ6II0eE4CJEACJEACRU6ACkmABE42AUv/FrTfknTwwZFtywJcJtK3LcTnrzyGx194ByNmrkaO4YFhS5uI2nLUv6tn2XIhvS2/P/8fHpTc8eE6JQvttGsZKVagkwjaCOhwlDjXjlywXavWRfo7cvn1ARlLRtWNLCRAAiRAAiRAAiRAAiRQcggYJWcqfzETNpMACZAACZAACZAACZz2BGzbD29eHvK8PkCZMJXNpC+4kQAJkEAJI8DpkAAJkAAJFCsCRrGyhsaQAAmQAAmQAAmQAAmUGALFcyIK+h9uMfS/9Acw+SwM+CIBEiABEiABEiABEiCBE0mACegTSZe6SaB4EKAVJEACJEACJEACJEACJEACJEACJEACJZ8AZ0gCxZIAE9DF8rbQKBIgARIgARIgARIgARIggVOXAC0nARIgARIgARIgARIIEjBky//fEA0eDTKgP9AH6AP0gRLlA3xf57OdPkAfoA/QB+gD9AH6AH2APkAfoA/QB+gD/60PZGRk4EQX6idj+gB9gD5AH6AP0AfoA/QB+gB9gD5AH6APlHwf4D3mPaYP0AfoA/SBw33AqFOnDljIgD5AH6AP0AfoA/QB+kCJ8gHGd4xx6QP0AfoAfYA+QB+gD9AH6AP0gWLhA8bUqVPBQgb0gRPlA9RL36IP0AfoA/QB+gB9gD5AH6AP0AfoA/QB+kDJ9wHeY95j+sDRfMDw+/1gIQP6AH2APkAfoA/QB+gD9AH6AH2gRPgA1zdc39EH6AP0AfoAfYA+QB8oVj5gmKYJFjKgD9AH6AP0gaL2AeqjT9EH6AP0AfoAfYA+QB+gD9AH6AP0AfoAfaDk+8Bf3WMD3EiABEiABEiABEiABEiABEiABEiABE51ArSfeAcrwAAAEABJREFUBEiABEiABIolASagi+VtoVEkQAIkQAIkQAKnLgFaTgIkQAIkQAIkQAIkQAIkQAIkECTABHSQBI8ljwBnRAIkQAIkQAIkQAIkQAIkQAIkQAIkUPIJcIYkQALFmgAT0MX69tA4EiABEiABEiABEiABEjh1CNBSEiABEiABEiABEiABEjicABPQhxPhNQmQAAmc+gQ4AxIgARIgARIgARIgARIgARIgARIggZJP4JSYIRPQp8RtopEkQAIkQAIkQAIkQAIkQAIkQALFlwAtIwESIAESIAESOBoBJqCPRob1JEACJEACJEACpx4BWkwCJEACJEACJEACJEACJEACJFCsCDABXaxuR8kxhjMhARIgARIgARIgARIgARIgARIgARIo+QQ4QxIgARL4KwJMQP8VIbaTAAmQAAmQAAmQAAmQQPEnQAtJgARIgARIgARIgARIoFgSYAK6WN4WGkUCJHDqEqDlJEACJEACJEACJEACJEACJEACJEACJZ8AZ3i8BJiAPl5SlCMBEiABEiABEiABEiABEiABEih+BGgRCZAACZAACZBAsSbABHSxvj00jgRIgARIgAROHQK0lARIgARIgARIgARIgARIgARIgAQOJ8AE9OFETv1rzoAESIAESIAESIAESIAESIAESIAESKDkE+AMSYAESOCUIMAE9Clxm2gkCZAACZAACZAACZBA8SVAy0iABEiABEiABEiABEiABI5GgAnoo5FhPQmQwKlHgBaTAAmQAAmUeAK2bcNvSfFb8Pnyi5z7/TYsqZfm/5SBHl/b4RebfLrk26ivdb22/z81UAbXdvj9fuHnh9frc4rPG7i2LAvFwUYx8/R+2XDug74XgXJ64+DsSYAESKCkEtBxw4kuJZUdOLFTigAT0KfU7aKxJEACJEACJEACJHD6ESiYgFNKwTSkmAZcrvwi56apYEi9NENvTh99chJKcOGoh9LjaztMscmlS76N+lrXK6W0mCQXdYLROT0pO80jOJC2wzRN4WfC7XY5xeU2nWvDMKCUckR1Hz0354K7k0tAboFSyrkXSunjyR3+74xGWRIgARIggX9OQN7i5b0eJ7SAGwkUAwJGMbCBJpAACZAACZAACfw7AuxNAiWSgE5+6iSoUoEEXJ7XjyXL9mDQsGV4u+9sPPvSFDz9/BT8781p+HzAQvw6bTMSEjMdFkpJHzmzLFv2J+6l9ctQzsJRj7J7bzomTN6AT76Yh5de/w1PPT8Zz708FX3en4NhP67EqjXxzje1g310f93vRBXNTxellDNEako6pv4yF2+9/B3uuflN3ND5GVx38dO49bpX8Pxjn2P4D1Owc9teR1Yp5czLsiznmrsTT8Dn8yEjPQP7k5KRsC8RcXsTEL8vAYkJ+5GWmo7cnNwTbwRHIAESIAESOCkE/H5gtzxy12wAVq8/MWXLdiAn56RMh4OQwDEJMAF9TDx/t5HyJEACJEACJEACJEACRUFAJ021HqUUkvZn4/vBS3HnfaPx9AtT0P+7xfh5wjrMmLUVM3/fism/bsLg4cvx9nuzcWfPMXj1remS6I2DJRlsw1CwTlASWv8UiNbvlcT4wkW7nETz3Q+MRd8Pf5dk8ypM/W0zZv2+DdNnbsGYcWvxhSTJH3tmArTMj6NXIzU1x/nWtrZPTNXTLdKi9Sqlk8gKWzbuwJsvfovuV7+F4UMWAe5qaNvpOnS9+zF06/kkLrz6FpSpeibmzYvD/Xd8jAfv6Ic/Zi2Fvg/6W9H6J0SK1DgqO0BAM87OynGSzXt27UNSQjLS5IOCjIxMZGVmITMjCxlpGUhOSsG+PfHQMjoZzXtyACFPSOA/IsBhSeCfE9DP/U1bAZ0gls8ccaKKTnCvlgQ3k9D//F6xZ9EQMIpGDbWQAAmQAAmQAAmQAAmQQNEQ0Ak5pXTiFBg9bg3ue/hnSd7+ie27UqF/99mUpHJEuBtRUSGIlhIZ4UGIx3QSzWnpOZjy2yY8/MQvzrek98VlHEjyFo11AS06uavt2LotGS+/OR1P9pqMOX9sR1ZWniRtgZAQFyIjPYgR+7SdYWFuxw6v18LGzUno9/EfeFBs/HX6Zqdepiv97IDyIthr+3RyXP/Wc783f8DD93yJbF8pPPnaK3j4xUfR+eqLcFbbxqjbqCrqNKiMZi3q4fzO7XHrA7fhpX690fDM9nj3rQl49K6+2L51L0zTABOeRXBjDlPh9XqREJckyed4ZGdlw/YHfEApJX5hHChKKQS3vNw87E9MlmR0nJOcDtbzSAIkQAIkUPwJ6MSztjI1DUjcD3m+Ai4TcLv+urhMG6Zhi7x9XPJap0v0ZmUBcQl6VEisETie6L2O5SyZrGVZEp8dLLpelxM9PvUXPwJG8TOJFpEACZyKBGgzCZAACZAACRQFAVmrQCmFjIw8vPvB706Ji89ARIQbbkmC6qSqHseybFnQHCyBfnASdqGhbogSjB23Fs+8OBnLVuyV+qL7JrReOGk7dML5qRcmY9qMzfC4TUk6m844Yr4s8AK26W9Ja1t1H8im+7ndhjOfLdv24/XeM9D/20VOYl0pJRL//qXH0+PE7U3Cg3e8h41bfHjilWfQ7e5rERYZhuxMG7m5NnJypGTnFznPzS+WJEE7dj4bL7zzEsrXOAtPPvA5Zk1bAiah//29KaghKzMbcXsSJImcKe6qnAJVUKLwc6UUlFLw5unkdSJS9qc6/la4NGtJgARIgASKIwGvD9BF3s7lPTyQGNaxzNGK/hDY5VIIC1dwewIxjX7eH00+WK/nrs9z8vRZ0ZXCNOlxgvGOUgqGLoYhsdHBolTgGQbZgrJyytdpQMA4DebIKZIACZAACZAACZAACZwiBGRdgswsL15/ZwZGjV2N8DAXPB4TOpEb+G7oX08kuKCJjQ3F5i3JeOm1aVi4eLcsgAILtr/WcHQJvyRnlVKYMm0TXn17BuLjMxETHQr9LR+98Dp6z4MtWk7rCZNEuU7qfvP9YudnO/RCUksd7zy17OFF6zYMhfh9+/HQne+jfNVmeKjXXYgpXQrJSX5Z4QJK2pUKLACVKvyYlmpJUtzG9bddhGt63I43XxqOaZMXMgmNotky0jOdbz7r33w25IOVf6JVKeUkopMlAZ2UmPxPVLAPCZAACZDAKUDAsiyERxrYsnE7vvv0Byz8fTE8Ifr5LY91+UC+OExBx14SUjjPJZ/Pj8XL1uCT/kPw2HO9cfsDL+DuR17GS29+hBFjJmP33njHZKWUxHeWc85dySfABHTJv8ew5c3KJx+teZ3ilwVSSZu0Db/Ph8D8fPIGdpRlm20hyMF/nO9xlv+gXt/xdvov8MpqMzA3f9Hd3wO8/BD1J3FW9oH7qRfnJ3FgDkUC/5AAu5EACRQFAf2s8cuzNs/rx8efz8PM2dsQFelxnkHWP1xc+XwWwiSBnZySjd7vzcaOXanQyVk91j+xWS+uTFNh5ao49PvwD+Tl+RHicUmi1von6qDnJWsvRER4MHb8WuhEtK6zhMM/sVHbZ/n9yMzMQa/HvkDTVh1x452XIy3FQl6uDZfLPG47dWJcCyfG+dC8dV3c/fgD6PvWKGzasMtJQv8T+7Q+FkD/Q4L6JzQA21moy+FfYdE+nZ6aDv1NaK1I+4E+spAACZAACZz6BHQ+JyzcwLJ5q/Horffhu68G4IWHn8T4Eb/ALUlol/vff7j+bynp545SylEzZ95iPPDUa3j6pb5Y8OdyeCVXUzo2GpER4YhL2I9hoybgtvt74b1PvsMeSUSbhuHEQ05n7ko0gZKTgC7Rt+nfTU7JH7TL7YLbKSaMwPvCv1NarHormK7g/FwwjzZBZSDIwTRwXJthHtTrOt5Ox6W5iIXkzT4wN7Po7u8BXqYsjorY3mOqUwfup17kgxsJkAAJkMBpQcCWLJwpz9qx49dh3IR1zu8ny2foTgL63wDw+22EhLiwd1+6k4TWP+2hE3//RKdSColJWejdbzYyMr1wuQz54PufJZ+D4+tEri5hYW58P3SZ848Wag7/xEZbEvWmJJk/7D0YUWVq4fpbL3GSz0qe6cbR4qOgIYUclVJwSYylE9iNz6iF6269DS899SXS0zLlvsgdswvpxKpjEvDLBwSJsgC3tHMfU/LvNRqGgdSUVOcfLVSqxAX7fw8GpU8PApwlCZRwAjqpa8lzPSzCwNxpC/Fwz3sQJYnc1959Exd2vgQf9X4X3374rTyT0xESqiSJ++/ikX+KU9up45i8PC8GfP8jnn35Pbhdbjzz6J1457Wn8Hm/l/FB71746J3n8XGfF/D68w+j+w1dMGXaH3i0V28sXbkOhsQoWs8/tYH9Tg0CxqlhJq38JwRsf+ANKG72ONzX5XFce+kTuLrHt1i02+eo029mzsmpurMtODP0bcdnj7yKqy99Fld2fhp9R2+CX89J2vW6yM4P8DPXzEeva+5H50tfw9AFGVoCfv1O6ZwdutP9AC+m9n4TV176DK679Ak8/O5CqdFygVZ99l+X4Jt0TtxSvHb1Y7jssn6Ysj3dMSvY5lz8jV2wX+rSieh52cPocvsPWJ8Q8Jlg299Q9zdEbXloavFEDHn6DXTu/Bz6DFurK2D7LElLOKfckQAJkAAJFCMCRWWKjkkMSZrti8vA4OHLEUjASgr2KM/p4LhKKUfWNI6dcNP6w0PdWLp8LyZMXg+lFPz5cRKOc9M/AaJFh45Yjs1b9iM0xHSSsLruaEUvqPRc9IfYx7JRP1+1rCXJcv0taP0PGSqlRP/RNB9Zr+djmAaWL16Pub9vxbXdb0BWln1MHTpG0glRv8/v8Dgabj2H9FQLbc9riqhSNTDwq/HOYlH3PdKSf1kj8Zs/P3bT9ul7V5jGY7UVJn94XeH9bYlFLOifUzlcvqiuU1PSkJfrdXzwr3XacO6NJCD+WhbOvdY/x2Hl8zuePpQhARIgARIofgR0XCDv6oiIVPhj2gI89+zjqFO7Lt76uC8uvaYj7n/2UVx5/XX44buv0b/fF0hPzYQnxID1N2Obopq5jmE+GzAUQ0b+gju6X4s+rz6Jyy85D1UrVzgwhJ5TWGgImjdtgLtvvV6S0c+jTOlYvPT6h1i8bLXzXOTz6wCuEnnCBHSJvK16UhKw2rIYs1Px25Dx+HbiNIydMhvjhg7HiNnbnGSeEVxlSKCv/xGTvDwf/IcHuNLmk0+ynDbJu9qWH/qTLZ8skHSU69c/6yHtXq8fVn5fW2S8Uq/7eGVBExzGsUq35cuLOl0VKIeNoytt/8Gx7GC7Hkc3SrHFBiVK0ufOxbdfjMY4md8vUyfi/U9mYW+2NMjCTU/UzjfAm7gHs36eiqlT5mLNXq9okLd0EXNODtnZkCUfkLQIA96dhF+mTMcYKQP7jsK8FOkjrU7iO9hHbNP8vF5LDydv+n7o68Lmf7CLBecnM4SFIye8/DKfYHvgaMNhL3PWUziEq9Tl49ai8KbvxqRxP2Hy5BlYtz/XqbOE/XtlM8AAABAASURBVCH3yudz7p1X983vfFCnF/pe5Vc7/bN3r8OIyT9i4tBFiM+UGYsRXlkwaZ2FFtErIk5fxzdkPK/4lJbVR588DO1A6yF7S+6zV+afl5cn9vmlaxbWzJiLqVNnYuHapIBsvmJbWPsdvV6R9UL3O0KvyPq8XmnXumxYIp8nnH0FJxfQyj0JkAAJkEAxIaAXLtoU/c3nffvSESLJXetY79vyjNffPoY8Q9JSs5Ga5YdUaRVHLxIWGSI0YcoGJCRmQidrjzfRqG3RffU/Gjh99lbo36Qu7Jl2cHAFQ4KUnMxcJO/PQuL+bKTniI2GGIHCNz1GaKgLmyS5PfnXTYULHaNWJ4lteQaOGDQNZ1/QCWXKeZCXY8EwCg/3bXmmGrJYjSltonQ5EzFRhmOzqCh8FGGXl2vhsuuvxrzf12Pv7kQn+a/tPryDTu76JQ6xj2iwoeuPdWuhDJhGwGYlR+MozI7VdviwhV0X1t+2FTQvQx39PuFfbDo+zMzI/mtfDY6hXIgpVwqlIt2wJM6SaCzYUuhRKYXAGFmFtrOSBEiABEig+BPQz1VDHoOhYQbGjZiEXo8/gYa1G+O1Pn3QoFlNJCV4ERkVhYd6PYZuPW7DqF+G4/033kV2Ri6cJPQxH7JFO39LPvBUSmHYTxMwduJ03H93N/S860ZERUVA/w60Jbbo4764ROi1u21LHOD3S+7IQqMGdZxEdY1qlfHOB19j24490M9mS3QWrZXUVlwIiFsXF1NoR1ESsCWZabgkCN25HpN/Ww+XuwZa1KuFGDMZk0Ytw34dwZpOmhXyVw63xw2Pp5Cfr1AGXME2icWVYYqcGy7pq6Nn0+1y+rrdJgxDwZI3GC3jlnqtz+0ytRiCm9Mm+rS8qAtWQ9tQcBzIpkzzwFhKGQE79DjQm7xxKa07G9N+WYDtVjSq1qiNphFlkTR3NiavzwSgRzi49FFuD2JiY2G6oxDuUTjaZkuiFNJ305hpmJNmoGbZGqhXthL8yasw+md5U4Rswlf2gZfYpvm53QaUzN8wTYfJwfnbTmI6ICwJbJFRhiH3JMDOkRNepjC1pQ0HNnVgzkrZgkj0ilxA3oTgPqA3tPxZePXbD9H/y6dxWc0oR4O+Nx5h7RK9llIwXS6Hp1szlM6WZUEZQZ1uuF1ap43gFnNWF3z15Sf4ZsjtaFDWBSgFT6jH0eERvUcU0SsikBnClhM9nlt8Ssvpo8s0oOSBc3AEkZRrwwzaEILQUFOGMRAqD1SXGYPIcLfoC7z0w0oJ64Bet2OH9rMj9MrYLrdu17oUDJfLkXXJnMGNBEiABEig2BLQP40x5bdN8ixw4cgPZQ8z2+9FYmIWvOFR6HBRA1zQPEoWNvJcU4fJFbjUzxGd4N2wMQmLlgSe54c+dwsIH3aqE9XyeMGs37dj79506Gep1neYWP6lkuedD9mWG3XOrImrrm2KG69pgLZ1w+GXJHS+UKEHrdOU57ZOkvudeKRQsSMq5XHq1O3YugcrVuxBx0vaIyPNgiHPXqfhsJ0t126PgdzdGzFz7FSMGforZs7bIjYreOTRG9QnYgdeSink5SrUqlceEbFV8eukeVBKwZZ44oBQ/okyDJgSV6j86wMHkdf1x3ok5+zdhCUrdsAnnXavXooVW1LkTL+01YHYQV+lbF+LZev36dO/VQJagP3bVmHJ6r0H/u82Xa9UHjYv+ROrdhzP/012/MPq+6qlMzOzoD8kV+oIMrr50CIMbW8SZgz8BRMWxCGsYimESzxY2L05tCOQnpaBQ6PPwyV4TQIkQAIkUBwJWJIPcLlsiTMUfvh0MN544yV06tQZ/QZ8gup1Ksuz3Q+XrHX1c0UphXse64l7bnsQE3/7Ge++/BZycrIDP8fxN2KIf8rBkue/Ic+qLdt2YtioiTi33Vnodt1lzvNHt5mmAUMe+OMmzcCNdz6JgUPHOkMppZz4Qcc5sTFReOqRO5GSmo7hoyboh7z0MRw57koeAd7ZkndPZUY2ZAkGQ/br58zFzC2pKF2nJR55uj3K+IFNk3/F9I158sctf/gi7d+xCL3v64377vseszZnSI00Bd+wMjbiq8ffxb13f4Vp69Oxb944keuD76fvBaxEjPt4AJ5/8D288tYo/LndK28WCgnL5+HDFz/BM498jH5f/YG9zpcwbEfvzj+m4Pn7XsVzr02CiENWLU591o41+OiR19Cz50DM3hH4dvKO6b/gcbHriwk7kJe0Dl898y6efWksNmh9tszQBKz4DZg6eSmS7FhcfncPXNHYA2/eRvw0cpWjVzn7/J1E7H75tE2/0cn7en7l4QcLfqX/LOLw05gViJfF47m3XY7ul9eA15+AP0b/Cr3UUcqSZLvldPZtXojX7n0LL/ebj1x5g90w5Rf0efpDPP3oZ/jom7nYJ4s1bYd+SNjSQ4lM6voVGPzBQLzwaD889sDHeLP3j5i8cB90G+QtW8Rg527Ht4++ifv/NxHxPoX9K37HRy9+imcf/gh9PpyC1fE+GEprFml/FhK2xmPLtgSkZPulAtgyYwKeuu91fDp1Hwy5VxM++xbPPfwh/vfmGCzcmgXDMJCwbB4+fvkjPCn1H3w5AzsylfMw0AqsrFTs3RaPTVuS4BVZJG/FF0++gZ73vI8n7n0Pj97TV8p7eLznB3j8vrfw1Gu/YGuy7ik6cvZj9vBR6P3Mx3jyvr54ode3GDJ+HbLEXiUiB1jI9fY5v+L9lz7Bs498gD6fzkFcLhDiAXx+nzDWxISIoNZztZJ24Jevh+C1Jz/Akz3fx8sv/YARE9ciQ/Qo4aalrbR4DHr+ddz77CTsyczAkhFD8PgD72H4vEQZWV7iB7Lnq6QQ4DxIgAROeQL6maAnsWLVPsTFZ8jzST8pdM2RRd7uYcuHwJ6YCrjvxUvx0buX4f1XO+HRLuWRk2MdfC7iyE2//RuGgtfrx+Jle5wkt34W6vojpQ/WaPtMw0B2jg8rVuxznk1KHcVGqTckRsiyQtHhpvZ4t8+lePXZ8/DCc53wwdsX4cazIpDttcXOg/oLnmlbDGU4/1jihs375ZkMZ7yCMoWf2071wrmrUbladZQqGwafz5b+R9qp5yOfzWL/sl/w+oMP4X8v9saX7/XGK/c/iHe/mAr9P1K5DQvaFkdpgZ1eUMq6Fw2aNsKaFdtlDD8M0xRZ25Gy8wOspFVz8OPw6YgPVB9oR+p2TBr8I5bszZanttPl4E4WsvoiZclE9B88C5lysX3edPy+KkHONAfncGBOW2f+iG/HLHX02Pl9AxKBff7QMnbg+sDesdFG3Oo/MHPBVuRJg235oEklLZuEH35agAzprDnZTq0IHPGyj6g5VoVSEqWIjbk5uZDTY4k6bXpsl9wk77oZeOjJTzF+0Qb89v0MLEn0wu0SXY5U4TullCS5ffJhgZ5Z4TKsJQESIAESKH4E9Hu/263kOWHg/dc+wMf9++Cay7vipXf/h/CIMIlzbJjyzNWWKyXv9fKchwLuffIePP/0q5gycyJeffx/SElKQViEcZzxg9b294u2Ndhr0m+/Izc3T9bctwSq5BEZiK/kRGp0Dsbr9cEnuRi5PPAyTW2jhTq1quHOHtdKEn0Otu/Y47QX1O9UcFciCOhM27+eCBUUMwLyd65MCU7zUjFrzGzsUh7UuvB8XH/peWhT2URe1nqMHL0W0N+iAGDERGDzpJ8wYMBH+HTkuvxvgkgSU/Qk//ErXvnoS3w9fCUsdyjSFv4qcoPwzec/4O7Oz+DWxz7DO1/8gNdfeh93d38bn330Ga7p8BSefPs7vPfpQPS6/wXc/PgE7MuRgeS1b9l8fDZgAD7ViVmfVEgiWfbIiduC4Z9+g/79f8GSfV5dhX1/zsFHA4bg209+xMsPv4r73/sEfd+bhE1Z0ixvuKYcts1fgJmr0+GJrImud7ZHu/PqwQU/Voz5FQuyAf1N3MIWUNK10Jde1JqyOM1ZPB+/zt8GqLI485quuKVDDVREDlbNX4hpy3OgZCHq9wkjAL6dq/H114Pw+Scj8PKTr+GKLq/ihX7fo98nX+Ppe17GTXcNx15hqRTk+WBj4Tfvo3PHp9DzyU/R+5Oh+PjL7/HyC++je+cHcNsLvyHFFkEAVs5e/PLJD/jqw3H4VPR26fg8nnr7W/T97Du88MQbuPH6D/DHPlEsspmJ69H/jb54551BmLtbAwJ2//k7PhkwCB99OAwvX/UIrnv4c7z72UC88XJvdL/tYwx451Nc1+k5PP7mt/hA6p9+4A3cdv8I7LBEobzS1s/FW+/0Qe9XfsGODKlI34uxYk//b0bg46+H4xM5firl4/5D5T59h4E/r0OWB7ATV+KFKx/FdT0+wKvvDRYbfkSfPl+i5w1P4IaeY6FNVkJCyQck8z97C1de9Saeeetr9P10EF58pBeuvPZjzNtlwYR+INmwZWjDY2Lv72PR9dwH0P3ej/HaB4PxoYz71lsf467rnsIV3QZiRYotWgFfWhImfvkdvu47EQPf7YObu/XFR18OwNg/E0WTftl6x0ICJEACJFBMCASf0ytWxTkW6eelc1LITss6TwbThTC3hX3x2cjI8yM9Q57JCn+5WZJ8DA11Y9PmJKSm5SAw1rGfC3pMLRcXl4Gdu1Odb2hrPYUNppSSD5DzECKLqZ6310WlbStxR9eBuPapedgSUR43d62NMnoRJnJHM9eUGC4v14fVqwM89PiFjVVY3eJFm9HkzObIkRjIkFilMBk9aZcMvuG3kViY2QQvDp6EybPH4bELPZjywzhsivPC45F5FDKw1pmdCdRtUA/xCTlIjE/W6g4MI72c53ZkSAbmTh2P2WslCSp4g2vOvWtmYezsrQgJCYWYgJzMdKSkpEEn5ZFvrxEWhdKlwp14tOWNd+OWi2qKfluapYflRXpKMrIlhgyPLoVSUSHSBijd15+LtJRUpGXmODaItAQl0qZPfDlOW0a2V2SlQmKtup26454bz0IYIHVukfUi21UDN913C9rViIJSCobEyvobyxZ0czZSklORmauvRIfU/Z2X/l+Q9U+SAcZxdLNhyE1aPm4xcmudh67NFH78eDzW55pwH0d3vXDPzRH2xzESRUjgnxBgHxIggaIloN+3DclDpKdlovcLb2HEmMG47eb78MQrz8KSZ5bPK88FaS84qpaXzzblA0cLN9xxDfp98AlWLl6G+2+8F9u37IY8alHYB7QFdfzTc3m0y3PZQHJqGpauWIt2rc5AubJlHHVKKeeoYwZ90vWazvhlxBe497YbnGerUso56jZddLjRpmUzefbHYPbcRbqKpYQSOI4QpoTOvARPy3kzkNA7a+tCjJ2yVyLm8uh0cTNEV6+PC8+vA0PlYPGoyVidp6QNUDGN0POZzihlGPj9x5nYmGFBycLOVj7M+WU50lzS/5ZL0L62G3n+cJRxl8Pqn37BpM3hePytp/DmPZ1RL8KNLXN/w1OPj0JOqyvxbp8n8cilzVCeb8GyAAAQAElEQVQKufj9m9H4dYvOYALu8AhUcJdHhXKRkA/3ENyUOwRlSpeTRV0sIvIbXBExqO2OxfaZEzDg5zS0bHs1unVvg8pu3csArDT8/tMsrJU35Nodz0b7qqVwVsc2aOrORcLm5Rg7MU7n2GH5NRHd56+KLalrE0qWPLMnLcSy/V5Uql8f7c+KQd0OzdC0XFnk6G9cT1wmEkoS3Zaj0HaFoVrZyvDtWoIvP1+GFnfchY8/eAL3XdIcscjAwpE/4sc/dFJYIXvxT3jy0VFYIIu7FpfehG+GfogpY1/HW3e2QnTqbgzq8wneHbYderP8BmKq1EB09npJZv8Oz3lXo9/Hz+LFbuegVoiNNb/Pwoif18idBkz5kKFsdCW43WUR6RE2osAdEYWawjpu0jh8uyoGT732BN554BLUDQ1D3O+T8OjzI5HS/BL0ee8JPN65JcqrdMwdOxGT5mVLb3l5IoR1JYRVKw23TrZXbIK3xn2D8eM+wC8/f4yZk9/Bsxc1QDlPKKJKnYe3PrwNTSKAhZLs7f3bfGSWbopXBvTFvLn9MfCFy1HVTsDUb4Zg2MwsQAG566fglV6TsDLFQLNzr0XfT17FV2/fhDJrV2Lm2jiZl0fun61F4U9Yhd49P8LodftRvkUn9P2qD6ZO/ABfvngl6nvSMWvEN3il7wxoy2W9hphKVRHq2oRP3p6JrFqtccmFV+HchtEIbDJ44IR7EiABEjiVCZQ427ftTHa+lQznnR9H3QzTQF5qPL59awpe+HYd9ucZCJXYQS9gjtopv0Ev8FwuA3v2piEzIy+/9tgHO785OSUbcQmZ0P0hT6n86iMOFgyEIQ9rF+7F6AkbsXZXKjZsSkFyugVXqAuuI3ocWqGUQk6uHzt2peQ32PnHvz5s3bQXlWvUgF6QHg2jfgrmeoGzHh6IadPexxnhe7Fw7nxs2h+BFpd3RLXSLuRJu1Ja8sgx8/JsVKxaDsnJOUjZn3qogF4ci7kh9c7DefUisfr3ZfBJElc59VlYuXA9Kp/XGU1K52LF2K/xzpsf4sN338cbb3yGGauTHF22zwevLLRD5WruoM/wtTCETCZz5xL0f/1NvPpaX3z01U+YtzkZfgTuRMam+Rjw7nt4r9+n6PNKb3w6bC5SZA5QFhJXzcSHb/RBn3c/xtuvvochM7ZCQkdsmPwtPvp+gRM75O1biu/79sXnwyZjZP+P8MZHo7EpVSvwYtkY+ZD8kxEYO3wIvnr/Q7zxykeYuiZJPkaHcLbEguN7+eWm+CUTfxSsBZRYYl8oXCmrMOSXPTjr4rYI2bseq/d6ERXhwvH6uf63RrRS7fP6yEICJEACJFB8CVjyjIiIBEYPHonfpkzCg/c9jHsffQg65vFLPkPp52gh5iuln9UKGel+nN/5bLz2yTtIzdqPz3t/DL9PQRlGIb2KoCr/YRQfvx/bduzB2W3OlOeT7ZTDtSckJSNFEtX6GajblFL64BSlFJTECdWqVESt6lWgk9lOA1TgwH2JInCCvLFEMTpFJ6OwasxULMzIQWTtNrj0rGgJkiPR6sJWqGNnYc/apRg7LRX6j92SGTa+5GKcG20ibuks/DQnQWoUVMZqTJq5Fdl2FbS7sDUkt4hsSYpa3kxklG6Kd0e9i9deuBUvDngQXduUQa4krKu164Kvxz2Pp5+9RRKKd6JVlGi3krBjZ67oBPQncDog9vkkuHZq8nfyBqbrvV4/LDtQp9+EbV829vtjcG2vl/Db/E8x7Nu7cWZMQMAvCd+fxm8TpaVwwXVnIUQ6VmvbHO0al5eFSzzmjZ6JZAWYhtgQUHnsvfQ3TBFJ2YJpE/9EEiLR6OILcFa4BavB2bioVXmEIh7TpW1DKqDcBmxAxrdge/OQ5o/ADW++hBFfP4iHHr8NX31xDzpWMZDjy8GWDYlaEL8Nn43VWRmo3Pxi9BvxDO66uQMuubqLLKD/h4euqAOPtQ/Tx8xCnEi7ld/531SyfHloffejkvR9Go890g1vDnsc1zcrLfcuE3u2xcEvsqZtOf+7ZUF+mrXXmwWrdE38b2Q/vP2/Hnju8+dxb7sI5KhclDrjSgwd/xKeeeoOfPBpV5xRzgNvZg727U4SjfKSRZLP64NX7pUl5/DEoNVl5+GKK9vjsqva48ywJMxZthWJeR5c9sT9uOO88s69K9/hSnzw5ssYPv5NvHBPR7Q6+0zc+tbtuLR5Kfj92dixVT4UEfVrJszB0owMRFZrjd6D/oenH74Kdz//KH4cehealbachZ0SOf1K+P03/LwmE0ZME/zvm5fx1H0X4aLLOuC+N1/BR4+3RKxKw/TR87AmGTBCDehvGeX5MhF1RhcMn/YppvzWFw9fUhmO58hDDtxIgARIgASKHYH9+3PkOeK8U/+1bfJe7vaYCJFnsZzieDcJN2CaCqmpucjN8znddJ1zcrRdvklZWV5Z4OVKf3n+59cd3kU/e1WIGzlbt+PVZ8bi7Z8ToCLK4JZ7W6JteT/mTN6KOMOEKR2PogJ6reiXD351glfE8Jf2aaH8krw/DaVL6+ctJE5QOPqmRzcQGp6FhYP64amHHsSwxUCny89D5VJK4ihb+h/ZWyklz3I/YmKB3FwfcrJzHaGDNuoxbamLwDntamD/pkXYkKkgnxnAt389/txsoN3ZDWBtHI9PRm/H2fc8gpdeeQJX1EzFz6OmQ0cgHkkaS0gGvfklvsr1uwE7FRMHDcfWMmfjkReexs0dSmH3tr1I90fBkF4/DhiKreUuwBMv9sIz95+HxFljMXWL2JG1FcO+mwCr+dV45n9P48FramLuoIH4MyUPLlnwev0KLuRi6sAhWOdpgfueeARPPNwDddLnY+DQ+fDCg0hfPJat3IHSba7Fc2/0wvWNMzFl3B9IlvDSMOxAbIFjbGKGbrX8lsMOGpGuOEqx/DbCYl1YNfJXLMiogNt7toZ/VxzSsgyYpnTK1ydnR33ppHNwoX9UITaQAAmQAAn8AwInpot+3/aEAMv+XIKyseXw6Mt3Qh650M8Ewwg8OLTM4UVbo5SS54OJlGQvLry8JTpfegUW/TkfktN2YgqcgC34KMrIzML+/SmoXq2S2BuwMzic/uBVn3/42fe4pscjGDd5pr6EXqs7J7JTSsmz0UJIiAfR0VHYtTdeaiG6wK0EEjBK4JxO7ynpFYASBLmbMXzkBqQjBOd1PQ+tq3nkzcfAmV3OQZsG5ZCbvQ8zx85AMhQkz4mIOk1xZZd60jEOYwfOQbacpcxdhrnrE1CmVhNcenElqQGULPYs5KBMs+Y4t26E82bht0NRMUbObRfqtDkDjSJ1gG3B8kSjYhk5h2wSpMv+b70Mw4DPzka0jH/7A+0Ra3udbwTpRYktmlaO/Q1/JOcgpHpb3HBpA5j6jbl8S1x9WXOUQwqWzFmI2et8MFyyUMRfb5awkHUEdi5ciKl/7kdoSBVcc3sHhChDFjdl0bVHC5RFGHYvmItfl0iKWLmlh+g1RH9eDszYmrjwIvnkz+9Fpiwy7IholK8QKQI+eZM15JiG9euT5Z6Eo1HrpqgXbcOflwevLOD8djm0l4VajLTu2b4XOzNE3KNEv/SV2Vza9SxEyRPEmyM87VKoWiECtvDQTxXNQqSPeCml4IONUlUa49w2cn/EJr8/AuWqRzpcG1/YBvWD9yoqGmWjwgDbD7+UI5TlVzgJaQvwxS3Fcw98jbmJXjS8oCvefqYNQvWNkfFqdbwYj794OxpnLEGfZ97FPV2fw3UXvInf1vtgik1++RBDq9u0OQHZ8KNGi8aoX9kQX8qWZL0fkeJDreqUFZE82IaSI7Bj1Q4kqFyUbdwcbWuFiqxfEuNiqyzSml94BqrYIUjbuQfbxB64TLlftiSwDZx/fw+cUysE3jwv/GJfQJujkjsSIAESIIFiRsDnlQeM2HS879WGPCOcZ7/0gTxfTFNBqvBXm4jKc1meE/ZfSQbbA4KWxFh6IRisPepRxA23C5HhbkRVqoSH37waz19RDrOHzcKX05MRGmrClmfSUftLg6hwbJTTv/XSz2mXjC2P47/op2B6gLzMCLS5+yV8M3QUHusEfPPGx1i0MxehYQp6kYvCNjHOdEnIoHlIbHK4iOar68q3aY/KuduxZEWCvsTeRfOQWLoRmshzXJVtj+fffgqd68SKHTFo3KIuIuSD40yRNAzZFXi5QkKAlO3YGBeK86+9GDXLlUaN5p3Q6azqEntkyVQj0Lnn03jujvYoJR9Cx9ZtiDoVQ5AnHxhkb1uPnaoWru9yJmJDw1C13aW4+Zp2iBC7tbe5XC7kpm/G8r2RuPC6y1CzbARiKtXFVdeei6zNK5GYY8P2eVC7dXu0b1BGmISgWpN6CM3OQI5XGynGCg99drRyeLOCOpqo6PcDnnDY25bi88FL0eSWHrj2jDBkSezkEudWEpOqo3c/RK/cnkOueUECJEACJFC8Cej3bY/HI0t8C+kp+ikFKHXwTd8t+YHQUIUQKfroCVHSjgObkjNb1seQk1wrV65O4Cv/4aa/NOj1+RAiduvRCsYORr7td992A0qXikF8vP5SniOldweKnre+cMk6Xq/b9TlLySRglMxpnb6zsiWgVrZC6sxfMW1NgizEaqB5nWjs2boXm9bvxY6saDRsVAMxKg1Lpy/Ewq1+KMOC5S6DTle3RyMjF+t/m40FqT4s+2MVNnvdOOPS83F2KduBKms7SezZiHAb0Asx0zRgSoslCylb3ukMlwGXbUDXG5LIlJwnCt8koLeDLYFzJW+UwZrA0Zb0pIHYMtEID7dgi363jKvFlH8XfhqxBuniwfXPrIWIzDhs37wD27fsQ6V61VAlMhLpu9Zh6tSV8CuX9JSFUkDp0feGaFZZWPjzb1jhc6F0rUZoEJGE7cJu+9Y98NZuiIaxEfB4t2LM6JXIDr5TikaZPkw3ZO6iQ5i4pMC2JFFqS6t+GbLLQbYshvzwIDQ8RBZNEHYmDJc+WoiIDJVrhbzcHHizRVwDccYQxaYPtjJgGlLkDvgR1Bs8ivxRXkoWV0r0mIb0NSz49ENJRvK43JKgljqxVSfebT0J0XFMjcLSLYxG/e8zDF6TjOhybfDS+z1QJ9SCX8YwxOaUJZNwS9PLcObFr6PPR9Mw9bd1WLF0H5Ky7cB9sGUQsd+b55Px3YiUTzvdblvmZsJUJiwBWSrE1EIHSrpws8SfomPCYIoT6mLo+YiYkk9KY0SzyspDuiSaITYE7nYYKlcIg04868W4aagD+nhSNASohQRIgASKkkB4hAv6ndp5TPyFYluesdmZ8r6f7ZOnImDJ4ictPU+Sgn/dW0IlhIWZcMnz5C+GyW9WztEtMY5bnk/yuHOuj7ZTSsGX60VYzTp4uc+l6FIjAx89OwJPfbAWe2Q9aCv7aF2deq3fEB3h4fKQc2qOfxceHoqszCxIyHDMTi4zExPfeAyvfjQZqlpFtO7UHBdf3Qb2vlVYuzcVtkvmrA0pRIsymNqotAAAEABJREFUFPRvTJumAf18xRGbJK91XUR9dGjswcolq5EHP5Ys2oVqzVuhimFDxZaHvXMevvzgU/Tr+z4++P535EhcIk265yFFjwe5z14zFFFKNElw6ffZCIuSuMnIE58JRdnIHMwaPEB0fYw+b3yNhdvSERpqIzMrHUZoJFw+Pyy58T4rGmdfeRmalA6Bz2dBKQUrIx1+twchLlvitjxYfvEpV5RcZyE71w/bcMMthum4VykbPolfLOMQE495ISSddqWUM54tMZBTUcjOthRCwl3YsXQNNmxJwNIRX6N1xbvw/FdrkWttxovXvoHR61yI9ogdViEKClQZcp8KXPKUBEiABEjgFCBg5z97DXnGHm5uUlwKdm2Pw54dCdi1Ix77dibIs8x/qJg8a3SFPHH04cQVFVDtcbsRER6G5JQ06E2p/Aa5UCpwXrtmVejksmmaUnvkK/i8ys7OQbSs7Y+UYE1xIfBv7TD+rQL2L04EbFmEyS2VBOH4kYuxK1ch0o5Dv/seRK3aV6Few6tQo/ZteG3sZpiIRvKmVfh15kb4lSG5UoU655+DcxuXQ+b+PRLEz8bC5RuQiSq4+IY2gcSqTDXwFgIJnZUTREsVoOQlBflb/vsMICcFqqE3JT0DR1OS42KvZcmCQMEw/fB5ARzeATKWDZE1IOrETom25STjjxmYtDwBbisGW8d9g7PrX4Wada9HzTpXosXdI7AxIwIxdjym/LIQW9JEgdYj5agveaMXCkD8Kvz00xZJEYcjc91UXNpY9NbW5WrUa/8O/pBPIsPgwspR07AgwQrMJqBeDIVjo0JgU0o5186VLXYjDOGRHrhULrIyspFtAJbyw/IpORpIT8uWhCwQEhoGT7j0Ejayl5cMUFCXPpfa438F+gfkD9pk4+C5nAaaj7m35cFhY9uob/DmwJXINMvjppcfwHVnRsm980tPBfjj8ekTX2LI6hTUveAK9J/wCRbs+hmbdryDq5q64RMOzv+uait4wtxwyZI0PScHeV4FS9p0MXx5SMrR/1u0Ep2BV7QsMg1lIjU5Ux6ywk1w6oWk5Zd+KalIhg1bkvrRksyW00Cn/L1pHNSTX8UDCZAACZBAMSRQsXwkgouQY5mnF2dmSBjqt6iKtvVjECJv/CGlYtG+bWXUKOOG/tD1aP3lESpJRhtlyoQjNNTtiOk65+Rou/zHSFRUCErFygebksw8ah9pUJYP3pgK6Pl4e3SqmYsZo1dhvTcaF3ZpiPPPLI0ISZ5KnvFoo0FCErjdJsqXi3RkRKVzPJ5d+UplkBAXD73G05wK62NLpWGEIdy3HZMHDcb4sSuwYOpCjB0+Axnh1VE9NhJKbLQLCQ60TpfLxP4EL8IjPPLhuQ5YgMNtDHyo7UKTs1vAu3EN1iz/HRvTY9CibT0o+W/9xK/x6ZitaHzRlbj30cfQ85oz4JbEsikJXhy2Oboi3fBYWUi13DI3A6YkyDOSs+BTkfBnbcJn732H7ZHNcO3t9+C5x7ujWeUQeH0eRETEwMpKQ54AMQxDPnRIx5xR47AiMRsuYaznY0TFwJWXi+wcJbo9MEwXkJuCHF8kwkNNQD4AtwEopQAoKKWkDse/ibgWNkxD9JvH7KskZvHn5iK63pl4+LV78eQjl6HnU1fg8nblJSFeBpfc0hGNylqQvDjMfL1a9+FFKRUYC9xKIAFOiQRI4DQi4Pf7ISEP1q1cg0fv6IlrruqIq688F9dIue2aGzHxp58h6QPotfHJxBJ8BMXERKFi+bJYv3GrxC/6aXmkFfo3rPXztjAbdb0hz+cUSWAnJkkOoVZVR4GOhZwT7koUAaNEzeZ0n4zlh5Jo1NqxFOOnb0YqwlG6WkW0P6sR2p/RBOdIOfeMxmh7RnWUj3EjXO3BLz//ibhswIQFlGuEay9rgkjsx7j+YzB2UTwi6pyNq9pHAoUsCPA3NiWLQy3uUh5J7gJpSXuxK07BcEmgb/gx76dpWJYdinCXJW+eOOrmtw0oZGPS6MXYkpmHsMhINDyjEfS89PzOObMJzm1RH02q6zRxCLbOno+ZKwP/q4eyJEl5FM36jc8WzRvGT8X0OAtuTxhqNqwrug5yO7dFI5xRtzQiDANZ+5Zg2Pjt0gMQtUfRGqw2YEBn16PQpFEpRNnZWLNgBVYnKBhuD9whJgx7N2bP3YZURKFKrSqoEQFAkrKGIcf//CXcnGS4kN88Fy/0Go21eSE4+6Y70eeRJggR+1xuN0zxPSSuwaK1GTCMSrjhqTtxw8X1UUnmkrBoBVZtSQUMjyyq3VAKaFC3PMLhxvZFq7Bqa570CZXFlYH9cxZhwUZ9z0QuH26NM2qhih2KpNXLMGd9usgacMsC2DAtLJq8DHskqV+qdlXULu8Sbl7YegBwIwESIIETRYB6i5JA8C27bp0yMORZop/JR9OvZS2fhbAy1dDrs2vxzQstUDHCQLXz2+DbL67FfR1KwcjzHfU5YEiCz+v1oUa1WMTE6CeYHkkeSvpwlCJdnJaykrSuXDFKPjS15LrwPo7+bB+qNKmCcxtr/VG47qGL8fkHl+OdVy5Gv6ebopLPB59M5GiPeL1ACw11oXat0jKOfhU+lm45vDRqUg3bNm6CSx6HTuL2cAG5VrKq81oGzr3/TdzRHhj60gO45/an8NP6MrjnuQfRpkY48nIlKgpOXPoEX/rehMi0dm7bgXJlwlC2fKlg0yFHQ+JGTSmmXiucVWYXvus/FmnVWqN1lcBcknbGATFlUa9aGWTvXIops9ZhvySKk9OUE1dpBna+Rp98UI2oWmhY2Y8Zw3/Bht17sH7hJMxcthsIiYCVnIyklDxUrVMDFcxkzJv5O1bvTEFSfCLCqjVELfdWDB8zH3Gpqdg0YzxG/bYeKsQNQ2Ibn9eLkMg6aFkjG7+OGoO1u1OQsG0VRo36AzENz0TZECUs/LKozjdGH4SflR+f6MvjLS7TkPviEl3BmeGITSmZf64XETXr45r7uuLeB67FQ71uwAXNY5DhC0OnWy5G8/IWcuQDeBE9on+wwjAMeGSO+loppQ8sJEACJEACpyAB/SwPDwdWL1uJTfvWoNdTb+OVF9/H229+iIioCEybMFXe7wEtdzKnp5SCjgkqVSyHenWqY/qs+bLGL/x5YxjKaQsJ8RxhoiXPYl25aetObNi8DWe3aaEvpRz9WSmNfJ2iBI4W+56i0zmdzbbht0xJdALrZi7Gin1piJDF2WMDP8FPU953yphf38foqf0w5tcP8d59DRGiPNg1awEW7MgG5E3Btj246OZz0TzSj/Ur1mLlbuD8Hh3R0C1c8wNtZZhwe9yS/DOgpDrwUjBkpePxuOAyDRzY5E3JJYnJQH2gtmrdSogMC0Hq7lV4/daX8cprA/D4tfej55sL4fNEIkT6SzdH+OBYZmAs2y/jyKgJGzBjzjpkuKLQuvtd+OXXfvhpUj+MlvmNmSLHqR9g9Oh70FqS7B7/RoyfsMHRZ4pul8cNbY+cOnWBnSRYlSFjJGDi2LXIcOehRucrMfA30TOlL0ZPFW6i9yc5jhv3HK5pEQ6/Jw9Lxs5GkijwiEmOXrcJwSg1+S+ZiOl2OeO5NEMZ4YKbL0TbMjGIW/Mrnrj+NXzcfyrGDBqF57u+is8nbYLXVQ1dul6IsqIiz3bBld/fPEQxhEO+XtMQSXmJ/Y4Ncg+CosowA/dK7FIiEnwZzr1yy70qUCu2Hj6WMgynv8clY7hCACRg8OtfYeJ2nyw6K6NV40jMG/UbRg6bhp+GTcHwn1YgPi8CZSIVLCsRY98dgM++nYJv3uqL62/9DkvTPYhALuaPHo2pK5JQ9+qL0S42HJl7F6NXD/GFN4fjw169cXWPgdiQGoIwmYuT1AZQrv0luL5VFKyMtXj99lfwat+xMuYk9H3keTz68WKk2qXQ+cZz0SgasHJtBFkE8YgKvkiABEiABIotgcDzqOWZleXZZBzTSsn9wZDnUnb8drz54FjcLeWuB8ZCF33++cz9sOT5oZOshSlSSkH/43mNGsiHoOEeeV7ZkKrCRA/UKaWfazbKlY2QpHAp5OZJPBJ82B6QCpzohZQR6kHqqg14/P4xYpcUOd7RcwzuuG80bn9pCXaKfW6Jq6xAl0P2MpRjU3i4G2c0q+i0GUcZy2k8bNf27IZYuWS5xA+QhWFhI0gHGcTyAe7KTXHPO19gwMhB+GzoIHw95FPcfkUjeETEgoHAXZGLAi+92NTftFq3ej0qV45ATKw8m2UuSh0mra/18GFV0OKsWshJyEP9s1shUnTZUlpdeQ2a2mvQ76338P3E9ah2SVe09WzG5GnrkemKQXSYyxnfJYOFe/yAisTlt9yMxt5V+Pr9zzBmsRftLmiJcp4suCs2x7WXNsKiQZ+g9wcjsMnTGNd0PgO7fxuKJdk1cNt9VyNi42/4+J2P8N20RFx0161oFuWGX7kR6lHwyYfhF91xB1qHrsegTz7Ch1+ORlK1C3FXt9ZwwQ/bE46wEBeCm3J5EBbqcewL1h3P0cyPvzRDpdTRu0ib7c1Deko60tIzsHdLGhp3uwtjJj6BM/wZSMuz5e4cvXtQf0iojt2OLscWEiABEjjlCJzGBgfe221cffN1uOmuLril5+UoFV3a+beO5LFx0skopSResRDicaND+1bYtnMP5sxbDKUC9QUN0rGRUkpyOAuxa08cTNN0+gbmJM9hnx/jJ89AmdKxaNuyWcGuPC9hBIwSNp/Tdzo6mnfJH3vGVgz7agzW5WSgTLPzcHOncihTtjQqViiNcuWklC+DsuXK4KI7L0NDKx2ZKdPR74sFkFgWtuWHceaFuOmCyjBUJvI89XHdFQ2gnUTWFg5bb0Yq9uftxd6ETPj0mE6tHxmJScjLi0dCSo5To3e2Lw9Je/ZJfSLSvYFAO7ZDRzxw29moYqZh8ZzReP3Vz/HFr5m45smbcUX9PCRmxiM7//9LzctIwx4Za098mmMfbANu5cey0RMwcslqeH2lcekdl6JCuVIoV6ksygfnV7YUqrTshOsvjkaOLx7jPxiK2WmAaeUhPmkf8nISkZZ9wHjYfkveKIHUGeMx4JfFyPTGoNPVnXBmldIoU64cypcXbhVEv+gt1+gsXH15E5h5W7DolzH4fmEO3O5c7Enbi4y9ycjywtlsvfd7kbwvQeafgJQspwYhza/Cu5/fjk61Y7Fpzjg81vNpXHfb63jnpzXwV2yAB998HI9fX1H3hml4sX9PvPQXe3N8Th0cNRYyEhKlPh6J+bxtXzYSUvdJXZLYbzmyAX77sGdfGvLNknobmUm67z4kpOQF1Ekt/HlyrxKkfyJSswJj+bMzsDdvH7L2pMEd4se+SUPw3A9zkerPRlzSenzyUi9Jlj+Jm7o/jRu6P4ybb+iL3xLroefTnVA70sLS2T/i4bt74f6XxsPX9nI8d3c7lLP24I9x/fHGV4th1eiIN/teg9YVXNiyeCJef7kPnu4zDlltbsQTN1RAdk+GQjIAABAASURBVN52JKT5HBtVqbp4+stncFurqsjeMA+vPfuKjNkLz346HXs95dHl3ofw2uMdoJdaPvkUNWVvnMwlAanZARZ6iiwkQAIkQALFk4CsSRzDatcqhSaNysv7t995LjuVheyUUvLYysbG1XFYKWX12nisXhPnnG/f74Ut7YV0c3R6vX6ULhWOtq2rQImQXvzI4S9flmS+dSK4XZuqiAh3wy+xQ6Gd5DmtTAVvRhY2rUvA6rW6xGPtOinrE7BhewbyxD49dqH9pdLnt9HijEoS10RAhoWIS+1fvQIaz2il47YcbFi1G+ERprPAK6yn1un3WrBDolC9YW00blYz8A8cS3An68Cjjul2m0hOtrB9wzq073iGo9qW565zcshOwTAgm4kmXe7FVyM+xS1nRUPwQFvqqXIm7n7pFfR+6wU8+XAPXHR2W9z9Zl88cmVdVOl4E1559lroZHXHns/i0esaih4bIZWb444XXsJbvV/BMw9chYu63o2nb2sDmCFoed09eOPd1/Dy/x7DrZ3botMtD+LN1x9E8zIKEXXOw6OvvIBXXn0Or77+BK5pK3GWGNLo6ofQ6/4OCBftZpnGuPHRXnj99efxyhsv4pl7L0P1SFO34MxbH8Nzt7R1EvNSgcqtbsD/et2AyjrokNlolrr+WCXoZ6HhoVBKyX0VA47VQWT0At0lH7Yo20Rs5Spo1qI6SnkcqMfq6bSFhIXAJQlv54I7EiABEiCBEkFAxwTJiclISvAhMd4Hn88HI/Cw/U/mp8fWz7cLOrRBndrV8NGXg5GYlOzY5Pf7D9iklMJFHc9G5UrlxWa/8xzUjZYkmbSOSb/OxqRff8ctXa9AdHSk84xUSmkRlhJG4PiimBI26ZI4HR3G6jDZznWhUZfr0KvXU3jr9S4oL+9SliwM9BuDU/LPQyQYf/WLR/Fcr0dwVeNwWPIHbpgmDBUhSdcw5NnZqH7hBbi4aazgkiSeMuQIVD7vcgm6n8OLD5yN0qFOleyicd5dt8iYj+C+q+sh+FbhKlUN3f/3lNTfhQvrR4icvFQl3PP5Gxg59Hm8978n0eft5zB62of4+L17cN/T94k9t+Kcqi4RBKp2uBiv9noO/3u8E6q6pcrQOz/cVZqiZ69eeOf9B9C1Xai8QVmwZJ7O/OTol4WbbUfiKs2g12N46dG2MDKAkJpN8OD/nkGv5+/CxY0DxjtfKpK5a5vT3dVx67M98cKbj+KeLvVErw2fo8t2zi1Zkdm2C2ffegvef/FJPCuLozp2LlCrFZ56/km8+PoNOLOy1qTgzCCqMro+/QB69eqJa9uWlgkAer3a7MZ7MG7m+/j+2+fQ93+P4LWXHsenn72EUb9+is+eOwcRsB1ZI7Qaur/2iPS/ExfWK+XUKUdxJM6793apfxR3Xlkf+s54yjbEvS89KXXdcXZlvZQCagi/V3o9jVeePh8Vnd5650K7HneI3NPoeWXNA4spRNXELS8/KPV34rIWAVujG50j10/j+f9djirhCqFVWuApYfra8/fjtecexKvPPSRF7O/1EN54Tsb+3w1oUj4CbR58HhNHv4qP33gMb77yKL4c+h5GD38Sr3z5FD75XI69nsOztzaFW8xpds8zGDfhDXzR+wn0fuVxfDnofUwafS8e6nUXnu/1IG69pIbjT7bPQtmWnfH9jM/x44jn8eGbovuFR9Gv3/MYOv4DjO7fFfUj5T6JTndsedz4/GNie09c2iTf76S+hL04HRIgARIoUQT0M9w0DVx3VSNYzoJEP0+PPkUlcUlYuAeRER7ohHBE/nmo++j9DHne58gHuq3Oquwkup1xZEwcx2YaBnSs0b5tddSvVwZ5eQcXUEd0l8e4Er1hYe6AbdrG/BIWauLoFkI2BbfbwA1XN5Hz43/J1KDnExMbhYs7N8OUcVPg9iiJX46uQwlD+G3kZFrIyrCQq7+NIIrkVWgnfY88IcDS+csRFpKN9uedCb8ENoZpFip/SGW+0gNzF0aAEhs9cJuAY6hhwu0yoZSCYSjoTSkDSulzPRenk9PH1I1SlGHIPvAyXG6EeFxyEYgHTLcbLmmW0FDqDHhCQiBoA2MpOHoN0S2ngToALo8HHsegA1UH5KQ58JI+huH0Clwfx16pgHx4eBhCxA7N8ji6BUSkqy8vD5kZuQW+/BFoKnQv8tExUYU2sZIESIAESOAUJiDPEtPlgiu/KHXw2XjErKTtiLoirlBKHjiiMyoyAk8+dAfycvPw5ntfIT5hP0yJDfSzThct1uuJe9D39adRs3pleeTbMAxDZAzM+mMRPuk/FFdc2hGdLzpXYhlLNPJVCIESUWWUiFlwEk5wrDGYZSRp+cIj6N37XnTvUAH6W0D6j1sp5cjoQF0pBbgjcdH99+Kd3g/g2fvawLd9M2ZN/R39n30dbw5ZjVxVGz3uvACVwwBLFify/qDVo3L7S/Ba78fxzN1tUDrEqZJdFNrfdpOM2RN3dqkLQ2r0yxVbFV17PSz1t+H8eqJIV8KGZcTi7BuvxVOv3Y1nn++GLm3LyxuNGx1uv1Xs6YY2lfTiAahy9gV4QRKTzz7YEVU8TmfZedDkiqvwVu+H8NwTF6CSTEUpA4bMSSkFpZS8kekjUL7ledL/IbzR5zacWxkwqzTAPa89ht5v34JODQP2OP3yJ1f13IvQq88TeOvFq9CsonJ0uczAUSkFw2VKHRBZrwUelARonz534Oq2MTJQEzzy9qN48+Vr0NxJQBtwQbbIyrj20XvQu/eduLJ1rFQApgHobwpFVKuPa+68EU+/di/+98ZdeOjBy3Bu09KwhTWgoDcVWhU3vHC/9O+B8+sG+itTt0TgnDu6S/396HFZPegqd9n6uP2Nh6TuRrTJT0BXF37P95ZE7CMdUF5302qVC61v6iFyj+DOy6o7SWDdhMga6PastrUHLmkeGCuqfls80fsRvN3rYlQINxHbvCNeFH/539v34H/v3ItX3rlPyr34X+978NI7D6D3a1fjjCpumYNCg4svwsMv3Y0XX70Td9/cBhU9FvyuSrjigdvwau/bcGW7yvDIwLYkGSq2bI+7JeHc69Xbcc8tZ6O8YaFUy0vwdu+e6NapukgBSlaMtm3BjqyAC268Go+9eBdefOtuPPnkdbj8vFrw6A9WhJszxahyuPapB2WOd+KiRhHQm1K6RZ+xkAAJkAAJFEcCth14nz73nJpo3bIqMjLyZIFlHNNU27Ylfsgv+edyKLSPfgz4JFkaLongG65tKklA/aS25clRqPgRlbq/hDCIigpBV+l/hEAhFYfYJ887nSA+mn26u/62a3p6Ljp1rI0zmlfUVXDGdc6OZ2c7Qtd1vwiJuzdg1bIdiI4xJEnsd+oL3ckAyjDgFBW4B4XJabtlLQmvF5gydjS633EJQkL1kxx/00YEtsOHOsbYgQ6QcQ7vhKNsCqpAyxGqj6gQ4ULqCqkSwX/3MiQQjIqN/NtKlFIwDHXIvHD4piB/DxYiIiIQyp/fOJxOEV1TDQmQAAmcWAKWrGuVUhKnQD44NaS4nHO32w1ZaEM/j4MWaFnDMJx2T4gpsm55NgMutwuW7cPJ2JRSYpONhvVq4bnH78amLdvxeK/e+GP+EiilDpSCtiilkJubi08HDMVr73yGM5s1wOMP3IrwsNBC5Qv25fmpTcA4tc2n9UcQkHckn6wO8vK8zrd31RECwQobfpHLyfXC6/Nhw8jPcX7nR3B/30nYmBeLq+UTrIeuqwFT3gBhmMFO8p7nh9bt9Vl6HZZfb8MvOnS9XtzlV8obpI2ALT7Imi+/WsEQG7W8V2z05vkcOw3DztfhkzdLOJvt98OR8foLjAVJiPsDNhxW73QqsNP9tU15Moas+wBJYGp9+vqgPQc72JbfGU+3O/IHmw45s4XJQT2y2Cug95B+Ms+D8xe5fC16kad1+Ly+A+Pp/yXYL8lnJQnvfDE52PAJI22P/1DF+az0Pc7/hFBsCMoGRfX8tZ1a98HRAevAvSpQW4it2kbdPy+f80E+Xod/gG3B88C9U6YNy7l3PpHziX/55Z4aMJ0xfE6dT+YqE4SSxVRQVvuC1xeQ1bZr/UE5R1bJ25XMM+A7AT1eYeiTm2nLg/eArzvjaLt8BfxOa2AhARIoEQQ4iRJJQN7GYckDLMRj4r67WqJc2XBZnPhhyHOiKCaslCx28vy4+YamaHXWwW/f/B3d2hZbnjGXXFgXV17eAFlZXpim8XdUHFVW68nO9qFOndK469azoMfRPI7aoZAGwzDk+WuhdJlo3PfYVRj1/fdI2Z8ryUjTYVtIl+Oq0rbY8CM8QuGHz39A+3Pr4IJLWjs6td3HpYRCBwhERkYgKv9/MT5QWQQntvz9eDxuxJaOLgJtVEECJEACJHAyCSilJDcANGrWGAmp8ejY8GJc3upiXNn2Ypxd6xJ88kE/NK/dRpLMLokRAJ8XaNbyTKzesBLt61yMK9pc7Mi2q3kxRowYglZnni3m246snJzQl1JKcjp+nNPuLLz72tNioxuv9fkcDz39JsaM/w3rN251fppjb1wCFi5eiU++GoJbevbCzxOmo0vn8/HW/x5HdBR/euOE3qRiotwoJnbQjKIiIH/8Lvl0TAegrkOSmYcPoGCKXIjHDbfLhUrtOuKhm6/CPXf2wAeD3sHAT65COUkk2spAwbWfMk1o3W6XgQMJPzkzXS6n3mUaOLAdsMWFgtWQei3v1mN7XAjYqWA6OlwHxtNjOTJuEwfHAgzTdMZyH1aPwzbdX9vqkTGcOSgDWp++PsQeBDZlmAfaHflA9RF7JQu8g3oUUEDvIf2Ukk8f3WKrS+Yvcji4aR0u+WQyqEfPxTzifkl/T2H9FQKs3HAFJyI2uPJlgzbo+Wv9WnfB0Q2Hs+5boLYQW7WNur/HbTr8lWHm89E2FVZcCIytYJha1uXM3e0yA/XOGIE6V4G5GmZA1i33KSirbffIfArKQTalDJgul9gR0OMWhi5hoKTtwMsZx+2MLU0HqnlCAiRAAiRQvAkY8hCx5EPFZk0q4LGH2kPezmVBYznHf2q51qH1pmfk4qLza+OeO1rJYkx/AHvIk+Nvq3/0gXZo2aIyMkSv+S8fNtq+XPmwXP+UyHNPnIvq1WJgSaJb1/9dwwyxxS8f5l56RXtcfEljfPj6+/LBby5CQhX8wvbv6tPJZ8BCTIyJgZ+PQm7GLuj/Y0nX/xP7/u74JVW+VJlYhISF4O9+yHA0HoH7YaBMuTJwS3yvr48my3oSIAES+CcE2OfEEjAMAzlZwMVXXYY77+2Jlue0Rdvz2jul1TntcNGFnfHA04/Jh8FhzrMjLxe45uYbcOttdx0i27L92bis85W49/EHYcqau6ieM381e5dpOvFV44Z18Om7L+GuW66Ta2Dg0DG46+GXccl19+LKbg+h12sfYO7CpWjUoA7eevlRPPXw7dD5KG2nUv8uNvsrG9n+3xMw/nsTaMF/SSD4N16pw1X4dOhtxhwrAAAQAElEQVRr6P/tU3ikRwvEOt85Vv9q0QduJEACJEACJEACJPA3CBim4SxYLu5UB089eg5ccu31Br4JrZQ6bk1a1JCd5HGRmZWHiy6og2ef6HAgrpGm49ZVUFApbYONiAgPXnnh/EASOjPX0avHc5oLdjjGuVIKst6E/l3qqMgQvNSrI846s7ITgZlGoSE6jmczXaazOH3gietxbofqePfF95CanOb8HIcMKXxt6G/LFpak1HW6WFYg8a8T12ERJr7qNxj7967Du588dDwmUOYvCBhyf8uVL4OwsFC5V9ZfSB+7Wd8v0zRRVvSFSlJbSyul/VSfsZAACZAACZwKBJRS8PqAGrWr4f6nb8cH376J9755zSn9vn0dfQa8gtbntJAPkyExh3KO9RvXwaMv3XuI7Psi+/bnL6LxmQ0cGaVO3vNAKQX9TIqMDEe36y9Hv7eewduvPIG3X35Mks2POUd93ue1p/D68w+jTcvmjjxkM4yTZ6cMx9d/ROCfR7f/kcEc9sQQsAv8/ITzkwbgG8CJIU2tpz4BzoAESIAESOBEEpD1iyyabFx9RUO88/rFqFgxCpmZXvh8lvNtHtM0oJSScqgVSilJ6ConaS35U2TleJ2Fze09zsQrz1+A6OgQALqfwr/ZlAossCqUj0SfNy/B1V0aQv9cVXauHg/O+HohJWKHDKOvlVIyB8OxUyfWM7O8qFe3DD7ocynOO6emk4z8d9bB2fT4+hvPT73UA7fdfTY+eOVtTPxpmnDNQ0SkQrgU0/lmlCVcfU7RP4ml++mks/7taNMFrFyyAW88+QbKlcnBZ989hdJlYxz9ShWFlY6q03bncrlQvlJZ6H8wUC/YddEw/gptsF3LW/pnN0I8qFCpnPOtON2fhQRIgARI4NQhoN/TddEW66PXayMlyY/4vV4pvgMlYa8P2dmHfmCZl2cjOVHLHCa7z4e8nENlte6T8eRWSjmxl9/vR1hoCJo0rIuO57bGZRd1wCWdzkG71megetVKjowlwZpSJ8MqTfffFPYtKgJGUSminlObgDJMuD2Bny3Q3zZSp/Z0aD0JkAAJkAAJkMApTCCQHLXRplVVfPLeFbjx+iYoXSoM+h/pS0vPkYSpXlgpuFyGU3TiVC9ksrO9SE7NhmkotGlZBW+9ehEevLctQkNdzmKnqNY5SilJFtvQ31x+4ZmOeOm589G0cQUZA874+lvNsq6Cnoe20TQDIbf+tw5S03KQlZmHChUiccctZ+Gjdy9Hw/rlRJ8liemAHIpgMwxDdNq4vlsnvPfZfdi5aTE+e7sfxo2YhvWrtiI7KxdR0QYqVnahUhUXYsuYjv07t8Zh2qSF+Oq9zzHlp0G4+77z8ErveyRxHeboU0oVgXVUoQnoe1SmXGmULV8WIbJQ13X6gwOdXNbnuiiloJTSp3J/bPkQwXKOLrfcs1LRcv/KwxPicdpL9I6TIwESIIESRiAyHND/44r+5rN+m9fFkPjF5Tad3Izb45LjwaJjCS0TLAFZ3e4+RE73M0xDnh1wiv6/wSBbbLTsTsJLKSXxj44pbIkbLHlu+eGThLQuOjGt4zVtmGEY4HZ6EeAdP73uN2dLAiRAAiRAAv+YADuSwMkkYMgiTC+aKlWMdH6O4723O+P+e9rgnHY1UL5cBHw+P5KSspCQmIVMSeiGhbvRpFEF3HhdU/zvhfPx8XtdcO7ZNZxkndajlCpS84P26WThZRfXw6f9uuCFZ87DNVc2Rv16ZRASYiItPRcJCZlITsmWRZiNyhWjccF5tfDIg+3wYZ/L8MA9rREbG+rYWNQLMT3dgI02GjWtjc8HPotHnuqCnNS1mPbzSPzwxZfo98pneOvZL/DGM1/g3Zc+x1f9PsHEHwdh66qZ6Ny5HoaPfwNXXN9R7IMUWxLkRcuwSG/IKawsMircSSTrZLT+Bwp1Qlkp/SGHJX7ug1983Zb5uSXpHB4RBv0b0vpbz/pY1H4jw/BFAiRAAiRwAgnI27ujPSwMqFENCPEAXi+Ql3diih6vSiWgbBlnWJ37DZyc4L1SSuIGw0lGu0wTuphy1M8tBW6nIwEmoE+9u06LSYAESIAESIAESOC0ICBrF0l8An6/7fxUxV23neX8LEe/3pfi0/evwBcfX4kvP7oSn314hfNN4j5vXoznnuyA8zvUcvjo5LBS6oQttkS16FZOcjkkxIXOF9XFy891xLtvdsbHfS/H5x9eiS8/vgpfyPGTflegX+/OePvVi9HjpjNQrWqMMy9tqFJKH05IUUoJQxs6idn+vOZ4+8OH0fezB/DQoxfhppubokuXGrj88uq47voGuKfnOXjl7Vvw8TfP4sZbL3b6WZYtc4SUE2cjuDl8I6MioH/LuULFsqhQuTwqVanglIpyrCjX5SuVR/mK5RATGw23201qJEACpwcBzrIEE6hQDmjWKFCaNwZORNH6a9eAJINLMEhO7ZQgYJwSVtJIEiABEiABEiABEiCB05KA5E9hOr9XLElUvwX9cxo1a8TijGYV0a51NZzdthpataiCBvXKokzpcCcZ7Be5ov3W87HRB75pDATGtZ1vaDdsUA6tz6ri2Ne2dVU0b1rBSTq73QZ8Yp9O7Op5HVtz0bQqpWC6TMc+/c3x2FLRaNWuCbpc2xE33nY5ut3eBdfceCE6XHAWqlavKAz1N2/9UPKfnhu4nRQC+gMTPZDpciEkxIPQsFCEhYdJCUVoaIgknV1QSjkfDGg5FhIgARIggVOfgLzVo1QsTliJjDj1GXEGJYOAUTKmwVmQAAmcFgQ4SRIgARIggdOWgE6EmqbhJN908tZv6YT0waLrdAIvKCd5upPKSo+n7QMULMl+a3v8/kPt03XSBJfMQ9uJk7xp+1ySiNacLEmC+/VvMvr80Elp/Q1pv9RpGw1lQMvJVMDt5BFQSh05mH1klVKFyB0pxhoSIAESIIFTgICOC050OQUwFG4ia0sUASagS9Tt5GRIgARIgARIgARIoGQTUEpBJ29NQznfjNbfItZF1yn13yfmtAmG7LQ92q5g0de6SBP+600pBcPM/11GSUjrZLMpR52g1jYy8Yzis/33Lo3iA4OWkAAJkEDJIyCPZJzoUvKocUanIgEmoE/Fu0abSYAESIAETjcCnC8JkAAJkAAJkAAJkAAJkAAJkAAJnJIEmID+W7eNwiRAAiRAAiRAAiRAAiRAAiRAAiRAAiWfAGdIAiRAAiRQVASYgC4qktRDAiRAAiRAAiRAAiRQ9ASokQRIgARIgARIgARIgARI4JQmwAT0KX37aDwJnDwCHIkESIAESIAESIAESIAESIAESIAESKDkE+AMSaCoCTABXdREqY8ESIAESIAESIAESIAESIAE/j0BaiABEiABEiABEiCBEkGACegScRs5CRIgARIggRNHgJpJgARIgARIgARIgARIgARIgARIgAT+KYFTJwH9T2fIfiRAAiRAAiRAAiRAAiRAAiRAAiRAAqcOAVpKAiRAAiRQoggwAV2ibicnQwIkQAIkQAIkQAJFR4CaSIAESIAESIAESIAESIAESODfEmAC+t8SZH8SOPEEOAIJkAAJkAAJkAAJkAAJkAAJkAAJkEDJJ8AZkkCJJMAEdIm8rZwUCZAACZAACZAACZAACZDAPyfAniRAAiRAAiRAAiRAAkVFgAnooiJJPSRAAiRAAkVPgBpJgARIgARIgARIgARIgARIgARIgAROaQLHlYA+pWdI40mABEiABEiABEiABEiABEiABEiABI6LAIVIgARIgARIoKgJMAFd1ESpjwRIgARIgARIgAT+PQFqIAESIAESIAESIAESIAESIIESQcCwbRssZEAfOJoPsJ6+QR+gD9AH6AP0AfoAfYA+QB+gD9AH6AP0gZLvA7zHvMf0gRPlA4ZSCkqxKEUGSpGBUmSgFBkoRQZKkYFSZKAUGShFBkqRgVJkoNRJZMCxuD6jD9AH6AP0AfoAfYA+UKJ8wEhKTAALGdAH6AP0AfrA4T7Aa/oEfYA+QB+gD9AH6AP0AfoAfYA+QB+gD9AHSr4P/O17nJSI/UlJx12Mffv2gYUM6AP0AfoAfYA+QB+gD9AH6AP0AfoAfYA+8J/6ANfmzE/QB+gD9AH6wCnhA7t27sD2bVuxffu24ypGk6bNwEIG9AH6AH2APkAfoA/QB4I+wCN9gT5AH6AP0AfoA/QB+gB9gD5AH6APHM0HmjY7Aw0bN0Wj4yyGZVlgIYNi6QP0Tf5t0gfoA/QB+gB9gD5AH6AP0AfoA/QB+gB9oOT7AO8x7zF94JTygZycHOTmHn8xZAOLQQYGGfDvgD5AH6AP0AfoA/QB+sDp7gOcP/8G6AP0AfoAfYA+QB+gD9AH/soHlFKF/iOJkE2pI9sMqeeLBEiABEigeBGgNSRAAiRAAiRAAiRAAiRAAiRAAiRAAiWfQImYoW3bzpd73W439Pnhk2IC+nAivCYBEiABEiABEiABEiABEiABEjjNCHC6JEACJEACJEAC/4SA/lnf0NBQJCQkYNWqVQgPD3d+TqSgLiagC9LgOQmQAAmQAAmQwH9LgKOTAAmQAAmQAAmQAAmQAAmQAAmcEgT0t53DwsKQnJyMBQsWOAnodevWITIy8hD7mYA+BAcvggR4JAESIAESIAESIAESIAESIAESIAESKPkEOEMSIAES+CcEdPLZ5XJh9+7dmDhxItLS0hw1c+bMweLFi52f5HAqZMcEtEDgiwRIgARIgARIgARIgAT+YwIcngRIgARIgARIgARIgAROKQJKKcfeNm3aoGPHjmjfvj06derkfANaJ6idRtkxAS0Q+CIBEiCBgwR4RgIkQAIkQAIkQAIkQAIkQAIkQAIkUPIJcIb/hoBSCl6vFxUrVkS9evVQo0YN1KxZ0yl169Y95B8jZAL635BmXxIgARIgARIgARIgARIgARIggX9HgL1JgARIgARIgAROSQJKBZLQOTk5yM3NPVD0dcEJMQFdkAbPSYAESIAESOA0JsCpkwAJkAAJkAAJkAAJkAAJkAAJkMDfIaCUglJHloI6mIAuSKN4nNMKEiABEiABEiABEiABEiABEiABEiCBkk+AMyQBEiCB04IAE9CnxW3mJEmABEiABEiABEiABI5OgC0kQAIkQAIkQAIkQAIkQAInigAT0CeKLPWSAAn8fQLsQQIkQAIkQAIkQAIkQAIkQAIkQAIkUPIJcIanFQEmoE+r283JkgAJkAAJkAAJkAAJkAAJkMBBAjwjARIgARIgARIggRNNgAnoE02Y+kmABEiABEjgrwlQggRIgARIgARIgARIgARIgARIgARKJIFikYC2bRvFo9AO3gf6AH2APkAfoA/QB+gDp6oPlMhonZMiARIgARI4QQSolgRIoMQSYJ7xqHnW/+qe/zcJaHGEghNWSkEpFqXIQCkyUIoMlCIDpchAKTJQigyUIgOlSjiDIpxfwfhSou5DLnlBAiRAAiRAAiRAAiRwmhAowvhSFP6DPQAAEABJREFUqZIVi/9XHnBSE9DBb9NIttmZr2VZyEhPx57dO7FxwzqsXLEMSxb/iSWLWMiAPnCyfYDj0efoA/QB+gB94JTzAYkbdfyo40gdT2ZkpEPHl06gKYuFA7GnU3Fidj6fDyxkQB+gD9AH6AP0AfrAqeQDp7qtR4vqdOyn2xIS4jF/7h9YumQRc4w6xyox88L587BlyyaN52C87FydnN1JS0BrJ1Aq8KmBXhzs2L4Nq1euwJbNm+D1+hBbqjRq1qyNxk2aokmzZixkQB+gD9AH6AP0AfoAfYA+cEwf0HGjjh91HKnjyS2bNkt8uRw6ztTxplKB2FPHoUUdWmdnZ2P92jVYvmwpli1dzEIG/8QH2Id+Qx+gD9AH6AP0gb/pA8uXLcGaVSuRnpZ21PBOt5UtW1ZyjM3QuCnzjE2aNkOtOnWQmpJ8VGYnusE40QNo/TroV0ohRwL1DevXYfOmjZJ09qJu/QZofmYL1KhZC+XKlUdUdDRCQ8MQEhKKEBaEkAFCyAAhZIAQMkAIGSDkhDHgM4ds6QOnqg/ouFHHjzqO1PFk8zPPlPiyoRNn6nhTx506/lRKQcejOi4tiqK/NbR54waESNzaslVrtGrdloUM6AP0AfoAfYA+QB+gD5wEH2jZqg1KlymDTRs3Ii8vt9DQTimF0DCdXwyBjhdP1Vi3KO0OEx6GYRbK62RUnpQEtFIK+/bugV4EuN1uNGnaHHXq1oOefGAxYAMnY7YcgwRIgARIgARIgARIoIQSsJ0ks44vdZyp400dd+r4U8ehSql/Pe9A3ArnGzdKKdSsVcsZU9ezBPiTAznQB+gDx+UDNjmRE32APvDPfaBipcqIio5CYnyCE99pls5JgV3wZ9kKaysgdtqcBnn8VxM+4QlofaPXr1vrJKDr1W+AWrXrwOVyOcG6nrRSejGgi75iIQESIAESIAESIAESOFkEStY4CkopZ0o6/tTxpo47dfy5b+9e6HhU1zsC/3Lnt/yQwWADTkyrlJJLFqXIQCkyUIoMlCIDpchAKTJQigyUIgOlipaBhF9O/GUYBnx+n75kOQUInNAEtM6ur1u7RiJz4IwWLREWHu44ieailNIHFhIgAYAMSIAESIAESIAEipCAUoE4Uyecdfx5RouznHhUx6WWTh4X0ViBUYpIGdWQAAmQAAmQAAmcDgQ4xyIgoBSjsCLAeFJVFHkCWgf6uuivhOj/5dHtcqFBo0bOt0L0zJSik2gOLCRAAiRAAiRAAiRAAieWgFKBuFMp5cSjgZ/kWA/blny07JyY9cSaQO3FlgANIwESIAESIAESIAESOFkEijwBrQ1XSmHLlk3web2oW6++rmIhARIgARIggSMJsIYESIAETiIBHZfqf0BQx6lKBZLTJ3F4DkUCJEACJEACJEACJEACpyUBJwFdZDO3beebzmlpaUhLS0WDRo0hFforJuBGAiRAAiRAAiRAAiRAAv8dAdsZukHDRkiXOFXHqkop8FvQDhbuSIAETiMCnCoJkAAJkAAJnGwCRZuAliBe/+7z5k0bUbNmbej/zdEJ6qX+ryam5Qorf9WP7SRAAiRAAiRAAiRwChKgySedQCDZrOPTmrXqYPOmTdBxq1L8JvRJvxUckARIgARIgARIgARI4LQiUGQJaJ081uQS4uMQEuJBbKlSzjdKlDp2UK/76aKUglJHFq1Tt+sjCwkUPQFqJAESIAESIAESOF0IKBVIQsfGxjrxakL8PmfqjDUdDNyRAAmQAAmQQAknwOmRAAn8VwSKLAGtJ6CD9127dqJGjVr60kkoOyfH2CkVSDrHxSfhx5+noM9H3+DNvl/ik6+GYOHiFfD7/Y4e/Q2VY6hhEwmQAAmQAAmQAAmQAAn8JQGllPPzcPr/1tu1a1fgCxN/2YsCRUqAykiABEiABEiABEiABE4rAkWSgNaJZ6UU9u9PRGRkFMIjIo4Lot9vSYLZwogxk/Dg02/g93lL4HG7UbZMKaSkpeOzAcPwWK/e2LhpOwzDcBYIx6WYQiRAAiRAAn9JgAIkQAIkcNoSkLg1PDzciVv3JyUCcq3jWRTLzXZ+KsQO/IR1kVpo25aju0iVlghlR2Gez+sE3IoSQY2TIAESIAESIIHTkYCOp/SXZy0J1mxLx1aBSEF/kdYKnBYLLP+1EUWSgA5OIjEhETGxsRLDB/73xmD94UfN35YbY5oGPvxiEEaP+w1PPXQ7PnrneTzx4G24/66b8PIz96Pvm8+gYb3aePGtj7Bq7aa/1Hv4OLwmARIgARIgARIgARIggcMJ6DhUAkvon+JITJQE9OECJ+DaGfMIvToqPqLysArlfBFDcuQo6k0pA4ZRpMuBojbxP9KnHC5HMFcBXgrcSOAfE2BHEiABEiCBYkigsIis8NjtSON1PGWaJgwJHJTEVYYRiBR0jJV/emSn07DGKIo5K6Xg8/mgM/6REZGOygBu5/TInSSflVL4edIM52c2+rz2FNq3bQFLPhrQOvQ3o30+P8qXLY2H7+uOSzqdg/c/G4jExGRH1/E6AWQcy+937NJ65YMIp39w53wyITIHrp1vNQTdLvDNB93PLx0LiIla/YmG1quPQXnRIkKWyOo++iiXUikvOXGupc05ShVfJEACJEACJEAC/xUBjnu6EwjGqRGRkU6c6JM4Vqlg7Ymho1RAv44/dawbiCADdcERdVsgdpUYU+JSp94fj6kjR2Dh9lznUseSuuh48/AYVYJUiad1jOqXo4XAGLqbDa072MfvD7QkrP4Ng3+cjTR/QMaSMQ+ML3G5rtXF1vVy7Rx1bC0xrYS3uskpTr0VHDeg22kI7kRY21ywxbl2KuyArY6M1mHBkvNgV8i5ltW266NcHmgqeHK4DY5qEXDqpVNw/o4OqQ++nHY9J7+MK/OydEPOHkweNBh/7EzXV9B9LZnf/rW/4bshk7EzS1fbsEWvo885atuFu5zrVhYSIAESIAESIIEjCehn55G1gZpjtQUkTtw+GJHp571fYh49klLBWhvO815X5hd9reMVW2KHOImnvvzwI0xasRvLpo/E8BmbRCoZvw0biJkbnaDhQMyg4xm/9NF9Rei0ev3rBHTQQbKzs2EaBkLDQgMAD9yowGXBvVIKWVk5+HHMZNzR/RrUrF4ZXifwB/SnBqZpwOUy5Qbbzk2659brER4WhinT/4BSSuotHG2zJTjUbRnrZuLjN1/AG+/2Q7/3+uI9KW+/3Rufj5yFuBzd34LzyYRSWtwpyvlWQ+DahoIh8zHlUwzTMKDFbMgmQaVS+tsPptiqj0pslHoJ8W0RCvbRR6WcHkB+vTOe1gVuDgHuSIAESIAESIAESOC/ICCxmR42NDQUpmFAx7H6OhjX6vOiK4F4MGPXSvz03ad4v19fvNu3L/p+8AUmLNgKn26W+FKP7cSKOvY0JcaUeFOqATsLOzdtRHyazzHJsqVNbD4Yo0q87LSIIpmXYegY1YQhMsqWOmmzbQWt29S6pd40JX6V+tCYsqhWpQxccq7HN5ToNk2Yphz1V3bsQH+l6+XaOep20aGUdNIvkXHqDd3PhCFy+cGxbg0UETYMA8EuutK5diqU9JE2R8YMjC3nohYyedhyrmVNGVcfdXwdsEqa81/adqW0zbq/CUNsCM7dqRcdwfk7OgLKxUwbSkk/0W2acjT0T/6JUlcoKlSrhjJhbrmwAak3DB82bUlG7bPaoWoYZD0CKKWkycg/mmK7CUPq8tWDGwmQAAmQQAECPD3tCQSe1xKDFPKgPFbbiQMnMZQ85uHPw+aFE9D/ow/wnsRo70r+8OMBI7FyT4YztA0VeN7j4Gbo2ECe+crwY/kf8+Gt0hbnNqqAiLBIlI7UeVE3ylephnKROsoCgjpMHXPk983Pcx9UWsLPjKKaX25uDpRSkjh2H1OlJZl+LbB4+Wp4PB6cf25rJ/hzyU1QSummA8UwDl6f36ENFi1bDZ/f79z4Qvz1QD99YnuzkWWVRafru+Hmm29Gt5tvwU1Xn4vcFVMxaa7+NMLAzjXLsCkuU4tLsZG2ax1Wbd4Hn1wpOxvbVy3GrJkz8cfitUjKAhxrxMbcpB1YMm82Zv2+ABv2pEOqACio3FRsWDYfM2fOwsIVm5HuU9Bb7v6d2LhzHxK2rMS8RauRkqdrxdH1gYUESIAESIAESIAESOA/IeByuaGUQm5Ozgka34ZlSzyYvQVjh43CVld9XHjFDeh20/W4qGU5LPrpa4xeuA9ihH4hVWLR+XNmYua8Jdi1P9upg/QPCQuD2yV6ALiMbOxcvwSzZ83AvKXrsT9PQUk99D53P9YtmYsZM+dg2YY98MrcIJtSFhK3rcbc2TPx+58rEZdpaWm4wsuiUvlSMA1AKQOZCVvw51yJcefMx5odKboSesmUumcbtu9JQmLcBvwxazp+X7IBaXm2tMlLxvCm78XyhXMwc/bvWLU9SUwOWCRBvgjIK3MfVq9ch/1eOdcvKxtbVq/AjhQ/4E3CpvU7kZIZj+XzZ2H6H4uwPSF/7lBQefuxfuk8ia9nYuGqLcjwS53WUaAopZCTtBPL5s/GjDl/YNWWOPikTluYtGMjdiSmYt+m5Zg5Yyb+XLMDudKmuyulZM7bA3P+YyH0usBU0qI8KFO5EmJCBIy2wZeOLcsWISW0LCKtJOjPApx1SnYyNm/ZjpSUJKxbPAfTZ83DhrgMiFpRwhcJkAAJkAAJkEBBAkop+P0Sg8ixYL0+V+robbr9RBTLsp1ndvLqXzHsl2Uo07wDrrmhG7pdezkahm/H4P4/YHU6oHL2YOWKdQfjGH8WNq1ehb0pKdixcg7Wxvug8lKwfW8aoqs1QJ0qUQDcKF2pCkqF68AC8gG1F3s3L8cfs2dhzvzl2JXqkzoRO41eRlHN1ef1QuUnjPUnF0fTG2zbtScO5crEIiwsVG64csrR+uj6RvVqIWl/KtLS0h3ZoB7dVmhRgOGOQY361VGteg3UqF4V9Zp1wNm1Q5GYmgm9LZ48EtNXBX/3z8LuPydg9Mw18EnjqolDMHLaMiSlJGPT/PH4ZtDP2CVrE2v3Qgz8bhDmrNmFfVuXYuSg7zBtUzbgj8OEYd/ip99XIz5pD+ZOGooffp4H6YKcXcvw0+CvMOTn2di0Nxl5lgwAW/7TRxYSIIHTkACnTAIkQAIk8B8TCMaSyjDg9XlPjDX6GxNKVGcmISHLQp3WF+DMRnVQq1ZdnHXeDbin6yWoLGsUW0QSlozHgIE/YcW2OOxeMwcDvx2GlfvELsmI6v9d07J12J6NP3/+AT+Mno0dcXFYNXMUvhs6CXtFDLk7MX7wQIyZvQYJ8VsxY/RADJ+2XjQDW+eOxjdDfsH6PQnY8ucUDBw4Frsk4N2/bhoGjZqDbJHK2DoL33w9HPPW7sTubSsxbtBXGP3nXmkB9m+Yg+8+H4Bf5q1BamYaVk0fgYHjFyFXWn37V2P4gG/w69KtSNi9HhMGD8DPi3bDkjbYfr0HElfi52snKSwAABAASURBVNETsTkrcAlvCmb+PBILtuUAvm34eeB3GDR2KrYkZWL/xjn4ftAorEsWWWs3xkusPfaPdUiQ+Hr+uCEY+ssiZEiTTm7btqXPkLnjTwz+biBmrtyO+J3rMGnEdxgxbRM0+u1/TsZ3/b/Dbyv3ICcnGQsnDsfwKWucfmlb5mHQd4Mxb8Nu7Nu4CD+KHb/v9opNcYGf4Ngj9lmpmD60P0ZMXyFrkX1YNHUEBnw/CQl5oiInQTh/jq+GTZXkdTrSti/FyO+GY2mC9JNmbaM+sJAACZAACZDA6UzAtnWkA8TFJ+Gt975Cckqag8OyLOiiL9LSM522PXvj9aU8QgN9nIsTtZMP+bXq5JRE5Lgr4dwLWqF+nZqoVa8pLulxN67u0BhhOtTQccxPk7AlW0tL8e7HjJ9H4c8dEpGkpyIzx4vUuD3I8vqwevowjJi5RYSSMW3ED5i+UUdLuVj16zB8N+I3bNi5F5uXTcMP3wzCUklci6DMVe9LftGRbJHMUn+KASfMA46HnmXZMPIT1tLjL1+GoU21D6rWEeUxe4mAlYk92+KRGBcvji4B97KpWJxaFm3PbCA9c+CJjkVEiEvOAy93WCSiIyNgIBVLl25BhZZX4LprrsXtD96OFmUVsnLTsGzOLKSWOw8P3N0dN93aE5fVyMOSxRuQuX8Htu6PxHV33Y0br78Zj3dth5Q1y7FDVIeEhkFlKzS77i7ceuW5KB8qlTDkP31kIQESIAESIAESIAESOOkE8hdDSino3z0+IeMrA84vspU+Axe1rYNVP76H/sN+xuzFa7E3JQtVWnfEuY0rQvl247fpy1Dxgrtx3603ocfdj+HSammYPX8DfEY4TEnkKlcIsG8Rpq1IQ6f7HsctN3bDvU/cjUpJi7FYMrtpa+ZhcXxp3PzAPbjxxttw58V1sXn5cuzevxtzZ69C1Utvx53duuK2njegUsYqLNqUjdCIaMTGRsPtz8WCX2dANbocD97dA91vvQc9OtXAsl9/RZxPISoK8HrKofU5V+Lyy6/BrVe0wv5NK5CcY2Pr779ie+zZeKTnbeh6s9h/RQOs+2M24vUizYnfhawrFNHRUfBIeC5XgHCJiI5BuNsADBNGCFCu1kW4tsvluOG2Hqhub8HqHZnA/s3YkFIa1/S8E12v745Hu7VC4tqlgd9gVhZ8TlI+A4tnTkdyxc544N5bcVP3u9FTbNg8dyrWSZI4KiIUXqssLrjiMlx62bXoeVUT7F40G5szsrF6zgxk1bgUD95xM2664150qJSBefM2A2YoImJiERtuIm3zLPy+PRpX39kTN3e9GffdeyPKJc3HlNWyeI6IhCfHj7INWwmXy3HNbTeiSfg+rNios+fgVuwI0CASIAESIIH/goBSSnJ5NmJiotCoQW0n0ZyalgFD4gRdMjKz8eZ7X6JenRooVSrGkVVKnXBTZXjni6E1WpyHluXj0b/fl/hx4kws27gLqTml0P7C81E7xhZ7QiReiiwQx7gQGR0J0yyN6u0vRoNK0WjY8Vq0qVMe4SGhiIqUwAYmwqOiERURIfHMekybtx0tb7gHd/bohjt6PoB2sfH49bel8MsshY7sS/7LOOlTzHeiiuXLICEpFV75hCD4acjRbNHtm7fuQGxMpNzIcBGz8ZeuqExJ+u7AlJGD8P3g7zFo0PcYOX4usiKroU55j6PD9lso+MPftnz64vf7YCEabVrXx+45A9F/0GjMWZaOVl2uRP2YLGyKy0HNJs0R0KDQ7NJbcUP7yggp1xoPP3kXyqduxfo1q7F6234YMldbRoKVBzu2OupXCdNXcOqcM+5IgARIgARI4DQjwOmSwGlGQMJBwPBIzHgHHr3zKtSJzsPGRb9h0Fcf4YshU7Ez3Qtk70Vcpgl3XjxWrFiOFatWIyUvA/t2bkd6ngFTstiGqZC7dy/yVAhy963Cckkur1wdj9zsLOzevQnbElIQU60xaoZAFkpAbJOLcPdNFyJk/w4kqLJoXq9ioMGshi7db0P7aiHIy/OJrI287ATsSAtFw4b1oL+a4ZNIu1r9pqiAROzMzIPPZ6NU5VqoWs6EbdlQnkhEhVjIyc3Bvrg0hIb4sWnlCixfsRI793uRvm8HdmfKvJC/1LAt+P3+Q2JgS64t7QtydIWVQu362j5L9IchNtINf04a7LLn4amnbkG5xK1Yt3Y1lu9Mhstl5H+x2oajPm8/du33o2bzBgiBTFFKdJNmqOJKx654r8zEj+iadVDFDegvwLhq1EIZTy72bN2D+DQTNZvWdeZsw42WXXrg+nMqAz6vI6skhk/ctgfuGg1Qu5SM67OgImuhUa1oJG3bB3j9yAsrh/q1q4optszPRFRUqFMvZvBFAiRAAiRAAiSQT0ApJfGCB12v6Yxz2rXAa+98jozMLGRl5+DVdz5FqzOb4OYbLkdYaAiUUvm9TvBBxtEjmVG1cG3Px9HjQsn1pe/AvInD8OXHH2Hk7DXI8msJWx75ljzng/bYzpcXbIlvbDsPfomN/L5c2LbUS15RxxsQaf1FXdPlR0bCLqSH18QZNWNEgR9QYWjeuCashG1IQmCTqCZwUoL3+VHhv5+haWpV+cjkJh5NoyEfMeibctYZjSXozcO8P5c5zqWD0sP7yL1z2pRS+G3WfDRtWM/53Wi/33bqD5c/5NqWgDqsFq65uyceuP8B9JTyyMP3orlahuHjlyAPoRLMW1DyX6CfAWUocRg//FLXoPMduP/my1C/nMK66UPxydfjsCc5DyGmcsT1HEQY7rAIlC5dGsjch+mjBmLYLzOweNUGrJCgNlcc1RBpPQ9o3WK3XBaLF40gARIgARIgARIggdOagMqP6WShYJjmCUFhy+JDK/ZlJGHPnkREV22CC7t0xd09H8EDt1+DUnGzMHzSSmTZYXDZudi7ZR02rFmNVZLITQ5thIs7NkO4lQO/raDNteRo+9KwbcVarF21BquXr0KEJJo7NqmAnGwvDFMiTyfwtGG5IlAqNhIeiWwtZcDQiyQxxrYNhMeWlgSyIaFsfkxt+2HBcMbQMa4uIgpDBtWLKlsulMj49dFQsqayYEmlqIRtWMiM244169dh9cpVWJtgoP2FF6NquMjlz1+6iW4lRbrqC1kP6EOwKLFZJ7YhdiqxxO+3AMMNlbMbU4Z/jxETZmLJqrVYtWE3cmVcld/ROcr9sywDhmHDsVt06YG0mZCEsRaVVn3Q1WKADZk5bJ/MWViY6mA/T0QsSkdLAllPTiuXsSyZtCHKRMrpq4/KMCArT8iA0Eode6HkPyCw6AQ3EiABEihWBGgMCRQHAvo5rcu1V1yE89q3xKu9P8f/3v4UbVs2x43XXiqPVXnK6uf4STFWHvLOOBZSE3Zjb4qB2me2x9U33YYHHnsE3S9pgM1TR2DG+gyoUBeUBSiFQGSj4wAENqWk0jlV0q6cs4O7wLXlBBOmo0NPTzMw8nVImHFQvISfSfRUNDN0ezywdbAm6pQKQJbTI17BllKx0bigQxt8P+xnJO1PgWma0DdBB3A6Ga2Dt6CaEWMmIzEpGVd3udCRMSQIPELx4RXS2XCFo1SpSIRHRDpfe48oVQVn1C2FhD17JNBXEpB7kevz5ffMxI5te4DQcBhZOzB17GSkV2uO8y+9Fvc+egcqJC/Bn7t9KF/ag3079jiOpZTCtjlj8OPMpdi0egZmbfLgqu53ovuN1+KadpIsV36Yot1QBrRzKSi54osESIAESIAESIAESOC/JqBUIC7T8afH7Tkx5khyVCvO2bkEw78fimUp+ipQoio2QqsGpZCWtB9eTxnERoSgYceuuKFbd3Tv3g2dzyiD5MRMwO2Gy5SUqSx8QktHwR1eGR1v7opuPW5Gt+7d0TwmBQkZoahcNhpp8TuQKvNSSua2cy6+H/obUiMrINa3H9uScqCUkpKGGSO+x4wtWfB43I4x7tCyKO/JxK69CdKu4Ba5dFmMJfrCUTHSA8nRwjCVI+vspF0pBU+ICzGR0ShVrz263nAjuotNN13eEiozHnm2KaL5faSv1+uFX1ZZusZK3oLtyYBb6gEFwzBkr1vgbEopeEJDkLBsKn7fEY5rbpH4uusNuKF9XRgi6XLMVrAtEQ+JQdkoP/ZuTYRSyinYswPxXg9KlQ+BbSuk7d4G/Q0jpRSQsBdJuSbKVCuP6LBs7N6R7PRRSmHDjOEYMm0L4HE748B0o1SlWOTs3YMUr4Jym8IiBXv2pCKsYjnoQF8psUiJHfkvJXMxRFf+JQ8kQAIkQAIkQAL5BJRSUEpJPGDhmisuhP4mdNtWzdH1ms7wS8ykVKA9X/yEH2RIGcOHTXNHY9DoBciVq8ArFNXObIvqYV4kp6YDHhO+AnGMf/8W7NiP/DgG0HGMoQI9nTgg/0I5H6qbiIwtj5CM3diRZsn8TSkK23fugx1ZFqUD3ZDfPf+qZB6MoppWSEioBHg2vN68v1SplHJkb7npSlSpVAEvvvERtm7fBaWUJKINKabcQIXs7BzoBPWn/Yeift2aqFC+jNMPx7HZkljO3L8RU8dOwuSJEzBhwmSMH/0DBv62D3Wa1EW03N3qVUtj85zRGDN5OiaN+QlLEmy4cnLgD41E9vYFGDlwNP5YMB/TJ85AclglVK9YE2e2aIqsVWMweNJszJ0xBqMW7Eb5WrVQLiwcrtx9WLJgLmb9OhY/zl2HjLQ4rFiZgDSfH1mZWcEvYRyH9RQhARI4cQSomQRIgARIgAQCBHTcqhPQIaGhgYoi3iulYInOiDqN0aBcNsZ+8SmGjxmPCRPHYui3n+D7eX60adcIMaEV0faMSlg8uj9+mb0Af0wfiwEj5yA9JARuIw8ZGVlOXKxqtEHz0on46avhmDN/IaaN+Raj5m6F5Y5A1eYtUDF3NQYOnoD586dh0Lh58MVWQpWKtdGicRksGvk9Jv++ANN+GoHF8aGoVT0M3txMpKdnOv3bndsMO2f/iJGTZ0mMOw7fj1mAci3PQXW3Qm5mBjKzcgPf+pH52L48ZKZnIEu5Ub99G1grfpbxZuDP+bMw9LvBWJHsRmSIBNsye1vkUaYmKrkSMWnocPz222T8NPZPZBpe5OZZgO1FZkYmvHKqRaUCuVnpyMrzS7I9FiE5u7Fw/lz88dvPGDJnK/JSd2D58t3wSoLbtP2AKoVWbVsiZ/lIDB4/A/NmT8aAYX8grN7ZaBEO5FomkLYGP42YgBm/jcNXQ3+Hp1ZLNCwbi8YtmiN14UiMmDoHc3/7CRMXJ6FK/RqA5UV2ZjrScoAyDTugkWsdhuqf5Zv3O34eNATLc2uh0xllAV82MjMzkeezEdhssT3TsT1wzT0JkAAJkAAJkMDhBEzTcP6PoWslCd316kugfxrXNIzDxYro+mhqFJTEKYAH9ZucibC9U/HJl4Mx9peJGP/zjxjQ7xNsiGiMlvonwqIqo4JbxzEjJI6ZhJ/GLUKWyxeIYyQ6ypY4KTs/kMnLzkRmthdY9iLhAAAQAElEQVRSDSgFX24ujIqN0bauG5MHDcTUOX9g2tiBmLQ2Dy07tMr/+TCF02H713dYqQCosLAw+RTDjxxJGjvg7GAg5lwVunO7XHjxqfuc5HKvVz9wvn7/49gp+GXKLAz4fhQeefYtzPr9Tzxw903Ysm0Xvv7hJ0lMG04S+qjq8+3xlKuNNq0bwZO1H0nJyUhOTkRqTgjOvKIHbrqwKfTEa1/YHde2rY6MhHi4q52Nm2/tila1y8BllMbVd9yBthX92LpxA+L85dHlxh5oUdZAWKPLcFfX8xGeug0b9/rQ9soe6CJBfZkmnXDjZWcga9cm7M0pjQu63oObu7SAlRAHf+k6aNeyMaJdhWJgJQmQAAmQAAmQAAmQwMkkkB9I5uTkwOf3Q8exenilAnGtPi+SogyJOW0oTxV0ueM+3NixIUJ9mUhNyYCrTF3nH6++skUVZ6h6l0iMekFtZOxcj83xEmNecxe6nVMLhuVBozbtULuM2KZK4/Iet+Pc2qHYvnEt4lAF19x5PzrWkCAzqiluvf0GNIpIx/pN+1C6RRfccd1ZMr6JFld1xw3n1sT+besRj6q4/o7uaBSqEFquPtq2qA3DBsqddS3uvO5smMk7sXlPFupLnHzbpU0hoyKiajO0blYTwTS9O7Ya2rQ5A5HSL6zaubi1+2WIyd2DtZt2I7L51bi/x4UoJSbZtu4t0/NUx1U3X4fm5fyIS/ai/sVdcUuXDqhWygTcFdCi7VmoFC5y+qVCUL/FOahbykBs88tw/YXNkbFrI/bklsHFN9+Hmy9sAmQmIMcCDFPJugAo2/xS3HbjRYjM2o2NO/ajcturcet17WTukPvrQ/mmF+C8WqHYuy9V5nk5br2xvf7yMiq1uhK3XHsOXElbsTkBaHf9nbi6YZhYEYEmbduiVpQCQqoI4zvQqrwP2zZtRXZsM3S/sxvqRgIwYnBG6zaoFuuWC/0KQc1mrdG0mm7U1ywHCPCEBEiABEiABAoQMAwlz3DbKUZRx18FxjnWqTJ0fhGIqtsB9953J9rVikJORjLSsvyocGZn3CPP+0Y6/jJq4pru16JZWZ/EMX40vPhG3HL5Oaimgx1JITc4qy0aVIpwhqrSsBXOqlcWkDm5JNFuyAftUFE47/rbcWWr8kjavgn78krhku534ZL6UYBkqkVUjiX/ZRTFFG3bdr617Pa4kZGRHlD5FwSVCjhbSIgHTz50O15+5n5UrFgGCxavxJRpf2DPvnhcedkF6PfWs+h+Qxc8++hd+HXGXHw7eDQMcRI9iB5XHwsWpQJTCq3cGJd1vQ233dIDPXrcgltukdL9JlzariGiXBItSycjpAxad74et97aDRe1qo8qNZvj3LMbBYLrmBro0KWr9LsNN193GZpV144hnSSQLt+oPa7rdhtu794V5zWvBlOrM6LQoM0l6Hbb7eh25XmoFhuLZhdcjes6NUXFyg1x6SUdUCY/NlWihi8SIAESIAESONkEOB4JkEA+AYlD9VlGejo8brcTxxYWV2qZf18CkZ/tKoUm7S/CNV27oXv3W3Dj1ZehRW1ZoOg40hkkBHVbX4xuPW7Dbd1uwLlNq8AJWc1yOOdSiUUrhzhSRngFtLv4Gtxy6+3ofm1nNK0SIUsX3WQjtEIjXHJtN9wu8e+V552BGFPXA7YkSpucexm633Ibbr7+cjSuHOhTqnYbXHbRWYjU4bNtomrTc3H9zbfg1h7dcEmbugg3AsaVbdABnTs2QURAHTzlG+CyzhccSBqXqdUCV3XtIXF3d1zVsTligzGvcA7MHois0hxXikyPrlfijJoV0KDl+TirmszJUxMXdbkYtaPzlZthaNnpSrTRFXYoGp3TGT1uuR1du5yLajGRaHhBV9zQ6UxEmVregAwBDaB8/Ta45qZbHBu6dGiGUk6ALjK2H3n+cDRsc6HM/1Zcd3EblMu3DzLnas30nG/DrTdfj3MaVYDSU/aUx9mXXYozKwZm7ImtgY5X3Ihbb70V3a7qhLplQ/SQQERFdOx8GRqWC0Fgi0DT8y7DOQ1KBy5VcPaBS+5JgARIgARIgAQOElBKyXNcHaz4D87EBIkjJIYqVxvndr4a3bpJDvHmbrjqwnYSd7gCz3uxK6rqGflxzBVonh/HtKgeJi0RaHNRl0DcIld123XBJWdVQW7CLiRl+mC4nYAFVkgpnNnxCtwsMU2PG69Gq9qlEFD+385fTD5pL6MoRypXrjxSkpNhBX5I5S9VK6UguWspNpo2rof777wJfV9/Ch/27oVXnnsQV1/eCaVio+H1+tC4YR2nbsr0P/D9sJ/FSUW99Jd94S/bgt/vk+I/tIhtNoI32Ib+F7j9fr/YbIsdlhytgD4xLNjmtMu10yBdbdGh63Sx9O9eS532HMsKjmWJLjugW2RtxxapcxSAexIgARIgARIgARIggf+YgC0xmo5bdfx6MkyRqFfizGCsGDjq/+X0QFgqRtgHYslAbBpoC8SUOuQUEQk5A9c6DtVF63BCUREOxJwB3fq3FG2nA3TLIWMH+wTk82NfUaKZaJ1OET629IRsjpxcy2ngdVhs67RLPH2wX0Cs4L6gjB7fEn2BOdlOrB4MtXUfHYNrGT28dYCJdUh8reUOlCNsFwa6swi4PGEIDzWd35b0O2sD0SP1zuuwfpY2SOoEshPH60tHTozTNvmdOVrQ9Y6YrBx1vb525GSn7XX0yDlfJEACJACAEEiABIo7Ackt2oc86yWO8FsSdyA/mpDIwIl9dL3EaFr2QByD/JjBBkTG1nFLWhymj52A+OhmaFk7DHoznD6B/jqeCMY5uu10KUWSgFZKQmqBGVuqNHJysqG/TXK8AKUrIDtLbp6+4UopmGbALCdwFr1ut8sJTHUS+vkn7kWZUoGvSAQCPxS+KUP0uKSYhxbDwMF+CoZpOu2GoaCUAcMw4Gxih2GaTpspR0OunXrZ6a/p6zpdDENJjX4p6RuUN6CUXEs/09DnhuiRI7iRAAmQAAmQAAmQAAmcfAJHjpiRkeHErbGlS8uCwXZityOlirJGFYgVTYkNTRgSL6LApgzTqTclhjSMAjGmvj5wqWCYBeQK6FBKx5z5bYaBYBfImWGYB3Xn91GOvIHgpqSPGdQt58H+jpxcB+WQ3++Q9kL6HZCXE0dHvoyhFAzRZzgKlGOXVCG4GaYJI7/CMEyn3TQNKKVgmCZMw8Dhm5I6U7fpYpiQgyPS9LK7cP8NLWEamo1eGxhwhnVagYL9DCPYopxxDl4Grk1RaoodB+pFk2GaOHgNGIZcF6wANxIgARIgARIggeJOQCkFwzRhHigGpArBTSkdR5hOuyENhmEceP4bpinnChAZZZgwo8rjkruex4sPXY2qYVIP2ZSCodtE1pSideA024yinm+1GjWxbdsWR61OKDsnf7HTt8OQm6dUIJEd7GcaBpTSrXBusq4/s1lDXHHpBX+hkc0kQALFkgCNIgESIAESIIH/mICOJ7UJW7duRrUaNfQpbGfPHQmQAAmQAAmQAAmQQJEROF0VSR5TJ5nV6Tr/o8zbOEr9365WKoC2TJmykjQ2kJgQL8dAQvnvKFNKOf0K66NUQJ/+tnRh7awjARIgARIgARIgARIggaMR0MlnpZTEqQlOvFmmTDlHVCnlHLkjgZJIgHMiARIgARIgARIggf+aQJEloJ2J2LYTzNeuUxc7dmxHTna2cw2pd9qLYKeUgv62NLiRAAmQAAmQwKlDgJaSAAn85wQCcaqOT3fs2Ibadeo5capOSv/nptEAEiABEiABEiABEiABEijBBIo2AS3JYR3ER0REQP+DLuvWrnF+jFui+2KCkGaQAAmQAAmQAAmQAAmcngQU9P9Ft27dWpQtVx46XtVxq1Lq9MTBWZMACZBAiSfACZIACZAACRQXAkWbgJZZKaWgg/lq1WsgKjoG69etc66l6cBRn7OQAAmQAAmQAAmQAAmcBgT+4ynquFSboI/rJfkcFRWF6hKn6mulmHzWbFhIgARIgARIgARIgARI4EQSME6EcqUCwXydunXh9rixeuUKeL15UCqQnNYB/4kYlzpJgASOToAtJEACJEACJHA6EdDxpi5KKScOXb1yJdxuN3R8qjkoFYhX9TkLCZAACZAACZAACZQkApwLCRQ3AickAV1wknXr1UdEVCRWLFuK1JQUJwmtVCARXVCO5yRAAiRAAiRAAiRAAiRQFASCiWelFNJSUiQOXSbxaAR0XAow8QxuJ4sAxyEBEiABEiABEiABEhACJzwBLWOgVq06qFVb/8OE27BuzWrk5OT/44S6kYUESIAESIAETigBKicBEjjdCCilnHhTx53bd2yTOFRiUYlHi4yDXWSaqIgESIAESIAESIAESOBvENBfNAiK24zJgiiK/fGkJKC1c5QuUwYNGzVBeES4k4Ret2YN0lJT+bvQxd5FaCAJkAAJkAAJkAAJnBoEdMyp40sdZ65ds9qJOxtJ/Fla4lDd9m9nEfzudEhICCy/Hz6fD4ZxUsLpf2s6+5MACZDAySXA0UiABEjgBBFwYi/JPOfk5CBCcoxHG8Y0TadJqWAE51yetjvT5YJS/x2LkxIxKxX4yQ39u3vVa9RC46bNEVuqFHbs2I7lS5dg86aN2LN7F5ISE5CakozU1BQWMqAP0AfoA/QB+gB9gD7wL32gxMdUEjfq+FHHkTqeXL5sqcSX25w4s4nEmzrudLndzhcelCqCgDtfR1R0NMLDI6CT3CkpKfRT+il9gD5AH6AP0AfoA/SBk+QDaTLO+vXrABsoU7ack1BW6tA4T3/xQMfBaampSGGsJr6ZiuT9Sc6XJxxg/8HupCSg9byUCjiDdgKPx4OKlSqhabPmaNSkKaIliM/Ny0V8fDy2b9+O7Vu3spBBSfIBzoX+TB+gD9AH6AP0gRPhAxI36vhRx5E6nmzUuInEl2c4caaON3XcWTAO1edFVerUq4dy5cph5w7GrozduXahD9AH6AP0AfrAAR9gzHciYr4COrdt24aIiAg0atz4qN/oLVWqNHJyciXHuBU7tvHebBcGCZJzLV++ghMKKxXI0ToXJ2l30hLQwfkodegk9f/CWE4A6N+JbiSLhuZnnInmZ7ZgIQP6AH2APkAfoA/QB+gD9IFj+4DEjTp+rFWrDnQ8qePKYMypj0odGnfquqIsFStVRrPmZxzbxjMZ1/43sT25kzt9gD5AH6AP0AdKpA9I/Fe1WnWoQn4GTalA7FeqdGmc1bKVxGnMMTo+IMxanNUKmpuOhZUKcNLnJ6uc9AR0YRPT304JFAuWxUIG9AH6AH2gxPgA39P5XKMP0AdOsA/YtuX8xIZt24WFmSe0To/J92uLPn6CfZw+Rh+jD9AH6AP0AfrAoT6gY7BjBXm6ncwOZaZ5aC7H4vav246hoFgkoJVSztfmlTKgf0ychRzoA/QB+gB9gD5AH6AP0AeOxwd0/KhUIJY8Rsx7QpqUUoxdDfrp8fgpZegnJ9MHHunYDAAAEABJREFUOBb9jT5AHyjpPqCUwrE2pRRjtEJiNKWOzQ0ncCsWCegTOD+qJgESIAESIAESIIH/ggDHJAESIAESIAESIAESIAESIAESEAJMQAsEvkoyAc6NBEiABEiABEiABEiABEiABEiABEig5BPgDEmABIorASagi+udoV0kQAIkQAIkQAIkQAIkcCoSoM0kQAIkQAIkQAIkQAIkUIAAE9AFYPCUBEiABEoSAc6FBEiABEiABEiABEiABEiABEiABEig5BMo7jNkArq43yHaRwIkQAIkQAIkQAIkQAIkQAIkcCoQoI0kQAIkQAIkQAKFEGACuhAorCIBEiABEiABEjiVCdB2EiABEiABEiABEiABEiABEiCB4kKACejicidKoh2cEwmQAAmQAAmQAAmQAAmQAAmQAAmQQMknwBmSAAmQwDEIMAF9DDhsIgESIAESIAESIAESIIFTiQBtJQESIAESIAESIAESIIHiRoAJ6OJ2R2gPCZBASSDAOZAACZAACZAACZAACZAACZAACZAACZR8ApzhcRBgAvo4IFGEBEiABEiABEiABEiABEiABEigOBOgbSRAAiRAAiRAAsWVABPQxfXO0C4SIAESIAESOBUJ0GYSIAESIAESIAESIAESIAESIAESKECACegCMErSKedCAiRAAiRAAiRAAiRAAiRAAiRAAiRQ8glwhiRAAiRQ3AkwAV3c7xDtIwESIAESIAESIAESOBUI0EYSIAESIAESIAESIAESIIFCCDABXQgUVpEACZzKBGg7CZAACZAACZAACZAACZAACZAACZBAySfAGZ4qBJiAPlXuFO0kARIgARIgARIgARIgARIggeJIgDaRAAmQAAmQAAmQwDEIMAF9DDhsIgESIAESIIFTiQBtJQESIAESIAESIAESIAESIAESIIHiRoAJ6KK/I9RIAiRAAiRAAiRAAiRAAiRAAiRAAiRQ8glwhiRAAiRAAsdBgAno44BEERIgARIgARIgARIggeJMgLaRAAmQAAmQAAmQAAmQAAkUVwJMQBfXO0O7SOBUJECbSYAESIAESIAESIAESIAESIAESIAESj4BzpAE/gYBJqD/BiyKkgAJkAAJkAAJkAAJkAAJkEBxIkBbSIAESIAESIAESKC4E2ACurjfIdpHAiRAAiRwKhCgjSRAAiRAAiRAAiRAAiRAAiRAAiRAAoUQKGEJ6EJmyCoSIAESIAESIAESIAESIAESIAESIIESRoDTIQESIAESOFUIMAF9qtwp2kkCJEACJEACJEACxZEAbSIBEiABEiABEiABEiABEiCBYxBgAvoYcNhEAqcSAdpKAiRAAiRAAiRAAiRAAiRAAiRAAiRQ8glwhiRwqhFgAvpUu2O0lwRIgARIgARIgARIgARIoDgQoA0kQAIkQAIkQAIkQALHQYAJ6OOARBESIAESIIHiTIC2kQAJkAAJkAAJkAAJkAAJkAAJkAAJFFcCRZeALq4zpF0kQAIkQAIkQAIkQAIkQAIkQAIkQAJFR4CaSIAESIAESOBvEDhpCWjbtuD3++HzW7BtG7ZlwecLXgcttmGJjN+RAWzdR2T0daCIvFxbdlBejqLLn98nKOMXgYIiIqWVOePrNuf6wC4wprYloEfG0PrEvsN12IeM5Ye/EJmA2oM6fVqXFOd4iPxBmeC4+ujTcw8oOXLvjC/8gi1yrXkdbrsl9UGRgkdH9i/0OzKasWYoeg5cy3lQl753fpmTX2ScOmnTcn7R7dTLUdfblh8+LaeL6AycW0fch4BcgXnpzlL0OD6fT3QUbLMP+EhweLm5sIRtcOzg0bl/Ypu+9sn4R3DRbVLvdwRlwEJeWq/uG7Dd79iuz6WrSNuBa9Fx0BaxJmhLwUqRFod2bNf6NLujDavH1Db7haM+Wofr0bp0ESN0u6NPM9YlX9bWfztyXbDNOc9vt/S9cdgWsCJfnx63QK0eSfj6D7sPTjVsmaujV2yV7oHKY+wtkdc26zGCx4L9tL5gvXPMt9dRKYK6To9XsDqoU99ffe6TeWs5XbSsX2zT5w5zPb7cL18BGafeGeDQnWao+x8Yq+D4ByqlT369X8axYQsrC7qfr8AYPqftoKxuP0SFtkvkrQKVeny/1Pmlb+Doh56jaIHl3D//oe9BYsfhf4dSpcWl2NLHErt80ufg3bVFwNEt4x+sFXE9Dz12AXt0rS6WyGr7tV3SXVcdUYJ6HbkCOmztl8JfMy/YyamX8Rx5OWqbfHK0Du8rdbpet+vinBeQCeq08m102qVPULagqMNQ2Ab7BI/Bvn5pKzg/zdYX1CVzCJxbzntAwfkE5CwhGNR48Bicp7ZHl+D9PCgReP/wOX+bBXSIIQf0OjfKDryXiD0F56T12DL3gvboOvlDDdip5Q/v4AjonegUv9J2BUr++MGxZc6BevE70ePXfHS3/OKMm18fkLPgDCX99bXvkP4WpDrYMzAXafc5/QP6ffn6A/POrzsgYwXm4wwQUGOJ7bpP4OrgPng/g7qdY4F+thjil3F1/RH3Q/xV211A/KBi58xGQP9R/q7y56AnGxij4LwdBdLfj8PH1Tq1vF/6H33sQH+9ZyEBEiABEiABEiABEiABEiCB4k7gpCWglTJgmiZcpgGlFJRhwOUy86+RvykYpilyhshAivQRGVP6BIrIy7WhENhk4ShCIm9KEVlHTs5FICgSEJS9UiITaMMhm3LG1LaYztim2KTlDChJIThrfcgmYymlAjoOjKNlJGGAwzd1QKfrCJ1B+YMywXH10SW6FY6yOeMHxnQk5NowTYej7quLS64NqXfaD9sZpsj+hX7DFBnN2FBQSh2YhyHnQRb63umxTEPB2aTNME1ho+9B4KjrlWEGWOo20altM818mWBfEQzIGVByXvClHB9xiY6CbcqxSesJqrClpyGyph7HzNcvR0ef2Kbr9f015BwFN7k2xS7TESzYcPDcEL26b8B2U+Zoij0mpKsIqcC1y4ShgvcVUNJHj2nqShTYlHJs1/p0m3TB4ZstFUawv2k4+g2tR/xPmg59iT49jqPPmbvYp2VFSqlA34Jtznl+u2GYcLk0W1XAcOWMZ5oGpBYFN8MwZd5H1uu5OnpNaVMFexx5XtjczAL99BS1PtOZS8B+07FX9xR9BebrVEuVfhlGQNaQdn1+yL3S99cMtJuGgqNf6grKOPVa0WFFCUM9N+kWaFHK4ePUHagEkF9vmsIACobYo2UKjuEydRuAfFndfogK6WPKvI38SoeFyrdb+uo2XQzpD9kMw5T7Z8I0DCi5dm6itBmmCdM82E+q5F1MCygYIqvvuWlIjwNIlchLH2mTWhzcFAxT1x9aC9kMkdX263G0fqk64qWUcvQ6cno8BDalxDbhbxao0y1OvYznyMvRlOKSYmg5DUOEDshIvW7XRcscrktEYRiGw8dpz5fX51od8jfDMB2fzr88cDDy+5qmAZnGwXrRo3WY+ihzCJzLfOTaLKDYMAN61YGeB0+UypcXGa3HKDhAvpj2UX2fXKaMn18HkTNM07FXTqVWwTD1tQlD4ZBNif3mkZXO/XDGPLwtv7cNBcMwD8iZpiE1AGRAwzQdnqYcD5b8doj32YAzrtMenKMBZyilHJ0F762pdSvkbwqGaTr6XXI084tLywCBtmDdsbgbpsNHuhzyMoSHHtsV1KGPjmEBMaVUwD6pN+QcBTdlOHYZCkfZFAL6XTC1kHCAbEopmKLPNPMZ5V+79PVhugzDFE6HVhpis5nfX6sFNxIgARIgARIgARIggeJEgLaQAAn8AwLGP+jzN7rY+os/Iu9D/KZlGPjl1+j99USs3roHaxdNwwf9+uPTn+Zj1/48kQHy0vdh6qgf8dkP47E8IRtJ2xbg0w++w1fDx+GHEVKGDMPbfb7FzB1ZjrxeGOfGrcfYET/hmxGTMHL0L/jmm+H4cfY6pFhaxJbxpejTtI3o/8W3+HFJsr6Clf+1ory0OMwcOxzvyDifDhqNb74bivc/+wGDJi1GfJ6CEmlL65IFZPqOtRg5dBQGj5qIYcPG4vtxC7ArG46MzpHob1JBtrz0eMweOwJ9PvgWBXV+P3ER9uXCkbfSdmDy6JHo8/43+OL7n/Dd0DEYMOAbvDd8LpJEJ2TTOuXgzEEf0/eswchRc7DLG1jl5iZsxYRRQ/CW2P7FoLH49rsh+PDrcVi0M12LQzo6ySc7cIWVv/yAd4bPx36/rhAu+Q1BuzNE3y9DBqH3h4Mwcdl2xO3dicnDv8WLfQZi6voUx27b58P2RTPw2Tcj8OOMDcjUqjL2YMpPo/HdsPHCbzC+GrcUmV5g6+wx6PP5UAwe+TO++OxrvCvn3/84Hl//MAw/TF3n2Ka775gzGm9/MxnbnHlb8Gm7rExsWDobH/b9DO8NmY0EXSdA8tJ2YbLc7y+GTMCSPTm6O5SVhsXTJ+LrQWMxbNQv+O67Efhy5DRsygKy4jZi9Pffo/cnwzB15S5kO3p8Tj9r72p89ekATNgoN0XqRb1Tf3CXiWXTp+Djj77CB9+MhL5H3/4gfD4bi1Xx2bDTtmD0sKF45c3++HVLZoAPsrF58Vx8/dVgjJq7CWnOUJajMjtpB6b8OBh9PhqEITNWIj4jUB8cV0wQHTlYNmMKvh0yFiO0Pw8eiynL42CL/8kNhZYJymfuW41hA7/HOx8Pwtc/jMAnXw7DL8v2OWMlrJ2HAV99jXc+G4avh4zBd4N/wkcff42RSxPh92Vi0a9j0PvdT/HFpPWwtZOL4vTtq8S/h+KLkfOxK93r6LHz/06WTxiO93+YgQRda1sQcfmDzcDqORPQ76MB+HzkbKzalRaoDxqoZZ1iO/UKOVgxayq+kb+zwT/9gh9+GI4vRkzDugSBJAr1FPcs/x3fDx6NwaMn4IchP2L4zHXIdAy0kLJrJQZ/+x1e6zsYixI1OxuQe7981jR88eVQTF0bj+XTJuCjr4Zg4PCf8aX8vb/z2RB8/+MEDBw4CAN/W4P1i36Xe94f7/YfgW+HjhZmP2DglNVI1erEVv23EDQ/ZfPv6PPuCCxLzJMWIG3vOozo/w3e/mQkpsnfR7ZTC+QmbcaPg4fjy6GzsSV1PxZOHo9+H/bHx9+NEp8ZjW/0PfpqMrYL05z4tRgq16/2+QHz43yiQeZgZ2DVnJn46quhGL9EfNQPaBaJ6xfgB7lvg8T+IfL+8NnXP2LaxlS5fxlYMGks+r7/NQZO/BN7siCbgj9Jvx/IeCMnYNhIOY6ajrWJfujbK38ImPPreLz5+sf44fdt8EulbfuQuG01hn09BN9NXICdGaJG7pSDIjcFE777Al/O3CE1gXq917z/nDoB73/4DfqPno3VcQE2ttMIecsJnO3fIvfqyy/w+ieDMXbhDmR5tYAPCVsW42t5fx3663IE3qN1PZCwfiG+7v81en86DN/Iffms//f4aMBPmL4qEbaGAR92rV6MgZ/3l/eRISIzBt8NGo73Ph2McSIS0BwAABAASURBVAt3O0psvwVndF8KlsyehPc/GICPvh4p92AMvh04GH2+/hlLdwXsBfKwbPJQvDF4ITKcTnagr9yLpdMm4cMPv8aXo2Zg5V65y7ZWn4lZP43E+wN+lGfRaHz4yQC8138kBo2agP4DBmH0kjixUMvl4M/xw9F34Fyk60uniG5HBxAn9/T7H37C9yPH4PP+QzFZ5hcQyRewc7Bu3m/4+IOP0XvIgoAOcci8hO3yvBiBD3+YhjVxOdIlCdN+GoZ+A8biz52Zcg1hrw9ebFo4DcOnb9EXsKSvPsncuRIjhv2EbwaPwa+rknRVsIOcB8ZW3iTMGDcG3wz7Gd9/PwyfD5+FXX4htX8npowcjN4fDMTn8rep30s++3IQvhj+B+LzeytlYeuf0/Gt/tuW5+PgoSPx+XcTsGJfNlJl7B+++RZviS98Le9r/fv/gM+GzsDWwJujPPcTMGfcSPSR5+Un0v87GeNLYfrBt9MR78vAvPGj0O+rHzF0+Gh8+kl/9O3/o7w/jMfnXwzH+MXb4RUbIO8tiycMxhtDFiHLmY4w1/V2Ohb/OlHu51f4SP89Dh6BDz8fhjGLdubfLz+Stq/EkAHfoveXYzF3bZxTr31Jd0/buhgfv/ct5uv3KKnQ7w9ycNg5w+QmYN6MiXj7jY/x7bQNkJBB7oNP3qvWYcS3Q/HNz3OwTf99Jm/B6EGD8LHMe1OK0xOCVvRk4c9Jv2DqmkRHbWCXITZPwNdyr/oPmSj3NzdQnX8vAxfckwAJBAhwTwIkQAIkQAIkQAIkcKoQOMEJaAWlbFiWidgqtWDEb8CaeAM1K5ZB9dqR2LFyFdLDq6NslAs+nwVXeGlUjwyBColE7ZgQJOxPRUzlM3DFBe1wyQVtUTZlDWauzUT56BDhayNp1a94pd8vyKvZAped3xodz22HyzrUR/qGZViTLCKwDvzv5nvX7kWGJH+mTF+EFABKFnOW5ZcxS6FhaT+WLNmOaueci8su7YTrO7eAb/kvePr1IVi534IhlBKXTcZrn09FWKN2uKhDG1xwwTlo4tqAd98djJXptswT0Dvb0RmLhmUtLJHFceWz83VeehawaiKeeW0QlkvyzAgvj9plc7BgaRxqt+6Azp3ORZcurVFG5SE9zVmaApISsWWvZEm8ddl8jJIk/KhZ65BiK6kF3LEVUT8yGX+uSUDtc87BpZ0vRJtSu/HB+8Oxar/0lMSNZdnQ0t68RGzYlIQN82fizx2SVJHa4GJaKQVb7A4TfVXMJCxam4wqdSqgVOlyqBWdi+3rlksCbyI2ZwHKMFGhVhnYXh8qVq6KUCtREmqjsDmsPi46rw06X9hEMnXxiI/LRG62D3XO7YSLOrZHdOJ6YRmGCzuejYvb14YVl4g86C0FqzalYNeSOZi5er9UKCjLgq1CUbVuBSApHn9MGIVBv20DlIIZXg5VI1wwQsJRtYwHdt5eDH3/U4zdGokLLjoHF3Roh0svPReN1Q4s2JCFkFJVUDZvNxZvykDNWuUQYlvw2yaAPKzZkYL961di8qzlsMRPIT4hDc4rwCYUdeqUx9YlK+CvcgY6dzoHl13SCc3KhyA5MR0qvBIqxLgQt3I+vh74M7ZJnkDZHlSqXRNhmf9n7ywA7Cqu//+Z+2Tdd7PZuAtxIwRCEggBAgSHkGChuLRQoUK9FCkVXJMQd3cnIe7utpJs1l3fPrn/M/ftJhscSvtr+7/TO+/OnTnnzDnfOTN35twlrSC+dSPCHSYBNL4BQqIb0Dq0jG37c2jWrSWxYUraQMzCBLG7jEXvvcf0Y04GDb4i6M8D2nFy8VheXySBYgQb0V/PKb8fQuNb0MCfydaTJgOGDOLWPvGsGf0u4/aXEdOsLSE5BzhYmcSQwf257poB3NErgeKiMnyOUNolh1NdlMaCKbNZdEwPrElYcmOSw70Q1oDEcCf6PwG3XK06jcP55Rxdv4mtp6Rj0SMgwb6AM4wWrWLI2L+Pkvh2tEgKD0KoDaI2CaYBEaL8Bcx55y2mHTEYdE1/mUOXMWTIAFqL/+4/VACC/8FF43htQSod+/dncP++ovdlRJ5cxu/fX0OBzyAysRExDr8Eutbw/qTVtfMgnCZNU3BW15DSLBZfbjntBoi9V11ObMkpdmWFcO3Avlx3ZVOqZF41bdaUiuMHyQxtxvUy5269pjO5aybwt/nHgmOgfxXiHtUUZJ3myKkDrNx+VtcSHt+Mbo19bNt6gMr4ZNxSq9FwR4bjra4gqmkrUiKiaN0onEM7jhDVsS96Xg+97ipah/spLKnGHduEeLdJxs61fDBxBQUB6cwUX2/eGFdVNQ1aJ+J2BDi8YgIvTj5Is75Xcs2APlwtug5tb3DkcBqVjjA6Ngnn8M4jOBo3JSEEfGd38uo/ZpCb1IWhYu9VA/tzZeMKxr7+PqtOy0wLiaNJkxjKTx+TYPk01qZXo5QipmEL4r2VhDdqRFIYslaDErtyC/PJOpnBpnUbOaOXC10r81KcnnbNYySQvx9S2tAizik8prRiJaWEW8Y8KqUll18SwfaV28n2hxLqNCXg5iSiplieI+nRuTWRCvEXqTchpmkLIvIPc6A8zrL1lhuvZVA7F4vf+zsvzjmIFyeJLdoQyDzEsaokhgoe1w+5mqt6NqcmLz84jywNRJgjkrYdYji95wg1jfpY43z9tZfTqZEi75xlDBX5pRRkpHFg20a2yYcsU1tg2RdGm5aJnNy9j5rEVrSMD8GnB5lyzpZHMmjQQIYMugRP2h5OBVrK2PTlxo6xlBWXy4oi9uSeIbssk+2bNrM1y7Q08vtNBBbxuw18OP+QrPf9GHzl5QztEEpqRp58YBEyC4sApnLTrHEirupzrJk3n8nrM9HMjphk2iT6hTae5nEOts75hJwGvbjtyobsWLORUyWyyvjy2bxqtXwgXc66o3nopJQILjnOuMmf4Ox8pfhGY7bPnsryNDFK2vwyVnp+goc1E2ewy9OCawdcxjVD+tO0/Cwn5OOwMyqJVuHl7DiQSwfB/bprruQWaW8k/eUGQFHJp5Pe4a21JfS+6koG9+/DNVcPpHdMNftPZBKR1JLkimPsznHTT9a1G2+8kga5G/jTu2soETWc4TF0SIK9u1Jp2Le/rFX9GXbDYNo6Kykoy6PIH0nvAQO5+qpOVJ08yClHU67p349BbRpSU5SLD9E+r5jCs2nWeO7IrkEQRxwTy19bxnNi10GM5j1E9iCGyTr56ej3mbpX1h0MopObEyofE/dkmnRqFodDY6L/7DhQRlpBHqmHj7BqQyo6CZr6BoKdEjp5CdOiZRyV6SeZN20aK4+XS5NBRGIzkvxVuFJa0NCZy6LVewi7ZADXtYWNyz8lS+w2ClNZsXABE+euY9/ZcoLJZN+C6UzeWcNV1/TnmmZVTJWPvYcrpFX6NOVmXzYCNgI2AjYCNgI2AjYCNgL/iQjYOtkI2Ah8HQLG1xF8P+0Kd1gMSYmxJCbEEhEWQkSslONjrbpQl2F1YzjcJCYm0rBBHGFug0Yte0rArzuNkxuQHEhn1W4f9/zoXi6JdYA3h9nTluLrcQN3921Fo+REkhsk0qhdd24e0ptGbi1SjovKQPmz2FMew8133kxS7j52ZfpRDiXBDzCcus8E4uPiaNy0AY1SGtK8TRce+smjdC/axOilh/AHPCycthJXj8EM69GEhtJXw4ZJ9L7hZrp4DzJ5wUErAIIEGnSvhmWHlhlLkzqZrTvz4I8fo1f5Fj5auBu/M5RGDWKIi4unUWPpV+Q1atSJYYN7kRyttBg5yCqUlHRcpFm3y7jv+u6kRDqsOqnGcIXRIDGOxPh4GjdKolGjhlx+9QCSS09ySALAmsY6JEuh9NghYgYP59bmfjbuypADOhjq4uOsQ+Qlibz4eJEZFYo7JJSo+CYM/cEPuNK3k7dn7ycgB/PQ2FiSExNo2CAcR2UBB45n0bBtJ5o2TqZJsx7cNuRSscFF016DGNK1IQ2TRbekWBnbJJrIGDVv04OhV7XDAdScPATdr2XEpQ3Yu+0wEhbDYeiohoPwqCiadRrIz37Qg+3TZ/DpOT8OZwgJifEkJSXSIMQgde0C5qU34P6RA2idkiR9JZKS0pj+115DnyQnDne46BpPfEIcifJxw9CWiw2UnSPbG8mt99+C69h2dpcrxFUIXASJgyixMyEuluSURjRq2ICUho0ZIoHuzk2jQQKwUdGNuOvh++jLXt6Ztp8aJXrHxtAgKYGk+HCcCknWj4xXKBrfOJGXEh+JWwMgreigstzLdi9nzsEAN40YTCvxh4aCVaNml/DADZ3Yv2guWwo0LmJBrY4Od4T0E0u8zJlGKcmCd38ub+5l08Y03BGCQ0IMCUlJNBdZjSQ36XsVw7o1xCU6RkdF037ICJ7qH8r0j+eRVq1whkbJGMXTMDGWUD0/pDtDKXL3n6FJv8Hc2N3Jlu2H8EmdYfoxDQcRCQkkxcWQJPZGhjj5bNK+a4j56RuXMmt/GPc+cI2MUwO0bSmC6ZUDr6RPyygo38e4Jce4ZOidXNqiASlie0rDptx01zW4Di1j6u4y0S+WmPhWPPL0cBJPr+KjlWdFByexsXEy9xOIjXDTftAgBnVoTor4XEpSrGDTAEtW60u5+fLWhGtM4mJJbphsjWfTNj0Z2CWBtH2p1GjlxTZRl8qyHNJruvKzkZdweutOCgRzZ0g4HYbeyOUpNezfmy3+qzCAqvwiCGvE4CubEOZ0EZ+USKL0kdIoBe0zjVKacePQy2mZEIrhDiM6vgWjnh5B03PreH9JmswpJ9GxolNSHEkxoTgKd/Lh3CNcctPdDGrbgBRZ/1IaJtHqiqu4rmsT3Hr8ZMwT42KIT0wixOFl5YKFnIy7ggcGtkWvhQ2Fp12/YdzUpIDJ8z6l0uEmPjKKvjfezUN9/Yz7aCm5fgeu0AjxoQQaJMbKmIOSwVKBanLTs+h2/0P08pxg7Qkd/ULPHCFwEiNzQvetfTzCrRHgoqTkyRUWSasBI/jx9bGsWbKBLPkI4ZCA3qZDxfQafBUdUyJxSgBPaWKhd4fH0lDGJjEpmRYyz5o0TqHbwJt54f5L2L9wNotPeQmNEHsFp4TEBoJrEilC17NXX67u2yKom9ZdZKGcRCUkWGPQICVF6JJkbWzF9YMH0K1VqFAEyM1NxdF7JA90rGLdpjPn11RkVKPO25dIpKwxQR2jGXhtf3pa45Fs6ZrUUGSLnza+9EoGyzoXToDTacU07H4Td3f1snHDcelLcNN/bi6lTAlSnimJpFPHRjSR+dqy/1UM6y3jKW2IBgqdDMIjwknpfQM/Gd6ZLRLU3Jzvk3UslLiEeFnLEolwe0nN9NKmSztadehOgq+CIvm4gSuOy4cM5cauKYRLwB9JWmbu7m0c8bZgUJcGNGp6KZc1rODTDQdEWzD8fuuOmcXuIzlENW+HXscbN2rKdcMG0yHCsNatBoKJ9Y5sJDIE9yatWzLkustoYkCKEGu0AAAQAElEQVT1oVWM21DEtfffRtcmSRbeDWV+XSrB6kvbJOAMjSRZfDshIYmmDZNo3Lglg/t3wZt2kPQqE0N8M1HkJ8TF0sh6FzYQmkYMufEKGocl0Kf/ZfTt2ICGyQ1JTowlqWFD9PpxyeV9GNCjLWHyaeNcbgYhfe/l3nblfLL5rKBJbXKcX8NTGqWI3yTTus9lXNqokt3HCoRGyRyIJikxloSEOGJkDVHiTQ4ZdG9uIUXVDRj18OVk79xEasAil1a5W5dCwCEiIppeQ27n8atDmTx6EWe8hsgMF5lxNBR7Q2tKyKl00q5Xc9r3aoG7OpfcSlCxgvEt13FluzgMefGYSApks21HOo16X0Fr4W115WW0qD7BJ/tKpVEui0ju9mUjYCPw5QjYLTYCNgI2AjYCNgI2AjYCNgL/kQjI8fHfpZcfny+YrR7l4OvTWeqs59ofn88ndD6q5Tk6TgdytIolTPtgFqWXDOHu7vHSAjXZZzmU4aJvz6Zywjcxa4O/ciIksXkbWkhMS5+sHcJelZoD4W6adexH1+gitu9OFRkKw9QnSqz+9F97ej0iR4IiXi3L3Yirr27Jmb3Hycs7woGCcDq1aWb15fMHMCWyZhJDrzZxZB89RB6glPATTNoOv9hXX6bpasg1V7chY/dRijSZBPF8cvCU87c8VXJw136q4qMIC1MXBUKVMnAoIRFaU2cp1l0+r+AlOhuOYE3W4QMURcpBNyVMKkRPNGM1B47WSMCiEb17Nid3z16yfYAcsoWV+skn46H11s26vqaiAn9MG+59+BryV0xj5nFpkb5qpF9PtZyGwxvRszFM+PtfmbJ8F+lFPhokxxEe7SZCgmMx0oH+a2It1ydjWyPPEn0nuUkDnPjYe9RD09Yp9Lr8EglG7+dYichUDsEXCNRQWuGn3aC7Gd62kA/GrKRcqvF7ETV1icO7ThLT6RKahSE8PrR43Z8R05R2jV0WjbbHLwyiuRApHEDuuQoCATdtL+1La0c2W3fkS61C4yuFC5fPi9cvfmI4rbpzR3ZztDqaOAnQIw7m81RQGd2JRwSf7FXTWHC0DI1rjacGgYjPJp/oofWp/VdUrGZxJet+6GAqZlwLOsQgegREPz9+MSiqRUuahOSx9XgwEGgR1/6cl2c9C4/cDUOcXrD1Ct7iLlIDFWfS2XuqnJSEMCto6he7yqvdDL7vXnpWbOXdeTpY5sT01lAjOmqmgNJIlbKvwEGL5CZc2a8Vufv2caYalDikoALSR3AeW+hyURLdA4b2vyoO7z+E0bYHbcPFNpl3AWSuSHtYZDItm4dL/PkI+SqOS9qHoccvIKDouWHGNKZjioNDu9JFtMJTVYFqdgU/HNGFLVOnsq3Aj8NlUuP1WgHkyJRk3KKYqeeWzD+NtZZnmmE0biid4xNav4ycIfLkkoDPnpNFNO/cEu0tmlZqKc8+RmF8Fzr3uYSYkmNsSPPpakyjCTf3SebQlk9J94ASSaePH6E6pQcNtU1AQPzTK2uI6XTKUw0nD+wj04gjNkz34MdbVY6Z0pdn7u/F3plT2JDjxXCbeKq9Ig3B4iA5zgb06BwlWATEB0SMYGUa8bRtGU8I4BPn8gn2fj2a/jyOHiumZbcW1l9vmgE/+v+ETlho160ZFSdOkSm6ulQ1Rb5YbrrzXlrmrOWdhadEEtTUePF6/VZZya+3upwzZwJc0qEdPZq72LvpsKWXbpNm9D/D86VjrgmUwpTxCwiiVw6/gxZn1zB5fTbFGYc5XZ3IZR3i0O2mUSdRM/llvvjFnXx4RHHT9CE3Ei8bRLfQCg4fyNBEYncQI+vh3F42pHolCB8ja7nUqHryxL+9Mm+VrJ3SgqfgNKeLoyWAGSL6eylOyya2Swv6d25E1r5tnDPhPLvgWt8+va4jYc4mKdHo+WiaNed11f5iumNpkhghQBZxrrqCiJT2DO3dkLP7dnHGB06n9jdo0q4DUVmf8OvXZrFiZyqVRpx8kIsSlKRz6iUZv4pSL11uvIXbm2Tz4dhPkVUFh9hU45WBJJyBlzdg19wZfDRuFiUprdH/RYNMK0uIKYWABs96gtQzeTjiooiWOrlolBBFYdY5SqXdsllXqmR6tnWx9L3X+WDBBo7mVuOWgG+KvIuETJZcH9qnDP1AAZ9uPo4rLplY4MDOU5jyjuiZgoxZwPIVU8uMTaZdSpxQ+PGIf8m0xCFP4OfogZOEtuhAk9DgmOm1yifjhfVYyebtR6gJiyM6NFoCubG49dzye/HJ2uQTHCz5EREkJYgGvhoK03OI7dSS/h0bkrl3G7nSj2Wb3MVp0L5ArS+Qf5pDeZF0a9dAt0oO1MoNzgHT1Faa5JTmUmE2pttlXUmsSGPTIVn4REFxbeG5cAVqZF7VRHHtrffRpXwzb808bDX6xOaaSnljRTajd1MXq8fq8dpBaOvetIsy8SunRadt0dl6wIt+RzpDg20QS6M4BzlntEWCr+CAnWwEbARsBGwEbARsBGwEbARsBGwEbAT+gxD4pqrok9Y3pf230indmxw49S1j7TwWnE1g1P0DiDT8BAImlVWFlBkRRIXIkVYJtVzIwTsYTJFDnARo/BhyXPRyNKsUwx8i8Uw/HTokc1oCvbk+UMJ60dFfKZSSjNInPUJj4nCUV1JZVoJPuQlzS+hH2qnNCpPICDdeOYCW6tO1YbFxURJapRTB/0GIyDREpkfolTMUZ1Eqc6fN5aP3pjJvaxrVYkPgsyfciwRe/KBcbny5aSycMpe/v/JXXlpZzqgf3k+XWAc6CKEMBYWHSHPEEVlVTUzr1hJQO8z6VI8oq0SYKfnLL8MB1eU1RHe4lkcHhjNrzDxyq0MI0+djCZRgRDHsiad5uHcYK2ZM5Ge/+DOvLzqEPqqbYotfWy721/WglPQp9V4ZQypOk+pzESZjZSQ3p7k6w6cHg4FgrTaGgRHwUOWMYNhDd5CUupQx2wpwh+lxEPVFaJEEvOMiBUcxQ5kK6Q6lZGSkD59PQOYzSWSKReTkZ+AJS5FgoIsurSM4sGsXRUKq//r6Yi4nYaqELcvnM37iBEYv2klmtUndGClR1CtBp+jmQ3hsYAhTxy4mo8Il3zu0DnyDZGKK3hqrogovyh1GqLZFKcsO+cEMcRPm8lNWXP0F8hQBCRr7qqrIO7iFTWmhXD2gpdCJLJdbPjas5+Pps/nH+EXsz6zAL76l7RO1hc+DP7Ihjz1yFZlLJrEqvYywcLcAKwqIBD32ZlYGZUoCP84AoY1bk1h+nG2nSkA5ZHbxJUnbFJShlCapobzUQ0RMqP6XNrRJKP0/aQzIHPeLLxRWiG0qhEgxXqpBFNTjqUw3oe5wqkuKtSBdTXWVjyaX38ndbXN4d/xGCgMhhBhg9RjwWXellEWvf5RS0psEfDRBQBES6ufIp6v4eMIEfvnLMRR3uJXnbm6LIT6DUEokUQJkZaQ09lLhaM4lyV62bjyK7kGJ9A6DBxKff5L1R8ulrpDDR330ulzCz4KtVEgAzkmoP4/Vc+Yyftx4Jqw5RIE3QEDsRCfRp7qqhuSetzOyczEfjF1Lvk9scJgY0l5QVClBywiiwk2UEt2VVOq79O3z1donVfoyBCckgFxepYgOdyAMqLr/KXBERENlJZ4qMAyXmFaOI6olT0vw+8D8KRL8DhAVZohfmFhJbp5zB8gNb4JTxqRdpxaUHtvO/kppVYgGcv+iyzTFjy80KK2X4OFO7MKou3qwf+L7fLAinc5X9SPOERA5SrS8QF+/pJS0KUFC6f4iSYz2UVFeapGEiR9m7vuUMVPn8NbEZezJ8wquAeqgtYj0jwT3Qn2lbF0yTYK0U/nHR3PYkukVHU085Wc4UhhNc38VIW06kFB8ko3HZD2UPgOa9wuzSUDsQbRWSlGXlJKy1Iv5lOcXU5hfQ1RoFc7mHYguPsE2/dfjMpH0P1kT3rw3v3j+AZqX7GPsW2/y1C8/4JPTZaBlaAHUJYWSQGSFiuTWR+8k5tACJm7NxRkRiqFkgISsUe/B3HVNL3r3HciI67oRLUuigCotn7+qqz2YTrfwilwFofLO9MgHm2pNKuNkCNQQxsB7H+OZ65PYtnA2v/3ln/jzlO2UWP9cESinQU1JGrPGz2P02CnM23HW8pmAfOgprKhCRUQRLuAp0VwBSsmvzKca+RCDKOYICaHk2A4mTJ7FH37/CvOK2/L840OId/qlFdHNwKzJYcnEuYwbN54p605SLQHpgAxsQOOLQoukNimlQD4wmYJbjYznscIYmsl4hrfvSGzhCTac8ELteAZMJ2FmERuXLGDMR+/xo1c/ofXtDzOiayx+nwmILC4kYRPZXvJTMzAaR1PhbUiXJrB7y158QmaIXZpLitalHA6Ut5xAeGOeHHUFqcunsvKsX+avE/Q7khC6XzWYGwZ057KBA7nxylaijzRZ3LU/0qmlhdGIbu0TObFlC2cF16qqUso8fpni4p+1pPbNRsBGwEbARsBG4EsQsKttBGwEbARsBGwE/qMRsI6e/1caqtqDn1l3mqsrKKxDqelw4S85xEcz99DhluFc2cCJz+/AMBQhzjBCAtV4/H7kjIqphMlTwJbVs/nZz17l3RXHqBC5pqeU7IxDbNm3g6lzl7A9N0B55gn2ZVaBBCn4kmRi4quukkOlm5CISAw5elb7PBLAuMBgSjCrotovwSInEsOxlBYt+LKkD8tapkQnkfO8HHJr8Ec35urrr2fkPTdwfc+mhDoMsc9Ri8yXSbpQb0pAyohLYcjt13NP7yiycyqJbBCFwwzI2VdhiEKZh7LIPbObGTOXMH1dGqhStm09iVkbzOArk4HD8IvdTgaOHE7Xqp18NG0bVaFhGBp44Q2NacLNj/yQcR/+nmeuacCGaTNYerIUpQxU3ZgK3fnLVLhEseK0HHJO72PJwiVMWbQXj9PL/h1HrOA1ojcmKMPAX2PiTL6UZ+7qwrZpU1mRVkO4S+lmQsKgwuPDp23RPASTkr6dTkfwof6v0AQ8FaTuO8bBvavFJxZxqMKgOP0oh/U/zCl8Al09Dj8eM5KuV1zFXXfcwp2Du9EwRGGIXtQmJQym+MKA4fdzadU23pm7nSr9caG2/atvCodDoZRBhEvw8lXiVRLJoS6ZEtzw4fUbYqu7rvL8XTld+LKPMH36fGZsKeHWp5/hrkvCpd0v4+8nvkMv7rr5Bh689SoJpIbgMAwMaQWFFKkWbKO7X8+oy0OY+MFcjlQ4cDuwklP0OJOVx8m9R1gybwkzVp4AVcbOPafwCn8tGZ9PCqWUVW1av05CJOhVI3MFa9SsSuvHkDnuEF+IDHFiKh+VNcFxRTOKiIDUef01YnuERa9/lDybZjh3PDCcxKOL+HDlSYxQt25COubLkqWSYeL1GLTqcyV333kdjbz5FBlRJEoE2zQDmJqo/Bi7z5awb/ECps1aS7EJ2Qd3keoThVAoCare0D7Apq3HyD55mIKULnSTwLkpdEo6F4+l8lPMgAAAEABJREFUxhHHZYOv4a67buHWK9oT4zQEb92KSJAc8MqccnPz/SNomrac95ceRUBCp3D5uuML1FDtqYeFbjCVrDUui18/6qz71ItJaIgp80D0Fx10vZWl0S+RZ0OCfxJ/RK9pShnizyYpA+/k/o5VjJ20lNMeN2EOiwNT+TmxO5UzGdsYP3cxS07KOll5hs17i1DCW198kCP4awpucgUfrF+FDCt+06DT9dfSJbqMXFcHrmjkJiBzxdCNfFXSPelcIwFAh4y/THSQ9d4guV1v7rllKCNvHcglCW4Mw8BhSGP9SwKjNc4Iug4QOhmDEUN6yrw1UfK/smP7OZyXxuwZsu6sScM0Ktm97Qh+dHt9IfXLyuqHL0wiVZmU5B1nz+FzLJ+zlClr0nBSyv49J/BJnxKKFE4XyZdczi9fepHxrzxML+dJ3h27hhy/NAl4ptwuXApfjdQ06MuTd3Vg7fgZrDtbQ2jt5PQKT2KzNvS8pBlRIlwoQWTwBSnE5ZS12if+phtNanwBXA6HhEVBTEa0lwI4Ixsw5J7H+fiDV3hB+jy2YjpTthRYbT7NE9mIIbdfx4jhNzG0awouGUNDOYhwiALeamoMU4uz6PWPqQxZS5QUFfojWUSLrtxz240Mlg86Z0sMEuPdnP8vl2TuKVcCVw67nrvuvoXb+rYQnzQwpA+ltAwR89lLqpVoX3J0P4fyU5mlx3NthlCVs2vb0fPjaYhPVxNFz8HXSf+9MfOzMaPjCHWI+QEh/4LLrMli5/5sjmxYxPRZyzkn62/uyf0cKQcxuRZLzidlONB/uRzf91Ye7uVg0sSFHK50EubCSqag3bRte7q2SZZSECdltXz2x0n/4fdyS4sSJo6Zxey1+8iWD54hoSGfJbSfbQRsBGwEbARsBGwEbARsBGwEbARsBP6rEDD+5drKyVhJIAQ5KCrpzeFyYyVHuBzQ/VR7PCg5iZmm/EjBKwFVn+mUNlB4+HT6HI5GXcYTN7aU06KJoUo4drqIsORmtIgp5+DpUqkTWgk4EJrEwP6toMqkVfv2RMsBs+BcFpWh3Xnh6eH8YOTtPP7UKK5tUsDybWdBepBe0UnVFfQRWvRUcmjNOJJBdOvmJCa2o1V4GaezdBAGOcz7NRXK8HEys5joxm1pIELkDC2/Fy6lLpSxZAbIOJJOdJsWxAI+OdQjAbiIsHAi4xvRp19vmpbkkF5QhYaDL0j1RQabBROnG3dIOI2H3MOw5HRGT9mERxkoyfjz2JkXwk33PMzTD97Bw/eN4Ge3diV/70ZOSLBPy5MhCoqq/dV1tcXgTeRY/xl6ZDueeqg/6Wvms+hgBVFRDgIV5ZzLzJKApInDHc/Au+/l9o4OTmSUWrz1ZdWVA4YMDBXWR4AeQx/i2Ydu5+F77+bnDwxCHd/EziKLtfZHoYMQpijZ7oa7uK1JHpOmfEKuHOMNoEOnhuSdSKUMkS7BCjNggtT7PIWkp5dJSdxGfqVVfoNXSdZRihoP5VeP3in93smTz/6A/qEZrD+QKQRKhipA/WQqB2FhYURExNKpV196Nioh9UwJpvSplEIphTBhxrTmBz8YSPbS2Sw7UY7bXV/KhbImt55MUAQoyD9DZr6fThLU8RbmcNarLQO/3xSxCl9BHlmVUXRpF8Fnk+mrIaR5Lx57aATPPHoXV3dKRBQDkaznnVN8Kzw8nGZdOtG7UwsK09IoRCclPwqHxoxwrh1xD50qt/HhouMQ6sJKNaWk5VVwxX1P8Mx9t/PQA/fw7N19yN26jTTrzyeFSmyQrqQQvLRtnqoy0k9lEZAHw6cJwmjdthVVp45RKHUIg6kHFKiuKOVcVgnxl7QmwltERmZAWpUEzwOI8RjeYjJyK2jeqalQS51uFZ21P7qb9eHpOzuwffpMtuSahPL5pD5fJfAYuFwuIiMa8tCoq8hcNoOl6R6UYaCRP7MtlWbX3yK+PpxHxO5nn7uFpPKTbDqo/wxYw+uW4HJfOLiEd+ac47Ir24LohbrQm6kcRISHEhGZTK8+vegYW8ipjApAaPRVa4OzUU+eGtGNfTNnsjEnYK17SZ1aE1mVzakzPk1NQBYWE0mqkoz0PGoEBqVEiMgI+LwggbsWzULJOJGPrkaaglmRdyoTZ+MUUsKhxhtACQ9KSQAtkmGP3EOTtE+YvOYUZohbOgBVdYK9vo489+RIHr//Th556GFG9Qhn39adaOsNICAZ3Ye+SzZFnio5w7HMcvzyfP7S9VpxRyRx0VHExscgHQv+5yk+VzgvVvxDSSeq4DQny9y0aNNEaE0CysAZGi5jF05ih8sZcomT3Nx8CoNT/SLZegxCwyOJjIygSdfLuKKl2CgY7jzu4r7HHuTRB8W++0fwszvbkbp/F8cqdY+WikjX1CVT7KA0k2PyUULHhIVCmpTk2ktjKh9DU09UcN1zo3j0vjt4WOQ+N7wjp3bsJaNK6BwmuTl5FBRVW+zhjbvww0duplFVOmd1u5BcdEmfDq2E4Nfx2uHc0ugMH07dTqnhssh0l3oOBQQnIdGUVn3dTz3tSGkYj7+0ggqrUlFQUkVEfAJRQiyuFcTMW0DquRL0K0m5wukx9AHu6xtPxmn9ntSEJoa8qyKjBfvIVlx3VUeqKgrJyq3ikp7N8OWlc7JKIWoTkDXY0qm6kNPZ5QRQMq9MHE4nrohwrrz/blpnr2b0mkyUDl5jQYIynETGiPyIxlwzsAtuRxmZZ4uFWwi+4DJNA1Q5O46H8sATF8bzp7e35tS+XZysFn2AgFZGZLtl7xGZdClP3tKU5RPmcMIDopJQBC8lN00qN4qPH8R12XB+/ODdMpZ38KMfj6Rl2Wm2H9IBeUOEBoK4aeLarJQS9w7l2ofuoUPeRiYsO4JPfyG12oVexio4XupLbRIBqJAkrrt7JC88+wD339CbWOWmUbNkgkkFb/avjYCNgI2AjYCNgI2AjYCNwAUE7JKNgI3AfwUCcpL61+rpLy9h54ZdlOGTYJOD1h2aBTs0GtO/azTbly/nYKEPl0vhOXuQBXvOktikjRVMytm7gnEbqrnnkVtJ0ZrKAc8oOMzyTacIRDTjnrsvJ235fDZlelCGw5Lr89Tg8fmx/s1HOSJmpx+jMrkjIYEAHo9fDqNR9OzSlPT1Gznrs1hATuFerx/TOtspnCrAyU/nMC+1ASOHdiPEFcVNN/ciY90Kduf5MJwODKXI3r2M1Tnx3HFLD9wiyjSM8wdLdZFMRKbJ6Y1zmXMygXuH9ZHwKUg8CJ/Xhz8gzPj1Dzn7trP5VJ5VFsVq78GbJdOKEASfrV/pxye66zgUxHL7fTdQs2kh0/cW4HBIgC8tlWLDTUycg0DAKzlAg56diJXD9No9xSB26H8Cgbok8ry6j3rP1gEeJXqaJPW8gXuvaMS5k5kQYaCKz7Bs6aeclsO+ZvFLICLVE0OXZvH6kboDvenXY2KKFFCilz8/m9wSD3HNIkQnL/o/1Q5t24EW7nxWb8ggmExps8BBBxwDxIl9w2gRyCSt2LRkd7zudvqpw4xdeAwPBoahLNbUXZ+y5IDYJ09+v6/WH+RBuNL3pRLRsYUMe0B8xSSg4ujVJYHdn+6mRIZBaRmmpg1mv9eLLzhIwVHKOsTyHalSVhjy4SMgjmP1KjYm97ye+y6PITWjSOTy+aTxlfFCMygltxrSDu1ma5qPhD430j8pnzlzD+CRNocMoPIWMmfeZqIvHcrgRk6QIIY0nZdrBvx45SNOdcAUrPySLyhuartlLlg11k8l21bt4IxAqpRpjacyEB2kIq4Nj44cgHkmg3KxB0lludmcy3HQIkVkS6DbK3Mouf0lNKo5xvI9hUKhrwB+8eFALY+SqrK8o6zddk7kIjlAQAJFHQbfxMCoU3w4W2yTWkMyklIPbmXFlrOQ1IcRfWNZP28Z5zzgFNsdeNm3cAWpUX0Z3i9RqH0iKyAQ6F4UAiVthtzJze38HDtbiaAj+AiZdel2v4y7ZbhVY/0Ia8DvxS/jpp+jugzm7m4+JoxeRk7AkKo8Nmc6aJMQRkDs1XMjENGFXgkeNm/ZTaVQiAsR3+kyukdnclw1pnOCS2rNWouQu2nNa1/ARCf960/fzdK92fLoEr0DmBZeoqN8ZGgx6HZu76TEhgppl6vZlTw4oAGrp8/nZLnCqdWS6kDOAWavPYymMoyA9CHOKnMawrnupsGEHVnJ8uMSiZVB1S5clbGdmdtqGHrTICT0i/4DdP2BRotT4heuxK48ekc3KrMzKfI4pAfI230cs0lDwsT2gPhWQGzo0rcdhYf3sSdH+tNUUmeNuViqHx2CaemxPWySdUs8Saq0xXI7f5l4a2rwSp/Ud97z7cGCKfPHK7KtJyWjWZXFjLHLcXS9ips7RQarZdwsOfJkkXry2L5tBydKpAJTcNV3nU3LL3218xZHOJHhYnn2Hk6FNKSx27TGNyB2JnbtSXL+UTbtz9eMKKmz7Ks1Q9tXcXI/m45nYf0RvFAFtA8FjUXMx1t0jENlDekWGbD61XIbtOlNUuE+1hwpFQ6DjD2bWLbxtMjQHJB2PA13oza0DJNm0V3/BrMpuknnmkw+hPpDErhj1DAalGVTUFX30gKlFIZkTUa9ZMq4ncdR6pt070pixSkO6Nip/wzbT9fQq09Xa74EhF96Al8Wixet50Rh7RhXneVUvqJ9x+D7Wol+Xmuei0DrCpB+Yi+b9xUR3+cGbmpTybQJWygKKByytmidSg5vZMm2NBQGSq9FMhY1SIrqyAO3dWXXlOlslfeppkUms5YvLiAEwSv/zH5Wb5U5IzrKNEEEERA5IgadlB6YTBnP0BSauDRmAcEtQIPuPUnMOczmA8E1Ssl4+mQN17yar6N8XOrr3MM70/fhNazeMUVucM+gKSrZuauC1u0jLXkaS9PdlMtbm2zYtK92/umlxkIOXQqIM2pJel45Yjryg7v7Esg9S75H/FgoEOWVUhiSNR31UkCMtuzTdcrHoV2HyCisQAerMzas5lhIe4Z2j5RWU/jlZl82Av/BCNiq2QjYCNgI2AjYCNgI2AjYCNgIfBkCxpc1/PP1wcOZ4TbIOraRv70+neJLruf+K5JFtCnHWRcD7nuYUV1qmPLWW/ziT6/z8ox9tOo/hKs6ymEr4OHInkN44hMp3LaUMR9P592PJvGntz+hOi5WAn+KFgPu5o8PdObI0rl8MHkBk6bOZfS8dHreNJQrWrnIObiJFZ8e50xOGrlVipAQB9XFmZyWQEq09wRTF23mWMZZNuxJQ7k8rBo/lXdHT+Ifb49lwXEnj/7iSa5tFYYOQrUePIJf3dmGrfNmMnrqIj4eP43Zu0x+8NxjXNXEjRgkR0wTZTioKTnHml2pKHcNqydomZNF5hjmHjZ4+OdPcn3bCPwlqazfnUWoq5Rl06fw7pjpvD1mEm+tSCU0MgJDUNIyTbkr00fqns3M3JaBUsV8Mm8Nu1KL8Zad4RMJNDspZuPK3WRV+IlqewzO1FIAABAASURBVCUPXNOQddPnsnDnQVas2suxY2mczKnEMJwYEkLbfzgbl2Cxf8VC1p/IxzSU6G1QlnOSLUcLcfoL2Cb6nzubyrZDGZzcu4ODWRXCr+RQ7GDQHXdyW59EqspBpTSieVQFs0X/MVNm8c7MfbS/6XaubR0utMg4VXBw+3p0vFLln2Dprgw8VQWsW7mR3YfOkpZZiF/0cjtqSD0qbc4wMrYsY/WeI+zedpgTmafYuPUYZRLYMCSQ4GzYh8dGXUfLED8BwBXbjh/+YhQdq/aKj8xi0qxFjB83mXn7vHTrmUTluaOCVQWO6jx27D/JwV2bWLLlDDmpx8jzGYQ4FZ7c0xwvchOaf4DJi7aS7VGCh5IxLWf7pj2Uud0cWzufd8dO5f0x0/jL6HWUh0bgKTjFjgMnOXB4PwfzvcJjYJphDLlvOMO6JknQRRSUy9TBCcOgIi+V9YdyCVGlLP54Jh9OmMkHH0xk3tY84pNCMB1JPPzjR7iU/Xwo7RNnLuD98Uuo6HQTv3yon3yUMTGVkj4UDgdUFRxjf3o1od5MVq0/SKHPgSFjqQNAWQd3sVds8qdt4z3R+Z0xU3hD/Hp9SRjx1SVs23NU8D7Epn05gqPW2ySh7w08cUsnQrRw7zmWr/6U7amFZJyRQL7DjVPm5MmTGZgRoRxYuULmzUl27T5GESFkrJ/HRxNlbowVX56yiQqZt4IgAcMQnzVxRDbj8V88Rh/zKB+JbZNmL2Li+Cks2l1G685NwJT14IEnGNXTx5xJ0xkvto8dN52NNe34+Y/vpnVEgOK0/ew/mcquvftJLQcltgaMWO585G4GNg+XDwKAzD+n6eHE7vXsyjFxlZ5i4ZYTFPoNxErStm8jrcZJ8eHtLD+Qg2lEcuMdt9CqfCdjpq9m1bK17D6dysFjWRKcMnBIH0XH9lMcEk7piS1MW3sSj1f6cSdx6YDbeGhoa1zyqJ1dia2mt4gNW47gC4XtC2fw7pipvDd6Kq9O2o4jJpyKrIPsOXaa3fv2cbLUDNqgorjt4bsZ3DIKU8syQ7nyvkd48spwlk+bzsfTFzNt6mzembGfxLbNifAXs2mj9BHi4MSO3ZypgJhLhvDCE4M4u36hrIXzmTh5Oh+vOM2AHzzByN7xmJXn2LnzCMfTj7LlhMw5iWoH/IrmA4fxwPWdiXH6qcw5zIw1xzl3+jjHSw0MwdLw5LIjtYpYo4hlc5dzODeHbdsPU+V2iQ/Ms8b8w3GT+PvCYzgjwoNYmGKEqX8UqrqY3es2cs4fQtnJHWw8fA4r6CcBR01hoiRgCTnir7vznQTSt/Hh6Gm8/cHHvDZ6GWUdb+H3Twwhzukjfd82jpc58AjNOzIX35V3whtvz2Jrpo/EBCQFBD8FgXJ2bT5OudtB+q51bEstEpzBW5rJ4nk7SM9MZe/ZCrFP2+jh4N5zRMbA3k+WsPZEBlu3HaRCz/l1i4L2jZ/M3+YdEL+PIiJQxf61W8iojqDq5DbWHsyhqiyLZfPXcSQnm6OppThcTln3ajgp4+yOc7Fj+SLWn6qgU+eWVJ78lHfGzZE1aipz0+O4//5riHcgcxqUMlBmNQf3HOD48ZNs3Z1KtYSJDcEqvNkVPHXvFTQIE/u4OJkW1uCrkHV7zSo2n/ND7mFmrdxFamENjpRePHzLJeydNYV3Pl5NzJW3clf3CKtTh0NJnyIvrBk9kyuYP3mSvN/m8M74NUT1u4N7e8VRKevWxsM5uIwS5os/vzt6Gu/K+ExYeRx3UoRgHsPwZ37IXY0ymSJze/ys5UyeMpPRGwpo0yaFsowDbEw3cZScZNXaE5SZBu2uuYHrW5YyYeJStsq6v21/Ori8rJ061Zoz7340nvdn7sRsECfKgfIWs2PVZtJr3FSf3MVa0ae6PJNF83eSfu40e89UYsj8M4xqGc8somIC7FqzhHXyoXTntgNUutwc37KeXUKnxNaRdw/Gs30xk9cc5FTqAY5keTFLzrJp72kObV/LimPZpB5Ko0JkugxFefohMswofKm7mL7sIKUYGEpBdQ47thzieMYxNh/NxSvzSoaLlMuG8gP5MB1j1qCTUOpbbTZBeP3y3lm5ZD3Hyw1yj2xl8foDFMl4q8JjzJgymzHjpjD3aASjnriV5m4lPnKxFOxkI2AjYCNgI2AjYCNgI2AjYCNgI/D/OwL/VfYb/zptg4clFRLNzY8/xx+fu59HhvUk1qF7VCh9C2nAtff+gFf+8GNe+c1z/OGn93JDjxQJYsgBzQhh0EO/YuZbP+HRe2/j4YeG89Sj9/HbP/2aX9zUBi1AjrQ06jaARx4dweP33sx9I27jicfu47HbLyNFDmzJna7gp7//FS/c0Y0GEVaPhMc15bYnn+PjD1/m57f2o32zJlwz6kd8/M5v+P2TI3jqkfv48TOP8NNH7+DS5pHBQ7qh0H21uvRqnnpsBI+MGMZDD97DM4/eTN9aGq2PUsE+3DGNuG7UDxn7zm/5w1Na5r2WzJ89dgd9W0QFZca05PZRTzNG+v31UyN56uGRPPPwfbz00o+47RIrmiI2Ki0WHCG07N6PEQ8/ybt/e54f3XU1PVvG4opqyl1P/pTxb/yKH97Wg5QIh+jpZtCoHzPu1UcZ1qsTtzzxGC//4kEGNgkHLU0Cbl0uv5m/v/0y7/7uAa5smyh4I0kRldyGET/+OR+/+Tx392xJoyYtueuZn/OXJ4bQKSVCc8uhW8amQXd+/MRdtNOxASOGa+55mF8/PZyHR97Jj56+n7sva4ZTIbQi1hFJpz5X8osX/8h7Lz7GsJ7NCA1L4OqRD8q4P8lNbeOlfyWEYmPXq/nT3/7Mx688zuDuHenRdyh/FjzuvaI9UYaQSDBAx1p63DKch4Z0xCFVciqXIHRLbhk+nKceupP77ryJB0fdy8+fvJsrGoUS3qgDD/3yBT7++4+4sUsbOvW8gl+/+iuevrYLDdxaAIQ1aMuoZ3/GpA9/y9O39KVhSLAeFUmfG+7gjTdf5rXnR/HUD0bw9MP38PPfvcDzN7QlIqE1D//oGX7/9K10SnSBjL9SJiquI48/O4JLLPnKCmIiKSKpJbfJeI1965c8/9BdPPbAXTz++KP8+ZePcFVz0F7mimnCDcPv5YfS/sDdN/PEI/fzg+u7keAUtxEKjZR0g05hCe0Z9dNfMvavT3PHwM4kWv3pFkVKpwH8+k9/YvRLT/FD0fnpR0by7DPP8MrPbqJpeAyX3vkor4lfDOuRjIZWKd17NDfd/zB394gCVyPuGvU4//j9vVzWPA6ZAij5ONC23y385fVX+ej3I+jfvQ09B97NO2+/xKs/uYdH77+LR34wkt/96jmevl4MElUchgpqLQPnjGrMDXfdxTNimx6n+x8cKeN0BwPaSfRPcBOH5tJrb+GHDw/nQbH9B6Pu4+kRV9EmTvxaglaxLS7lpy88y/P3DaZlpAiXyxCPj2p9Oc88MYzG8mxdzhDa9hjAL37/G0b/WebBZW2JF/wQTVr0vZGX//ESb/78Pq7rnCw14GjWl1fe+DO/HjGYa66/m7/84RlGDmotYXWslNCuF0/++g9M/sfzPDyoDaEWzoqe113P0C6NLBqJfll35YpjwF33855g8mdZ8556eISsKSN44fcv8NTAFCJSOvPcL5/ll6Ouo020QiugbQhrdilPix9ZqGksjAi6DbpB1pjhPDR8GCNG3MEPf/QoD1zRArcrlsuHj+L9d//Iz+7qT9MIBAWDBh0u5ZFH7pW18Bbuv3c4Tz96D9d1TZJ5Ij4Z3ogb7x7FK799iEEy5xwy3oZD+Ix47nz6IQY3iSA8uSPP/O5n/GbU9XSIJZjCkrnmjocY/dFL/PnJG+jYIJl+t9zDu+/8mRd/NBI95o/JOP3xz8/zQK/kII/IRmd5UuGx9Bh4Ky//9Q+88cvhXNGxEaFSjzIQ662sH5M79edXf/ojY179IT98RNbWxx/i+R/+gEdu7kmDENETJ8279uVXL/+ZD158mqdlLj7zg+E8+9Pn+LOMfeswJDlwaKGyxvUcdDtvv/VHXnzyJi5tGSf1Cld0Y2588jn++uO76V/nQITRZdBNvPzWX3n3hQcZ1LYZfW+6k7feeomXnrs3aN+D9/K7F3/Ow30bYzg0/a38+S8v8tYLwxnUKZmwqBSGPfyc2HcnPVrGih5yOULpIDq89sZrfPTCvVzZOpKwJt147MdP8uyo23lw1Ah+8ugwuieLcUKulJJfuVQona69mz+98DQPDWppYaWUIcucQa+bHuDp61oIEefXFP2glNI3mT5JXHr1Nfz4Vy/wzu8f4c4hPWkZr53VoGmfa2SOjOTpRx7ioWs7Eqp9zOJT1tyGaK649R5+9dyDPDLidp5+4kHuv7YDbhEdLuvWrU/8hI/feoFfP6L9+R6eevxhXnvhCYZ1j0GvHCo8nituvlPeYXfx4F1DuXfkXfzs2YcY2imB6GadefZ3f2Dcy09xz6C2RIlMHI14+Ld/5P2fDqNvh2Zccc8TjHn3d/zxCZGv58yjD/LiC88yakAyOrncsfQecjMvvvYibz1/D4M6JsuH2sbc9PSP+dtzd3F5iwhNJjmcrlffzKtv/413fnk/A9s0pvfN9/CO+OsfHruJnk3DwVQ06ncbH7z7a0Zd3YnWLbvwzO//wPiXH+KKbq24pM+N/O0vP+L+QR2IEon6imrRg8ef/w2T35Y14PpO6Kmr6wlN5prb7uPVPzzCtR0a4BJMZbikiyiGPfYQN7Wr9Qept+itH2X9hiS3YsgNt/L7l37L32X9vPHKzsShuGTI7eL7D/LoQ/fynPhIjwYhMv5wkQjsZCNgI2AjYCPwn4WArY2NgI2AjYCNgI2AjcDXIWB8HcH31v6FpyfTOliBwrACVYApWZ71b11WhoFSKpjrKuWuJNf99ZdS6nx7XZ1UCIW+lP6pzfXKwqMrz9fIs1JK2JSuDuomz/pB10gMTYrKaldKSVnU1frWlq2K2p9gqzxIm1LK4pGni2TqZyvXtiulrMcv/JG28631y3XEUqeLmiaoJ+f7RCfdoO+Sa0mlBPWquSh9puHCY7BU18d5HhGqlArK05icbwClFHWprni+5nxBKOqVNZ3OUgufqT8/vkjSRKKM7lIphVLBLC2fvxRWOzoJnb4FszQECwgB9dOFFiVNtbk+QW1Z1d4hWNL68FVJ+ldKoZSqRyXlz9kizXV1UvyySynhrd9Y71EpZfWjlDpPcaF0vgpdJ11dqKgt1WPjs2XNEyRT0labgxUX/2pGEa5xUaqWTu4XiJQUa9cDqVdKWfKk0poz8qiLX5CVNQm13Isa6zHUK4KQU5vq11v8UiFXbWu9W/3KevzScXC5qkeqixdIFErVZt1gZWX9Xvyj68T285X6WaQHlfoCGRCk4HzSz3XzQimFUspqC9YFy1aF/NR/0mUZFqnVl37Sd7hQql9WVr2iLimUCua6mi+6C8n56vrl85W6oPRPMCulauXC2DpPAAAQAElEQVTKsyinYVBSlEr9a2WllDzqbD1+7keaz9ep8yWoK9fdkfRZ2gttCqVqs9DVXVJVV5T2YFEFb9Td9eNFdLqiNiulUEpZT2Keda//E2ypX4PQ1/kDX5mUusCt1IVysB9lybEmFOpL5CiU0lmag0xSqHdZbbpd1a+Ucq3/fmm7kOirHltQvEJJvc5ygy/kx0rSZN31T125jq/ujjTWtUkRXa+zLutsla0f7VWglH7QGSvpR52DD9Zv7c8FGmGqrfvym6YO2vflNLrlfF/yoJTmkkLtpVTwWcupLda22DcbARsBGwEbARsBGwEbARsBGwEbARuB/z4EjP9bldXnz3Lq22mk1OcZlPp83beTGqT+rJjPPmuqL6rT9V+Wvy39l8n5qvr/yz4svb4f+C1RX/Sj1Gc6kOfP1HwR27+17jvr80W2fFHdv8ga6epfJFnEivCvxkV9fj0IssnvV1xfK/creGubVO39290U6tsxfAW1+pwsMesr6D/fpJT6XKVSn6/7LNE3IPksy7/vWZT7egv+fep83z2Jed9Y5Leh/azQi3gvevgsZb3nb0pnsajP+a9V/RU/30r8V8j5bk3qu7F9C67vy77vS863UN0mtRGwEbARsBGwEbARsBH4xgjYhDYCNgI2At8Ugf/jAPQ3VdOmsxGwEbARsBGwEbARsBGwEbARsBH4AgTsKhsBGwEbARsBGwEbARsBGwEbgf9oBOwA9H/08NjK2QjYCPz3IGBraiNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI/C/j4Bt4bdFwA5Af1vEbHobARsBGwEbARsBGwEbARsBGwEbARsBG4H/ewRsDWwEbARsBGwEbARsBP4rELAD0P8Vw2QraSNgI2AjYCNgI/Cfi4CtmY2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI3AlyFgB6C/DJn/vnpbYxsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBG4H/fQRsC20EbARsBP6rELAD0P9Vw2UrayNgI2AjYCNgI2AjYCNgI/Cfg4CtiY2AjYCNgI2AjYCNgI2AjYCNgI3A1yFgB6C/DiG73UbARuA/HwFbQxsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBG4H/fQRsC/8rEbAD0P+Vw2YrbSNgI2AjYCNgI2AjYCNgI2AjYCNgI/B/h4Dds42AjYCNgI2AjYCNgI3AN0XADkB/U6RsOhsBGwEbARsBG4H/PARsjWwEbARsBGwEbARsBGwEbARsBGwEbARsBGwE/qMRsAPQ38vw2EJsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBGwE/vcRsC20EbARsBGwEfi2CPyfBKBNEwIBO9sY/HM+oP3o2zq8Tf8dETAD+Hx+AjJ3v6MEm81GwEbARsBGwEbg+0XAlmYjYCNgI2AjYCNgI2AjYCNgI2Aj8F+BwL8xAG1KACtASamf3PwAWTkBzmUHyMo1ycnjG+dsoc/K8dfyfp4vu05ujkl2fbnCdy7bL/2atX2ZZEn/54Q+qIs/KFOe6/QKypJ6LSv34r6ytA66vn4ftWVLnsipu2d/hlfba7WJPlZfdbSiTx2t1V5bb5U1bW1/5/US+nNCY2UpZ9XrJ4iT4Cvtmj9L89a217VZfQsudTjpequulvZ8P5991n2J3Lp+tc6aN0vraLWZ6Dpt578q5+ZDUTFUVn/NPJModUAi/Tr75e7/TATVlMCqbruQTepirKbm9fvx+wP4LV65S1mqazs10Xx+TSPtwXKAwAUCTKm32oXPareeA9QjqZV14abpLpIhxFoHXa/1kMfzxJZ8kanbAp+x7TyRVQjqWp/Xqv6GP6YycDodGIrPJbvCRsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBG4H8fAdtCG4HvioDxXRm/LZ/Pr6j0GCjlINRtEBFhEBVlEOb+dhEtw6EIi3AQF2MQ6uSiQJ4OrrnDDWKjDcJCFAYXkil8UdEOIqRe04EiXPqPEj3ChSdG2qIjDXTZ0sulcIcZxMQ4iAxTOAwuSlqHmHB1UR/UphDhixSZWla0yHc7Rc/atrpbSKhBXZ8RQqvxiBZ96mjDhE/z6jZLjuhR158zxCBa6yv0uh+do8VmjYeOnmr7HKK/lql5dY4QXV0O0UOiq0EMa3ES/LVpUo3DqawxCQ9Vlr0uraP0G6XtF163ftb9Ck5ar0ito+jgljZD8A2PdIhNBuGCsaGF8tVJB3g/m4Mcpozr57Nuq6PXwdaqapPCIpPiUrFLN35RVgpDlNHZYRhi18X+ppSBbruQFYpgUkphOBw4HAYOozZLWaoJJoWh6zWN3INlA+MCAUrqHbrdIfW6rLOUVV0nfD4ZQnORDCF2OC7wy+N5Jku+prfyVwgVqwyhqc97XshXFbQzSbuqTGf21EVsTKuQJ8H7K4PdFon9YyNgI2AjYCNgI2AjYCNgI/CvQcCWaiNgI2AjYCNgI2AjYCPwX4WA8a/XNoCOVRWdzWbV3Mn88S9v8sb4WYwbP5F/vDeRpXvOEnCCkkCXDoLyJUkHHg2hK808wvzxb/GTV8ezJaNaAs2gg5HCjivUZPe8Mfzm1Q+Zvz2NKj8SdpP+xUp30WHefeMtJm3LIywUTH8msz74CK3LpCmTeOkvb/DXMTOYPHMO7304gWWHz7Fr+SxefPlNPl6ylawqMJRJQIHTX8nqcf/g1Xn7qZZnBwErYCqd4QiUsmPlPD6aPAct9x8fL2DfuRrCRPdAQOiE3uktYMfaubz86hv8ffRUxk2dycfjxvLSh7PZmxUgQpWwdPz7/P3j6UyYNptJM+fxxquv8Ors3ZQJSKnblvL6397g5fcnMVZ4x06awmt/Hc/atGKUC1xuyD+5m6mTpzNp9gKmTZ/Ox3PXcaLYT2gIVGYdY9GkD/nN38awcNcZqgMQgpes41t4552xTF+zj9yycvatmsGfXn6Lj5btJisnn12rZ/DHV0XnsTP4WPodM2Ycf/1oBntLoPrcAaaPfZc/vj6e2dtOUO5B8BKcTb44Ka2nIlSC3XU5RILmpgykMhQh9ep1u/VB4TM84RIY18HyykqTUglC1+/I1E4nFdU5x5gxaRofTZrHu++P4Z15e6jwSYN1+Ti6eS0ffTyNiTMWMPbjmUxcvo/82vbcE7sZ//bb/OrVMbw7cRbvfzCWV96ZxYaTYrDweytzWD19Cr998U3+Lr7zwegJvPrmZBbuzZJWuaqL2bFqNr978Q1eem86H06YafnWy29PZlO2tMvXglo15cEUsAJ4PfksmzGfpftqZUhL5v6NvPv2BIv/zY/msSmtXGr15eHw+sW8K3qPHj+VMYv3UOTX9QHq5Go8zYCf6sLTTBu3kO3ZtQFkwVlTBrNJQP8Vtz9A8K+19T+1IfqIfposIP7+6axF7CgIoVG0S3xd6AIBAgFT+PQ9IHz6bkqdH7/UB+XavzYC/+sI2PbZCNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgIfB0CxtcR/PPtBpWVAbyhifRoGU52Zh6N+1zP8Ntu5eae0aycOoG1x6usv5rVAa0v608pRcBnEtOoNe3j/RzZuZl1e45TLhY4TD+mA4y8A3yyaSfHC6BF+8ZEOvz4JCDnCjE5c6oAdyCbPTt2kKtAeSvwRnfkzpuv59YBbSk/l4azxVXccdN1XNs2imqjAR1aNiQv4xwxrTvQKBIJsoHLMCkqz6OwuIazBzdxstCP02lIqE4R4jDZt3o6n2QlcNOwa7j5uuvp0cxFVmaZFRQ2JUKtJKLnD4mlY8eGFJzJIqbDNQy/aSi333ITl7WJw1PikX68+F2NGHLrjdx8/Q0M6RTNiaNnCE1IJDocWrVpRyAnnUCjXtwybCi33nozQy9pRI2nEiMMcnYt5K1pm0joOYRhgwdz3dAb6ROXzeSPxrMrz0dS05a0jSzndIFJy3YpRBo+CdY7adK+JfGuCBo3b0JCfDhdWyRQeO4cYU3a07hhHJe0iqcoM5P4rjdw541DuX3YTVzZIpS8EmjQvB3NnKWkF7vp0bM5Gnu/KTgL1nw26Tq/l/xzmZxMPUdqWpaVMwur0X+F7a8WOaczOV1bn5omdGcKqfTWUHAui1PCc1qyvqdnleAT36iuBk9NbUdmAFOC2GZFKqMnLMPfpr+M62BG3t6X0KpiCksCQuhn27QPeGddPgPEjltvGMydd1xDo+x1/OnNZWR7IaF5JxqbBaTXJHDzrdcz4p7buapRFq+/Po09gqMzLJGuLSNJE9+6ZNB13HP3bdzdM4xF733IknQPhETTpVMyxenpRHW/lntuG8q9I25hWMcI8stEBfEHgUgCz/pXUZ1/hnVLVrJo7U6OFQi/JqGadImId+wz0OK/tmU5H78zjVMyFyqOrebDhal0H3Itt998Gd5ti/h4bapwGSgJEAelKgrTj7Fs4QqWbDpEdpWuFZKLLoXhcOBwGJL13SEfD5QEmkEKcnlJ7D+Sv/zwWlrFu1HKEJ8XGsHYEB7D0HwGhn42HDjkjp1sBGwEbARsBGwEbARsBGwEbARsBGwEvm8EbHk2AjYCNgI2Av+VCBj/aq0l3kp1lcSxnE7CwyOJjIgkOjpKcjQtu/anTVghadkFBNygg7N8VZLYmTLcRCY24/Iru5C1dyup+QGcLkVoSA17d6ST0qYlifExhIe6cAABwyC06hyHq6MZcM3NJBTtZ+9pHxERjenfvxcNYqKIjY0WvSKIiYkjJjKSRl0GcGlzJ+6wMKIiIqQtXALPIkwCnYZEVQtOn6L50Pu5NCSHLcfy0MFvJc3SGxmnTuMLT6BlkxiSGiTRvfcAerWKpNIDDmURgXIQFh6JxiIqOpb42ChCQ+Pp3O0yOjRy41HR9B44kNaCU4NYxY7VazC73sY9VzZDSaDVHR4lvBFERUUTHx9FhDuSlr0upUfzZNzFeaxavo6I3jcwuEsCkVY/UXQfeCs93SeYt3ofNZFuosPDBYNIIsKcKLAC6K7QcKIjRV54KE6HUatjhMgIw+VynH+OEr3iBbfQyHja9xpIjwSoMUKENxLLpnC3BC354qQdQrzOqMhh3nu/4OlfvcDzv/sFz/7qJzz/6vtsO1dFSfpa/vzzn/KT3/6Cn/z6l/z0lz/iyVcncjzvHEs/+B0/euEX/OqPv+UXv32e537zZ6asP4b+S3RPlQ4siy3iJ9IFFBZwOqOA+MZNSYiLJi65M7cM7kFSjIE3ew9TV53hyjtupENSFNFRkcTExHPN8KFEHl/L/B35ONwhREeES1sU8WJzbGwcvfv1Ib4kg4ziapSMY4TwRYnPxMdHExsTTavLutMyooRDZ8TplUFoRCQxUeJbsbHEiIzoyGjaXz6IKxthJYf+VUr/4oxrzpDbb2JAyygwLQvk7qRr30sZIEF9zd+xcyeiy9I5VQQBXxWVZiRNmsbKB4NGJLi9lJX7LFl1P6YE42OaX8Jtdw6mkYW5PAAAEABJREFUU5KLgBnsK9guQOlCTQn7tmxg1aY9rF27njnLt3C62IdSSvytmIN7jlGUl8b6Hccp9ku/hWl8unE7B9POsWfrDvYdP8XmrXs4dOosB3dsY0dqmZZqZxsBGwEbARuB/2EEbNNsBGwEbARsBGwEbARsBGwEbARsBGwEvikCxjcl/K50Ot7o9QW5A4EAgYApwU5wuCQ+eHInZwMN6dAqCeWRYJgEi4OUX/6rhKyixkGLS66gfeAw6w5nEwhzYGYe4bg3mg5NUzB81fglFqn7coZCwek8XOGhNO10GZ3jKzlw4CSesAgaJoZS4wX9zw4ERDd994uu/rBEkiUGqP9ZAl2vs3QrATnw+ktIzzJo260xXdslkbF3HyUSlDPEKr8EJLt060Damo/43ZsLWLs/B5cEJxskhuATuZaAWtO0zIAoqeXqIGTq3m2cJUICnQ4Cyk1iYqwEgeHExrksPhXBbTcPJIkAGiYkqGjxSzAxRHBMPbmfY7nQJNFBaUEGp/OdtG+egCF6eaUPn7bRDKVdyxhy0lPJlvigE79g5JfxqFVIbqbGQLKWrcdN3+vyRc+itFOUPrJjDwXOaBIjNIYSmKzllS5F2ldfynBaQe34jtfw81/+hj88cQ+NCjczd8NxAs4wXI5o+t32GC+/9Ede+eOr/P3Hw2mXEIopgdnkjlfy1C9+y2sv/IxrU/JZsnojZwpNCXor6dQUmBUBE1RyK3o39vL+S6/w0dzNnMz3kpQca/3b4blpxzhnNKVzgzAZOQgIg7Bghuk6P8dOpGEl/U9TaOOtBx/7t+4npH1vujeJsGoCls2mYGlYz8WHDpHua0K/tuJASJJ2v2S55AEObtxNphlBvLDr/tAq6xbpQyDB9HvFxwKWTlZ1wJCgfjhOV7CuorgAT3giifrf6+5wLTd3rOCDv03mrXfHsiusJ6OGtEZAwpS5pEVLDBnDkDknju4XG7XM+jlg1SlSNy9j7JIjMiYuPKlb+Ntb80gPgDcnm+NncsQXHRxbOYdJ686A4WDTjHGMXnmE7KP72Z2ax47ls3lz3i6yzh1i/d68+l3YZRsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBG4F/DgGb20bgvxoB49+hvRVo0z+GE1WZz/aVs3nrH6/y4syjXH3vYwxoHEJFjZIAIl+bdEAtUOMhJLkNl3ZNYc/6nXgVHDkq4VsJODZLdFFTI5EzqTMlIBymajieX0hNFfhrSmjQJIGMg/s5V2LilICuxP0+36fpQ//THRc1WPqDL2sP5xxx+LOLiWvUmMC5PeyW4G+IQyExPlpcPoLnH7gOM20L4z54ld+9M4/jxQFCnCZWrE8LFVlaN5evhL2fzubDyVNZsPEQ5b4AVlBaopU+CUE6Ck8ye8kOml19N4NauKmoNrHsl8Czy+Xl6JYVTJo4hWkrtpBVEcAQYzzeCirMEAmyCtYB3ZkAITdTmbjDXPg91dTUgNIjL/R8w2RqOglqGt5y9q6axZipE1i0+RAVorNPDLPaNc1nsil9fGmbBFuNsAQ6dmxH1zYtiQ01qaqqJiAyDaePMwe3s2LFKuYvXMGxfA8hkeEERHnljialSRNaNEkmwmFgSoDVUGKn4AJyl7JSJqY7nrue+RGPXx7HzhWz+cXzf+K1BYdl1MHrqcJ0huAylHCIhgor6VtkmIOKyhqLTv+fReYd3834yXN5660PGP1pPp0v60TDME0pLPIlxVl9juVzZ/PWX1/lJ5POcMfTj9I/2SEhfmnXmAWq2LV4JmMnTmLiiv0Ui3/qwK/0KgQXX0op0efiOlM+OPgCDqmvYevW4zTpdxVdY6HizBGOFYXR/7qB3HjdpaT4sth1oghEhkShuZBEplV3oSZYUkEywbNtm1a0bdOO/v37MfLJ22levIele8twNe/AjTdcR++eneieEsbp01kYsU3p2LwxzVpewtBRD3H/Nb3o2Diexi3bM+SWH/DDa5sGxdu/NgI2AjYCNgI2AjYCNgL/cwjYBtkI2AjYCNgI2AjYCNgI2Ah8WwSMb8vwXeglzAX6JyBh1dBY+lx7G6Ou64EqLaTCCMFtBWclHCfByoAEX6UkgTETf12ZeknkGPioCkTTq2dvYov28smOVM6U1NCkXVMiayolcKiQ+CNKgsK+4jKKzh3nSMZBFi1ax6kyF778Uxw5W4lyO+sJrl8UflX/WdSRR6fbz6kjWZQXHWLFynWsz6gmVOWzd28GKkRoRH/lDqF93xt44Q+v8scnbyEs9VPmfHqA6hCFIxCQsDJoLJTpx+uMpssVtzDq7ru5bVBP4twGTskKg3B3gE1LZ3AkpBcjrmtLwGPiDHHgciCtJl6vi7Z9BnP3PfcwcuiVNI8SW5yKUFcEYaoaj8+LFY8lmJQ8+Kp9GKKfywVmQGFIcFRKWHijS/LzJZfS9aJ/wBVJ16tu5/477+XWAd2Idhm4XEqC33wuablOh7rwT49wIZlivxJdq44u5sfPPMOoX/+VXVUtGNCzFU6H5kSCxBWUFBdRXFpCUWkVNV4/jrBwyo6v4Y/PPc09z/6GxediuH7gABrHgcdrRdytTpSArKTkjmrEDQ8+wUfv/4Gf3NiYLTNm80mRn6joGExfFR4reC6UwS6RIaSsKkC4BOv15KjxBEhs042RI27hqSef5M3f3E3VmnH8efoB8TNwmDXUuJO5fvhtPHRnT4yiXDzuMOk9IFkUMAMEjDC6X3cro+4dyaibepEY6hDslYwj3yCZopMhmPg5tn4FB1ydeeD2bjiVn8MbtpAa2pYhXZrSuk0frmzjYe3qfZSAyA6cH1d5/JrLpLraIz5VQ7UeYzOK5DgHpbnVlGcdYsKEOcxetIqNp8tw6z+5N/1UeZFxF5+TnpyOANUeH+7a+eQO1/Vf06XdbCPwzyJg89sI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI2Aj8VyBg/DNaflNeZ208SimFcjglWOkgttM13N7TzfK5c0nzKMIdEgCVAGp0lIHEM5EIGzFSlnjs5wJphsMhAdka4tv3pnfjSubMXEhR6CV0bowExhQOhwEK3C7IzT9HdXgffvjkHYy4/RaeGPUgA5sVs2HfWSSGZgVHlVIoVZu5kJSqq5MgoAPc5Wkc9jTn7uEj+cEICSjedw/39W1C+oGtnPFBiOkl48gJikwJntZAqx6DuHdwW8py8qiQOkPJj1y6B6VEtgSAnU4XDpeT5r370JlCjqbn4g2ByuOfMGdbGdfdcQetQ4VDQMk/nUp2sQclUWillAQABQcctGnTiY4dQtm/Nwd3TAot432kni1F/9vUEv+VYCdiXw0n0kto0LQZDaIgECqdVFXh8yvcmsgB4YFqyiQwHWK4UIBS6nxGklLBZ6f0bygnna/sTrIE/NPSzlDlBi1GKYVcQu2QYHgNZ86mk5XvxeUEU0d3pcW6lAHyQcKd1I7Bg4dy26338NRTP+S2Hkk4ZGSqq1z0u/NHvPnKz/nw7V/z/B0diZZgr8/rI6JRF+597HHu6hqHJzSe9q1bEC/9K60ASD/yI1dVfj4ZZ89Zf4lsGtFcceftDGlucvxsDbHN2tIokM2RfA9KaA1RWt+VV9c5aNuqldSCXzlwiM+GiM85RTN3YlsG9Ejg4Laj1AiFw2FgSDZ9DqJaXsX9l7uZOmYh+RKYNaQdka6UwiXO6HA46NSvG02Vj7Np6VQgSTCpdQl5kEtolVIopeRBbNFyDD+ntm9ge0kj7r9vIE0ES91YVenDHRFCkBJCI8IIVMuY6kZdaepCbRZ5SimUUrUV9W8qWC9thvikIR8wqr1uGkRWsWneUs4l9eWeW4dyU6cE/L4AKAeG0rpxPhlSIaZYzwEJ6lsF+8dGwEbARsBGwEbARsBGwEbARsBG4H8GAdsQGwEbARsBGwEbge+KgPFdGb8pnyE9hEoANeA38flqqPZ4qK7xUVXt4NIb76ZV4TamLj0sgUSTytSN/O2d2ZwoF+lnNvDKWzM4kA9hDgjURrfMgJ+aGg9eCXyWO2K4ontbDAmkNu3enogqP9U+r9WH/reP8VSRlXqQsrhWhJUHKC7zUYybNi0bc27bJ5wu9aEI4PXW4BG9PDVefOeDZya+8/U+DFeAs/uOUB0VgSE8ZcJbIrnhJW3xnjnAjsPlhLtNjmxexZbUMpBgdHFeHvvPVNG8dRsSJGCne1Nyl8bzsqurq6murhE8PGSnHmLP8VQ8NaXMmroAs9NQrm4fQmmJFzNQxI7de8ks8kv/fjwSaNS8nmovZZU1VBccY92WY4JjQ64bOoCiHcvZeqoCf8CH/icq0nevYmdFY4Zd1R13FUS3u5w23qMsXLGHHJGBr5T1i1aQG5pMSnwoAa95frw8EvTVwWOf4GGNn9BbOld6OJdxhF37UvG7A3jFliqNo0fGQMbBLMvhwIH9nKlwECJjaAra1CYNg7eqBH/8Jdxx142MGnkjQ/s0J0Q+N3i9AZRRydaFH/HrVz/kNy++yS9fX8yxIo/AWokvLJke/Xpy281Xk5hziKVrN3HOIzaFB4LSzeDdn5fGshWbOFnmxVtTQ/GJo5xxNqRHgxAcCZ0YflVDPp27gjRpr/F6xa8q2DhrMfnN+nF73wTB3C9jUYP+Z0EqxTc8/ho8RafZuCuHNr3aiieZ4mIea/y81R7B2kX/226kWf4nvDX/BF6f3xrnamnTMmq0DCnnZBxn/caTVgBbHJu6pP8Nbp/oqel19kqwVxle9q2aw8T1mTRrlUjOwUPs2HOCAp9Bi05N8WekklrpFd0LOXioiKQOLYlFknxIUBpkKeo5o+3XMqtlbHz+ID7SdNEV8PnwCw5lR49wtDqBPpfEYsicLSstplLG9vjZQqrEBr/fh0fGuVrKAVNECN7Ws8jWz7XdSoN92QjYCNgI2Aj8CxCwRdoI2AjYCNgI2AjYCNgI2AjYCNgI2Aj8VyEg4eF/tb4BwiIMQmry2HYon4iYUFJ3beNYbjUhie2445bLKd63ksU78yTgV0NFWQU1Eh+LiAijOGMfB1LzcYdIyFYC2IZLUXj2KIfPFpN3eh9HztaQ3Otqbht6Ez1TICv9ODszq4gL8XD8yGmOHtrL7kPnKCpO50yJIjLcSWluFudKXDQMz+OTDdtJPZfNzh3HIDqR0hOb2JVWhCHR0kB5IXsPpeGOjSL72EGOnzjN+n1pFGVlIN1LQNqJQwLFhzMqaRDv5vDmlezI8dOua2sy1s5l8sLFTJ2/gurW13HnwBb4K02QiKAp2eEpZN/e0yI7gsxdC/h45mxmzZnDpNV78YU3wJl3lAx/A1Jc51g2fzYzF81m3JiJbM83SYxRHNm7h8qIBCpObGDK9NnMWjCbj2esoSxMguMBg5Tet/LkbZ05sX4es5avYMr7r/DizGNc/8gzXNU8hIoqE1fCJTzw6P0k523ivQ/f5+2PJrLT1547rulFrAt85aUSVE/DHRlJzol9ZJzLZ53vL9kAABAASURBVN+RszijY8jYMosJs2czY+Z0Zq3dQSChCdWnTnCoSBFn5LFi9gLmLFnAhJkLOCofEBLjDQnEBjifBArTcJHYuA0tEtyUSlA9v8BHSWVA4rEKR0gsLds0J8JXRmZ6JmczM0k7m0e510FS01Y0T4yiutSPSunHLVe1pSIvi9IqL/qfKNGBco0zkiJbtKB1VCULJs9n8izRZ00uV997F/2SDfE1CRbf9xiPX+Zmycy5TJm3lPETF3A8vA+/fe4WUkIg78RujpS6ifFkMGXKHMZMmsmb41biuOweXrinO0ZNAVv3ZxGTGML+zVs4WeSTwHZ3fnDH5RRuXc3SrUfYuu8sKjqWsxvnC/8sxk6eweh5G6mIbUSc6Ghd+uOKUniyT/Ppmi3kOMKoPL2HZVtP4vdXkn82i4yzGSyaOpUPxJaxExayObWCZlfczMg+TpZPW8DEKUvJb3olT9zUEYcE8QPKsEQj96JTB1n26SECUW7Stm5i4/4zeJQC6VeGwqIzHAbFZw6yYPFyPl59jmtG3kXn+Gh6DhlA9NEV/GPScsqaNCW2MpdDxw9QJb5WkXmSrAoTs+QcBYFQzLx00ko8KJGlZVuC7R8bARsBGwEbARsBGwEbARsBGwEbARsBGwEbgf9qBGzlbQRsBP5ZBGqjVP+smK/iN/S/pkF0wwZcfcdDvPSbX/LMHVfQIj6U6kpo0X84r/3xOW7tkkxU66v5zc8fpFMclPriuPzyfrRtGi3BS1OCWkr/iw3ENO7EnT94lp/dM5CWcW4IbckN13TGWQ2RKR0Z9uBz/O23P+S2nm1o3qUfDz/7U54Z2p3EcIXXB+HRzbj+vid55c+/44mhl9OqUSN6DRnB73/7K+v/PLBLs3iU0BnuBC4bdh8v/vFXPDK0Hy1T2nDXE8/w4/tvoZMEVAMB8Dvj6XPVXfzxxd/y60dvp2NiOC16XceTjz7Ig3fcxqj77uPea7oQo4QWhVKSTfA5E+hz9XB+//sX+Pnj9/PgiJH84J6R/OSnP2REv5ZEJF/Kb1/6HT9/aAT33DGcUSNH8PCjz/LSM7fTPCaMpr1v4Plf/5rfP/2Q8I3gB/eO5IknnuNnd/aysPZ6FSmXXMF9I+/jvjtv4q4bBtHYe4Klyz7h8JlS/AGTgB9imnXjwcef4bc/+TG/eO4ZHr/lMhrKxwKftDnCY+h+/YOW/T+88VIaJiVJMPI+Xvr9r/j5oyOx/j3jkQ/ys2ef4q4rWhGd0J57nvo5r/7qaR6++07uv+tOHnz4GbFhGK2joFoC44YCAQEEu0B4Inc8+xp/f2Qg0RLwx3Dichoy1hDXahC/ee3PvPHSn3jzNcl/fY0xLz5Et4aNufnZP/Ly4zfQxOWgxojnxif/xOQXR9KnuQudlFLShdJFCEvkmrsf5OdP3iUY3cWzT9zFkA7xVpvEZUGF0nXgUJ5+eDgPDb+Fxx6RcbjtMhqHKSt+2qB9H5785c95+49P8exD9/D0o/fx8588wQ9v702cE3Ancu0Dj/DmX37Bj+4aRPsEqZTxbX/dvbzz6pPc0r8z/a+5k7+99hv+/Ny9PPXwSJ565AF+84uneVQCxSIBJFirlLKK4Y3aMPiGW3nhdz/nxWfu4ub+7XA6Yxn80LOMe+NX/O2lX/HWq7/mgzd+yrC2kaDCuOzGO/nRw3fyyEP38fTw/qSEalkKQ98IpsS23bj5rpG89OLz/GLUDQzq0ZxQ3ST91pEF/AES2vTintuG8ewzD3BT9yRNQULHK/n9y7/iN4/eyj0jHuSln9xM1449eOLnv+RPDw2gcaRCxbbmsZ8+z4tPXU+r2BCLTwYheLd/bQRsBGwEbARsBGwEbAS+LwRsOTYCNgI2AjYCNgI2AjYCNgL/lQgY/y6tXU6IjjAxHAF8EgCVeBcSq8PrMSmvMKmRoG/Aq8sB/FIuKq4iuUM/ujd2U+4BCXNZQcGA8HqqA1QKny8ApjxXS1lu6LYaaSuvCODxgs9nSpBbaGtMaRNa6VAHjmuEvrw8QJXUaxleuVcIjyXTF6Qzhbauvo7OU21e6FfaLRrRuUJkVUhbnaxKKXtEpkf60brpek1bP9fJPk8r9FVVoq/ctR1apm6rkWctR+eKSlOwA7/0WSn66r9krutHt2t6jYPuR8vXbVXlJuGtLueJxx+mZdURVu84SbEE6yX2iKaplD51rhKdNb9Xgs+a3xRsfdK3hYvc9XhpeutZaHV/lnzhr5Z2rbOnKmCNpa63sthvyRRZlsxazKyy1FVXCr3w1+ls1QtNQMZN218muJaVBdC5VOzQunk0j+Ag7IS4IdQVwOkWJr446b+I1v98i74H88V0wTqTi+5CovGRSkxxGG1b/XYtT0isS9cHhEbfrQql/ccUfzPRdTrrds2jyzoHxGB9t+g/86PrAyIvSB9sPF+n660ssoNN5/vQNFaurb/4JvQC7mflXkQj7frfd9a4mjL48hhslsIFPvOCXVoPaQsSic2fea6rt+//mwjYVtkI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI2Aj87yPwfVn4bwtAa4WdTkVstEFSgqJBElZObqBomKxIbiDPVtkgMQEu6duZIZcmkhSFtCkaJEKQR8mzQUOhTU7SdfpZXdyWbAgNkhXJUta0QV4sumThbVhbr2Vc9Kz1sOTCRfVSZz0Lr+apk2fVfVaWpqmfhbeOvu5+EV8tbRAHJToqzutX2xakV+i+g2XBwMJN6mpp6tupaZKTtBxFXKRJ+769+fkvHueZOzvRvqkhYwCa3soix7prOfV01TLO6yH1Fz1rWsn1dbawFlmari5rucnCW2d3/btFLzLq11llqbP61biez2KnyLF4pA/tD/FxEBVp4Kj/577a0eplpRSGZKUUSunMRUkpXfeZXEeh2wwDw7i43ZD6CyRK2g1LNrVJKSV1F3gMQ2RInVLBOsMI3vmCpJQS3jp6rKSUCtYZUm9l4SeYlJJy/Rys/sxvkOaCHsFmUwLIhkNhlmdzIrOQgjNH+fRIgdhiCEFtUF9kX+BTokdQltJ6SBu16bPPtdX2zUbARsBGwEbARsBGwEbARsBGwEbgn0fAlmAjYCNgI2AjYCPwX42AjjT92w2Q2BVOB1+eneCQdjBxumrppO4reYTebq/Fqg6LWsxcLoUyA9LoJDIihBDBVOPrdPLlY1An4z/0rvWvF//8t/vw/0KHSinLDBWRzN0//g1jXnyAAe3jg3W1bdaD/WMjYCNgI2AjUIuAfbMRsBGwEbARsBGwEbARsBGwEbARsBGwEfi2CBjfluHfSx8MkP17+/wf7U3JUJsS0pf8X2+hbcD3i4AEm5VS1l8+Owz1/cq2pdkI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgIfFcEbD4bARuB/wkEJCr5P2GHbcQ3QUBiixJn/CaUNo2NgI2AjYCNgI2AjYCNgI2AjcB5BOyCjYCNgI2AjYCNgI2AjYCNgI3Ad0XADkB/V+RsPhsBGwEbgX8/AnaPNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI2Av/7CPxPWWgHoP+nhtM2xkbARsBGwEbARsBGwEbARsBGwEbARsBG4PtDwJZkI2AjYCNgI2AjYCPwzyJgB6D/WQRtfhsBGwEbARsBGwEbgX89AnYPNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjYC/5UI2AHo/8ph+79T2u7ZRsBGwEbARsBGwEbARsBGwEbARsBGwEbARsBG4H8fAdtCGwEbARuB7wsBOwD9fSFpy7ERsBGwEbARsBGwEbARsBGwEfj+EbAl2gjYCNgI2AjYCNgI2AjYCNgI/FcjYAeg/6uHz1beRsBG4N+HgN2TjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjYCNwP8+AraF3zcCdgD6+0bUlmcjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI/DPI2BLsBGwEbARsBGwEbAR+J9AwA5A/08Mo22EjYCNgI2AjYCNwL8OAVuyjYCNgI2AjYCNgI2AjYCNgI2AjYCNgI2AjcB3ReC/JwBtmgQCJuZ3sFTY8NRAZdXFuUqeddb1+v5Nc2WVSUWlacmrqoav46usDFj0X0hby/8NdPjqfiw5306vr9P7+2r/tnjpfjUe9bOu+0/MdTp+4dhq/7LG5et95N9lW7Xo4/N/h0n0FSymnpuSv4LkWzf5fKB1rY9rHdb171+Im9io6z877yqtOWtSX6am+y5Zy/rSNaB+/7JWfB/9fWsdRYfgvAt843XqW/eh/fuLsvStZelx0vfvnEVO0Abz+7VB5FbJuFTKuqzlf5vx0Tbp/EU2fVG9rqvLn+Ox9OD7te2LxuPb1oleNfK+/J6n9LdeA/6TGbze2vXpC7DV422NteBY//7Zev38ZdniE9m6va782bvVVteH0NZv1211uX59/XJde/173VyoX1dXtnhr+6urq3+v367Ln2vTOtby17Xre/2seb7uWdPUz3X0dXV1z191r6Otu38V7T/bpt9jXnmf/Sf7c51uen9dI75d5wdfaXu9sayjq8Oz7l4np+657v6l9NpHviTX8ep7HX/dXdfpXPdcd9d1Otc9fx/3f3Y8vzG+X4LDt7ahdpy+bxy+tR7flz3/BXL+GR+x9tNyaNVzsW5eftO7fmfrs+4XjY0ef52/qO3fUaf3FGLWNzXlIrp/BpOLBH3BgyVbA/cFbd+lypInhn7T8bPov77/76LKt+CR2EogwP+VGv+XGFh9y3h9C7C+MWmd7KAvBDEOfEeQ/XJut+a2rOfn57GUv8vcPc//JWupPhfp860l+9v28Rl63ZfOlqx6/em6uvzZtvPPn5F1vr6enK+t+ydl1Omo75/t64vqNM13HOJv7Ff/K4T/PQFopTAMheLbJR1oKCiA4lIoLbuQS+S5sBh01uUiKRdI1s9flTVNSZmSIJaipAQKioIyvoqnrMqgqlJRKLSa/yJaqSsUOaW1+nyu/Ut00v3Wpy0oFD1Kg3qViSzr+Ut4C/+N9VrH4lq8SsVOrffX9f/ZsSqTcSsSnb8J79fJ/j7btW0loluxtqtQ8BcdCyXr+iIZA12vx1zX/UflQtB6f1+LpFIKQzLfQzIDUFxqojHU+NUfcz1/y8qhLn/pnCkKjkVZpWHNO2suyBhVVCvKxRcLxf5CGafvmrVupRUy16oUxcV8fg3Q/Ut/et5Xlius/oXuu/b3Xfg0bsWlispqAz1//p066L71mqbX1e+iex2P1lnbUCXjVip4WnK/Dxyt8VGUy7pcIeOj/UGPaV2/X3TX7cUyp7UP6lwketTpo+96vuv6Op/V9BoDXWdl4S0oCvr1eflF4jsix8JJyufrpe7/tFxPF31o/B6m9f+MCL+sT6WyBmlf+Oza/lk/ON9eCHW+UChjq+nqni3fkHfIRXfxlSLxd92Hrtd3zVeXtW9pf7P2H4UiW2QW1mbtY4XCr/msLGXdpnn03crnWtsgAAAQAElEQVT1ZFs00r9eU631VMZe0+r3Q12brtdl7dt6rlh9C4+uq8t6jSnSOhSJT9f6eX0Z2gatW53txbX8lkzh031quVqeptX4nKfRNmida3k0jZVlHLRu9Xl1fZ1My1aRXXfXdPXlatoLdn9mbtbjC/JfjPO3rhNctF7af76vyaAPt5+XFTzqfr7+62t0kFyvRRoXnYsEA2u85F5nr37WvqmzLp+v1zQyRnpuaEx11mNjzQFdL2On66wsZUu28GgfsepkLPW9rr5Orr5bfiO0Wiedtd/Xr6/zE6s/odPjrHWr8yfNY9FLm75/L7kItJ9KzObrga2l0B+tLDyE17p/n/p8gSyNgbZV3zUWdbjpOjv/k/P5C/D+HKYyzt/WR7SrKKW+01lXf9iom7/156ce/7o5otvr5tPn9P0mNv0TNFoP/f6okoAQ3zIppb4TJnyDpJT63s4vSFJKfStdlRJ6yfyfJoUyDJT6v1FCKcX3dYbkWyallDVe/AuSUsqSHYRVoQTjb2unPqfrOaPfY/odp+ewfm/q/cR3eY/o96OWUSRzWZc/uw7oujKJV1kxK01T9PVrZd2aoted+jrVrTta96J6snQf2gZdr22q4y+UvWO+ZL1P+6ysz+r5Vc9avn7naYysdUf6/ir6L2rTMrQelo6yZ9F3rZ+u1/rqffb5PUvRxRjpMcNOX4uA8bUU3wOBt7pKgkaVEggpo0jeyDoXFJdT7fumm2Uvxz5dxC9fHsuOs/IZSHTyf4MvVuWyqS0qUSCXt7KcEvGgUjkpFcvsq/b6cTjAUD5q5JRtChJOeXbIXdd/PpvothCjjH2rJ/LK+9M5VQERroAsKvBZellncDohUJXO/DGv8ebiffjl2eUIfI7WYdZIoMgD0rdTsqH1+IpcJ/u8voaJyw3OipOM/tvfmLXrDK5wEacCfJ2sz+r9vT2LTsqAMCo4uWUOr/59HIcq5NkpwyH1X9aPobxUVVVSUVkh/lJKcUkJRXKq8QseIS7QY/B/ZpPoYOkt+msdnGJjjejqldVGj7UeF93udJgEvB5q5JOlftY6W/c6/u9ylz61DEPG1DT9n/c5aVe1bbpdNLiIxhBdwU8g4MdT45ePMabMRb7zF29TR4oBX+EZFox9h999vIVKeUbmpanv3yELjOhF3edTItiHx+vH8nHByxC/qCwvpVDeKEVWLqVCfw4WUk1Th7220yFzwV9+hqUT/s5f5+zAGYL4YS7zx77BR0t3EwhD/MhEj6HG9Ntml1nAxrnv8ZeJy8kPiGxngPr9O6X/6sJUZo35O++tPEaE9KcE/2/bz3el17q4QwLkH1nHP954k1VHSomNkLFWJt9V5jfmEz/U46ECNXjkRGRhLHXWXcbxm8rRNoSGBcjYvYi/vv4hOwsgym2i18hvKuOL6LQeeq4G8g8w/u1/MGnDKZSMj9MIfCk2Whc9p73V5fIOKaW4rAK/2KTXI93mFv8KeCpkrSql2m8S4ga9jquAhzKJnBdLLpc553YrXIaY4MDqS0k5RN4HXolMaL2+SF8lc9Y0fTJP/ZKDd+1LSl14Ns1gGQJYcsTXqOOTOzLuut4QG02RpZ+tvjSd5q3jE70Mq86H1+eXj6zBTVW1vJq+w3T+n2PxyX4lO6uK7Nwqyit9KKeJHn+Npb67XIJ6jYeKigo8gp/GXK/9hhMMmQ+V+hQhGOv5YZheKspKxJ/KKC2vqH3flclzKWXVNQT8NVTKu6VS3oOVHu+FfjS/+I2vuhKPRL+c0qfuu04Hd4hCeSvl43iZyCoRWcJby1NHpwJeeccG+yyV3X+JZL2mllfX0sqeqErW2mKpt9orKqmsrBKbfBhis1f6rqyslI95pdKH5JJiissrCSjQ9jpdijAXaBklst/SMqoEu7BQJfPCFJA8VIpM/Y6vlqincmDV+z1VlAt2VbKum4JXlUVTSfBZdJY2zaPlaZ2Li0vkHeBDz02f5hWdNPbVPtFTZGpdrDGQsmEgdOKSGlctt6IcLaeoqJgyj8gQ3Ky5aYgNQq/xrMt1c8LUc6U26zmkZSqZX6bMKbO23pRysE3slLIp9fqdW1Pjo6w8gD4MybZAFPlmV8BbLXyCtWxmi2XvXCj6FpZUSq+glEK/M/2y1yiXk5AePxOF/s8E9fu5WvAo075V7SOgaWSMymuf+UySpZoiOfgV5ZeRX1Aqe64K6q9xGguNg/ZxZGz80rH2YwtjwUyXlV90lTHRvlRUVEJZlQc9tnp/W1EmMmvbissqCQiPy/BTXVEm+5CS2lyGV5TXsjS2wT6Rva3CbXgpF38slr2gR6L4us3pVIS4TKprfbVCxlHXa51CZE32V5dZa3JZZVAPLVe3WTS1Y6yUXzD0SdZ3v6ASOD/XlKCsx08ZoHmM2vXTFB39okNZqU+wMvkm41lTI2tpEei1VKCjzj4t96uyIeuxXu/r/PiraK2287qCU9YdXef87H601nbd9l2zoQKCmQ/k3WLJED1BcBRsrGfpo47G5AKmuk3bRC22Gt86GVa9zJm6Z4fIrBsDi66enKBsPxobLVPTUp9X+g/yio6aT541nVE7huf7qKu3/EDLQ8YmaJvu08rCrwwsH9AydF/nZdfZL3IMS4ZgUDvnvd6AvAuC4/71PmIKckiqYNeyqfzq1UnsL5RH6VtcTRe+Muu/WMw+V05WTqmsMeUyEhAq+xCtt96PeCuDc6HKa8r+RKH3J5/zQcG7/riYgqcptmiMVT3bdB2i10X8Fm/QdlUPk/p8PgGhqjogcx15332lObWNJvKKA18FG+eM55d/W8i52haf7LNqi9/xZgb5itOY8s7bvDp1GxIuALG3tiXY/o1/zdrxq+bQp/P49UsfsTlbM5t81fiZnlwWjXmL345dR6Ff0wdkXun7vyfXHuMoPr2HN179Gx+uzbQ69v8z+IpQjyx03q+UYVJTXU2NL4C/5BxLxr/Lbz7agHxrBjmjVn8tv6Xmd//Ri7Bwm1XFbJ4zmuf/sYRzplTIVXuT0j97edi3YhY/+8sUjuaLrIJDvP/qG7y77Kg1P7Vzf11fcpyWfYqPzLOlZOWWyfm3lBI5f1TL3rBa5pP1Xqi/NjnqrVNfUnbKe7dG9ql+BRe9Ew1kPYUIdw17F3/Ei+PWUBKAENnzXTTXHdRbC0HrEBau0GcZX729l0PW2hqJu+l4W4l+5wufW9YkTa9l6ne/3mOWy3vbLWcjvSY55B4ZoWS/7MUvfhRcb8EhvFb5G96dsh75xUavLCAX2fgN+XVfms+o3adrPUsra9D6uQ1wip6G7LMLisrQ+yS3S3AwOI8LdvpGCAhk34juOxHpyaU3wjnHN/PKb17giZemMH3+cmbNW8rYN1/n3fXZsmibBMRp/bJK+2VC+SQHahcH3alplV207dAGR2k2BVVaKpjiWH6LJyDzOFin6euy/jP40gpTNmFeTu5YzdSZc1m6dg0r161m6eLpjJ66khzNdm4bf/vH2+wt8OGWieaVg6OIRfcb1MeUsvRnyntQdPM7w2nZtimevFzKvGCIBXV6+IRRLxiWyvJT4/ETltSYZkmhFOQUW4uOvJvPywsIvSEb5bNbZvO7F//EslNeIsSRpdqi0e1apohC2+ur1U2fMfyyuPvl8BYQg3WfhlnF7vVb8Le/mut6Nsb0mOh6v+YRgZr3vCyxJVAPPy0r2IeJptd01l3zihD/eX4Ti05+dF1dFlG19UGcpFlQkbLfR5URStu2jeQlk0eJB5QCbZfm1f1c4A3KNjw5bFv1Mb/8zZ8YO28Zq9YsZuqU0bw1cRmpZX6Z4Kbw+/GJ/VY/IsAnLzG5WTpcZJeAY42j2OETG/wyfhoHv9hk8WocrPoAWp8L9YJDvXoht2TX8WiZehH1lp/g/T88z/srDuGTcTNECU3rNnysn/ZX3l1+CKcE/nwyTj6Rp3XTdy2nDgPdr7BZ8r+oTtNaGcFN7HGGGMQnOAiVhVTWZ4tPt+s545CVMTbWQXycg9hIAzmfEZRtyngowiIdxElbguQIOXBLTAH5FsNXJ1NkBPCLIK2r7kvTKyVLhyjgjGtM9xQX2dmFcnjULaB9VdPqHNCABKuDcgQHXV8np7YJ/VxSasphsoqc9KPMn/o27y89Ym2iZZJRenwpf/rTy/z9nTd5/b03eeWVP0oAeCV54k9O2TTqbrQMMRSfBFJCElNomegkJ7cIlwRjj+/YRn5MT269phtu2YT7hUHjrcdDTLP6D0hB62ZlwVrLuzib+CQorsJjadk8ibLsPKqETkmnFo/YFhC5vpoAMY2b0yzSR64c5k3Rsb4cv/ZDobXu4pt+8eWA9K190/Jl8UuLXmT5LbqA+Kdp6ah9r67OJ3wWXR39+XsAiWmS3OoSon155MpCFfSFgMipzSL7s7wBrZfMJS3f0qWefK2frg9m09JF12k6fx2dyPT6vBgSKD6zcQIvjVlCqROUdKRp/XW2CGaWHWK7z6rzY8kRfmtMpN4vm1Cfz6Bpu+aYxfkUVQYwtX5CL2TSvym2+CXX3qXSkintdf1IldCJP57HRcoi1yMHw6jGrWgS6SUnrwz98VEJcR2fv84ezSfjgthgZu5k8tRJzF65gsVzJzJm1loyK03coVB0Ygsz5s5jxaolTJ4xnwN5XsIdHvatmsQHs5ewZvUSJowbz5rDRRgurUMAbXd4qJd9yz/k5Y9XURUCiA5yXaSzO8Ih89kpOXhPjHcQKoPpDpPn+Lp6BwlSjpE5b4jufllkw4QvLs5JXLRDgoHioX4kYGoQK3XRoYpAQLpDESnrRWyUIRs+eRZ7laGIEppEWSNCxW6BXIKZIK9ovjLVjbEI9ssYaDs0ff1x12uUrguuDzJ2Fm3wHhw7Pz7pMMhrov3RL7L8UqdzHT8y3yy5Fn/Awsuq03RSF2wTY6Qzq6xlSH2wRihF16DcgPhPHT+CSfBZtwmJcNddpqx/UF5cQ3bqNt5/5Wc898oYjlQqQiUQY+nmAHfBHl5/6Sc8/85cTsr7Xn80DUjfrlA/qTvn8bsXXmTt2RpCw6Di0HLemzCZRZ+sYsGUt/jV7//MxBWrWbtuLu+Mn09q+nFWzX6dn7/4NqsOZctx3xQ7xQJZS/BmMuHVX/L32VuplDFy6PVPQFOyJJed3cvsmdNZsGYVK5bPZfzkyezMgRAJQuk1D9mYe8oy2TDzDZ7/w2tMXrmc5auWMWfWDMZMms9ZFzjSV/Pm6EksEl9f/onsmRZ9yI9+9lv5sJ1HaISXE1uX8eoffsHfpsxl2ScrhH8Ob30wka2nC9AfvlV1PusWz2TKgsWs/HQtq9csZ8aMWWw8VogzTFGWd5L5o//EL177iG1n8vCJ7jK5KTi2irc/+ohlW09RUX2WVVP+zk9efJP1h85QVpXF6kl/FzxeY8aK5SxduZR58yfw9qQ1VMr7Ni91A2+8/Bv+/PESjmUWUmPK2Mn7SY+jlQNgiO0VRaksGv0Sv3jlTeauXs6iHdQo/gAAEABJREFUJXP54KMPWbAjA1OwFJD1dSELn9JzItZZOwcd1j0yRIkXCq4yzxJkvsTLfNFZz8NosVEpmUf1eBIT9Fw0CMi7R2KzlnzR8MsvUVqbUJ55jIlvvMgDz7/LuPkrmTJ9Lu+/N5qX3l3E7nMelPhf4dmTTHntz9z5gxcYu60AU4kv+6s49skCXvrHaBbszaMi+xgf/+MlHvnVx6w5KjSIdLm0An5Z80sqwJO2mfHTpzFfxmvxrHGMmbuR3BowlPb/AErWHQr28ve/vMHGjHJ0kDcgYyeX+Jef05um8Ns//4U3P3iTV//6NyYs20GV+Lrn5HJeeuUV/ibv7r/+46+8PX4RZ6XjsOI9fPDmX3j59Tf4x9t/57W3JnGgyItD/FjMx9SCHUJYmc3qRbOYu2oFqxaMZfSSzeTL3jtUlbN77QJmLFrOJ3LQHztzDgeLIFLe96l7PmHqnEWsWbuUKVPG8slpP2EyvrI8Ysk2se7WOlpv/KLCDfTIyrRFr70Jsj7KtkrWCcFBFIuVcYyQDyyGlOOTnLgdylofRZwo+sWXLEvItwNZ2wLSZ8BaZzRuWo9AIGDV++Wu1x1tsq7TZZ+8BwPiR27BXYbA4tP1VhZCUwTotccnyvqlE03vlzoRhfYL/ewRfwt3V/Pp9L/z1tLDBAQD0+9D7zO0HE3jE1CETXTj4j7q1de167uW7xBQ4uWdEy5Y6GfTUERFO4kNF/QEDE2n5ItOnGAbJ+8XgQlRGT2mJorg+0n2ozEOwrQMPyghihaZUfJ+kqkr9Ao9PnrPGi/zq/7YWPvgeIfsgxVW/5pX5pv1bjORfsAdLnNVy9NjKnWazpAoR6zURUoAQetYl40QBwnSh37nITRxone8POus348uGQDNr6eNHpNQ2UvruR4hcoJ2gTPUIWvDhXVCv491IKS6BmufLSrwVSn4vgulfafm+PJzKRYf1/R1/qDHS+ur6y5kE9nuUn7mIFMmT2LhmmUsWTiJD6cv51QZhLu9sl4vZOz0Baz+ZCkTpkxnp6wbDodgJAppeXU5YI2htqEuO9GYhIn/uUOlTnDTeOhsjYUwWrZrOTKmETL+CXpMZDyt8QtASNgFPi1LY2kIiPqvHyXGeMGM2pIpjOfnggg3DPDnFRLXpS+XN8jh7b+MY9b6A+QWi3CRUx8bv9DXipHxD8icDeZ61XXNYjzykV3ao5rQqbGcEzKLrPM52iYZaI21liemXeCpLVk6WnNO5rLQB6sVpuwt/X6JVXRuhVGYL7EK3WKKfwZk/yBZeM6TS0GfJQLOBNq3jKMoM59qvjqZdXqJHL+Ug7qZQfm6TrKItYQEfSkgGPhlrvul/yC11Vj/R94fei2IataJxq5SMguqrFa91wpIH0EcAoK0rjYteVadXm8kBwTcgEUnfch6ZfVSls6bf/oLM/YXaybBJRDUsY5O1hUoZupfX+OtledwRDeka0oIudn51CBDU3mU1373GrOOBSdAoK4Piz9AnY1CetEVqG3X+lk08uMXHTVW+u6rhw9KIRtMVGgMPVsmUJaThxwxLHmWnFpZ0rVV99mf+j6g5Wu7Lxof6Ve8n44dmhMoPEe+hjW+DW3ifWRmldbiKSrU9nNe53odmaK/RyDISt/MR2+PZsG6laxYu5rli0fz3LO/YO7eHBxy1vLLmcsvigq5NT7aTj1+fhkPn7zbpUkwM6XNh34/GGUHefcff2N1hrwTZV9oxbxkTPyisyHPBal72VOQzK03DyRJ5Ot3iIWJ4CfqiiwuZK2vz0POmdMsmvYR783dQmUYOB0ywll7mDR2PIs+XcmCORMYM1/2Ex4INWo4sG4GY2fM45NPVzBh4gTWHCvBJX15K0o4fWg7H73zHosPZuEQWRpXbdsXZe3n+h2m8QveTQIytG5fEfNGv8LkrWcJDQe/YKFpgtkM6i/ABJ8D6Ls8ButN0GWXzI30bbN5d9pCVq9dwtQJY5izOQ1/iLQXp7J80Txmz5/J3E/2USRnfr2/rNNRw2Lnr0fA+HqS706hZDT8GDTpehmXNgmjUc8hPDnqbh576B5+/swtdE2SkURhOJ04ZEPncDjEcR0YenHg4mS4XITKp1ynCtY7nY5aHgPDqK0MNllOVCGBaqcQZ++YzUcL99Dm2uE8NvJWRtx+G08+9jD9GhrWXzG5U7oxcsQdtJWXp0927jGy4QxzakGKcNlkRMuGSvYemKKT3jRF6bqoKCLdbqQKZTiIiDSIiDCIlru8e63+lfQdKbRRkU5iIiMIk02NlnohBwgog5DqLE4HWnF50zAO7t1DhRscMstlDuAKM9CBREOYlPDHSDAhxAlRjkr27NpORomT2HAIoHC63DTvdw8/Gt6TBrI5DJhKXv4Gmkfrpu2KER0NkaVlO1wGkaJvpNRFio26XskmLlI2hNFSFyV9xUk5TDZYYbKB04HNaKFTwo/gHS40mjdKZLgdoGXqpgtZySbTSZTGIDqKCPkcL2wWnVvs0n1Ha16ncFjMSnAzMeKacEWfDkSGN+LqYcN54uF7ef6hWwk/tojpWzIIiI5hsrGMka9kSlgNsTU22pCDgOggclwSoNWydQ5zCYFSRIgtejMYKbrEyfjqTbO0WLqEiG3ajkjRRdtqVYqidfbpNqcmrpdNkWlIEKsgJ4fGvftRcXQn6cXIGChLCR8uel03ktsua4VfXjxhUU7LN0JEN62HUFljq/vU2S0dyFTBwkVwjRJdQrTuYk9dt7rd5TLJObmLSeNWcajIg6H5RGFN5hLHK047zMIFy5g0YykLPz1Mnge0bLS+/jL2bfiU6TOXMG3RZg5ne3CHQHW1if7rnLp+Pn9XGIaBw5DsMLQoLkrKwCWCQsUJtF1IMmppHQ4DQ7CUKusyDANdp7OoZNXV/dTIQUlv1E1vObn5xRTk51MlByWLLiCwhjbgquHP8puf/pgXnn2WB266nC4dutI0AjzyxrG6ESCC884lfuckJiqSMAHSXw0JXW/iqREDSJGNtN9UhNWOu/bBEI2j8DrFl/R4Rwr+4e46zerdpZNwCThERjmIjoqSzb0Ty2blCMoTPs0vUx+lDNyCi1vmbT0JoECvI3ojHlHrj5ESsHCLb0TLfIsTX5apKXNBSMXXw8UftD51c1TPPT2fdT96PopK1E9iBnoOaB+KjowiJjwchxDpeqeMkSVL9AyTeV2fT5f1gU3PJS2/br4b0qB9z6GxET7dr9ZFzEDXaTqtv342xPDoaJd4v6JR9xt44OYriPRjrU+uWn6tV/15qfGPrMUhXII6IbI2aJkxgrPu23C6CJGIncYhOkbGVHQQV0fjECnjoPvW63SE8Cql0HSRQhMp4yvwifYXrqAdDrQNUbImR0eFybohvQg4St4nEZpPcoSMh6pj021C4pMvTJ2vupNHHriLZx+6GefxRczaXUyEr4DFSzcQ2vkmfvTUCC6Py2T+iu0U+0TXlM7cdf9Inn54JDd1qGb+/CWk6TkphyyX2JlzbD9rdx6jWjm1WyBTua5XqyjDRur2tUycvlgC20uYPH0hH079hAPZ5ZzZt5nJUxYyafpiK4+buJD5645RIWt0aE0J+zdvYNrMRUxbson9mZWEREBN9mkWzJ7P8oOFOGWj6fAX8OnC5cxdc5gyJ8jwUVORxYpZC/hYgpvpFT5cLpBvLlRUinqCxXkFP1sQ7A09v3V2GHp4LAqj9tnh0HVBVJVV5xC/NHA4gnellFV2yrO2G0HEcEib8Fk0UjaEBispDC1DZ4dBsFrqhMZhGLVtwb4MeXY4DByG0BFMSimsOl0vWR7RyRCauvq6Ol0PCo980C2vCaFtz970aNMKVXiUHftyUDKOfr8iwu1hz8GT5Jd6adyhJ33bJ6A8AUyZzI78THJDGtGlZTT7tx9GXAnTmczVdzzE4w/fzp1XXCLv4hSuu+02Hh01imG9WpPY7BL6dWpEaGwr+vVsSrhhEhD8ZdgozkwjodcACRTv5Vi2X9YZw1ov3I5KPlm4iJIWt/CcyB1130Pc1a8pXlnjlSFWiFGmzyQypQX9urQiMqYZN4+8m0fuG86PHnuMm3s1xfRBwBVN36E/4Anx9ccevI12YQEiW3Tnqt4pBDxuuve7jIaR4XQZcCePj7qLRx95mEdvHkCizEGn8rFt/lgWnnZzy30jeeiOm7nv/ru5vYebRZPHsf50NS3ad6J3yySik9vTp2cKobLfqTGcdOjWk84du9Knc0eaN29Fzy5NiYxtSt9urWnSuBl9ujSX5zbcft/dPHrfPTz9+NPc0DmJagG0Y+9LaZcQSXL7/gzq3ACngGWK0UEvENsN0B8GE1u1p3f7ZCKTu3DPA3fzzBM/4IEeTlbPm8PBciSQGsQZSfoAqJxQU3KOVXMXMlHeq5NnLGbSjOV8KkFch8g8tXUN46YtZrKehzOWMG7CPBbvzKSqOo+VcxczcfoS9BydMHUx8zYep1R8JSAY6z+Gly6+/NJjJeMd3aIbgzslECP3R0bdyQ+f/AG//vE9dKnewUt/m8j+YoOklp3o37sHA3vFsGryLPaX+HA4I+h2WQ969Owm+KUQ1bQrA0VOZEpHBndPRskKI+Kt/iurseZ4wOeky9X38KiM1zMPXI//4AKWHCrF7VQoZeDwVbJp9RpOlop8QyTUCVCg/F78EY24/t4f87uf/JgXnn+eh264lEg5QHuMCLoPfZTf/ewn/OKnz/PcqGE00bjKHqpRnzv4xU9+yi+f+xm/enYUXePc+PwiT2TKEKKDp9uXzmFXTWtGPnQXjz3wIEO7tyBC1qUzO5ez6EAN14q+jz8winuu7kW8G6oydzBz2SE6DBvJ4w/cw6jbhqD3CdIdIhadtOoy3cnY9amMkYyfjOvEWStYvTuNKqEKF/kZOz5h7NxNpMmHcXmNUlF4mjnjl7IjqwJvRRqzxi9mw/E85KiBR/9owV+QK6oEX8HBqd+B9d7nWhdniIHeE4bJPVLePW6p1HX6HRUte9VQTyHpWcVWYMal+YVG04VrQvGR8EiHxR8h9zihD5eXoxIMHEXH2bjnJAFNRyg9h4zk7n4tQLB1hzv57HteuhWPAEtH3YfoedF7sNYu/Q51OqEo4wBTJixlZ3aV8IBRnsMnSxYxf1cOGldlgCfzEDPkg8m01fvQe1GX7AT0/t3lr+Dglg3MmLWEGYs3se9cBSFhQl+YweKpc1l5uAAdKA+RQEXa/h3Mko+4k2etZPWuM1SLzWEhJtnHZR88cSUHC724xF5VdJal8sFr2YHC4H9l5IKMPZuYMGk+K3cLn+gjWwnKsk4yb8p8Pk0tR+toikFa3/JTOxgzbQ0Hczz4cw4zfcYiJst8niTzd54EGLLkY6NsJa0gpctbwf71KxgzeQlb0iqt96ZeJ3IObmVKLd/EmcvlQ9pxSgMKfZ6s8oh9kmth/IKbkneSw8qRMVFEh7pRCkkKl+whHQ5D2ozaOqmuvbT++g9JvD5Fq8vv4DFZywKe+EQAABAASURBVJ95eDhxZ9cwY3M2zsKDTFu6j1bXjOSZx+9hYIM85i5aT5lMAIe8UywxgoFsiXFXyEe+JcuZMG2JtWZNlMDL2Gkr2ZdVRsb+LUycLJjI+WHyzFWs2pWBRzZYsjRYIpRZwuYlSxkzdS0HsspRLmTdqOHkni1Mrse3Yme6xae7Lq/Eem9ZAmp/lAyKwzAsWw2562pHSlM6tevIzTLHb++aTGxcAxomOCxmTVOHjcOwANMsBPcYRlDOhWqrzfpRqhZXmQsx4YTK/s8ik/eQwzAsPi3PqrMYLvxYOjqCNIbIqWsxDIfwOXDHRhET5kbgAYxgP7Uyz5NLwekK0sdFR8r+wcHXJVUrw+EwcBgGymJQGFK26hxSF6zEcDgI1jmwYiWG4ouSUoa0izynm/joCNxBpTHE5wxD6h2S5R7kViKznlzpwxC5hiE0UnbUYRjVhBGP3MvgNlFYSdotGrk7NJ3VRww33H8vd12aCKJDaGgooW4Xuh8V2Yr7hf/qFi50UnV9WPwGShPphs9kqw+H1qWWRgit/iw+B05pk6qLuaTC6XYTKpNb1bY4DJGhs9BL17W1F9+UEhppd+gstJr3ovEROzWHQxan0BA3lhzlJCQkBLfsCXUbKBzGBTmiCvWTT+Z0lcwRvzOBftffz3Oj7pC92u10DvMS1rofg7o1win7ifAYl/iPsuZSqJybYmTtdkiHOu6h13k9R03pKyzCSaR83DOi2nHn8JH0SXbgMZV8OHQQKWeecDlPhcnYhCZ14sFHbqF3Ayc1Wn6kUXtmMghxciHJuiFiwVdFUW4eJUWSZY1zCIUyA3iMSHpddTdP/eBOnhh5A94981mxv0j2EUeZt/Ig7Yc8IG13cVOzCpYsWUOBvLsC5aXouEpRaYmc/x0WbqbI+7LLHeYgVs7N+n0Zo+9ihwqISq54rr71fq7tlExNNThCxEbBRdNpW/XLTgkw+qxr1UmbdK+rz3cVkIC7I6Ip1w+/nycfHMF9A5PYtnQuR2RMzmxeR1WHofz6Zw/TouQwOzMK0fL0enxegF34WgSMr6X4HghMs0Yc2SSgd94iz6yspNDRlAEd4vCVpjPtvQ94c+Jixn80hhde/JAlBwuESi7t4HKzLinrwfVjyKOfU5uX886E+YwdN5WZmzJkayPVQiO/+OSQ5fMqOYQUsnr1LmJ73cBVbUMoKvJSVRWgsNxB1wFX0iIMzh3fzieb9ssGyYcney/vvD6BdellOMhnyYSxfLD0IFVucHlyWDN3Jh9NWsKitdvJlz6cDqgpPs7MSbOYOmMaH0z9hLTyACGhyBfp/UydOI3xM1aw/kA6NQ4HViBbKyhZ5idOoSs8kUYgPpk+V/ak7OgejuVBiMwEh5wEj66dzSvjP6HEBxXpW3nrjQlsPFfO2QOfMn/hHOYsmcemNC9hlLNp5TI+3byMsR/PYc3RQkLCTPaunMnf35vCtFlzee2Vv/HWnG0UBpCXDBSc2MHkKXOYPH0mExbvoMSA6pz9fPz2u7w/cymj33uf378xlbV7D7Nm4Sxeeul1Plx+WOwAVSybvdlTZaMyl9FTlnMoP4DE9dAHBnRS4AiUsnHxbEZPXsysFdvIqvRba1WIUc3+dUsYK9h8IBuT3eeqcLiQxdPUnGghnhovAQHL7wdPNWJLNEmJIehNnBJcts4dzetzdlMpHPl7V/DaezPZk1sjiyic3rmWjydMZ/TEmaw7WYkqT2f66Pd4S/SY9PFH/O4vH7JcDo+GAzn4lrJ9xSImzJjPuIlz+eRQIYaMSaD0DMvnzGLslFmMn7Wec6KHU5YmU/rTl6HAJwHdkydNrr6+HwlVJ9mRVgqyeCtpDBSlsXXrVvadysNwVrNl/nTem/SJ1C3h9UmbqPL5OLJ+CWMmTGPMpMXszhTdQ70c3rCS8dNm8eH4BWw5VWT5h/Z53ad2bbfLlI33p3w8egYHC6tFtrTIeDqdkLN3MX97403GzJjLvKXzGPPR67w2YaH1z8SE+PNZNfkNXvtwPLMWL2Dm9NG88saHbEgtQQmvRwIUIgndh77rbGoHlYJZdIbFM6by8eS5fDBpBYcLgiicH2tNI4xyCULyIDvLk9vWyPjOZfz46Uxfd4JqqSZQwibBevSEmbwzeZlsamt0rQx3UF6NR2HKy06FJdJnwGX0bhYrvhCwaPx+k6jGPRjUPUUCjFGEub1kZvm5pHczHJXCb4jzigKG+FH1uQNMmzSFcTNX88mBDGqkMiB4n9gwkb9O2ywBAIisyWK1zJ+Pp8/hw4kL2JlZRZiM+9m9axk7dY4EEaaw9EARSnwEscoU2bocKM5goczzseLzK7YcoNQ00F0bNbl8umguYybOYOysT0mvNJB3GwEBSVixkhSEHEOCg+tmj+G1sXOZNnkCv3/lbSau2cPuTZ/w7htv8IcPl3Kq2Itb9PEWnmLBjFniJ9OZtGwvxQocpaeZN3su46ZM54MF2xE3wGmZb4qm4DZ8nNiylA/GzWXGkhUyN73ilgqHjHP2gfWMl3k3esIsVh3Ix3Ag5gX5HKaXPcun8pcPZMykz1df+TvvLd6F/kMTHTDOO7yVSXq9EP+csnQv5SIvZ+9qXhX9J3960pqLJam7GffxdFnvznD88A427T5BuXThdptyGFzHOJmTH46bzdqTZbKgZTDj/dG8N2Uu48d9yO//Mp4lmw+wcfU8Xnn577w5eysFfrFX+FVNEbtWzeOv/3iHNyevIrVaKotOMWXsh0xet5cVUycxfVsm3upcls2cJnN/GuMWbiPPC4bCwkXgR/aZ5B3ZxgQZp4lzlrFVggemgKexKT+7jxmTg7wz1x2nWrBRwmTKAMvrC2eTrvRqHkd1iZ9KRzhJcWHUVNZQdO4UJytctE2OorQyQLPWbajOPEB6hYP23XvTWPnJLfUT1SCJMDm4llWCK8yJN/sEm49V0aVrF2KdfvEVsBZILiRZTji9eQ4fTZnG7KVLWLx8PrOWrxX/KCNt11LGTZ7EnFVrWPXpKpasXM66vWdkrAtZPu09/vr+WKYvWsC0qWP521tvseK0H0fhEWZPG8MyCUBHyAe8CEchn86bxowVOymVuaNjFfmHVzJ6wgQ+Hj+W1UcKUIKf/gvImpoLetUvCUTWo7/wNHOmzpBg3HzeGzOVlac8Ul/FjpVL+HjqfMbKuC/flyd1cHztLF5+exITp83m5Zf+zl+nr2Pnzl1M/uB9fvHKRPEP7TWFLJ00npffmS5+N5kX/zqWBbuyLH4851gwdY7wz+TtjxdzIN8P/jzmjx7L+GVbWTp3HmMXHcBLGZ/MnStzYRofzFhHalmQvTh1DxPFFydMmc3bExawJ0/Xl7Jh0TxZe6fx3qSVHCn06UpZE2V9kVK1+JzGoqa6AmQDf3XXGA7u2sIZeQyTIHRVxmlO17ho2aKV7BXKJSiq/U7JgQPOZRXJ+CZz3ZD2lv8dKYLY1l3pEO+gvDRApbzvTNMv77oABYUBWnfuTGKkorLKK0L88oHQFB/WeshgqCqOHqvh8qv70dR5jp3Hc/GJoygUpvLhkS8FuWdPcK4UvGJCw06D6N0I0UdZcwFJpqynNdJoYiJLIz6vlzO5JTTp3JOUgHTZsAd9mzsprzE5u30ZkzZkM+iW22gTYVKtoRZn8MvBJRCQB6HPO1uIO6kN7VvEU5F5VAI+hfS5+mpahgYoKvdRUhIgueeN9I7OYe2GA1TKe8xb4xdMfLW2IdqLvt4avKJLjWS9h6uRLx8alxr5OBl89onGAfxil7+minPZVbTt3pkYwKN5xBkDPo+UTan5gktBQPZtNRLhNOX9puVU10BSUoKsoX5rf6HXi/qchqxzNSWpLJ02lgnzF7Bo+RJmzpnIG++OZ8uZEk5uXyDzcyaLVq225uHSFUvYcjxbApJnWDJ9EpPmLWT52rUsXT6b9997l5kbjltri1cDWb+jLyuLTdViv+nzUiGBer/fgz80mdueHUnH8sPMWpuBOCklFYr+d97HTfGnZF+8HY/IC8jJq8bjReOH7Js8Wo7fS7XIlGYs0BHsagzwgbt5Lxn3CARaiIwmWvt1mReNptvl59CWTVQk9aJX4xDBOHDenwRWkWJqNcTfo4gIjyQiJpqYKDeG+EdAfNvhjCQqIoLwyBhiY8MRWIXej5INV6TUh0ZEExsfbgXrTJGGjI+SBcmfk8mOU2V069mJ0Ioasmpi6dSxCYmqmh27jpHUqTfNnV4yiqBxqza0S4RDW3dQldKVXg19ZObUEJbSlu5NHHjERhVUVveAQ8xO3z6fMVOmMmf5ChYtnMo7b/+DjxbvpFTozu1cyJgx7zJu2Q4qHVBTdIjpoyeyNbNUAtDHpDyBT46eI+AWDL8kAK2h9siZwyE0RUc3MUnWvEmTpss6tV8C3ZCxZT5/Hz+fdRs3yntwDofLApzZvppx0xcxddI4XpW94OkyB5Ghso4c3Cjv0BmyPk9j2YFiDE8uCyZ8wD8mLmTahI/5/cvvMF8+ihn+cjasWMj8xfNYsHoHp/Jy2LN5LdvTSmU8AmxfNIG/fDRTgsPTefllsXfFfsoF9DCHybkDG5ko7+TJ8p6fsfoQ5YKUQzxAmqWElMDlgoK0rUwZN5p3ZV+d4YOwqnOsnD+O2dvPYAilK9THwQ0LZJ0VmkkL2JdZjkOCHy5PAZ/Mfoe/vDeGaQsXiA5j5b3+FmtSK/CWpDF//IcsOZyP2zDZu2Icr7z5HpMWLGTOghm8++5rvDl/LzVORc7x9UyQ9/8HM1aRK2PlLj3JwjnjWbw3x9LP5c9mtfB8PO4jPlq4DomhEi4Ylsq7efb40XxyUrCQMRVVkaMZZSc+4f2Jc9ifU00gZwcTJ01i2qIlLFm2QPbbb/HauDkcKzSJDIPywmPMmzSajyZPZMqqfaIPloycAysYP2USM5YuZ8niGTLfX+ftmWs55wOXnGe+8KOTdhCthLeQT+TM8aHgOWPRVs55TGRZl5ZKdsqa8uG4mbwnAf89uX6pk3GoHRC/35B9DxhxHbmiQyIVsj8pI4qEmAj8nnLyM7MocSfLuway5Z3Wpm17VPZR0iqRNU9Z44n8BgwILU9j+fzJjJsznxWfrmblJ8vlrLuBU4WlpO1eLu/wyRI4Wsa8+dN4971/8P78PVQJn95Hes5uZbLsw8bImMzbdpLqgMhzVXF8x3LZS9TyLZjOe8L33rxdeMSH/DWm9f7RBpm1OKRvXcboyfP4eOw0pm88I00+ti+axIvvTmfylPlsPHGO7LwCKjQMMpmzDmxivIzb+EmzmLL8APKtSHiq2b92KaMnyZr70Vy2ZFRInVzSR10/FWcPM/HjqbI+r2DR1gwZQ8PyW1P2lXP13lriCh8v3IW8jgGNkImwayGk7ljLmHHTeU/O2qsOFUidXP5SNssZ4H3ZT8yes4HUCh96fcF/ltmT59TuVZZwQM6pQk1N3klmjJ/G+DkrmL/hCOXKibi8bvpcDg51gFPFlJ4MAAAQAElEQVRbV8oZYw4TZP34YMYWCjVl6VkWzZ7D+Klz5Xy2VOT7pLaEFZM+5q+j5zJZzkK/++MbTFx3khpp0UZoeTrrx6wDn/L+mBlMX7CCjafLEEh1NRUZB5mq91hyBv5o5npZdwWDslSmvPcer8uZdeyHY3jhlXEs3nKIT5ct4pU//YO/ztyBhD+oydjH4pU7OZZTZslS1QV8unCu7APn8uGH05i7LZOK4jOs+mQj204XWTR+ebfJhVOeCo4fYtXazRyp9XVVlsnSWXPExnmMnriUvdleoRJ99IBIDtpSwfZVS/hw3DTemyh7QhmWqowdvPXmB7wvH38/ePMtfvP6dHZlWyhoGCwZ+kf7hCnvGzmuyWMJaxculHk8i3fHzGbdiRKpk0v6kV+ZKfo3QOqutTIW8/h40gzen7kB6Y6T65YyetIcPvp4GnO2pCNTACVGBSRrLtFY+jUJiCxL50A5W5fNt/anH05eyRGJo2i6OnId/5CtFrFx7ejeMY4SWaTP7VrKuPWFXH3LrbSVjU9l3jEmvTeOuXuycIVWsnH2JN6cuY3cwmzmfPgmr4xdzelSHy5vHivmTGPCigOcPnWEzVs3Sb1oJP6uz1FT1u1l+dTJzNx5itNHtjBm7Bz2ioOFqyLWzprB+Jlz+Wj8XLafrUKvl2IClrOIIQFXLN0u78sVbRuIDwdkMUL2cZDYoC1dOyRQnOvHo+JJjnFSVlZKICSJJokRlBQUyt6wiuxyP8kNGxHqB2diU64aNIj2Ce7gfxnHFyfpFqfsdw+snMor70xi4vS5/P3195mw+ggeB/jyDvHppu0cySzECIHStJ3yXpvFODlLTfrkFLLtoFrWwbkzZjNZ3skfz1rLacHXJbymGKekW5/MyUYd+9IxCko8EJ4YR4j+J9yqwB3qxlNSQq7YUOo3CBNGzSNs9vUtEDC+Be13JlVKYcjOvqogjzPnstiyfjv7ZTPklkCrMyKBCG8WJ/IjGXb7bdzYqpIZsz8hzwt8wYgqpSs9sglP4Mpbb+S+QYlsmb+cvfodI03aMfUEFl/GX3WW1JIQGjdogFtedqZEGhwOQ140JiHh4RgSuWsYG0HW8f1yOFU0TIwVxz0tG7ZKIpNiiQsr4+SpXNHdZMuCKWwuTOb6G/tzWec2RMjbQvflkw19g/aDGXn3jUSd28iSvQWElKUxZdpSfC2u4MZr+nFJM5mYopSc2c6bZIoWYcrDyRKn6NeMHpdeQfvQbPZJBFrJxtt0O0iSIPKZ1BPIvoHGDRPx5qSKPRDfsIls7uPo2mcglzYz2L1iCqszXFw25Equ7RzNxrlT2JFt0qqVm/RjZ0nscTUP3thbDpNL5CXjJ7x0DxNmryey22Buvf4qIs+uZvyCw8S3aQ4FGZytTuHGO2+ji/MUs2Vz2qj3EO7o14QDqxazXw7MPlnDG7TpzR133kSbqv0s3ngAbzgY2kZZyN0hAfYsnciKjHCGDL2SAd3bIWsPErchUw5si4/ADffczqDG5bLpX0muTG4xVxZnJCmU4cCQr2q52dlknTvLGnmxHPB3484BLQkLcxDp9pN6KoNKAxolx1N29rj4i0FZ+ibmfppGjxtv464+sWyYM5WTISkk1mSRWhrFdbffTv+GFSxavIHKcJODn8xgWZqLAUMGMbRbLBvnT2d/TgUnNq/koLc1w2VMezeQxUv0M6QvUU4uGXUp+4qOUR7fgU6tW3PZJYmc3H0Ijyxc+q0TEtuI6Ip0dp/IRD5N0jyqgt2791MV25xWEkg/s2OeBIH8XDvidoY0LmTRso2kntzJwp259LvhVq7t0VpeGOUSQAHpSvoMXlq8KySSBFkIw5xK3h5SrxTKn8OKeYs4ZbTmmd++xeR33+Rnt11C+trFrDuURUH6duauOkqLIY/wznvv8PpP7iExewNLtp6myq8Ed7FJRF18Keuxoswkpcul3HvXTbQv28v0VQetemusrVLtj4y7Q4rl+5bz7tIMul93NcOG9SBr9XSmHyynYPcaFhxzcNeIWxjcoxFmiaziQl/7RscnE1ZvAEQbqiWQVyEBCt1clw1XKCHKT0DUyjmwmaz4XnSNRQ7lYEjfAcEhpPK0bJoWUdbkCoYN7kvnZokY3iockS4S5EPA6dR0vAKor7KS0Ga9uPW2YXQLO8OKdQcoqkiVIMFBWl11C8OvbItLDvzizlb3pqxdIb4i2QRN56SzIzcOvZye7ZvhFtjcIm/v6jlsK2/KvQ/cRjvPXmYs34UZJqwyYEIiBblEP9MXwBEVKfPax6n0IjpcOYw7+zRg59KFpIV24I5bhhCZsV7WkByRXcnKhYsoSL6ch4ffgHFsJcv3nmP/psWccHTjgbuH0iYhjEC1FyU6IMnthpx9i5i0LpsuEgC65tIeNIoIEJDTry9rOzOXHaH99bdz/+VJbJWPFPuLTdwOQVwbGuYkOTqEjLQsmvS5hnuv68bJ9cvZne3DV7ibyQu3ktB9MHdcewXGqeVMWn6Chl074pCNdHqRSXykKQdMB96qcFp0TCZFFbP/4HGqRCfPyXVMWZtOz2G3MrxnDGsXLCDbSCDRyON4QRgDb7mDAY0KWLR0E0arqxhxbTcyti9h66lqwkMMmQdhtOx2FfcOv50mxVsYN3Md/kZNiCw5xa5juTRu2oIEdzGfzJvD6fC+PPLALUSfW8es9afRmATkS7behFSd2cmEueuJ73Y1N17Vl3YNowQ1B6qygMULV2N0vIFHbu9P3o65LBefjZQDc0A7pVCh3xsCtOF2YJTmcqYklEtkU1hVUUiNL4RQqUc2Iu6QCBxV5RR5/CiXE0MpXE4H+ekypo3a0DxJhFUVsnnzTqI69KFjgkMCXzUEZAxMabroUhAaGkpMq8v51YvvMWv8x8x990Vu65qIzwwhqWlHRjz9B/72uz/y6ot/5jdPDKZqxzYWb95PYv/H+OCD9/jwp3cSXXiYVZ8cpiw8hsS4BELNCnJyi8kprMAVFUt8dDgOq/NK9m07SnzbTnRpG8GeHQepEoUM0UPPT9OikYrzl4mYB7L+TPpwOukJvbn5+kFc1zWO6qoaTq+ZzbT9AQZL3a39k9kwdQqrzwVo1qoBZ4+dIqzjIH5wzxUUb1jK0jQHQ+Wdc1l4BrPmb6GCaNq5CjicYzBw6A2M6h/PisnCn+HHDFQS3rIf99xxC+19R5m6WNYkI444Vx4bNqYR0bQdrePLWTV3DpvLmzNqxO10qtnP5MX7qSo5LQeElTi7XMWwGwZyWXMXJRI8OrNqNovPJlq0l4Wn8vHMTdaHM8s+sVfbLzcECvziTB36DcCVvpfdpyuIioCjp9IJi2lDm2Rl/SfRhiYUZqeninwJBESnyCGi9+W0cuay90AZ4TGhhBpODMOwfASRHCwbhISG4BIBCkkiQykpafANE1PeO4XRbejYqjGX92jO2f0HKKlB3DOALxDNwKv7ULZzCr/4/auMW7aD7EoIs/xYZNVdIk9JvwH5IJJ9Nlvm/C5W7jiFw+VGth6YjjDCwp2EeXJYuHAVIb2Gc3PnCKoEJ2EDZeCQWVmcn0Nmejp7dm7haLkbMYmyojQKzTgaR4ViymKrDIfoZuD3u2iaFEFJfiaFXiX2IUmhlJJ78FJKWc9KXbiDQp1/NjA9pfLxMYdzR7ay9kAGXtHZpZCkhFLf5C70UvriS9o0zgFPMWfOZJF1JkNsPyPB9wF0TULGTvPXYzXBkK+B4aGR9Bj2nART3uetn95DUv4utp0swh0WTWzLnjz6Iz0P/yAf5P7OT27pSazTjyskgd7X/4APJv6d8a/9hoHRWew4dIRCGRMV1JavTVpfJVT6LuA7ZA+r/CYBZws6Nw6nKO0UprQ5JfBeHdKEEQ/dgrFjDpP21WCEy8nLYhUBQiMXyI8hmXpJf4QQCnA6ccqHyMKcM2xfu4WCuN4M6R6Lcprk7v+E3RWNGdizOX6J2vv1O9cMCtG3gHIRqqo5tGkek+Qj4Hg56H2yP1dkgtvpIu+QvDPkIDtl6njmrD+O/ubtCInEe24bsyTwNm3aOCYu2k6ONFhbG1PhcMjeqyyfwqpKsg5uYd7s6Xw0djRzt2ZR6S0np6SM0nNHWDZ3Nh9P+JCPF++hyC/vwPxCqsqyWC+By4nTxvLB1FWclH2rW88f6iWFzLUwYpr14Imf/YMpb/+VuzvAmhUrOXC2mrD4BiTHhXBkzQzmbivEGR5LfGI8ES4HSk6wcYkJROmvswKAL1BP7meKeu0wlEl1VSidBt3IyGu6UrBzOZvOeUlp2pAcCaKdqUmiS+t4CQCvYeanp7nkigEM7tqckpx8QmOiULm7mbXsAK2vvpX7r2rG7oWT2euPl/dsISfzHAwYdgvXtnOyYtEqcp3hNGgQR2zDjlzWqxPNG8bjqjrLgSNnCUQZJEtOS8un9eXXcc/V7Tm4ZjkHJbhanbWFyUv30bjP1dx+zaV4Di5ixvpUVLiCwAUDraXIHUmDhrGUHFnJlIUyniFRxMbEESP7iIBDhr04je2ni0nsNIjuznPsPp6ORxaXwvQdLFq2m4QrH+dd2Yu+9dN7SSnfy9y1x/A5w4hNSCRe1o7yvKMsmLcab/ubeOWvbzPp9T9xR+cwti+aw2YJgoRGxJLcIIpzuxczfcVZ/OExxEXHERVqgAvKju/mhOxdO13RnzD5IHc4s1jkg9MVavURFSJKciEZon9SQqwEEgwcMt9DY5twzYifMXbMO/z2zs5kbFrNmn1pmCI779gOTro6cGXnlnhPbWF/EVjdhoQR2bAdI596lakfvMkjfeNlHq1kx3HxnRCFVz4AXegxWDKVkoKfTTOnsjQzjptvuJKrL21HtGGKHlC0eQHTDiruGnkrNyTnMWHyGsqEQyFOJ3c9Ftq/kHXWaYApe40QTzZnZTxbt25BeHgoDk8JxX6D+FgnMdHhOGTdL6gMoHsWERcuh1s+EMXSts9t/PFPf+Tl3/yZv//2V9zWJRafI0x8qjOP/eF1Jr/zJ25u75CPywvYkmlKUN7k0PZDBGKb0q9PY47vP0R+ORiGk5CwcOKTOvHI74Xv7T9xawcXO1cutPicYrssZbX9K1HexGvGM/Dum7h3QBLbFi5mv89BiwQ3p07m0W7QYO6+IoW1M+ezLTdAIHc7783YTeP+gxl242V49y7mo5Vn8Wdtlf1mEYOH38LwXin4JWgUqOtF+gyUp/ORBKYrW/Tjxqv6cWmbRBx+cOJnuXx8SU/oz6iRwwg5tpLxa85anH6Z4MJK5fFPGbfqDP3uuI0H+sexUgJzR8tNTi2fIePk5PphgxjcryMJIYY1QqacP2LaXG7tVdp4DjN9zWn0H+NMGT2bjJhuDBvcj8s6NcYd8GMKBFZn9X9kgHV1zva5vLM8hz43DOamoQNoFKiW930JCyXgut/RlmHXX82VDQoY88Fc8omkSXwFR45XcumNQxk1MIk185dxIE8EixF6r6lletM38MbkvTS7G1fNagAAEABJREFU8lquG9RXPt65BIEQ8Kfx0eglVIveNw0dQqvyXfxjzCYC4Y2JkD3B0exwhor91zcpYrIEX0M7DWDUbV1IX72EpalVuFJSqDp9iL3nqqXDAGumTmRlTgOGDb2aG/p1xlV0DkdUCiHZh9gh71AhQsxEyRrpl4eY5vEUHT3IgSw9alXMHzeVXbQT/qsY0LCQcR/NI82H0INs7VGYnFiziAVHndwz4naulbPElEkrqWrQAWfWSfJC23HniLtpXXmUSUv2CTUyBmbwLsXzl4Bi+sowkjsz/K5buKpJGfPnb8L6myvRTQeOhYT8vct4f+Fpug25iluuu5x2YZVky37fE9KQm+8cxvA+cXy6ZDknxXzDZZwXbxVMxFYDWUY4sWouS09HM0J0HhSTzrhp66y5bci7FUnaNh2EdhguQsNduL25LJy/hohet3Jjt3DKZY5FxyURJv58VNY3d2woyYkBUo+exBeXQNvYSo6kZuKIdBJiuHH5KwhLbk37llHkSpD/lATZYiSWFCJ74l3H82jStClR8nUxJiGE7JOpsk8TXWXuuBt04k45/+uz8uI1O/AZWJhTm8SlqJb1pNzjra0J3pQyMAzJsiaZnkwyq6Jo2byhnJVSuKp/O44sm8Dro8ez9FQYV1/TmyhvgICMSrkYVu0zUUExX/yrz2WyhifEhpB1toQ2l13NAzf24Oy66czbk0dM4+Y4co9y4EwpLqOclUtWYnQYxv039yPOaeAszWTO7MUUywf1W2+9htaBw0yZuwr9B1cuGWuTYHKEujCrS8g8ncb6jak06nsNXRNMGvW7lqZF23hP5ltV8z70ahZHQGKMSn2l1kGh9u95BMSVzpf/hQUTw+GgJP04m7bvYsOeE+RX+7EmtCOE6JhYGiQnSWAtgT7dOxFHBXJ+BWRpqfMEapMs1BBOq/atcKef5EhWtbw8PFTIZEect5bKupleD3oSOx0ueVYiTW7WpQj4fejFzh0eRVx0JA5xHIcrXF7QEeKwQms4iYyOkpdrCF4JUOw6XUzHnrJIJ8VIfSROodeqxCS2o1VUHnuPnqU6IDIDXnHWQ6T5G3B512aygY0mKtyN0ius1bf86LJT7kU5ZJ49xN5D+9m044i8KEvYe/gERTXI5lsREh5NtPAKJS4JbMRERVi6uUPDcLmcRMXHE+UrZd+RczRq35VWSXG0bt+DVq4idskL2xmbSHxMAkkNY2nRrg8dEh1UCe45xw6Q7WrGZR1jiZQFrH/nZmSKDpmOaOLF5qSGDWnaoiG9O7UiMjyElORYevboSssoB0VlXqKbNaGRlE8fPUWxfG6qrq7GoxB8AwQwcNZksfNQFi26X0mbhBhi5Wu8U/DUAcQjx05SLgvNmdNpFJTVUJCVQUEFGIZCJw2NqRyyaSrmxJFdbNi8hZ3p1TRIiqDgXB4e6SUiMobIEJcmxxUWhcYlzPCRf/wgsjelJC+VU/r/KKo4k7P5bhLi44hPbEBKw0T6dGpLpByyPKWFHDp8juQOXWmSGEvzrt1pF57DjsMluN0O0g/vYPupEppfPoCu0VAtgVqtoX43OKTr9H1HZZN3hKXr9lBYEyD35FY5hCvxC/FCp1v8SOvoFI805DAfSWJKY9p26cxdstDmpx4jT059BWmpZFTUUJZ9msyKSJwFx1i3+SjuFp25tHNTDPHpgPiZZWjtjykK+GTHqddfEY7ADcWpHMlBxr4XXdqH4ZADSMfeA+kcU8Dx9HxyTqeTG9EM/Z82N4hwE99qEM88/0t5UTaVuYOMmaqVfuGmVLAuslkzmkUqDp84RUGVIUEI2fhoMlP/XMhmwMAtj/u378ff6BJ6ic8kJLRnQPtQ9m4+gBkVScmxPSzecYbGHbvRvUOMUINSis8mZRjUusOFJrHbbzoI9eey+WAVPXq0QPmw+E1xGllGKDhxgBPViQzo3YKEWPGLcDdKgyTCIqNiiRZfVl7hSWpNhzhF5qlUciu9cqD2yMY3FF/lOXZs2kduVB+GSGDYEBtFNE4Z7/L8U+zPNunVszst4qOJigzHIXID3moOH03HRw0Z2rc9NeRKUCBPePXoy43zST/IyzgiIoboxBSaNo6nT7dutIiLJjyhAW0768BfEr6yakoKsziWniu6FXM6NY2ainIyzpyjxh1C5r51bDpZTb/Lu5Cc4KJGbEJwNMQpDu05TEiLHlzWLpbIqBgkjoQhBmQfO8KZah/V51I5VliNtzidk9kBZFmWDZFoKPxhEVHExSXSsEEcHdp3pn2Si/KKCvKO7SM/pCXdL4klJqkRl3ZNlsDXPvJcjbl1cDtyju0lU4JK1RnHCe/Zn9aCu1tsjIkIJcQNqceOkl/jozT9NGdLKkWHDNKrAkTHxNOgQSOaN0+kS48upES6iW0QS8dLetO5YRhl5R4CgrFSLqJiY2nYqBEDB3WlJvUwqWUhVjC1YUprelx1Jdd0cHLy+Fl8pmy8j6TjkY1/ZtoZqhSy9ir0cpF+bBeFoW3p1TGJRPHHSAkeOMTusoLjHDtbibcqi6On8qmpLubsmWz8Ls1rYiXBVh+WokL97Nq8HbPtYK5p58Yrax84Zf1Ce5rcHTjEV32STWEw3AY12YdZnxbK9df3J1k2N8cPHYIW13FHnxBCRD+9hsXFGiIFSwb1knSLWZnPwd07WLN2K5v3plIqJ3xD9PaVnZWA9G947oUX+PmfXuWTDC/FhedkfY2m+6W9SZJ1KLb9VfzwmZ9w/8AmsojVyLwyObz0TR548mkefP4fbM2uQImjm4b0XXyADYfLadrpMi7vkEzxgW3sKwaXoWSNqKdUXVFPDikHTu1he26MjE1rEuJiaXnp9dzcpprt29Jo0KULzeNjSWjbje5xRWzZm487MoHYxCRaNBNfa92Nvq2TpC5O3v8N6devDSGyAa0SNOJio8Uf42kg77mm/a7jsoQSduxLR4W1oWfDCg4eT6dUnL+yqhqUU/w9gpTmrejbpxODrmjC2T2n9U6CI6dSKZYgVuaZs5zavp00Zyuu6pFEvPh6j/7XMrBpMZt2p4nv+Dl0IpUiCbCdS82gUG8QkAHiQlLyWCPrfUKzHvRvVsH6facoKiohM7OYxt0uIaS6Er1mC5lWCf1/tJJ+8P+x9xdwehxnvi/+7e6XhpmkETMzMzNLlmTmZJNszlLObnaTZcryhtmOIWYmWbJkMTMzD2iYZ17s/696RrLsJLv7v+fsved+rttdb3dXPfVwPfVUlSSf4PSNw2zZcwUrUcvpM0epEkrbjev3V2+j1g6vu9Mod8InZ7lx4hxlVafZoHmntDlO/dUDmM0jn2OjMzMKRq3kG7/7BeYMzOfK9p/zzX/8CYfKI4R8rvKtdnQGv2VbxNqqOXX4MHsOHee62TGQ9yUSLq5KMGBxctvr7Kztyb1LRxFUIpUw3cWYi43PClN66QS79+3n2JUyWtUHIXa1YjJx2rZ9GF3RcVnye9sRTS3kNX21twlXR/N/6WFZNomWWxw/fJTdR45xvbZNJN07cv2XkIimZVlE6so4feIgP/3u3/NhwxC+dO9YkqK0x5vPIkokRCNBU9U1zpw6x7mrpTr4DJEc9MnWDrbs8bPv/Cm/9/X/yVf/9vvsvZYg1e8Sd2PU3DzHts372b73INeag+QqF0kOgtT5WSr/9W/xb7s+/MGE8ESRSNg2RFva8HWfyJOLi3j3qRc5U2WLD8n0X8DselhcEvEGzu3fyQ5tmuUVFRD02cQk95HyDBYvG0jXrBA+n+JxRiYK19I/iDThhI9uY1bw20+sZ926e1k9KpONr/yCnYpJOYpBTz7+IOvXrOPhRWO5svkZXj9Yg1MwhHX3P8z96+7hMW34JF18m599qNgYQNy4WOI74YaJxIV7yGTue+hBHpicz/bXX+ZYdSMJQWR3G8LStev50poplO54jQ+Vd7rSfTCrCzMXr+S3H11H5s2PeeGjM+BtpLrC+sntKok3/ug4flIz8hk9bjAp1ZWU1jURli/7ktPITm5j48vPsvdGE7K4dG5wJIgrh0sYv/8E3a99s2ScmPLHzoP6E6y6ynnlsRHNf03q709KJSO7K3369WPmohkURy9T63SlS3EGnTr1pVi0q3VYX+vNeS4ttVc4W9pCuKmUq+U+xbAssrPzKMzPYeSQ/uTarZg/kZWcHMIfTCYzI5mQP0BaZhbepquUmpySTpb6FGieH9R/EL2zbJpamik7e4KG1N7KITPJLOzGmIGZXD1xmvI2+OzmvWVyaNLonO3n2Mcv8caBEiy/Q1TzvPaRKdOcf6Gkjj5j5jG2WxsHD5/F/G2PpprrlIY7MX5cP7KUJ+T1msgTX/06j0wtlu9FPJ2iYNdcdZ5LDTmMGzuSLjkBAtmdGT1qBAVc4+z1JiyNrYiVQZeMOLs2vMQG7a45fsvLhUL+BMdPnKGyJZPxM2bQPX6eXaduItawrQQx6T2uWMXdl2JxLBbXOFel6Jt3y/YRTArQa+xEBoQauaGDjQZttJzef4Bop+HMHtWXSMNFjpyowJcECeGNaWDb6hdKTmfMhKFktjRSWlVNqwP2Z2mKlKVCtIyd4n/g+HEUZWeQnpFKQGPc0bx8QmvlJvF27fwlbilXrb14iasJdfI66nnndjH/07f0VJfTO3fR2G0qcwb6SCoewvQ+fj56910+2HKc/Wdv0hpHY8f4MJ+6vPW45VJ+4l2+8fU/5Q+++XX+4kcbqPf5sDROonELx/ERyi1m9MA+pFDKtdIoSZEb7Dl5A3/hcGZM6I979hCHKtow+nPVT2rBp0QzlNeZ0YP6kmqVcf1mI6ritkosC9BP7xF9CJ+5yPnKZqJaGzVoVzYlLZ3svDyKO2XTeeRQhuX7aWhs1sHNcaqSezGmZ6bWeF2YOjKbSwdPURnLBvnPBzvOExkygck9srAxMutHd93Vw5xrymXyxB5kK8fISAtg4RCP3+DgmRrcRIM2b2/QFmvhyoUbmMsyCRKuvs9SVh+nQgevF2+10FBVwc2yMnadKKXH6HH0kP2ysjJIsi3FCbCUbw4vaPZylcZIhFg8SlPJOY5UBZg2cSA5op+eliz+XH7d1a6fOHt2nibYbzTDcjPJye3EopXTyK68yl4dyI8Z3U/yZzJo/HDSKi+xv9ZVTMkiVzorUO7Uc9hIuqWGlcsbxxGVdqSc23+EupwBzOuXRVZGJulBCzsYJHHpNGdbs5k2sojs7ExmTexH/ZmTXHYC5Inf/E4FdMrNYeKogeRlBCgqzKBoyFCGFyRRp5zdCqSJnzRSQgFIlLL9RC1DJygfzcqky+DBzJ06hJBwZeRkkxo0luHOZbTg015HjtZvSaZ/y1n2XYoxYuIgctS/3/QR5NScZc8lA2kRl89AMxdPXqJGjnbp0lXZp02bksrfon7y87IpLCokN7+QacMUYxoaaRM1S7bU467bkg+A5StmTA8/585doayujagOdqMxdFkdBU7sOYrbbRhjijLJyXLgoMEAABAASURBVO/CjLnT6KHN0IFDi6nUxu0V+UUkEsfbjzXd1PNTt+YDU33q5AXq4xGuqM/NljBV169yIyLI9j8NQruZXBKah4KhGKe2KB+r78I9y8aRpM3OmMaq7QuRrv2roOZp5EUp6RnSqZ+E42fItHl019g8fbWNWKKGykgP6SAZ1wmRlZFO0Law/EHS0vLo1Lk3I6dNY/awrmSnBEhNSRY2CKUU0K97CudOXaVOvEXawmhZg6d2PrksyWR/tlI6NrApgRhHt+8kMGQ+MwYEqb95nHd3XGPi/V/hj77yW9wz3OKDd7ZRKRx+4TC49PgE+W96E1AoJYOszEwyMzLpOWg80waGOHPsCk12KrlZaST5HSzbwhdt5MSBbZyrz2XR/B40lJzmfH2IEQN7eP1HDh9C5PpxrlYncPzySSnf7CtYlkuz5qy927YoZiTRrTBTa3ILK5jHtKXr+O3fepyVE3oSdFFMtX4Tp5/X/wYNSNO/oeV/Z7WME9Mo7jRmOuuWL+YPv3IPU3pnYVvGYC5mso8reMjmtGlB6Xr1n2bAOIMpliOW267wix+/zL6yVlq1yRxVXE3I2W/3MN0NZjs5mzQnSl1zI64GdSIhKBFJKGl0NKn6BJQwE+TteiUecY0YV7jMZBzXwDc0Y5E6TUYBkpN8tClxNP9ou6nXGOf0xy/z6k5NdJpYwjFw7DgtLQ3YgRT8VoLWcIKYcCKcUoPHonkGfHCzslrtKRRkxaiN2PQa1J/wxUNcro7i87le4DF8uOLL8BkXn963nq7kiCvZCUtfTUoogkG/FpQJotgK/DatLW1EdeKeUKIW0zMcDqvNFX+2JqI23FAyPi0G28RfIBTC1olqs2RzDbxsEVGAC+uJrrjpLzpmYZkUSHBl7/v8ctNR6uOiIRjLsox47UXmoaWeSCJISkoQgz9i9IiFpWDeJj9wbGhraCWaPYB1OjHsnOyieI1BYwoKytGUzkyft4iH1q/h93/3SRZ3r+Olp57meAP4pFdZUgE6QcIcJEgfxqYRbRxg2cRbWmm2Cli0Zi0jCxI0tsVxdTgQlUzRqFaZYsCKhAlHHAIhwWtDoQ2/NooT1NVF6T19GSsHK2l77gf8/Y/f5qxoBuQ/rrGDBeZPk5xsyKSnVmDhlhgZvQfSiQr2H6siEJDdYq4CfoKE4GUmJTZ6l58ZPbYqyW1si2KLh9bGFmLpQ1i1fAZmw2/t8uk0HHmLv/vWv/DesQos4frUwQWfXJaCtW7hAdtqV72nQIEY//LeVY8YcJXAxi2bjk9sdQwlpxAywVnwtmVaoOOBuRLqh0Lqpe1v88O3j9Esf4qId2iH5a7L04u+lS9rrMWUtAdxO2ySlJpKS1Ud6f2m8tWVAzjzznP8/p/+jB1Xm0A0XBV1xdA2xbybOrf9RSDeGxIBfyqUHd5Dbf4A+uU40rHLbW4kEubk1JUva4qVbROfjDsX4vKTqPxd8zWlJz7k+fd2U93WSkx1MS2m3NQi1q1fQOr1TfzLv3yL53fcwHYME2D4CrfVESMJrUdoazO4E5grHm+lNWIR0Phu0EIx0GcmDyybSEqbS0IdLdWLvAFtL/owvCSMP2pctUUjaPhiFi+RaNwbB8agkZjhzacDgghVDRG6TlnO8tEDGDFpBQsHwLvPfoe/+fEHXGty8ck3RUb6jFHfGiY5NYQlHsMaawp1WPqvUZt0CS0eYuFW6iI5zF21lnGdHI+e2PR4M3zF5adR8RXR+DCmsSyXcFMUS4mpTYIWjSFL49pRci73pfuI8WTVXeDw6ZucrUhhaM805N64wiPVijK0KBPzSUOx1laq7M4sXreKYZk+WtsiGD1ENF7CwutaEPfeIzoQsDG0DWOWhIsLWVs4QcLEVVkiLB4T8rG44q4Z15HWZsLSrV+2rK9roXDUEu6dPRgrKgy2ha0YHGlu1UZnJkaOtnCEuFGOwS2dxLAE20ZtU4Rhs9ezaEQRqsYyjgW4lkWSYsWl3ZvYX9+V9UsnInfE8SfjOGH5kSsMrp5hIn4/6Y6DHRDO2ku8rVjZZ+YqpvUK0lp1lrff2cCO3W/zt//0M57aeZHyMx/zkxcOcCthEbASGL2LpIjqdgL4mm/ywRs/5V++/11+/s4WSmM2Rk43kMXYuet4aN1a7l21gmEFDm2aE9BlWdKaXDSh/knJIR0EWKp1JXOC4lFL+f0vf4nfe2QF/TL8mifi+IJw4+AhbloOPccuZMGQnoQSl9l/5Bbab8IyTBkUwvLZO9bQRMKXRIqysIQZZy64gTgKwwSTHdxEgoQ2zJJSLPlSq3Skb80Jxm6u2WAyuOUvZty3yQ/MokKsY+a3uOaXsOkftzSv+TWvxCk9tZEfvH5IMamVsMaMZUlWMRWTj8QNXuFzldC3yUd8Tpzm5hYyBs7g8ZWjCTbUenNfEJeE5rSE5QMrTJPGi18BrEU+4u8xjq88MJMcmzuXSHjvQg3xMLGkDEaOG0br0a1sP7yfmtThjCh00VDB+G1CgI5Kgw4HyxNd6RGM0RCz6DWkDw0XTnCh3FX8taUXFyO3QW6eUp15VTH1eohPoSFhOQQjFZysT6JzWpoOJeKEuvaje3ID+w9fw3LUU7eGK6ldBnPPg4/yp3/4+4wNXmfjrnMkkiysRELYaOdPsTyY3p35qxfKfx7h/pkDVW/hOJbnt5HSQ/zy/XOM1Cbe8CzQUMXRIseywFKOEHbTGDJpIQ/cv4aHVyxmQKaFGUNJaTkErWYao2EQNSOTGaeuOtZrcZqUkqIFEsRkTzO2RA7TntBYtCwHgamXuurXlTzem/fiCi6Gk92fJSvnie6TrFHiH9LYdhxR8mAEradH0zyF0zZ4VH3nloKMj4QKBrFi3RK+smYWgVtHOXYzgbG/ut0Bvf3iCo8vKUjJvlf4yz/7a/7lxR0kj1rE/MF5OLFmYimdmTH/Hh5cfx8PL59L33yLVvlxMGRTcX4Pz//0Z/zrz96mrnAcy+eMJkd+FkeKvE3gP3qKId2CcDF8JGRD41tYzVQ2uAQzcrAFEBM+x5Z9tVEyaul6Zqae43u/+JgaLTaNa0hDCKwdT4de9eHdXohzLWwp3wnkM23Ver72lQcprtwuH9jNgX2b2bjjY16QHH/1gxc5f6uUjS89zYcXol5cUTfMZfAkNB5bFKdze46hZ1IdF29WQwisWNybN315AxneLcjVS5eIBMHReI1pwggHcxk5qCu1V86g/RN8BhkQ8CXjDwS0kLQx8b+gcz+6OA1cq0soX0siaIKTxn0gozuDch2uldXjpKThdwLaNE0QsbMY1iOPmtJr1FngmJyMz14uxmeMWizp0dyubeFGW7Fz+rJ04QJy6g/x0ps7qRPeu3GYPgbbbR2Y908X4ZYPJrs1bHrtBTZfaNCmRky5OIYMCclv5tyE5qeWVpeM7E74WstRekxLww0qw8nkZwWINTWTsB1c5SwNkTRmrriXSV0TNOpQ2XXjRBTroopnBqtcXGMlgauYGo25XsyPS/8JOYBuxdWY6mJE1ScSiYg7Sz6UINwcxQoGvXfjv3ZKACItwg2efHeERTAxwok8Zi9fxrCkUt57713O1YKxRygW4dS5U1SEuzF/9nCGjxhF4tx+jlW2CZGDBXj4TJCXTElJybKvHwfjpfoxtxg15Dw4fXvvWPrPwJin+Pd1ZsGaJfSJneON9zdxrckmFAphVd/ihDZsI0VTWKEYPWhQDud18HirFWzbFjawLBvzasvOuvn1l4vYUJOl4mJZPqLVZ9l5PsHAPoOYvnAqxVaUC6cOUhZHK4iEdClQ/Zp+ppclY7imSsW29fOpu6MlUk+rNsvSlFclNL4jxlaot95bNC8FbZ8317XkD+eLT86nm4dD7d4TjVtICD5Vc+31g5v5uDSHtStnkufKP3x5LLznIeb1SaWpNYbjcwjo0CM9aKOwxt2X0bWZc9OLRyoXXsP996xn9ZzhpAhPQoAW4leCmX4+faN6R2viyjNnuVBZQ+G4xcwdMpyuGaXsPnCZuBBaBs6Ujn6OeVdRuPH07+EUXjVD8zWe/tHL7KpoVZ4ZJS4gk3fElVO058eyRzhCXP1t0W5rlL+GgvLFBAkxZacmYTfXEukylC8/OJbaj9/gG9/8Nu+ZBZxomFtdidQ1YqdkEnRMvxgx9UW8JnR4HBZNn1kLK2foOW0FTyzsb7rgKVlvrcqvzXtMuWyTL5f1D9+jeb9FsRjSM5LER4KIchaTW5qcquLQm3z/jcOe/dq0znSkuFatj10nWfO/68HHFL8Qc54OsA0romRuV7WWXlqpb3FJT02WyhPKeaLYsp/V2kaz4yfZsTw8rt5TArK59GJwxqQ3DXFcozPJpxtzuZLRPGsaWgimJ+GTnyUUK2KuK5tYijVtxINJhPSdUJsVDBIkQoO4MXBxyWfiZZvijTwCs7/gKu+OY+GIiKt4FFNcE7MQa6QtESQtye/xGBHOgPJR9IxK7oR0b3DpE3OZp9ff0NDYQflYxA0SCsa9/m4gRHIgQnNjzIAjkogIrdp8cTTRtba0QMFwnnx4Hp2CbTRJ53GtEQyNiHBav2awmzZX/mSami7u4vvPbqZcyWtE6ychR2J7j/Yfl6qWGClpyapPYGKux1P1OX70kzc5VttKm/YYEh5jrsQ0GhKoEUyIzJfV8WxojeLTJlSLeLY7jRHPszW3GSq2+cHwIwTYSQ5tN0/w0genGb5Y/paVoEW5pM+xQHzH48Iq/AmVuPRuZNGykiQdLozrHuPw4TNcvnARt+cgxQQUt11Plwbelf5jsr3pZ/LxiL7Nu5mXbAeqTm/kF2/tpDbaRlj2xrOvx96nflzR1q06F/MuxiSlRUqSy7GtH3I0PpD1i0eRJl7rLp/iWrxAG9s+mpoS9BzaC1fyXahEY9IVb676CoMQtuPCkOVX4yeCjROXj8aUb7XKn5ykJGhr83zS1CcSUc21Kcxfcy+D3DP8+Nv/yPfeOE2VFJSwg/iVi7XpUBGtXwKKoo3G5lIruixLL1qTZXcZwtovPMrv3jOByxuf5q3jNQRDLmHlOTHNr0YtsgAI3PCoB59f/zUNtHv6fw32/zqUFVQA8Ss4eWaCtAwKcrO4sHU/FTgka/PUpyTT2Nuv03q/FvAB59PkrICcRfUpKWL55lH2liazcN5IJkzuQ1bIj9/fbnZDwdHA9AnMCvZg0uBMLp88zHUN2KxUh2DQIj3L4tb+nZyqd/WdRMDnJyCChmRCg9EJJJMiZgKOowARICkpk2SaqNOJbEqaTYroOb4kkv1tHDpwmsxhS5g7cRi989M0SEJkpacTa66mFZv0ZJuQdpt9/qCcHe9yhVvV1FTdImf0Su6ZNYZpk8eyctFiBoVK2H+5gUDIUm+XuPlTn5LZHwrgKCIEgwEtAF0NUFvJlk2SJt3cADQ2tRFItwlZMW2ixMnMSCWkScrnE12fhePJGBB/NvnZ6ViN9bT5bTJSbQXzBhK+dHKkH9sJEpTg76gBAAAQAElEQVQNfOpz2xZ+vft8fgLBECl2K6cPHcHqNZnFU0cwtCgdW7Oq3ygPXQlwU9IJ2S3U1jeTJp5SxbvPsfElJ5MedLDSujFz1nCWzhvLvHHdSZftFGvwLhuCwSB+2VBxBeWytMShW7fupEXqqFESaeASCYdk6SUp6Me2fV6fUKr6hbIYOWUESxaMZ97E/nQSfUsTcyjgxwRsXyCA35ZdtTGamhyjUZN1QDpI00ZInRLYDG3IxK1Mpt7/KH//h1+kc91+zF8lTQ1amEAfTLNovnqNQLfBzJk3Qae5Y5gzdz4LBmVx5cQRGkKWkmkLv/Tllx79PvDrp/3dEg+2fCIJJ7WYqdNHsmzeOOZO6EVAyUThqFn86V99g/uHO+zceoiauIVfk/DtIGzktiwbn88hEYtqYRAjogVILK0b/fNcLp8/wulLESVkMS4c3cGpmmx6d80lr0c3cptucPjsRWrCcZ3+HeB7//Bn/HzHJeIOBP0J4po0wwrgeJdGkXwU+fyhXUfwD5zK5LEjGNslHdvnQyJ5ULd/jL1CAQeZjMLcJJ0/NGLZNrZm0IbaelJy0okrCRk0ayV/8/d/yKrCCt766CRhy0JpMOYK+EGfKrbn+yG/D38gRJL06UgHcdsmNVzFnvMt9Onbg7QAmny4cyXkdynpadBcTz0WGbJ7MOCTrgKSDwIi4PMnkRGAs4cP05Q/mmXThzOqZ6581Y+ljbv0XlP53a//EX8wq4j9W7ZSIpw+UTC4Q0lZ+OPCHabd7wxuJyD+UrXxZpFcNIjZs0axZO4oxZxikiSM8bWAP4DfEZLbt4X8QXWm3htXAQLylYDkdRwHw6fjBEgNpYp3m079RzB39mgWK9aN0El0cyybhfc/xr/+8cPkl+5l2/kabI0fzGX7yEz2UVfXiCufzkgJyd98+OUvOekpWE4Gw6ePYOn8McyfNoTuUpcxuVgyvfEZnkwRX47Pj9/vx+cLyH7JxJubkGeRlWIRrm0knpyJSBHM7M+kQanseeVZrhUMo0eKz1sgBBSvg0YmH+IpgBvMZMIMQ3u0aPejU3IAx/HjDwTwyXH8so+hFxBtn2gH/Grz+WVJV/gsgtrMSZVMbm01kWAWOdrwsmw/ARMr5Gd2UgZJwpPRbSTzZYNFssXYfjkQ80TT4t0ilCbfrKuUz9skp6VK7w4+8ZmUmo4jXF2HDmfB/HEsmjmMAZ1TiKmvZeElVGKTm8e2c7C2iDVrp9EjA20kQHZuZ3LsFiqUxGQoNjTUlBBPKaQg04aa62zR5l/nyWtYMjYbhWasQAEzl6xgcr/u8mPZIC+DUEYhvYqL5DOQ0NCT0O1Mi7ajxD6RM5Anf/db/PQ73+HvvrKe3trcjbg2vqRMhkyaxNIF01g8ewK9Mh3Mn4ZL9tVzUjG6QZqLXN3Nd//tH/jZxsu4oSDgkNdnrPqo3+xxdDYDiSBJiTCHz5VjtTXy8VN/yZ+/tJdmBYeys0cpU6+UgCs9xL0kNGF4VN3t25+Xg6+1ils1FrbjIyilWXaSxiDU10awbBvbilCtTaN0zZEBzQF+f0Dj0sKy/Hr6ZUe/3i0CAb0b26NLdGz5QKrp74SpqI1S0CnE2e37ifWayZSxwxjVK4egxo0lcL/6BoMB/JaFlZRCsnjO6DJMsWskUyePY3TvfLIL0wlXVmJihC2jBhywSCUjBL6CfkweN4KpE8cyYXBnycGdK+hHc65gAkHNu/Jnzct9B42nt/8yL75/nj4T+pMUt/GLfkCyBfwWwYClDbUyes5cyMKpY5kxfSzLFi6jHzc4ebUWX5rP8/1QMIhPcoaCFj5bdCS35VgERdSMPz0IpFqEb5ZCTg9mLpjI/KmjFW9msHRMV8pOHKTKZ5Hiu8W2bSeI2nJ7+W5GcW/G9JbfaUHIXZel8RIUTce2NY9AOOZn8NBcGquvcOxUBWkpCT5+823C/ZbxwKxO+CVLrr+BU8eURzVCanKAgCMiiRitopNWVMjQ7HK27r+JnTaAkXktHD1zjUiyRXqSTUqqjdN4niMKqH0HDSNH83laXjot1Te51QRZmnMzNZ7bKm7RrIWEP8UB2dCT3+/H6NH2eA5IPxYaErRoY3JQ70ys2hPsPhMmLTUkHTrSfxIpopuq+B/SQcEZHQ5U+cDvJuS/YHt6DWAO7TRVkDdiPtOLann9zS00JlkkibTLpy9L/uVqY7XntIf5m7/7B779rX/iL764TOMtiKvJwUotYuzkyco5prF85ii6auxHXYuEDNFrwmr+XoueR8YWENOGgGMlYWwsF8EVT1HhNfRctYXNH9H8NGmMHpLkAP5AiBTpwLb9+CRD4vJBjlb7GTN2gAcTTAqRZDbfLEgkdeaxR+cTO7mV7RcjyrnQ5VM+6MfvDypH5FOXwjQatlTdLEPnmVhx+YTl0EVjpba0XOv5WSybMZ6+3YfQv2c30pJTKSzuQac0x1uAR6UDrca4evosDbJddoqNP9FMa9QiIyWZyhOnKRW+zDSbZH+CluY2UjNzaT13iUs6sUlJ96N9ENqamnBMHJcMMR2AtoZd0nKK6BKKcL6igWCGTYAwzcr9MtJy6ZOfzHXxHBa9dF9cvhgnEMyif7c8GstuUCEfzUqGtnAYJ5ROCDSGLf1+clu24+nTtLQ11XD44Cmas/LpnJFBSHlHNObQc9I81k0dRKTiJo1OUPHF9LdwfD4c+akFimFoLEUJRxP6+uS2tKgNyB+puMA+7dAOnzOOKVNGUay4a2nMBwIB/L4AxsfjYYvC3sMZ1rmVLa+9yxuHqpi+6iEmdXOIyf4BfyqDJ4xgycLxzJ86mO46IXNtPyHh8DkWPo0Vv1fA80tfkFTRDvhtAgE/fr9o+RGceaooZjjiwS8H8AlHSnYSsaZGYrZNlvq11jYjQykXAWNirLvksh1soqT3nMzDq6fia6ikVs6TnBIiXF3DpSvXCLsVPPX3f8r3Nl0myVfFvhO3SM3vQZ6/lIOHL9AYidNUcpSfffuv+P6mC+AE8Dk+sGyS8/rQM62a/QeOUVoXxxX+w8ePcCvehX5dUj0YS/Nb7oDZPLxsDJHqCh0i2qTKF8pLLnkHEeHyLXzzz/6OTZcThOrOsv9GTPgdHNlNg5NIJKYSJapNESwHn+OgIebhNu9SInGNz+uHD3ChLYVuxQXUnztAuQVXdjzD1/72aW5qc+9WyRWulkKyNtkc8e7Ky2LauD+8/yS18ueinFySEkjvrsZLRBuedFxCZN4CWaQ6LVTVt2JL92lJAfyySzDkkCl57KyumpdGMmPyaCYM74FCG7cvsYwRJ+CDstN72FWSwcp1c+iXJdwKNLaRUWNi4tzp3LdkJPl2G1ZeN3qmyDO16BLUbVQyr4Vl2aQXDWTewsnKOaczZ3xP8S7mbUf6sSSZS7yunINaSzQmiulfbHHy/EUatUF68YN/5WvfeZWSRDJ1Z45wNWJj5glHfiYMxGvb+zW7BXQrSsOywLQZPZv36OUj7L7hZ+mckYzT2i032Y8vYGlsBQgEVYTHMnoxPq7vtLxkz1+j4tm2LVrN34rIyCJZflU8fhHf+Os/5vHBUd784IA8xWq3LRDKTiNWX6X8xsa2ffJNB79whlIySNI4yhswksnjRzNj4kiGds/CXJb5wSJd49YN5WJyj8kTxjN9TB9yFItSnTaqNV6M/QLGD5wgGakJTm47jNV3lgc/sme24oVfeVGW8qxa6iOW6NskB32iH0QP+VvbXTHEEkUzO6RQmOlwq7QSy7YJ+v2qByctlex4G7WKk4auJZ+rbfNRmBXEL4cI+gP4LbAEH/T7CcgfMJfimnnkZCTRXF1HWDht2yHo8+PIoQJarwWaG2gQFVttJia0WskUmG/B+AMBLMvCHwgQMHhFxOrI4/x+n9oCBP1+fGonkEOG00xJZbMnq/EHC13qHwz6CQjGsiyCAfOuokbLUv9gEL/sTWYGKXYLtU2O199qaaQ6kkR+nk9IwDLqIUiq8ozknF5MGjOCKcr1Jg3uQnLAwfEFCAm3ZVkerYDfR3tP7lyBYEC+HfDG1ZUD+ykJDWb+uOFMH9qZZJ+DRLkDCxbF0m9ZWRUJy8Yv3I4FdWePcLAmnbVThzN+Qj+ytV7xy3ctyecXgqDewe/pxR8ICAtkJoUI5Pdk4liN7SnjGK8DWE2p4LWCz+di+WyStDex4903aFQ+9vDMTuLVJi+tgX37z9HY7GKTEHASaZIx6Ng4/iABGyJOEkPHTCB68V1e3x8X/jz5FpInoOKXPvyiYeH3mXfVSRBLtAPiz+f4Fc/g0uF91OWMZtW0oQzvUUDQ6MOH4gCfXBYEgpanZ79oJ2nNbkk3Qc3JF/Zv52y4O/esnkjXJEiIhiN6lvYvEuI3XbmhpfEadv3iQyjVHgqFRMdPMBgiJLy28MeiUdp0mKBXAX3mNrSSLTTsaK5rISk7ixTlcu1yBeUjLomMITzxu7/D3z42kdLd27gSzSbVbaJZ+4IZmTZuax2tJJFjkEidiDc3EaHsRhnxIMRawEkrIj85Rk1NHQn5pgUCs1TwLpPDtYVjxhre9+c//7kG7P8c5H8NwpKrNlVe58KNSm5cOs/RU+c4duIsh49s561d14jGmikvLeOGEt0GLT6qb5VxU0llaV30LsJxakpucFMwF6/VEksrJK31Cm9+dIzju89ztbSU66UN7YbXpKoYStCcUMRsxi1bzxj/Rd54cy9HdSp+5twVThw6xKaDF2mJutRWXKekopySm+U0+VLplJng0OZNbD16mavXSyktuUZdIpfxQ/I4vOlV8XyK0+cuUVZ+g5vVMbLzQpzf/TZb95/mUkkJ169dx9dpKINTSnn17a3sOHKeC9dKKSu9TqUmafk1jh3l6tlj7Nl3lrag5K+I0VQXo1LJjN9pZed773D0Yi2+nAKSas/znuQ8fPoiZZXl3Lx+i2gwhbRIFfv3HuRMfYBxY4dQf2YPuw9fYPvObZSk9GJS/xwab0o26bNKumxuqOLWrXJKpOuU/uMYnFzOR1vPc/rkSTYcr2SwgnZO0y1KBFMufdQ2RKiqKKO0vJzKujD1tRXcLCuhpKqFrNw0bhzewsZ9Fzh8o5yKslJqmhNYCn6W9B/zFzNOidKVbS/wxo4THD0j3itKuHSjgf6jx2NfeJMfvnSYj3acYv+5eiIJMH6iHwWCJq5evUFFZRnnz5zjsPzl9OEjvLLxAMmDpjAiC9Kyc0mU7mXD5lMckT3Lym9xqbSO3MGTKKjbx8+e2sEm0d13upyaxlYqKkq5eauS+uaot+lfVnaN663JjBk/mOYze9l/9AI7t2zjmtOPWUPSubzrNZ55/xxXbtURyiygU06y/FQ82o6Sr1Le2X6K+voGnUrHqddJbHV5AyHNHJePbuHdnWXU1VWKXhml0nWVAmJVVRXl5aWyXwttrsUQTZKc+4AfvXSIj3ae4MjFRhquHpG/bGSv5GmyMiguyvUmEalGhC3MZVkuEZ3umLRjjgAAEABJREFUNTdd5tl/+J88/KXf4v7HnuDft4aZvXoV3cNn+fZffJWHvvwV/vnlY3SatoDpgzuR130cy6d25dz7P+TLX/ptvvq3P+Fq8ijmjulJkgPBJJc9v/wej3/9Za67hhLIjHoJUlSQyoVdW9h7/AK7r5QrIJdS3W4wtUOirYlrGic3S8q4VN/GsIkT6VRzmneOXeDkSdnhWpC5c4dw6/B2nn9tB6fOlxNOyqF3p2w0j4mO5eHRXIOjirjwlVy6LntWUF5ymTOXK6lXgpWS4nLuwC5uWrkM7JlGvM1oxvL6WpZFPAx5vYYxOK2KV1/eyI5D5zl/tZxbsv2NyibK5aPlWpRerWojW7qtPr2dd/bIvy7coPzWLfnwWV57eQN7T1+mOhogv2sxytGRudDeAMm5fRjTzWLLe2+wcf8ZzorHsrKrlDZZjBvTm3NbnuHp946wffcZTt9sJKIErkRxqUx0K+qjWB2R1o3GqJZvl5WXUVEXob6mXGOujPKKWpqa6rlVVk6pYl0ktSsT+4fY8MpzvLH5KFt3X6K0sYFT29/hrY/PcLG8nmBuMcUZfjA2k8HiGkBDR48kcm4Lz799jKNnzgp3FVeuXye932h6x0/y0x9vY5N8btfBEurjKHnxVIiVcKmrKqPE8FUfoVkJeql89tqtWjIGTKBf4CY7FC+OHz3G9pPNDNZGXZHfpcFJYcTIvtTfaqZTYQ7JIYt4ayulihXlih9XrzfRbfhYCpv28Z1f7GKzNsh2naikoaVe47JMcOVKLiNUi26Z6JXXhmlQrDHvpRXVig1+kuwwNy5e4PDhE7y/t4Qe4ybQjQYtMsvVv4wqjUFCPRg/NI+D7/yUlzaKxz3nOVcWRfmztJLwEq8u/SdQ3HaM19/ex75jp+RjZVy/do1Y2iBGd23lnefe4N2Pj7PjwFVK6lx8DiTiCXxKZm4dfJV//+VH3NKm8obXXuNHz7zI27tL8BX1YfLAAs4c3Mm+Q2fYfryaoeOnUOSr4/kff5dN56q1cf0BP/npL/n5Wzu40pzNqLGjtXk4nYU6gBvXNUWJTVcmjitWAukSxxa/7TYxv5GmGqoa2hRfMinQoW12ejJ+S/KE26i9cYLvffOrrHrsK6x/5El+5x/fIzRiEnPHDeDmx9/nyd/6bZ74659TEujO9ClDSW9poKa+htraOupk/Nr6WsXDahq1aVJz/qh4P43baRjjBnWn37BRDMqOcvLoNvacCZORZRO9tZ/ff+wvee5YNeaSy2Fcz9Km/5x+Lm/99HV2HTzOwaPnKa1PYez0oURP72fP6Quc2L6dM/HuzB6eTWPZdW/j6FpFM9GmKq4pTl5XftASbdOYL/fmqKrWBJbfprWuglOXr3B004dcDvRj2pAu5GYmcePgh+w6donDZ2/KB0qpaGrRGC7nmjZpbzVGwSpiwqR+nH3jaV7VIdr+o6e5Ut1K1rDJjEm9xtM/3czeQyc4dOIyNa2pTJgyjMptz/L8x0fZf+SMfKfZk42OK6Tk2aeduVrxfu3mDa4oFjbn9WbMgGIKeo1ieI5Leek1SsoqNH5LKK1soPTaQd7bcwtfop6y2rj8OqH5oo3UQA2bNmxU3K+lpaGWi958V84lxas6yW38LiLbXLlepjFykytlTcQbK9mw8whlFY206NCxpiGqMdNEIMXHrfM7eOOj6zTH41Sd3817209w9sIl9u3cyv6qdMaPHIhf8TFhWd54iDQ1cO3GDeEu4Yzm16Mnz7F/71E2bdvG9ZYQNXt+ydO7K+jZu4jKM+c5ePysxv4W9py5gRtwKb92hZtVlVy7dJqjx85x+MQ5tm3S3HWxlEhaDotXLsB34WPe3nKWk+cvc+7cRd5/fyfBQfNYMr6QxkboMm4Oo1NLeeWZd9i47zD7duzkmU3HyerSny7aPGmqruPGjVIqFBOultUpNjZy7XqJeL7BKfF8TDzt3XeSzZu3cU3zQ4OZ08srpfMzHNLcc1wy7dm7jb1nqnBCaAPNxdLiplV6vebhLeHqzRpqYynMXz4X+9yb/PCVk9SEo7K7y53LArMQqZO84UAW3bsX0atbETk6fTN/Kq+1LUzDpZ389de/wqrHvsrahx/k68/upT7hp1F9mqI26TnpTNbhQ+LsDl79cCeVcYuMNIvy/a/x2Je/x7k4NJx+j8cf/2c2l0c80q4ZXHqLNFRyVn5Rcu0SB06e5/jJ0xw6tIfv/fIg3WavZs3QZFoUqy9fuc65SzcwcyXqm6KNwUeW9ydcWY3OkeQ/5d58aOaWi5Utwqy7Q8xQMI4dgLqzu/lgxw6OyXdOHtzD1ottDBk3gV6duzN15jTmzRnLosnDSbMsuivOjOqcYOvTf8afPrePWMBH+ZmtbNx5lrPymc3bt9JaMJJx/dKpvbSPjduOcPLMBfZsf5+z0WImj+uBW3GarbL7IeW2R/ftYuc1l9GTxlEQKeXZf/k63910A3I7M0ObUZWHd2i8nmfr/uNYXYcysFMGw8ZPIP3WITbvOK/Yu48bge46cCmg++DJDA7dYOMm6UublgcqQowdM4Q0qTYm3i2J7t0uhNtaqb9xlB/98+9x/2//Ib88EWXG7JkM7eKjubaausZ6GiJ+Ji9bx+w+adTIx1p0auvqwK6uuprGtoj8BVLSbc5teo5HfvvHHG8VYhGQGUD0QgE9UnLJCzSw88MD7NFYu1RRwa2Scio1R5apXJePGztV3TzPxRs1NLc1K8dsouz6Oc6WhMmWTD3dM/zi55vZuP04e47dpKohrFyilJvKf+uao9RpE7ZE46W0KkJqagqN144oll2mtKpOMbJMuXwJFdVhwSnGlpdR6c3zGjPqc00nQflDJtCDK2zfdp6jhw+y63yU4Vpb5GkjJOpaWLRfEolYpNlbiNdpTdN74nLWTutJS3Wl1lQtXL+0h48PVtB9yEAG9OnDqKF9yU+NsPf9DdTlDGfx3EHc+OhHfPnLv82Tf/tDzsb6sHDqAJxYK/XSeU1jmLS8ASxdNh1Ov8XXv/bbPPw7f8yrhxsZsWQlk4pDtCpXqqmppaHNx5AZa1g5tlBy1dDWWM3Jg5s5WJ7OqGH96N2vH6MHdsfXdpnNm/ZTH42obw1bf/HnPPLlL/HQFx7iD5/ZS3M4Qk1tLa2aC+ORNtrqS9j84r/w+BNf4c9eOEjeuHks6A9bPtxFg53N0FGDGdCjLyOGdKby5H52Hb+JlnC03DrHCz/4Ovd98Xf44c5KxkyZybi+WcSikK64/8zffJ0/ePYYGvImsLSvWf1FTB9dyOF3X2PzwZMcOXGRm8oBL9xsYoQOK9oOv8FPPzzCviOnOXO9FpdPLtuOk5QK9afe5Z+fep/yujK2vPkaP3z6RV7fcxNf63nefed9Pth1lj0fb+f1o81MnjGFbOVuSm0+QWTe4tKNTuXObP8ZDz/029z/hS+y+ot/xDP7bxFItMlXT/KTv9A4+co3eO1UGyMXrGSYfZktO7ZTk9mfcQO702/wYIYWZ3Lr4jY+OlSGq8S8uuIkP/1L9fvtb/DqiVaGzV7K2M7yJsfF74iwfMvVw59VSHr4Bq9tPsYZrY2vyK9vXK9RblzONW0EldVGiNRWcq2klAsljeQOm8BA5zof7jzPydOH2XisjbE6AMwt2cq/vrBLc881WqxU+uhAStREwfV0l6n10MjMCl5+dhP7NY8cv1zCtWtXqWwrYsaoLD56+mk27TvOIa15btTF1Q8s5VxmPPcYMpEujXv55xd2ske2OnaxgnAiUzGqJxc/eoP3Dpzg+LHzym9KNI6byeyUxfX9G+Qflzh64SbXLl+lIbMvU7qFeeWX77Hv6FlOnLvBjZs3qVIee+i5b/HI373LrRi6EuLXaMZi0pyJ+C98xI83HNKYPsXR87doy+nJrBG5HP54Nya2btpwAN/A0YxOi1N27SbXtSar1PhsrqoQ/jJuVDUJJ9hmAOut//hx5FQd4ntvHeKQ5vCLN6q4cvEizcXDmdY1zIYPjnPq9Dle+vgSvaYqd441cV12KFW+Vtsao7KsTDTKuVnZhpmnrqr+ZkkVDTWSs7RCuK7TFM9n8bQeHHrnRd7ff4wDkvfSrXpalNea+fy69l1q6+uFR7iU75TWN2PWRNcUG69dviH79WPOuBxObtzOiTMX+PitA7i9xjJBazGkHUdGcbVxOETjpHnv6/xs02H2HT7FeeVMbU113Cwt5VpZtQ5+WjF8XxNf1eEEWOiycGNh7amUcEO6uqS9jfROBbRc2sWGQ+eUX15VPqe2Gs8YgndFEcWbSWRd38WP3jnI/sPHOaE1aiKzmPSG8zyntdXxA2e4Xlap/aB6qrUnVCK9XC2pJ9xcxVXRMXtEFbLLuGlDqdn+Gs9uOYIZ2xdv1nn4239Ae+WkJsPVfS/zs+01iqWFlJ89L589x77NG9h9oZS2QDKF2oO6uHcLG/Zf4OK1G5SXlyi2R2kLQ+cBI+hlV1KT1oXOqQ6WG6euopQSxf+S8grqauup1Hq0RHqqakpgE1EeW6I8s1QyNJKWU0DDhW28rfXUycs3te9VTll9DO2/4uUoRo+RVuVd17mofbty5WFn5JthN8GNfa/w7y9vpbLuKhtefpUfPP0yHx0pJzRwFEPSqtnx8TlOSZ5tO8+RLb/tmwtNiumXLl/guvahbly9wPkrtdjSwakPf8wf/f3zXHOlF9HUQ/bQbdkkWmu5fvU6B3Zs42BFGuPH9kQbhlwtu0WZSm1jFdvefomNhy9S0hAnI6eQPr37MqbYz4E9BxRvz7Np/ynyh4ynm3LPmOZ3M0QcGeL8rvf48OA5Lly4wK5t73PZ6c2EId2wWkXbFiN6GD34QlB3cSPf+Po/sL2sDdumXT9q/3/4/j+avNT038efKysaIzbXttJtwkLmyS8uXL7OhcvXOHehmi5jB5Gh3Z0CBfXpg9JpirrEMruxWAtXS/WGM8syRk7QoE2h6bOmkEcD8fwxfOHeCcSvXaYkqR/rVkwkVwlRAlv/mV5o4FqYP14fT+nB+sefZFxumxaNGlganOevVtF5/FQGpiWoCIcYM3kShYlGKiKpTFi8lqlFbZw8dZPU/tOZNTiHZp0yjVjyJKuHZXD55GXK4l2YOX0YgXCcMQvWMza/SUGomSHTlzOpU4yaQBfWPvAQQ4K3OHLsGsEe45kmPC0KOgnLxkrEqG1o0mKrHxnhFlotH0l+mwZtvnQeNocFw3O0iG3AKhrJuuWzSa68wJkKPyPnzKWn00JLsIiFi+aT3XaTMzci9Jm8nBXjCrl1/gY3IgUsu2cN/TJcmhPdmDJtGKmRKLWtLgPHzKS/NhWq/T1Yt34xBa3XOaUg1nn8atZP70zdrTC9R89gVGdHyVwMO28ws0b3UeCMUhlRQjd1LFmuQ7+ZK5nbzeLs+VK6TJ7PzL6ZtNYlsHRaZ1sQaYOhc+/jnkndKDl7kcstuUxV0pPR1EjKoNk8uXYuoZrL2sS7QXVzK/97lXgAABAASURBVK76oMuyLOItLTS4xSycP55Q41XOXbnK2YvXSR64mK8+PF+yKJHrNYm1i8bRcuMS18LZzJg9haxYM1bWIJ54bD1d4mWcvnCVyvow4ZY28gZOYmyvdCX0UcLpfZg5eRCJ6hg9JqxkzfjOlGnhdqUtl+X3rKZvbirZnXqS1XJdC7FSiscuY/aA1PY/meWDJm0UBgqKydUCNNxm4/h9uG2ySfYwls8eR0Ab/aXVdaT1HsvkfnnamGslmjeEmeP649a2eX+aO7PvHH7rvpkEai5y9spNbWqFSevZm+Jklyvyr8aMoSyeM5p06STmyl/0lGqIRi0Kek9g3X33smb+YpbOW8SqJYsZUZxG5xHz+NrvfIUHly9k9sz52pD6H/zRoyvQmomoP59Fj/wev/fIepbMmsPS5Q/zP3/nS8zpn01GcgIXh8HagCmgjcYouizNz66eQcauWMXSHgnOXCmj99wFLB6YQbOSdBzxZdkkIi3EdeCyeGI3mpsiOD0nSLZxJLTpefFKG1PW3c+CHukUdO1Kjg5bLly4hN1nEuvm9hVVkRAe/QqdS4YEdrWwu6WJLK3fdKb2SeZKSZ0SPLBjLtHUrppcxmghB1E5jdGJ6Yv4sJWItCZ34577H2BkZh3HNO6cLuOYO7JYmxgtJHIGMmdML6qrI/SZuIxFQ1K5dL6EjCHzmTs0R2iy6dslwNXzV7jqdmX1wqlkqTahYidcIr50Zq15gjld45w+foWm1AHMntiHqBZ+xVMe5AtzB1CrTYJz4r0hBuHGJtJ7TWCKxkaVKmwNDEuyxlti+AsGMXNED6JNUariaYycMpFiX5hKJUBFAyYyqVeIOvnWxBVfYPXQdK5euKzDkArqw8n0HdCdWO0Njp6rofe0xUyWbuM6uJMK5B+QPWgRX1w1gXjJBU7eaGHYpDn0zmilKtifxx9dRx//LU6fv0ppXTPaB5F0srNtk9AmTiKjG1PHDSGgeFEdDjBIG6m9kmLEk3uxbs0iMpuvc+xyJT2nrmH1+C4k2lwUysjsPJIHv/Aww3NCmD8p5bbJz9N7MXP8QBJVrcQ6jeSx+++ha6KUkxevcKuhGfPXQXP7j2dCr0waKqPEkroyZcIwkgxtTe4DxkyhT2acZrczi5bNpyByg+NK1PNGL+WBOb0JVzRTOHQqY7sGqW6Mk0jYjFryKOvHFVFy6TJXSm/RokRCapd7SL4IJBUN5cEH1yjuXdHGZSkFQ2YwtkuIplgSi+7/IrO6ulw4d5Wb2rSJGP/SyMCysBKQSO3OuBH9SXFbaVbyHY/HieuEvCUSYNyCFUzqHOeUNkMLxixj5YRi4m1xJX9jGdw5SKMOweJa7UVjMRJKAKMRV8lhgqZGl6TuY5k7vg8JzTHGHhbtl3nK7eg2biWPLJ5KfsilqS0hmi7RiE3P0fO49561LJs1g1nTZjF/9lymaHPWdvJYdv+X+Z3H72Px7NksXfkAv/vV/8HCAT7iWX1YtuoB5g0pEB8OckSmLrmHVTMHad7MZOLc1Tz56JP81mMP8aVH7udLjz7M6lmT6KmdG6mDUF5vxmmTv76szmPSkusYFUEGy77wOMsG+bVgucg5HbhWNcXpMmYpj8/rQdmVG1ysT2fdE+s1NqE5kcW8WWPIUryJajz0mDDF21Ro02LVlzeQRdN7gWSNOg6xcLMWJ6VcbM7ngS+sYUAIhixcw4ohfk5eKaf7pEWsHJNJjeJ8br8JzBqeRatwGgYHzVvD760eQq0WVecvl1HbpDkttRtP/s7DjE6v14LmEhdLK2mQ7xdPXMkf3DeeNjOGlWjXNMthDBLahfT7LTKVL9TKV4v7dsHX2khdE4yYuoKHF4/CF3epq2qk87BZTO+fRVV1M3Wazwv798Tf1KxNQIegxn9DUysFo5cwb2A6NVUNNOkgszGlJwvmjMdXV02jePHJNK2qd3NHsGhKP9zmFmqr6rCz8inMSkJ7U9gBP3YkTH1yHxbPn0FmWzm3WrKZMns8SQ03uXT1Jte0ih276F6m9wlg/mSxYwMa69GmJlozB7JwxjAiN65roXxdOdkN4qFiBvROoZ4uLF+6gK6+cs5qEVhy7SrnqmIU9x5GgR9uNVqMnb2QXoFazl24xk0l/xcaUxjctyuBOGT2n8GX7p9HqO4GN26VsPONH/HznY3MXT6d7iGICcaSzA8++VtM6xLn/NlznLhYS7fxi5g/rjs+xfrWhkasnKEsnjqYsPht0MLUzRuqjapBNF6/xiXNL5euXqU5vb8OjxwtNNroN3Ee4ztFlTPc4Jp4ulkfoeugQaRHIY6FrfykrbaBQJfxLBjfg3othNtk5kDxVJ5Yt5iCeAVlHbEa2V1DHzcG/rSuzF/3MHMH5BJRHmX+WryRIaFR02vcMh5es4JFs2YyS5tN82cvYEK/AgJJnVi4bj3zRnYnXu+S0n8OX35kDcOKM0jx4V2pnQeyatlYzDyT0XsQQ4qTqKtq9tq0esHErmhTLb6eU7hnUmeqLl3jwsUrnL3SwLCVj/H1NcMIiE+z0E7t1ZfceAtNii2mn6sBO3rZY/yPpYNJtSHR0kCy8qbVUzvTWNMqGjaWfs0dClkkiaeiwUPJiTZxXZsX567W03/2A6yd2Ik2yRyJJOR3Lo3KSiZMn0mvtAT1LTbdR81h1rBiHDltz0GK39VXOX3xGtWhgdy7fgUKz2QPGEtxtIwLV65zuS6VBdLl+Fzw9RjBgNQIN2SrM9eaGbzgIVYqx26OpzBs6gIm9clAw0z57QrNN4rr565TGerH+jULybdR7JzE/UvGaa67zoW6JOauupdRBdAa7ME965bSKXyN41dqGDx3PYuHCpfGlmOUI6GN7Gau6jJqMQ/p8H7htKnM0sbYk1/6Pb6wbBwSj6IxK7l/4TSKAhBN6sbK+x/j8ftWMEKb3/6UPqx8cD1T+nQiK8mVFaD76KH0THWpa5TDiUb7baNUEV9hL5YsX0zn5suYsTJz6Tx60kRDPI0Jk8eR7jZSF4GMjCIKs1OkT8mX7HDr2Lv84MUtNCb34rHHH6B/oMabv2/VtRJW7pHRayyTBuTRrIV1c7AL06aMINAYI3fATJZM6EZFWQk3bzWS0XMckwZmaq6NYGX3YeqYAdjhGNXRZIZNnOzlH5Ziwr2r5pBUf53jV+sYMGsdy0cVEmmRJLatHzyfMZupmV3Gcd99S+ib4dDiJjFpycN85aF7mT2ggEByAbNXPcFvPaTyxAN86YkneVLroTWTexCzs1m4/qt87fH7WTx7DkuW3sfv/d7vsLBvGv70rmp7mHn9s3XwrPl8wWP80VeeYPXcOcydu5zHv/g1fnflKEKKtbm9p/LA+gV0CyVoDWQye+XjfFk5xoReeaR3Gsbax57gCw8/wpdE58t6f+ze+5naPRUndwBL1z7AWuXNS+cuYsXiZcwY1JnMnpN4ZO0yBuWHsPNHsn7tWlbMmcn06fO476Ev8bXHVtE7KUGnkQt5+MEv8iXNr1964kHReJwvrFlArywf6b1n8ICZk2dMY8bMxTzy5Ff58trZFPkgSX0J5TBpVH+c1ipZHowyLdcFvYxe/ShPTM3j0hnlLm2FzNf6Jk0H+YFBi/nGY9Owyy9i/tDDLeXYxndNfMBcGucpIaS7zowaNZg0Lz+JYvKNeLiVaHp3euYFqS8v4WxZlLGL72P+wExiEXBNgDM4RN/M5eGUrsxadA/3LVvCXMWy2dNns2j6FHrmZtJ12Czuv3cN86dMZ+785Tzx5O/x1ZVDFViDDBi/RHmD9PDoQ3zpMdn7kYd5UIeQXQMWxcNn88C997DA9Ju3jCe+8Lt8efkYpC6Skz1JMHOSx0bxaL5w72R8Vy5zPdCD++6dQnZVPeHsniyYNghfW1T5QBJjZk2mdygMqf344iPzSK+7zsXLGudauz8wJhfk30PSW7ikPLBeewUPLRyKLQKu+TXqTu7Eo7/9EKNTa7T+vYpfefqC0UU0a2Ny4sNf4LGJOVw5e1nzYgUtmovU9Q6PfvnW73x1Pb21xjyjua+0thFNEfRd8ABfnt+DUh2mXajJYM7SiYqlLQxasJblg2xOXr6lvHkJy4cmURdPZ80XnmB+5xZOKBdvyx/G4ildlbe4DBg1kS5OFZUmRBvClo1hObXfTL7+5CxSKi9y8uxVjek6wiQxed1aFvSEy4qtjTkj+cpjgiFKoHA484TT5OitdgazlMflB6Uz4bTtdpxO8Xj+5xcXkVN/kTOXyug5aTZTeli0xHO59wurGRqo8PCmDVvAV1cPhHir7DmF2YPSadDeRzi9B0umD8cXDhNt8jF41hSGpsVp1rp70NQpDM9O0BqHocse4n8s6EapDiZPa4+iUgdnbS0NFI2YxvQ+KcqPm4h3GsyiSVoTNbbQ2KA+M6YzNKuR1qiPKevvY3Efl4vKI+uyh/OVx+doHwhvCNiOBNJdNHw+f/j4DOzSi5y9eENrgghR5ZCDJ09jaIFYj7aR1GsUC8d1Jqy5DOnAK/EIdcndWTJnkNZVbXSduJAHZxZzUXz6+0zn3pk9Ff9iomAZ78FcqT2m8PWvLCS9/jInz1zVGrWGkNYyX1w9lqYLV6hK6cf9q8bhaL+jPpbGnNkTybKaxE8zuYPHMVP6q2lzKRi7nK8/PE452AXOXiqlstnQMRTa5xPLtkhJAiupMwtXLKDYruL8peuUKP85UebSv293/AFH68DVLB2UxIVT14kXjGWe1hOOxorCJJFQLlOXPMmD07pi5jvLilPfYDNo4jR6pTZSoUOJ4qGTGdXFT7XZgNa+W3MshwnTxpLc2kLR+OWsGJEt3CVkDZrB/DGd1D+G4Q1zWWDF26gqL8fuOoYZQ3OpulajPCRBUnZ3Rg8fSFKkUflnnHgsRlQ5q6U9udVrl9ApeoNT569Dj6k8vHQcaZo2Y60N3Kxupe/4afRPi1BWXk9T1Kb/qGHkOxEalYtYZjCDZ38sC1d7P1XaUL+o/asJS+9lVu8UqirrKRw2mZGavJuak+nbv4gq7Tuerkhi+pI5dE3PYfpK+Xh6A6dOXVMMmMUDi8ZofQcx7bl4g84XpPewgV5+cVEb+zeac1l+7wOM6eRg/vCqLdpiw7AguaCg10AGFKfQUNNoqpFqvOfnP79ZAx2m/M0A/ystngHkLQV9R7B69VIe1ES9RsZfvWwe6+9Zwbo5A0kNZjNp4VLunT+KTik23UdN0yQ+hxGdNfJEvN3GfroPHcs9a5awYGw3gpaPflps/NZjK1gwehjzV8xn9tAifIKnvQOOAlNOtkXIcQmTxqhp01m5aCrzZk9m9Yp5LJrQk1Sfjy7a/L5/3UIWTuxDagKSivqw7tF7eWzFVGYoiN6zZqY2cy1iVhqTli7jtx5awqqFU1mrZGVG3wyCeb1Z//C9PL5kDBNFY83CcRT5XQL5fVj94FqeWDuHpYvmcN/KmQzKsTFBQEOTgeMmce+9C5g+sBAzD0fk9FnF/VgrTJjRAAAQAElEQVSycjGPP7CEeaO64dOmSp8JM/iSJoNlM8awaNFc1szuRzDu0G3MdL7w6HJm90+jWUG6nzZsVi6byeolUxmcH9RkZtFtxEQeuncR47olk1bUk6VrlrN8QneC2tnw5fVl0ZLZrFg2l0Xje+IoOQ9kdWXe6uWsnjGYoqxkhkyby+OrpzGwcyo5PYdxvxLOOYOLSEorZumD63hi1TQmjZnA+pWT6Z7mQ/EFo38LiFipjJmziC9pE3TV3GmsXb+QuSM7E9dGS+eh43nk0dU8tn4+MwYX4nMh4Vq40r+bks/kWQoG65eyTnYyvnKfdL12zlCybJd4HMLCPXz6Qn77saXM06b4oiULWDG+CwktvlK7Dua+B+/hCfE6d0w30lKymLpwGQ/OHUZuajI9R03l0Xtmy79CNLTa9B49kdVLZ6pMY0hhgNY2lL+MZM2KOSxfNJu5o7pqoY/ikeXRTu06SH64kOUzB5MXUF0MSCtk/LTZPPzActbOHUL37n2YPm8hDy4fT4+8HPqNncyDa2cztns2cg3CMZtCbYo/+shaHlu3gGmDc/GF8pm5YB4rl8xi5dyRdEq20F4c5vJyVL2EtfFU2HcMDz6yiofvXaaynCceW8XcQTma3FyyZKOVq5Zqs285q2YPJT9oEY5AIu7i+rMZp4T/wftXaHKcxshuqaRr4RRIsrC0EVReEWXU0mn09YmQpLUty7wQzOrOsvvX8ciyqUyUre9ZPomuKR6Q1+5Lz2fcrPk8KF8Y1jkdxGx27+EskxzLl8xh+oA8U0VSfg/mL56P0emSqYPINEYXhnYqehEXQT/kdc5h+MRxLF+2kHtXzGLm2D4Upjg0aUO25+ARjO6dSbilHV6kPNzeU/2N/f15vVm57h6eXD+HhRp39y+ZxvDu+QyfMVd2n8Oo7uk4wXzmaqHwBY3t6eNGs3LNDAYWFmpTZSarF81SmUTvTIeIfM11LWnDkm+JZkoBc9as5gv3LWTZgums06bs+C5JRCI+Bk6dyxfl0w+tmIk5EPBndGLm4iVaII+nd34SZqNWiEg4IQZNnsPDq6YzuHMKOUqK7l+3iDkji0lLLWL60sWsXTCSAqk46stiyuKlfPHhldy/aALd031k9x7B6uVzWbF0DrOHFIF4jBseVQz+iHYxu42cxhOPrGadbLZEi9610waQ7Lr4iwaz9v72sbF0Sl+0VvbGnekXV/zpOWIqD+twaGSXFJKL+rPqnqUsU7zFxIbCPixeOpuVKnNGdsGJaixie/TD8t2xQ3uTlYTnb3ZyFiOmzOGhtbMY2ycPlDSkdR/KvQ+s0bhcyuIx3UnSOJ+yaCH3LhpDl9QU+o6dwgPr5jNei8TMLv1Yvno5yyf0wG8H6TZ0NIsXzmT1ioUsU5wOih9XC/VZ0s39C0bRNTuI8iaipDFGi8rfemQF9y6dwsCCYIcNZTvdYcWHjO7DeOCRdRqbc1i6aB7rl0+gULqOhwqZu3qldL2U1bNHUZyK+lrYtoU5UCvoP5r7H7mXLz12D19+Yi1f/eJ93D+1h2fXsJXByKkzuWf5HOaN7o7P6Mafw0wlOAb+S4+v5beevJ/fvX8GvbId2nSQZFk2CT2z+41mydT++PXuyoauYuHtEotDj8mL+MKqSXRJFx9xG9vRM+zQZ9x0HnpwFY/cv0LjfQVPPraatfOGkirdhAO5jJs+R20reVDzzugeGYSVE/llw5UaG4uG5shnIWLnMkvz8urp/emquWfdQ+uY0ztEtQ4T61QyBk3lD35rkeJTGhbQUl6Jv/8QZo8t1pdLwrLpCBO4gRymLVrCkw+u5L5l0xnaOaixadFjxHjNuzNZsWQ6wzsnGVejcNB47l27mBkDckjO78UiLXjXzB5IdiiDoZNncv/qGfQxK1TFydyuA1g8ZRKrl89gaKFwirKV0ZXFa9fxBcWiiZMmsmLBFPrnZzN61gLuXzqZ/gVJgkK0QgycNIsnHlrD/StnKealqd7FyezGkntW8sT9y6WzcXTPtMSrQ58x03js4VU8sGoOE3pnYWTG+7W832CKnwFjRrH2ngXMU1xKk63sol70zvLLV2wKew1h+ZqlPLxyEgO6F9F/+CQeWjeHSYM6EUiAzkPI7DqIJcsWayNrITNGdiM3vxuzF86X7y9i0aRBFKY5mh8gkNeNOQsX8LDGxIxB+aTIdstXLNLB4wg6pfg8v0sk5zBi4kwevG+55B6rcRQit9cwVq5ewPIF01ihw+txPdKItKLLkoyW1y+U1Ykp84Vbh9Drls3hHpU1K5coV5lN3yyH/BHTeOj+pSyfO4Nli2exfOl8HnnkHlZN6k5Q8aWvcpf71i3nvpXzWaP8YbX6P3D/KhYPL/Tm8rAWV/6cnixU/F88ewqPPvYkM7Kv8YNv/4IPT5TSrIWGG3OJJRcya+lyvvz4er70yBJmDSoEjdFowiKY1YUZixbwyPp5TBtaTFZGITMWL5Q+l7BWNO8xZfViHl0jnrP95PcZzbp1S7l/1VxWKFasEO37713C4vEaj5LfdWx0XkSK9D1nxRLFpgXMHtmDDD+0RSz6Tp3PE/fOZKBitc6IPF25ruXFFX9aZxY/sI6lRj7x5pq4J43GFDP7TpzLEw+u4OH7V/KY5v8ndAB+z8QepAQLWKKxsGRcD/xtlsZHLrPWr+N/3DNUuZU6604rHsSyReMpcKDiWjVFk8czqWeaWoxj2XpCSqe+0uMi+cES6XoOq5Yv4r7V85g6MFc8Ck6emd25N/NWLGbF1CF0TjP95K8W8vMuTBg7iExV+QuEZ+kiHlw9m0n9cjzc6uo9LY3jFA2Z/N595JsLWCXfWW1y6mFFmNhvcjOMzJI9Fspl+rxpDMoN0Kg5uc/4GSwd2wWUBxUMmsC61fNZvngO6xaMoTjFoln5VEqXQSxfuZAVmlvXLZ/O0KIktLeGlVHMLM0BaxbP5J4Vc5k+IJdYGFrtDMYrV5neJ4NEXN8RPz1GTmDFstmsWTCW7pIxKnoR+UpWj6EsNfWLpzKySzJa3xKPu/iyejBn0RzWLJ3FjMEFuMLrYkln3CkmvnYbO4vHZKdHHlipMbmEReN6kaTI0SL47hPm88iSCXROhmbF1ZTOI3jkC8sY0ykVf3JP7vvCSlaOzyfVZ0mPcW6VNNJv3lRGZvr0LWqmWm9SL8lB6D1uHI8/uVZ2GqMNoRle3O8+YAz33rNYvtiFbDvCiWPH8Q1ezTf+x1q+8sQ9/M/75pHUVKoNFVdzcj/uuW8NX9DYXDSpD5nJqYyds5iHF4+hc0aIzkMn8Mi985nQLZloMJc5K1fx+IopDOrahRmLF/PQkon065RGr9HTeVxz89BOyaR1Haxxs4TFozpjDqGTiwewXPpcpTJ7WCdszWWugrxF+6VXjO4L+ozi8UfmMCAniajyMSezK0u10bh0VCdy+0/ky0/Oo1+GTWNtgooGh4GTZ/PEQ/MZqKSj1clk9IxZmp9W8MjqWYzvmY6ZY4M53Vn58DoWDc7Fln3DhOg9aiL33buCR+9bxAKNpWTPNhadBowXvhn0SAsQ1ZoioPi45r6VzB/ZmUEzlvPVVSO1mZCgTvTrrGzmrVzOg4uG0rVTL1Z4OdAyHrl/GU8oB181oSs5fcfwWw/PZ5jmGKdwCA/KJx59YAVPPLjc8+XuCvYNvk4sXb+ce2b2Bs2PNQ0J4ik9uffJdSwZUUi3IWPVbzWPKh488eBSVk4bSE7QJSnVJU1jgdYqytzOLJs1nFRPnRaWZXlv2KmMnb+EJx9Qv3lTMOvlOYPzsCRv8fApPKo57CGNrekDC9rhb/ezbMFAt+Ej+OIX1vJl5SZfFj+/86V7eWhuH0gkM3r6LMXMWdyzfJan62hEKNS/gzLo3U5AOLUzC1Ys5cmHlmNkePSh1Xz54UVM0Lql24gp3lrjUeUbT9y3mCUTexKUb/iziln+wH2sGZ1PS3OC2poETvFQHn5oBbOHFNNj6CQe0xrF9Htca7LFWmtmBlzS0/HW6HRclnm6PvpMmM1vPb6CeeNGsWDhLGYqRvcdNoWHFeeHdU4huXggazTHLh6lmCPdhDr1Y/Hi2SxXmTW8WPOPC8oP5mrsL1swixVaSxWE8C7LwogK6hfI68XK9Wt5ZNV8Fs+dxb1aA/TN8KkljYkLlng6WLtoEv1yzVgGdcX0N8EjQ3P42vvX8Oh6+eSYXqTYwuiGGK784wnJvWr+FFauWs6CYQUEMzopV1nPF5ZPZOLEicqFptE7FUjrzMK1a3j8nvksnT+D9asWMLwgxqVqi0kzptBTMGb+MTQNbUPX+Oh9D6z16C7WwXSG0CR8mYydPoOl0tXKeWPoFJL8JDN06lweXDmdvtk2uX1GsE751rT+OerRft/GmdtvjPLItdyvuLxw0ULumzda61oXN6ULs+bPZon0uFR+nOIKbzBP65ElrJ0/mm6ZQXopX3ronjmM7plBSqf+rNb+zOJxPSlSTF6+cjErZw0TLqObAAMmz+Nx48OCH98nn+z87vK1FayfO5yuxZ0ZLX4fvme6DmHzKO4+mJX3LGflFI2fJEiQzpgZM1hh5nXJWKx1q7iRPSwJY2Hp19Vc3HXYRB556B4ekk4n9M0mLbcbS1atUIzuQSgli1Ez5vPwkvF0016Funj9rGAag5UjPqh1z6Q+2Vj+TCYtXsVX1s9m2oSR8q15jO0SMuAgYxhaxhYZWkfcpzjy6H1LWSpbJDs++k6ez1ceWcrMiSNZOH8O84cW03P4BNavWcSsEZ1JzemmOWkR6xdNpr/s4roO3UfO4DHDs+blCf1y8fDfpgPYDgzWge9XHlzEsvnTWbZkNiuXzeWRh9eyVgd6jsayndaZRfeu44trZzFjwnhWrZ3HyM7JmP2UsPY4ug8dTN+CNFzBRhMBivsM5551y1kxvhc5BZ2ZtmgZDywYRZcsHxHFi4FjJ3O//GXu0AKs5HxmrryHL903g7HjxrN+yTSGFYW8HBVx6+VKWu8NGDlW8/BC7l8xm3mTBpCT7Cer73i++Pg9fPGxdVo3reV3vnKvt560tB4MKB9ZpDG7THssKxUPc/wubYr5SQXdGD95Kmavb92iKUwY1p0MnZaVaXN5wIyp9JE/RDW/a6Aa8tgWhHJ6MXnaONYoZ5nSN4eo8r3UrgNZvGwx6xaOoig1hX7jhHPZLFYumcHorqm0KUdF+edUjf1VWksvnzYEs78UieNdxt3NXlOXIRNZt3IOSxfMZK3GyBCtKcNtBsTCwJiScC0coOZmJZlDxjGmRxZeoyXmVP/5/Zs1YP/mpv99LbFogrBO29vMqVk4SktLhFZ5W1gnUTE5U0RW997lgNFoHANnkhzjALdL1MMRJ6xJzySOUfUxcJGO+khH/W14A2OcIz3dIj3VJdoWp04JQ2OjkpK6OPWaLEVaizNNmrXm20VzMAnhq62OeYvwpqY4dYIV22ao6WQvTmWN6sxfX1afJiWlrlaVtTWCr0/QbOD1G9b3nQAAEABJREFUjMohE6qvM/WCqzfwwiOR7yi1TfRr1dakgWDoGlc1eqoXXJVoNLa43hhraxbNKtHU6ZTBUyf+hZ5IS5yq6jiN4sEMwjZ9G17rRKtViztbljUw1aLRrMCTiLrU18WoU7LmGmnEn4dP9Iwu0GBxpZAGAyNaRv+tkqdK7W1RiMlWhl9zApWIJ6g3sikJaxbdWsGo2aC4I58l7lsa4lSKx7rGOKZvg2SyxGxUx6I1qje8GflNkPX7wBSfrUCk9gb1aRT9JsnvvbdKS0r0fYILaPMyInvWitdGCdfQGMXYwue3sLXD39AQ1yldnFajG4PP4FB/y4a4/KZeuGVmgn7zncDQMLaOymECqrMkn6FtZGuVYD7VGbqmGPyN6t8sfCbqmDrDc1j8NDTEdEKXIJHokEF6jEsPcTmQkSGies1TBCSDKz83+je8tMlejuPSKl02SZZ6Fa3zPTgPv+DN0/DmireG+hgNje2ltiZCo2j7JDtqa2qKSfYYTS3Sl+T1e7xb+IQ/7OGP6aAgTrIS6lCS8ToVyZjdbTiLdQLtVx9QHe2X6yYErz7CHddqMKan297U/iudJTToYhpwCb1j/Ehw0WiUsHbQY5JZVYrHLnHBtZeEtPIJjXZE7b+hgEtKKKGFZ5xm8WtsiHjybCWfjchHjR6MPn6lSAeORnGTdFNbH6WhIUKdfCgqHozPGV2bd0e6aBWM8cu2cIIm2TMm3sPyuybRNMXMQ54/dujeZ/RoubQKtk7+ZXgzfqDu+DV5RltjVMsWBqdHw8Aav9M4F8vc4dWBmOzVIL4kDpb02SCcrVpM28ZXVd+oPsa3/OLTjH8zphtkT63/PfgmwTQLd6vGtMHrWOLWSng0DM8J4+OKA42CaRFsY1sCW2PHkS0bxXu9SrNXB6a/KaafGRuGFzM2bME21EWol058Rq/6Nrg8uvIXU2f6mRIQn2F1MuPL2MaRHCbeGlzGXgbWjBuDr7o2glnMe7KKPyOrztm4PUaMPi2N4Sb5sRnTjvSVEO4WYxfJ0iwAR/z47tKv0Yvhw2/iguA8fUmHGlafGkMB9TO4G6SbBtE2sjRqjBpdmzHs2VZtTRrbhiczTg39gGxvdg8bNO7qOkptbYy65pjcPYFHVzZtEn+t0o3Hn3TSKhluw5tnjfQuS2HwefwKr7G/sYWjd1N3dzE2SQiv8eHb/PikD9Pf05fijYk59XrWm1goH7Elo/Eb48umrUE8mbjjD4Cxf5P8vll+Y3AYuxkem+QLCem8UXjM3BEK2qSm2KSH4hr7cbxxrRHrC2UzdepEbToE9WVh29y5LI0fM7ZNHIhpnGvISTdg8Jp6UxKqtNTDFa2Y4kNEk0xc/h9X7Ijp6QqriSVRxcZEPKz5rZqym9e52thGVPlDvKM/8sW4TjtNn7hoxVQMj6Zv+7uI6LY68H3Ck9te6/WPExUP7TGKdl6F59OwfOoyvAd9CWyNt7hw+/xowZvAs6lPoJKhWWPZ6NzEPTOfmzHgxXe1G3sa/zPjqF6+0Koxb/Rm4r6Ba5EdsMHzO+mzVT5q6o1NbH2b/KJRNjYwnp+YcWbmFtmtQX5sfMTMAYaHZsEZ+LB01j4HcGes2xo7Jl8wuE2MuD3/mfhoZNFuOsbXG5oi1MtfTHu9/N74iZm/4hqDDao3cpo2UxokT4vkMe0B+aCJw0YuEyMT2X144g++xtoRubS1NGIbXRjdif9W+acZG3Uad148U19PfvF4W37zP5O07vpuVB9Ds0m6rlfxeJbuGzt4MrHZjG2PJ/HqjS1H8oum0aOh2aB+nv5VH1B93IwzzRlt8kWTj3j6FZ/maeJZs3TcrLFt/gSRdVesNeOwXm0NKuZZb8ahyTs0/r0+evcHITXZJWDFZJ6EPEe+ort9btW35svkrG7Mmz6SwmSzCWJhqd27ja92+KXxdVOMj3tjwUysAnLlG6Y+Lh3oVTUdt9dX+M2n994xnuUTpuruYlClat517DgRxVujX6Mfn9/C6OB2MTHD+E5Mnb05Wf7XLBlNu6tx2yi9Gv0bPzV5qoExsd/ERq9etjN5lueT0oSxcZNitmlv1bxli74jW5v41aaJ0+ANaH5NSPfGpk3yaxPvzVgycTeuHMPQNDi8OCdb+s1cpzzA+J+Rw/iPT/UG193FL/smxL+xm2c/w7vxF/U3PmHazDyhvQ3liha2dGjy46j4S0lSHNCpckI+bOS05DcZBf1ZrMPe1IANn1gQc1mqSvbJU2XrqOzUpnnVjHezTjD8G9/XGYlotFFdeplj50o5cfwq7++5oI3N0XTOtkgo5hmfN/lHi9YNBmdEeExuIFURVfwwObVSYkxcMbxWK0dv07hs0viq1tokrNhv8sAqrQtaldcbvdbq3fxPzpATJGT7OuXzpjRoo8DUGf7vFMkRiFWx4eXX+OBgBRHpwlKRY3trAoMnJh1Was1i7GdLKDO3mjzGrFfaZDxbTtoiXVdrDq0WLeM/Io2rCbtOa696jy5Y8g/Da42BE49GzoRqtYSQrO3rH7kFlio8vrUmadAaw4zvSsktC2mT09b07uLpQvIb3zbrsioPZ0xzjJnHXQzPhj+jE6SDavFRLXxVetYqtoWVkBu+69WvTmswJJdj21iay2qqYhi5I8obvH7qY3huE3OpaRahoEVM/VsTIXqPnMC4HjmgAWTqNLQxJSbZI/J/s6YN6xkOm3Hoqs3CrHXNt6mPyH4G/u4Sk1vFlUxGIzGtnWPUKQZVVMaoaUyAdGjmm1rp2ZTbulbDr9yW4oLRk5HZyG5kMeu3ZvmPWUt69ZK/SraoV36VsKx2m0lP5p9isaUPx7FxO/Rn1orGPw0eY2sTt23Nn9nyZcf5FfLItHhzuASKS69eTBNPCY2ZmOr0KnESmPe49yH6GpMenBQSVx9XPCH/ulOnsaZPPn2194vFYsRMP5VYB361SOftcTKuvh6ZuzsLvys6Bt6UeAeAGQN3ePfwxTQkXPErG3p0Eu141ebxY/gWzZi+Da8eLtmxsM9Q5ozrQ7ImAMuyjEraqev903QTsiyKF66H1+CIe/xaHvxtXgx7t/vd5tUDMD9347zNh3C4omoZ/jrqDF5Pr6Jo6MQMjCvR7tjF+8CTQQRd09eTLYFauFs3MeE0uZorJcQ79GLeP+HXlfkSwhXDoyM+5VG/ImO7lGrsuCWKcs12u3k0PD6kG0ND7+K23bc6eO/opod7p5/hy8DF46It/uOyc9zjV2B33yJ2W6ftMptxRjv+u/rFRfc2nHmXYJ4cHn9SjNDcoe3VCf5uMubdyGlJzwnt9LoqJg6YedDkY42aA7zcRvG3WbmPWXu2Km6YOUJD0Ju3vXlMH3Hp28yBJrcy+zeNgjfrK0v1JjY3eOMZL/eMCYeXHynWeLHbrGNMDFR9o3JSVXtwd+ZSzYExxTqTb5qcsKUtgWWDJV2bWFSn3NGUWsUOE398yinMWqRFuJo197fvpVgeTlv8RJSLNWp+MLRMvENyZxYPYe743qT6wXYsTzafTs1a6mq4VV7KlZtN1ItPM+eY3MLkHCbuNUouDRJvfWlyAUPP5AkB8eCT3sw87NWLZ8y8r5zgtlxefmByGuUspp+JX2afwOC/DWOeBs6nnNCvA/Xpk0fRVQfQCdfi8+s/14D9n4P8r0FoDHOryqa61kdNrUNdk1+pYYDWZp8mf5uKSouqGofqGpuKKqisdjy4Sr3fqoTbpbLaplr9q3VCWKH6SvUx+Ko66qs66m/DGxjzbnA2NVuaDBzlDDa2N0m2vxsXse58my8ULG0ceZXj2IIVnGZKy2uysB0Hn4rTUWzTYNk4WnU5d+BtLMC6U+/gdMAbcDqu23QNbEJKSiQUkbCwbAtbPFl6osu2HQ024VCdY/DoqWrBqV7ftmWZTwyc135XnWU7Hr+2JRDBOYZP7wMsy/AtvIJ3bJv2y8LxYGwEjsFp5DXvltUOb6t/+7sPx7GxbUdPB0OCT10WtuN49J0OGEd90WX4ctRmik91ZkDrkIo85WX5uRaF+Q5FBe3lznu+TUEu5Kvk3YYp9NG5SCd6RX6K8i2vLT/P5pO+pk74DC71N30LOnAX5EGecBWovlB1hYIpzLO8uvwOHF692k2/O+VOm91OTzjyb/NT4KNI8AXCY/oWFdji2aVA+IuE39TnC76dbrt8pt7Qzb+No9BPcSc/RR385Qv+dvH6GfqSu8jQUuncKUBxkSM6iI6R3Ud7m02BcNzum2/wGx7yfWRmOAr2FubyfgPJ5Bflk2ajOVITsiYB02aKZdnyP5/saGPs6HNsLD65XCUl2I5gHGzjKGqy9O2XUYMBP8a+qsKyLJwOmzufwcGnLgu/dsENjwV5jnzB8vR8W/ZC6da859+ll0+9G/1IL50K/dJjgM6FjvRgSTeO9NL+nu/pwud9F8pexgZGV4XCXWCK6vJzE/JHV7RNQU8VtRk/MfCFHTYt7NBxoWgWyxa36bXTMDQ/awfu4gXyPX6dDjk7fFV+k+/Jd/vbwfMrUyd4w4OhX9jh8wWya6c7fbiDv0g8GtgiTx7u0CoSbKH65uUa2dqLef/ET9thPd+S/vIFl+fpzBGfpti08yc48eS1GXw5bvv4EewnuKx2WPHdqShAF+moSLD5gvF4Ey8FwlEgHg1PBXniR7BFBT7JjGyQIF/whZ4sNkbfhtfP9s8XjnzRb8fpqK8tu6u/qe8oeXoWeLhtjS+bQtEuzDcyGJqf6LpQvBQI1sAbvN7T6yeeCjqKxmDnIvNuSz719fhzMH1Nn3xPvvZ2Txb16yQ7FchfPHy38Ru8opev788WA1cgvF4/tZtvA2Oepr5IOO8U8WPsatrzPdoO7W2OdGaJR8lpaJk+0qfBkefBiUfRb9eLT/xb0jlkZkBS0t3j2iKQW0injKCCBFiYyyWhucsLFx3j26cdcp/Gud0OgG3bd8a93VFp2TY+xYeAv30Ocbw+tnBa2I6Nz+9gx+upSurMiG5+blY0YwUFK5xmlsSycTRH+wTrOA4+FduysJ3b73RcVnudh184BYMuyzL9HcUZP+0xSpW6bcfB9xlYVX/qNn2TQg5ZGRa5Zr6STu/YVO+FsnGRSoFiRbtOnTv6z5MN8++CKZQd8gVXKBubPoWyQ75g2uHkU8Lj1QvGgzPfHTAGLl/2+8QPbArUt6ADv8FleCk0fVWf/6ki3B00i/QsNMXgVinIQ+PNoUj+1D63+jAwHh+GX+EpEA93fMv0Vbm7vZ1/ja+O+txMja8uRSxdt4j1c/vRqwhyciwMrQKNwU9wWRg+vf6SzfDv4fVkEM/iz3yb8is8S+5P8DjyY4d2ONvDafB6Rbju4O2Qx9ArEK9mzjDxu9DoQHJ68N7TEi6jB8nk8dCO83a/drpqL1CR3oo8vLf7WORmQ7rZiArJh+X7VodHGV/y+Wws2yG1oJD8ZN9dY+s2kHy1wy8dPU0x/u7Yt7GAZVkdY0y4PqkGy1a96tBlfF79fSOVV/YAABAASURBVPJv+66+avnkFkxykkN2luSUPgo9OfiU/vI67GJslyfdGL21y0vHvGPLrjZF8pHbMPmebZxPbCIbeLrV09ii0NDy9OrQbjPLgy1Uu4HLE80C4fPg9Cy4bR/h7VTkp5PmKK/tDjzkq82rM7h/jRzteOng2Se6poi+8H+6zaFAcuapGJxFsm9+jiV72oRkT1u691TuC5BVVEiWt/n8iUrvfjN2Tu7Qb0G+Ixk7dCXZO+W7JKf7mbN6MbO713H+1AnOnDuKb8AUfvveYXTJcsnN82H0Y0phh0x50k1KMrIz2LbdEQvRZenbkW/gXbbjqE2+YPi1HRzbUr2iqWXjqK39W1W6LcvCEi4D0h7fBad6JYf4aGL/prc53pJJz675mD11Fwt1wHYcvbneu+dnluoxl4VtO15sRZtTCTNhCL+jOscQsZDfu8okXW7TVY1uq72f1iR3fF59XRUBim/b64cuy7L17WAJC/o1eN0OWsKqNp/kFyFXrbaN49F29LTxWPD628KX0PrUEq+CN3PMnfGijgaveHEcG1c8eMWrc7CEGttWP8fDiba9I1Gbunq8NaxZj9Y2puMPZFFV6WpNjNa+7W3t61Trzjq4ssqiUuvhSq1pTVul1rre2ld1FepbdsulXGtg0+aVCpeyCrz+La0+zcm2WFGxbMDCth2PJ1tM6ubTlyv4hOQxtRaO5PMZuW8/jfzqZAnHnXrZ2bFtYQbUZvo4tsXty7Js6cGHrbq7+9mWo3W4jfYE+U2X7Tjq6+DYNo7eDV7bbq8TOrDsjnYLcxlajoEzRX28Wstq72vqHBt98tnL8vDIxh6M4+H08GN5fY3/OurbXsenLkt0TLspzh0AC9tpx+N4Tx9em4g7nj7b5TF+rCqw9C3dmu92eAfHSaKwOB+FCM+L+cz1abq2OBWAZamf01FsbrNjO+28mG+rg1+PHz593W67w4djd+C1O3A6etrtdfr1eDUwFli206E3y3x4744IWndk+6Sf7Theu6FjWxaWZXFbL5ZlYTuO197eZuvd2OaT/o7a24uNbfFrL9t21E9FsLaALMvCo6F3sNppOMJpcddlYduO188WPIJzPN93cGwbx+Cy+JXLUpuxvymObXvttuN4eBy7vZ9jW9yGM+9YloevXQd4l207Xh+vTvBe5Wd+LMEEAo7mHEe5hJk3HM0Dmgs65oB8zXvtOYijOcVRm0NBxxyZl4vmOFvfFnfmNc2NHrzpr75mDi5SHnZ7njNzU5HmpPb5V3Nxgc/DaeZTU29wG7wGX3uxPBrteBzxYHu08g0dzZceLYND77fnLdNm4A3OQs25BeLTwyl+btM3tAwPOYVJ9B+YT88CC5M3GpoF4j07NYY/KcjwQfnaYK4mM9dHkepN+238RZIrX3OkwVnozbniTzRu07pTdzcP4iVfxcCYfkVGF+prngXqa9o+W3Jybbr3K2JgVz85WcinPmPEzz9/rQbaR86vbfrfV6lxZ8YejqjVXDrE08+8ze6rDfhDYBKG2+3/XU/+D74cnbokJ9skJ1kEApZOy/UestAe3G/mWsmPSQxvF5MI3QH22tz2Ccx7T3gJxq/A/po2LzHsQGRw/kqf/6TN7Uj4OsCUzIkPU6dNCoNbJDuaXI8nVxVm372tDWrqEiquEpQOkP/wYZJBoa+/zrsvv86rOy4RNvCi1d5iPv7Xilj7X0Nwp7d95+0/emnnO8bNIzv40fMfcKo86oEbvXkvd/+IOQ8+HuH8vk388LmtXGlNeBBevff2f+3HsixsFf6Ll2XZgv8MsNvKmW0f8P1XdqK9I69RLHvP/94ft8Pv41ReOMDTT73NgbI2j6TxNe/lN/5YIFmMPBb6z3u3QO/o+k16Nf6LYFoqrvL+G2+z8bhWBpjL9DDP/77STsHl1KZX+cHbR2j0XEA6aG/49YS9tna5LPHNp4rG1G1DRRs4/PGHvPThSRrigrQs/uPLwrKs/wAkyvm9W/nRS5u5Wusxwad921J/G/2Kidto9GXsoNr2Ggs+9c2dy8OotjsV/+GLwaMivJbXx7z/hx0+aZR+PFrhGvZsfI9fvLWPKq8i0bGI+wS0/c1uf/wf8CvW//dwcUddFraSbevON//p1c6DS0PZWV5+7gU+OFXn9flkfFpYpiZg/urfOr72xdVM7pWDz9SJkNdm3v8Xym0eKs4f49nn32TX5RYPW6J9MHvv/6f+tPP+38id58uQCFex9c1XeOaj05g/RIKsYqnwf+myMIPD2PjT/FvCaP1fwvi/r5PbMWfEuHV2P0899Q6Hb3nZhFjuUIYgvLdYA3s3vMOz7x7G81qTb3gN/O+7/p9Wx/+yJBaWZf3/haXdJ1zqrpzgl8+/ykenPe0qF/z1yr1dW3FmNz979n0OlraPX+Nf/38R/m8Dvs3h/z8E7Ha9hXKZunAp966cxz1rlrNqen9SLIPvV3Uaj0NDs03c/NNMAlGafYeg0alP6wnzN1mCfgvrTouGoj6CIZuUoF7U764mbZCqPsUmSW0B9U9KsnVIZ6m/iyu7uq6PvCFL+N1HZtEtGaKu2izhFB6f6KSor5Y06PNutO3vggtog97A3MYfDNoki5btWAT0nqK1UEAIBNre5/avEFq2RVCwQfFleLv9btaQasZWvyT1TxHPpj0kfMlaS+mMx8Pi9VeboXEHh75DwmfoOeI/Wfynai12G6dlWR5fHo8GyChWz8BdfBhaSaJj1myWZREUzpRkC7/jkUVV7cVGbZBk6AnHnfrb72r3qy01tV0nhu+7YYyOkgx/KRbaj2rHqb6e3iW3kUkqIiDbGhwhf7tdDBdGNyHxZXzBfN8p6pCsvnfD3mn77ItkT8jJPinyCaN4wbmKhZ/UJzDvAvcYMO+mRKIJGhoS2ih3Mf80oLp9fv8GDcisv6Hl8+rPNfD/TQ10hJq7hA8waMYyfu/L61k+rpuO/O5q+n/o9dM8/j/ExP+LyNr/d/HqJuJEVLIzUqg+c4CjNxvwBdBEFVcC1V7aNyRc768yxOOJz9SLU81oCeGIeyXRkeS4HlzCmxiFR09BYukc2/trD/po79OOr32N6X6KRkyZXEINCfU1f+Uirqe6eXf7xCq8HTCm0jX0vW9TH1Oi7O38aMECljTqqr9YBXGYMLBeSXjtqlS1i/I2rFirNmXe4m//9m/5q+89yy9eeJ7v/fD7/PuzH3CuNo5JQFyvw6d/bCVKJom7XUwChRIAQ9O0pSpBUS6GpZ/bMCYRNYmXSe4M3N1tt2FSlETZmvlc8W8SFi+ZU3LiJS3qZHgx+rCULH66TbKJRZP43MEheLMzGVIyZPCbBM781UqhxtSbuju8SGcmaSNh0dwkRP/J7Qp3NBYnEcjEV3OerfuvEDF9pAPPfrKNsaHh11TfXdxPwbRD/Eqd8FuW6ZWQbQ2MKz9NeH4Wl1/KVUzjZ4rbAZMQXIxo3PRr5J2ffo/vbrkuWBfzV47iHZ0T8omYcBkoNeIanrWgSM8Icnr/Ts5UxFRt+hgfMzgTdHRVvVwoFvWS/9zkNg7uPMjV5tuYvOZf+TH0DO9x0bmN5265Y16ly7G3nuHvXjiIt22biGHgvSLDuZj/DGqXaCyB6zaz8Rc/4N8+uGwqVRK0y+iQHWxl9859lLevDaVHtYm2wZXwaAn8rtsVfu+vM0knBiam5x2JZI94R18jgz7V020fw+qXUImroFFvGftqEKbmh7h6aB9HS9oZMPjv4LiLfjsuqDh3kB//4z/yP/7ih3z757/k3/79J/zVP/2U57acRjmzMLuSXnoXndt4EsJjaXGYqL3Ka29tpy2/kHhNLVINiTtwifZ+hi/5rOE/rmdM8tym/QlvCdpjICLkcptO/I4uOuqE2/yVNUOj9txWXtlXR68BXQiJFwNr6k1fsSdEd92GoPzabSnlw9df4js/e4VnXnyd7/3kJV7ZecX8rVDJKdKJMPs3vMfu2lQKghFuXTvEv//TU+y8HveQGV0b+oZWPBbD/PUut/QQf/+tp9lX44FwmwfDh4FD6UF+GhzetZer9e0whh3vrfE67736LF/+4jf5zkcX0X43CTdCyemDfP9b3+V72uS9FYPa83v54bd/zD8+s5lLte0x1+hL6hDfrWx87ime3lXhoYz/ivCSy3MolxtHtvGDHz3Pj3/xMv/0rz/iqS1XvT6uGPqE74R48Kp/5ceT2Uoix65k+8fHqfHwJj6xl7GvqXPrePcn3+fH20vwLjHaro/2Me16lZ/9cdv9WjY3sLfF+MRH4iQ6Kk1dTH4UF2xMdojpeRtn4g6tdjkkGiammT5ed1XEvb6f8GLaYuLd4DMyxu7Cp8F+h6+YaHk46i7xvX/6ARsvN3tCJG73Fd6EB+BVf/rHjRONJAikZVF37gi7ztZ67a4ZH+oXN8Xr63r0YsJpbGyADH8ebx2yGTBTF9OA8+S+LZOeHvyv4DS1eHpAV1peFjeP7ebg1fYY4eGWzJ/wIKC77nZaRl/tJSbYdroJz/YJ8eWNC1MpHuJGFlPEqKkyOvykrt1/5ZWyZ3t/Q//TcO317TowjLjtvBvcwunViOZtnLf1ZOrvFANrePBKoj0WqY/B6Rqg2+0d+Aw/sWiMhJ1MoLWErTvPeeOxbO8r/On3P6TcY1t8Ccen6brt9vLoxDH47x6DrugkRCOheS9q9CY/OPzG0/z9S4fbD49VfxufpwePOcPgJ8W9QzMhnXUACO+dfsL/CfQnb5/0i8tOCU8HRk7PVuLFe2rOSC8IcOnAPo7f8mY/gbTLZPgxBTtEVqKcj7edoFZyxjQO2udNA5oQbjM2O55iz/X8r/37bl1w13U3b+3su/+hHm93daWvmMaGkd17tneWiyU83Sc6dOVV/zod/bo6aSYhfRicRl6JIMHa8ZlvUx+T3B5OjxHX4zVu+nTowq0+xb9+66d8XNpxgN7Bh+n7m3SA9BQTjmBWLlVn9rLrQvsEcVuGuKH5CVEsw7vop2Unce3gPo6WtupLrAqHx8tn4L1G78fwm/DsZOA8+VT/iQ0S3CEjGp5feDhlVwEbuJi+b/OlKrF+G5+BUY36uVhiRrg6kH0WjxrbeZBuEtKb0Uv00g7+6h9e4GSL6+k0Eo0SiahoLIqkuLzr9mgoZ7lxkw3P/4A/+LO/54UD5SSFXAwuw18gGS5ve4lvfPNv+flHx6huczF/TTguNMnhcl798bf4m9dPYgVcseNi5oZo2y0+fvnn/Olffotv/+IFnnr25/zzd5/irYM3FQMsAlYrV08dZPP7z/Gt777O4Zuykw/xm8Avepe2v8Kf/+1POdWCYJE3iZh3t+N3G67w3ss/5k/+8p/57tO/5OkXXuQnP/42f/f8dm5cOMVrP/8Xvv6PP+KNXeeol+4MT6a7GUeWNlPjtdfY8Nov+cmLr/Hi66/o8OFFPjhSqmWDePNFuXZ8F9//x7/lz5S7Pf3iL/nuD37It1/czFUlcAHxWXP5AD///j+RNf8LAAAQAElEQVTx9W/9gB//8kWeVvnev/0zP9pykbZElNPbXuVv/vKbfONn26gXcb/t0lJ2jjef/zH/8NRGLtW0YGnj2W6rZs8Hr/KTZ1/ihVff0CHmL/jOT1/jREULjaVH+Nn3/ok//Zdn2HKmEoUVz0cs0Y/VX+T7f/EXPH+oHDsgyeT3+pWiXJDL0FLNwY3P8ad/9fd899UdXKxswvRzpQtb8ldeOcLT3/4HvvkvT7PzUiUx9bFjbVzZ/xbf+s5PeXfvRWobb/De09/nD/74r3juQIU2uwEryrVDW/jxD77DU5vP0hoDmwTmn5bxVRzhn//xn3hmfyXJITz/4Tdctll3aj1o1m5e0Ua9z8YbM97aT5vjXr2eZr1pNrXNOtJs6pt6U5eeZssPLWo17cvF+fz6XAOfa+BzDfxXNGD9GqDE7TxIMfLXNP/fXvXrePy/nYn/FxHU9PHfz62rhMkJOmRkOBQU96BnpyyCjkjLaWyfz6vPynRI1gTnWhapgsvKsMlUXU6WQ0gbqUIBjkVKmkNmukOGTol9SnFc2/L6m1PvpBSHnCybtsobnL9RTWq2haW53dRnpttkCm/QcTF90vTu1Ql/norZIE1KtcnJccjUBGoZtYioJT7TRM/wZ/7EgKoIpQpG/U19To7416QsVggFofTUMapsm7QAxF3rDr+ZaTY+S7TRJRktJZ8xX4ghQ/qT1HSLUN9ZrF+1msfuW0FO+Q5+9tIWGvzgCE4iqFP7bQtHc2UJR09c5MSpS5w8fYUrt5pAmYA5cW+uvMmhkzeoDSeI1t/i+IkLnDh5kWMqx89co7QuguODaG05x1VncJw4dVkw5zl56Rbevw8atIk11nDx/BVOnb3O9apm4bdwJKSjY/5EU21H27WONhu/kpyyS8JzsZwmJTjmVN6KNHL18jXxeJnz16po0QZzMOiSaKwUX5e5WRfG8Uu6mDabLpznbEk99VpLRMKq4zddLpayOr/PwUlKZ0CfTmQq07EE7to+fI7qTbFt2d5V7d23i6W+jmk3Rb5jfPOzdcg+kZsXOK3NS0cwYGELX3s/G6+KT1+ubN0OY+NIwX75KiTTe+BgBndKFbCF4/PhdHS2bQefIx4B1wXbcXDUL71nb3oXpuP3+lv4/eojOEelo6usYGH7/PhVsnv1oFt+Ch44v/mybYPfxhGddjwultX+bep8XqVFUe9+DOmZi0+oXMvQNv1UbBvL/KdNiSPnKvH59K1NuO79BzOka7qgjRzCZ2R0AhT06UW3nOR2XUlAn6M20Ta0bNFSldfn9o9l28LpiL92OANvFprocmUP06+92JhNX7CwHQfHtrFVHNvm9iXJSMrIo3NhJkmKHabedhza++tp6JtKFaGWPiG393D6hFqoC3Th3ntX8YUn7ufJJUO49vbP+cPvbtcmtKTXgsHweRtPO0mLWPNVzpXA2EljWTC9tzhDPImOR9PWtziybByfIx7anz61GdqI+ic4bez2Sn5FZk9hlvoLhwjbTvvTl9mfP/jG48ztk6sx2NHu2B6cbfHJpf4JcZJoK+HH//QT9kf78dB9y7hnxSIeWzOehm3P8bcvH6dN9C07yskz18kfPInp00fSq1Mxw4b0plBx0EMoWWxH9A0dY2/TJ7OQEcP6kh/Cu2zx6Hgw7XBgk1lYSKecFBRCPBjzI7YgtZghg3tS6LSw9Y2X+ehyGFu+V9RvOP2yU+g1ZhR5zSd4TbvbU+69j3sHJNi2dR9VcWHVRpZtxyjbu4lnPjxMeZMqhdhV+dStRAULIhe38O23LzN+xQruW7OUL6wZTqy6ntvwn/Bti4dPYfA+jF18RuZAEn37daEgIxlbLa5r4/Ps6+DoaeYmrBR6DRzEwKJkvOszOhE7XvXdPwkxaTvC4dh3bOjFqLv62jJsQgxbto3xI0ewhiefnh5OtdlqczrwCBzLinP2zGVqWm3Mt5Gjvb2dlqYVLPVpl8HGkYzt+IQMeallYTvtsIaWwUFKLkNHDKA4zY+5bMntOHY737ZFe0/T0lFkbMt28AcczaEF9O1aRHrAUqOLqXc8/KJhkLuWR8/wY1uWYqSBMbhVxKeBtdXV0rvPxCJ0Cc7UO3qKY8ks2E/hFMyd2yIpK4/O+Zle3mGq/X7R7uDfEXLXVN5V2mkZmPbic2y8GGW107HFi+04XixOiAePF/MtXEa/7qfqbA+z61qYfu2wNpIU7sC14/VJr7aVACRwzQUOXW3DEU4+G4/U77M8o7p23A6OY4tfYRGfBqew6cNSvYNj8CGtCd7n9+HTjlK/Pt3IT/Vr8wmyivswtn8xJtUxg8Wx23lzJJ+tPi4WtuPgUzF1PvFscLrocl2M7hynvY9fT8tyKOrTnyE9cry5Rp1xOvo6Xrurjnfdd3CIV7XbttXeaFmf9FOdwPjUpYp22qafKTamp6tf23Hu9LU0fm/7Q9AxEEYXtthqh/GJJpbmtU755GVlkKW+Pn8AvyOLuWBZtofLtjueQtFe1/7tiDc+e32GNwPiYmE7zq/XI59cli0YXwdv5qnOQodl2Rjd23fx8VlfNPEEy/L4dUTLUV90tdPu4Nex5SsudOBz9O0I1qfSAY5rfFffjmkz8UINVmoeQ4f3p0g5NLrsDj4cA6d2hFLVn9yuKxK2J29SZhH9uuSRErCR9rGMXKafim2361kNd+6knEK65qd76wNT+Z/FH1HCdu6Sz3TqoO/xpzaRMbW4liVYRzpqhzf1lm2LTxtbTwMvjjB15t0U27Iweg3XXufwhRbB6dsF2/k0HgEJr+qEx5beHNvCn9OZEUN7ke23PPiA308goOL3Ydq5+xId8+9nuqmdGNUnn6ayC+zYfZCSuEVQ82HCtvHV3+DgkcOcLovTfdAgCpItwpoaA9qkvnm9liR/nPLT+znbaOEX/bg27YPKmQb3zqW6OsLwmct54P57WTsmkx0v/JitJVFsO0QPrVfs+hIimb0Y2SMd829IoxzLX3eDG24IS5vE+45XEAjKgpId77Iw028grytDe6ZSUeNj/JxV3Kc5+KH1i+iX5SM1vx+D8qNUtWUyYlRvcpwEWvp4SCy/jVV3gad+8hRXU0dx74olLJm/lHVzhnJ904/56dar4PjoPmAEWbEqYtnDWbd+FY+tX07+9Q387K2d1FpQ3GsQfZIbqPN3Z/HyFaxdtZLHFk8kLxCjzfEzqFdPOuWFuHngQ97cfQNbc1NaYW8G98wmR89BnZKwm0u1Uf5dPq7uzIpVy1k6dwFr1q5jRiebK+V15PYdSq+URqrdIob3y8GWEK5l44tFqdaaK+oPcnb/YarD4JPePRXJnp6wKVmMGNCJtpo68gaOZ0B+CvEoWIKLR6Co5xC6B5qodwoZNDAHfyxGRPj69+9JtvxnYPeuFBV21hgqJitQxea3X2V/VYxQwEfXXpqn8zLp2rcPOaEEUfmCP5jgyuV6cpIaOXnwIOVAwGxM6/nZ27KhpaqME6cuqFzyysmLpdSKrySf1m+XrmpNd7G9XuvSIycuc6W6hda6Sk6fOM9x1Xlr0NNXKalXJ+Frav4slc+/P9fA5xr4XAP/dQ3YtuPlO45i5H+91+eQ/6doQNPAfy8ryu8IKqmqv3qSt97Zxrsf7+BMeSu2HMZ2LG0WX+LDDz7m9fe2c7Qshj9Sw44NH/Laxr2889aHPPXqNs7XqN6vXKTxFns+3sq7G7bz/o4z1Crh8jWUsPHDHew9cYWju7azad8pNm14lRfffJ8tx2+RiLZyZt8O3tmwk7c3HuCKEi6nsZwtovHOxwd49/W3+PHLH3P02i1O79nB08+8yZt7rxO1wCQgcSVWWz7cxuvvbOfAlSY033N29w7e/GA3H23axlNPv877h26QCEDJyY957pXXeWPTIc5XRUiyGjmw1dDeLrlPUq0ja6kCL+nw1G7h0yImFAqRlJRMcihIUk4RY/sUepN9bQJvMXsbXp8E7QRXt/+cr33zT/nTv/sL/ujP/4Q/+pt/5Lmdl7Ec8bDjR3zpz77HkYo2Gk+9yh/96Z/wZ9/6W/7yW3/Nnwj2T7/3Ehe1Cd186g2+rrY/+Zs/5xt/+5dq+yO+8ZM3udFqEbt5kJ98+2/5oz/7I5U/4Y//4d95+8BV4krIWq4d5KffUdufm7Zv8Mff+nfe2ncDNxRj80/+gj/94UtcVdLkrz3Piz/9F/74z/+Yr/3Zn/C1P/9L/u2ljdwIW1jXPuQv/uKP+OunP+SWbBh0a3j93/+Av39tL43Si/kH7fX4ldtbOGHhyg+2bvyYTTv2s+VYCVH5kiVoK1zF/u1befuDLWw7XqqFs6lVg7mNI6pvTIuDrZsMzA4OX2/CsiRv3XU+3rxVfrWVracriTeW8cpzr/Kj59/n4xMV4DZxct9u3tu0nXe3HOJ6g7I34fRQ6mluSxsEV47t591Nu3hf/rzzQjXxtlodBEByUgA3UsWO9zex+1K9wBOc27eNN+XDjTHEA5SeOcDbG3axbdMeLtW5XnKqlI+jO7bz/pYdvLf5EDeb2j3BsqJcOrSL1zfuYduWY5Q0JnBsfs3ltvtaoo3zh3by/iaVzQe4WC2iWDSXX2TzR9t4+/3tHLhUJzkbKZHTZab4iQubFavnyM4dkmkrG3ZdoDHSxME33+SHz7zGO3vPUdFUS31bgqSg49GxrDDnDmznrY/28fG2Y5S32aJiEFmUnT/KBxt38P7GnRzXxr7ULnriz1W77qoLx3j7nY/4UOPl1Zff5e3dlwh7QGBF6zggPjZovL2/4zT1ruyaqGfvpi3sVKJ7ZO9ePjpwlQi67hglRjQSJ54QrKpvnDnKho3beFc8HL7W2M7XHViwtYBJ0jhMTkkiJRSUTAEK+o3j9397EfEDr/HisRawbGqunmbDhi28vWkvF2oSWIl69hw4S0lVOR++v4dTFVHPFleP7+OdDzYrTolfUWu8cYp3393Clp37eP3F13hhw2EqwuiyqLt2mg83buXtD3dz/Eaj6sBqrmDP1m18IJk37jlPs2WB6rZv/ojtp697dtmhg6OSW+Xs2naCarMjSRvn9u/m/c07eHfTPi4qbmIu6dg0a5hwfc9mtpZ34v4Vw0kPBbQ4CRLK7Mb61UM5s3EDh6paqTp7mFM36zi1fSO7zlZwq6SGNsWngBs12HBo5vge+dJHO3n7g52cLK2nrroB2x/CES0DdOuc9K3x8t6H2zlwudZU4YYjRG7/aVWvpuNHenUTPoYvuIdHx8LTP32HspiNLZsEk0IkJfuwW5ppcgMU5qboYCGVeLiFVuOkto/aq0fZURpgZP8CkmxT2YH3rocxtTRIzc0yKhpaycxKJiU5RFrxKFbq0MA4sGVZlJ89wnuKH+9sOUxJx98qMH212+JhsxItHNmxlfe27OO9nedpiLuom0qYswf28PaGzXyw+xxNqow3VVAdtglqk8F0bqu8iok9xqZbDl2TtUyta0jrxVUB223m2O7tsuFO3vpgK4dLyAdgbAAAEABJREFUWoXbokLjY8OmHbz34U6O3WjB2PLW6f28uWEbW7bt5OVfvsrrGhteaNJuZ5nkeF++89aGvfKDCNVnt/PTnz/P029t43xdnGj1DfHyMR9s2qqxc4WIBVXnj/DOe1vZsmMPrwjfK1tOUp9Qgziz4vUc3rlTsWgnb8jm56rCtFVX0EoKAZ/h3aXs9CHe1fh+T+XojWa8nqZJ/T0dSiet5Rf44N2P2bJrN4evVivOGCiLRP1NtptY9ME2+YtipKn2OilMqL9lWVSeOyL+drB1534++GiP5tgwNRcO8er7hzH/DEq45iofvreZA9IPop5QbrBj8zb56Db2X1J8uwsn5kpEicbi3P6Tw6VnD2F0/IHyiQNXGoXBALl37FN5/jCvSD8bN2/ntVfe4p0D14mLL7fyouyisXv2Mnu3fsye6xHseC37t27nnfc/ZsepcsGBFalm3w5jQ+l9/yWa5aqWDgauHDOxYgsfyH510nei5gofbtjEhu0HeO+NN/jFG9sRO9BaybvPv8xPf/kW249cpc2yCUufmz7cytsf7uLUrXA7zx16Q1eb8petm7bx4Ucfs1ExMiYd1Fw6wuvvHqA85hJruMlH733EHo8AWOFq9ny0hQ3b9vPRwYu02g5xbWrdKG8hmB6CGCC6jaXn+fDDj0V3N6crE6LbxEHNvW8o7ny8aSM/ef5ddpyvUz1gWbSUnWejYu+mjVt4Z9d5GpprNA7h9lyD5sqD27bJv7bz/rbjio1ilPbLE0c4Gm+cZaP89W3FyaPX5CNqdpvK2bFlq2y8lT0Xag0pU6siv3H1UL+Gm2f4ULHIxO339p6nIQFWolExdLv6bWHzoauaa1CHqOcPrgmW+rRj9RzeuY13FA+2Hr2JmXbceIKW6hL5yWaee+E9tp+p8GjWXjnFhg/3cOryZTa+v4uL9WGab57WfLeDDzTv7DlfLYy6PWFEqoO32qun+GDDFt6S3i7VxrGoY49kfG/zbvG8gZ89v4F9VxtUj9epvXuUK5pfXnt3K5u3fMwLL73LR8dvIVFpKjnDO+/s5dzVC3yk+HzqlosdvqWxtVmybmP/xVrBWbjKgXdv2cY74u3jw9eJCL2sxEnNH2be+lD+aQ4jw6WneePdrWzZvoc3Xn2dX7x3kPJmAeu2rDbOCP495RXvfLCdw9dqaaqtx7WD2sgSgO7qyyc9m723cTtGB0aHt2OpUYGYobn0DO9rnJi4c0S5gbrptohVX1eM2sLbG3Zw5FqTAVW910tP3bGI5pO4/NP4ikvpqUO/If60j2FL0eqkYvR78tt3tp+islU4pLSKC0e9mP/uliOf5FjGP3btlD/u9OLd2co2xbd9vL3tKGfPnpZt9lIaF8bScxi/eks+eaZSg6OpjLeeeYYfyia7zt7SHBXhrNYV7ymHe3fTAa5Ld264ku2y+b5zV9m3bTe7T1/jRl0bScGg/FI84XLzxEE++Gg775p4f1OdvOpPZG9rA9u2cfxpDJ82GX/pMQ6fb8afZBMKxjhz8hpJ2tDvUpylfMSHY0kHlk1StIErdfV0mXQ/o5Kvsfd4DcEAaJ8U1B4QD6FQknLWED5fiP4DBlKkDdWrJrZo0nF8AZKSksRrAJ/Wb9qvxB+yKLtaQTB7AAtndOPSYekG8CluuHreuS2H2/iTkoKkqAQzejF57FCyM8Sj6IWUewX8DhbtV0JvIQfO7X6HI4mBLJ3Rj/RAAL9f/YsHsmpqT05uU1utRWrIjz8Q8vgLSo6MrEJGDyygpuIWDXLwgN9HIKR2U5KDyhX8uAUDmTy4O6kOogSpfebw6JIhHH33BXZp0z012ZEegphN3GDQomTvO2ytyGX5qgkUiX+/8hO/E6TPuMmM6JqN7ToEg0mEgkHNizbmsh2IKF+5cc1l/tq5JN06zLGKOI427k37nSL9+7VzHxJ/waAfs0F9p00vtvKIYDDk4fb72nEjrn1mDan6gN+P7diqCTBq0UPMzb/FC69sw0ThUMCR7pMIerpNYA4pgk3XORfLZcqsBWRUH+f4lZjsqtiQ4FOXK+dw/FCy61n+7C//hK//9V/xJ3/5db72p3/O99/YT4tVzwdP/SN/+M1v8k2tJb/xN9/k97Xee37fNUqPvcNfffNr/InWoH+tNegf/9k3+esfPM9J5c5o/onGPkXq84//fRr4HNPnGvhcA59r4P9oDdj/ndyZDUPlSDRc38dzr26lNTmHTtnphBwLzb7Em27y1mvvU5lURM+0Kt59+RUuJvzUndIC6FA5BV3zCFUf5ue/eIObYjRcUakFvUWXrvmU7n2Ltw/eJCkzROmht3h281EaW8q4cLmOWEsbVno2eZlBYk2NlFfUkt29AP/VbbzwwUHcnCC3jm7UJtoV0gsLiV7Zxs+f16aHnU1+oIaNLz/LjnIlI+FS3nzjXW46BfTKDLPx7dc5We0SSlzg3be3UJuUQ9fkGt555QX2lUCmr5nGmJ+szEwyksNsf+t5tl5L0K1rDtHLW/nFm3tp9KEkRcmg5DG30ZEpcZ2QxzTRhyuvs+NiPQPHT6a77WphYGHzyeVaNn4lN8GsPqz5wp/wnb/6OrPyynn5F69wSBv7wdQMMtNTCCgR8QUCBNK7sfDBr/H9f/8W33hkGi3HPmbHmQpITieY2plFD/wB3/mnv+Of/+7f+Icvr6F3hpKJF59l880QK7/4N/zrX/wOo63T/PzF97ThVsueN59l4/UgK55U21/+LmPs0zz10lucqYiTmZmhJDAF5WTsf/d5Xj94i3Grv8Z3/uGv+dKczhx972Xe3HWTcCiLguw0qo59wPMfXiLiSyEtLYO05ICSJ9DBPr9yaeVlmdbWCp776fPsrwmSl5NFRlAKVb1fHfa8+gpbxFvXLpkcfOslXjler1qIS68JwdB8jWd+8R7XgkV0zWjhteff4uStWra+/SbH2zrRpcjR4uIWypxpi7bhS80iNz2IW1vKxeooBZ0LiZ3fzs/ePKqljFC7CYzt9Malra/y448uk5uXrT6N7N9zFcv2c27LWzz7can3fn73Jt7aU2LAcSvP8uI7u2nUV82J9/j2S0ex87PJyU7Bp00JddCqoI5LWnh16VRA0/EP+dHbxwUN5z96jZ9uvEZOXhY5GSnoXEB8eE2f+fE0RryxhZKb1WSL/6Qr2/nBa0cFJ5957iMq0osoDkW4fqOEiOWjQWPvx68fISqI85ve5K2zcbp2yqf6wmXqbYdwPErcSSc3M5Wg4+PS9nf5xUfXjXY59f5L/OzjMo+v3PSQ/DxBIAhN5zfz/dcOY0s3+aFaXtVG2J6bomChhY90KFqBQBUbXt3AmYhiRKdkDr31LN//8Lpamvngmef58Brk5+XQcvojvv3sAVzLT41s8dPndnClpoWrp89Thy5tHty96EngA7eJSyV1JOtwJ59rPPvM+1xqEywuAjcvXjEbUYlEwqyp9O0SVWOoZ18GdfJz9rT84tYJfvrSLqyiIvKjl/i58JRr4zQ/IwmfbJ2Rm02ajHF973s8v/kmnYo7ET/zET987Qy+pBjbXn7d2ygt6pyusfACP9tRCfUXeeq1PcRzO5HnhLl6pVRcNfP2U79kV2WKJ3PFvvf4zmsn5ZdBSva8z0/fOkDNrVtcuFlDw61zWmTsoBwpM17HOW0wFMtfrCu7+PFr+4igy1t8ql1WvX7xInaXHuT4jZbiomWeLkmFfeicqOa8DtkC6Zkk+x2SM7LJTg2S5Dbw9osvset6TMiibH3xeV470UiB7Ol3yzlwuFoxqZKXdBi3r8Lgi3D2wi1SCwopdEt5TrH1citYOn27PV74zGUlItRH05m/5l76VG7je29f8CDMhk+8LQxFw5heHOaF7z7FP75TxsBRo+gUgIQOlDbvKmXA1Al0SYrTFknIpoYHr/udH8u2PLvmDRtFn/hZvv6H/85P3j7AzUZH/ppitEf96S2qO0V2cRGh8kP88MUdmA1d647+Yux48RlePdZEQW4WWclBbRgk8Eu1Nz5+g+f31NBNNq/a/x4/eu8ylhPk5Oa3eX5ricdH6c0ymhMpFHcOse31F/nwbLNXn4i7Gr9CQpidLzzL66fD5MuXsiJXOXSthbpz2/j+q4dxpO+CpHqNn1+y51acoK+Bd195l9NNmXSSj2584QXeON4AtYd56tWjpEiO/MZSLurA0e/ahCNxcvLySBHDJaXlNMWSxUsKu954gffPtRFIreM9bW4ebcigU+cAO157nmd3V4tHjcFf/JINl2IUiK+U6svs1+GdLX/96JXX2HzJyNHCzRvlxAsK6BK/wNO/+JD2f7ElIXsksIx4laf43o/fo8SfS252lhbSjhbCQYhX8MIz73COPLrmxnnnhdfYUxIB9THjUQ/qTn/Mv7+4j0RBjmJklG1vvsX2Eht/9CqvvLyZi61gS66jm97jg9P6SFTy4jNvcyaeS9d8l/d16LPzZph2nC7tl+U9XFOpkVB6XvGmcyHFrmLEs+9xReCYNsUB8wy0VfDe6x9y082mqFOIfS8+xU92a+QFkzi58WV+uvkcDdqAunjuGttefYNtt4J07ZLBntdf4cNLDZoL3mDD1YDiaQY3rkhXWn2X7H+f5z8uobNsFTn1ET9+6zSWP5kzm97hraMNFBQWUb7/A/795SPgBGhuieKEMhT3k3Gqz/DjX2ymPreIrna54tq7aPgaVqVzdLnUVd+krCWHYvG765WXePNCAr/G5BuvbORUvYvlczi9bQPvHKkTfDPvKO5svOqSb+bWlCCufNMv48XLDvP089u4ZQms5iRPP7uZpizRDV7XfPw6VxUHrWt7+cU7h7HkY/nNF/jJ91/lXELwpUf49lMfUh3KIS8vyIUzl2hocak/8zE/ef0obQJpbbjFjbIIXToVUrrrLZ7efEW1YP5kpvdSfYFnXt9JW1YnCoJRrl4q1Xhu5p2nX+Zwcz5di102Pf8iH9+ICtwiYWwmXpuu7ON7z3xMXWo2+fkW1y9doKQ+wWnlIu+cc+naJY/TG17j+X21SBkahy5xbSJBK9tfepXNN2XD4kyOvf86b5+pVRz34WojLTMvl+LMVl772S9481Qj/uQmPnj2Zd4+XknVletcraigoqyE2rRCitOqePnptzjSCEiXCc0xiLfwtQP85OW9+DsXkdt2np8+Kx0p/41f3MEv3j9Ncl4+GbWH+JEO5C4bJeGSMDmHOic3XuCll7ZTnZVL55ww7//iKV461YYv1MzW11/ltf1XuFlVTfnFY2x47m1OxDvRtSDKey++pUOtGnZueJ99NSl0Lcqg5PRFwsCpd17l1aNtdJUv3tjxNs9uL8MO2ez/4C22KI8tLMrl4gev8/ON5wQNh9/4Jc/tr8LMA6n+KvYduIUvUM9bz73K1hsRwcS5cLGcQGYRXUK1OtR6jRPVLmKfhOwjFRC7dZTv/HgD5aFccnPSlVMkZFc/cJMXfvY2V0JFdMus57Vn3+BobUz1lraWQl0AABAASURBVLQgHHpDiLw32wFaKCkp/7XxJy4fNrSuSqZXDzcqRhfSUH2Lmtoo4eu7+fkrR0mSzNmV+/nBszulizAfP/Mc750PY+JdquLdkZImiNbx1rPPsfVsHY1lVzl18gzPv7SV1mzJ5yvhGcWbq+EQtLaSlJnlzZ3xSC0XdLhjcri2s5v5ofzdsgNc3ruBn79+hNKqOi5eLBP/Jfzyqbcw4Ru3hTPnK8jpXEh283l+/sKH3DIGku8YeZUK66BEXSyxFI3qEHUsU4oa2XXoJPWOTaC2jAtVDXTuNYj0RDPRhGBdcKTW+som4uEYnQd3ZWzPDK4eP0ClBT5pXSCe/7tunKg6+Wy4eOYEt6zejO6VTCxu8LgdMHoKb8KyCcVauNEcJj2jJ+PGjSBUeZ6TV1ySAzbGzup15zbzv4c/nsCRPs+cPE0DqaQlIZniHbjvgCP0aohwQ36Urbk1JeR685gAiUVcUrt1I6WlWvlrhIA2dA29eDxGPOES0SHL3rMN9Oo/iALh1/SH6ReXIK6doLbkMkfPlJOaG5L8apIewi0Rek5dzsJizR+vbEXLGvzE0bk5Ui2nzpaSVtBTY9qlRbmG4TQR1W96Hp2U81rSXUIGapdT9fJWqZFw0xXpuRtjhg5lQK7LscNXQXkFguWuq72fdOuKn7vqvVdTJ/h2GK/G+2n/Nn0EoBpXuVTELmTpPStIOvsur+6txdGa0ZVe1B2FEPxy01uXqglp/dB90HiGZDdz4tRlWmUzu8MXhOqTW7rx+YOEcrqw+OFv8JN/+yceGpHE0X0fc/xmhMzUFPJ7TuT3//rv+be/+Qe++3d/w5Mze4qWj9SMQuY/8k2++6/f4pvrJ9Gs3HTTIcUW2SsWa+eZz6/PNfC5Bj7XwOca+P+UBuz/VmktC8uKc27/Tm5lDWPhvMGMHNCXIp12q4GGCwc4Vh2i76BudO/RG6v8KMduJdOnZ3eKu/Rm1NiRPLBuEfn1R9hxKkx238FMHD2aHkWFSgD8lFbUEMvMobs2kTsV92biwrU8vmA0nTNTyCkewPCemUp+8xk7ZRb9CjvTo1M2daUltGRk0b1zF4p6DGbGrHE8PH88yQmLomGDWLV2OcNyYlTUJqi+epIjpRF69erKwD5KdCrOcvh6Hfl9etO5oAuDRg9m8ZJ7GJrVws1bEQq7FGoTNZP+I3rRPXKWbcfqGDpjGmOEd+GcYTSf2smRcgj5lFzcNe/aSoau7HuPF159iR9oc+5CvICB3XJIxCzMf3eBtptLSU7Y9VHQqSdDxwxh1uwpFDZe5GSpkkvXIhGP4/VRphFT8u0EQqQkpxKwY5iNO0eLzoQAfEpOj+/6gGee/yU/e/51jiqRSHFLOKjduR4DJzJrVm8G9x/F2gceZdW0YaSLxsGrEXoMGM/s2aZtJGsffIxVUweTZrURUVKZsHz4mis4fKGS7G5jWbxoOIN692L6ohWMy23i7MVL1DRLfscRTwmOvP8cH5yqwlEyEhdTJkHSo13Ou35d825BzeV97LoWYMGKiQwf2IcBXTNxErYSxOts3ldG0eDB9OnRl+JgDbv3XTK9QDqw1bf2/DH2lsQZ1L8rfZSAJ5ecZ+/VUq6X1tCc8DNo2BS+uHIwTlImhVoMde41gCHdMrCye7Ngxjj6d+tCj85pNJVVarkDlpXARYjdMt7/8Didxy9h/PABjJ20lPtm9sYOZNCndxeygzb4MujesxMZfsFjU9yjO11yUglIZ/s/OoTdfxKLRw5g8KA+SpYtorIhdicWLptM9y5d6FucTmV5gxRXxUdbzpA/eQ7TRvQXfBcygp6I/KbLychm2rzZDOraiX5dc6grqyCSaKP8xi0ayGTkzNmsmj5IiXYS3fp2pSAtiAPUlNykogkl8AO57+F5FPuSKMjPIqewG6P6dyYjKYMeGhsFGckQq2bjtotK3mczeVhfhvTrSnrAxVbmfmzbPiLFo5g3YhCjp85hRKCcD/Zf9nTnKNkVKdK7dpZuO8sGA5k4eSZfmNGJI3tPUlZ5kW3HG5k8bwojhw9m9fzBlO3dwwmS6d2rM4WdezFv4Uwev28qOehybGMR0Kt5sYmClcrUaRMY2a+Ynt2KcFoqqZJcKD4pbec3XZbGkmuFtChOwpeo49qpfVxqzWVA7x7y6WJqzx3hXF0KAwZ20rjPYsTYfnTNjLJv50FacnvQr1d3+nQNcHrPMRpyezKgexF9h45gwpQ5rBlbQEVJFUSauXH9FjEdwk2YPYOlU/vC5d18dMlh+rLRjJLMa+b35Oq2nZwNZjCoZxdtVvVm1qrVPDp3NEN75OggJh2fCziFzFs2jZ5du4hWjjanK7zNHbWAha4YbS1hbAUgW7KrouO2sKwgKb4wjeEE6Z16UJiZTLdBoxlQnEFmp6707ZxFctAvfs/wwe5bjJ63gFHDBrJg4VIWje1MSm4hvbvmkuxLCGeAyXOnMaJXF/r16IzVVEtNm6rvto0+P3WblWa0BSe5O7/14GhOv/kiH5dFSUn2YSXMyjeJ4TPncd/axTx8/yIm9cnEkW3Pn7pAwfjFDMnNIKFxnpSejv0p2TqoCL8tj3Oy+vMHf/JVlvW22P/+L/nK7/wVP995CyyXo3v2UxnqSv9e3RnaK4frxw5zzfgJ0g+6ms6wYU8ZIxcsZvTQvpg/cR3Q0tVPGzu3ncTpMoBePbozsMjm0O7j2Em59O9TTE6yoQw9h49hxqSB9CzuRn4oSvmtFhBurdK8B5XHeXtfLWOXzGG0dDt12TruG+zj8JY9tBSPZM5wM35mMDL5Jhv2lpLZrT+9u3RiyOjBTJ6+jKX9Urh2UxtpbfVcLanHyu3OhDUrmd8zlfTcPLK16Tto+EA6p9iy7UhmTRksX+lGYUqMGyUtsns3enTqxICRA5k0bSlLBqVSVi4eK0/wweEGpi6fzqihA5m7Zjmz+2YRyCmkX/d8ku0EkMKoGTOZoTHZRcVfV0GFsbn06plPEGf2yYftXtw/bwhDBwyge3Yyfi14w9fOsOtKK/0HdKfPgG5kaszvPi+bSDcKseqZYPfmXbR1n8iy0QMZMrA/PfPSCfj9pBV1oltROlYM/GmF9OpaSHa6n+iNs+y63Ey//sLZTzirLrPrXLlwWToYc+UJ3Lks781ixNx5TOjWmZ69O+NUV1ESNYMKwbY/MzQO+nTvxACNyYmT5IvTstm/5Qht6Z01NxQodg5i7uJ7Wd+vkQ37b9JNMvbu1Y0usUr2HLvElco6ahti9Bw0kifXTiI9FGbPtsOE83vSVz7Xp4uf07uPUJFWwKAeRfQeMJjRE8bz+IJBtFy/QVsggy6SO6drP8WBfCqP7edIQxLD+3alz6AuxM8f50B5k6SxpDnDs0Vhr7Es1UZAt2496JLazJWyMGmdO0tnGdhxCyc5n16KS+agNXLrLJvOtjFnySxGDu7D4B4F+M08hE3XPj3okpVCkgO1x/dxtCGdIbJV797diVw7xJGbIYb070LXrt2ZOHwEyx6aRTe3hsqwy8k9W7kRGMqaqQMZPmISj66aTOfcbLr2KCZfB7zGe5Ly+7Jg8QR6aK7rU5TErZJqzCX3QcKg3SRuXCsnGsxn3PRpLJ85APv6fjafi9B3eDd69xxAbvgKW49WYK64OllEObFtB2VZw1kzYTAjR87gwaUz6OFcZcP2a+QOGErvHj3pld7Inr0X1c0H2hy2AiFovsmWIzco6NWb3r360DlQye6DJTTG/ITSsxWXhzF9wWpW9gyzces5Ugu606soj54Dx3Lvl9cxvWdneoyexpIhnenZuxsZbdWU1omEvCmufMXS89yRfVyJ5NO/Vw+G9ulM1dkjXKpNZoDRY/feTBk+ktX3z6JIh+43G9XXUjEJkmJOgebLbsVdGD5iCFNnr2L1wARbthzDbBD16pxHF8Xuh9ctY0pmGe+dbGDgQOmob28yG8+z81Qt5TfLqY0GGThkOPffP5O0+HU+3nOZFKMP8dM/u0350wX8md00lxTRe8gwJkyayn2al2tLKwm7pXy47TIDZyxltGLKjNlLWTmlG6HsAvooJqT4xKsi9JgZUxg7sKt8rAvBcAOV9Rqoso3ZNDMQF3bu5FJwAA/OHMLQgYPonunDCgRxLx1nV2mCQQOk10G9Sao8yc5zikWmk2t+2otRiZUwOFMYNf03xJ8O+PpbFZRWNpPdewD3rZhJv04+jm/bzc1QESN7dqdPn2xunTvD8dPn+PhoLRM0DkYp3s1Zs4I5A3LIK+xE185dGDJ2IqsfXEdx+V4OVqUzcEAP+vbpTsvlI5xtSNYGdzZ53QZo7swkNblAc/JUesqvB3TNpOqG/NOfQY9uBXRS/F6xaiEPLh5Hl+xcuhTnkGQDVgrTF01XvtaF/j2LiNVWUx/mU5fnBqpxtalIIJvRk0ZQcWQ/JXUut8qvUOd2Z2jPEG2tCSwpycUioGdF7VVuVtvEKsshv0hx5RRnSlyS/PJIVwhN/t5Wys6P3uD7//KX/MU7VSx/9FFGZdtEYmr/zG37obW6hqpbJdQkqqlsSiHPd4ujZy7QFrS8WMvtS/jNn4536i/z4buv8LPnnuON7Ueo1EayrfFwG+zup/GThJugqS1B0O/H74jPDgDXtSCUjKMYFQ634Ip3vx2j7OxuHXY8xR/95b9T2n0Vv7VoAFYrJNTP9gdounaYt157nWffeZ8zt5px1aI0XK265UttdhIL195D9qV3eFGH23ZSSHKIlhulUZv3TjCZgGi70ql64D0MD79uM9VVswOVp8/SkhLnwrVa8gpSdOizhxsxCy17RJ//5HJxhec3A32mUQaPhVsJ9RjOvfN7suuNlzhSFSfJ73h4jJ6SlLOcq6iipa6V+vpKMgrSuXriBGaJEXBE6TMoVYNOI7XOgrSMAo2DzuSkBEgk4sSicSzborX2Eu+//BK/+OUvefndHTSEglqbxbw+tj+FVG1S+wSHa2veN/oET3d8fn2ugc818LkG/jdq4HNU/6/QgP3fz2WY6uowKbnpmKP4xqY2opqoLU27kYZGWrRYPr13Bx/uvUqXsdMZkBWnsTVCPB6hsS5BUyCdzGSb1vo2Ki/u5aU33uWjvQc4q8V7wK8JVZsObZoAHUfvyjAsJ4HZvDN/5alVC8hw5WneeuN13tuxm0OXK8EXwGxIhiNR0YjR0pigVdmHz+cjoc2XprYICcePT4lMY0szidZmzh7axVt7LpAzZBJDiwI0N7RqUo0TVnJn5MHnKJFwiSpDi8ejtAhPc1UtbXYaGckudZIjmpZJuq+Jai1CbFtz+V2KTyRsek1cxgP3rOe3f/fr/N7Crnz41L/x1pkmkkOC/TU7spaSpnBrEw3iwQqmKnGN0KZETmptx+wmJKaPFKuGjU//DQ8/8WX+7BcHyB0/ixmDC/FH20iIEb8/QCgYJCkkveiwIN7SSkTJVUCLMEvJmFkwZ/cex8LJwykMxmlR0hQMhrBNmxYTWaaPSRTjAAAQAElEQVRt0kiK0v3Sp+slIla0FeWLUnUqQV9C8seIOdJFUkI8xnCjEdr82YzWJurwlJu8/MKrnG+2vQTZMG9yFJMpJbQYdE2FVyzvt7miEiuzUJtlCRKJKC3SOUpK49rkagyHKTmxhw0f7aCty2jmDS/w+ogp79nQqCy0pZEjO3ay4ePLFI0Zx8iu3Vm0eBKN237Ol/7ou7x8oEKwLlH5VLi1lYQyv6aSk7z4zGu8v2U3B85VEg/6O9TsIlVBWxW1kRC5uckYns2mf37nDOGJ0dYWlR30KntFwrffISJeY7JrXIliaWOY/Lx0r2+iuU3DxMVxfES18fPMs2/y3pa9HLpUjRVMUsc6Kpv95Bf62uG1oajh1J7HiVdD/xOdyXdEOlJ1keefe51XP9rNjnNV2k6I02x3Z/WyIVx84Tt88S9+xodn6jwcba1h+bZLWP3GLFpE3/pdfPX3/5Z/e+MoTaqLRaNEI2GaRNQlTlhyJCzZpqmeqrhN58ygx1erGUdSjhuLU1ofJSM7HVf21E1+dhINNXVEhc8jqqcbjhAWbjOmjAxZOTmE4s1cL2mkNSmVbI1rU++mppPpa6WsMSbawqCxJ+rgC+IIz6duowjHB+FaNr76Cr/8YCfbDl6iQYc3JvH/FOyv+XAtV/uSbbTKz0LJfuob2gjXl7F9yy4+PFnLKG2EdJdJ6puikjlOq8a9K9i65mbqyy7w4ZYdHKrLZe7sQQQSjbQoHsUibRg5opaDE5c/5g3g3lnd2PiDf+Crf/0Me8oVW+oaCPvTyQgkPFiysklz66lqlF+aBadinceuBI/JFtFoDBmVWPVZXvzl67wuf9lz9hYxO3Bbve2OYI4YUoPEWqOY/3GgwSGX0cPFddtk0xCpoSS9txJWXDXxxfi/Kxptkbh83dbuUwUtdqY2+VzxFiMW95GfH4SWNiIaiwnLlr5r2PT6GzzzwS42H7pAfSyA1rfiwRWt/+CWH0XEUNGU1Tw0pI2nnnqbS60+go6lTqKHj6y8PPK0aSUmqT+1mR+9d55bV3bx7oYPOVXRwvndOzh8tUZyC15x/dMUDR6LUF5v1n/pq/zk3/+Ix0favPPCu9xMRGjRvNNWeYUPtdm57abNtBnjyQ8YDKZAovoWrb4scjOEW47cpLGdUAy13Cbq2mI0XD/B+5t3cI6uLJjSXzxHvfEfcy3xE+fs9g385OWP2LTtEFfrXAI+SzDmbsdPVaV0m0F+lsEf1yZPiOR0qKmJkZEju4hmIuEnKzdAS1W9Nhci7XOdgm0i0YxrxoLmTYomsH5ckF/+1d/w+//+Bqd1mIr8MiY/aW6O4Wrkndv1IT95aSObth7iitpDMpDrhrXJECfa5sq2BsoiGFKsq6ynOTmDIunC+G48kEZhqg2RVvm88NkORKv54KVXeen9nWw5eIVG+bcIGeH0EKzeyisbSMvNxefJ0arxnsCx4zQ1iXfNtSd3a1xtPkuWNikndE9VD93Gn2igvC5GXl62+EqQiLURVlyRq4Dm8Ug0LhqCdSO0aYPAUlxqamwmYXDuFc4tp0kfPpFJPdIEpNubYPT0bhfX0EjU8qF89ul3dspnL9Egv/a3s+1BmR9vHEiHrcphjB7ScvKxFVvamlsIxy2lAe0dwhrX0eYwV47tZePm/YR7jmJq32KmzJlD18qP+MrvfYsfbrzgxYO65ibqdBBqYsXhhjzFisGEdMDcLJnisbAnb0R+7A84iFPZJ0Y03IYZl9UNYazmWvZs2akDvQoGTp3EgEy/YbWjxLm0+0N+9sL72iQ9zNXaqHwO3A6dySn1EZXOoqC5v7G2glgwi8ygi5GvWfOVqzGJrnBbmIhigpigpa5JNqth38eiu/cWA2dOp5/MVdscJa68oMXYt0WbEoEQAeUZlVVt2qDNxYv/2qjJyErDsixaWiLeXOMT/vrze/nZs2/zwcd7OHqjCXNIrmqQrRKu3ooGsn5uH7b95J/57b98ih0lzSSaxIfmz/MHxMemw6QOm8SU3skCBmNSCFNTGSY9PwPb8KT5JTktmRB1VLdEqTy7jw0ar9W5Q1kwugjkNwr42KJJUyvN0WZuntzPhi27aS4YyvRh+VjC4SbiOrBOSNY4hYWZxFvqaZbN4sq5HFtosPBZzex663WefX0Hm3afpUp5k+W1qd1SIUa9/ChSV8I2xZsPTzcwZvpUipPi1DVFlEtFaRbP0Raw/T5s1/T5pJj50vh9a8Tw4ZKbn02ssVb2jBCLJXC8mAkNkrMt0oj5ZyA2bDlD1pDRjOxTxBxtcDrHXuVLf/BP/GJ3KfG2Npo1B1dcPip5d3A91Ie5E7pJhY00K/bHjM+Jn1Z8+Hw+Yk2VNLjp5GQjX4krDtnkFySB7B325gEHYg3seOdNfv72Tj7ad4aqsI+A434ihN5uVtSRJt59wp1ItBL2eHdorWlSfGjgxPbtfLDlMp216TuiU1A9Pn172GzRilb/hvhjYXfofdjM+UxKOc+ffO2v+Ptf7qE6DDUNTTTXlLBZfrDpvM30uSMJ1FZSG0qnc8iVbAli/jRygxYJ+Vqb69BuY6lGm57hhgr2fLyLDQdLGTRlBn3S457OY2H5pwJUc8kJnnvuDd7RnLz/QjX4AqBB1Kb5wvbZetOn8TflUZ7ebH03l/HOy2/w3KbdbD96hWY3wJ2pQs3mtizzizwtQWvMofuASQwJXOajAxc5f7mUXG2c58RaiH4CSCIap+7yOcrCFezaelDzpU2qW8WxM9eJBn3G9cVaTO+dmbVcc/CSyaS2lFEbsUXf0zR3X6bGrxypprGMqzebuXV6H5uPXiCQkcT10ycpbQChbZfRdBTPViJMPKM3C5ev1UHzAywd149USeGqGJC7i6sPn/zYLwOmJdu0mLilGGSp3tyWaNMmPfsckpKS23lPOHQaOI1HHl3PpGKHyqo6XOGwEmAJj6u1R1rP0axcs5rH71nDuJ7Z8g9bfokuF8uyiIVdkopHs37hIA69+UsdhITxywCu5Sc94FN7CxFLsLclc8F1HB3QW1jCcvftWg6+eAXHbkaJ3DjKlu37KLdSsWsvcvBShJDmXS++3d3prnehxrIstNT1ahOuhXEXS7TlXoiyio2lX1WZX6/YEri1DYbOXMO07Ms8+/LH1Fl+bMDyQaS6jrrKq1yrvMiWj/dTEUtTfnOOM6WtWN7EZyjzyaXPhPonRWp498dfY9UDj/Bv2yroM2gc/bumal0Xx9a6ORgMYNaTAb/0pHzXtWz5Tgtbf/YnPPDEV/nLV/aQNXAOC0d1IhwBW7K5bgIzn/H59bkGPtfA5xr4XAP/n9GA/d8pqeYsoXcImQlPi2RLmVtAM6mjp2M7OAGb5IxuzF0xlwfXLOWRexYwukuAtpiL4zj4NBEGlUS1JZLIS2pg56YtRHvO4MHVc5jUPROz0WfbFo5tY1mWiqZjEXVRnRK9VCWNF/d8wKlYLx5cN5tFw7rgkMBWQmKLB1v9vKfV0V/fjm23T4qabIOO6lOLmbd4DvevWcQjDy1ifJcULCXLtuNgC9ZxbCxLtMWHlxBZDgGfTSAlhKMFcWvMIiA5fdrEaVUimWLyJBf1wbss4bBVLMvCstHCA4q00d0nWMuly9eJBcBqzzS4fVmWYJVFJKVlkCncNVcvcsvNpijbj8FhcFp6MUlImEwmLXuSb3x1BcWSO6OwmIJs9ddGeaubycy1v81f/8lX+Ne/+RKPzOiMHcoiPzlBVcUNaht9FOT5OLvhB/zhv73AuXAWxakJbpXfoOZ22wdq+/bznKmIEgo4ngCJ1Hw6pfqU4FzgaplNXoGPcPlJTpcLX3YWKSkOESXgBQNn8uCSSfgrT3K2wpLeLC+HUu4CloWnFz59+ZP8xM3GMLba/fiNHaRzJzmIL5DK6PlzWbVsAQ+tX8aiMZ29zrbtek+T7JHRlTWr5ghmEY8+tJCRRSkUDJ7z/2PvLAD0KM7//5nd187vcnF3dyO4BwiSBA3uTmkpVChVShXaUijuEhJiECAOBEJC3N1dLsm53yv7/86+dyGhtP96+2tvuvPu7szj88wzz8weKb/49be5+/Q0prwykU3VksUFAmEcybFixkxWB7px+YVnMvz41oSVWBl1+5clLb4hU439GGFlDsjOfh8OrutITkevRkkWuIGgniEQcP12V34acaBSybUjP3A0PxzHEAk7bJk5hQ2Rvoy68HQdprckaGIQDBFxolTY/zMxR7QF7zq6S05rM9dxMD4HzQXd7fOWT6axoLQ1N444iyuOa+Mn21H1dTrjYh79xX1c06mS19/8gMMinx4O+HKpG7dZX77xo+/xw5uHsGvaOGbsqiGiOWncACEl5MbXz8UxDugjRkgb84pYAqtHyNfPSE8H+xfgVTrAMJLNkW7llTEiKREUFpJCAsYYDOC4Do7jUFlZiSfbNG0QIiTbVNo+tRsdAFTpcCgrxcWHFZ66dHm+7+ih9nJEzyGSEaZ0y1ymroHhlw3lkgv70yR4hG0tbPLmOknern01DkHJyr6dbD4Yp9fgTtiPUqmtenON/ODSi0dw6xWn0iYNEgkjPV2NrYMRTsCk0GrAKVxywVCuHnUpV5/bixynmrhxCbgOjvgY4+gZlRADL7yaP/zsbs7M3cObb8wjL5xNRJvxSh3oW1inupIqEyE9xSBsHGOoK74NMCjUseujqcwvb6c5dTojhnQgzY2rx0IK3vP0EKBd1y6wezMH5eMoGjqOUbuhbLc2pqFGdGuXKZwErsbP1Tg74mVcF9cVZyPQ9HR0+kCl8B0nIB0cNepyXBz1B9MM0c3zmLiyiksuPYvhFxxH87Q4cYGICI4jOhbQvh9Vbbtr+Yhfwktj2E1X0HbPp4z9bDdGH7yQVCKPHWTP6iI4N5RLt47p7Fi/ja3b91AsvyrK20uRDrZcR3xcR1gki8XR0+HNW9hVqJ2xngk34YKrz6KdU8p+L0CqHDKr8wmab2dx+SUjuGnkEJrI5panBTcpIYx/+Gl8PYLWRuLgaGyCkr3Ncedx+UVDueqKi7n23K6gtSahXxMMQ/UhJk1bRrvThzPiwvPp3TSIljn12svYH0hPxY2VU15hRN+t/SDnaq4YHTbLglYnHdhWVsYJpkcIBqSj2lzH3l3Z38H1SaVw+g138vvv30ifimU89e4aPCcExhBKC0iHA7wzZTEtThnJiAvOo0+zsC+LMQ6OT8vonqxIeaM55FZVUmQ3v+p3RQdbHBfXcUjRAl++dQ4frDMMv3yo/L4XuXaMfVksoKcfTxt0V3pU4jqWT9D3HeMGSQ06eBnNGXHZ2Vw8/Hx/nT2hXQ6INz6NMCn6kFmpQ11fvoCLo97k5eEZx4YfMAFc12B0Twm5eOnNfXkuvsjG+vM5SYcN2CL5jX93RMcQiKTCLvnskgqGjxrKRcP60STVYDf7FqyuGmOw/3MVw60c9gONG0gnFA7iGFA3tgRd8U7J5vTzhzLywvO44frhnN2zgQco4AAAEABJREFUEZGmffjmj7/Pj27sydp3J/Dx1lJSUjJpPTAZK6658lKuOqcPWToMTti55dvJwfIyRneMyBvcQED8DClBcBt14trhZ3PpiAu48dqh9GoYxtrNGAOxfbz37mIannopF59/Lj2bau6KhkE2wyEUMmACspmDEb9IOIynj8ex2nEOBlwcKwMIxj4bHAOuco6spp244qKzuezS4dx0+Tl010FkteJV0LVwktU14qRxD4ZJTXUpLykhYGmp35AsrmAc18VKPO/96RxqdjKXXXAGp3dtiCMZsUWyuMYjHovQ59wreOIXX+O8Zgd567VP2e9kkZWexRkXn8NlI4YpxxvOWd1yhOWJt24EiWgcK3VQL0VwgoFkeyREQHL1OvNcLr3oXK698mIuPbGlEOLSM4AxDkRcMDkMumiY/Pkcrr/2Ms7r3ZhgXKeWTkBrg4PruJTo43EoLY2IAYRn7B2o3r2Y8TqgOfG6cxgxfAgtbXDx1GEv/x4gFIDU1n25unY9ue3yk2kuOLueuLKL61geBmMM1u4cVYwx/purGOCqs1IfcUwkAySDrY5J9qfYhSHSnHM0RpdedB43XHMxJ7VLJVtx7ke/fJBvXNSKz996m1WHQmTKPj1OvYhLtW5dJV+88qQ2KInFsXK4TvIuuroIpGTg6qNXeQVqdwmqH1sc15c1kBqCvcsZv+Aw5155NsMvPom2mRD3HAt1pKaEgtQoNxARXUFc0bH0w4opJq01F9mxHT6MG64ezomtwz6eMca/Y5Iy/X/jj+DsnIjldOL6+77Do18/m6pFUxi3eB+RzEwadOjHJTZuj7qEa4YNpE8TyakP6HXxTib2+RnHxXUMxn8DhQHCzbpy1UVncunFw7lt1Bl0yE3RNzGPQCiEYwyrZkxllenBlcrhzh/YmrATF7bB1fgaY47QUgOO3kJpULHiY6ZsDXLDiDMZcXY/GkbiWklIFvmO0BT79WrADQQUz2sINWnOcf1bsuyD11hU3IETOkJF1BGci4bPkped97CtvCO33jSCK0dewI1XX8FlQ7JYvXIdh6tBJscYR7CGqN4bdjuF4f0jTBn3HnsThnAAjPodEUxW5AMeh3btocO5N3Dn5Rcw6ooLufGKC0jbt4oNB2oI2OS2du1FJYlncLQwVpoM2nbujbZyVMssAdnE9usmW7ikpMIure15lQna9WpL8a4dOtw3pIRdjGMIKn4VbdlORVpj2rUOoW+D+DR0+FqjPePQkecT2DCVd5ccRmEWhREsfVv1nYyMxk3p3bctpas2srtKurhBHNeVLxvK9d799FGc1fwgk6YupUZzw+L07Nmaiv2b2Wb/zWnl6AZwQhAq3snKHaXEjItr45psZGxfBCq3rSPa9iSuv2qk9q4XcP0Nt3BKsxhrlq+lJgVcHcBqWAUNjvCSFVwHf12rKD7E5s178MKQnuLq418p9o+EIgGXoJ3fFRVErY11mG2JOI6L4wb0oReqwk25+LLhBDZNYeq6Yuy0DAbhwKH9xLNP5J5bL+IKrbm3XX8NJ7co4bMVe0g4IBUsqWOqIy+MhTLpfcrF3KQD6Lvv/gZ3XnEmrcLVVGtDkdG0L/f88C5+8fDX+fmDlzIw1xDVOEsLBl/2DX5ww3DaZiQINOxAR60X9g88XPHyfcqYY3jVv9Rb4F9ogXpW9Raot8C/wQIK//88rnbBhxBte7alZM08lm0sUQJRRlFxKSVa4TM69iWzcDnvTtvKjoNF7NlfQmkN/uIXs4cu0Qq2Ll/FHqcVfTtm6sAqToEOGOw/mbD5wGEqlbhGlXVUVpZr016hBETLuE1MTBX2/3jiUHGNErAAlSUH2ba/lA2788S3kqqqKNXCKauoFE6CmuoKypRJV9fEicVqqKwoo6S4isZtutI0uo5xMzeybW8Ru/eWUqav47GaKsGXa9H1iOuLekV5uQ4EJXgwhFdTzP49ZVQ16ka3JlWsW7aTqmg565esprxBd3o1h+oYGFSUNNRYOYRfVlZKcXE55eWlbF70IeuqG9GtU3sCSgQTRy/OwrE6J0p38d4bj/Othx7hmSmraTDgZI5vmYVXVUpZRZV/oEC8iuKKBDmtu3LCKRdyTs8gC2bNYNGGCvy/lqvcx9TXf819P/wd3/jOI9z3+/fZVtWWc87oRuXOz3jiZ7/ggZ88wu/fW016bmNym3XklBM660v+vKP6VpHWsCWNG6ZTWVZGZXU5lSaF088YQk7lZl589GG+/eNf88iT49if042TB/ckh1JKdcAodel+5uUMH9SWRHkJ9i8cpB6RNIfyrZ/zi0eeZd5+GUumQkmavTXs0JfG5euZNXcnRaVlFBQVU6jNLW4Hejcu5/3xc9hbUEyBDplKq+QPFsmg1AkadepGs8o1vDh1E/mFxRQWV1BddYBZkz9ld0WQXoN60j47SEyHqyE3xsG9B+WvNbg6eCo/dIC8olI2bt1HvvymKm5pO0pqdXdbc0rPLBZOm8LGwyWiW0KpNqaJeI3vJ6XaHNbEDdnpRgn1BvJKyik4VOjLUKlEcUCvJqz9bC5rC0spsX5QWEFpZRTcEOUF+zis9i27D1FUUEK115xencMsmTKX3ZKnVH2FhWXElNHnr57J9x4ew7oyySS9vUTyHgoGNAfy2CmbrNm2j/xKD7dwMx98uJQ9iTQGDuxO2wxDtTLsCvliieS1/5XCuukfMGd/Bc179KZPm2xiVYaQdmMlB/dzqKBCG62o/LWcItmyJtiYAW2CzJ65gP3FZZo/JRQX6Z6AXn07UrZpDdtKyyndv47FeYaBvTtqkwEJ40jS2ktjXFlWSXlZPrOX7aJxl+60btmBbjnVLF6+Swf85axeuIZoy270DXqUlpSprULzKalnLRX/Fq+plO3LKC2tkG2CeBX5igHFHNi0k/2yW6Xd8fiQyZ+4dlxW71L5VKHktM8lh7bw2quzSBlwERe1CtO6Qze8LR/z9vL9/tjlF1XpkMrDxhI7xiWlURKkM0A72DXTp7JMMaOgsFBjGiUej0mvMso1rolEwp8r5fKJSn08mvLpEgoCuRw/uDsNTTVu+z70yMhn8aLD0q+cxZ9tUFtPurtx8uWz5YpVyb9GjCtWVYp+OeU12sEFI9QUH2CvfGLzjn3kl1RRmdwBoLAoWaHl4LMY1q6AtyYtJl/jbP/61Or51qT19DnnXAbmovPlCsrlB6VlFcQka1SHjxauWHZJpPVkSPMKpr73GQeKSijS/KvQx6SYYmKJpVcRFa8U3OoiNu8vpmDbDvbnV1KpHUq0sgJLp0wHxMeMmPzO8rP4VfqAYWIxgrm9uOnSgXhFhyiJmuQg2V89GqMfPad3GsRtN1/LA1+7nq/feR0Dm0Xoee4IzujelK0fjeUHT3zAPsHJy4hLD/tYtm0lkz9exQH5hR3rtfM3QavOdHVdOnfvyOHF7/Ph5gLFD8muxcifQrX8TKMedMsu4dOZiyjUGlZYUERRWZHkS2VA1xyWfjCBNXlFiklFsr3GQwx9SW1QMwFCRNmze69stlkf5krlCzX+mGDpW4O06MWAJuVMGz/PjzVFGsfKaArd+7ejfNM6tsv+pQc2snR3iH59W0JNCcXSo1y+7MU1FxU7ymqk7d5FvPXZdmJZLRnSuw1Z8hvjBnEritl9oEQ+ESDixNi3ezdFJdslSwlllTXJ/7JBY1iuBSqhj0lVitFFiqXx5t3ok13IpIlLOKwxLy4p1jrtEa+u8v2zpKxKm9Yw8coitucVc2DDbg5oblZan8TIHxKge/cenfG2LeR9bYSLS4soFK3Dh8sJtulMW28TL72zTvMq2V4jP7A4GIubwoBeLdm5ZDbL8kooKS2h3MZfvyuNYMUBlq7Mp7SigAKNSX5hlU+znbOZlyeuIV9thYp9NbVzgdoSszFC+pYqF0kkAhjZc5t8tnjLXvYVlPk+Wwv6xS0RpUJ5R1lZHouWH6Rtvx6kaM4WKeaVV1QRk8OEmrSnV4NC3pqocRTvgvxSxagSPp/6CasPVNOiVx966sOnCaUwsFdjVk6dyvJ9RRQUFmku12ipi1GhtbSkohobK6p1wFCquahzGP8Aq3D/Xoolc6vu3UjbP58X5u4mXzG4UPHW8ke2xi+GgBMlb+duCku2sfNggcargkQknZToQZauOEBJeSEF+YXCryTcvAsdA7t5/5ONFGn8iq08JRVUKMeqkAxlslWxfK1x7z6EdnzOmwt3Ca+IfMWAuHw8WlVBifyxOp4gXlNNeWkph7QW9e3fm8SGT5i8Og87b4qKKxUPo1Qojtm4WRHz/Dyt+NBu0Spjy+58SuRTUY2xpzUB6VO1cyPTP1nEYSeHIcf1oLFbDW160jG4l7FjlnHY2llylEftRDJyG3sP031IDyqXf8yMdfnYvLNQvBOh9gxo7TFj4gx2Wjz5eKGMG1cMK5VMxaITz2hF/+ZVvDdmNnt9mCJsbJIzU6N4aOUuydvAnM1V9B/QAxMtprC8knI7ZmJtAmEdDJazc0sRxdt3ikYJ5YqBnnQxxjquUbzpTnTDh0xYmSc7FnO4uJqE9K1WnCzVOlgjX4pqjpXJ7hX+XOKY4skXy8WzVGM7Z0MlvZRbJWIaA+lQrvks85HWUWPqbOfNiavx50FxOTWaP59Pn8GqEo+ug/rSJcehLL0FvTun8sm4d9iYX6QYVkhxTQJP+bDlX2ZjthenUvxKSkrx3Db6oGP46P2ZykNKKJTN7Idluw5Y25SVV2stjBCIlrJ1WxElW7ez51C5bwOrhA159t6rfyfKNyxixuYSxbJi5QyVmgfFmC5daVW5ntembcL6dkFBKdYfLE5djWuPYHmVWF5umK+OPx4KZUKJs2rOXBasP0h6m14M6tQEV37as39H8hd9yIyNBRQUFFKgMfDadqNfgxLem7iQQ4pRxcXFWB+NatzLNA/teueJYttOvYnsmcdbC3b7Mh6Wb1mbp5gY+fv2+7ljQB/RK4v2cUixfNOOAxQqL6yyH9Rr19dqiyBL1dgxLy+jrCJGwk3BKF/ZJL/cu20XeYpnyVgqprVXJAIJrZOV2qtUqOZXGPr1709D+UTzPr3JjsUp096iXHzKNXb2v8w8uGY++1KbEy6LU1AS43BZjAZtOhHb8BlLthZodYpRKRnK5T+VqoVVAU4453xalnzCy++vp0rrdLVsbmmWqT+quFBcuI2VG6pp2jhGXn6UwoI4gdTGtEnN48PPV1MgfYzx8KzcGoiKinLNEdXKKmoSik/BEIEAxKM1yXbxL1F+XKS1rrJ4FwuXr5Z/uXQ//iJOTN3O+x+u4bB4WzqFO1Yyad5++p95Ib0yPcX/Smy7lb+sPEa4xRAuPTmHDyeOY9GuKhmsmlLRL1ctEf0C5dNFhYdYuGAFB8oT2PWgVHG3ojqGo3lYFcziwosvonm80J8LSuFoNngE57Qt54NJc9khOW2crqgqZ9OyT1iyvUBxrYZyzVfLo6IqTsArY/6yA6SlO1TqvWA8SdMAABAASURBVKg4RlFxgi7tMtm6Yi7LdlUrRntJ+4hnpbWPxszOuWLJWCFZt21exoqtJTiyU7uBx5GVt5B3pm3kYHkxpXnbmDhjCZFGrWmUbaiR7BanTDaqUUxOKF5ndDiey8/uQtn+g/ItmaG0SHFpNSUZLTGFcQqKYhyKB2jTohG7589kY0G1PNLYEfuiOsKLVqDpQYf+Q7l0xOmcf1o/2mRClWJTQn53aMd8Hn3ot3zzoUe5+9u/4cVZW/ECCco1PzNbdue0c89kSMcmbP14LFM3xchM0/hLp4Vjn+VnoxdRYbkpT/Psvb7WW6DeAvUWqLfAf7UFtKz88/SziUdMSUab/hdx5YlZzJ7wBq9NXwKNmhDN201ZpCe3XDNUX/w/4JWxk/h4yUZK4+A6HkX71uow6yOmrK9m2KUjaZubRb/TTiNz+0e8Mvlzgh270KDqMDu27KYqrRFpVQfZsa+amkCEzoNOpGHBYsZ8sI5GJwyjf2Arb7w+jb3ZbemYk2Dbsu1URXLISBSxY08pu3RQnZUdonDvYXbvOUwgM5dg0Wbys7pw7agLCWyZKfnGM2XROgqUzJXkJ8jMdijKq2T/wYOE0nNJlO0nP9yTs/o1YdX0iXyyI8iIK0fR+PDnjJk0jYX5jf2/Dmgmi2u/BY5DIF7JujVb8HLbws45jH9/Cu+8O4H3VlVy7jW3cW7XCBU68HYdQ11JaHVObagNVI+2pCWqdfAUoMcZ1/DATUqUdHAbyLF9HcnW1/lAZiv69exEdiBKWZVD/3NGcGLzIPbfzo6mt6V/ry7kRhwqy6qVuFTrYL6GqmqHPsNu5YGrzqVVhkdZNKj3m/iWvl43D7h0Puc2vnPNecm+Gtt3A9+6/kJahRNkt+1B706tiUjBJoMv59t3Xs2AlilKQKI06HImX7vnToZ2DRELNqJ39+600KFnuZfK2ZdeyTmDetGuaS45qWCkb0p2Q8zBTSzZVYJfjNHmDAKNenH3rWdRNHcSz455n03xXHK9AraVOFz2tVvoH1vFcy+OZ8yUz9lWJGcSsmc3fTKc07AXd916PmbNDJ56dTxvf7KWkkS67Lifd8d9wEuTNtN7xDAd9KXQ+8QTaLj/M978eD1tzjiP/mYNT4+Zzr7sznSMyG/yqsG4GH9oXE64+hau6ljB6Bfe5Jk33+OT9fspK9zJIbLJjO9kU2GC44eeQ8/Aan7/6gyWFGXQJbeG5dsq6HL+tVzVrYo3nxvLWzM30bBlI/bt3knb086jb3wlT7z1EZUNOyjZK2ZlnsOw66/h9NwdPPXceCYuPkTTVuns25BHpEk2Zdu3seFgObb4oumh3anncHLaHp57dSpbMtvQPaeCHXlx3OItTH77PV6Zk88Z0rt5zT7WFoRpklLG1r0VhDONDtU+0NyZRbz3OZzfOUiznoPon7GXsTq0X7trB/lkkhXfxbrDhguuv5YTI5t46oWJvLO8mDbNUli99RCNT76Yqwa4zJw4nbFTN9DtwlFc3CsNO6B2rCUiGAcvXsHaxZ/w/jvTOdjiTL5+cSc153L1bReRu2ch49+ZyqdFLbjzjnOJJIrIr0kjJ1jIln0VgANKuD3RCWijfHDzPsjNkT22EGs2hOH9g4x/dRIf7ctmYKdUdu86rKTf4ArHAIe3rmS7l0MLc5CJ46fw1rh3eXHcPMInXcEjd51MWHDpPc7kG1cNYPO0iTz3xmRmLN9JlWTetaOSRo0ctizZTHHMpff5o7hpsMO7r4/mhTEfsmRnIWUHSwk3zCZetJ+y0mLKHMmeKOWgEyS6dx1jx73HxJWGcy4+lWYpDbnmhvNx13/E25Omsoru3HPrmYTKD1AYzCI9dpgth6IQK2TtrhqaNA6wdUMBbU4bxqmZO3ny1ensDbekc+MoK7eXSTt7GRy7EQw24dr77uTMrH1MmDiTydNmMvr9FbQ87zq+NbIHQSBv+y7FwAay3Sp26VBr57Y9hDUfS3Zv8efLlffcxvHeOvnfaJ4b9xHLtuxn9/Z80uV/hes2U9NuMKMGpjHppfF8tNOlX/ccNurgf/OWg+Q0zOLAzp1UJcDVkBkjhhX72Lglj8KSvazZVw7BAF7C0PaMC7h1xEByjXQV2B9dGhNPNaG5bTdADVt3oFW6kVt5uIEg4VAIS97iGZN8ata/D83KtzFuwiwmTJ7C7PwW3Hb7OWQIqNkpI7n7vFZ8Nm4sz4/+gE/XHSDmo3lYPphcrrljFK0Oz+eJVyby0S6HFrkO27YX0+/aW7micwVvvzyWV8fPYtnuMuE48jHwtLkm1JBLzu/P7g/HMXbGDjr1aU/Z4b3oLADXNYLzwMnlmruvpXdiDU8/N5qXJkxnvuJD+9Mu5uoBYR2Yaf5MWe/Pn0u6RDikD6qpDTIpKThItPAAZeFsUuJFFHopVG/4nDHjJzErrxlXDusDDZpzzpC2LHtvAp9tDzFqxBD2fzKOt6ZsoUOvDlQX72SHPhDntMik+lAe5UV5VAdzyEkc4nCiCXffexWdS5fwxHNv8dKYj1m7t4JD8hNycijfuYPK5kMYITYTX5rAx0UN6N06hf1bD5LAwZXtNURk9DmXuy/qyrKJr/HihE8pzWpGavF69oXbc/cdI0nf8hFPvTKeMR+v4WClBkSXVzuCnYeN4pr+Lu+8OJrX3llCvgbGTdRAZl8uH9qZje+9odi5lrTW7YjkrSM/3IY7b7+UrO2z+YNovmU/OtTS1B5XlBPkbd2Kl9WY+P7NFDUdxJXHZzLl+XHMOBCiT/dsDqzbo3EB42n8UTEujg4aV37+Ke9NmkVxp6HccWEHvEM7qMlsRKBgH/t14EK4MZffeg29Klfw9Etv8/r0BewqD9E0vZzZ77/Ha6/OITLkbM5on02Xsy7hpkHwzmujeXHsLMWKMmrK8qhOySZYU0CJPszuqwrRKDvODh1c9D/jFJqXLOXlqUspa3sy999wMgc/e5dnXpvIOwu26mDeSFCw9ibQkhHDB3Fw7jhenb6Hdt07ETq8geJwd668oBd7Zo7hjRnLCLRoR3r+Rg6GWnDLbZeQumEaf3jjfRbnp9I8q4wNO/exYX+C3AZxNq7Lx2l5svKNk9j/ofi+/g7TFu2mKlbFoeogDdIS7NVB46G8cjKb5lC25QDhHufyrSsHsmHKBJ7U/Bg7ew2HDuxkV0UKjcIl2P/s+8Thw2h+4DOeHfcp4badaODms0WHI64+eCakUWpuGpV7NzBOcfLtJTHOvPhMWqQ04No7rqDZvnn+GI+XLnmVSf3lcr4Nmg0Yxr2Xd2PJ5DE89coEJs5ZR1FNKhfdegOnpmzjxefH88b7n7H9cCUFimMmN5uo+ORFM7nylqsY5K7jhZfG8trkBWwtrCGnywBO7R7hs3dn8OakxbQ4+0puOC6D/O2FpDfPoWLPFv+QJtRiIJec1pi5b7zFOxuhR5cmFG/bRrV0kUpYH8zpPZR7R/Vh/Qfj/fXkw5W7qYmV6zAondxInH06hN93sILs3HTKDxcK09Fc8nyfxDjE5R8LPvyM8ePnkXrSpdx9UhYVuwtJadyQ6j27yKsSSmpnzYPh5OycLf3HM2baSvIV5VNMMR+On8KrY5fT4qwLOb5hkBMvu5aLWhfy5gtvKYZ9woYDVdQUFuBk5JAozqdC+XZ+IlVrbg37yl3OveNOzs3cxQvPjubZsTNYuGEfedsOEmrUgPLN6ylTrnbVKU2Z+cZYpqyroW+vxmzfudeX33FszIOsfhdw7/ktFXPf4OW35xBr0JJg8Vbyne585+5zMaunKVebwMSP12rOSx9dnjG4oPm7SwTEa8d2KnXYOOIr449LwCQEHaBxZoIVn81i9Jvvs7f5YEYMaUVuj7P5+ojWzJ84mufHTONTjUE00Jyb77yK7lUrePK5t3hRa/jKPRUc1oFlphLUfTt3o+8VpHc8kXtvPIn9H7/Ls69NZtrCLZR7huNOO4kGeQt49ePNdD5zGIOc9fz+jVkUZ7WjQ6MKFm/ZSTw1h5Tq/WzJj0Gigg1bishplsmBVbsI9TqFkd3ivPr8uywqzKBvhwhrtx6WDvbyQPEoNc3FlOexdk8Z0cNbWb+9nJR2fThvxNWc3i5MWf4hluwopFGTBuTvWMeWrVtYtv4wsaq9bNf+JS0lQKyihF37o3Rol8GmFQtZuTmPrXtKaNoim+2rlrLjYBy3SW8uO/80opvmMm/9LrZu3EI8vRkplQdYq73X+rUr2a1Dz0M7DlITCpIiEffu2YXXtBMph1bz+co9ROXwdq2v0Rq1aXeCVs1T2LR6GVuKEhpHDw0nhbs3sqksk9aKkfNmzeC9D6drjZ7OjlgKDVIla6Sl5uOtdGMj706dyYcfC2bOJrpccAvXn9iKRNxjz6ZVlKW2IqNiGwtXH/LHotdpozirRTlzP10s/dexJ9GYps5+Zn0whXdmTGX8hHfYEEunkVvOhu27qSg6xOZtB6hQrPdqPEItT+CGS8+gZWaAeAxqArlceN29XNi6iJlTZzBr3lxmTpnKnLwsBnRpROHWdeyONqJJSqHo5ZO3YRVr8kso2HeQomqXYDhAtGAnO7RvaauYvnTuMvbJmdygA2WFrNh8mKxmzShYOY0J06cxefJkPlqxl8yWTTEVkNrmNPnccNyts3jhtdd46e3ZVLc/hfOO60QYKMnby478Yg7v3am8uJpAwNUBsUO3M67i8hM0JgHYLTut3VGifd9u9pe6pKcFdJC9n8PRDDo0rGTu58vZX47mDfjriOgqpSalYTt6d+lEhinXR8u4PhjF0Nmz5nOYZu270r1Dc9yqKqqqarD/lF9ZZZzUhi3p3qMHjZwKCmJpDDn5dPp3yiZ/7x4iaSIsbCcQIhQMYuxrfa23QL0F6i1Qb4F/nQX+jZycfy5vo+0nRE06g4Zdy4+/dTc3X30Zt999N/dfcRx2/WnS6zS+ef/X+fZdNzHqnEE0Cgo+4dC44/GMGHEhd9x0BSd1yKBKC2LDridz/0MP8PUrL1DfVXzrlnNo07QdI2+4mwdvOY+OuRElWJAruAcefIA7zu9Lg4aduO4b9/PDOy/nwqEj+c69lzO4c2fOvfZr/Ojms2nbKIuup4zike/exBnaJOQ268sdD3yT287vTUoUGnQ5ka9942t85+5bufn842gYDtP+xEv40YN3cHr7VDLb9uXO+x/g1rM74SbCnHjFbfz0W9dzcvtsAko4L736Sq6/4lJuufp8ejVyicXBGKNNLXiBVDqfNJzvfvd+HtJh7U1XX87NN9zIN2+7inN6NSIh/gKmrjh6qJZt2p16FY898iC/+MmDPPrT7/Lt68+mc06Acu1u2px6Hc88fBP9GoVI6zGCJx65m3O75VIp+zXWRvBXv/0+1xzXktyeZ/Ozh7/HL0XjMdH49c9/yuPfvIRODaA8nk7/oZfygx98j9/+5Ds8cOWptM6EGiVgnpNOn7MvSfY9bPtOp02WcKpSOee2+3n4juG0jxjKYwFcuJEjAAAQAElEQVTa9D+T+771LX7zyEP8+OujOKVzQ5tr43Yayi8eupsLeudQUuYRzOnKXT/8MT++egDNdOit/JoKfZFvP3goZ3QIS2slqfrVnkW/0KzPGTz40H1897Yrue3mm3j421fQM8vFye7Ijffeo/dbuPuac+nTNODDO9bejvGfm3Q/ifu//TV+eN8t3DF8EI1S0zlh5Ci+dtOl3KoN58jBzX24xr1P46Ef38ddF/SheZNO3Pqdb/OTOy/j8os0Xt+5Vgf5ER/OmCRdgtmcMeo6Hn7wLr539zVcdFxbMht24aavf52f3H4BPRs6OE17cc+DD/Lzey/m0kuG86Dmw0Xd0iCQwzAdYP3iu7dyxw2XcP8DX+eeczqT0rQbd33nOzx89wiGjbyMn957GYObO5DamlG33c0vv3MTN1x7JT/43p1cNaQFNVUh+p9/Mv1zQpLNw598enKz23OjfPrn941ixLAR/PCBUQzo2pXzLrmCu2+8jDtvuYwzu+Zg0ltrLt3CIw9czZA2aXQ48Xzuu/UKbrrhSm66sA/pomUa9+DOb9/P9248j34dOnPlnffy07uH09faOrsd1951j/Cv56YbR/GD79zN1YObCivMQM29O6TbrTdfycVD2iRF05gY9fqXl8AJZTPknPMYde3V3HnZcTQO+j2Em3Tnyuuu4KZrL+Oe64fRPUc2cBpw/g138vA3LmFQa9lQoMYYjO5o89Csx8l863v38Q0dYDbKzuasa+/itw/eyNUXnckd994tezVJwjqiJZwmXQdz233f4Fffu4V7brqcO265lm/efT2jTu1Kioh6Rj8E6HLS+Xz/e/fwvXuv46rTu5AaSGPw8Gt55Ad3cvVZ3dE0hEADTr/sRvnC3Xznrss4o0djspr15/4ffovbz+pIZlYOF976DR6+5RTatG7DiKuu467rL+HOm4ZzQvsMSQOpbfpyg2LfLdddxh1Xn0XHNCC9BZfdfi+P3HEe3ZuGINiQU4Zfyc++fxsX9W6Em9OW6+97gF/edxnnDx/JD79+NWd11sQVqi++NLZaEMzlpPOHc/v1I7n6spHieynnD2qVHBPBtuh9Gt9+8D6+de1ptNcBZ8fjzuB7suXdI/qR7YCT1Upy3M5Pv3Mn37ntYk7q2Yp2/Ybywx99XXGwOympmZx21W385vu3cNnwc/na3bdw08kd6X7yefzwe1/jurM7kSI6SB5skd9dNOpqvnvPZRzXOsNv1fmKehpw3tUjGdgwqGf8do4uUsoYg+PYmsp5197M1Sc0wVFb21Mv4ft3DKUZtmjuOT5Dwo06cul113HvjcO58ZoruOeaM+mq2GGhIIWB513GTx66S7JczcjjWiMrq8tgjNEdIi16c8c3v86PvnGD1rKr+ckPbmVo5xz15XD+dbfwU60L37z1Ms7ukSUc+bS22ClBT9ssaHfySH7x8H3cccXZjLr+Zr5/5SC0rxWu8WVGxcnpwNW3385PvnMHD4jOGV0y1BphwNALuNOfP6O45ITk/Gna52x+/P17uXpwM0INO3HD17/BD0YNIrdlL2689WpuueZy7r7pfHop9kAqJ152I7966CaGds+l+eDz+fnD93PXlWdz+fW38P3LB9Op10l8+4f3c92JLcjQgfUV99zHD244mSZBidCwK9ffdQcPS65v3j6S49ul0bS3/OS793LvJYNonN6AYTfew6+/dzNXDdWhzoO3c/nAZvhWl+0d33wBep09kh//4F7uv/ES7v76PXz/5nNp7UJmxyF841tf44ffvJW7Lj6Blhk+Jk6t3XEyOfOKG/npQ3fytevOoaM2zfavRdGcHDz8Kh595BvcPmIoN0jGb91wKo0RzQ6D+foDX+NH99/K3ZecROtMR63IX+zNoUX347j3W9/kgSsH0iA1lzOvvpXf/OhWLj/vXO775s1cdVI7DCqSwb97MRKRJpxx3nlcdd013DFiADnqcJr25K77vq75cgqt0q2xILVpd25WnPnJt27nG1efTaesCJ1OOZ97bx3FLTddxfVnd5O3ibY+TJxx+U08/ODdfFvryxk9GhDObMfN33yAh64YRHaGYuLIG3js26Poq7gXbn8C3/vRA9x32YnkinfLgefw0INf4/vfuImbzu1DVlA0dTnq041Wxw+Xz93PfZefxuU338G3rzqBHHV0G3o5v/7Zfdx98Tlcd9ttfPfWs2kunKyOx3Hfg9/g+3dfyW03X8+PH7iBUzu25rQRV/OzB29lRD9rWUPb44by0EP38v2v38g1Q3uSFkxhyMU389i3Lqdv0wya9jiDH/74bq44voW4QbsTzlaeczc/vO9W2W0QTVt0ZPh1N/PI/VcxpHUqqW0H8q0ffIeHbjuf8y+5Sh/9LqRbw4BwDXbUTHZLRl59Hck4OYJTOmViS0a7/txz/9f4scb49itOp32yGWMMSRsE6XrS+Tz04D08JBvdcuEAGoSAjHZcdeed/FRr7tevv4D+rdJo1P1Uvqu84msjhtA8LJjcrlx35938+Ft3cN+N59KjUQQ3oy2Xal7des3F3HXntYw6tT1GoI06n8C3v/9N7jivJzkRNeg46KRLbubRn9zFDeeeyR33384tZ3fB7zIOmhICCtD9lAv5wUP3+OvJqFM6EglmcPrVN/Pz+y6ia2YK7Qad59txeJ+GgtdlHJ8fWi+DaS05d+RQbrr5Wm4+u6tmAqS3G8z93/8ad10wEJ3zCQHs3PraN+/lh7LRXaNOUI6VQr9zLuebN1/KzTeO4grpELCQGS24+IZbZJO7+OatIxWLUwm36MN9372Pr1/Yg7SUJgy7/jZ++vXzaZ8uBL1fdNMtPPzdO3nwjss5s19rWvQ4je/94Bvcc2FfMlNTOW7kDZpTd3Cl5ubtd9zKPUM7JuXXrxEJCNHv3Ct4RPngfbeM1Np8Dz+8/jQaSaDMrqdy/7e/Lp+5mVsvHULTcBIj+QvN+53Jt7/3Ne69eACNUxv8yfhjXMfn1LLfKdxx29XcfMMobht5HI3DtjlFtriCH4vOd+++ipEag5CanUadueaO2/14d/8dl3B82zRaDx7Gw5Lz6lPbkeqTdGg/aCgPCvehb1zHtUN7kan29O6n84MfP8C9F/Qgq0UXbnngWzyinO/8kZfyk3su5eTuHRmhdfzHWsd7NNaEddLpfepwfvLDe7julPaE0hpy/k338JvvXs/Ii4Zx/9duYtTgJpIKjJH2qvqlSasWWnNu5f7rzmdgm3Qqaxpw2mm9yXaRnzbl5OE38lOtDzed0Ze2Hbpx8W33cP9lp9KxYdDP5d1gA44//wqN14M8cNV59BG9ky69gYe/93Vuvuhk2ue4VFZA+1Mv42cP3cpZvdrTqccJ3PXAA/zw9mF0adeOIadfwve/eQ2ndW9OKA5x5V5Nup3Cnfd+k58pdpw7sC0hu28ATFoLzrnsen6qNfLa806gvdbdBAa7OKa17MVV0vnhb93GraMu5rorLuGGm27jh7cMpXmGoVp7GzKacuaFl3DjFSO47OKLueGayzirZ2Psx5xozKFl7xMl2/1+7Dq9RzOCkieR0ZYbvvkt7r/qZLp3HcT1t32dRx64lds1h2++dhS33HibfPd82qRn0O30S/nmHdcyfGBLAgkwRnyjDj1OHcmIgY0pkwyuhI0G0uhz+gXcePXFXH7RuVx22WXccd0I+jVPJaNlX26+617Nkas4tVtjsjuewH1fv40bLtJeIwAJyeTmduC8S2/mZz/4BndecjxNU4NoaYG0XOW/V/Ij5VrfuOFSrrv8Uq695mq+e89NnN+nARVVYPdGOZ2P49a77+EH2ic8+PWbufykLr4/xmTn1Ow2XHTzrdx71TD6NIsQs3qoeqmtuODSEXRLhZwep3CH1t7b5K8NUiEqvLTstgy75jblMw9w+7AhtEiHmIcfQ42Clf2Xh5qfNIpfPXQPQ7ukUZVwCQYC2KlVlWjEhTffya8e+R6/1p7y1w8/xFOPfo/bzuhMm15D+fHPvstFnVOpLDM06TeMX/7se9w/oi0GWwyDLr2Jb13ejxT7Kl7JdvtSX+stUG+BegvUW+C/1QLOv0Ixu2hW6ytvSWmC8ooEZbqXVnjYr6sxfWUute9lCSqrEuhDNvFYVF9Qq6iwsOVxqnQQq1yAeNSjtCROmWhVqpYKJyaEyvIEJf6zFmitXkfgqjzi6i8vjfv9lr6VoVora5Vo1+HEqhMUlySoUruFLxN8WaWH1u0kz1r5yi09NVr4Eh8eEsLx4dVn9fTpCr5ai7r9Km91qJCsZeJXreRDa7qfRFhYW5O0kvJZ29hq9aqUXb4Ma+Glng7ZExQVxylWtfcS6R8VbWujmGxYUJzAfplORBMU6vkL+wmvKI79q+q4+iy+rZaGX6WTf0Auxna86vpKyj1su5p92Y/tS/h9lndVmWSy4yAbGQHX6WZp+/bV+HlSwFNWZN8rNa7BoCGsA5oAcUzAwzgGY2LyhagO+06gV8M0pXwGNVJX7F8jxuMJ4so8E6r22fM7Pere7d3ay28+6ucIrvAT1gHV54lGoq7WIiXh4qLnSWePRDx+LL8kQ2F/cVk6VhZfLp+O8ETXvvvgavsynVoRxCchX00cdRfGl+FFS01i6B0Fl5D9hRePEtUB9OCT+9PWP4AGo//hF+8r5Feb6H2htwVMtlkdLB9Pm9wj/XWCqqNOB88+i4avn8RFI2XhLf6Ru2B8yoKzbX6tbbPtR6p4RasrteGRLoK189onaQEE7+Op3d7r2u3zF7wt4BfVyhb3x8yTVBxlL89/TojmF9B8Mca+XyRlsLTr4EwtsCc5rX5H99WNe6LORoKta7NwnuWlauWpg0loJxIXvO1L1Opl7wITti492Pe6mtQ5KXtc8OoWkOTWs5VHpPRu++WntTpYuGS7uo65PDzh1dH273UEBWdlsrLGhWz51r0n9K5uXZ5vQ8s3Ljq236u1Sx2MpW/7E+r3q+h/GUaEaq9aeoIVWG2bvSXbrQz27f9X6/hYOJ+X5P0jXDHw4cQrefd8/7A4ttbJbfWqG3vbfqTW4vv9ouHrWMvE0rPvtq/0wG72HS6mpNLQoHm6PxM964+1Y+PDSr4jdI88eF+ybbLDymVx/CoZbKuvo6Xnv1s8jb2lqXcfTvLZu14tuD/mVr4kSELxxsa3Ol/38ARoxz2ptyc5kvR89dRnaVl8q59ea+EtvufbsK7f3hNH5PJZH/mxetTRsHA+LdsrghbH9iUkoM/Tth9VE3bOiG48XqnDgKjicRIqpvUkiWdlSfjtfs//h6bVNxGP+/CWTUL2qqPj34Vv249UvUdrKjWmNbKN+EgWv0/tvuxHy23bjqJn5fE0RyyPZLUtFts7Qsu3hd+sNusroudD1NJJdnn+uMXr+kTTyhoXTKK2zeLUVcvTH1P1JQQT1932efZZ8ts2W+N6r6Of1CVxRC5PuhyB17OP/xV862AsiMWxciV9SXGqDt7n6fm+4/PVu4VX0iIVGAAAEABJREFUQ61etXzr5LHM/Or58lgcW30c264HX17RSUg3XwfbflT16niLpoVJdiXp+TKqXWSskL4MFsano8aE+o7A+IhJPNvuV8Ekm70juP67furskZBcVkZ7V/Mx1zGy1dKqw7My1PWLxLF4mgs1VRVUKI9LWBnrAEQjKa/nz0kfSW0+zFE2sjxsm1/V78MJw75bfN8fkgIcpZfnj4Hf5yPUvouubbNj7YlWnb9ZEMvH0rN0k9UStT1f1C/DxL+kSxLfk3Rf4Ngnr3ZcE4K3VBPWDpLF3n17SxYLV1c9vft9gkvU4ti+Ov5x2y4Y22b9MaF3y9u222avjp998YGsyyRkH1XBJkTTb1b/ERvouS5fSggmScv7wo5WcCEdS7u2v1YXi2NtK7BjLsfxSA0ncOxpqQNGSUqV9iHa8uBJFj9PV05fpraY9imVys1Lta+xuTwqEo1q7RdKtKcq1f7E/rNeR3AsnM3jRdPuKWy+XqX8PaZq9zv+3kk0fXx/vyOZa2nG1W5hitVepX2MyGB5WZmqtBeytCqsTOrw2z2Un3pUSD6LUyYYu2eq0L7G5yM4q5snxey+r1z9/l0y+/sk4Yu1v1ezfC1OteS0zZZnma+fh7VBeR0P4dq9lq0WXkuIv686svcSspVN6lMjG1VWe759PW1g5AZ+m8W1ctjq7+9q93xltTzsPtPuD+3+tlz2lPi+D1s96uxQZtulnycFLF3fnr68CXwbSM6kTNTyp1bPBJau7bO21HD7tBN68MdZeHYP6ImwLqwdKtWmcEFCtinT2Fi/kEtii8Wze9di8bbtcj18XCH7dxkifmSfDHo90m+fLc+6/aLd7xVqn2npxMTQtlfKNqEgpIYSBNy4ZBVhksWTEJZ/8q3+t94C9Raot0C9Bf4XLKC05Z+rpiMOTZtAsyYOzZuqHrkbmjZG1fjtzQTTvJlDZnQ3xVXVRIs3sa2whOZNXJo08lSphXXV5hyh17SxOepZMI1U1da8qSs44+M0s8/iXSdDM/XXPTe1MtTKZNubqu8I7hFaklv4zZtYeqJ/BF7PR8OLVh3dZnq2tOy7rc2F47f5NC1ebVW7z8+nLz7+u0Mz0W36ZVjRbGLbfBhXdqutem96VF8L0Wqm96aNHeqefTy9W17NfD2cL/B9+1hajuwFTYTry1zX7sNLXsv7j/pqcdRXZ2dfFsE1lVyWX7I6R+lkedt3aNgAcrIN6emuEhPjJzbg0qh1F/rY/9cauadRPfoyxuDq07vrODiq9jkJY46823aB8eVijEniuha3Fks0nLqqflSMMYJzRc9gjMFxXVzH4jj4/Ax/VOxfCtg+13FwhAMGS9d1HD0BanNcF9dRv6qFdWrp+HDuUe2248vwwlETYJJ0a+ED/j1A487d6d4kHWV3IBiOFIPjHsvXGLWJnlNXfTmSbVYudWNMUp4kjA8A6nDcJC1jDLbPdRzbDJjku5vEs3QcwaBiBOPU1do2K6iXMMJKsGvNXiqJsX75Ug5VOb4v2MQXWwR/BFc0jG1TtW2uU8dbDUddxhhcX04j+hwll/Gf6+SithhjcFxXOEnZ62j/MZzjw7iOQ12f0bPr6t0x1JW6NtdxMEbtqq7oO7UwjuPi6tkYg+NY3GTVK37RwzHtyUYf1nUc0fQbqOMjUmow6ndx3SQt19FdrPmjYnw8x/bXVWOOQBljREN0RNS2GpN8d/SOX4z4OIJRdRzJYlT17DpqN9hyRC71O7aKhjGOj+McoWMhbTVYGNextDiqGL89SZH/b7E0HPGxgD4vx2Dsy9FV/T6ceCXv5hiYOrldx8ER7NGo/rPaLJ7rqF/Vt7Xxe3xZXcfgOtKjfBdjXn6PQ60GcWH/Zlhfx3Fx3SSeIxhXsOr40mVwbJ/r4DoOYoctRs9OXa1tNEYwroPjvxscxxWOhNG7Uweru16xxejZdS08GOPg+v7o4Nh2x2CMSbbpDkbtSXoGFbX5cK6D6zjoVdXg6t2xuCB49fnvDo69WyC1H33VyeA6glF1HdGyAIK1OEfTs81HV6N1wfYfXL+Jwso4hbu3K2agWCHelp9oOapurTwS0JfD4jh1bXxRjDHqd7HwqDiOQxK29q5+NYu4o1uCndt3UFFTw7rFaygmCeOpB8H5sh/Nw7YdRc8IzhgHyyNZbYsaMX6b5es6DkIDDI7r4ooeKkbttt/HEIB7dJ9xcF1VwTi18EI5chm/38X2OY6F86lQR9O22eo6Dn6PMTiuhXNw1GZpG2M4Am8Mthjj/BHfOhgLYozx+x3dUTkC7zo4jsEYo7vjw+gRNejZxXWcZLvuhqOL8dsdtdvq46CiB19e1+IZjJq+fBnjYPVwHQdHvJP9Rs+O3+44DiKD/XFdV+0Gg4oabZ/rOmpzkm36dQR/pApGkKC7W4tLbamzhyOejmtp+FRre5M3YxxfBtdxcETDtho9u4K30HX9jn3Rwp7AylHJmi2H9VbMyvkbKcHB1WGk5yMbLK4jBB8FFdF1HMG4Dk5tu3Hsc21Vv6B0GfU7Pr7riI8loD7XddXuv+iuftuHLSb5Lrqu4+AI1hgj/Dp4MGp31e/onqyWDseUL8O4Ti2MMaLl+NVRW20rdeWIbWr7HKcO1sFxVYVfB2vvxhgcwSSrwZAsdfxd5yicWlgru+s46FXVScpiX5KoR9pcxxHtWorqd103+a5nx3VxHdvv+HdjjPrqnvGLMXp3HRz1gcERvOvq3d5Vk+18qRgsTGa6Q8MGytttDl6Xq2v/kMzhHZL7FkMz7QuSz0lYm6cnYVzBOH5+nnyvw6mF8/P4ZH9T0a3L5+1zHXwztX+xXzG1ewvhWHkkV1O7HxDMEXjb7rfh7zma2j4rn63iZ+F8efXeVHDJ/Yt0UJ+/l6q7C8/2J3mbL/jadp9vbZvPzz5LN9G0NI5Uvfs0LE09N/Nh8eWq2wvZtqQMyfamFlbVl1N3S6uZ5Gwqvs1Fw9Zmej7yXkezViaLZ2HqxiMpPyTb3eR4iG7y3VF77Vh8iYel0ezLtC1/i+vDQpJ20nZN/TYjO8kOtXhJvZL9ze2er7Y9CYtvB5+GaDYX7WaikcRJ9tnn5Fi5ontU9emIj2hanIa5kJXpEAq58nBDXTHWv50v3uva6+//Cxao17HeAvUW+F+1gPOvUFx5lBIq/my1iZTyNII5rbj2gYd46vujOLlzto8TcI1/1zpVf9eI/bfZwc+5OboY/8U7cgLpv9b//DkL1BrRt1nSfH8O+j+kz2Dkz+DQeuAwfvn4j/juxYNokpoUrz4nTdqh/vf/kAWM8YVN73Ai93/7Fu6+9ESaRWybobbL76//+estkIwV0KznGTz8u5/wzeHdk//ZrjrMX0/uL8ZI0nZoc+LF/P433+feS/qT4yTRk33J5/rfegv8cy1gSK6JKfQ671qe+f13ueGUbth/FgkcDH9DqUf5r7CA0eD/t+0L6vWB/yYbWB/9r5hs9UrUW6DeAvUWqLfA320B5++m8E8gYA/R6s8e/wmG/T9G0hjzf0zif7+4xvzftZk/7//9JvyXSlDP7L/bAtan/7s1/PdoZ+3q/8Xnv5j9v4vvv1jNenb/8RbwsL74Hy9mvYD1Fqi3QL0F6i1Qb4F6C9RboN4Cx1jgf/3lP/IA2hiDrv/1sanXv94C/1MWMEbz/n9K43pl/9stYIz5b1fx36KfMQbzb+BsjPm38P03qFrP8j/aAvJD+eJ/tIj1wtVb4D/bAvXS1Vug3gL1Fqi3QL0F6i3wb7DAf+QB9L/BDvUs6y1Qb4F6C9RboN4C9Rb4l1mgnlG9BeotUG+BegvUW6DeAvUWqLdAvQXqLVBvgXoL/K9YoP4A+n9lpL9Kz/q2egvUW6DeAvUWqLdAvQXqLVBvgXoL1Fug3gL1Fqi3QL0F/vstUK9hvQXqLVBvgX+jBeoPoP+Nxq9nXW+BegvUW6DeAvUWqLdAvQXqLfC/ZYF6bestUG+BegvUW6DeAvUWqLdAvQXqLfC/ZoH6A+j/tRGv17feAvUWsBaor/UWqLdAvQXqLVBvgXoL1Fug3gL1Fqi3QL0F6i1Qb4F6C/z3W6Bew/8AC9QfQP8HDEK9CPUWqLdAvQXqLVBvgXoL1Fug3gL1Fqi3QL0F/rstUK9dvQXqLVBvgXoL1Fug3gL/qxaoP4D+Xx35er3rLfBfaAHP8/D+C/WqV6neAv9QC9QTq7dAvQXqLVBvgXoL1Fug3gL1Fqi3QL0F6i1Qb4F6C/wLLVB/AP0vNPbRrOqf6y3w77SAPaTVWS3/FbXWkAkpZYzB2Hc925Po/wr9pMvfqoc1xf+V+rfq+N+M939l7Orl/Nst8N/sv/W68R+7xv7tHluPWW+BegvUW6DeAn+rBerx/n4LaFv0H7u21uc9/7l5z3/M2Pz9U+D/PIX6A+j/80NYr0C9Bf56C9hDWp3V8l9R7UmzqiOlqsqLKatRaqJnVP8r9Ps79PjrPePfh/G/PlZfpf+/bzTqOf+rLPBV417fBv9EG9TT1ppCfam3QL0F6i1Qb4F6C/wftIBdwupzBOpzGTnC/0k/oL78Sw+g/f88XmdD9v7vNP1fy/+vhf936vaneMvsf6rr/1a7/Xz1N0v8H/DPM/xd8v/Nih+DWFMDFRVQVu5RrvuXq2239avav9xm378K1rYfqeJjYWwt95//mK/ts7W8HP4I5k/gJOl7lJZ6RGOG7XOn8ZsXZrH+sCf9PMpqdbN0656TOHU8dK+FOdJe+152RI6vgFFfsh/ZkK+04Rd6/Kl+ySc6X+b7j36vqoJ4/Jjh//++2Hj35fplpLr+r2qva/tTMHX9ybtHTdT6o/en7Vg7Jl9lGzu2SX851s5/qr0O1h+/P0PX5/UvGB+fz5+Ro1LjF4slLfXv/v1nriH/TNp/jd3+0vB8NNzRz38NL9/vK4/1W+sPdb7rx5A/4xsW1q+Kj/79L4H9R8L8A/n+RfPxb5H9r5XRwmvef6U86vuj9ro23f/kGPy5vr9Fp38Ajl3/q6vB/ldDf43P/qth/zlx4T8gD/xHGfJvDT5H8U+u0/h/TXhUs99gyfv9x3T86RcLX9fr46mh7l7Xfszd9h/T8J/98s/xx/9snf+d0sk9/p3s/07e/8A485cYQjBH+6dej8hv5+CRl7/j4WiafweZvwvV6litPWx5hXdkj/cn196/Y638o7W+jtZfuZ7bfM7W5N5DMiu/+LK8f5JXHc9/9v2v1MmXXzhWbl+3PyWfYHzY2n4LW1ZubaBa23ak39pF8JbmkbYvw/wXvdsczO4B/q7J8H8c+V90AJ0MxMaY2q815t9qNmP+Ov7G/HXwf69y/4wg/6/V4O+1wJ/B/7vGwmD+DOl/SdffJX9yHv2tckZ10FdQCAVFUFRqDz4MxcVQqFqkau/Fte0VFYaiIs/vs+2lZUn4UvXXwRb+CTq2/+haKlrlqhWVhnLVStWSEvEVvqVdUkvb9pXbAxn118EUCa4Ov0xwxXq3OBcUtRYAABAASURBVJa+z7/EUBlzOLRtJzNXltHrhKF0SHMoVbuFsXJbvmWS28JbXEujTPKUazGzz7bNwtpaKJ0tz4paOSoE48sq+1h7WNuVaKH0+9Vn++vaj+CLV50etv9oGydhoKTc6KAdrEy27Z9VLf38ArB2+Ev9xhiDMcdW7UZ1WGFTvyQVY5L9X961GmOSAPo1xvh09PiVlz38sOOQTDhM0hflE9YWdhys7CWy9Zft5/drPKyvVshXym0VXB2OHT+/Te3W/nZ8LC3bbvlVaGxtLS6Wf9fyszTrqqVToPYv8y4oFLz8r0Tja2Hr/Ma2W9p2btS12f5/RLVy5xeCpf2VRvwXNpp/BC8tbl940RcE/yG0vyD3Nz/JZf8i3KPhjn7+S5DtB6ECxRk7rtaX6mKQ9Z2jfdrOC+uLtv1P+ZLtK1UsORKjNC/+FOw/st3yLRFf65f2+W+l7eNqTlldLQ3//R+kg6V1REbNZ0v/T1ULa+dwmWJGuY0Piu1H4obGyvbbdcjGDd/WtfQsfdtWLjzfFoI9mocdv+Iygx1nS+Povn/ns5XFxji7NtiPXH+J3/47YP45ccFg/h3K8Pflb18p8l8bfCyRL1VjDLr8atf5I/FZjbrUrn7hKHTr909fFs/C10EYI7yjal37MXfbf0zDf/aL+TvE+//Z7+8g/X8D9W8wgNzj/4ZuXymlwXxl+9/Q+JcYQjBH89PrEUbGHN1zpPmvfvgHkfmr+dYh2D+eKijA38PavKduX/ePXEvt2mjzMPsHAJaufbd3W+2z5evnAP+fPMXC1u1tK5Qf2ByhsspQtycpqM0hbN7h70VFz+JYPn9rLRRNW/8afMvT5jHJ/AX+ElyLU5cPWd2sPY7oIz0sDSuHn/sov/OftW+qkP7WBrZWaM+WhNO+SnKXKueyNrL2+ao9n4X9b6rWhoWF8mXV/5Q/MqqbZ/+q+7/oANooEMcoOHSIAwcPsy8vn9Lqv/LP8r5sES1mCdUvN/+pd5sc2b69S6fy7e8+wfubNCvU4NlTEN2PuUTXh4+Xs+7TCTzw4LMskJNYmK8Ct+3/qGr5/qODfE1lOcVl1f8oEf/ldDQc4plg/9oFPPyjX/HmwuRgxOLWWur6M1cdRPTAWn7/8C/57QcbiFl4L6HtgH34V9Y45YryZdWJv5GpnUd/G6r90mYXCPsXsb5NopUUKgLq7JaAC64igb3HqkrJz8/ncHElJmj89qCJUlZSxGGdmJRW1mAEH3A8AkGgplzJQAlx26bqfqk6XjXFirL5yhryDh5i/4GD7M0roFJChEMQMAmqyoo5rJPxw4cOcyDvkOpB9h4soEpmchI1wi8gX/35xWVENQGD4uE4EAx6WHkLDhWSX+XS54ST6ZwTY/fuw5TUxAk6McqKizh4qIDyaIJAAOnqkYhWUKCdd75W/hrxsHpbuS3NUNjgxMo5KFnzrCz5xdh/0cPCmIAhNSKVSwvYfzCfg4cla0EZjnBSJItxwMePlpGXd1ixTv35JcQNWHxL39ag9K6SPYvKa7A2dB2w/L+yOgmd8caOqnHsJtHCGuJHtdfCyKsdjQ1e8j2WiFFVE6OwOOEnFkL+s1dNZZnGv5BDhw+zX/rv3XdAY1FMVBHcMUb8pH9FKYcOyUeKKvHUpiFBRqWkSLY+XEiJfKRaMSdfNj7kv0fxvsQ1LrsXHI6ye+dhdmm89h8qwvqi9QmRJJxiSJOtK5WZxWUf335u0k7WhvY9UVUqOx9i/4FDFJRHfdvbMTbxSg5r/Gz7wcIKnJCRr4Dj1VCUL70OHObAoWL5rCGksbD0rD1ttc+hiCEjFarFW1ZMjp0LqemGFM2Fisoq0HtAdnZ0T1d7wKuirDKahHXUrXZL7+hqasfL88fGjmMCy8/C2L6EF8fU4jqaFz6c7BaPJSgsjpFf6P2RHdX9L7g8qsrLKKmIipenCrHCXbw3+nUeH/MJOwtsm4fvB37vn/6x/mLquhWD5QaitZHf/+SnPDFrZ22PpVf7+E+/eUmbeqUsnPoG9//kZVb4y0uCo5cXT7JaUWJF+3nvxce5/4mP8MGiBbz9+GM8MvpzSiyA4JLrlX354+ofPhdCVEEhIh+pVLYdk7o2plmfjleVyKcPYudeoewdVmwJyiesn1k/SVYP6zehYA17Vn3Arx/7LXP2eGSExU+wSRj+dEwR378HxvIOugn5RCll1THf5/9qerVzJxQvYOHkp3jkxRmUSP5QbftfTe8YnTw/rlbs/pynf/cYby/ZTThD9iDxlTax+ljbO4kqP0e1cSNPcR2tf8EAOIr7ITdOSf4hxf1Cqj0IhzyNgUd1WSEW/oByWvtPP9nxcmrHwDEeQY2fV1NBRU0saafaviP6Sd+6WO3Pdz82JHzammV80RbznxFNi+vH+CPxJI6pa6+NG3Vwbh196V7nQ07tmhKTM9oNvd3syQ3lPP9ZV01FGcXl1Xj/CLFqJ2XZrpX8/pFf85spG5NU44l/DP0ktT/762H0vz8L8hd0ekl55Sf71nzIQ9/+LZM3aWcvTK9WRz3+5VcimlwTla/sP1iMsjvJ6IHiWHlxAfu0Vu4/kE+ZQr8xf4qsZBKKUWZdWlYhrxSc5CvRWrtnbx579uWxe+8hSmyyZbsEqxvRvFU89oNf8dTHW+0rJBLJ+3/ob41ymr95HyWd/7T9/kMV/keL9RcZwFOkEmOvkmV2Pf7R66yv0bu8/i/JLyzkv7NqmH32lbuX8+gPfsHTH+3w3+18Sj78Db+aS6X6glleO3++mkKCCu0tS6tiJKoO8M5zv+G7T33IYTulEtWU6ES1Mmpfvhr7L2lNRKsoKqnA5it/Cfw/Gsb+1XNhEdrPgJOIa29XqHOk5L7O/fK66vKVa71dO/9/1eYCiepyf+0xohPQGmrX9FAwyr4103j0sceYvSvB/y/fCmidrSorwu5dD/p724Ps2XuAg9pbB7THSLV7RtF34lUUl5RTt8/5/8n3J/tlAyu73VNaef8knHge3RdwParLSym3uVxQdhOdo/u//GxpB5045YX57Ncecb/dq8vxU+r2UsJ3ZLOgaMVlx0rtX0L2WbntAe0n/fVgj+xQVCFmEFB+FQ57oqe9megdLCrHc/DzJcvraP5Gq8sXOVFcLpZQriQy0slo/UloD2VxHBPnC7iYngVrPCytP+5Tv6KOnx9Jbo7wSOL47aJvfcwc3WdxHMvbw8/h7LsP52kNreOvu2334aCOd11+llBQKy+PcSjfIxqTOv9jl8zyT9RYCZEuqCll7gcTeGXCbKbPnst7b7zKs1NWU5bQuOmAJKZB8KwYAo4rCdFNHR72OaGXRG2bV/ts3xNazOyBiEWTdyl3SSSrYPy2L/+IT1R0mnXvRotAOYe0ubMgx9D0hVCraBslYJ6bRvc+7YhUlVHqL4ISSzR8/vZeBy+eR9qkiygce32pvw7tGLl9ep7vuNU6PEuIjpUtfiRB9o7Rz/YlLI6tol/H8Oj2aDSuZRtWT36Jh57/XIdIFsoTWy9J62hc0bC86mha26vJIkjpo+All9+ozjrYhNo8v/HYH082TFgetVUoopXA8knCe0k5ki/qq3238D5wkp6GQw8OzXr0pmWoggKbDavlGPpHwavri0syRBUA3cYd6dUsSGG+gp7tFfwXsiUF8MTX6m3brYwJ6ZUETfi+6ENZPI2J36XnOvhELa6aLIqql9RN7Qlt9KI+Qh6v/vpRXliQr37UfxSM34/GJuHzsvRstfTr+KLgWq3D6zoeVl4LY2tdm0/4Sz8S1z98lBjYwBcKx9mzdgY//d6PmLqlgpCCpp1nlkblvlW8/crPuONbjzBtYwkRLQxOTQkbVkzhqefGsDKv0IZgEiIaCsfYvHAiP/n+L5mzL05Y0cSqYel4+vEAEzvEstmv8b0f/ZIxM+bw+YKPmTzxVZ58ZTKbi2I6gI6Sv+VTfv+rh/jlm1OZM/8zPv3sE94d/QzvbNDhYayA5dNf4Hs/fYzJKzdTVBFXEEe2E20t8MX7NzH68R/y01feZcmqBcz6eAbjJr7FxE82EAiXsWH5FB757n089Nw7HIihQ+k4FcW7mPb684yevYhDVdizRNGTtLJDrGA7U98dz/uzP2PBkk8Z9+brzNq4n4QOIiJUs3HuB7z57hQ+XTifzxfNZdb0cYyZvpyCqCEs/KpDm3lv0gSmzZ3L/M8/Zuz40cxcVUCaFmc7ltY+kfhepr7xax56cjKHHHBkK9uum8afY2pCq152doDcBnXV1eGokf0hku4e1a7+3AA5aQbjGDJqcRoKr5Hac7McqnTq/6f+EtryRnOlYO82Xv35T7jzp6/xwUdzmPzBdF5++RUe+f27rJCxjA44inau4MXHf87IG37GxHUlGg8PqktYveADHvnlGJbuL+Dg3u288vOHuevnY1m1v5S4ZaAqSLHxdCADu9Z9zrgx45mz7DPZ/DVeeHcuh6IQUvJQeGA3c2e+xaNPvc02xV7lKAhd1VMVTLCIjyeNZdKsT5m/4ENee/01Zq4tJj0jwapPP2DsO9P4fMkcxo99hbFztuGkQN7mebw1eqLPb9rkN3npvQUUiF9AiYmmqU/XAGUF+1ky5z0e+/0rLC+sUoIERh9C8nZs5oMxz/L02x+RL8CAomtCh0s7N65m/OtP8vSUNUQ0znVzKSlvcjzt+IbTkuPVQGOSm+NqHB2MgOxBp+1rpLag6FpZnJBDjuDSAuDqObdRADdh/r9/Ce1pDH0/E5G6GOaJh40R9l2P+EUPti1ZZVM1WjgLk2xLYOn4MYM4n7zyBD8bt15QRnaqZva4SextcxqXnNCOQCyqNo+6eCTSgrN6JzSvktXSlrkw8RqqYwksTALpH61mxefLCPS5gJvPai080dEqqIcjl8VNyuQl6VlkVdumm+Bq262R9SbOSTjZIKE2z2/70o8QE7X99o7JoE+/DgRLSiiLfQlWr0YyJeIxnMymHN8hh9KCErXCoaULyW9yHLddfgKZahE76Zbw+YuFWr647Lu++SH1qcjfy6LZb/P7519mWR6Eg+BW5fP51NG8OeNTFnw+kzdee4PZm4px1OfJSSx+stoxiFMTC9C2e3cyYhWUal1wxCohAewY2ir1JIusIQMk8Txs+zFV8Mk+fJmP9NW2J0Qkplivm08roY1fVLKkBkuZ9eZvefHTnUn55HcWtg5f6IL/Y34JdSRELG7vsTheWgN6tsqmqrSEGstH1fb5MgnG0kscuXMsTbX7cEf0s/1ID48aGblp6+50SI1yWMmbkXE8wVt6ftVzHa7EIZSSYPPSWbw17j3mLZvLu+Nf5Y0P11Ahm0aoYvmcd5mkcfns06m8+d5s8hTv3ZrtTHr9LaZ+Ppc5syfz/JsTtD7WaD1N6p1wDaGaPN5+6ke8svAQATuOYqpLepCs5otY7cd4G6uzHQKK4WkZAWz89tsVC+w9Qwfa1h74/S4N1J6T5RJR4LBz1cYKGzfSQ/KRBCQvWdkiAAAQAElEQVQEl6m1ICfdwejd8kZrioVplO0SCghG9tP5BPYwmj9TPBkqUVeFY0Hte7y2zb/7DKAOtm7uWvC6Z4tjY6TFR/D2Pa7xsO8Wz46PfV4+4Rl+8MoCJDaefMW2+7DykYTg6+jZdpGxKGLsafwTtdWzIcdvt/xjkjO1ZVf6NDIcUly3HV6t3ybUVyeTbYvbd1VLO/4lXnq1qKpeLR/Lz3Kw7D1/jvn0LL6q3yMB7RpSLT30KFzBqq8Orq7N76j7UWNdf+IIU4ORzDEToHnPLjQNVCgfjvkY3lfCf4mPD2nbfKnYu34+zzw9gQ8+/IxJb73Jb9/8nCL5dqJkAy898RbvztTa/N4kHn12MusLLR/PXxe+kAffBsbE2Tj9Ne59eBJ5lkfFJp793RM89MvnePTxZ3n4V2+xYF+F7VEVDf0Gm3SgR66nsaj0aVhb142BurG+kKizUVJcPOkeV5vn6+r54+up7Y/hPJ+m3y5YS+9PVcvH0rSwvgyytScc/128FPZ91FXvvMj3X5iv1dC+JrB4FsbWpHhH8bR41k/9Dg/PQLQqhtWvjrZ99nEF4x2jgxosi1oZLIyFtU3H1K/s9/DhxT9h9TgGofblK/CsLl+2QS003tHwdTSPaZMtaoET4nsMHcH5XbonYjXUaO04Qs+2CV4kj+Ih3WU3TAp9e7TAKyvx/3jE0k0IUL215BJf6Ck6fuOXfryjbJoE8W1T67+J5DxNdhyLqbaElcuvSY5J+9Q+i67VMfl2LCrqi2rtCbfoRFfFmYLCyiTAl2gmcT1fB0vL8vN9T3BWbvscV0BPWOzYLp75+WOMXlVu37C+YOWxOAnBRIWDV8iEJx7niQ+1Hkea0rdLYyoPF1PjoIR2B7//+W95d0MyZ/FkXx/X1y+BlxRGgEddaqyDsX8AYXsK187ke4+8xVafjKcDswQxfVgVKIhGHby9+20W6UvV6mb7bU3CeNjnumr1tvp9CQ2FTR3SojHzcLWO1hTvZsxvvsXPJy8lprlllMN78g+LH9fd0q57l5rSUXykt98vHxQIdf3JtoRoqyaSudi2T17j0Tc/pky8HBFIaO9dE3Ox+VZmvFL5VtwuwdqHCaeWrsDw+coW/p0YBzfM4Ymf/4BHx3zAp/M/YdaHU3nz1ae111nA3krj7xcqt8zi1394ky01ENRqFxO9hAS0cvk0a5/t+xHdxKMOxm+3Oslodp8VrVQ+rmcrQ0IEkv2SU3RsW53eCb3bNTHoVfLhG7/hFe2TQqn4OiWOwksIzscTT0/tEpL47kW8OWEC0+fNZfa0t3lp7Ex2lieUWnjY8TMBg1O8hsd//RgzlcNmpSXY+OmbPPKbP/Di6y/wh6eeYdKctVQGpbOpYd28Dxg3ZTaLdD4wYfybfLbbIyVgaXHEphIDu0+yeZDNe47soeR8VqxwegCb0wQccFNcLNyRqr1VqnIiCxdI+VKfcq6cDAedwyu2G1K0r7b0Gyg/sjgaEl8Gyz+U6pKTo7226KWnOBgZJqHgbvfbloaTkI9qr3AEroFLVpqDUyujq0MCK1OaZNHUxSg/y24YIE15XXERsh3/U8X5Z2rrO6KBoo2fMX5BFZfcdjk3XDGCO+4aSf/mYW1E0QAECGgQBKYXIwd2MPZFP66jgdPd0V03tRvss19jRWzfuR/5POr4ot0C8sfFOAEdPoleSpiIdntuLZzjOkfh/jEexiEYDCARscX9KnhjcCRjslrhObZ8qf8IxDHtkkPvlZvnM2XJftEzUstg+RlsMWpLwqBijEm+W756prYYY460B4MuFrf70Mt54MoBBLDFYIw5AuPoGVt0t7ySOji4Tu041PbVtTuOsS2IyBc01FbbytHFGIcv8GrpGQfLJwlv8PuTLyAZ/HfHkb3rGjmmuPqsZv3FNgYCbhL/z8Ab4xAMiJ4TJqJPdEHJii2Oi+s4JPkleRm9u06yzXXtvbbdOD6s/2ZltH32Rc+u4+DUVtdxUJOlrmqOtDuuS5JvEy6/7Tau7J+DLY5kqcO1z7bNGMfnVdfuOqKZ7GDTvJl8vlfvRoFKgc7KWwf3BV8LfGytVA5k//kNu0jjGkx+ng4+GzG4f1s2LlpOhQNGATIOtO7ck/7HncHQzkE+eW8ia4sMgYxc+hw3iJ6d+zOoRxNCSrASOrX2DuyiIKMjg7pmsXLRWmIh8BKeqNhLFPXs5rTkhEHdFbRbc/ZFl3Db9aP41o0Xk7V9OhMW7CYaDtN7wCC6Nc6gw3EjuOWai7nxhsv52tUX0i7FI9CgKSf2EX52C046tR9ttZm2fwggsxCtcWnbawA92zSgWaeTue7aEdx47ZV87YarGdgsRHUoW7QHc9rxJ9C4dD5jZ2wkpjjQqH1XjuvZnQEDBtK+Af5fVSdkgdRIjJWfTGFNojs33nwx1111KbcOO5lGrnrDcGDNFJ6fspFeQ6/itlEXcMUlI7n9svOIr3ybF2aux0mH1R9NYI3pxz23jhD+FVw//GQaeDEtbPiLVSBFdNbvp8nAQbTwdrBkcw2pIbALI0cXJbL+V+CCLYzXIe1TL47jmRfH8tTL7zJ18X6CaTFWfzKLZ/RR4JkX3+apF8by+6ff5I25u4mV7GPK2Ld48sXxwnmbP6h/9LTlFCpTq6nGT+aOZmWfjQFPC2nTjr05rktDmnc9kVuvuoS7bruBH9x7BZ0qFvLjX0xgW6VD4269GXLqaYzoGeADHehuLBVySi7HnTqQgT37c0L7prTq2JNBHRvToksfTpKRA/IFT0wESVWN0WbM09g35tRhV3Pb9bLjJWdTvGQKn20pJRR2qCytJF5RRkX0ywmypYCSxTg5XU7l5msu5eabr+S8VsVM/eAj8hMQbtKDyy+/lltuuITrT27OkmnvsKEgjpPZgjMvvIbbrhvJbZeeRuGCd5mzqYRQxEh3TzMAlQTV5dXEysuo1KSJJxwkNiZWTXl5udasCip02Oc6spdEiccqKauMCadEH/jEXBSOvTybvxD2qlj/2TSeeP5tnnvpbZ557R2mLNpBleZjqlHfnA/4zauz2a2PBOGIR/G2Fbz+0lg+P5igYu86Xn92NLO2lvmHRLE/8ycoxjhH4kddDDPG4GjC2Hc94hc92LZkNRg1GmOwMMm2JB2FLiDAiZddz90XdJCdwJgStu0rJS2nNa3atKFF45DaBO86OOIjMthiTPI92SYOOqRYOeNDVhY7+DC2KRim+8kXcfeIfqTZRs9gLPJR1RiDpeHUxUu9o2rbdAPMkX78YmrfLX+jXv64CNHi2+o6TrLfONg1U2z4o2Lh3QCOOlO0joRDAX9cU3sO5a4rT6ZxMIlh6dVVoSQba3+rNPfsX/G4JkZlRZUOumNUlFRjXBBZYlGPJt1O4drrL+P2G6/m9JaHeHfKHA7GIaTM1vNqCekWirikpjhEImFSgkEf33YHldja9tRUh3CtTAJPXsaQovZ01RThputQMkXwtY7vzzuLa/v8dmEFIw6ZgpO6eoOA4n5mqqHGy+SUETcz6riWJKLqMuInWItraQQd22ZIU/KdZvmpZohOJGR8PpZ3WoqbHBvHJRQK+fqk6SNNmuajAUzAYOHCktHS8GVwDFY3n4/gHI4t1gaBkOPDRCIRwpEgrnAslKN8yMpmca1+lodt92sihsnuwPBLruMWxYdbzu3BxlkTWFWYoHLfQqYtPMzgSy7lnmsvJHfvPN5beAAvNZ2Ogy/kTsHfdtP1dKtexrsfrSYesfHEENGcX7FkNsv3RokoV7Hj5/nM7I+HXB03lsf0ce/wtGL0M4oLTz3zpj5wzedAURFzP3iHxxUvnrZx/4UxfuyYsbZIOol+aR5zpn3ACy+N49WJn7Jyf5VsDYXbV/P682/xoeIo2sAFi3fy7tjxvP7RFmJBfD+JHVzP6BdH88T4hewv87B2tZvM8gor15+uxnFw6qpJwtl310m2u47u8jHbUwdrjPFxLLgxyWeL4+gZW3S3765jIcDiuW5yVPtccA33XdYfFzCyn213xCN5NxhjfNr2XY/4RQ8WJlkF4zfiwwaE6zhhIpEQAcVdVEwgIP9w8OGFqybBOkfaLG3XER31OcK373q1YKomiad2p7bRGIMP4zh+n+s4WvdBRNn8yWwW7Km2j9hidbU0bRWabTq2qtH2Jas5ts9/M7ia+24tb0e8vqhfwB/Dh2TxsP0e8bQmXDTqam69eiR333wGhZ8pv9hQiknJZODQi7lLvn3zbdfQ5sBcRs/cLGSD61jdLL7WQK3rxnHJ372J6Z9toCYcJoBKTZRmxw/n0V98l9/+7Lv84dd3ckbrNHWA1KKuOG7An/tWbjcYwDmq0/h8LC/VJDvhOj5/Y4zsazCAMQ4W36+2ATDGHNVW28hXF8vHdZI0XNfezRF813GQiD5iz3Ov4H7toxz/zcHiOeq31fht5gueanddB8fvMFTuWsrEuXv0bo7QdkwSXje1CVY4jq22ARXd/fej29R85PrKfnOUDOLFV5SvwDPi4TpJGXy5HXME0RjzxzSPaZMtSBYrr+scRUdwtidWsJNZM5dSonlnjMHCOUfuSH+TbLO4roNfjKP1OMVfsxzH9eesIVmM+hwLa6voJFuP/T0aJglisDiu88fycXQRsIVL1iTHpH1qn42T9MGjcWqfLc9g3X5TcSboJHGQ/El6Do7akq1Gz45Py/a5roMj3paGfXaVfPmWCLbiurtuYUT3uvljMI5gbRVM0BgwOZx/401cd0Jz/BIIynaaiZ7eMtpy8z03cXaHDL2AsXwsbm01QufLRY1Obb8rfWx3TtdT+c7dF9JC+xyoZP6cuaw54CFQfy2rg7d322ZxvlyNqZVbtJMwBgtfV11X/ckOji52Dxvz8wyjXLqaPQUldOh9Oul7FrO+2MMVjgkY0jMcUnWYZ1UyriFV+Uc4IErqjyjnsWt/aopBoNh+m1+kqd3mFDY/sWuhztRpe/yl3HLR8WibRdxxtea6fn4StvlWKIhjGQBh4aYo77H4FldNyUuLfcIJ0WvACXTIzabbiRdyy02Xc/dtN3HXqKGULhjLM5MWUCro9LYncYv2rm0CEJNd0mrzpDrZrV6Wvi97rW6e8ALKi2w+Y/VKlU7pYdi1di7zt5bTQENtD6uDIceX+2hcS8/yqMurnEAqJ198M5cMboO2M6AxCFmdrG6qEfERu+QlxkZPMS+dweeM4uZrR/K1Gy8itGMG7y49jBs08gWDG6/g8w8/YZdyinDQwdPHJy+rA5fc9F1++u1v86OHHuSGc/qRCRRtmaPYeIDjR13GrddcwY2XnEtb+Vg0YTBGALrEVmPmsWHOBzylvbDdQz1t91ALt8kTjfIr2Pix8qWxc9hXDnlLP+Rp7Yn9nEr3p177gM835xNMgd2LP+IZ7a2esXmV8q4nn3qD16auoEB80hLFLJs9i+dfHsfzY2awQDgaRn8NDwTjbF0yj9df0/7t9feYuWw31fKxFC+PaWMnY939bQAAEABJREFU8eaUFZSEINVUs3nJXF57faz26+8w6bP1FCaMzh0hX+PzzMuTmLe9gtRUiFXv513t099fugt7DlNVkZAU/zuX889U1WB88nEFyap921mysQDfvKntOX1wZ3KCMVa8/yo/em4WBxRcyjfM5me/fJEPt1dSaJ9/8xLvzF7AK0+9zCdK3A6s+IRnX32H119/je/8/A0WHw6g+cXhDQt4/oW3+MMLr/H6h5up8bnKZW0Q8J8rWDp9Er95cSLjx3+ioFlDoDaobpo3Q3jCfX40Hyw/5EN/cYjmv+J5idoDoiJmTJikLzhjefzp0Uxbk+8DlO1axRsvv8XTr4zl+QlLKLOt4u3VnipVqf/N197gRfX//vUP2VZsAaB4+3JeeXm06I3jd5oIs9ftZPo70xg7/l0mzl7J+lUf8+OH32CFTtmjRZt5/tHf88Jn9u8LPNbNncEzL7zNcy+8ymuzNtXqDHtWzeE5bUpefGMsv3t5CuvzpftHUxj96Ub/EAyNwAbhPvvaeG1e3uK16WupkjhF62bzo58/zTNvTOK3j/6Gb/5qHCvsn4yqr3zPGt546Q1N/Nd5btIiCtXmHdzI2NFva5K/zrMT5rHPEhFt5aN46kdUl01/l8efn8DYd97nscdf1qaoQoHhfR782Tg2xwSRt4Y//PIJ3lxS4GMUblnMyy+N4ann3uTVmRuosK2yvUxpn5JVL4laDhtnv8NTr03ghWdf47WPt9bql+RuN1QWYd/KuTz1zFuMfmcKH6/Jw1MwdNVRun0pr7zwhsb+dV6cskYb6irmTniZbz86mjdGv8l3H/oVv56wUt8wYfunE/ju7yazVwJF9y3h0Uee5b2NhZTvWswjP3+SJ1+fxBO//R3f+PlbLNpdJeoQL93L+2PG8Pzr2lw++ypvL9xLTf5W3p04mY83lfswW+bN5LmXx/LsC6/z0rQ1VKt11/zJ/OiXT/OcxuHRn/2K7z05mW2VULJpLq8pII4bN4E5Gw9hTAXz3p2gw8W3+P0Lk1lcO1ZeUn1R+uKqkkg2kHva8QaDkHe4kIqKBpwztCelmxexugjCjvUMyR2toKQyzAnDr2EQaxj93hKqhROrKKdap5fVNZ6sbwgr0O7eXyQ5mnLWaZ04uHYhG8sg4nhYHzjCXS9VOnmxX2btX5LokWAohcycFGqnB9XV1di/WEvEo/4Y5u8q5mCkHQPbpvqHMpU1NYKN6dAmQTQByaiiux6iOo2uVmMiEScmn6rIzyevPJWuPdsT0GFeRVkJpvEArhl5Cjs/ekuHeFFCJkFFdY3PNxoXHfAXGLRIGDdK3tYNbNkbp6QE0jr2pV/HZqRVVLBk3gKc7qczqLVLYWGM4uIEVWnNOPfkjmxb/ClyCSJEObh7DWt3x7Eb+uzmPRncI5dK2Q3HJcUrZX1BFU1bnspJratYsWQN5VrgHevnHF00kA44JduYpQPUd2Z+wudLF/DxtEk8+8orzNmSx77Vs3l74gfMXbWCFWuWs2TlUtbsKiFRdoA5099m0odzWb5qGQvmf8Rbbz7PKx9uJGogZmU5mtVRz57nUSWbxmoqqdEn2mhVDfHUZlxz65W0y5/HO6tKFQ9jFFWEOfuaazhZPvLUuKXIjNTIRlXykUrRsHSqZdyY3qv07rOQSvau4ZTtDS3bdKFj6wzKSyEQTidTyU5MThLXCDft0plT+3chI5AgYZGOqtaXY/EGDOjfCSP5DhUlSM9qQChRKrkcuvfsSk6KxjE/jpuaS2agmkJNuRatO9OhZZhDeXGiTjYN0gwVmhwadnHErzZuZLdpy6lD+tA47Im38X0j5mbQpV9fTuzWDFdjhS1yYCfcgD7H96Nvq2xNoC9LaoHAqh2iim3LZzD63el8tmQRc2ZP46VnH+MPU9aJb4ydKz/krTGv8NqUpVRHDBV7VvL+5ImsKtB4HNzABxPfZuHucjSF/UPoJOW6Xyunfa5i1YzRfOcXovPmGH78o0f52esfa/zn8PwTj3Pfj1/iw+Sfr1C4ZSmvPPcGL7w2mt+/MZs8Cbl3yTR++shTPPfWZB772S958PFJbKwQ3dID8sH3mbhgr14SbFq8UGtoEUunjGHS3M1UyEqrZk7myRfH8PtnxzPbBiyirPp0itaMMTyhtW3GukJKNn3KSxM/5M0332PepiIcr4I5701k9JRZPP/sW0xeuh8Zg6TFQObVM+xc+glPPjNGMXwqT/72WcYs2E/JrhX84lfPM2tzJUT38NbTz/H4O8v9dTBxeDNvv/qmeL/BU2Pnsi+ZEOCJmqd4pBuFint1a81rszb4sdfOwdpuC5Kstb4b3beW155/jefGTmP83B1Kel2CGtMts8fw/SenkedDH2ba2xN5SYnn754ew0cbi/zWOp7VVfjqxeOGhu07cOrx3cnRIWEClXhC8yyXLl27kVUd53BpgvQGuYQ0D6ujHNnwWAKuV8bCKeP1EWMiE6d+ws6yGoxxCAaibJw7lRdeH8OzSrA/2iAbu4D1V6N75U7GP/s0v1Hf66Nf4PsP/54Ji/ZgIuDES1k88x1eGjNBCfabvLNon/YhlSyY9Bzfkz99sqlAOVOUDfPe5XevzWD9zl3aYEzmk42H8cQjWHOYj98br1zmDZ4d/YFiHJhDK3jqiSd44rVJvPLsk3z/0Zf54PPlfPz+aH7yyKM88e5i7Lcr1zFUF21l8htv8LPfPskLH6yiOgD56z/isV+/wceL5/DMi28xf3sVbuEGRr/+lmz8Kq98sBRN/VrbeNixC7txti+crhxoLG+8N41lu8olt8EVvYPrP+Pl197ixdde0xhuJ642NL7a81FdFaRD5240yzEcPBTHC+eSpfkf00HagR3y8czWNE6JU1YToVObDPZu2kiJ25ghfZtTpjhTVOnSIDudeFUZ1fK0FG0Ud61eyC5aMbh7MxKVNdi5a6gr1hvBHkDPnTGZSVNnMGfxAuYu+IzP12ylprKQpZ9M5u1pH7FkzUqWr17O4hXL2FQIwZIdvP3y73jizXf4cP5cpr77Br99/CnmHohTpcPl9yeMY7ES6tQMQ6YOuD+dMYkPFm0jHhQ/6bxtwQe8pdgyVmM9f3chnto0BNi4HI/XyffFXSbSSzXz3xnPs6+P58mnXuPdlYe1kBzinaef5IdPvMmLL7/ODx55nnfsHFY8WPLeaH719LvM+OhDfvPEe+whzrZ5M7R2TRDsGN5Q3lkhqhU7lvK7h3/BT95cSFl1IR+++SJPTttARWk+c2dM551Pt6CRZfmUsaL/Ei+9/RY/+uGv+MUrHzN/0Rye+f3j3P/wC4pr5aIGJRvn8/Kro3np5dE8OXYe+ytts8e+5Z/wxDOjeUPr5ceKRW5t/r9+1nj+8NpEnn/mNd6Ys1vACdZ9OFYx9GVefXMsD//41/z05VnMnz+Pl556gvt+9ALTa+d19NBmxr02Rh9/R/P0uM85LOz9K2bys0f+wLNvvcdvfv4rvvOb8awvgwrliq9PnMnoMZOZvvyAIKuY/8FE5W9v8vgLk/h8Z1J+u2768UBa71s0nd++OIEXXnyVF6etU7QRWnIw9FB76b0uvuxaMluwY+T7b/Dc5KUUW5BoPrMnjeOpV97myWffZYV81TYbx/+lVdvOdG+f6n/QTTjZNMp0KSmpwIRaMqR/E2JyiFgiQqPcVGqUA6KyZvLL/PzVuZTq2dNi7BXuZO7ig/QeMojm4agfg9F4R5VnWNlqFO9CkRAB62TC+eLycIIOxTtWMObNl7nv+08xefnBZHdVPh9PGs9zyu+ff34MH6w4rPYalk97m5/+9l0+mz+Nnz09jX1FVayY/hZ/eHUiz+jjzbtLDwkO8tZZPxjLH54bzeiP1iteq1kLiqebfykmJuxD9WFmjH6eB38zhtdffZXvfP93PPnOAhZ8Op3Hfvko33p0AqtLBBgtZd7MKbz16QZ5t95jku+dCVpj3uT3L7/HKsv28Foef/RJfv3SJJ576g/c+8PnmL6+HMr3MlEf6ie+9w4T529g9dJ5/O7XLzF53kKe/u2bfLankFUfjfd1ePaZNxSTD4gBHN64iFdfHcMf9EHpjRlrKLet0sH+YZd9LNy4gBdeHcezz7/Mqx9tVtyBRP5mxik+PvXCaJ4ZJ/+vsZCaQVZx+Yp9K9i0kBdfGi3bvM6b2jdFKWPma8/wk9+PEb83+N5Dv+TpqWu1rltozdslH2k9Gc+Lb4zh8ZensSMGnnKS0W+8zcuvj+OJl99nzeE4xA4x8ZkneeTp8bz64st8+6FHeWXODnlyBXPfn8yr787gtXeXs2vTUp78/Yu8Ne1zJr7+EmMXH2D3yk+17xvL8y+9pv3Yat/XHflWvOIgU8X35794ksfe/BR9Z5NQCVYqZj732jie1t7vrc+2E1MrGlM7f6yqeJUsVq78hxff5MnnJzJf+biSBl578nf89Ll3eOWZZ7n/B48zflnS1sg21lctmcq8TUkbvjiG58bN53BVObNee5pH3l6l7jib573HD38xljVVetVl+WlY9AQ7Fn+sODOGMe9M57ON+Yqtjt9eun0Fr736Fi/KXs+8MQv7fRDt6V988kl++vQEXnxW8vz4GUZ/uIxPp07gxz/6FT9+6RMOinjJjrVM0v51/o4kQ1N1kOnjx/LsG+/wzNMv8Ornu4kW7+CD8VP5dEOBz89YfYSrpZka7XXHjp/Nij0lfl9M8/Xd0WMlywR9+JzEvO2+Z2FzX2sHC1RzcB1vvP62xv1NfjdhMaUy8PaFH/HS1EUUVULRyk8YO24yoye8w7xdVXja705+a4xP8xmds8zdVmbJJGn6T/annEVT3lXMe4vHn5vI5/vkM+W7eO13j/PLF97lpWee44Hv/54Jy/LkM4LXeOrXv+y+EQMmADXllRzalUe7c8+lXWIv81cdJpyqOb92Nr/86a94XmtLqXQv37OaV595kakbSgjU7OP9cVbnNzSmH7NHOlTu/JxHH/uDcrpJmq+/48HHRrN0b0wfjvNZNGem8o71lGj4wtpXLJw2UfnWBCZ88DHbS6owToBA/ABTx43jzbfHKA6MY/7OMj/PsD5IbampriQq56ipLKekOE5BUZS0tr245ZKB5K34WGNSxZ71c5k6cz55windsYCnf/M87y9YzGtaZz/aXkTZ/pXyndG8pLxl9Ifr/VgWDnpsXzqLl9+cyNtj3uCJt+ewfvUSZkydzBS73suvHQ3++nlTeGn0BJ1pjGbs7A3Ewmiv/rHOK15m+oIFvPT0GD5dvZRPP53N/A17SajfjRby+ZQJvKz84DXFkg+W7ccod/D1chy8KIRa9aSvvkSUaz0pI0xuZorGpUrrCERCHus/n0tJo4EMapWi/aSHo7FLxGv8fnswnghEiEQCBDXS65euwLTqQ8dwBRuUrwUbtqFrY0O1fE5oskrycgS7c8lU3npvKp8sWsS8OVN48enf8tzUFZRpnPYumczo6fM4VAWF6z7i7ffe418sf+QAABAASURBVNOFS1m+ahEfTRvD718Zx6o8+e7mT7FnUrMXrWDl2hUsXb6IlVv3aZyqmDn6SX6rs7wZc+fw4fRx/PYPf2Di4jz0PZbVU17k0Wdf5r3Zn/HZJ+/z7FOP88rMjcRC5cyfOon3Pl1LVH649IOXeeyZl3nno3nM1Vnb6y88yZOjZ3FIMpZvX8TkCa/zkuaqnSIRDvDhpIl8snY3iRDUVHpJZf9HfmWSf56mxnj+Yp3baQDDBoV586c/5c4fvch7i3ZSrV2LYwI0aZTOwR37KJbdU9u0IVy6h52FUbI6tCe+czVbynM4cVAXYrsX8tyEFXQ8/VwuGXEckeIC4qEsgrEtShQ/p/m5V3HPradw6OOJTN5QLaUMsQQ4ctqts9/ltYVRzr9sGOefM4B2OQGcQIDovnm8MWsfZ14r3Atb8fm48SwqQsFFcksejirGWsqrIqPjcdxy3Sgu6hpn2pSF2H9G5OP3ZlDS4WzuumE4/Ro5WO4YI85JApXVqfQ/5wJuufFCmu9bxLvL9kFshw6Pp+H0PZcrL72A8/s0hJRcWjZvSOtugxh6Qnc6No5QenAf+0oSBDJbkeMUs2t/uYhGMbntGTnqCm4b3pm1M6ezvBi83bN5YvRKOg+9gCsvvoDTWhiKIym0S69h7Yadvjwly6fzwox9nHDB+Vw56kSqFir4zjlMpjbEocJ9xNsM4dY7rqVX2WomztkknDLeGT2Fml7DufuWiwloYr+zdCfzps5iT86J3HXrxZzYKYMaTXowEiJhf9n9ySReWlDBeVcO46Jze4E+QGwtTaFh8zSK9u7hQBmEG7YiXRvXnfbPFqt3aPP7OY1Pv1R8zqD48/d5b6U/GFoTPcnBkWJ8Dh5OamuGX38pt57XWgnJLFaWWhCjRS+B4xhqdi/WxP+cVqcNY+S5pzKwXQ5GfuclChg75mNST7hUPjOM6sWTGL3KoX/rMPsOVNBv+Ei+e2U/dsz4gE9k11ZNsyXzDg5WQ6BJS7IqD7H5UDWpLdqSVrqfyib9uPHWGxiS2Mj4WWvwlHxPe+V1FtONqy45l5Hn9yZwuIJgTgtCRXvZtLdSgiaoSm3FJVeO4o6LerF5znSWKDi26NqWyv15ZPYcyh1fu5xM6TDxs72kt29LbkYuA04/m+M65LJRwXHy7gbcestVXNq6kDfe/IgyUTXirtsxl842ku8G3GhUCUQ+aS260bXfCfTILmXFioMEI5opCbALq0lUEUvpyJVXnEXRovG8v7KKlNSwbOdhjDgYh4AW1PyaCnJatqfPoJPoEDjAynWl/hdhf6GirhhwAzjRMnbv3MHWrVuYrGR0fWAQV5zeDjdu4RwcB0oO7GTzju3M/WweOwprCEcMeAZHPMEIxtEvxxbjEHA8Kq1dt+xgzaKPmbenhpSwi6TFCThUVVTT4vgLGN4zzjtj32VPzCEiHE90dWGLUaSqiLl0G3Q6XdwV/PLHD/HLV95l2a4SQrJNvKqUA3nVNGmcLnBRtnxdazOPcOOmBEuLOXQwQf+zLqBt+RJ+9sMf8OvXp7BGSZ+T4mKkpwlBxb5DxFPSaNG1AScf14uKnavYXACpQanqifSXLhMIEU7LoffQ23j5lcd58f6LCFfsZfX6XZisHBo27sqV93ybH93/XX70nR/wwKU9dEDqkpqWTrez7+SF537F2794gEFNE6xbvspPaBWWfS5fwQ5jjCqqDq4+HAaDLnbD42U3olvrANs2H1JfAFc+Ek3tyM23nEX+7PFMWl8lnhHfR+x4GWMEZ+mY2vGTfiRLIgE22XU1Nq4OvnZu3sQsJUCJ9qdxcuccEso8YgrepeVV2EOlJNaXf43oio4JYL/879x9iOwOvWkSUJtn0MCTGnE5tG8n8UZdaZftQtyTTC7BkEt18Q4Om6Z0btMYx26UDX4xxuAJzv5bllHPb0r+qD9alaCsKiq/SjaB0f88qvXVurwmzp8rnnGJKBZnthjEvT/6Da/99kdc0BHmvz+DlcUxUjNzaZLlsObjt5m0qJJIZg5ZmVmkSh8TTCE7J1d6Oljb1c1n7TFqWRqMPsCgRLBLi2yK9uXR8sQLue/GUyibP505RU256qZrOTVnJ+9MXa3oJD90Mzn5vOHcev35BLZ9wvuLSmnauznlu/aR0u0sbv/aFTTMW8zbH26H9EY0SRSybtt+jPy+fZ8etM/JoNtJQzlvUDvF6km8thyuvuVKbuzr8N7Ej9iyZSmT5h7gjKuv5JaR/Uir8khv24TcnMaceOYZDGyfzup3RjN5azojLzybq4a1Y+nYN/lgR0w6GeJS1HEk59Z5PPn2MroMvZCR555Ic+8wG3aXkt6ijWLpTnYW1ECgGR3Tq9iyswiXGNNHT2JPs7O4W7GxxcG5vDxtq2hCQjQ9R+NbtIEXxy6g2ZmXcfdNp5H/2ft8sKGKUHoIL+H5sMkfD0/+QGw/Lz//LsXtTufqi07nTNkplIiKU5j2uQH27c6jQghetIqsLidws/KD89pV8MEHCykCjGINKnXjpgYl8R5l5RUc88fsxoCUdiRjSJN0747DNOjQmaYZ6GOQCOARDMLmT8bx3sYg5148jKEn9qdRigNBQ8maGYxbXMrQa67kusHZfPrB+2yXYEHFKalOakYzWqSWsbcym2GjruTy/kHmzfwQhUv2zh/H5I0Bho4YxsVndmfLrJeZss3l+D7tKT6wncpQBuFQkPR4JanNO9OzawvC1YfYuruElIwEKz6cxOKyNtx4+7WcmLOPd96fQ03jHrTwDpDvtOTSa6/j1IYHmfHJSpqeOJKbhnVlz4JpLN0DKbKPSW3KKRdczF2Xn0rZkjG8PrdQc7MJlXnr2VqRSa+e3WiQ2CHfmka0y0V88+aLCW+ZyftaNAOyidUvFEGHNh/x5oc76H3mRVx65ol0aRwhQZB44WYm6uNOm7Ou4muXDWbvnPF8siNBiuyWHHKD/Z+1fzjiUnRwOxUZHejYKEz5wUISoQiRgEvcDRFMCxEtKUXfTXED4DkOYVPIdh36tunalWyNY+nelXy2IcqgE/qRGq2kRpP1WN/CL8YNasOWQZcho3ji1d/x9ovP8cJPrqFregwnJYuWXc/gvm99h+9/8zv86Ns/5NZTs1m7fC6zF+2lzyUP8tKzT/Dr2y8gtXAVMxZsx01NJycnG6OPkDt2HGBbXrHkzSHH+rYHbs1efbAspEWHwQxQ3rBo+XaqYvix1PqnQHy5vvipbfFqCDXryS3XXcaofmGmT/6YikAuzbOrKahuwsgrLuHOoblMfe0tFuijWdee6WxetIGKxl04qWcj9iyeyvOz9nPcRcO48rIhlC2czEsf7yHcshfn9M1i26otbM8rpMhtydkndSAlPYeWkQrWbtpDJQ492qSzf28BrU4awTdvOpnChdP59FBrrr3pWk7I2MP4KUs0O6AqmsXxIy7h5hvOVK76KTM3F5MoXMaTo5fS4ayRXDzsFAa2zSBulRVGKKM9l1x/Cbed2YRFH0xjjefoA0Mupfv20/i48/jGzWdSs3QmHx/M5orrr+PsJvt4d9pqEvFq3n/7fQ62Oo27b72SFvs+46UPdtG4Rxuqdu/F7Xgqt91zFS0LlvLmrF2ktGhOwwY59D3hNE7r0ZidH47nnS2Z3HLLNVzRLcq4sTPIi4JcB4OKF8VkNGXoqEu59bJubJ01iwWH1C6AhG7HXMbRq0dFuBkXXHoZt192HPsWzGTJLo+qVR/ynvjceOMVXDW4qfSOCfaLy1hu8l/juLLTDvYnGtO7Q2OQv+IaXS4Bc4DNBw1de3fCFmu6uNabuF4cU87c2fNwug5iUPMA5ZUx2SaBF84mpXgDEye8z5uj3+L3r33IlsIkb0taqLoMEkh5TFuGj7yCUT2r+OC9z5H7sPL9MUzZ34jLLx3GFac157O3XuXjPJce7SNsXrOe4pz2DOzSBE85Qkbz3lxzwyXc0BumfDCfKm8fk8YvJPuUkdxz9Zl0b+BR7gHWTLr5l2xmpAOhBnTL9diXV8mQkZdx70VtWTl1KtuzB3H7HRfTumAREz/eDoF0WqZUsW7jHlz5zar33+HjghbcrfE7r9lBfVj4mMqczrTiIAeDbbj6+hu4uEUx77wzmwrFtnYNs2jdewjD+nega89MCrdsYltVQ47r256I1o1I4+5cJR1uGRhi5gfzKPPydFDxOZEhF3HPNUPp3Ug62IFXrPQ8A+xj3Oi5ZJ94OXdcdwYdGoRxqst57+0POND8FO6+9Wpa7p/HK9M3C9Yop4mTkO9QtlaHxgtocd7V3HPjCez56F0+2ufSsW2QPXtinHzZxXxjZBcd3k5nbQHEN8/iDxM30++C87ly5Dkc1yqVipKDvPXyJA61GMIVl1zAWU0O8uwz71OmeNAmt5rdh8Kcd+UV3Kk59dHkmeysSaFbpxxyW/Rg+Dk9aNG2HRmlW1mwJ07PQT1pYmqIZbbkwktHcdvlfdj+8XTmHZTYbkIfzTIZMmw499w9gszts3l8/GqtmXHiWR1l48u56dTmLJg5C/+PujSm1jbWOrs+m8y7ayNcq/G5tlcNY16ZTEFGC1o7peyrbsDl117DVX1CvD9xDnnWN4Tk24dyPps8g92Njufumy/jtFZQHk6ldUolO3cf0Jrv0q5NNiV79nK4RjLq8rT4aApRvvlTnhi/ku7nXsjwc0+mb6tM9bok4nm8+sp06HYmV8pefYMbefqlWZRntqNbqIA9FRlccs01XN0zwbTJc3H7D+MbN51E5fKZTF0RJb1lC7z9O9mo9QWizHz9dT4tb8eoi8/lWu0dw9VxAhlNyIgeYO2OIvFEs9pDk5iY9Aq2aA6HtrPxsJ1/1Ux+YxxrQz0kyzDO75ZgnD5OrSvzcORbdm5bvA8nvse+Jqdzx80jGdg8HalI6+Y5HNi8nkNRyGzdVvGsIScMHcbAFgkmvz6WVW53n+aFPQzjXx7LGp0CJ2kmJAvs+fRd3t4Y5gblZdd2q+bNl6ZpD9ycJqFi9lU0ZNS1V3N1P5f33v2Eg9a28lerhUXWdzB7wzUeZZV5lAXb069dLif3a8n+NYvJE3zLrj1oFM9nX1GcBukeoXSXOC0Y1DPCZ/oouzV1EHffeS1dalYwXh+l0pp3Ia1iH9UN+3HTzTfQ19vEtM+WUZOTQ+vMCjZt3k0iBNs+e5t318LQkcM49+SBNE0N+uExHquhQafTuPa6Kzku9zAzP1nux5mAQRECvxjjoFeMHMR1XQKqNRUJ0tt3pEG8XHEHOrVOY++2LRwqh6btGxE9tJXNhan07tWDnOg6xk/6lGzlB9+49kxKF09i1o44lZtm8fqsrXQ/dRiXX3wOnRpGiKQ1o0luDl0GnMHQPo04tHwSby8oYNAFw7j8nOMoXjyeCfMKadurGaU7N7CzuhF9u7Yht2EzsmoOsHF3IdqisE57kJm7Mxl28TCGn9iOVVPfYPb2KOkhg7ZGvl6eclQUh9wUF1N4gF2RLKc6AAAQAElEQVRlaXTr3ICUFI9Dq2ezpLQJp/Zvi1dVQ8KLETUh0oMxNi+ewvjJE3lLuf6slQfxdABwIL+YyuJdfDZ1GuMnvMZzb81ka2mCsPzR46giG0bCYbLaDeLe7/xGe6hfcHFXmD3jIzZpb56qPVJuZjpBB4KhMGnNe3HLt3/N6Bd+yyPXHE/Jto06eynUXMkivUl7hl/zDR759rf53oM/5js3DSNn2ydMWrCFRsddq4/sT/LCj2+hffUOFixZy2GNyYxZC6hpcTI//M2TvPzoDzi7aTELFy1hR0GI3AZZZDZogLdrDe/Pnk9N63P5heBe/f1j3HJcA1bMm8m8NTFSMrNp2DCH/NUzeGvqGsrdTBo0yCYjIkeTstr28r9UnH+qspp8jierBhsy/M77+fV9IxicW8wbj/2MB19bQrWYR1JSSIsEkpM0GCY9LULINTjBIKk5zejUsROdB59I37SD5FU1oE1rweswtl0Dj4MFpbB3E+u1EhTuWsa8BXsory5i685CbIkbo1sZq5ZsJbN3b7pmpZAajhBWhHC9OPkb1rOjNMqO1SuYuy6f0rJ8du2LCcfRpJHcekpeopPQk2lKr0aVfL5oJev3lmhTWKPF0KNhSowls6bx6dpCBpzan1yBWmzHSZo3p1NrQod3y5FXsV9BOa7Ep2zzcjbHWnJK/1zSUlNo3/9UTmuXTkyWCEcipIWD/iRKTwvjip5xgqSlpWoTZAUJ0a1lOvs2SOdVB2THKIkY7FqyguLczpzUKlWw6fQ77XT6pxmcSDqZqRGCwJrVa4g370nn3FTZug2Du2WwYcVGHBMhMyuT3NxsMjIa06t7Y6iopKp6B2u3lVKev43PF26iqKqC3TvyieQGWfvRB8xcVkTXPr1pm40flI3kR1osWrqVnG4D6JKRSmpKKhnSI6ig5YZSxDeMtYxxQ6RKp/RUh8q9u9iWV0KBvvbOX7qFMgWlnXn52OJYH7IPR6q1rqFd1+bsW7CcRVvyqS6PK2mztpEcPnXYvn45hamdpGO2ZAiSEgpgh6SiaAvrd5ZrY72RzxdupbiynL17C3AyMsjOyqJhehrZPTrSIddQLBcLRFJJTw36mlmZ09MjhKSLfU5Lz6SRb7Mc+vRshlNVRVVsB4u31DBgSF/S01Jo0qo/w05rixHzrKwM0oJWfoceXRqya+0KFq7ZS3nUIRYDN5xChmRo3DCdjMzWOqjJobykHCcgngGHUEqG/DeucdwunDKWLlrOxsMVlO7azR5LFo+kFY4Y68iDcSBaXcY2HUSu2T6fKTNWUBUtYdP6pdp4QMhY2wncGMFVk9XjHK45PpUZ4yawqSwgvsYfY7vpriwrYvvqlSzfMJ+pn6wnqg8Ja9evwo5YUBL4ooiUxAExdmpK2bNzEyvlf9tLI7RsYNi2aS/VAjTiKxBKDmxj7fr1bNaBTqUyITvs6ubPFgE40quyaB8bNmxiw869lNYk0ASuRTO4Rgt21OHsiy+nTeGnvKENYUI+6disimSxY+PpIDLSoi+3fO073DH8BNIOLubxR3/K+OVyAtclZldfMTMae7EVon4lvJFBbBJq/831cPP+3Hv/t7lx2GCCB+bw618+xger8nEjsq8D+w7u1XxbzcLP5zN3RznlBzayYXsRWm3Bk9yieszlGyFO8f5NfP75Cj5btYOahOPPb1fyR8u38cpPv8e9D36L+x76NXP2oyQHqe9RuG0RE975mLEz5rFfh5wNW7YiS8TjqvaypO39/1ulo4yIMQFtZJLQ1gY1Si5oN5S7z0hh/EsTlXAGiAQdZBX+v0VAniBNVSEb5Tdb8w2Ns9OI6fA5bhwMDo7szB8VIda2xTUekQyHonXzWFbakovO7k1QJkwIJBBx4NA2xeRyTj73dJqnePqi7mETpxSvjLmfraT1yRfRvzlUaNyNkRPW0sVydxz9ckwxRjIdA5fsNoL9SlGT3Ud+PcX9mITzogmCjVvQvXt3sqp3sv1wHBOrJp7ZjObBAmZNHKvNWSWhgIeFt34Rj8c0pkdI+Q9/PH6GkNaOzOxGNG2aTlbbrvRo3YDMtGzS03Pp06MDwWgJFcJu0a458aLtLNAhUVHUJVoZxcbmjIwschumkaHNUd92OdSUCdq4pKank6GDOaESCIUJug7BlHRSwgE2rNlCcU0F6xetYNWBUj8WHXBSCRVsYdykBeyjLSf1b4CjhDWkGBYWrXCghIWr9tCk9yBsvE1v3Y+eDctYsmwXtiQSyfHYsmYVFQ16MLBDOqkpIemR6schR2tHunIFuRsYV/MhRbKE5VH7WLG5kOrSvf46nV9RxX7FBFSMbO/oXrJ7J9sPlpC/bQ3zl2+lrKCYnYcKiDkBbL9AkldCzqSnxI6VrCjK5PhTWpOudTS5FvjeSzjVrg1hH88EW9Ijt4x5i1eyaX+p8oOYPV8RhaQuevjiMgZHfvPlHvvxI5jicHjT5ywvb8uIs/qSoiBp5wQY2bCc5et30rT7ALo1SCGkOGalDmpN2bp5K0Uah53LV7BufxFV+ni9uzyGkVKaEroHiERSydYGK6tBOq27dqWBiVJZFWfduh3ktO9Fh4xUMhTDejWOs3bFJtL7noLdhC9dsoVAsIJV+zI4vldrgoGA1u50IuEwobJytm3bq41hAUuXrWB3QRmHDuUpFwtqvLJo0LAJjZtn0a1bJxqmpJHdMJXWnXrTWh+FdAaPo8ljnIjopZHTpjendMth+8b1VEbSyLAHkR26c8rJ/eiVls+qnSVUF29ktnKEcq0pO/ce1pgDxhDShNi1ejU1zbrRo3Mq4WCYsHI9Yxwqd6zTIUWMoj3Lmbt6P5XVRezaWyw/Bk942KK7CTgEyw7w6eI99D37HDpmepTrw5NxHMQCW+y4JTSXLeNY3JCWbtgydxYHGp3IBcc1IVZyWB9j8+k97Cy6K89y3AApaVlkpRrx4pji83Y85SLrmT7pY97Wgc1HKwtwXAO6Srd9zI8e/B73PfgADzw6lv3y56L8fZQ47ThuYCvcGDTueRZf1wfIqwY3Ur4WxVXjgrd+zu33fJ0bf/kaG/JjBIhjE8DyzctYcbCUDgNP4YQOudoQLsb+NZjjmCOyyQx8UQy+3iaD7jr8W7RoFWv3lFFtfRKHiPK3LM3nSFoazQedQL+0MhZuK8RJS6dhs2Z07d6K484YjLtmI9VNO/v2SMtoy5DuKWxavoWyQIjuw67kopZ7+cOLc+h6wTl0zQiKp9Y40c1ICfqihCIpZGXn0rJZquJaT3q2ziYzM4v0jFwdMLQlHKuiRpCNu7fB27GBhcs2UlBmiCdq2L9yJYeyOnF8p1TSUiLaA7h4igVgaNu5CTvnr2DR9kJqKmKUIzNpj5Ce1YhmTbPIbNWVXm0b1cbQbHr36ESKV05h0X42bDtIRcEeFixezuHiSvbt3o8TTCcnI1MxNEsxtBV9OzQkKic3bpig4xBMzSAS8lizcSdlFWUsVqzYoFiRf+AA+f5fPxmsX2FCNGvbivi6ZSxYc8jXo7KaZLGTOfnk/xrlW0iXrp0ac1AxbeHyHZTpI3tNTYygcvqSzYsZ/clqor2GMKBxGMtAXHS3lyVmcEwZH85YQduhwxnc1ODV5iLG8dg4/UPKOp/NyAFZavfoM/ImfnDTqSjll22XU9T0dC7snqoPSQ7BlHQauQ4mtT1X3nANd910KbfePIrOxZ/z29HzsSp8wRsSivNpGRn+3O/SvTPZXiVl1RUsW7OfNj17kWP9q9NAumUUsnx1PiHpk9uwMR27duGcM/vTokU2zZuls15juGJ/JbGKaspIIzdQzKxJU5h/KEK/ft1pKKae58hKHFuMQyQlleyGTWmalUaLfj3o2DCDTPlaRnYHenVqqjWwBDQJUuXj6empkChnkw6ii8oLmbdkBZvzyjiQl0dpLCifTCc3x6636fTu25bUGuV4xtV+zsUJRkgJBwkGg2TosKRDxw4MOOMEBrVrRptmGWyUDkv3VhArr6aUFBqFypj97hTm7Xfo078XjR3kGo7GCpUU0iJlfPLOuyzYnaODuNZa2/ewclMelcqFFyxeyqHiKg7s2keVoF3td4VO2eZtbNZ+omjTcuat2ENF6UG27a/RuGWQrQOUbOnYuGNP2mbFqaqEzUtWUaOD48FNUklLyeb4s06lbf4mlhxK0cfJllp3U+k+qCeRA1tYpgmYrrHMaZBDlui069yd5pFqKqqMbBwi4AZFI4QbDBBJz6ZDh/Z06XYcpw9sTbsmmRRuX8bny/ZS7cSpqQKrqOMEyMxIlV1bMeLE1hxct4ZDJkj/LhmsWbiS5VsPaw9jiCoXFAYY/LJx7RYO15SxbvEKlmh+lR/azW5csjPTaJDbQDE7nX492tPAVFGs0GiRPFkXAuSkw+rZs5ixaj9tjz+eNhp7CUx62LVgBLRHS08LC9J/JSG/sk+b1qykKrc7A7SPT1XcikjPYMBQuncNm8py6NOriW+v3oO7E921ju3lARrkZNGgcRPxTKfPwK60kH/laH3MbtOTrs3TKNPa6gTCWL8LR1IgupX566vodWJfsrW3TG85iOHHtcE4DqkZ6aSEHCvKMdVoHcyQDVMjAYjtYsX2Snr27urL0qbXQBpV7WS1/1/GCdez8SAheQKsm/0es5YVcPwJ3cgKgQmkkJWegmPACYUIBJI8w4ndLNtaJprdfJotRbNpdBerthQjyNo9Q4I1q7ZTpvmwUnvWdQfLKdu9mz0mQEZaBjm52aSlp9OtS2dynEqqdchN3WBSVzy1GIo3LtQatJ2Z0+ezubiGwztXs2IfpKQ25owzelG0aTFbqwyVm9fg9j+JTpX7Wbczn6qKvXyuWH2orIx9Bw5qPxcUz0yys7LJbZxDt87NMZVlVMUdPx6lRCIEjMeKdTtp3HUg3Rukap6oug42lw+ktaZDZj4LF69iT0G5fDCKTQskJH+uGI2V5wYQFeKyd1A5WbpyWetdjtbD1IxsWrTqxHFnDaS3W8zWfeWUHlzFJ8u1ZlSXcyhvLxs0DxKN+9OzVQrGbcxJQwbQookc1wNLLyPksnntRgLNu9Pdzt3cjgzUQfeG9ZsoCWeQndWAVm06cMKZJ9CneVNSldNHlMsRr2b1+j007tSLtvLF7NYD6ZpTxtp127F/HW0SiaRqCY+4MWQGa/j8syWk6KzgzK7pVOhDydI9KZxzQR9a56bgSk8bE4JBh/bHjeTuW6/g6quv4rKBOcya8CaL91fgmARpTTpx7sWX8I1briB918dMmLMFk2aQgZL8an+t3eM6G9ClKdmQPv27EM4/yL7SCmIyfDweRybQiijcynyWzJnFxHc+4eNlOwg1aEjjrAhGsSJQvpcJL/yYux54gLu+/zMmr6qmsiCPUoJ07tmP5q7BazSIu+77Frdc2Bezdzu7YyFadepP5waG6mBrLrn9O9w/6gyahiqo0smxq4lh1/EifYRt3/94FKKodtM5YUgv0hSL9ucdsSmOGQAAEABJREFUoEJiGTdEsxY5/n+t+r72O0ZrULzOriSL3CL58F/+6/zT9ZOTJnmE6Tz4VG6+95s8fu9ZHJz/Cau1yAQVoK2xPQ1Awq+eArqHTYCsM8X0lckTQEqDNjRwCtl7QO5VsZudRQFat8qE4jLigYgWvDChUCbnXHcjVw3O0Urtofgo1lFKlVWmpAfxeUTjclA5l3hV6wDFUVBPDwUJZbblujuu4bS2SZO4tXJb3lYuN+JRtvFTntBX5yrBp2qCu8T1td1wwhXXc0mXOBOff4oHn57JgTiIAzGJCpXMHzeGMUvyCYbSCVnycsJoSRUmFCEsaeLRGImEiy02gHlKFmKa4Ja3VJcuCfUn8J3UDRIv3c1LL77D2vIgqWlBFCOxP0XlUYIKGkFPsNIz5oa0KYNYTDpLFkurWpvNYDiUtIV4hKV/XIsCgomLedyegkom5a6iaUgoC4maABmpoiXhT7j4Sq45uQcDh17CjSemMXP0s9z/2/fZXiJw4cV1yABaXMQnNSWllo+V31Mvvi5WDnX4OlnbGslbXS17mBSyUsQnmM2ZN1zLVYOaC0GXJrZMhm8PhRbPCUL1Vp59eiJrohq7sGwnGAEI+IurpqxK9sjUpj0h28Wxwd72xqRTTIExw+cV5rTLr+faIblUlFVLxoQ/bp4MEFVy7KgFyefLLJ+x8iZkt4QaPLXbMbH2lWI64AKjxSVRVU6NiRCJQEIB0fpxWAmJpROTfeOORl0+/OLzE1hZIvnTwiTF9wQiOYUTs87jRbWoxUXT1x4kS9w/iKrWBholc5mEQ0EinU/hG3cPo7URiHFwdDv6srQtrtWlomgTe92u9G2ciqPDgJ6D+xDftZZ1u6LY/6zHBnephpEvVEQdjrvgWvp4y3h98gKq5E+WhSOAooLtFIa60zs3DScjR8lTdyUGq9iaB6GgwROML4N1zngNsbQWnHzWUK66dAR333Ud57cp550332SV/EZrEzILrQYM5fLhw7jl6ovp3jDNiuzrUkfLn792DLwE1u4JrUJG9KNRQ8MOQ7j04qFcqbE8u1OaxtBg9ba4cn28qH4b9eaakcezbcZoPt5cRFgJWZ2YqBgj7WLgpjXh+LMv4v4HH+Tm47P5/JPPiTppNMxxKFLCIxEkW9KnhUa8pAAvNZUMxZi4+Ji0ZpwybATffuB73Ng/xLzPFnBYgxLQhvNw/n7S2/WhkUmjQYtuDG5nWL58A0XCC4iYpNTvF5en+RRyPfLWzuI3v3qcJ6evo0XvMxg6uJ02W1Uawxacf+Md3Hfn3Xz9tmvp2xDimsuO4kSZDlsmT5zAy+/OJC9rMNddOIjUuGhbPXWz9tHtmMu3lxXC80hIUeu/Mjemqkq6R8lpli3dPfWB0Vcvz3MZMuoaBieW8oe3Fvo+4mBBPPmA/5C869Go2stnL+aOdCO7DecMv5T7b7+MjF0zGT1zI9GAgyNft3NMlIRfS0vIjmtwxUDiEUpxqFAiP3HuAU4YPpy+DRzsPxETCAmmfD+Tpy6g4YkXc05XHaTazb0QUwMVzJk6nYPNTufy09rhVIi25mzABYmEX0Tc2sE+27utxz4Lx8LYRlXbr1eJ6mFltuZT85+8HPGTGtIroergOC4o8Uvk9uHai88kZd8CJn+8gEIvjSAJ0awlJcLWdnVyikxtxxc3O17x2vjheTXY/0OcRCKOlbFaMS3hBIjEq/h49GjeW1NKKJyCK1trwRKMYo9gY37s8agRHWOM2j0szXjC07NVM+HLZOefpVtRnSAlolgUDhJoOYA7b79Am7Be3HrbcHL2fMovHv4Nr35+ALTx9uRTMc3bhFeleGmIhBx8P/NCpChGVit2Yov4ihPVlTUE09Kxa0RC/lYng+2zNrf8LX5cdCWerFWpNdkhQwl9KBik+9DL+NrFfSxFMA62VNndrUkly65pwRzO1lpzRZ9cojb+G1G2hC2gql514FlJzE0hNez5cSce10Coz+ctG8XsfFNTwepZPDl2AXHxjQRdjJDVLMjkVTdu9s3i2vrFs57ENxAxVG5fwfsLDnLq8BH0bAAVim9HcL1SP/anpEaI6kOb5W15aJTUHtN6kElqKEhMh/ajrr+CXhkuEs+XRZppHBOqcWy8jdbEsZsIRxO8LIp8QXjRBFHpF1ZuEq0po1S7jhMGd6Bo2zIWL9lATaOmNE53SdicRePoWbFjUWqEF0lNJZQI0rDLGdx4+WnkBjQWarfjFlN8q4lGiYtXXP4VralRjEZyWQoGo5GLKYm3B5uBUApGNKs1oJ78MV5TTbXwq6uqiSkXSdfYmniA3mdfw1UntyUu2X1bi3ZZWYyAPug78odq5UCWOipR4XpumPSUELgNOPPSGzi3ezpKmTTvjSA8jCZlOF7A9A9m4/S+kBEDmhCv8kjNTMOVwWriCYzkiSpvdFNSUQpBaorLrqWf8uGeLEZefAYt9aFr36pPeO+T2Ux941l+8LPfM3f7IZbNfIVXZ+8mmGI0hzzxS14aclz5SumB9XwwfiKvT5zIrBX7MYEAcfGMNB3ArXfcztfvuId7rjmbJoptiYTx7ZVQ/LQu7brIBjVoSAioJxYz9Bx2Gz/+3oM8cvNw2maiueZgVV+5ciPlNWn0Oe0EenXoTFbRSuZvrsCuwcZAXaW2eLKpfazcPp/fvfwxRfLtFM1ZC5eQGl4iQUK1RjWBQ3rEoaYy6vtXXLlKjT7ueYpDFZUJ3JQgAtZlNEZBPOV81dYAgRx6dm/Ioc37KNbBAYpHntrjmlt2Xttnzz6rxuQ7nv2LKjFPaCxsX5WdBwT1vzJmvPoW722sIhRKQcuISHmUaM0OpOlduvg5rmTFkSyVW3nqmclsEmZIOaQxMoD4eqJreSd51VCjCeTzklzV8mEcI3tXEo2HyBLdkGzSbdhl3DOiu8a2gpjgEsLRCzWS2RgjE3p+TciHErJHlWJxOC1VYxIkrf0gvnH7CNraDbdwffDKnbz+7BgWF4YIpbq2VXPH0uBIsbrbmnBCUJPHmy++zYKDDqH0CHJlxYcobsezeOCafhz86F1+8MNn+HhHtfCNFU13z6drTCUL3p/OnuancMM57XFlW8+4OI7DlgUfMiu/FbdedSI5so3vID6mfmp2MXHcDKbOmMhPfvU0Pxy3lD0bF/DLl2axvUb9skR1dTWxeIQBOiio3ruFg1YFg/jafssf+UPcf6/RfE2Ipxevoky5Z0RjktBYJRJG60SYyopKyS0c2TSqGGXFqdy5kCdenMVBxbyInNilhlKTxfBbruKMzDxe/91vefjtpRSJr29Xy/ZLNS56cflqTAS9asUlO0Z6t7a1Y2803uoiLjiZRrLGsD6XmpEquYI06nka37jpTBoEqqlUnEr6Cn7MStQyTYinJ79KiICXSGD90MZvy6N6z1KeeGE6+6RDKGhwTZQSk8m5N17FOQ0LeeuJ3/Gj0QvJt0TEXaroN4fLbruGM3IP8PoTv+bh8SsprYrJPkEyFP+sT3Y971K+NrKPvBs8x8GWcsWumBsmMz1EKJjLsOtv4KKu6djYan1T0wsvWqORQ/smjwJ9lLF+atffqAJMQpydaDXVyi8jsktCunj22Y1RXh7Xfsfz7WTpJETHri9CQVMPvAQxC69q53VMc8nqHy/eyRsvjWdJfpCwcmiDLVZLgxFyXOuC5RMMh3FCASoPbeHpFz5glzRLiQRxBGPHx2Ilq0eZYlA4NY2w4NNbHac9x8V0UGe5BPM0DhY+Kv7++FhW4uSoetqRDxhxJTcMjDDr9Rf49u/eY0cMQiZOXEh27BKSP6FxTOhdJBEaEKe6vEZzLxOrbEI8LDyCiZZX4QVC2MPMhHCNm6bYFFWc9NTtEY/F/HtVVRRrl7iVUTEiKh7WfTwZz/c9MfKUh0fjLlpqNW8SxBTrw/qwiYWRnTzheOJpqwRBjwgQi29fPOU+VVqjQ/46kiBhAkS0gJTJ71GxbuLhcvyl13LdgLC/hn3zMdlaNrAHhf74WaK11ca5hOJ4VSIgWxuxEk0CSZryNVSMMfqtUX7ikao8MRIKEmg7hK/fexEtiVKteeXZaqHqxkTP/uWPDThGD/ZK5LNudwq9ujYkGEijUdchdE3PZ+XKrfLJBE27DaFVfC9L9b7mUAZ922RQI/micUOq4q0bD9J60HBuvKAvYX3ssnHE5swxzVuFZJCsRrpZe+mGl6imQnuHlNQwsRpr77ivo6vYtG/pFN6YvoKEfDCoxcbID/lSseMgsbF3O/aJRBwt25jiAsqDATKzLN0o1pcsjCf/SAgmrnMvu3ZWVVQRd0Kkp4YwpGmfcz2X9MmipKSGsHJANO7V1THlEA6OnY9iZm0ZU3wsU3sgEgbJHZV9I5IzEa1Srgae1tCY/KW62sP2xcUzyb+KcuWb4XAgqW8CIsFw0oYgGYSLquyUGkqw5pPprEl0ZdQF/clW3Fq/6CNmfDKLN557hu8/+gIr9+fx0dgXmLSqgoCrmFgRpaQsToM2/WmbUsCOA5WkZ2bg6H9ozazwGtCrVQPydcheapAnJpBKHFuMdFWfZPDk71YoxwHrItgiBE/7nEC0iJXzp/L66JeYuLpCe/oLOalzCjVaR6KRhpx+0c08cM893H/r9ZzWJUCFclJjNIqehxE9VzQ87cuqq+PIYMgUeBofy8/q4mg/UlUTVZtRv2UMxuhZl4UzPg0UE60ORn0uRslqNJTDaeddzuAGB3l3wnS2V6P8S3xBMPjFkvEf/st/nH+mfhpHkffYun4F85cd0rO9DC26dqB1TkAjg5zGI6bglhZxtLiEcDTodlExJqSAFiJFE8gYQ0qj9pzcJ5X570xmzLTNHHflzYxsHySRkoH2bLTq3YNB/ftwXJ8utM4JizC1JUJulschfUUy8lInXYmoAm4kLUJ6gzQ5eIQ+fXoweFA/BvVoR8NUx8cTy+Rdi14wECYr27Bn5TIOpfXkjL7dGdK1sZLHkPA1qUwDLrj2Rp741VWkbZrHZ1s9bEkY/VYfwv5FQusTTmdA7150yo1gtGBnNs/FK9hPnhJuVwlsSImHBbcLUCCUSsgxGLl8TMlQUMHHcYIEcEjPSqEibz2rDqRx3mm96D+4LVl24y/8Vs0yKNy7j8PGUUByCdhZYiCcEiYUDGD03EBfpSoKi3GsLcSj8HABkazGIDuHgiHdghLaEBbNUDBISmYDIo6+iHbszsB+fTm+f3c6NDYUxrI55eJreexXt9E6bxkfrtf4GkdJi52m6TRKN/pSdxjj84lgE2IR1qUgruAYShOsE1ZQcnDDqWRlpoOBNr27M6Bfb4YM6EKrnBT8YtShB2PChINBIumZsGcNyw6lceHJPek7oB05kQChsCuoL650fcquOLSfasnlOqkKpC7BcIS0rGylLtCkm3Tq35cTBnSnTYOgZAxI/5CSDTDi49sgbECCxbSAhTMdHCdCwHGJyKbGhAiHgsIJgmAsfFA2j6Q3JjNRyM59lTiuS/Ij740AABAASURBVFBVZhBI0IdPy87E5G1lyT6HYWf0oN+QzuRqk2GTAhMKEdYHgnDIJOHDepcsKAxbGdLSQjgmnQbpDvG0xgzo25MTBvWkT5fmpCailJWWUaV4yVFFJPG06oVTDflbDtD25LM4fXAfjlc9+6zh9E/LZ7XanQxDSjBERCfnKakRbbo9YpntGaVDsZKlc1hbEkduREi+lr/jMJ3PPJ3TBvdmiOp5Z19AF3axelcxIcnmel5SAsf49EJyAK2VKK9Hax7Nm+mra6yM0ijSN0IkFMCRjsqPyGyZRevcVNZ/9hkHwpAuGwQkV6ropqvmZjmUHVzFovXFZMjPQj4ufhzxMnPo38Zh87qVbDkI2RkRQiHR1zhGqxK0Pf4iLuxUzZwFqymTTwRkZiup9doUilm5YhGbDkOKAwH5ZOf2rUkLVBNNTaFnv04UrltKXpnoZgVIkyzpboLVizeR3qEnnZrBklkz2SWCAY1BKCeL7p2aI0sSFP9ETQmHShvrwLUPQ/r35viT+3Pe6YMo3bySg1WGNMWeOrNRW4wxxLwg7U+4msef/B3P/+YxfnjLCDpkekRlsVA4i859+nDcwH4Mlh83DMaVCjuKqzGaDL6OZ3/zCNee1gGvsJAKOaEdP1tj1RWUlFdj9a5l5d+MMaQoUQnpw1RQ8E4oRMiB/csXs7qsBUP75oBxiKRElNBFMFr1TWoHbrnuDArnfsqyojghULuppRNBquMX4/+ioRRMJQfzdlOoxTeQgKibQqPsAGUlpejMQjBGyWqEYDBMig5twkEw8o+q8krKtNFy9F65fwMfLtrHoAsv49QOaSTk444G1FQcYPbs5TQYPIKRQxr7c90EHMlRzpJP5lDYcAhXDutGjgtYeH3wKy+rpEqHZWKCI/9OlX5BzbsUzfOInYsCDWuhSVGcrJMp6KhRtghLvpRwkGA4QpqeA/J59fzRZZwAAeHUxKoo3LGVlSvXUab51bGxBBEdTwtGh+Mv5ZpTWlOaX0S1PKeOhxsIYMnaascvWllBqTY8X2biarwimi9hyWxMCP85HMIYQ0j3SFqaDt2LWLDqAL1OP4n+8p2WWWGC0tXCh4NhwsIVAuFwmJCYGWP854h016O6QoRDQflAkm5uRpBoKJuBWkeHDOzFgJ6tcJX0Z3Uawr3f+Q4/Or8h86YvokjyuBqjVPm5Y3LITfMoLKjAcRzpVsah/EpyGjbCL579NWRkhyg7sI+oEYyTTthFXpDsi2mj5qakYvFDohEKBwiRjZ2TkZY9tF710nrVg66KJ6hYX9WNjIwMMNC2T3fsWnP8wC60yLS6BglL58hRDmvFiORmEqgq4nCxwXUcUsJB2SUof0raJSSbKT1g65LlFOX04xTlB4M7NySiMbPyUluCITDiq18CsnGK9bFAUHY0RDTQThAq967lwxX5DL7gEk7sGMHmuwHpbHGwxckiOxIjX/4RyXJ0ABYmKD4B4TfMCBF10uh3Qg/OO6MXpw5qS674aLm1mBjjEtK4heQjlmYwGJIermQJ0jTTpaSoDLvGZYTjFBSXkpHTmLDic3b3s+gVXseL72+ifbu2ZIQ1BvLlsLWD5A+mp2quQiC9NSed2IOhZ/RlSNdGRMTE8giHQwQ0z0LiGw6GCek5EAz5a3lQz0bzL+GE/Hiao3h+6FAB4dymNHAdHMGHU8IEMAQys4iYOFmte3Dm6b0586Tu9Giepk275AGM1uVs6y+Hi0hoomVlpRFwAr7OqTkaQ/leyx49OPusvpw+pDNtc4LE4sJTDPMs/VgBc/Xhx+l6rj5OtSYFiIccGrRuhluyn7KEQ2YkTp5yyfQWLWiU4bF7zecs2B3hwsvOoWeuq/XNkNb2JK657EKGDDiOk44fQNvsNJq360HP1g3Rvg+Moa44jiGhzW+z7ufw48d/w4uPPca3R3Ylrt2w47qEs5rTb2Af5acDGNS9Na7slNukFRmJ7cyfv4GS6krlpR/x5OOPMX7JIULhIJgAzTr155STpWf/rjRIcfECqQSLDrJ+b54OAgp5+9ff5bH3llMdTLB5pdbCANi/zEWxqUSxUFMLvyQ8/7Z/1XJ2BtoyrF83juvegvSQq7GQ3bVgOYEQmY6Dk6gkrzRBkyYZmiMOIa27qZpLxgTJbhShsqCUhIVzoPBgOcEGOeTKFol969jpdue6YblMfGs2FRiMMYRDYSLynbCeTShIOBwmHLJ99jnk9xljCEvnSFoGTs0BPl+VR8+zh9Cvdz/aZIdxhNO4UQoV8qlK48h+KYRl1xTFgJo9q1hVnMXI43vQt08bjW2AoCP68s2I8JK8Qj6fcDiEegjpHlR/ZkY2Edkus013+vftrRysh9Z/ySAeoXCEcMgFX7YQoVAQWYqY4nskNRXHpNAgzSUWbqyY2V3rdh8GdG1OqlBqgxvR/etZsCvA6ef0pH+/jhrDkGgaji7GyB7BAGmZaZC/k4U7ajjj3N70Ux7QKC2AzanQgUO7Uy/goZ/+gEtb5uvDyHps8bwECc0HL17Jso/mcKDhYG45rwcZ6jSOwVFmsHv5XBYfbsj115xGswhYfQwQk8+V68OgF2jA+dddxdVnD+KMM0/lpG7NSc9tzgnH9yaydRNrNcZh2VFhgIrCAkxqNpmWgGgkr4B0ChIOh7DNQY13OOiSktIAbWUoKKzAsf7iViv2VtKgUQOMawgIPkV3I6QDK5eyzWnL8AE96NelqXBTCBEnntNRB7R38tR9Z3Jw8SdsPJjQwUMZpRUKaEnmR34t/4hohkTQhELY53AogDFGsoUJBYN65otnxdjMVEMwo5X2dD0YPLAv/To0JiC/CoZCwhe8qNtxD4eCkgcSCjSBSKr0MZiA2oJhItrzGmM4tHoJmxItuMTqIBumpKQQIUE8sy0X33I7T31rGEXLP2HN3jhGI6Mph0mUUpnekUtuvoOnv3UOhxYoPy+LkJnqkNGqO0mf7E6nFtm4qIiPfsnJTcfVx7tWA3swqF8vhgzoRkv5YkB+FAmHCRkwwRDhkGTUS5smaRzcvY9y4xCMhHBEJ9Igi8xoOfk1Bjs+pqKEolgKLbODBGQ3SycoOk4oSUfTE0+HNZ4bIUvjaVy1y6bJfMJQfmAjy3c4nHFOLwYM6ECOEELijV0XJH2qv1d0yNt7mEjj1ri717CiNJfLj+tO375tyQoHJK8YUlcMjTJTiEcaKb/pyZBBfejfrRVaWnBDYSLhEFKDYEhjJT3DwSSebTM6+Kskg9Muv47HfnkLzQ4s5LONUWy8i3oBxQcHR4t6IBDQ+NXy9FBxycxOoTRvPwkjGOUqkaBLULEgvVEuwaoSfVQxONI/Vpanj+RpZGUZXNetlccQln0jGoOwjGdMkHA4SDAYkqwhwYSx66nJaEhmsIzdO6p8WoGAIwsBxsIECQnfGEMkEiZkdTVAIOzjB21faiNyQlUUlcZ9fCdWQkEVNMnJEKD8FKP/RSmoacCZl13L7357I833LOTjHQmcjDTCoSApYSNYj7hnSNHHaCe9KQ1C1RSUxpI046XH0PT8oBYmVzlGTHN7gPasx/t5Ygv5uUPAjkM4JL4QDIV9HhpSji6hoIeTYnAObqKo+QDOObEvJx7Xm1NOPI6LBrdi58Y1HNBHqwY5bTixf1OWT3idzQ260CYDSEkjLZggnNuVM07uydBT+zCgfZbma0D2Dfr8Aq6Rvay9AwRk/3AoRDAQJKzxa5Aa53B+IeEsh6y0MK4TJiMDNq9aQXXjIQw7rhs9Wjcg5AYJBcTvqCsUTiEcDBJJSRWOoxokM+yx8rOleI270au9Q1xjHfH1NgQFGwyGCItvQOMYzMoiTILGnXpw1pl9OfOELrRrlkVGdipF+3dRI3/KbRDQh1+DYyAR9wilpBNUfGommMrCIhLap2ZFXPILyohkNCDb2lE+EQ470kV4Vs9QWPoHVLNonG4oLqrw9U0PRMkvqSAzpyERT/RRUQAKBeLKdxWTop0YdckQmqdANBqkZd+zuGrk+Rzf9wStiX1onJlB2x796ZoWY/PqdZRlBGmU5RKIFVNW45KZlkOX1rkU7N5FgdbMRmlGH4+qCKVmyzcsPyOGX1zGDRBwPKI11Rw6uIuFC9dS07glLTPSNQ88XPXLbLhejLjy0Svu+iXPff8a2oTiVFRUEAtAwAKEJVeXfjoD6afzn940TYPsZm3I9KpZv2whW0oqqdi9gBd+/zivTl8NrTvRNljDjg1LWLGnkmjpTt5+4df8fvynHIhFiAQd2T5Bw+YtaJgaZPOi2SzPE1zJQT7+bDkVkUxatWxCqtZdHemR2WEAVw0/m2zFgkLPJeg62GkSCaOD/0qKy6o06vzXF2n9z9NRfirihki0gLkfzuDDJWtYtmwV77y/lAZ9j6d7KkQaNCGteCWjxy1k2dINbN9zgE3b8ijO28POPXvZuHUPlQmoKtzJ8lV7KKos4/DBfDauWsqCbUWYTsdxaqtSXnp0HDM/Xcj85Rt1mCO2Cmcmbsc0lX6n9SexdCovzFjAwgVr2bprL2s35JHT+2S6RFfx65em8+Fni1myfheKy0K2IdOAFs1D23eyY+8u1m0sIKV5K6o2zWb8ZyuZvWwne/ftYte+Cha+N4Y3daC1XDRTmnWgTY5w5U1GE5ZgFm0aB/j83XeZs3wJy/YeYtuGbVS2GszZ7csZ/fQkPpyziE8l14FyaJIbYdeST/hoxXbK0prRNjWfSa/O4PPVq9iiRGDLhh1UpjQlu2YTr7+7kIWfbGSfbLVhazENjjuN/mYzT788i4/mLJYtNnGotJztW3eya9cONueV0/O402lbs4bJ81azaMlHfLInTYdgXYgV7mbLjr1s331Yk7uEnbv2s3XbLtmjA2cPzGDK868wZfZCPlu8kUOVJTpoe5+3p61k6bo9GE3cdo1TQDrje5TLkNMGE183neenzmfestXs0yA6+spGRnOau/sY+/JsFqxdybbd+9m0djPlLbpzRodKnnvybY3jAuYu3+R/fRdRkn7kUXJwB9t27WPLpm1UpDalYc12Xn1/ASs+38iO/fsk72ErgURI+PeWvU6im7ORl179iDmLl7Ny2x42b9jy/9h7Dzg5juvO/9vdM7M5513knHPOOQcCBANIkZSoaMuWnG2d48n22ecky1YkKTFnIhFEBoicc85pAWzOeSf1/1c9uyAo23e++9/57nMftaZmuqterlfvvapeUDTFD2LOYJsPfvgWW/cc5eCJq1Q3B6kqFw3Z4EFtmzYs5dyWDW7frIXMXLLbi3n7nQMcv3CRmyWl3L72QEG6lDvypZvFlQRDTbJxCbdls5pgPsvm9Ob02jdYt+cIuw+f5uq9OloaSrgu+teuXqPBn0ORe5/X1hzm1J6zFOtN4eU7NTRIj9t373O7tIFIcxW37z6Qv5bS6iaRFdfIoS3bOVdcx7jZ4wmeWsMPNxxk96HjnCluxG0t5nt/+Af8xcbbxmyaDuOAWmMJLj5ClF6Gz6l0AAAQAElEQVQ/zrr994i0VlDaEKVZm8VavYDw+RvYu20TB85WKKjf4YY2D1du3EHDRNpcskfO4YmJPWjT4aClwH778l4+PlKGG6yltD5CU12UuroGfFYl2z/ZxNEr1USU0C1JEW2u4+q125RV3OPc6bMcOH6WY/sO8daOs2SPmce4jAh3bl3jVmmFiohTHDp+joMHT7Nt10b2XKwBrffrN29RWnaPs8cvcFDt0KGj7Pz0EOVWHA0l97guH7p/5yKHj57j8LGzbN+6lV0nL+vAIMi9G1e5dvs6t3RoEJUjtVspzFiyghFZYep1+m3ZEtJ8ZCrbHyBYcZXdO7dz4NxFju05zNbzjYwbPxpbiX3AhMdZ2K2ejz/ZLZ++xOkz59m5+SOOt/Vj9ZIZZItOc+1Ntm3axxEl2wN7j7DlUjujJo8iq76aQwc3cawkRLSqldrWENUVbbS024QeHOKdHae4XR3Er+QqUUQp9rEibdTUVEuXeHILMsjNyiRFBWBUJ/ntrc1U3D/O3/zG13n6q19l9Ytf5A9eOUabz6WxppJ68fBlZDJhxChS6g7w6nuHaFeC8/uhdM/rfPF3f8yJKsMtikmKKGA1Vt7n0o0S7l6/pHVxjiNaN/s+3cLPtt9m+pdWM0Uvn5rKbyge3uDC5bs0a1Ntip50FR5fntWD1rpGb+01KiZdvlnK3Zs3uV7dQtQkfW8E4iRDglrJteNs2nqY05cusmf7Vo6WpzBlUj9SCFOpguTs1Zua92IuXrjJ3cpm/HGtfPyDP+bvP75Ginubn/7gZQ7fvceJre/y/R+9yssf7KEu1MDWN37MmpPXuXF8Ez/+yWv888/WcaOulYvb3+Anm45x7/ZR3njlDb73kzfZe60WK3yfH//5H/HeyQoStYFrKHnA2cvXeFD2gCsXL3G9pBGibRTfuMWFG8XcL7nDhYt3qGiOag00cfvyDS4rVpTcvcbpy6XUtYaxO/0qNo0ybZg2zVe9iprv/elv8sJv/zlb7yUwZ+UihqU4OnivobqunmY3wKTlzzKrZ4DKsnJaIuCGWqmtrqK+PYrfBwHN/8V1P+Qbf/Qal9sNA5eo8h36rigp4aZixt0HDbTWl3JD97cVoxraWrl/5x63bt2lPJRIT627bWs2c+j0EW4oJ10TXFVZGcUP7nGnpJ52bSqLBV9cUiG56rh92+AWU6bCrLHqvheXrl9WLmgMMmLaJFLvbudv3t/HrgMq0O5WU1Nylbd/vpYjpy9xo96hb+8uJFnJpNh17N20n4sPQkybN5nI5Z3sOXmBo9u2cDN+IAvH5RuFcGzZVnc9R0yld/gcL7+5lwMnT3O9ohVLforK0+6ZLnvWrWPfictcuFuq+blNSVsu86Z15chbP2LNp0fYf+wCN6tCooTwLLSMSewxhOk9mvnxP3/Idq3RA6dvaM6MfYoV8+9x9W4t5rDIMhU9YPUYyYxe7az9yYfsPHiGw+fvcPfeA+6U1wq+mDt3i7lc1kR2j0IaLmxn/YFz7DtbTMn92+IdpvNKiEcyqMm/azVPF5T37leUcO3yda2RVqK113n1Jz/l8M0HHNv+Ht//wSv8ZN1BKppQXECrxyXsJigeDaP+1AZe2XCUwyfPc6/0PtdvlFM4agqFjQf5h5d388m24+w+XUJb1FU+NDxRYdtAiXLX/Qel1DSFvHxXovl+UNXOyEmTib93iI8PXODg7o+5Gu3N1DE9CTXL3zKKGNUrgzadSmYXZROvU/HGmjLVaiXcv3eVm40BxkwaQ+2Rd/nR+wf5RLHs1O1mWptruKe1VHyvhNq6JvF+IP8qprSmlRrlWjP2oLyekD9ZG/QSTqk+3L19Iyfqc5mrjVaD1l9JmeZVOV9uh5szmEkDU9j91s95e9Nhtu29wI2aCGatWa6Llgc9xk4kp/IQb71ziL3Hz8i3S7h96w7hwnGMyqninZ+uZc3Wo+w6fJ3SFnCsqHzCIsXfyp4PXuKt/Re5c3EXr7z0Ov/88nvsv9FK4YApjMxp5NDOM+zet4eL7fKxyYOwruzmb19aw/Xy22x77y3+4cev8e7WUwSz8xk+chxTxo9k1tTRZGrzk1Q4lGHdE4josNnis8siRENNNfUtYW0O47UhS9JhgYMVDcp+jRSf+oBf+crXefZrX2H117/KX20sYfCoqcyb2psrH/89X/nar/HtH60nlD+ZpdN6E2xu0EudamrqG2hQMdvQUEeVcnxruI2bl08qv14le/hkZowdw4yZk+mVHOLioW0ce+CSrtPB9pvb+cqv/TVbr7Z6QkZkV3OT3qUb1r2jvLPnFPtO3dQ8PuBKVTN+BaSGirucvXSFvet2U104gpk9kyi7dodb94tVrzXKvj76yL96tl1l44HzHD/2KfvvpTBn5iCaS67zunzc7TaBx1aMpvX4B/zNuqtU11arVirm1u37FCunVKo+uqt1dlexuLGunDuKTTcVcxu1qbxfLLhbt6kKJdNbeX37u9s4dOYI50vKtLbukzRyIgNDF/jpG3vYf+wM57S+bl67Q0tCPmlN1/iZ/OHUsWvcLS3lbnENNRWl3FJeuXu/ntaGslgMVU1W78XQ+9y8eYcK8pk3IZftb77Mht2H2as4c78+RLC2IhYTHtTQrtrlXvF97gi3yU0kL6ld63o7p25WMWzKNBKubuB7aw6w++ApTt+u1dqWpS01fZzMLuS69/jgg0McP3yJ2w9KuHmnUpEDDIilWN9YXqw9wQOuXr1DS1we3XyVvKsa9cShk9wpreCaavmm85/yj+8c5uS5S9SSxpDeuZgr6trY8v1Ln7zKX797mFtXj/Pjl97gv37vTXZor9NwbTt/+o8buPLgBu+88iZ/+4+v8daOi/JWOPHzv+ZX/nozdXYyPfsPZOqU8UzVgeaU3mm4TppelOSRE7mn2mgXu0xsP76fjy8GmT5voiQQdxds89NWyx2t7Tt3y2iMBCm/f59bd4q5Vx9h+qwxtJ7dy37hH960mfupw5k5PInK6/fkeyXcuF/nyZLZtYho8XFe332S/WdvY/JV1c1LfLJmLes1J2fu1FPYsycFyRZ7fvBnfPsHu6gVf8zhpqlJou2Y+HRL+e9BbTP1pQ+4qfvbDypobq7jjvzs7oMyHa41Y3Lgvds3uF0ZZvTscdTve5+fbjrozd/FkiaiRh/N9c275TSH2nhwv0R5soTSpijZWUmUnNjLvgv3KLtfwX3F3eu3ymhVfk/v2g2n5DSvfHpCe7Zb3C0pp/LGJbauW8NHh89zSr5R0L0HeTrsM3K7lowXrWDDm2vZoXr41O060nv0oEtuV+aMy+XTt15mvXxyn8aK9VIX77LkMRDoPZwZXRt45W8+Yof2y/uP36A+2EaFZL159y4ljVEdopZwVza4dr+RoikzGdByln9+cze79p/g0Nk7NOQOZNGoVO1F9nBC+/mPNp8jd9IUhlit3NV6vaW1U97iUltyX3Tuc1N1bnx6Ju3FZ1h/6CqllaXa25dyU7VUXdAlPrOATKuYd98/zLH9l7hfdl9rv5aI8kK8W8/pIxc4enQ/Gy5HmT5zON2yc0isu8xPth/n2JHLFJeWcOleQ0xL10wujJg+noQrW/lHra9PD57k3LUqIpFmiovvcUM+V98apEy56aZqo+LKNuG6ilPmp5kz29fw0o5zquUeEJ/TVft5P6mFubRcPsBr8rPzZ29xT+vxxq1az6a21pHh2ktxuVfraX769j757Uku3HnAlUs3lQ+GMXuYj/2bD3Jc9tqw6wZ9p06lp13PRZ1v3L5bQl1zC/cly03Z7m55I03V5dzWHNwvKaGq8gE3lUNvXr9CXXsXFs8bwLWP3+SDnUfYc/g0Z283EKx/wDXRuXvnDuX1ddy8eU9rq5gbla00ldzhpnS+dvmW1lkuiyb35tr+HRw9dZ6tWw7g9J/AmJ7xuFoTuJaMEOTYx2tYu+8cp849wJffh/6pEUqu3xLNB1y6WSNHSiY7WsuOTXu5UJLI4qn9uLl/p0dz++Z9uL0nMK5PgmhGsbUXRFFrtObEd2Wz5uQgnx48wanr1XLnRul9n1taN7XtLhVaF8b37movxCOX3skQbChly8bDOn+opaQ2SlNzRLmungZtaEpP72Hz9iuUhAIMHj6YaFOQvOw80uJdmuK6MGF8X65veZmfrjvEJzvPcLEkSGtTJfflo8WllTSY/yyH/OG+1n11Za38s4QHJbe0129jxITRtJzbyCvrj3D4xFnuldzjpmJxamEBNRc28e6uC5y+dp/y0tvc017YMiYELO2VH9y+rFhcxs1Lp9h/8BwHj51k87p32F6WyapVj9GLNuWL+zwovUdxSS2VOtsqVT139949+YRLXNfRjO0WYsPP3uWDLUdUX1/iaiUMHatznPYzvPH+p2zTWc+BU3dpigTISQhz8ch29l6qps/Y6eQ1nmPTzgvaU2/naF0q0ycNIHTvLsVlJVoL5TSFob32AbcePODe3TuUKPZO0KF+9OZ+Pjl0gX17NlIcGMjk4YWEWl3AxZdo8eDIe3z/3V3cv3ee919/i++//BofHComLa87Y7VfnjJ5ODPGDCFFNsjtO5zBBQ7Fl/ayeddZTp48xyef7sffezLDugUoGjGTIXG32bxJ9jm8jzNNOUwZO5D4NojYljxHbM1HdWdbayu1t47yj//lN3j+N/+EzfdSWLZkLv1z/TTXVlGlukclFuGWOipVN0SsOLL7TmV27zC7N29g//lWIlaYJsn9k7/6Jite/AbPfflZvvm3H1HZbSarZwyh8ez7fPtXv8nX/uQn3EoZyJzJw8jK6sHiRdNIqz7EX/72Nz3en1blMWv6JHplBKmqrqOupoZo0QBWLpxFVtV+/uS3f40Xvv17vHWhncmzlzJ5oE1zXZ3qslpqGy36TV3Kk1N601ZRTl1LO2bpJeklzYNj6/jq1/+Jg9URozUdIc27/3/ty/7fqZCtwxRDv3DAGBZM6U5rTb02IvUk9pvG158eT6IGncLhfP2ryymI1FDbns7Sp5YxOtch2J7E/JWLGZoVISq/T0zJp2e3HLKzU8jIyiK+6jw/fHkd1xszWf3Nr7JiSDKVektV3xym02NtR4hA4ciF/O5XpxNfV011cyLznlrCqDSIJvbiN37ni4zPCAm3Tm9eolieRVw5g6UAEqXNLmD50/Poojej+WMW8o0VQ5QcGnSAPp+vLh7qvfHqO2o46fX1ohHHzCcfY1yOmKrA8+kHO435zz7Nkh6WNgUWM1evYnHfZMLRdJ781tdYplP4cgWd2qYWrxjpP2s5z8zoIjs1EY7rytNffpppea1U1YUZs+RxFvaLx5c9hK99aSGZ7bU0pQ3ly88tpGsgCMkD+c3f/gJjs9spq6ymrjlEeyhMgpLB8wuG0d4exukynG88N5uM1jpqauKY8+xzLFSiiIYTmbN8EaPzHKI6eO8+bhYrJnYjFEUHT1/nq9OKqK0yC6dFfRmMHtqHNFuLFvxhlQAAEABJREFUvDLK5EVLmNUz2WiLbdveb+bwhfzei7NIaaimqqYVn2NjRdshrjfPf+UJxqY1UK2DiSkrn2Bhb5uQm87jX/s6q0ekUmn4NAcJY3u0OhdgWDvM8QuXM72bSzh7JL/+lfkKsLXUZw/mq1+eS3c35MFblo3B8Wf341e//RwjU5qorGmk7/QlLBuVTXvEz8Kv/wrPjc2mqqqO2pYgETlZoNtonl06mkQpHXEzWbBqMcOSZdfUvnzly6sY7K+lpi6OaU+tYEZRgGDIx7Sli5nUNc6zWf6I6aya2ZewTjkGL3+R31o6kJaKKiqqGzW3EW/zOXT+Uub2jBLK6MM3XlxCQbCG+oRevPjCIrolBAlb2SxdOZ9+aSElZ5f+M+azdGSmCp0klj3/FBMKgp682cMW8Idfm0+W8Kv0ZrM1GMJK6s7CCf2xgw2ClyksNVwCfov0lCjam9J37DDSNb9R2civuWqRTxRNWMUz0/opabdT1RZPn36D9BY0QpOmy+dAezCOKctf4Nm5wwi0RWltCzBo7BCShRt1HAJKEo0hBdQZz/D42AJaG9uJWBaW+iNtIdzkQTy5cjZZbi3lNbWUVTXSRTx/dfVUUl2XmrZExi1YzYzuVmzu6+qob/UzeMIoUrQxD+YM56nHZpGhpFJdX0dlbQNJhcMZq6KpuraFftMeZ+mofGrL5AtKQOXNNj0HjKRHUoR28hgxojvRxjBRo4ymM5A/jC98+UuMyQ1g/vWFZVlIVM1RAkPGzmBEFx/VlfWU14UYMOMJlo0pwpYt2uLzWfKFLzOrVzxV1fV6AVGvTWw/vvClZxirBCu1GTx9IcN1uFep8er6dgbPXCX8XJp1byX1YsygHFqawR/w48hPQobm6mcZnhymoTWKZVmgOcOytV4gnDGI1V94kccndCfU4BKMRL25DWq9Dp22gq9/9cs8u2IVTz72BM8+8TQLR3cjLrUri5/5Fl+Y0o3mRpfcftN48evf5rHhaTjxIq9P4ahZTO0a9f6aW4/I/cUbQu0t9Jy2nC/M6Kbirk561lLZKt/72jf58pQeOJIt1BrPoEEDyUsNYw4o5AJab/HMffYFfmXpYPwiGGproffMZTw7o6cXd1wZ2HIRNvhsiEtJoHf/YXRJa6O0vJ6qtgRmrPwi8wdmEtGmJKSCvDmlH8sWTSG9rYnG9og2EPGMnruCRZqPdvnMmJnzGN0ti7j4JNLT00kXzajeqvcaPYNpQ3uRFIgjNTWDjPRk7wVCZs/RzJ08ikzZPiklnYy0NOJ9ksmXw5zHHmdCrzRCIYjqoKHWzmfu4rl0sdswOcWW1Rvrmwh0m8zS6UNRJ61RG4uIDvsbyRuxkCWjChSnW2UTvMuVbyMIG2i3Ehk45Um+9eJzPLlsBauf/iK/+Ru/y4szexGM+ug/7Vl+4/GpJFkCTu3Bihd+nW+/+Cyjc30kFY3g2a/8imJ/KinJMqJA+k+bzqDkdmokL8aq8hfzG8rsz1Mrp8rrQ4RDcUxYsphpvRMI64VF5sDJPDOrD1F/EiueX83knCA19Yksf/ZxpvRydGCi2PPMQoakhogKfuC0RawYm6s4F6Jg9Gye1vy3hSPykSRmPL6MCXkR2sJR4ntN5g+++Ti9HM2jcnxjW4Tswh4M7ZelHFNDKH80zz0xSn6RxcoXHmdkWgv1irmFo+fxDflLa00dNU4PnvvqkwxNl3LSx1Zs0g9x+UP49W+tZkCgjkrFjnbZ0+eGNeQwV+tm+cB49TfQdfoinprW21sfI1Z9mW8vGUCr8khNQ7NmztAE17I8/yWQyxNf+xpPD0uOxRvJYv4JYjTQhRVPzKKrL4grPrbgLW8OM3nqG19i2dAEykXT338Szy8dga81pIPNYTy3chy+tjA9py7jq+Jbp1yTP3YxX1k0GFv2QbSMLziOS1oqXqwJNrfSShfmL11AF6dJuTqseclg1JR5jO6VqZidJL/NID05XofxruQRFckT0XwXjnuCryyfgK+mivK2TGYuXcrAlHZackfytReeZkBiM6XVdTSLt9FZmiNU2rSu8vSiaOGoHoS1gbNT+7Fw4XQy3SgJA+fy7MIRejlWR1UkX3XYasVHS7nSRSGHrhOf4dtPTicrDiIWtOvFQ7fRC5g7LJNG0eo+einfeGo2SS01KvybCIl3UIB9Jy5iWt90wYSJLxzN4tmjiJd/tJLJ1PlLGJAaxO0+nWcWjMaWH5S0pDHnieeZ0RWqotnMXLyQvgmu5EA045m+8kWeUl3SqE1inUlQ4OlmWRZh2SalaAJfeWE5+W4Vd6tDDFHhP6VHMq2hTJ740tdUp8RRVSHbtEawbDDIlvQJKn91GTKZWWMGkBoIkOzFjVQSCBNOymPhssfoFV/Hg6YU5q58ijH50OorYta8OfTNSCQ+KZXM9DRSEuOwIpI1GFEscalrsBk+4zFm9E2itR2wLX2Zj4Wln5BdxKJnv8TqeSNJ0AYvaAKx8NviCpi57Dm++cXneVqx6akVT/LsqieY3ieR9qQiVj3/bX7vV77KF1Y9zYtf+ia/9+2vMC7LIqnrKJ7/6q8wRza3og6R9L48tvorfGFaT+JSu7Ns9Tf5ddF8/plVPL96Nb/yla/y/GPT6BbnYvsgsecoFgxLo7K6RdKBJSO5QObomXxz1ViCVfUk9p/K158cQ6Q9RMR1iIbbFEfqaU4bzNe/vIyufpdwyhCefmIiWYojBj+paDTfeG4W6W31qvsCzHv2GRb3TyGoDWVh/9EMK0Q+3o3nv/QYw9JCqjmCpAycxDPzBhENhmnLGsjTq6aQK3qhoJ9JixYzvVeAsOJRxqApfGFWT9rdbFa9+BRTM9uo1Twt/+LjTM1xcBP68Ru/tZpBiQ2Y/9+YvnOWsHxYOpHcUfz6l+eQUSt/KBjJ1780myLFgmBaX55YOZ1CR3Ew7GPMwkXM7J9MSBvg9L4TeHb+QG9uR676Ct9e2JtGHfBXN7QSlAbRYDbznlzIiMyw8lWYPpMX8MS4bHlRMktWP8HM7ki2NjIGTef3fnU5XaJab4qZbaEwGIfQl7GXnT6Er35lCV0jNdRJ/hdeWMyAhHaigGXbKJQQakti+orFjM0Myt7defHLj9HbqqXOKuL555fSO93F7jZK+xHF+epqssYs5JmpMjTgOPqSvDl9R7N87hhyE/wkp6eTk5VGggOB9F4sXjaDfmlxJKbE+tOSAh7/3qqjv7R0BIki4comUTUT33w9RvPCygkEpICvzwgm6aVVq3Srro4w6rFneW58vjA0z5aFjS75Z/+ZC1gyPJeQYlB84VDlxnEkqP7OGb+MLy/qQ2NNHXXxfXjxa08wMFFxIG0Izz45hSz5XlQk0kbO5defHAvyS//gGfzqqmH4fbmMHtwNf20t5dFclq9YSDcl1kFTp1Lkip4rROmOfBsdusX3mcgzC4bg05oNksuiVQsZLB+MKKD0UzxeOS5fOTBMUr+JPD9/CM2qe4tGLuYPvjaXNMW7qtpG+YOkUT03dOYiFg7JwuSTBNlj9bIxBMIuPeeu5LlpXSRmi+JALgufWsSwVM2nZEkeMpNff3oi/qo66D+Fbz49mjgnmxFDepHQUEdFMIMlKxUrM4zcNo6lX18XJo/qQri+lrJQttbGbLokBhi2/Iv85pK+NHk+2ULQ9SyN0dU2uczJ55lfeZGVwxKo0H65oS1ERDbI7DWRJ5cMJl6yhuNyPJv1TJFPpg/jd3/7aQYnN1GuPWWj9qnBSBLTnnmahX391NTWkzx4Dt96ZrQ8N0LekBmsmt1LMdAllNZddeksshzll97T+eoTozQnDdLfYez8xYpRycrXLoGcIXz5S4obyNfj+vPF55fRLymImzGcLz8/m+yWeqqqwkx96jmeHBSP1W0Mv/b8TBI0v+GCUXz92emk2wr+IDUtM7Mk953Jd359sea7hqqaBlpc9bthek2cx4pxBZi1HM0bwuoV48lQDEHS2+jyp9NvyBBvXVZWRpiwdBXTCiFOOv7G6gnYFZIxuT+rn5tHN1cyCsW2DG2U34Z11CqqmWvaGDpvGQsHKcZFE1ny/NNMLYhSrfVQMGEZX39sCHYkROGEharti2SHMG7mQJ4wMU77s7D2XGZfPdPUZW02E5csZWYvi5Byw+D5q/nWU6MIK/9XyGfaFRMjyl8Dps3TXjELsxdM6jGWpxcPI0F1STiUwOQlC5lc5CMkfxihvcITY7KokSx0Gc+vfnEe+T4pgoNjm98kRk4YJNw6Ksttpj6xnIm50BpXyNMr5pBrBXED+Tz53CqGJjdT3wrDlj7Bk+OzPf0iRWP51RfnU+DRtDH2QVdS/9n8p19ZSmE0NietZqbk/30nz2fZBK0xrR+nYBDPLJ1Ikq31JBzLcrW3gLg4m4DbSlz34QzOilPct/EpiLmKFaQN57mnFpIXaEZbYZJzh/LCN7/G+MIkNOUE22HIjKf58pIxUFdDTUOLclaUoJXA2FmLGVUQR219mOxB07R/7kekLkig6wSWzxqGowPZnDGr+NqKSfh1flTakq68vEQ1SZCicSt5amp3vUxvpMuY5aya0odoyJVWFpZlYSuXN4TimbDkCSYX+FV31FJTXUNbUn/Vul9jZu8EWlWHtMf1YOGimRTaQeqa0pi0YAnDs6I0B12iVhpLnvkqS4Zk6MVoLaa2Rq7uKxzKl154VjGxlZIKc9bTQhvitfBppnQPUFoaJFE+sHrFNFIa6yhtTmDmY88xQ2u2rimLmUsW0T+5jdYwtLcH6TJqDnOH5tNQEyV39GKenj2A9vI66uzurHz6SVTm0RaRTrYDEfDlDGXejPHkpyaQmJSK2UslxTu4GmsPRmlXLdPkJjNlwTKG59g0ukkMGjGO3FAt9ytqsQom8twT8jsLwql9ePKJpXSz67mnM6Fpy59lVp94+bGLIzuiS2Bia9Fv+mp+7YvPs3r5Cp5+6ov81m/+Ns9L1oD06DXji3z76QUUJoCZs299YSX9MiM0ticy86lf49efmENOXIScEcv45osv8sKqx3ly+eOsfuJZVkwdQrLjZ9oTv8rv/9qv8MWnVvPc89/g97/1qywalU1LAwya9wK/961v8dVnVvOFp7/M7/zmb/CFWb2xQxnMW/0izy8ZS3yz4OZ8gd/5jW/xtWefYvWTX+Q3vvVbfGPVZNKDkDZ0AV959llG5kBDKInpOlf59q9+hYUje5MT78p3oHD4aEb1SqXFLCzpjtfr3fw/92X/79XIuA1YgVSGTprK0nmTmT1rKvMnDyDNdoldfroPm6xieCGzJw1nyvTpLBzTlZxu/VmwYA5zx/QgyYFbJw9zP2c6v6vNxReeXMrXf20FPfx1VDRo0pLymb14Ec+uXMCCKYPJjYtRBkv/0/SpSO46dBLPPbWYRbMnMXfODBZM6IFPExuX25ulK5ey+rG5zBjVg2QLXTa2vnECdB0wkmVL5jBjcL530DFixkK+9NhUJowczKzZ0xhamG69PFQAABAASURBVEhB/1EsmT+FeXMm601iKibPG8Yda4dEw+PJ5Tw2YzRjRo1h8axhpEt/N5DNtAULeFYFz2NzxtIzFdzkIuYtXcKqGUNJt1xSewzlydWPsWzqKCZOnswi4WYqGBcNm6JFsoDZY/ozYfpMZgzKQdjYGQpmy5bw7OMLWChbdMlMY9iUGTy+YBJDuqUJJEpKl/6ywVQWzJvC+N4ZnryB/L4sWjSHWdoJxMVnMloHHMtnDyU7IBQrlXGz5/GFJxawbNZICjUhmb2HsmD+dBbMnsKEftlIHTBKE7uMDboMnaCDliUsnzOV7ilxOD5Hgy75A0by9OrlLJ40gklTp7BABzoZ0tVNyJU9Fkr2hSydMoTcRG8WUL0tPIvMrv2YM38uCyf3I9UfoNuIyTz/zEKmDx/C9BmTmTK4ICaBBbYlFAmRmN+P5Y8v5/H505gzYyrL5o0hPyCdfBlMnr+AL6yaz5JpQ8mVfF0l7xOLJtE/N5GU/H4sWjqLaUPyRAgKBo1m9TNLmT9lBFOnTGaB3vjm5PRgzoK5zBvVlfi4dIZPnsZjc0dQkGDJpg59J0xTsbyEp5ZMY0SPLFKyejJ3wTyWTR1CVlIcuQPH8/zqxcwcP4RJk6d4CSCz20CWLp7D5L5Z+FLzmTJ7Dssm9Uf1M2m9ZLcnlzB9QK4OJ2XHQeN4WoH7iSUzmdgnk1DNPUoD/Vg5vb9nB1dFF7qzgEByHMMmj+XJZdOYOLiQuCiY3J2U30d+PJOnVsxiuvQo6D6Q+fNmMHdSX9Ici3DUUrIGN6UrE0YUSa8AfUZP5MmlUxjdJwe/Ek5IfNK7DGDe/Fk89dhMJg8t9Pqj4kFiDuOmTWHlsjk8tmiG1skMHn9sPks1h8kqONpdH10HjuIxrbGVi2ewVD61dOFMVq1YwHzFAWPX0dOFs2wWS+Svi+ZMYfHCuTy5ZDLdk23SuvZnweK5rFwyK4Y7fyorVixi5ZT+pCQkMGDMRFYsnsLwrmnYkkei0tpi0W3IYPpkJaA9hnSSfgpHpg6Ny+zKjHmzWDJnEovlM1O1rsyBaFh2RMV5aySBQeMmsnjORGbPnMKimSPI1+a9qV00RD8+uycz581kqcYXS5cpA3OINLs4GYVMnjObFbNH0F18I0pGri+BXiPGK77M48kF4xiQF4/22iJkSSY1Y1vZ3eg3b0QBBC2wbCMJ5qVAn9HjeWLVfFYpdj2xYh5PK44sGVeEPyGbaYuXsHhEPoQs2uKzmbN0Ls8tH0K2gy6Xe/er6D5mFjO7iaYo+r1+m8wuWmOKAyuXzJa9p7FkwSweXzyNYSrmJJRwLTJ6DPJ8ZPaEvqTKRxC+JTJWalfGy4cMqczu/Zi9YDYrFkxmSGEqpg8BCQxz+X3QY1Afli+b6dn6scXTGdszjbApumw/+X0GMGPGFFYsncmCacMZUJRKNGQzYOJkpvRPoy2Qx+zF8/niF5bxxWeX8eLzK/ji4+PJTkpi0OSZeim3lBfM2BeW89Xn5zIgO5E8rZcXnl3CC4L/4jPL+JoOS6b3zQArkXEzJzGsIIGg5iWzex+mTJ3EY1r/S2aOYljPDPAl0mfoMObNm86qxVO0ZgfTJdXBTkhj4OiRLJw/kxULJzNtdC9yk3wY37csC3OZ7yBx9Bo1mWdVdJm5emL5DKYNycdnfMoN0GvsDJ6fN5DkALSqeMvqOYRVT85jWIZNQl4vnnh2EfOGJWB7ga2NO/eb6K/ieWyy4WDjTQMORQNHay1MY0SvLFLki3MXzmHemL5kJqUwZNJ0Vs4dS5cUh/jCAax66jGWzBjGyLGTWDa+L0U9+2stzZN9s0lIyWT8nLksmtiPgpwcxs6YzeOKnT0yk8ksEtySeSyaOpSuGfEYv8jsPYxVq5by1LLZTO2fiy8+nTHTprNI+WH+1OEUJGqBSdT8QWN4+on5TOqbjasCvXDwaBbOncLCOeM1RwFcD8xYDOMuHu2UokE8/tRyVsydzbhuqYrtNgYiPrcPy7Q5enzeeMaPHsuyhWPoodwUdeMZNnUWX9BB0HLR7Zvtx1wGB29OXOykPKYtWBjLNZMHkZWUTM8hY7QWZzF1YB5+22CodcC7SYXMXrSEZ5fPZIbi2WPzxzGwRy79Rkzi8SVTGdUjHQVsxs5ZxBeXTWbcyCGKq1MZlh8QEcTWcLdIiIeUVB/5vXozZdpkHls8k3nTRjKwMAW/YuX0pUt44Wk15Zrnn3ucL+uFaFai1REXPHPIR235yxS+oNzx2OxxeHXSqG4E2qIkdh3MyieW6KXUXOYM64Jf9owq6Ol8B9eXxujp01gxZyh5CXF0UU5bsWw6w4oSMH/dkjNgpGLeFBbPm8QQxSLvwBSLUDskZxUysG8hPt2H9OIlMbMbM+bN5jGtj15ZPhX5Nt2GjeOZp+UDS6cxomsKvuQ8wcxh6dT+pCenM1B+tmrJFAbmJJPVoz9LFJOmKT76ElIZNGYs8xXbVypXjumaRHMLpKhOWaj1P2tET9J8YOJz0Elj3Kw5PP/UIh6bM5qu8mXT7/mNdG0PuWT0Gik5lrFy4RTVm9NZPmMgKbZLRHM4b9linpP/mRo009BUfrOE1yaf6TNmKi8Yu69exgtqX9FL4THSI6T4baUWMHX2DB6bP5EhBUm0aqNh5/VnxcqFPK9YYuBffGEFj88aTEpUsmotWpaluYpj4PiJjO+dzKO5xu2Yk5CVxVTl+4WT+hCv+B61HC9PtfozGDt9FqtXzlV8n4eJGatXzmPGgAzaW6RLfBYjJ09h1Yq5rJg9hr6agybJlFLQlyWrFjK+SyImx7Qn5jNL+XL+iG7kdxnEqmfmMzgVausiVFdHKRg+ka88O4MxvW3MVVlcRmq/8cwZlSlni2LrRMLSgEsSQ6fO5YurZjJm5FAWzp7MqKJ0InKs/N4jmDNxPIvmjqNnmoWLTc+hY1mufD6mdwY+j4BLsuZznuZ4gdb72N7p3lrPVtyYP2cUBQlgJecycfZ8Vs4aQmFeASMnTufxhePok5tB1/7DeWzZDEZ0zSAztzuzFs9nwZieZKSmMGjCDFbNH0tRso/EooE8YWrLKSMYN248C6cN0EsEl4TCQaxatYzHF0xl5oxpPKbaNisQR4/R0zTX85k6bAizpNO4Aflam8NYuXSG6ptskjK7q8abw4LxA8hOTGLghKmsnDeB3um29ExkxLQ5fOGJxaycM5ZeaQHi8/qyeOl8xZBcEuXzY2fOZdn0QaRbkNR9CKueWMTs4V2wXZfsviN5Qmv1Cfn4xH45yB0xl0BleygaMlFrfAlzRwxk8uypzBjWhYABMM2yyew+UAcXc5k/rh+pfsjqO4ZnVy/VXAxh/KQp2tQWkZjdlVlzpzN35lTmju9DoqT20M0XjnLieJ59aglfeGoZzz25THl0GVP6ZEqPfjz++GKefVr9Ty3li8+tYKlqwjjh5Qway6wxPZTRwLJtbDXLskgo6s/syQNje6j4LMZOm8mSuZNZtGAGUwdka75dzOXppxsrMZdJcwQzZQCZvnh6DJ/ASuXWQbl+wVp0GzaWRfKVhbPH0y/TkeQOXYaMkl9NY2zvHI8/0mjY9Lm88MQsJgwZorpkEoO659FbuItVCy6cPYHBhQm4oRpuNyYwRTmkqw1R7QkdyYCTyIAxk1m1cDy981LJ6T6YpcphUwYUkZqWI3+cy5JJA8nPTGP45Bnyn0kM6ZKC3F6yjFUuW8oTS6YzonsqdlIuU+bMYenkvqTGJdF31ERWLhpPr3TJnljI3OVLWTm5Pz1792XuornMGtmdJJ+EUOwZPHU2Lzw5h4nSYd7cSQztmU/PwaMxOiyYPZEhRUm4xnyyc8x+cfQZPU55cyqL5kxicEFsZl07SblvNsYnVyg39MmOEwMQWseX9svJRcxYoLnVfnnx1IFkStY+oyezcsEkemc4ZKi+W6L6b1LfDJCigZw+LF2xlGdWzme+cmW2H6JOBqMVf+bJLxdMGYTZz0IywybPYIVeaHRNscntN5LlS2cyumsiOEmMnr2A1YvH0qtLD6bNmsWiaYPJTbDBtSgYPAGzP58nXx4nv5k1NE8HjHH0HjaSubMms1h5cpLnQ+iKo8/Y6bzw1DwmjR7MjOlTmT4gS/2xj2V+ZKus3qN5Qmv+SckwoX8WPl8aY2fMYYX4ZifF02XwOFYtmuLFc4PilVZY5PQZxqL505in2mVC/2zJp1E7hZHT5/D8U3OZOnq46sCpTBmUpxWkMRnXw9UEpXYdpPy7nJVzpzJn5jTtA0aSbVQMZDFBci6Yo/5xfUgSLKrLzd79sVlDyU1NpafqDxN3RnTJIL2gh3xkHjOH96Soa2/mLJzH4okDyZEp3ahPvjWJZ7QXNLqN65NJQnZ3Zs6dxeJpI+ihem3ExCks13wO75pCevfBqhFmMV9+nOkz6iQwSDlpoYnF04ZT0HHwJTWQ+pgrr+8wFs6bpvU0VbkrHRc/vUeOx9Qrk1W3WtjkDRzD008uYnLvFK2nOAaOmyhfnMJCyVCYID8DPJp0XNI5Rz7xpPasTy6dpT1uNpYv3ZuTZdOHkqf9fpeh41mhGDA4P74DyXpII797L55cPZtFk4dQmIh3GO8GMhg5cSIrtf95bNZoVDrQ4ktlUL+eZCZAe1j48oWWSBwDxk3lOdVWTyyeRP/cePwZ3ZmvdTh/dFcS49IZMWW6YvtQ8nPyGD5hEisXT9W+MYlwELqPnoKpt1aonpy/YC7zRnTBL5xJC5fy4mMTGTt6mPxlMgOznZhc4hmVzboNHcOypXNYuWQGS/Wyf/mSuaxQHd1FG/rmNoj6k+TjY1ih/e3EfnnkFHRj9uJ5zB/Xm4w4m6DqmXB8DtMXLeB51bOLZaf8RDAHvIG8nixeuZjnVy1g+bSBKP0RVzCAxxWvF48pUMxzSe06hPnzp7B43lTG9EiltRHSu/ZnoWqvWSb+OBDI6cmMObNYPnck3dL8OrB3KRw8hiUGb+4EBmQFiNWCeFeoDW+NrNSZ1heVb15Q/fNl5YcV2nua96jIN3RyTsiXwqgpExiUm0BLO+QPHM0y1QSLlI+WzRhClt9F70cwh/aO5mKG1szS+ZMZq8NXw8PFQi7zsJmziO6jp/G06qBVmu8nVQ9MGVSAHXa9g/TuY2fyzPxRZMVBxqDJPLNkIr0Uz5rFO6XnSJ5bPYeRRcnk9R3FqpULMDSe0N75qVWLWTF1MBkWNLhJygfjWblsLisXTmBY11TCqrdcad7eblM0cIRXtz6uGnd8v3ycoEsLmUzWXnex6twk+UpzyKGw33CWaI9kZJwxvDsJOuswNkjrPVJ0p9EvA9oEG0nqxjLFsWUT80mJFUpU3K6gy7RJTOiSKK5R+Nwi4v/f9X8Ztv0fI49LNBIhEomqxX5dJZ1O3tFoBPOXDBHt2iMGLurK6aIebFg4rgAdx6au5CZnLt/g8tUb7Pr4GGl9xzOoUF7jhj38sIcbVbAUQsfHVQKNmH41j4cNhGC7AAAQAElEQVR+vWfxADl4B88Yrvs5XHS5GvfwDLzrdsgUwbz1N3S8bsFEJKd5jqjjF/3FyODR6IAJ69cVb+sRegZfj1hGXu2ovGfBfMY/6vGO9SP+/9JmGHiDLx3DahHJ5Yqosb15juoey8bQjEiGiAcjSWRCdcboS35khRhOVHeiKjwDGzbwwnMRhGibvkg4rCAZ1kbE9Gqg42OJpqERki5NNbe4ca+MMr35CktGYwvTjHweDY+mJd1dTwaPj+h/nqJ4dugW8eBdz6c+R8eTvUMA8yMhjK4ejCe7bObh8nlepk/MXOOHgjNkXMNLskfMA/xLe3s4MXljMB3ymH7By7MI6zSrPRgi7NF0cR/aMap78Ph5PKKe3oaOK76evIbvI/AiSVQ72Lb2oOhJWNt6KFMn/UicAqFeiozQ4YHZVEt9g+Y1S99+K6LvoOYqQiAAcX5wBNjWFqGxKYLZvBv/M29lW9uj2L4YjPZp+J2o5tkloKRh/vKpriGE+Wstv2gYOrbkNniGTlAJwfQHNBYQXltz0PvPXTS3RGjRG9/mZv2KvhOwPBmQzUxf53iL4Mxzq4K7z+cSknzmn1u1tEaFr6bxJjXtIfD4it5nuGHM/+lQg/g4DkRCER1mRHR2bGRH8kN8vGwfjqJzFOLiYn1G1jjZxGdFaRNujFeEdiUOYyu/E8F2XOICLpGg5JcsbTrwaVFDiSNeuIaGTzZuaQrR2ByWrJEYvnjYilGt6ms28LZ4qi/gi8nRIvmbRE8vmHlUHsM3TjxbNd6qQsTMg+FhmoGLSo4mzVtnM7Zvkc38wmk3vISj81gyUlzZKUKbbC53JKQ1m5A7gLlTRpAYhZAOqU3hYFowGMX4Q6ts3tZucNR03y66IQn4KIyhFwoJP9zRDG67K/rwKB0PtxOm81d4ruY9qjXXLrkaGiI0C9eyLPkohNTX2BjxDkrqG6NK1i4KHbQafVtdb/026BClqloHKTURvN/aKJpWD6bS9Hc0c28KnZDmtUqw1R2tskp+L56aBZrFv0UyGfZh6WpsWif6deLdYmBcVPRF5VsxmRqbo8j8oHXapvv6evWrNeqwKiSb8guX0apdPltdKzjTRLtRc+6KodyBds1xVX3UkMPW2jb/2qNWcIZ1nC9KRIY3xag3f+pM6TaKeZN64UjmR+cgLOYmHriKHaZ58dWLpS5RxaGwbK4hxZ4oXpzRs9dvYLSGI148krICijyEdx/BdXE74YTrwcgGUeEbeoa3iWMocxi6EQ/G6GUsoF7Nt4GLCseSnlHhxWAisRgonhr6zHqyjyucYChCsLmMa7dLeVBWg2pCTL+hZfCNH4XFS5LLNwSr2Bs0uqjvc/Q8yhbuQ/3EVzK4ktfVr0fvXyBYohn15AtLvoia+TX5NCrZDI65F9EOGOkrWgbuX5AS/wTFn9RkSepGaJVPmrVm1qRlYo/WrfefTpDvNzSEqW+JYjt8Li6YGGTWvlkfzfIhj4bWuj/Oxqe5aRZuo/yp7dE4bGJxRyw1OJYDnXFXYKJvYctWLaJnYnRE9og3MUp4JtY4ks3Mq199Jt47OtD14qTimSsHNvHP1Rw1ibeJzWHZ18QhD0Zr2VZMimjBmDgeUc6xZR8T90x8NXARxY5W8TZxPCRcE6Md6WJin9HDy0WKsXHKP0HFo07dkR5GHhMTTYsLaK4U3+rqgng2EGyLZHT8FiYXtWoNmjj5ufxmdFQzNvVsr9xWK/xarWcjS1y8hV/6tmuujO0822gOA7JJs+KRhyO96xVDTHx3RKtTJmM7o3dIjmDujYyPNqOPWftGHp9s69dat+0IftnL5D0jq4lFppn7NuUjI0/AzKXic1NjkHrlm7CZL8nkEKWlKYyR2+QLkz/apLPJHya3m/mJ2JAY75CqQ6KUuIjnszI55kpM78asWWPpkqAnyzYhVzfo1+2IARH9RjHrMRJupqKkgtu3rnOzrg3zF6MREbIkS1hBydQ+5hlzees4SkQ+ZtaF8SV1acnE+oSm4OBqPEJYMGZ9mvhh7s3aikbC3l9qhWVHM9YZp8x91FuPUdESCflVWPMfEv+ggqXBx0jv9Uc8+pFOeMnp4XbCK8ZGDf0OWHNviBp4I7cL0j0i+SKm21D16IVFL+LJLAD5bKdsBsijb8a8oShhE5PEAynvdvAx9D1egnn4scSrI7ZEBGdgIgbvIQAiH/X4RzRuZHMlR9DEPenxEF6G9e4lgycjIvwojQ4eRq7OFjF8pEfnc+evwTeornA6783zwyYc029kkXAx2Ty+EYUaF8v6PG9p4MGEBeOKiKFreBn2BjQqvQw904wPGOxOm5lnoejjPpyTGHxEOVR9D3Fjz+gwqM/IicwZlYNPWCbH6sf7RKVPWLYzvuRKByODsYHbYTtPPt3H5jJGT0vD42vs/Zmfu5/T5zO6KH9oLBwhIrlc8XjoI+hSPPRoyzf+dR2iRI1R5K8RyRnx7lFfVPxM65DJkBKMR8vAGbsaw6r/s48lWWI4RueIgdFgp6yGtJGv0wZIUfNs+IYNTSO/4G3ZI6J1Y/Q3edZ1LfW6nk3Chqb4uoI1dAxNJFdE+GFvzJXc8glzLyyEGjVzIP07121MR9ETjYjgIsI1fZZgDS1PR8FHvXHRijEx1GJNcG4HzbBwo9646Ok+LHoSD1e4n8kXQzPfpj8iOK8Jz+gflq5h8TMtIrzOMQP/sEk4g+vBdOCHO3hZbqfOktX0CfahHuZZ4524xrddPUcMDY9/J25UVgRLuahTfwNj4OmAD3fQ8saFb8Zc42+6j5gxkLljdjDPpkU1dzIXj15GloiHI3mjrnAkrfT2dNOzgXU77Gt42B0+bOiZFv1XaGIpM3TQCIt2NOqKTEyWcIds7kOaGvrFj3Q0evkCUQLKdabmMHkwbPKg8rDJvaYeiDe5UfE/Kr3NHsrk2/iOfZupG5pUU2krg8npph4wudHsMw0dk98t6eLdq4byaiPVHa5qF4Nrxj0cDRgcUxPUq/4PmfpFtYah25n7DV+zITF1TrNysLev1a+h4Sq/x+SXXbVpMfvdoHK74d0mvmbv69U8cRZGR5PDTf43tYyp3UwN4DO5XrCm3+juKLCZPlP/tageDKgW8moo1VUtql1MTWDqKlt26ayrDI5f+np1muCwwdRQCtg8rAU13lkLGp1MHWNpHpsbw3yu9tG+yYwZGNPiNA/GjhHNo8G3NMfGBkYWo4etfbMHLzn9qqU6ZfBqJdnc0Hi0Gbt686C59uoh6d4u2/mFb+ibDVmj5taWHWz5l6n/zB7fjDmqtY2spi41shv8ztaoeqnZ1Kh+SFB9aeo1U6d21rFxZg40ZupuFAPMfJrxoPTxeAvH7O1aRMNRDWd8DfE3cEaGNu3JfB0ymrrX0DVyJQg2RRtwn84MolrTWjJYwksrHMjSGUNJC2gyNCG/uDZlzv9nPkbD/wBlLGzHwdEhstPxq1hE52XbDj6fxm1bMObXUqyK3fu0STXBtfukhTw9PsPbfJbcuUV99ii+snoSmRa6fJ/hi4bXpV7zsSw7Nia+Hg/9OqYZuoAlePPs6+h7FBddZtzDM/CW5clnYO0OPK/bu4/J65gOPn99JkMMxufYeHz0ZdsOPsPbUZ8lPMnrGFuYZ+/RJsY/hut09Nu2E+uXTObe6eBrGXzR82hKLsuysB3Bqtm6R5dlW9gac9Tn6B5zWR30vWcL2zE4kgldwnMcjduO9LexTFcnvs9HwO/D8fA08MjHErxfurTXtNNv9Bh6JzSh8yfpK1o+Rzj6FR/HsT2aeHwcjTvemCVarpKOfryPZT0Kb2E7TswGnbL8qzLYMRjBOmq+f42X6bPAyOsTjCHj8fJk1ABg247ksrEtG0cwjmNjWVbs3iBgYTsxGAtdGvMFAsQpgsZoWliW5cH7PFywbCcm2yPyW5bd0WeBZT2ER5ft8xMfF5Aclp7A7sD3ia8t2PikNDIzzM5Rw5baL3wMv8TEAFl6K5iVCdlZkJNlkZfjkJ/rkJttkZNte895ObbG8GCyBZuTZfotPVvk5fopKvCTJ/hs0TCtE6+TjumLNZuC/ABF+X7yxOezZj+k7+GK/2djTgesJRiLXOEZunmSKdZi47ni/S9wc30UFgQozHOEC7nZHbCPyGr0yRUtg2/us0XHa0bPbLuDd8ev8HJko9wcYyNbNGPyGDlyRcP77cTXb062I3394u+L0enAz5NcBXmmzxYNyBavbA9efHIdwdrk6jk708X0e82DsWRvM27RidM5luvJ5Hhzl58b+83LEZzkNfzydZ+QAHpnoYNcm+oam8oqqKj06817Fo31LiWVeq5WM79qldWCq3WoUauucTyc6lpbB7wIz1WDGIytMYuKKtNnGlQY3Br1VbodMI7o2MK1PLwK9VeIh9cMXpVNfb0j+WywHPlzzGlja96S+9vqU7PMvYXJA5Zlo0dzi925boz/e/caU4luWbbWiIPzsN/B4PBIvxnrXDdgYTsOtligy7IsbNvx8B3bVn9swLJ0LzhHzVa/wAALS/emzzRbRCz+9cuynYdyOaLh2LawY7CxMTv2oG9LYwZGtaIKQZvaeh+VVZYaVNQmKEak01wLZWY+TZNdK9Vq6myamhzVPxaWZT3UQU/YjiP+tvrB0PeZ+ObEdPKJn2XZOKZPOmBZHq5P45ZlYTuduJaGYs+OxpyOflv4hp5Pz47B51EYG68LsGynQwYLc9nCcxzb4+UI19DohDXjXrMc5RhHjtxCep8RDCuMpymKR8uTV/i27cTogtcfUOwNGF0cm0foISfyfMeyLBzDT82xZRPJa+nX8Hc+j4B3WfZn8MIxetqiYdviKz7mHj073piNLVrm3rY87H/x5VfxnZbqKN6ata21q9iQb+KJ4kTnWs439+rL8eICeOve3Gt95+YIT2vexJ88c684Y+JDjn7zvH5Hsc96BMfgW3h4HTQ7YXOz8eBysm0vDhl6uaKTnRnrN79mLNYXi085ksHA5XXQMrJ5tD/H2xI9R81WzLM83ka3XOEaep6cho+ec0XH0MrLcYjxQbaRPLkOeYLJMXp78lgYGA9XOF6/GXuk5eb6KCoIUGBwBZOnZuByvBwmejmm2ZIJ6R3TJ1v4uerPF05+nt/DL1QOyRXvmP5WjK9odfZlG3oG/pHWycvQ85pkzs1xHurk9YnXZ7+iK3wPT7DGLkYGw8Pg5efan4/vnfLIZnmim58XkKx+TKyPyWkrX/ge4ddJ3/LsmS9euZrvLMmQlgoJCY7n13Jdz0eTcnPJTAxg6X9ex8MvC9txtMYcbDl1wK/faDPxeUMY1y+eah3YO6pPHAtdFj69gY4L+HE6CZte2xYv0xwc0VAXlmWebTwwfZk143PMs4XtOJh7W/22dspxpvaxLcFaOFpzTse97TgenMCIrWEffvEPiL/BR1es38ERrGmm38KK8fB1wPt92IambeMTfVv3WFYHjo0F2I4jXg7q1pPljfnU5zga/enk0AAAEABJREFUtwDLxhGu04FrO47gNQYasjF0vTH47FkwHi8+f9m2cD1aoikYx9DsADE50jK8vP4O+roPmLjn93lyefCWFbt3DI0YHI9cVgcPn/h0NsfwEe3O585fx7E9TIPTee91dH4Jx/Rb5vlzfB0cQ9P0f65ZOJLZ59gYHEPX8OoEte2YzI5jY4seuiz1xWAMhjqEaTuObOxga8zRvYGN3dseffNs+VMoyEnBUX1gsB5ttu14+JZlYVn2wzmyLMvD9+TTve04Hpyhhy7bcTD2jpOfxfzc+gwe+IyuHoTvyMaObWNZksu7tzRgPha2+h3Re/gr+Ni9YB1b4xaWZXn0HdvCXLaHY3t9tsZMH1jYjuPJ6TiGF//ysmI4PsEZGJMQLTuGY1tgadzY2DEPgKVnR7AevHgKxHTiaN0EjL9Jlxh7C9tx8DkxvpZgDZ0YGcuTMzYWu3cMHLHLtoUnOv6Odet4SBa2aDhOTF7H60OXhe3E4G1v3MF5OMbDy7JjMD7B2t64FcNzbCzAsm0+k4+Hl+l3hOM14Zlnn3Q1sKY59i/K8xD1IU0PVzR8HbywLDr7nI4+s7W1HafDXhYxPg62pXs1R2OO/ci9Y2NhLiumh+xlYGzBombuPX66tx1HdB0swLLsDt6fx3dEzzTbAPH5y+rUUXScDgDTF9M/hmDZTof9LCFb2I4jPoaXTQcKv3hZouvz2Ti2je0BWdiOI1ltLMB6SJN/eVmWBxuvF+0md2UpX2YrD8bypKP8bHfkdAuTwwvyOp9RnrcwcHnKf3kmhyv/Gdw85VBTX5h7b1xjOR00DazJldmC9cY6cE2/wTFwBt/UbbnCM/e5gjXwnc3kcw8+x5F8nc3WXs+STHitEyZXuT1HvGPwVkwX6ej15XyGm2N4mP5s+3M0TX8nLSOfkcF77pRN9LM78XJFT88GJ9vwzNGz4HIMbbXcbEPbNAcjV7bwstXvNd17dHN9mHqis+Vpv/k5ONHNzXGELz07cfRs7JTXyUv9hqZHzxuzBW95djH9v9gMvU5+5vdROjnCz1e9lCM5c7IdT7Zc3RuZHj4bnbNtbyw/NwZj6uyHdDpk7hzLFXy2oeE1C8PfzI8ZfxTH9HnPnj7/BpzGcjp454pepp5Tkm3i4hxs+baFLtU5ydnZpPm9J3X8v/2x/69Xz0yMGlY8A0cOwi67RbjnNFbOGkK6T7tQKWCG9YNlfTZpJribvuabB/ib76/hbGXIPCrXmrdu3u3/BV+WZP7/KYZ01ue/Q+QXdf6f4ft5nE6KwbIrvPHSm6w9XhIr7ToNL4k65crqM4pnv/wMK6f0IV79RmnL/P47mmX9eyH/HcT+/4JIFn3+21Q6DBNtq2D7+2/yk0/O0uT1ufK9/zbqvzXqoevovvjEp3zvlbWceRD0QKOP2Nrr+De+zGbFDIXrS9n24bv8ZMM5WkyH3kDHaJuH/04TLw821Mj5/Vv451e2cLPN4LiIjLn5heZBQ2sFW997h1c3n6fZQOjNaceIefpf1ySfIdZSepX3Xn2bd/bfifmjhPvv8etANei/0DowI/Uc2bqWH7z9Kfc9naP/UucOIpGGe2x6fx1rD9ygMdpBrq2Uj372Cm8eKot1dMDGHn7x+3+dv7e0QE0dBMMWPiU01avYNg9bwLyVDYBj8bDv0XHVgtpgWZ9rnTT8ohcn/IB+DR2/DxWVeHTMc8APyqUerhmzbDwZvH4H/AZXOGY9mfaoFcyY32fh62jm2RaAY3ipPdqvR7DQgYOlJA6WbcXuiV1GTtW7WB0wRtfYyOe/zV98ROWbn2+u1qzL5/uimL+AMZ7h6nS48/5Rav8qLTPnar9I67M17PI5PMF+RjOK4eXNjYP0dHXAHMG8Mff6bFf6RXGJaq6jmL/CrqlFB/ufUfhfe2eJn45yw80c3fgu//jhcVrMnzmIiav23/1oMvR5CBZT1aXi0lF+/PK77L7W6I1FtHbNTSdsILMXy599kheXjyJXdvDGzFdHi9EJc+fUfsWnNZy6H8v5D/8K04OLye7d/h/+cjv0a6m4xZrXX+fV3Xc9iaId/d7D//CX5WH8u+aBGKyH8O/++p/B+XcT//8BaLzfoEe4f/E4b731MUfumTz535P3s3E35kAE7x3j7/7xfY49aDMEtf7/fdb0gP+nvlytb4MY5KZy/A9e/pCTHamCf2OOOiUqOf0p//CTjznf8X/a1OlT/I9eHbpTf5sPfv427x64qYpDRBQTO3npKfbpWJBWIJcZK1fyjWcWMq5HqsYMpOzZWsrm997j55vPddQZJjZp+H/402mXMHdO7+FHL63ldI0h4vLfXiItiksf8MMPDlLhhQDxN6IZ1EeaK5297uYHvP/yy7x3ojw2qv7Yzf+935YlO3eIZ/Qwt+GGCnate48frjmFF0Glh6efGfxl67DAZ3YzHf9T9um0a2sFn67/kJ9+dAS9w1dC/Nf9zPD57zbNpz7/KpjYqd+l5s4lXv/5W3x8xlsEfD6vCeR/8mNZ1r8RZf4NgjGBaLx7jldffof1pzuClRbl/5A9RUcfMXEfxr8DH7zFjzaciu0VNPg/RE+U/td9OmUK8+D8Xr7/gzWcrTd/PqVp/p8U6hfRLMv6N8U1a/oX4f9N4P/GgGX92zz+G2je0P8K/h6hh1+dNxaWZXU+/K/7lb94xEJtXNy3kb9/ZTu322NaxL690V9+/dICv7TA/yUWsP8j5HDNP6uIRImqmI3o1wQD17uPeBvpzzbhCCbWZ/45n/lTfNct4ZW/+Tt+dqSOiI4vK66dYdeRm9pchwiFosKPeiqE7hzhz/78JU7WRLxntB2PtDxg3boTJPQdQo9UR7TdRwKfi/nnHOFIjF84rF/JZGQMm39u0yGnIeaag4YOuM5NcdToZHDU3wlvYA1fQzei/shDGjFenX3qFqjL1c2v8ccv7aNeT7gRyReVPpJDAMrlphdXMsXwNN4ZYDXiqiGL7Hntn/irD8/HnkKyyedkikG55qRC9jByuq7L3U/f5g++v8Xja/7ZXuhzOJ3zFCGs/nA4RqN4/4d853ubqQkbVq7kihAMRbDSsvFV3+Tk1UpxkPayS6e8n9nKxcyngRd7AenZs0+ER23k4Rl9wyEi+g3f3Msf/flrnG4yPKXt5+Yh6vEzI2bOPFzR7LSb6X/YPofnet2u5DT6GbzwI/P9sL+DkLF/SOPGR4+//2P+7M1jOuYRCc2RwTUtrHtPL3VLOULBMFEnhQynnpOnb8U2YcL6zC/MXHrAAndlg0isdfDsGHn4Y+ZIqpGVl8mDiye5UeVNAlHxjWjAtE5UI695Ni3a0WnJUhHpQHwaRVRy+PQd2g31R+0iWjHLmIHPmtsJo/kw8uNLoEeezeUTFyk1+/vPQL27z/hHCUVEMS6dLPE8euo2rZLV2DrSIZdB+Aw+4s251yeeYfme8Q0jt7nvRPk8vOgbBDVXExAWfV9aPqkNtzl8uUK9+khuY4tYiyIwdT76EQ1TC2kgIhuYkc94RCWTxu0kirJdLhy7TOw9lis60diciWdEPASl+ahm/ZtbaSnsRl6glZpmiIbbOLFzB2ebc5k4IF3kXaIP9YtojYXVIlrJGopU8PY/fJ+XD5XqQR/JFBXtmOyC6TCCK/zP+qSTQH/x06qzk4YGl1CwmdJrh/nJTz/gUlMYc4BsYk17cz3HN7/LzzYcpl6HeeafKBkdPDqGvmyS2HaH937y1/zuH/85f/pfvsvv/dE/sPN2EynS7fC6H/Fb3/kz/uy//iW/+52/4O0D1wkGwO/osODMbt58+x3efedn/OCdDVzVIXiSxuqKz/DhOzrc0Cbjx6+8yr7iCPE6hI6p5cpLwbFK+eAf/5Lf+7Pv8id/+Zf8yXf/hN/+83/mcGUDp9b9mN/5zh/zx//lL/njv/gLfv+P/5TXj9eQLLtt++g1fv7B+7zxxs94c8c5GqRIvNXM6V0f8NM33+Gd91/npQ8/pbglit+WzR4qK0AL4pJsvH+SrjfS5q10ivnn6UmWdwie+Gif+tP07LfBCdh03neSM7+BBBsPX3Dm19BNirOwdaCeZPpMEx0zlhxvIWeST1j444VnxtTiBetq/g09f5xNqvrEEk0NtmORnOygF+fEYCwCgjFyG7hE8QopbtcpsYTC/Devz3z9Ef/qwDDrwdCPmLWoeBkKxmBOr3uFP3r5EJYTT26gRTHuCk0aC3twRmKpJL81z1JB1FwMDZPPURw8+eHL/OkbR2nTCKLrSinDK6Uwn/qbpzkfe8ujoSgP/Vxr0xWxiOJY2NwbXLVH5TdrBRzyCrIpuXSa6x1xUmiyk4DlYXd3vct3/nkbVXp0I2Ed2EsnreFwB11DI+I9RzQnAhJORHBh9UXUwtIx5q/SxDxLFg+qQ18z5grH9EU1ZqzhevrF+EQEZ8ZizcX8pwp8KbkkNN7kyOXP1r3hFWvRDtljGP/at7GB6a87s53f+f3/wpqrISwxNuHX9CN5TOyO0YvoST3Vp/nL7/6QHfcFIflMbg5LXs++RkcpYnDCHXbxaLllvPZ3f8fLh73jls/NT9QzcpSwcCOyy79GJyKa4uZ9jMwenIF9pN8b/BdfLhFDV7YzMsXouOLfYVPJbUjYkqH68iE+2lNML/P/B9FYzc5Xf8xfr78kr4OoqY8EG5FOEf1GS2SDP/8hBzoSmdCJtFewYc1h7O6D6ZsVICrCtl5qPSpSVD7bKXtY8gsEV33hcDRmW9nT2MH0G7zPz78mxnT+QjP4kYhNbpdU7p09yw0TwEJX+a/f/R7rr4c86IgE9PQXT1NPaYmTlZXE/cvnuFETlI3Csr9k6GQh+E45I9LX7ej/V+WxLDx5k7JJa7zDkQslhMXH+GfM3kYE2VxzEKMZFbyLuQ8pyITCZi6ieHoHMimIlnLo9G1ajV1k989oIFtFPTyD+1m/y2d2jSLRxdDCMo5s1nT3eO6cPMetuigRzV+ow9YCwn0oU0T8o+qKIzs1yNljV6j24lKIsGA0gNHd6BXRc1g2iUbaOL5zJ9esXkzqq/ysOtyMRaR7ROOeyTrm0zwb3p6O3oCh+GhzH/HJCJ49O4YfldHQd6WgoSUWHoSZ13DHQ+d9VLYzPB8+Kw6Z50j5cb773R+zt8ygiqdHSz4Sl0I3uxqv1vKGNObpESGi30flMcOdzZUtzLhpHoxHL0JY8kQ0r/+6vjHaYS8wmDkVvGCNWYwvG5yIeIY1V2HRMf2d/B7+Gj6CiXQ0j7cGH5Un0tFp+sKib2DDhqaZf0NUNISCFJS9TYeRJarHiFpUfRoVTMTgdMgR7bCrmQNLw2h+H+VjeHitg7cB+cUWMbL4UihMaODE8Ws0Yq5OvjHeMWlMf6w9tEsHXVd2D0n3qPifWfsKf/LaYVoNqOSLasyTwRt3FVcsUvK6EH5wkZO3lNgFZ+Q3vhGDiwompnvYyCa88CM6C1yDrmxiZItgxrypa7rC3//FP7L+RqxQCABZERUAABAASURBVMtGEeF6rUNOD/eRL8PXyB3IzsVfdpXjN2q9UVdye3gG/9/AjXp6RT05ooo5+sj8WrNad+Und7D1ZoQRo3oTL20ij9Dz5H2EZoxOJEbHGNrMscc3GluDHqzrjcdkisZ8wZO088v1YMOylwcunjF7Sh4s/S+qutwhKyfAvXMXuFEXJGJsKnixixHRTYy+kSWKHr1+V3P6sN8Q14ClETcaxtRBbu1l/k65Z/MdrwrCFUwnfFi+bVmW+BsEl85+s/Y9VWVDI7N5Dht5NGdR9Rm4sGQzvx5cyXH+/D//lP1V5slM/yO0xE/UH+pvcMKiFfGcwsAi/gY+ikRHPR6sgYsYfjGS6v/s42q+wmZMsphfA+LqPmLmRS1sEpbA7x9ey3f+biPm5aArm5uPoWngDbNO+IhwBY6hEZZeUT2bMSN6VLzMvWnRmIBIYKI6OwhZDl0y4MrpS5S04V2duAY+Ygh4vb/8+qUFfmmB/5MWsP8jmFu2o4MPG9u2cRxbccLF8u4dPTvYlmViECaO2Lbj9Tk+vw4KLCwrkwnTJjOySwL++BTy83NUdGcSF9C43yfYGK4/txezZ42mKMH2VLIsS2PxTH3xN/j1Rf1J0wbdti0+uyxsx8Gn5qj5dDLj2Da2ms/nU7+Nhbkkq2WLlhNrouGq27YdPBwPNwavbn0sbKcD1rE7aHy+T92gkcLBY5g9ujsJ6NIhsS3ejodryyamD6yHfQ62ZfH5y2HAuIlMGZLndTt+2cToIRoxHSwv6VpWmHOHLtAkvSzLIqf/SOZO6ufxtQTr/xyOeHfwNPr5dBBiiOf0Hcb8SX1JcMyTi207BPwO5v/wrFe3bJL8NuayHDMnzi/Yqp2zR68SFrzZUJjCw3E6YBwby0y87OH1Gd4+P45t48vrzewZIymMQ5eLY9l4MAZX497eRJNh676z37YE+rmPAD6HZ3l+ZtkORj9HtGK2siUHPOzvIGSJtt/nw7Yseo4cx8wRXbFROnY+k8Wnew2rVx/d+APyB38CPbsXkp0YkGaCx8HpsLMjnrZl+mLw5tlrplNdj36MaWxHuLJrUtdudM9JlW8aZBffL9ITsCV5PVrCsTvouUZ/6eCLS6RPtzwydBooCriWI1qGtmm25JStHmWOi2VwRauTJpaPlKxMstMT8X0ONvbwKH+/DspcO0B2dibpaalkio4vEMBnx+bAWOBReMe2NTcxnjHdZGMjt89BKB4DSzCdsjidnUZO9ftEP5CYRu+u2SSb0zlhuPYv6Og5jQbMR0WiuGHV39CBdQ2OY0ukKL/Iw7V8ZGZnk5mSQGw5uDi2ZBM/xzTJ4VoWdnsJF+5GGDh1NJPHDaVbEti+eHqOWciff2sZfTLjtR4FZzsP587XoZ+FLiedUVMnMqprih4kimth2zaO4WGa+KgXy3q0z5bfujx6yQ1objE9UWqKb3L25HEu3i3XCwCQmNihBm5cOsHxC5e4U96EyPGLl2UE0uG1mzOcJ556li8+/Qxfee5xRhYmQ3sjbQkFzFr6LF955im+/MIzzBrcxYsN1Zf28M6ntxmy+ClefGIlMwcXIjND3WXe/eBTEsYu5Uurn2b5lAH4VQg+KrlhGY2GCOSP5PnnnuPFZ5/li4/PoV92Hl0VBJr9XVmx6hm+/OwzfOP5ZQzJ78qggRmc2/sRu+9nsOyJx/n6svFUHljDnrst1N3bz4f7Shm36El+5aml5Jbs5cO9l4kmymYqZj2dDdNIO3cunOfgsQscOX6ew8cvcOToOQ6ff0BdQxXnj5/T2HkOH1U7cpa9x69ToYPspvvX2HdC901RTPiTOvjsEA+uX+Pg4XMcPiZ44ezX/dniOlrqKzh19LRoXdDYBQ4dPs2pWzVEtU7i7DDlt29w7MQFjpy6zp2advx+Cz8hKm5dZs/x296/pJC70FpTyolj57hdG8b7q3ArSMntmxyV7EdPX+duVTtx8ZYON/H84FEbezo/8vUvfF1jVrvon7tLxJGd5AiO1p9jO/gDDo58sOuQMcwZ1QUsh/TMbLLSk0lXYvAJrnNtG7rm2bLQZWFo+IULNj1GjGPG8CLphi4L27Zx5N8J6d3omZ9BnPRGa9r2i5/6zZjj2FiAIwP4Ou71iNWB63hwttAsEtIzyc1IJuDjFy6LvAEjmD2uD4kasRRTA5LZ4Po66Nod9Izsnrji6gjO5zjE4BxMv7Gp7Tj4HFuU8OToxLFo5tK5S1S7trA1ZsX0M/iObUtGdLkxHL+fQEKyYlYeaQkxgV3BeLCi74i+yZlC+Dc+rg5XRZN6LlQn0NPfzOFDpwkrzsXyqmFnYTuOJ7+jXwuw0rsrt46jdxpIEAJ+x9PFMXaQTWzb8nA67WKhy8pg3JQpjDWBzcyPZDP0TLMtQVg2xgbm+V+j49iCQZcClGU/ahP1q08j/8bHwpFMjm1jO9JDdFwTG82912xEASRDY/EdqnxFTBo5lvGD8ug/ZhyT++dgA7bfj9+xRcsnW9jYmd2YOWsc3VN8GgVLdB3N9dgv/Bq/tWIIGXohZKuPRy7XFR3bwfH4Ovj0a0AsW/c+G0uwlmVj7GD6Pevr2RGc10ynaPDI5T7UxUeKYlq3rDT8ZtxfwPTpkxiY45gnNQvbcTzePtVIPjGLS80gNzOZ1KRkHM2dX2vUUj/m0o3TAe9I704/MvI5nf1GHgPb2XxJ5OWkk5KURLxgfHFx+DphjJy27fF3RM/26NuKUz78vk65HMkYR05WOmnJSaTaDr64R2m4WA9pODi2ZUwEWNiCdcTTcWwsdfPwskjISFecSSFZLwod6R7nF4x80IA8Ss/ns9XrkJ6TRVZaGhlJjuYizpt3XPG2bMycObbdIXOAvtNX8sdfmU23tDiwHBxPBvNro0pFfbZoOOqX7j6fd2/E5hcuUcd2DFysGRjXwOjrURkd28ayLBzRkqqYy3YkZ8eD7cTubdvxeNpOx7Pj856d7O7Mnj6WnmkGEzzasokvLoHe3fPJTA5gYcwqeYXrdLSH8mjs4ecXZDP/LVcsC0fz6XOEb37VDC6fuyxPV59yF7os28Hnc7AeuXfE1ycdDR3Tr6HPfVzDRzAGzjSPh5kj2cc8e02d6sJSn6Fv+jyammfPny2LsjtX9dIygq1789LPEqyBcyT/Z/r46JTDtmN2tSyLtrtXOHOnHseWhDoE/QxXMF6fDPQ5qWVX4Xmy+GXvngVkJsVhC0ZVoWcDx9PJli0+j2vZTmzc0BW8JTn9grUtm27DxmL2FgH1I0xbY47GvCZ4YwN/Qiq5ig1JARtzudjYjoMH49jCAkt4nmzq9+zU0Y+5LKsD1sjhw7GA5ELFmIkMynb0gGejGD0HR3y9zke/JIjhYeSOS8qnV1EmCX7bg7C8uXbw8IUrUK//4ZfMYUs+x7E9GDvaTHVDi5e/LFt4BeP40++8wKSuaTjSxnF8kkf9Hbo4hqYh9pBObEzd0Kmb6NuO+tUZxcIx916zUZdiA49clmc/Yy8zhuBtR7iOzWeXRXxaOrmZKSQmJeJIR7/fQexiILpxPBxHvGz1SziNWJatZyfWDHHBNV47x4XyIH49Wyn5TJ0xjr4Zsdzjqq+Tjk++Ha6vo0EvzxBeZ78juZTWsaSjz2do2/InHz712+pzJEes38FcluLEjJlj6J5kJlpyWVZMHsE54oe6bMcRDUPL/Po0bsnBEdsQ14+dpsqxdQ8R18J2HI2bZtOBzqOXZTsPZTEyWWJgdcjlCFemUw9k9xrqnSUkSnUrUs3p8zdoNXw0GvmcjHZMFtv2ZLT1a+gY3rbtdMjiYAsHXS4Wts+vuO4nrXsXCjI1X+hywbFtHMngNVuSqU8jv/z80gL/YRb4JaN/aQH7X3b9r+uJrXGXG0d38O7Ok5w7fYJ1nxykXMGs9vpJ1q7bzPsf7+NieZtCB1hWhNunDrD2kx2sWbuVHWfuUVtXyYPKBsz/iRR6HxnVaUbl5UN88MFafvr2Vs5WRLCAqnvFVNSFCUXCetKnsZR9+89x6ehO1u08S3VIffq4nVkxXMOBzVt5f+NuNn6yjn9+eR27z9zhytmDvPqzN3lt0ykqgkIQ9baKG2z+eDPvr9vB4RuN6oly8cBW3lz/KVs2beUnL73F+qO3aTfgbjPnDu7hw4+389Gmo9wzncFKDu7aJb228+GWoxQ3GcA6Lt8spzkUieEpQN6/cJSP1m/mw82HuVUr60mxkgsnWLdxO++v38u5Bx4irg5PNARNpdwoaaS9PSKCIc7s2ck7a3ayefsWfviTd1l35J6Cc5S7Jzbx49fX8NonR7lVWktJaTX1jSGFeyg5f4S3393Klt07+Nkrb/HW1nPcun2ZjWs+4AevbuSk99dBUe7eKaFCk+CGxQqb5gfXWKc52vLpXg5cqSSqhGhkKj1/SLpL1w3bOXCtXrYKcmXven785nre2H6C4kYLu7mEHVt2sOGTbazffZEmJRC3rZwdazex++x19m3fza7TN7hdUU1jfSvtHk9t6x9cYZN8Y8OGrWw5dpegYSifKb54jDUbtmku93K245WnOQzCm2uLSN1dtm/awgfrtrH3UjVix035xZvrdrJly3ZeeekNPth3lXbRu3v0U155ayN7r1TJ26Dy2kk+ko9eu1fK3YomWlrbMeJY7RXs3baLj+VDH2w9QUlzFO9qLuPTjVtYv+MgW49ep1m+juhabhNn9+2WD+xkjdbA9ZqI6SbaVMKurTsk/1bWH75FUNPu0ZHs5tayZPsz+zX/u9mxeT/X5Bde4Sglyq+dYp385QPJd0mFjWVZVN88y8cbtwn+U47eqPFIWVoLuz7ZxJrtB/n4WDFtHaJaWgOHP93Fh+s3sfHQDVo8ifDMJvbCtQjVyHafbJHvb2PPhTL16RMMEdabbiOfnryP6xlc/nThGOs1Rx9p/vdervYomqHGkqu89/En/PTn69h/tQqJKjyLhvuX2LhxBxs+3sb2k/fVb/ousvaDjWzceYD33nibn7y/hzuNMW41N88JfhtrBb/j1H28JYpFUGv0Y9nik1372XWhCsRAZsdqKWPv9h2s37SDdTvOUOk5TUxHzNVezc731/DK22tYf+AKLZZNjey6VrTe/3gvF8raMHSC3l9xRXWAbJB8ND64ysaPt8rOOzh6p0UrolUx4Qx368vY+v5uTtyU7oqu144dYP/ZS6zbsJuzWku2/PXa0U959aNdbN+2jR+/9Dbv7rupIlzRre4BxaVNRPR233Cx7DA3Tx+Sz+yW725h26l78kmLttIrfCIbG5ttOXaHsHQ18J1NonqHj1FNYu6AYayYM4mCREuy2xjQkJPOmOmzWTimGwETBaSgPp5RTHz0mumXVImZPRg2uBdDh/Rh2NDuZCWA+SsRJymXAQP7Mlz9w4f3okgDPi3U08dOkTRwGpN7O0QTMhg1dizDC+Dykf1UpA1n/phULNtHzxETmdbLR2vIxRIvj6fkda18ps2bw9A+PenTvwda5tocAAAQAElEQVRJwTpShkyhtwrLwZNmM37YQHr370me24DbfTQj8ixJCT5fHPHJPhITHBzbh8+ysC1HBXEcAVW6CdqU246erQBih5jKz13v15IPfPLyf+Z3/vy7/MXf/pXX/uQv/5A/+vEGbtw7wSt/91/47j/8Lf/1n77H337/r/jTH77FxSqX+/te4jvfe5nTpRFtwlzlHkgINHBw/St89y/+kr/+p38Qzt/x3b/9G17fd4PqWwf5p7/6E/78e3/HX//j3/Ln//UvePnT62D+Cm/Lz/jzv/ouf/Y3/4X/rP7//Pc/ZNvlWhLtds5s+Qm/992/5p0Dt/GnQs3lnfzj33yXT25FSaGBQ5+8yl8Y3L/9a/70r/6C737/R2y+VEsgDuUGF6ULfvEy9jZ9lVePsX7jTtav386nF6uw3HZOfbyBn77xEe/vOEdx2TU2vL+Doxcvs2vnbvacuktFdQ01rSGDTlixJFR/hw/XbOKlVz9i4+lSZHrqrx/njTV7uVMvgOa7bPxoE7suVwqngRt3qjF/MRnUE8p7ldeOK77sYNvOXZwtC+JYZsCitfQ6mz/Zrtiwlc1HbhEyhDWkWaMz3lRfP+vFOxMPdp6+r7UggHCQkIlPBlCPyK88VPnM1TtVsknI85kHiqtvf7iZTTt287OX3+ad7We5efs6mz74gB+8vonTZZK9tYKdG9byzsa9rF2zgZ++s41L1VEsRZ4zuzby/r6bugPzl7dvrtnH7YZW7h3coTj3gfQ/wr2WIHXKWx8rHq7fuJUtx4ulhxHKIlR9g08Uez/edYi9sk3E5AoNOW1VHNi1U/lxu2L2KSpMYlJ/55zp9rOPgqtMSPjBXRoSc1iwchqt509yqcnCVu4IA1akidMH9vDB+q2s3XGCMnXW3L5HWVML3h8lVV9nw9r1rN1+gA/ffocfvLuDM7cecGLXFn708vuqI4pFC1qryrhf3UCL1qyoUnXjLBu8XLOLE3ebaa24zJoPN7Bh+37efeMtfix7XBSfQ1s28oNXPmLL2XK8S5PRePcCG03cV97efuoBUfV5Y49+ad7Mo9t0j81rtrHv9BUO7d3BJycqsawWjqlm+UBx+pP9V2kRfrT6NvvP3eH+nTN8sP00D6oecKekXvE0SlSL/vxu+dLu05zSC7n1W05ws7SE8poQoXDIsFGuqOTg3nNcOrGHtdtOodDv9T9qd0vx++bx/Yr7u9m+cy8ffbKfmzVB7p3ayxvrT1AvjIZ7l/jog80cLzEebvz4GptVD5r68citBjD+3aEbuizN0+0TB7QGdrFt6wFuNQS1dl2a7z+gvLGZoCEjOIcWTu/bxZpNn/L+mk/Ye7tZva7Wd7tqvP189Na7vLT+OGWt6tYn2lzBvh3blT92sH7XWapChjE0K38Y269R/b3ncpVsI2DZx3wrGMknXNqqbrFtxyb++aV1HLpptNKoFeLmiYOqF7by0baTlHh8RLOphD1bt/Oxaql3NOc369pxHZtgXQm79m7nBz/5gO3nK0RAH81TxZWTrP94pxdz9om/Zw9auXh0v+qgbby/9She3BC4yWH6kVgR3EiQW6eO88Frb/KzDccp78jl988dZb186aMNO9h/pdojF4m4hGrvsEk13ys/f4+NJ4sxhxsNN0/xvngfvXiNHYotx2/d48Khw2w5eZ+wGEUaHrB701bWbd6u9XCGGlFruneej97fyMZdB3j39bf46Yf7KG4RsD7eNOrL1b0lHc4d2su6T3by0aYD3KgJC9sboFw1xfqNmrsNW/j48A2qqkvYtnYdn5r/jlm0iVOfbtGL22uqzVo5tv1j1h6+wqkD+9i89wQH9Lv20/NcObaPNfLPy5cqqGtrpb1NtMXBbqlgz+bNmpODrD9yB/P/JRUbaebsgT2s27SLjzYe4IZqR80WJh7qKzbjlsuDS8dZs34L78uXbzRYRGtuqLaPxYM177zLD9/exuXqkCEpVKOpbvUS+9jW9aw5cp+oHkvO7uP19YcpbQ1z5+SnvLF2R0dt/aZq6yuqgwUkjgZb5jIPWG2VqqG3Sb6dvLdhp+KtHF3+UX71NKaGXKv91j5TQ0roWuWIN97fypYdu3nj1bf4qfzc1HLB0vO8pedXVC8eulmLLb97cP4wHykufLjpMHcVB1Ws8ckHH/LGltNUNQe1H5U9dp6jvLqUte99xE9U729WXI6qXmi6d5mNm7arXtvOdtWXxmc8W0nwzjhgtdeyf+tWxYgDrN9/XbpZmgXUGjm5ZxcfiPe6PZeoj1qYS6j6iWjt7OaVNzex+1KlnqH25mntlfZy+UEpt+7XEwqFaTcjCugV12UD+ek6+fQ+rRGdmWkkIpgICvm6Ry+pmjmz/1OP39rdF2gUo6orR3ntg21s3bHL28e+8ol82CMqFNU5B3fsZL384b11n3KuvIXmkhJK6ptpaxey5uf2yYOsU2350fodHO34S2tNupBlBQOi+Wm+d5GP1mkudn3KwRsNsnlMz1DFTbZ8slW16lb2XalBoMIzSK4o61a+dv/8Ma2tvazfsIEfvbaNC5URLClXfes8R86cVT7ZrXoqLGYNHNi2mTc37GXzxo/5x5c+ZLvqeo+Tvh5on7FGdn7/k4PcrBeP2ls6T9jBobNXPbwdV5uxo/Uc2bXT02ft1uPcU7gUqtTpkCdcz+Ftm/jZ+7s4V9IEoToOf7qddXuuEtvOdcBFjM2D3Du9n/feeoeX1h6jvN14vXRqLueA9hcbFEvW7TjzMFc3lV5VftsmP9zGTtVV4do7vKma6qdvb+HAxRKqK8uprGkhbAookbGDNRzd/Skfb9/NB+99yMtbL9KiiW6vus121Q3rNm1jw57LNEuBKtVs733wieLRXl772du89slxrt+9xfZ1a/nhz9dz8FYzlixeXlxKbUO78puR1cJSnN69ZSsfaN++60wZWGHO7d7Ku+s/ZfOmrfz4p2/zsbcO4O7hrfzgtXW8tu4Yt2vbVZO1xXKPYtsaratb9VHx0DRpMbvockNcP7qbd7ed4frZw3yw5TiVEYs6nfOsUQz+QHvUy9WucFyK7z5Q7I7ghIJc1Rz/9LUPefvjE5QoPzmtFezbtoMP125h64l7itlQdvEoa+QHp69cYt2aPdyob9Fa2sMaE08/3kPnnt/E3/P7d/D2J/vYsf0MpU2S0bIQU+7LX9Yb2Tfs4ujNBky3HEFW4pfXLy3wSwv8H7KA/b+Vb8fONz5Sx9YP13NGG3WrrZpLJ0/y+oazZA0ZxMCkMl5/azPlYXhw4EN+suM+XXT40C8/lQcXrhOJj+fark1sOd8gUR3MxtNJymDgwP70dG/zo++/yaU2bfz99WxRUjxbZ1Sq0cHVGo7XJdOtR3e4uYcfvHeUZlFQyIwlcDsBX+VF1uy6Skqf/vQN3NFB5Iecbclj2IAsTm16nw+O1Qj8Dq++vIW6osGM6uWy8a2POKMskBwsZ9O2E1DYm1FdYc0bazhVGaLu7C4+OtbAiGH9KWwt5W6rSNSWU0qmDmsGYF3fz+ubLoL5R0aVZ3h33UlPrqbL23n14+t6Ez6Y3m0XePnDo1SVnOO1zRcoGDKQnqkWFWWVXrGnPI7rikQgmaZLB3jv0yt6sElvvMH6rWewevRmdGEzH7/5LvurLNIcl3AghR49CklPjicgvd/bcIhKYaXENbB7yx5uUMjwgRmc3vAWr+6vpfeAgcSXHOUHb+9WUWSRUHeD99bvoVbxnOor/PCVj6lN607vHl3JTY7DxcYiQnV1I8n9+jM8q5oP3viEG65DljZtYb2979m7QLKU8PbLH3Ahmk+/Pr1ou7CV7793DsuXRNWVg7z7yQUaQ1HKH9QQF1+rwnErF2VDai7yg1d3Up/Vk3798rm27T3eOF5N24PTvK1knT9YB1O5LiV36qSVPjrEU/6DyAPe+9nH3EsbxKhBiez94CP2V0RJ9TWyfcshmjJ6MaJfEtve/ZAdt9tIy3fZp2L4UhXSyKWtuZ7b12tJTU0mfO0Q72y7QFjkI01llAVzdTg3gOYz23l99331NvLhz97lUG0q/Xt2oSg7RX2ynb7PfvIhH55vpX+fHhS5N3hJLzzuafd/dP0HHG/tysghhdSV19DesXuIypqW8B4c38gPNlwju193enbLJtEXxvIHoPI4P3/vJKnDBjM8/i6vvrOXMhWAb68/RlyvQQzOT6C6pELz0szaV97maH02A3p1oSgzAe135X1w5P0P2FWayshh/Sg/8DHv7i/BXJa8zEvQbWV89M5G7iX2YdTwdPZ/9D7b7ggiztKm3tVN58cVH0mrw6s7D5rp1n8AQzKbWfO+7K65i/dFIS6F3v36MqoozLsvvcGOO0FoOMcPfr6D5nzNad8cLm5+h9cOVgrUx9mdOzhakcygof2oPbWFn3xyVcxUvOhAIbNLX4b3jGPHmo84Xqbu9lv89CfrKYnvSR/5Y16qX2tc8shzN772DofrMuVrPfHf2c8/vXlE23jhuFHB2OAk4Fo2geQ8+nXNwr19lJfWnSF7yCAGJpfz5lubeBAB88eJZjOss01ousYbb2yntXAgo3qF2Pja+5yq95GTn0GCP5EuvXtQkJHIrT0f8dreEnp0K6JPag3vqsA73WKTGS5ny7ajNOX2En6A3W++z9Y77VjxcVzdv431JyslINz4dA0/33WPrn27MyjbT5k2CxXVt/jZzzdTndlT66CQ27s+5Oe773nwnf+0TGdvmh+wLEtxwqW5pVW6ErtcsGyLSHuUlvYQesRc5tfxWyTpoDop0SE+YGE7AZrvHuSdd9fw8qtv86ZeTtW2g0/+59ZeY9Paj3TQ9i6vfLif21p2VqSe4so6HV7c4OM3P+CfNM8f7btFmwX3H1TSHqzigA7xfvjKq7y64SRlomX4JHg8bRLkVxYB0tP9tJk3MTp42n8+wqih+YTabDLSEwhr3nzaOOw/WU73Xl3RcmDQlMcY5juvXPCqDofXExk8T4fbCaR3n8K8IYrNP32Z75qDYnsIK2f1wdEai1q2Zx+5Om58Dqt+8/u89U9/yLz+heR2n8Tv/e2PeOU/raa3+Ut/XxrjF3+Zv/2Hv+F7f/WPvPLdbzOpq0PYSSYnM404H7IzWJhfm/iEJHKKRvJr//lv+eFf/y0/+rt/4A9WDsWn/yWnZDPrhT/lpe//Ey//8CX+85OjaLq1l9fe3Yc99An+6m/+jn/63S/SveUUP3t3O/e1MUxIzSI/uZ0DG95hxzVISUkmOTWD9FSH0itHeH/tTuE+xd/9vXB/71ny6s7ywfubMEvMlu92pGFJ99nH8m5dKotrSBs8gGG5LWzQWr8acYjX/Fv+LHr2zCUtyeHuse28t/emNmWtOnyOEL1/mre2nEKrGsuS51jxWvN9GNkvkX1vvsJbCthxcVEO79rFhUqN+5OouXBUhzwPxDWOtttHeGfTeRQBaLm2i++9eZRAj5706plHil/0rQCEbvDSK59QltaL/m0AbwAAEABJREFUfv26cm/vGl7ecUf4YGoAF6NBmOLSBnK69WN4Nx9b16zlRIVA4mwPRnexjxVbBxCHv/Yi76w7gsI7KalN7FFOu0VX5b5Ujq1/hzcPVtFn4EDii4/wozc/pc2XSP2lY+y41EL/Af0oar/C97//EfdxoPIamz69gKlMEgJtHPl0D5cbHc2NjzCp9OpbRKo/TEVtLYGsAQwfnMg+5Z/dDyRW6CY/+dFabvm70a9HF7KSfVqzoqlq4OPX32VvVTL9+vYiseQwP3h9r3rBsozOfO6KIj/WKr5ZEiIzPYfeoyYwMK6co2cqQT5uKF7bs4Vtl8KMUk2SGa2kWDZKyGhlr1746vwIEtK4d2Iv26XjQNUazvW9fP+13QQLejMovYGP3/iQfcLxJfi5/OkmNl/WwtX6f239cZJ6K07mxVN5vwRfYho3D+5i710YrLjddHIz//z+MQI9+9LbX8J7r67hrFApP8X339gn+j01t5mc/+Rd3tShMro6D3l0C1I36gKBOBov7+P1zeeo1+FM/YPbnPh4LTuLk5S7BlF7fAtv7L6LlZBOfkoiialZ9O6VT0pSApXn9/H+pzdkIfAHb/LBmzu51mzTqNwYsZvZr8390XItEhrZ8NaH7C8P0L17N+JKDvHDt/ZSh7lcxU8jCBQf3MCPt92m0ORj1QqHNu/gbJ1DaugOG7ccozQKcak2F/Z8yuEHUqDlJi+LZ3PXAYzqbrPx3XWcqgjj6eYpB/cPb+BHm25S0L8HvbrlEu+ziIQtAikRjm78hD23QkCUQ++/wUeXwwxQDTEos5kLt5vU7yBgIil5DBjShTs71/PukQfqb2Pjq+9yrC5NNUdPnNt7+ec3j8mP6tjwxjbqCgYyrLef0rJ62o36woh9LHlUlPZogB49+9AlcomfK9eVAFXHNvLzPRUMGTaIjAeH+dGa8+qt5M2X3udMWzZ9e/cipbGW+zU1EPARitjkdevFoJQS3n1tHRdaIXxzN9//4DRJfXrQr3eA3W+/wZb7EWpObeO94/UMGz6AVF8bVaXNoq1PzOx49oqE5GO5DBzWleufruWtvXJKN8St+810VwwbnFqnuPcJ92SueJ9LxBdPF62hEQMzOPTOa7yh4BCXkcL5LWv45GwNTmOVDpKgWgc6nxwpNiyo01pts9IZPrg/ZUfW87b0DaT6OLNjByeqUhk8rK/ssJmXNisQYy7jGwg3xMkN7/H+mWat2x50de/w8strdagCTVd28cP39WK2Vw8GdM+k8c41KuNSKD2+j10XZSvFu7iaq2zccVq50ke09CLvr99LVWOQhoYWGspu8cG6XdxpbaW2soGI26QadXMs1lHPOh1C7a9M8+q7LplJCECRDs6tf5cPzrVp/nvQzb3NT19aj8oMCWvRmRNqruzj1Y1X6KI57efc5PVXd1Adn0nF8T3suh5lwODBRK/t58cfnVRMA7OQosZv7Tia75xn0/5rqrIgxapj7/Z93ArapFn17Nh6iMb0WG29XYe8O642CtnSS4QolqVbt5a1P3uLQ/UZkq8nRXYZF0pdIrcO88P3FFeUD/r3jWf/e2+y4Xo78SkWJ3Z/yqVgNkOHd+fu9nW8c/ABTnImBCPys+50yYin6vwuXtt0i+6qi3u1X9CL1N3UB1IoCBfrMPAQlZFmLl8uxU7OIknxDK2rpKwi+hakQcl5fvL6LtqyetG/Xw5Xt7zP6wdLJKyFpbojJngrm15/nZ0P4uiv+qNrVjKO7OEX1NVPPmDDVUdxdpD2BFt5ZdtN9erjGdsmI9nl8CdbOFsVxlV3qLqSWyXVJCmfh+8e5u1NZ72c2Hpjj2xwDL/qyP69Etj/vmxwoUEYtubNpTNGnt/0Iesu4vFrO7eNn+64R2JCmMN6UXzTKmD4iEKubPyID05WCLeNj199kz0VCfTT2ktPbOTqhRr8yRZHPt7I3jsRcNu49aCNPkMH0DeujLff2xp7yWTh8UW/7eXn+OHPdtCS1Vt7vyKyVR+4OKJfxXuvfUxJ6iBGDU1g97vvs68kqn4L8zJIN9Rd2sVP1p0ju083esXXc/BMGTkFKdiVZ7SXOYTTvQ/Ds6r44OdruOXGkVR5jvV7r5PVty9DJM9behlmVl3L5Z28vOk6PeWz3SPXefmdT2lw0qk8uYXX99wi6rRSca+UPcq12+/5Pf9KrTnFj1/bhhfqZX1TQyAfzgrdZv3mY1TaAVzbR/O9G9wjDZXCaFqN2GoWrubQjc9jkGJs8d6PeGN/tfqbWPvmhxyqS5VNe+Hc3c8Pdc7QFGzgE71wr8wYpDiSyoPbtdjxtuhbZOR3oWtOCgmJLezfuIW9d4OiE+ag9laf3PDRr1d3grfPcL7KR368j8rKWkjMF51uXN31EevPNJKYBQc37uR8Uw7DhuVxbceHvLT1Ll379yez9gIvvb6FUk1WUnydaqItHK/UxFHFmlfXcdnXm1HDszmxYQ3b77WS6Nznk09OYXfvrTODdj564wMOl0NaaoQ2J1V5tBDzr9tOrH1XvhZiQF+t1fBVfvrTDdwKg6n/orINlk1S2wPW6nD7XFUzra1N3DlyiFc2nCNn+EAGOPf42RtbJQUkNd7m/XV7qXUc4hPAdTJUKxWQYtex6b11nGrLZeTwnjqcXsOasw0kZ7ez692N7JOtIjXV3K+oobouSI+h/enTdomfae/bBlxQ/H3zSCMD5V89CzOIs1z8ol9zXmvj48tk6VxpQEGUTW+8w6d3W8CY5bNJ5pfXLy3wSwv8x1rA/t/PziI7v5Ciwm6Mmjiex1YtJU3J9kpzOoWZ6eTnZFB36yrXS6rYfeASWSNmMG5AH4ZOmsSqBWPIjk+lW/dC0nU4YWQ1h0Cp2hgNHTqYeV9YQM/Wa3qjHCWpqCs98tOVgB2iDy5x6GaI6TPGMnhgX5bOHELFmUPcrHGxFChxRclOoFvXAroq8E4eMIgFT8ymbxJk9ujD6MmLWDw4hZraNsI3LnCyIiraqaQV5OHX4e3R2xaFXbvRtUsXho/sy7iFcxiTGeF+VRvR1gbuPqilLS2PSaseY0o6WHlDWDSuN6kqNLLSAzRV1hEhnh46EMxLTdCRC1w9cpLSQBbd0lLJyk+m4vI5rpYFaSgtoaI1mTGzpjFndE8sdDkd06bCqodsk5XsN50Udc2ni2Qa0a8fE5YtYHhyC3erLdILsklRoTZwUFcyUxLIKSyiS1aiZ4aU/Hy6FhUxbORgxkxezJzB6fiS8hk0eCjPLR5DQkMF9VjStyumuE30QfG5g1wPd2Px7IFewuyWk4hezYuew5BJUxibnaikkU18UxWV7Q45eZkkJ2Zqw1NEWvFZ9hcHWDBvKAMH9OWJmX24degYd33J9OiSQ2GP/ixePJtnloyjMCWdgvxMUkW+9NQRrkeLWDaxDwMHjmDpiFRO7jtBfXs75cVl1ITTGDN+Bgsn5UsOsGQoB4vgjUscLm6ja34KaVl5JDZe58i1FrKLukvvAoaO7MOYGfOY2sXHrXsNpHcX72nZ3L5VLDoWbQ0N9Jo9h/y0FPIKC8nVIZgpq5ysISyZ0pWE5HTy0m0aGltofXCF/bejzF0wWUmwO72KMpQEbcKttew7fIte46cxWL49Yc5M0ivPcvxGFW3NjZSWVZNWOIwvPjaGlAQbcxn5TUF4ZM9Z4gdNZvagXvTp04OsOLBsR3v4Y1yPpNI7NZX0wkyab57n3P12GstKKGv00X/CRBZNG4B16xA77/iZ/fh4BvfpzsCiVHACxEWL2XeqlJQuuaSlZVGQ0MjRU7cI89nVXH6Bo1fbyO+eTXpqHgmt9zl5uQLi/aoazCLqhLXQuQBYcUycNZ4eGclkZ2RgtTZS165ugSak5jB0QH/Gzl/KnKxa9p65z40zp7ln92T52D6a01Es1QHNgb3ncNK60Lswj54qqIaNHMvjE3vQXFkGms8Rk8cxuFs6ackZxEfbaGgO03bxCCdbCli6YCADevegd048rhMHTefYcyXCxAWjGdS/D0sWD6FRb+dPqHa0bIhoIi1fIrlZyaTmdGNQ9xz59jFut6aTn6GWk0ntzUvcELzP70hn8PkgeOUEJyvj6NY1lbTsAqyqyxy76Se3dx5J8cn0HaEiLbONPbvPkT5qDiMG9mHozNn0DF1l95k2snXg1LUwn8HD+zJ+zhwmFEYpLmtVkZpLz+65pCfGI+HZt/s8WaOmMLZ/L4ZMmc6Tc8fQeOoA50NdWT5JNhswlMdGZnB830nvZZLjytDC/PzHwvYmh89dlmVjW5bXZ7Acy6Wu/D7Hj13k+MlLXLpaTn1yX5Y9/jSPLZ7PYwtn4ruygZc2nqLOyZTcT/OFxxdqrS6hf/gsL731CQ+0EYyEwyTmdGfqvEU8MaOXXuS9za4rVUTtCHZCFiMmzeGZx6bTcnod7++/QVNtKaePXeDo8YtcuFnpbWYjwSiBZIt7J07Q1qUfPdJsbQZcwuEo/kSb2mtXKSWZXj2ySbSj3Dh9kLu+/qx6ehVfXjUDp/gkp++Hdch0jpP3fcxZ9RRfWb2c/nYxe0/eIxqQ7o/ayvKT3aWIQX26kuxA1EqkS8+ueiGRodjsYvki3D6xTQfGL/O9f/4x6w5dIZwIVjiE9y8BXD53mcdgc7E2fC/x/Z/8WBu2D7kTjsPYOBwJK1SGCAXbaW+3SIj3UXP7LMWBHsydOVUxspBBYyYxc/xQ7LsXuVIbwtLLtKjyYHzTddZrY3Olwcan+XLCzdy/d42S9m7MXzSDQV0LGDhhDnNH9iRSeYnLJeD3SR8jEL9weXNvMWjmdAanJpKWm4G/vo7yqI883adm5jCgTz5pKZl0Kcil28ChLNOcPjm3F5lZeXTJMj4qmtp8WIk5jBjSn7HKm8sHOxzad4X4zCJ65KdITsEor/XWwVpyQMYlju49CslJjZdttWnYe4yGgtGsUBzu27s3+UkWlj9A6PxxHVjlsWxqHwYOGMzKsTmcUrwvFTnbRGZLN/gYqXgwoEsa6amZxEdaqWnUovbZZvBfNiuOou6FWtuJGIjUoiK6mHU4qg9jpixlzsBkrIRc+g8dyhcXj8CuqqDFn0yPokJ6dO/BYL3kXPL8Qgprz7DvjkPX7t3JSfZLGkjMLaJ7bjJ+X4CM3CzS9IKg3+CupPkT6Td4NBMGJJGclEmq3Uh1M4TOneB4Qx6rFg9VzOpGz+wE7Lg4qL7B3istTJk5noGKWQvnjaDh/EmO10gvafT5qdSTUSRczf3SWxSL8M07tWSkBDl+6hL1UQuLKM2NNdy6W42b2YUZ8xcyrhASMgvpXZRNvCWiiTl06yId+w1myPCRvDh/OAmand6D+jHjibkMTmnjgcK+PymbHl3ySY/XPEbbvUPniuYAgyZOZuHkvviTM+jSvYg+A4epnhjH8zP6y+cTlfMGsHD1DHog3bVTvHv8MHf8vVg+RnM7cCwLB/o5vPskIZC8fO5yJZ8VyKBrYQ5d+vRj4dTZPC+e18UAABAASURBVDs9k72Hr5JUkEtGehrZiU2cPnkLNzGD3qqdknWgNLJPAakJGXTvkU9mol/zbZFXlEdhl25MmDqa5768iH45uXTRRjU5MYBbc519lxuZMH0SpmZcPHs0Dcorl8siWIqVGEFoZq/ycfrwGUxVPu47oAdFmUn4lRvS8kU7OwlzxaXlqN7IIj3F0bvK85ytdOUbKaQXZODcu86Je3UCszD/g1b27T1D8tDpTBvUm74DupOpFygKo/hVQ/aU3onx8ov2K2w+UsmwufMYrHwwdOZynh6Tq8UdJmrFUdCrL0NGTGXxyAwayutoqb/EgWshxiofm5pj3rzB1Jw7yvWKZhpqqiirilA0dC6rZ/ZGZyOePPryPgo3JGXm6+CmPyuWTiGzrYrqVjh7+AzN6XlkpSXrECXAg4uXOXXxHId0GLd4/igG9uvD0ifnM757lmqeIPGp2Qzq2YdZK2fTnXoqW10uHTxKfe4w5g7qw6ChM5hWKJseuExbJMiDO6U0JeSycM50xgxI8WSxbcv7Ra6OcnpB7+4MHTWDRcPTqCgtA8XuSdKxW2oyWZmZ0NJIfRBsxUeTbwYM6cuYifNZMSqeozosdTO706tLEf0GDmfW6mdYMrArXVR3ZshHwkBW94FMndSflORUctP8VJQ0EkjLp1thPr0GDGDYyPE8Nr4rjeXlgtbH5BHbgtYy9h69S+/xEzQ/fRg/dxIZFdc5cLOK0weP09ZtNHMH92bwiPGsWjSF3skJ5BQUkh6QYjqALupSSG5qAEf6FJq4VNSLadpXPL10GuOLksgr6saY6fP5yqopDBmQTUFWJgkJQMVpdlyNMmPFZIb07c4g1SS2L44Et4Tdh+/Tc/JkBvXvzbhlk8iuOMPuK1qAQoti6X9hbh0/xX0ri4J06ZudRunVMxSHMxnQo4CeffsxePgwnpkzkHBlOfXCs2RXo7I3F0VF5CT5vKlJKSiiW04SPscmQ/ddpc/D2rrIx71ycwANlgctt715QgfcDrOXjfPkm7r4cR4fYnP84EEackYxe0gfBg+ZyuSiJj49cJ2E7C70LMqlz8DBjBw1laVjsqh8UIWTUkBuWiJ52jd0y0zg4pGjlMZl01X7qOyCFMovnOZKKJXRz3yVLw1t4cc/2ELauDksm1BEcmIyWRkpZBf2oq9yyM2zB7lJL2ZP6MOggSOZPTSeY/vP0KgpwjayQ6TqPLvPNTFJ9dBg2bt/jwx8roNDIwcO3SSusBtpaWl0SQtz7sRljLUtC704t8gcMJVn5xRy93qZ7BClLGgzZNQ0uqWmkN+lgJyUBEVduKw1Ups1hMWD+2iNTGFmUYhPD16W5SzxihJ1/ShRsP/wdfz5MX5d0yOcPXGNuJxCespX+w4axKjRs1k4LIWaynpa6y6y92KQ6Qti/jBfuXzuiBwC5mWd9o0p/ihKfkybO56CpGRy5F/hxgYatJY0oI8lmbX3O36Q64E+LJ/Wl369e9ItM4A5uKX0LIfuunTtlUxaegGJTbc5ZF6ugOps28O9cekiDUn9GdevJ8MmTKB/cjNVdSGqLp7icnMK3fPSSMvOoeXeeS6UxOmAv1B72m6MGjCQOfL7blYDNe1RLp86TbmdQ25aOl1z0yi5dJ6SSBI9uxXQq+8Aps15nGeHNLPzeCmjpkxl0IA+zJkzgciNY5wvbseybEmljx1PvwXLmZIfoqRE/U6IFqc3q2bkE7BcLCumM7K8i4+Cnn0ZNmIaC2W3huo62suuKca2MmXaeAaKx9JZw6k5f5gbVbJbZTWVOiQt6jORF5b0xU5IIzs1mYLuveiem0JiWg7du2QSHxcAKjlzoYouI4YyoE8vpk8aQFxDOXVR6KI4NXFUN1JS0shIgArtExKzi5Rf8ug/THM8dg5LRmbjBtIZOGgwq1eMI7WphhpRTc4roGtBOokJFm7JDQ7cqKOgIJO09FxSgsUcu9igeNdDubULQ4b0ZdzSJQxLb6G0Kkp612zlzzQGDOtCRvxddhy4S68p8xXbejNhyVRSyk5z+LLxbts7nAeHnPw8L04OmTyHF1bO1IH8Ma6E0ugl/84rSqPu7Gm9QLIo6tadrpnxWI5wcjNJTctiyMAiUurvsO9cKTmFhaSnp5ETquLYuWKS87pIxjzpO4JVX13BtL5dVK9NoGtSIjmKWc01zQTdMnYfuEOPmbPkLz3o0yePxICPBL1wunDwNKEeo5g6oDeDx01hTFoFu45oH4CFiWf88vqlBX5pgf8jFuiIxP97eZvNdkgFvFe4EKG5rZ1QUzWnj5/l8N0Q01Vo9Uyso7TOR25WMuaNWjASISk7VYK10xYK8+iLKjfUjoGJkkRWsk1TXRtuKCS4KKYWbGlQcvAlkehzPThLwT/BDVLZEhK9zz7t7SG9nQ3Tqrfb0RYX2+/gRgxOmBB+/H6b2roWgtro31DSO3SiggEzZzMuH1qaWwlJxrY2wQdbsR2L9rBL5lgVuz1r+cGf/TV/+NJ27iuB1147wKtvbOLIyfNcfVBPRIHRlRhB8Q9LMXNf3dhMmw5sjx0/w4mKVObNH8mAwcNZPbcnO1/6B7713Tc4VNyIhUvnG3B07+kgGiKnA40g5j/W3yZDR3XQEXV8SCxc7WYikRAtLVEPNxQMSvaoQcE196EoYSX2aDRMWInWZ0VwdbBg/qmt4/N7cEHBtWtDa+upUQk2PjM3VgzpELBd+LbjEAlVs/HtD1i35wzHL92jCZ8R0eMfFv92yVld1Ug4KYUUyW7m0ElOIcltpYoIEcnpikFUPMzHNYc0sqmtvuraZnyC9Zm5Ep0kHXCGqqtwlVi+MLeQbT/5e779V+9y9H5zzEaCMTQa6lto18HEXRWih44XUzRhJtN6xtHa3CwbRGjTpigabQHHxnHDyHSMmzKImtMnuVhVq0NpGDowSf0uIckXkq7Sispze3nl7c0cPnmOG6WSLQ75YSXhQBopAdfzu7ZgGCyLUHsdde1+0pL8Xn+UBNITHUoro0xevoTulbv5ve/8FT/cdJFmZDIjhH6R3OVN7eRkxtZEtKUdncFhfKDa+GVbHWeOneHgTYep88cxUAe2Ty8dzrkPfsy3/vDH7LxdT3NtA+3+VNLjYzK1BrUGzFy1NdGidVVTfJlDJ87QWjSG5VN64Bi+4mB+go1NtAZbKL5wlsNHr1A4fh5zdJAig2KbhWaAvObKPrppKWfje2v4YP9Zjl0ppjHsx2dJHw250RDN8qmo5iUnJ5lgcxP3K9sIpKcR6OhP18G121JPrRvxfCGs+Y9qrFVr0tbBDpEm9qxby5vbT3Ps0k2q22ziJHBNaQN2WjqJnm8EMf5o2TYY3Z1k0jp0j6qIS7WbqKmTQObTYWezZiKKH1HJVt/cTri5Khab7gSZptjUR3vh9pBrMDxl2uubaAk2c/3UGdmugsFzF2AOkd3mEK527y3yKddtoE7ypabExebcTSQzPUBDdS2u1kIwHKFdcFGtn7C3Tl3RD2kNh3HlM7jtlAUt8tITPfxgyCJByrbV1eNK13hP1ygpspnd1kitsJHK5sfSl2n6+Xd8XKQ2PidC1a3jfLL5YzZs2cSnx2/SkmCTliz7JSWRlFvIpFF9qbx2ipKwpYI4gYzkJB0ypDBq3Hh8VRdUeIeIS0wgOSWDzLRkinqOYHh6kMt3KnCTkrRhTiNXBzZpOb2Y0CuTkuJrXLp9gW2b17NBfHecvkmTFr/jc4hvKsHkhgF9epLkgFwAOR3x4SAXrt8isXA43dKlXkud5uoKgV5D6KPCvmjASPqnVXHm9HWunTpLRWIvhvdLJS23F6P6x3HpxFnKg+B3onRMv4hobbdHaW5pJ2w6FQfaW6O0tMs2ZlRxLyEthy46vOzRrSu5ackSSAOWmqbNxOOHTV0eYW1ucvUysHvXrnQrKiAO0ZJufr/LiQ//hl//3d/ka7/1X9h2sxZXdMJ6KZTkswm2RGiNQCAuDp8bpD1sY4VbcXVI++TskTRc2MpHu07Q7E/CMfE6FCRsJWiTAa3CbRF8QpxPUoR1yA228SU9xeTTjfeRLJIbxeut76/h3U9j8bo+7Hjr1fxTYLMeWuTzrhuST0aM6T1M82VyQcibEPOkpvXa0hwlKkdKyUwj0l4P7UHFy6hM4aqFaGsPY14eC1r0Qpg4Zu5L65pJz0rD5JtoezsKrwrFFjXKE6R2rukoKenJOO2NeOewtkvUIAfr+HTtGt7aoXhz6RbV7Q5K2eCawX+9BSWXeWlgRl3DT+swZObZ2FIxw7Eks2JOS1hz4PN5pNoVG0Lyu6j6o4onGUmW1nG79DN6uHguo3lol1LGzhHF1bBeTrS2aizaxOGP1/L6pmN6wXeTCtULcX6oqqzFl55GqreOXZkriqMcpDeZNPkTSfO5GH5ugrlvp6JOAomRKxsb2b3mgq2b1to6yopraa64zJnzV7Fy8mi+cY7r1cIRxNAZC5jX5QF/96d/xZ+9upsSJRlX+rTphZFIyF4h8Q8T1iGg4dkiPR35Ykj8orJLxJYdLDEyviAdw20ikDWQ55cO5dS7P+TX//hldlyrFUA0Nreibei0SVbHsQgaOm0RIrYfo2JVTQvxKSlY4hMVTFJGOtHGegwFYz91idajn7D8J+J1ePLKSZpUc9XqMPfw8dM0ZA9n+Yw+Gndplc9GNRdN4ukSljxhIh0EzdxrKSOTC1YfzWtbKIKtw4ZwUx1tViLJATy7E5dMsh2msqlNgPoY/amjSqpnK+ZGJXu4LUhIbzHFCldzHpJtjPxmzRi6luzV0NgKra1cUa44fLqKgbNnM6FLAiBNOmhWNkJWTorHN9ISRCUP3rIVvvG9qJnkuhpaIklkZ0Q9uHAkQFaqFaOj72iwXf1h2lwffkc2r61V3k8mKeCqP4oVl0Oiv5HqtjxWrp5Cw+6f8+3v/DNrTpZ6vIzcIvPZx41g1muT/NXWOrDlG/XNEUI1ZRxWjXq6vRsrFw0hWlGJlZQpWxm5NMeJ8cQ7ASzZxejo1aKm7hQNn1ZtuQ5lkhQ/XdkvqnlJz0yl/kEluSNnsWpQGz/6i7/mOz/YzA0dTCHD/qJcUTO/wpPr4vjjoK2S9e9+xEcHz3FCNUdT2IdXc5igqlzcqoQiViRmpeK0KG+bNR8FW7LElI3IR0KYOtxRx/1j2/npuzu9uu5WZTt+rQNMHJafdNYjxq99xpEFbz5memhpoybiIyMpQFQMo3aA7GSoKm/ibkOELPE3OodFx5+aSoAgraoPTRg1OpqYGjYPIhjUvkBeadTXE966jBh9vCd9Kba0CdcyjCtqaVN9l5HienxNv6s45rY30hgMkJZief1RkshMClNb3SYC+lhqWh8t2se0NFZyUnuxYw8COhCeRRd/G/VmvXas46D82jF2MCiPtJDWj7GbpkkmCipXRY1XE5EvhhRXH62tbY+fkA2wftqragjHpZER7xLVXqVdNXGiP0xVbbsO55I1H1HJDemKDW0NNWiqSw/bAAAQAElEQVSSPPrh9jb1Rzw/9xmiWiNh+VqwvRW5BdVab20NlRyTjx4vT2XBY1MptF3JlcrCmV25c+46wZQsfOpxtecIaw9nbG98vaG2BX9KKpYCRFTEktMyibTViq8E7viYv1oOxmWSnuQS1Ty3mbwme1uK8w1yyuaSaxw6dpqqjCE8PmuQ+BhES7k6Ko5+1UpDabl0iHOl1coBTRT2z9Y8u7TJL7zaAyivbyMhPQUx0MclKyOJdh0Gt2vMUawy8wuNNLRHaCm77vErTx3E4zP7QVuL5I1ozxEUbru8zI9fdgrqpVMwLoOUBFf9ISKKE+lZcaA6MxgMEbUdaLrHh2+vYcPR85y+9oBW14+vc97E23yqypqJU44PSPeoXkKanGcZ+qYmDrVx99wZDh25Tbepc5nZJ96gYGQ2N6mybai5jpB5qC/H/HFSWpJNS10jLa31XDhxhoOXW5mweB5DUqG2JUxEvtGquYjKH129mPaLR7328eGGMo7KZ49XBliwaDp5ce00tEXRVBjqWo71NLkJOkRF+sr2/mRS/FDZ2OKNG7UUBnB9hUzqbXP85FUaKm5Smd6TQs2U64KB4eGldSTeUcnSFvURCLh6sVtHu5NIkt/1eKgYJlH1fGU4m8eemEnj/jf49u9/n/eOlouKS0j7x6DxXxF3IyHaNecmJkASSYkuVdXKFYI0L1aCiWnE2y7X9m7k5TX7OHriCvfqI/j9lvwlKFoRwsGI+LqEJKlPsIaWiY9+x68ecJUHg8pLlg1R6d2mOvLB5TOK39fJHDGVWQPTaNO5Q1hrwIvTzW24joOmE1fxJaI9m7eXqa+lMRogVXVP1Mw76Rj3rK9pwFxSx/wQ0hlBOIrWD97VoHmisc5bi4fu+ZmzfDo9bGg15z8R14MxNglrjk1MdVuateUNU3bjAkeOX8AaNIVlY/KIhFsUqQTuodh6CXSdN19dyyfHznHyZgVR5YGQ/Ke6LUB2tiK7kVF5NGoUVyyqr4+SnBaHa/o1d2mZPtpqGj2arplrkf7l55cW+KUF/uMtoHDwv5+pYw5YVLD5PG4O8QGL+MJBPLlgOsuXLuLZJWMpys5UwA1S39CGrSwSUCC0PNEcHY44OHo2jybY2T6fB6PURUOrQ3p2Apbt4BcDy4aExHT8OtQMardhaGlXTxA/6d7G3FCJNZ8OOhzRdYRkOza2+Dq2pV9H/PRsWaQmBrATili4dBaPLZrL6lUL0QtHohrzC9/nCN4x8GoqXPDl8Ng3f4Pv/cGzZNzez0cHLnN29yFK8iewdN505g4vwK8dhg2IBOYy94mBOFJ7DGfZgpk8/thinlowlizJM3zRav7pr77NvOz7vPfhEepcC6UghU10WXTqoAc8XYwsImw7No6t5qCEoMht+4iPt7EsC5/PhwcLWI4Tuxe8bZt7G9uxsTxcB9v8gofj172t+/gkH+1NTQhI435vfuITAjTcOcHWKxZLV81iyaJhZAdsLEdgmjTL9hGQbdP1lt/X1qbNniVcW/VPO+0qx9NxsMTfp2YTuyzJ6Zd9LXWkpSQSaW+R3W1s0Qm2tOCYQxorgTFLXuAHf/lN5qTe4fX3DlGPbKS5kNZK7H6sQC6zHpvNch0mrn5iMeOL/GJgyV98kt0SPQfHtnFUbVkWZA4axyi9JV373joqek6iXxxYliV4W7A+SRvlwCd7aOw5l8fmTWPqoFwc15bfJeKGmghpjmzR80t+23EIxGdoIxhW4o0IXzRUMrQGXZLiNJY/kt/6kz/ku18dwZWN69l1r93jZWTHCZDoWDS2BmN4cT7MfBufS0mIIz63v3xyJiuXL2D10il0jbfoPe0x/v6//jZPDWxl7Tv7qfBlkBBpoUWHhp0yOdLFlxCv+fAxZPoCls+fxaqVi1k8ugtSv8O3wJ/ox0nMZvrSGSxbvIBnVs5lXM8kTOb2SzfHwbtMYhdJWq4eZdPlEKtWzGDRorEUJiu9i6AZs2wfcbKJPtTVh0hKS6JLmp9Qs4oLddqa0xYd/tqBRNIMgu3giIFt29LdkiziW3Gd9UdKmfH4bBYtnk6vdIuo/CY52U9YG8CgZWNr42ds5Fg2pCXgRNpoDll4dNpaYkVpiic2GD6A3JNYTLF0yGsRVzBI6y8Wm76wbCJFCRDV/Pp9jvwF/Al+yV/I4oUzeWzZfJ5ePpsR+bbWmU3A7yPgWCKdQGIgqrOHjjm32mluCpOglyiW5WB08wnOtnVv23q2Qbr4fI4nK1aADB2SVugFhK3xQMCncchIjseS74ctG9PfJptF/QmkmlHXfIFIes3Vs2UjmR0cdfp8tn5jMDJtrN+RvCId0ua517gV/KfvfIc//N3f5VefmUTc7TLu1Gidq+h1LOFpQ+8XL7uhhXv3ymgNgE/9ltaaLZ+P1wane2Y8ZRVVtIimX/3YwrNT6FWQRVNlOVV6FDks2yUUimPApLn8zu/9IX/8B9/hm49NIEPjPtn7zoULNMYX0KdHIlFtLMDC54fG8utcL7UZMjIftJeQitrAgu3YWBZgOdiWT31hwibOqt9Wv2Wa7ceVDkYsyyCqj47LsmxsAdq2+e1oQrKleLjNou/E5fzOt1/gT/7TV/jGY8PIFF5EfByfj7j4AIk6rE9MsAg4imkqdAPJPXn6W8/znd95kT/67ZVMLJCviqerAnj40l/hu3/0p/z1n36T6d1SSc7MJ73pPmfvlOImOQRaqrh2XYfxyUV0T3dAckS1mRq76AusHBzPndsPaAr6sZUz0jMLSHfvcPpCmTbPDvHN9zl3qwzXl0dBDsgpcTwSliGDd0k+kSRy6ygbTjay8MlZLFg8mqIUB9cCfbBk7HhNlGU5GL+xbREhdvmksyOitnl09WXZBOJsbNvSfqOZOOVf5GtR11G/D8vy43NsHDV0+XyOd+/oPing0NLSimXb2AGfB2frPkN5wvh5bE3bGD935RhplpDMvOo3fP88G45VMUebvYVLptIzDaLYiLg+jpot4M9/fB2yO+q2pIPjNUuyOzi2je3YniyO7WDbegYswLJ8sedos14++cjI9MvHIl6/wi6WPya7z7GwcEH4SckWVtNVNu6+x6iVC1mycAq90wK4Gk7WQV1Ysa/NEg/bwu+3sXSP1neiDn50Zofhb2lT1xrxa907Rgj1WXRerhW7r3hwm7hRT/KllfN5fOl8nvnq04wNFLP3ijnSBTu9O6t+5bf4+99fSeLlnbx9oARLh9yO8B2RxXJwfGp6MDwd2xEfBzMee7ZxbHG1HHyCC/gdPfgYPHsFf/dff4sn+jax9t3d1ESTFPMsD9e2bXyOo3s7Rkf3jm2BPmma22Bbx5yrL6iXwVZ8CqmAJTx18fnLFl/THIOOEoloJtN/+lyWKnc9tWopi8d0xdao7ZOvaY79lqUnR3gOjnhbgCPZHVvPth7Mx/HhN2Pq8yek47fa0HmSJzM6bGjTWk2LjzOQmCmFBOXjMI3NsXzs8zviERs2MdC1HOIdC8vy4XNsbCeO5DgHUotYuXQmyxbNY7V8dVhekpAs0MfQTPKFvZdftm3jxBlcB8cBLEd0dG8hGsmqaZuor3cwcIa+etEDjoAdx9atLXj96tmfkoRPOrR35H033EgoHCDgWOSOmM+f/9ff57enJrLxnU+4qhclluV5LUgoR/iOoac+w8fWhDiya1zAIXfAeJYvmMVTqxawdOogeqUk0N7SSLusb9sODrHLdmzJpSYa5t7nGFEd0lMCOiNrw7JtyWt593GJASxfNote/HV+8KdfoFvFEd7Yfo2wZWEpVmm5IGDRE33Jb9uWeSSgl6NRxbDN12C1DjcWLRhNgQ4GXQuEqi8bf7zhA221rThpaaq5fNiW+iQf3mXjU6zy6XBLWYNdmw8SGLaIZfOmq9ZJwbssB58j3lLCtm0c3duO7Q2ZL0++eD/pVpRmHdzYgrF1gK+tDOkZCeQmQHV9C5b6jc9IfED2joLjk+6WhV+0Hcf27OfIL717C++ybR+O43hjXofP58GLHCTGYWmeW9otbHX4/T4cy8YXl0CCL4TOlDD9Nm00tDukKP5gLk9o0VHcyeo+lBXzZ7BC+47VsmFego+I5RCQTAbXsR3RUOPzl6vDOMv281n8c5CYaubXkc0sD8+xpZdj413S1fwGVP9ZbQ00GbkdH0of6g6QlOSTP7Xj2LZwoaWpmUBCCkhnn+RwxMDWmM/RuBqi52Lhj4vDtiDRH09az+HePmrVY4t4Yt5YuiZaWG4jZ0vSWDI1g63r9lAnHEuL2hw8+wN+4VokyDfDykVYNraItTU3YfuSDWs6L19yAlZ7M60hSzA2fp8Pn21L3kTiJE/XsQtYvnAWhveyyb3wocsSf9GTuUgfNI6xaZW8tmYP1fGDGJIGlmUR8DuiYWMD2aq7g3oxIAb6WDSpxgskJBCnMct2xM/SXaLWsk2XMfNi/FYs4bGJXbUPsXA0bz7JYtsOjmNjSb6AfN9tq5M/WKLp76ABWA4CIS7FR9ul/ewoTuCFxdNYPGc4OfFRzFrikSs50aFd8S8qmrbtk/4G38Gf5CchMYtZi2axfMl8nnpsLuN7JnuYjuMiV6f3pCkMS7rLe2t28sHhCmatfIKhaQ6mlk3L681K7X1XLF+ovcw0eqVCyHUI+HySz8KWkI4FVnyC5tglqfsoHtf5wWPLFvPMwpFkyGZgY/RFVyAunXi7HW2dsCWrFdF92CJNdtQwrr4sTYiFw5AJo6D4JGs/qWbI2ELA0keNjkv4jiM91WzNo0+CWJafpMR0fG4bwYjl8SDUSlAyJ0jm3KFz+AsTY+emsOWtTyiOOMQZ3EAA27KwRMvn8xNnhYFUFs8fSvv5nazZtIvTkb68+PRE4tvKWb/1DEVTHmOJ8tzwwjjVDsK1YrIYmYw8jmNjq1m2fjubqHo8HAfLAsfYx5fF5JWztTZm8/QTS5jUPRmf5eJIXkdAtiO6wndsG7M2kI7xAQsrJVlrPERrO9gas61m1WyQmJwkLiBUzOUYfNNs8wQJAZsEnfOsXDiTFapLnl4xhW4+jdkOPp+Do1tLxZBl+zzbWHGqjVQHTJg3kyUL5rL6yQVMG5ijPVwET0ZbCPoUH9YLwoYCXlw8g6UTe2mewR8fT4IdpKHZjskY58exbXzK34kSs6U5TMw+Ea2nCIHUBHwg2R0sfnn9P26BX6r3f6kF7P8IuVqbGqipa6C+KajAb9Fv8DDsGzt44/Ad7pdU8KC6mbCbyawJXTm38xOO3q6gtLyKSr2RDgZbqK2r9/CDShg+26W5tkZvC2u4vucYdxP6Mm2ApQKzlrr6BirKW7C69mFUZpgjJ/VGs7qGg3qjG9d7GL2zjLpRvIjjhmlsaBDtBprN23oVG7W19dQ1h4gokTQYWpU1+PoNolf4Ki+vO8e9knIePKjGyNHW3Eh1bYMOByNEW1s8+VpaW6k5v4O3j5QRTCliYI8svR31kZAcR/Xt69wvr+bCjRIqG5owg+xx+wAAEABJREFUG0wshT4F4DAwfGx/6o9t5eOzJbJJKVV6C159+Qzr9pygqj2Bfn17kJPoEBUKpgknqsKzzthGcrTqTWezNnK1um8ORghLpjqjT4OyhoKx21jGlZvVNOqgrqFB9hRcQ1tQZ/MtGBr1TW3aoLRSL3q19c3aiEVpFlyt5q1BWbyxvl62qqVcc9h10EiSy47zyZH7lFVVq1VRpTeKEX8CVkslZ2+U8+DCbe7X1FHfEAIVtOH6Ms7fqyHYezDDUurYc/S+N4c7D14hbcgIekaDVBl5xa85ZMqDKK0NjdRJhrLyNgqGD6WoqZi912qprLrHrtPlDJw6imjxZTZtkY3C2pD26UqeEq0xj0k2mmkS+gxkYFwxr751rGP+qmjT29f2libNXz0NrWGiHT5W19iGwXHjujBzTA5nD5fSZ2iufBbccLtnpzojX9AlMdlH5a1L3JOfXr1TQmlFHU5efwYllLJ261lKKmqoqKimqrKadjuNyYNzuH7ytGSv5dbJwzyI68n4wckc2rCVU/frSe3Wgz4FGUr0RnfxMz9WEuNGduHKvj0cf1BJpWhVVzVQoXVRNGoIvut7eePQXflLmdfXcPsqH2/fz70mh779e5OXAPF9B9PXf59Na2MyPSiro7aygrpwN8xfhG59Yy0XHpRRUlZBTYvxRDrdi6SCYQyIL+atd45y70G5YGpo0lv7tsZGquUndfItmRJzeeLGpRBoreKU5r/s8k3uVzRqPQVBft7e1KD5rqXi8ilO1CQyYUgPeg8dSGbdDfberqWq6i47zlcxaNxw/KF6KusaFS9aiOjtfJN8r6a2gXYSSIo2cu58CRU3rnK3vFFwLaQOH0Z21Tk27r9LmdZsieaiprKK9uShjO3SyvF9t6msruXI7gu4PYYxKgfMRNsqvnRHvN+i7t5Niqsb6C7f9t3cyeuH7siu5ZRUNRPRoWWT+Qs9xYQqHZ7HDx5Nfv1ZfvbJBc+nSsyBq2JTW5OxSwM1NS2EItlMHtOdkjOHeFBVS+WFQ1xpz2XyyBTaa+swsapB9o6GWjA+Va9D3VCwGdNfrTXVJn+eNK4LF7ds47gXDyuoamqncMRourTeZvcVY7MStp0qoc+YEeRJkSjG88HvB9W5KlYh1NKsuaqjXvLXaD0af0eb40bpUq05rK+XP9Q1ab27Hnw4KtPI5lEbItVXOXjiFHcqFFtLizlyoZiiEeMpjFZz6sQBLtyrpbm6mpOnT+N0GUqvwkSGjR6Be+c4J6/XUHLvKjeDGQzuUUS/YWPpErzGvlM11FXf58yDIP0HDyZDa71dzmMOhaOyoakKrfpyjlwqpqD/GAp9Lm1mFyTVfG6YC8cPEew5hSEpLi0KLZGEdAYMLKDu2iVuldVSdv0Cl8t99BrcQ60ncZU3FHON/Pc5e6mGggH9yU6AoBdI+dxlySnaZa8m7dojGhFLouavvNxmjm/8Cb/+23/J13/zT/ji7/2IfXdasWzJVnaFN/75j/jq7/w+X/uDv+K9I3fx+aLUlJzie7//F4L/Li/+2p/yVx+coV39jYof8Xl9GTG0H6OH9iRNsTGn33QWjEjiyEc/5He+8+f8+n/+O7ZcCzFx4Wz6pfh1SNOo+WskGJ/FwieeZnR2lHrFxfo2P32GTGDK8GyOvfcP/PZ/+i6/+Ud/x+57EYbOWsjQDLB0OuBrvcpf//7f89bJSszl2Vk3diCJQKiWM1fKqLx0h2Izz/Xt+AM+msvvcft+HS1tbTTUNyr2N9Au33AjIerkN7Vq5i+wLMci0tpAmdZXxYOzHL7lMnZCP9DBSGKohqOnr1JRXUmZYmG9ySuhdurke7Um1ovgSOW9qtMH2HGtgoqqSqqqmyjXGvYPH0GP8F0+vVhDVVUZ207cp8eYkRRpUqKW5Xm6408mIVLPuXNlVFy7SnF5E3U6sGhrbMCTUfw645PURZNJveFtZG8L0a58aeDqtaEOB2O5r0Y45q9ymhSvajRXzVoQltZCsLme2to6buw9RUlSb8Z31+YqJYmW4vOcvF1FjeJnWWUDNTp1sv1xhOvKuHm9muaIXzG9kStnHlB27wbF0q2yupWkoYPJbbzC+7tuUCq7l1c1UllWTlt6X8YVRjl85CpVql1OHjxHqNsQJuU4XN/5Br/xt1vw/rDZy0pRWuvLOXXkJk5BHMFwmFbF56D8oijfYe/HB6iRLc4c2cN2vbgLp3alf+8C4nyudG/w5tTIG5LujfX1PNTdxArF2+b2MKHWZuldT4PWRNDkyZo66lpDNN28yobtB3jQ5FOu6UlBqvhHZEPNa41iWTgcokE+WlPbRItkMrauF4/K6iC9Rg8lu/ome+/UUll1i08v1DBw0hji24r55z//C36yv9ybLk9FxQa9eda8xXywUS8fSClg4oB4dr67novKXQ9KKzB1G6ohajW3Zp5qJWNnbDV9zZKhRXWXua/XZtQwaG+qp1b+UFnRQCS7B+MLbI6fvEJldQ3Hjp7G7TqYgYU+gcrxvVyRyYRhBVzev50zpZXK7zWaXw0bGVPScOrusP+k4nRdOZVVNZTKH5MGDKZL21l+sKEjV5TXap5ET2hewCWd8cPyuLp3JydLRLOsTLiycWOLXl42Sz7pXV1PKH4AUwcG2P3+Rq6qZi4tr6RK+SNs5kc61yk3mL9IbPBsXI8/cyijugQ5eewGlVqbJw+cwtdzNEPySlj30QHu1oTp2rsX+Tkp2Ca+0nHJhg2ySW1dM206/G1VnVtbV0tdm8u4Ud249uk6dl8t435pGeX17aQPHMrwhPu8se6E6oNKyqRfazBIU4NkV15piURVYzZ569HkzpFjB+O7e4HT8vnKsnPsvRVl0oxBFJ86zLZD12mJK2RA70IyAngHFGilW4DJY7V1dZi1GjW+JT1rVCNEdUAYaK3gxO1ySi7f4n5lY8wXFCOi7U1aQ7VUlFxgz+UgI8f3x9depVpJNtU6N67khluo1TqvlQ1NPklOsHlw8yIPKkq5fq9a60d20Ivr+vo66hpaiSg3mnqktr6Jdi9JWMhMqFhiwqAsrh0/T4XsfefESe7HFzGubw5jx/aj9tinbDtfRmlZJeWqMyLEk5Uc5tbp46oVayk2fxGrtdKguNRs1p/kaWg1DKI0aw3Xal01tJr6zMX8Z+FqFJNrqlqJdh/M4ORyNq85SYn2F/dKa6kzL3pDhYwbls71w+e9+b9z6BTFcb2ZPCARc9ne4lKeHDOU5pNb+PDUPe6XlFNW06wXt200yNbVWsdGX8O/RjZqCRm/tYRuHB5SMxKpu3Ga05K9Wj5ZVtVAfWOQVu2lajrkjdXWWmeyncFG82mwnX6jGJVeybp3D/NANikprdIBuc2I0cOxS0RTubyy/DxHbocZN66flnYjNbJJnfYpxs/rNf+18q+w6yPRbaX45m2qGtsZaWx9dAsfnyuRPhVUSCYTt05+vJVbmRP46lcWk3B5Kz/ccpfWSAKJ/ghlt+9QWttMryGjyWm7xnHVLpWqRw+er2TQmCF4Lz2jeJcvfyhDMurYsfmw7F2l2F1FVU0Vde3JjBucyZF1b3OquAyjU1WjmS8PDeSPtooc15/HnLH53Dl5maxRffVSCZVj7VofDWr11MqpBo4dQeD+RY7KBlVlF9lzq50xYwYpDLZQKb+rr6shqPpywlDl/fXvcOKu+JWWUyn/iLQ3y5/rtU7aiUbaFDfrqahpwJc5jAl5Daxbu59i2btUvtKk+BFsa1Z92EC19hSuPwWnqYxj9yu4c/UWJVVN1JtFgS7tU/VNtzGD8d88wqZTZZSrrjX7nmr9RgpH0Nu5zevvHvdq4lLti5pCBgOtDwtLt5U3b1BaH9FLhRZaSSAt0aVNJioYOZqEOwf4+b7rmrNySiprVXNHaVV8ru7wu1BLC/WKQ+V1YQYNk8+e2cja0w+4r9hfWttGJNjm5Zs6+URQSd+X242JPRI4eyLm/+eOHaclpz9DusdLEk2m5kMfzArLkC8Ocy7x8X2HwblJGncx8urG+5i9dK3kqFcOjEZDNMr3yivqsPJ7MS7P5ejJ61pjNRw+egZf7+H0Tq9n+0c7uK1E3a1PD7rpJRS2oxcG7RTfKNYcNtOq2qNW9US16oWwXmpfuFJBGyHqtLeKT04lLhrG9flJjYty68olSitvcvN+HbVNzbQp5jd4a6GVsOpTE/NrFK+DOsdo0R6kRrI2tQWV45uoU5yormwhXNiXkZnVvP2z/dwtKcOcY7QKvrWpyVtXTdrTmlxSJ7q1TW0QF0+0uYqrV6tpCvdg8rBMrhw7Ij1ruXXwEOWJvRk3yNgqimUZa7m0aN3XineD6qkoMHTUQEJXdvDO0WLNUznGVyJm7yGYWsNHZy1OXID2qgdcu11DS1ovxhQ08c7bsp3J6/LT5mCUmP3rqZOtoi4kJCTSXn2f09oTXzHnKRUV1IcKGD0wgePrd3G9rIqq0hpqVE+WtvkZpJgQvH6eS/LTyjvnOfEggfHjemG33eCv/9Pf8/GlBswVNcTNzS/bLy3wSwv8h1jA/t/KRW+gwKWmKUq3rhkqBCtQnCNt0Cx+84XJlB/cyrvrd3Lo0n1U3zJ46Rf4xuREdn20lrfWfMqRKw+0cS2FnG5khO9ToYJs/Jy5DIm7z5Ztu9h2O54v/MqTDAxEuV9cQ36vIoLFt2iy83nuy0vJrjzDJ9t2cy0whF99fjbp0tZVse3FSx1kVVvp9Mp2KFXwvl/eRmH3PEJ1lTRVlmFlFpLv1NBg9eO3v7WMtOKDkmkr247foVFFaF3IR/fCVGqqW2ipbCKlqEDFbQtufBpNF/bx8cbNlOeMZ+XUvoycNZsR1iXe3nqISO9RDE5soLhVllFSsrWxCQIpo5fyu0/248KODbz78V4OX60hqVs24QfX2LB5B/src1jx+BQyBetGLWwLgjX3qfBn0T0lwu2aOkqiGfTumkZ9VTN15c1k9+hOQkMZ5I5i5ZR8Tm3dw7lr9yluC9CrKIXyuxWU10HXXjmEK8pUuJRjpRWQG2iirL6VCgXvPt1TKBHcnTqHfl1SuXG1ikCPKXz7i5Mp3buBD7Ycoi2nD1mhKtpyR/L09CwOrNnM/sZcJg3Np+F2FfSezIqxaRxYv4erTV346q8uJunWEc3hp9xNHctvfHk8drCKaFoR+YFa7tSqLFBReUtG6tK3gNabxQQLxvPrz46m/MhuNm05jD1iEV+Z1YuM5DhaKq6xYdN29tfm8eST00gFosa2rgvxPfnGrz5Bj4ZTvPnRZjYdukaNklpda5QuXbJorG4kWFVPXEERqTpoMn5oYTFgzCRWf2k5wxPRE/KLMkpI10uMKFeq2pn++DL6tInm1tMk9xvDwLhy7kYL+MrXn6Gg7IA2ZDu53JZNv7x2rlVGGP/Us0zPKufjbXv49G6Ap74sv02KIzkpzPFdn7Jm7Xm6zF/CnK6mQHKxtW7QNWjBar48PsCm9zay4cA9uvXvSVNVMZBavJEAABAASURBVG6XWfzhl8dSemgT72gN7dUhjL8gE1+d1sbmnWy56mPh4zMpSMzjS99YQXrJfl5fu5MrbjYDCyyuV7os+MqXWNK1gbXa1K7ddkJ2N56IdxnTWQldefHXvkCv2tOy3VY2H71CrQ6d75a2UdQnm/q7D1TEWziORRRIGDiGp6cVsHfNFg6VJTJ1bCG37lSQOWQKs/r52L99L2sPlDB59fPM7+Fg5U3kV58ZQcn+3WzeepT40cv5lQUFtKtQSO/VlZRQDY16qdOekEPPpDYqk3rw/OIhXN66kZ0X25k4sS/Vpfdx08fyu1+fQtWRT/4/9s4CsKojXfy/Oeda3BMiEAjB3b1oBVpa3N2h7t12u75dqWxdKPUC9VJaijul0BYp7hEIECTuyb3n/825CdLt7tt9b7dP/pk9c8/MN5/PN3pCl8WfrycvMpl67kLOlITI/DCaxJyd9jyw39eY22ZcR7gCS+Ymw5D4AJp0603HyLO8v3IHZQ37cf+0y3PTVwfPUF5ZysmzVSTXD+dM2nkIa829t9+M+8hm3v14BWu/O4ne/J08W069htFkHzjCBfkS3+qWMQxrXMGKlRtZuqOE66ZOoWe4Ty5tTFLqRVJ0Lp8Sib/gxGQiynPIyT6JL7oudXzZZBYo2gyZxKxrgln18acs/HgNW/aewRvfnrkTO3HxG+2zr7BaDGLezal2jCo9KYBdDg4Clwk5Jw6zPa2ApAYxZO/fyYGTeRjeQg7s2UVGRTQJEVXs27FfdAKb3O54hdxDEFRXxlfJSb7auEnidivljW5ict9UXKHh1Amy2LtpvXzwWMsh1ZIpowYS7oXw5tcxvl8Djm7awLIdp2k3aAK9GxiUB7Vgwqi+VB1YzydrthPcYTijutehXOZBLdcSufJgyhydX5BHQGQz2jYNoFT/p5GU36aqijwKrHr07JBARZnCYVqUlBm07jeCaxtUsm3dZpbKJUZq/5Fc3zCEyIZ9GdUnmaNbN7Fk3dd4UwcwoV8zHDLILWXYfqI6iQiqfAHUb96eTq1TCRaAV8LD9MTSrmsXWjZIIiwgmFA5HIQEBWPIOhJWr5V8ZGhLw4QYosNjqBMdJZf/gcSntqKrHOzrhIQQEhRKWGgoTsNBQHgiHbp1o0Gwj8JCH8UlXipkQTTCkhk9+15m3tCeeKGJSmzJiOn3cusNzTFkUxyV0p7ebRrgqLQITO7AqNEj6duhDfWCfFSFNWDczLuYNqgTscEhRNRrzcgpdzP3xuYEyRwS6AGCookPreTIodMCAUMs175WDTozrn99vv94GevPuOnePZm8A2cJb9Gdfo18rFn1FUczCwlMqk9Y6QVk2OMtzeFUeQhNYpwcyCompGknbm4XLr5fz6crDtDwprGMaS2TpjOGYUN74929ik/X7MZo1JR4q4SsC1lkq2gaxlRx/GwpUd1HctegBLZ+toSPl+8nMqUxrpJMigJbcYeMw4LvNrB85WbKGl/LrcOaiOagZOyKW+RDc3PG3dCUvcuXsvZQlcwHDck7l8mpo9nE1U/Gm3NaDk8WDgNQCkovkpbnoaHMsWdP5XHmDDRoVEc+tJyn4EI2jqh6xDhKyZMP06fKPdSvFyQfi0uw3CZ5p4+yYctXrE73MGnOSPQ/JY1o2YNh3cJZ88Fy1h+qoEUr8V9GNkS3YLCst9tXrOZAUQPGD2tPxqalfLHLS+euzVAX0iiP6MT9M/pSumslCz/bSG5wAxqF5JJRHMbY2cNpUHSAL1duYEdZfeZMv5YgBY6gCOrGheGQshiEooK0/XvJKHHJ+n2GUuUgwGlQdvEM5WGNaB1+kfV7MvAEBnJa9iSfLl1FXlJ3JvWN58yR04SlJKHyMsg7f57gmESiVQEX5HB4ujJQ9geBXDwr9awSYuvXJbTsFOeyT6Ni6hJWlsk5w4278BTLlq1hxREnN465nuiCTJyyVwv15lCQnys44TSQOMmWOe5sdgV1UxMx5GJC1evNbaOakSbzxzL5eBzSdRgz+sSCO4KGYVUcPJ5lxymSZGmB0jyqwuuR4CjiZF4lqCD6j53I8IaFfCpr10crvuNErsDLz3HWiKBecAn7j+eKH86S544lOaSU46JDTlkY9eu6OJtZAJaPs6cuEJmchHU6jQIVyehpQ6lftJ/Pxe97qhoyZ9pAYg1BlbFu2D6HNkMnMK4VfL7wcz7dcJhS2WcoEU2dLoy/rhGHl3/Kl1vPkdy6GSHnj1Ac2oK75gwhOG2LrBXLWb7jBAVV1czwp1Yyz09oo/hy0VKWbDlHQpO6GHnHyZBDeWi9urgLjpNb6uGmWVMZWDePDxd9xuKlspdLz5d5pILYevFQmENlSQ7e0DrEOku4aAUxZuZwEi/ustc+PUfPntKXCEcgIb5sVi9fy6LtuVw37AYaBYoe4g/b1xV5lATFUzcYssvLkbtS6so+tFD2kzH9JnLHgBi2LF3KB8s2szM9ByMohZlzx5CSv4M3ZY/y4YodnLhwnjxnjPS9iwtymXHhYhnx9ROokkt5R8vBzJM1fsfKDXyx+iCpg8YwqlkE4aFuzh7+js/kojAtrA1jr28h8W3hU4YoBzmnKohrGI+Rf56ywosQXpc6Rg4lSe2ZfE0sqz9YwfaLofTqGM+J4xcIrNueG7vEcWjdRj5bsY+6149kQqdISk5K3CfXEz6n0f9926q80+S64qgXUMCJnCquH3Ej0ae/4sOV+0hq0464gFwyMvKIS61LYNkFiuQDZYUnmpRwZM20UEphSLRauOk5ejR9Yi7IuN3I6uMOxkwfQWqAQUSHIdw/ujG7Vy7hnY9WsnZHOoUoet88kObGcd78dDNZjgRa1Q/keMZ5ij3hJIf7OCpjD8q54AslNc5JZkYeyJjPOJFLZINEqrLTKCGZW28bRtyFbbwhe4S95RE0r+vg2Jkqek2eSL9IvefcxJpjTlknRmHfGcnkb0gIWsItrs113DO+PcfXfsFiubz59lguxQW5OGKTiDALKSot4jyhpCS45aK1EpSihrZ+134MammyZNEqtpwyad0ygQvHz3Gm0KJe3SgJyQIqL+2tiygTgUoHmcjHTGD6HZNoUnGANxcv5ePV2zh8toyItjfIOliXPavX88WKQyTfMI4J8nG25Fwe0XUTMCXOy2UtqgqNJ95TSZ5X0XdQH9zpW+1L0biuQ7l/ZCP2rfzMHiPfpeWSf+Eo3541aBgXjDLC6d69CWXHvietyKBb//6kVB7k0/UHKG/QjVtHt+f0tvVyxvgWd4dbmH59QxQWer+g1caMYdLccaTk7+Ktj1eyqyCcJrKenJS1rOPkWYxrXsUXHyzj0xVbZd9fhk42nRSU+Fwe6nfvy/ihg+kcJzV5fIVnOV0ZTmqcxfGsUoLbDGLWoLrsW72BZav3U//a0UzsEkl+1jFUbDIxXCS7yKL7hKmMbwnLP1zGR8u3cVz8VyS3vnF14/Hl5VBechEiEokzSsgnkAl3zKKrOsJbev748muOiE8vZJ4jqF4y6uwxqhp2Y0KXYD5fvIq9JTH0aBtJ2skc0Ry07tJ9hDXuzz0TWnN49ad8uGw73sQUYmSfnuNL4Pa7x5JwbofE+HJWfn0EPSWjk7IQM4kIi8TjKyDr9GmJ5TQ+evlpfr14J8563bl/Vl8KvlnNwk9Xs3lvNhXeMvKNCHsOyc4t4XxulZw5kqhIv0ho+8HcPbIVR9Z8wXsSs98cu4D+z1A5Y+sSWZHDWbnYxAxn8MQRtLSOyXy+nm/y6zB9xhDquZCpX9kxrJWyqqTuiaNFk7b06yBjXCkBWOhkYWBKoeBiJXVSkjDFpyXl57FC6pDkzuOcL5xxsmYk5u7hi5UbOEATZk++jvhAFy6Vy6ov1/Le5lx6jxlEPRVIl/69iTq3kxXbDpOeUUBE/SScFzPIJ4jY8ABZry5wMjObowe28tijT/H5CRcjR/TFu381S9Zl0KxzawK82WQcLyK5aV2c+afJyzlPVVACSUFVcpYvIqPQlHU2ilKZR86cLCK2YSKcPk4BdZgxdyytrQMs/PBLlm46IPcpJRSWBpJcL5BCWZ8LLhYQm1QPs+Sc2NiO4XKW27tiDXvOVNJ/8gR6h5ySc8BG1qeHMHbWaJoHaFcpDFN7SeSXuWmYFMq59GwqgMCWN3DvuE5kbv5S5pbVbJIP8hWVJXJX4aNR3XCOy+W2J6UTN7TxsGHZOo4WRjFmzjR6etJZqMfQhj1kyQeQvGwvdVLrUCFnvRIvJPS6jsGplXzyodCENqRXUzdHT5UzYNokrk/I5p1FX8qepIwmzWI5u/8i8V2GMrVnOF8t38jnGzPpMHwctzQS5StN6iTGEubR+ovCtc//dg/U6v+/zAPGv1NfmcqFvaJRr5v5+V2TublzEm4baJDS+Vruv28O988bx8heTQjWmqggOt04iofv1/BRDJYLpnpJrZlxxzzuHdNDJlkHkY07MHb8ECaOG8ltU2+mc7xbZBikdruRB++ZxvhrWxIqEGdsY0aMG85UyTNG9KZBiC0YpRRK2jEj6DVsHD+/fShtokNJ7dCXex+Yw5hOCYQlpDBq5lzuH9uDCAcEpXTl1jvn8rPbJzNtSAeinA6a9R7GL+8eR68GIQQ36Mid98yRjXBDohp1ZvaMUUwaO4Jpw7oRJbJc8S2Zc+9dPDh5MMNuvIm77xhmbwR9VRYuh4mIAMtN8z5DePg+kTtnDIM7J+AJT2bk+HFMHzeEmeOupWWsrJ7CTxlKfsETl8rQidP4+dwbaR4XTeu+g3n0vtF0TQglun4Hbr1/HhN7JwtuIH3GTuO3dw6XTUUqna8fw6/uG0PvJknUbdGD+x6cxfDO9YmNbsCYefO4b0x3kqOCaHPDGH5x+2h6NE2i402jxN5J3NAhQfhBcqfreOjBW7lj4nCmT5/MfXJIrhsYQs8RIufBqYzp25sZd05lZKd4wQ9j4JRZ/Pb2YbSt48IZ3ZyxsjmYOG4EM0b0lIOloHgSGDptFvdPvY4WsQ70IbNFj4E8eN8Mxl7TmEAgqnk3pkwaxpSJ4t/r2xAsMGdUCiMnax8NZfqYa2kWbQgUDN3PknUloG47Zsybw8N3TGXWyO4kBJg06DyIR++dwvVy+PEktGL27XOYN7AlgUpTgKrTkpEDpC6qIMkVncygMVN4VOKlQ0IgnsQ23H7/3Tw8+VpuGjqcu6YNpqkoFFCvDXPuvpWHZo9i6sQxPHj7RHomChNnJP2GDmP6+KHMGD+YrvW0RYrWA25i9mSxafIohneth197hVJKpMrjDKPP8An86t5pzJhwM7feOos51zXFlKa6nQaJf+bxwLwJjOydQkBQHINGjWWWjI8ZE26kc3KQYEFoSmduu+s2fjZnlIyd8fz8juF0SnAit+4MnTyFX9wzk9smD6J9otYJRLh+0Ck4sSUzxDcP3zmZGbd0p25UIE163CRxOoORXVMIFtMQrU2NrELoMXQiv39wisR5P6ZPm8iEbkl4olMoYAKCAAAQAElEQVQYOmYIk8cOYc7MkVzXKlpj2zmhVS90n06aMIrx17XEI1BPfCtmib6zrm9GeHAwfUZP59FZ15IU6qFJv+H87pHZjLulr8TQRG4b2AQlNPFtr+Wh+2/l7mnDmThlMg/Pvp760h+ENWTk+OEyDwyT+OhHwxBBlkfTKOX3tlPG56y7buf+cX2R8KRuh2t5wJ6DxsvclEqgO1jGzHAeulfs6uCP/8gmXbnjnrn2nDBxUDvCXQ4aab88MJ0p17UlPlSEEES3gTdLnw9h+uSh9GsSLkCDlO438Yt7JzGgaRTBdVox++6ZTBvYgri6rZk2byZ3je9P40hBFfrOA4fzyH2zuW/eOIZ0q49LwFFNelT7bCQTB7ah2iS0TdJsPy7p3rAIk6QW7bnxllHce8d0pg/tR6dU8b0znLbdejNh6lQemjuaIX27UD/SgT4oKaVQ4hflA19IXW6UDyczx93CZInlCde2IlhBmRFCpxtGcqvE7YSxo5g5rCdJ0lBZBZVVJvXa9mHi+GHi81u4rnUsyG60SjaOofXaMXr8cImD4RKvjQnQMmx5CqX82VcJATFNuOmWHsQITaXWRdq0UT4jmmuHXU/jQGFp4aeRd4UZTvcbbmHymJuZLHE0pGsDHMK7zOekcddrmTb+FsaPHSm+ao+ezystJbRckfz1ssoIbpg8Q8bu9SQpKBd7PNHSPw/ewa8fkvzInfzqkft46ueT6JLkIbn7SKnfza8eFvjP7+Mvv5jFja1T6XjDaH79q3v49c/u5NeP3MUff3Uvt9/YkvjkTtzzi/sZ2Socn8/AME0MsU3bbIbV5YYxU/jFw3fxm/umM6ZXIwJEhyIrgHY3zeDPs/oRIYCiImjY9UYeffQ2bmjooVzOuI7I+tw0ZjK/fORufnXfNEb0aESYA0LCQF/oe3PLiG7Thv4d66Ow8MkFrqG0+QF0GTKO3z8ynVHX9ZOPZxMZ3ysZIyCB0XPm8ci0G2nTuClj503nTvmwV1/87giO44ZxouftI+maEoorNImBw4YzdewwZk8fy7Au9XBq1rKqpXS5nkd/cTuzR18nl+Zj7QvXlPgUBo2ewMO3jqSbPT85aS+x9Mv7ZzF30mBmzJnJ3UM72TEdltqVKRJjE8ePZPKN7RFzbM5KKfEbkty0vm4kv3t4FmNv7sv4iROZ3b8Zqe17cPudc5guPg9zakOV2C3oQXXoK2Ph53dPoG9qDPXb9OKuB+cyqlMikQkNGDNrHg+O60JMZCQdrh/Fb+8fTbvIYKokfhNb9mDo4BuZO/UWOiYGCDN53LHiixn8/sGJDB98LbPmTGVGv2QwQ+g7Zjq/vXsMneqG0LDXEH73yFxm3NydEZOnM3tgczSHuDZ9ue++W7lv5hAZi5N5eMZgmoQL39AUho0ZzqRxw5gl634TGccCpUG3wdw7qTthpq7p7KF59wHcJfuekV1SquGK0MQWTJg5XWTOYHiPVjRv3ZkpM0bLePT7MVz6v367ftx73zxmDmhCTGIDhmvbx3QhPjqSjgPH8ut7RtIlOZL41C7c+cBsRvdMJaluS6bdcSv3juxKSoP63DB8DLMmDGHmxBvpEO/AEdWEqXfcxl23tCYyKpbeQyfx6zsG06pOOMmtenPvgzMZ1jnB7ouEttcwbdIwpshaPq5/c4ItH76iEjxJ7bmlRwNkWNsZnYLrMXTGFO6bfD3No10aAkGJDBo3lZ/fM5M7pw6ifZIEZ0ASwyZP5ZHbRnFty1gC6zRm1JSpPDjzJlqLDm2vHcYj90zk2uaRoAySO17HffdMZ8qgdkQARmQKQ8Tv08TvM0b3p3G439FKKWnVWV6OcAaMnsSjMs5mT+xNghNkOpEGD92GjeX3P5/NpEF9mThrOrPlw0OotEQ07irr7xxZK6YwfVAHoj1+Xkr535jh9Bs9UXhOZ9bYm7lDYndin1Y0bd6RW++5ldtubk9soDByxonNE/n5fTO5R9bRfs2j0Lzvu2c243sk4w5OZMi0mTw4pQ/xBjgiGjJ83Ejp92FMG9WXhqHCgygGjBrODNmHzJSx1a+xDjiBiz/kFzzxDJw8hZ9N70dyQACp3W/k4funcm2TEGl20+Wm0TwqY/WeGSMZ2CZeYBCQ0IwJs2fz8F3TuH3yAFokJNJ9iODdNpjmYYEktOrLfRJDwzvE2fgNu1/LjIlDZX80mhHdG9iwiNT2TJ46limyPkwb0o06ARqsMPwTFXFNu3P3A7cypmsigRF1GDJtHg+P705oYKj4fTJ/eHAyQ2/sx8zpExnfJR4jsA4Dht3C5LFDmDltDKN7pNjzUojE8213zGXOTa2JdCN70SaMmTZVYmQwLeNcBDfswn0P3c1dE69l4NDR/GxcL5o0ac3UO29n1vVNCAsJpd+4GTw841pSwhQ6KdHRLrmj6XfzUKaME5kTb6ZzvWDdLFnWoe4DZa82hwfkg/6461qjvW4mdeT2+27n4dk3M+iWYfxs3jB6NEmgy+BJ/OrWYXRLDUG8K+NxNL+4awy9mkZL3U2TzgO49945TL+2OcEmuGXMzL3zVh6ZN5qx48bxyJ2j6J7sEdxI+g8dwvRxtzBD5tYudW2ngo47yQqdnDTtOVD2TnPts9jgzkmERdZl/JxbuXdoe8KCw+h80zh+fdtQ2sSJw4REGQY2bVASw6bN4Xf3juWWm27g9nmTGdMtiZSO1/Pze6dyQ7NI3AktmSX7x1sHtcK/t1YopYQLmFGpjJdx8sjd07l90k10Sg6w4Y279GfaxGFMnTyS4RIfSqDBshbcI2esib0byD43iSFTp/PA5F5EOxR12l/HIw/PY3LvZMF00KzPUIlZsUf2u4M6JROT0IY58lGvc5IMVmcUAyfOkrPIzRKbisCUTtwlcTVvWCfChDpWnzHkTDNFzifjBrRAtjUCVaIzdkaSO6EF02+fJ/01nkmjR8m+cALXJOu+Duf6sZP5hazft025he4pGsYlOpSBncKaMPzmdvhbkeGfLOv3BH4ma2LPapqGXa5luoyRyRNHMbxnCkoIo1JaM2XOXO4Z34u6ehFQEVw7ZiKPirw7p91C1wbBhDXuzgP3zpJ+qEtASDIjZ83gXhkn0UoYyB5j+PTpPHrPDO6ZehPt60eT0Lgbd9x7K7fLeAgJjabP6Gn84f4JDB44gLkzJjCyo3/MKqVsHcAktftgHn1wrpwZhjJ1xlQemHINseJaT5L4Weauh++YwpQh3akbKDKRmdy2u4y9+7NoMHAqf/rZHB4Vnz9/2zXkHj+E3O+S2K4v9903jwdvncDofk3lQ2ogXW+ZyK9vvZmWscEktOjDAw9O45aOdYSponnvmyRmZ3OfXKre0imJ4LgGTJw1m7vEN/VCXYIDhtg7SOa6aXo+H3s9LWNESWlRMl7lJY/C4UDsKsUX25huzRLRPWTZvwJX2ClSfHTXAzMY260+wQEJDJ42mwcn90WHk4pqyFC5Y5iqZYzqR2qIJgmhz/BRzJb+mzZpOAOah2sgddr24cGHZjPx+vY0a96V2yWe5wzuQFRxOtsvBDHnoQf5lcyTv3rkIeZ2NPj2+/NEtezLz2XfOXd0PwaPHM/9wzvTpGUnZsu+YXLvhkTHJXDz1Dn8fGpv6kaH0eyaW/j1fePo1SSR+m36cv99s5gs+/ZIE4zYFkzS8/Sd05g7thfJISG06DNMxuoEutd1E5nakdvumcfEa+qJTzz0GjOV39w7WuYSMcqIkPl0BNPHDZG4vFH2QHp+AaUUCp2cNO01mJ/L+np9+7r2OQ7xY6PuN/DQfXO4f670a4+GBLhCZH4bxS/unsyg9nUwZQ81WOaQX902gjZRoMIaMHbWTH4uc8JtY/rLGuwmuklP7pW7mfG9U7HPm+54hs2aw2/vGs3gftdx5+3j6NcwBFwJDJs6i9/cM4kJY0Zw311TGNUtHiRmW/S+npkThzBt8nBu6pDg1zmkAVNunUjvlCDBAcNQ9rv2p9YDtR74aTxg/BRiLJ+PKq9XDt2yGFUL1DCvwHS+8p8++ARXw7xeOZD4LLkUsaipa2pLDiq6rSYLis3xEr9LAAuf8LDxhKemtRGv+PH5vKKXT2Qg2eeXo+nlJqZGpo1uy/Reate8LOFZJfprdGrapXKVfoKjcanhJ7cwXoFVXUzjcOZFsrNLCYyPJkIL8SH+qdZB8xVeYIkNWq5PZPv11KiXsvD1CW6V2GlJ+UqddN0rbV6bDzafKsHzCZ7Ptlv6Q8pWte4+wbOk7tU0gidFrCvwrqTR8i2xowa35q1t9VXTeKvbfcJX41/WU2rC3Csy7Cx4mo4rbJVmQRKItNm8q3lYtq5+X3irYQiy5m3zEp5StWmv+hGg5lOlbROeWp4lb1232VS3X+KpiTXMbtQVyVLXcqpqZEjdK/yqpO4TXl55a75aH6/Add3/1n4Wem1fNZ5uq2Gt9dB1nX01QI1+RfbV+FRk2Dyr8Sybn44PkWHDLIkhXffZ8WKDNB/bbzVwrx3z/jarOi68Nr6YpLGvzgLUMqu0TSJPqmi5uv5j+vqqdfUJrqazfSpEl8ai2KBjsEbI3+rTS7SCeMnvUraEb1X1OLoSx8/Hb4eGV4kcuz+036Xsrc5+mDC68hH9vNo+wdHtWoa/XuNXxK8+8ZvUBVeTXpYnMv3OvOQXbbPmo/H8fvDZ/q2xW/OvEnl2XfjZsoSHVVOu1qOGXuNqHJ/gaJhl96efp5alYT+WXS4IC7YwlJfiIi+FxT7KK/2aVZT7KCr0klcgbWU+qnw/wkH0KdF0RYIr76JSS7wJeptWXuKjwIb7bL76r4VlL4rOVRUaX+DSXlIh8oRAwy354FYkMJ2Ly/y8+GESXEvstP9P7aQszxUYFqVigzbhSrgSrbQ+mq/Oxfq/6SFUGqdS7CyslllUYqHN1HBp/qtH7z+1vXmFPuTu27ZF61KY5yVX51x5S74ouVwQquQnV8o1WcO1veUlfrwamhzBKRDZXnFynpSL5UJT++OSAlohbbP4WPPKEVlF1f4xBalC+OWIToKCKQBtk8Yrkct6m480FAttnvAukrfhtIiQM49TDudCjiVfBpJbdKR7kzCpKpQ2VEr68enxKuPJJ+NKx5g/niyJV6/Euw9LYsCGX4pJ69KcIWKFhdRtWp9N4/MDBQ6WwKuEt1fel3gIv5rxXIPqk3Z/jPt5aPxqBsJTwyQLskSSDb7y569kaDx7fHjxSvlKXNFIxrFX7PKix551BZ4lel3SUYh8vioqKqrwiX/K5Za/MDefikovXrHnMlvrsi/EBpu+utFvo5YjUqWtxg8aXqOXVSNffKtpq+Ttt1Hz9eGVus6immjk56PrdqX6xxLeXplLanjaYCHQMC3TJ/r45VTz03VB8sPEHl0XfFsvkSdF2+YqKf9NH4lMSxB94hutj87CRhS0ROcreEq75iOoWNW2an1EfHW9RicZlcqQDz4+4lt1pmejCAxxhFJKo0q2bD9rOZqXAOTxw6q0YGRyTgAAEABJREFU7aKrH34ZpnUXIX59qtst0Vvj222ag9S9mt5WXgM0vU9o/NnPU+BXPZboUmXHUGV+AXnFZbbeGqVSYkP73PsDvjW218gS0zT6FVnz9No8vaLrJbxqn3lFPz/N1Xi2HVfgSAeIbl5bfxtfDLi07opONkzmSp+U/XJ82Dyu0KSGR5XoofEv+8yP5LP71C/jEq3I8YofqyR7NZ3U/Xg+kSYcq3WswbeulC+22ZyFRvPQ9F5p17JtePXPJR/a+DV+kLiRdl+1Tn67RLcaHOFj89N62TBBrtbFe6luib+84nufDhdR1mfXNZ2fr4Zbflg1jR4rVdpOYXf1Y+HXwSf4PqrRbRTL1sUrcMk1DdW6aF6azn6LH3y2PZfp/XWv8PN7xarhdYmPZfOt0nZK1m9/k4Xm6xVddfbDbHWu+rnET2h9Gkl08Eq5xke+an0EfBWdOMuOtyrB9V2hkyVlG6bVFaIreV3FoLpN43pFRy1at2t6XdfZdxlo2+jXSewSmbrdFqHlydjzt4lWUrdlap5Cb9lyfEhR2Ptpq6RN01LdB16hkUYsu+4TWZL9BBp8dbb5ST/aOngldnTfaBTrkj+0bn+LXDT8wdp0ma6G5sd8YF2S67PHlebjj0Wvra9Na+svdX9F4FK+ZOtlOVo/zc/6AX4NP5/4wyv2+Ww+2rbLWetWJW1e4atx9NvvS8uWV6XbhN6GCZn/Xcnp02c5l19MSUkpxcV5bP3+LPWbNUP/UYAlMebnJb6slukTWJXIELOr+0XadEXzFP5eLUeyT+ML3F/3VftGkKTks/F8opfA/YqI2yxpkfaidL4/foFTBw+QVWpSLynYbqNm2REU+/kRH3m1XrpR5P71HAtXyr00713BR/u9qqqSStHdKs7lVHYuOXnFFItvis4dZrdcSHdqGWPbXan3HWKH7hsdp5rWtlXT2vK9EoN++yzBq9I+EfiVeFpVYSZ+0LheeQu+AC/j2xWBS5vwldqlWPZXrR/YpDGuzpd51TgaNMzWVevkZyR8vKJvTV8KX2mr0v7UZKK31657RZdqHa/wm19iDY3wEHs1vp/1ZbhX+Gm4199QrYfP5umrhol2UhceWq6fce1vrQdqPfATeuAnuYBWhoFDTs6GcXlm1zBTYDpfCTcMQw7ZpmQDQ/CVUlL21zW1UobUL2dBQadL/C4BFIZZjWcYaFp+kAzDFL2kTYFSGtfE1PRXyESnmjbT3y7oaHm2Tf4Kpmmiaf18NC/JNXKV8rebCpvOVck3q75kS14cw27qbP9Vo/1XaYJv8xE5htYDhWGKTFN4SVaKq5MADNOstkFh85a6JlVK+WXqCth8HMLDELhhmEJjostKad5SNhRKKT+NaUgZ1BV4xhVlJCnDT2eal99K4IZh2rxNww83hK+AMUwN9/PVzE1Tt0s2BKYRUDaOhiuFnS7JqOahlOBfoqtBUhimKXr722pouTIJ0BQch2TTMNCUmreu26yr2027Uk2oYT+oG6Yptgm9EhzdXl03hKdpClzAVMN13ZR2nf1sFEY1nmka+GGgroTVALk6GYYpcs1qG+VdjeenlbrIMWyYEhm6bti4NghJStdr4JqXgb9NYZi67m9Tir9OAjRrcAxDm4eSt+07P5OraGp0NQRH05kaRymRY9g6maaBIXWqk1LGJbhpVCsg7abIrKkbpin2i2yhUcLX4TAxDU2n30qgoGw+Ujf9cIe8q1swpGxWZz+Mq1ONPMHR7VqGaWpeJka1ToZhiA5SF1wkXZZnii6aCjSdQ9MJjR+C0Gt9/NlQCp1q8Oy6wGxZhkIphV02DRT+ZFTL1XBDcDRUKT8/05R3NUzDfyw7nYrwMJO4WJOEOgbxcYo6scjbkLpJYh2ThDiDeIFp+NVZ2TiaLsHG89NqnHjhlXBF1rCaHC/8LrVVy7PbpPyj8L+SreUatp423aV2hZb717r64T/G+ypd6qgf4cklWJzISRA7E8Wuy3IVCfEmiT/IWof4uL+GJ4iN8ZrHD/ATtGzxi+aTEMclmVfJuYIuQfjoNq2Tza9ap7gYiP8rPgqtt9YzNsYkLERhGFxKjthkWjaIwSmbew2siS1dNgwTPZ4MITCrYxeUHYcO00Apf9nUZXRSGKbQSPaHntRtWolFDfMD0cmO8yvHquYh/AxT0xvUoBpC7xCYlmHqt9Q1PcoQPaqzICsbePWPElytv2loPBNT49l0/vIPsDEM89I4VupKGoWpZZsGCiQbuFwOfAVZnCsz8OWnsyuzCFPsodqPNpbQaN2NK+QjyTDNajmIGQY1OhqmiSk6IknVyDf9ejjkrQQOCkPKZnVWfiDaVg3jiuSHXeZpNymFaZq2TENkKaX5V2ddB5QN89MppTBMKYs8KWIYQmsaGFL5IZ6p8QwDpYTGMEWOYWdDgQClLDCpKKUu8ZGiNGk8U2Aakeq6hkkWfkjyRMTTrFEd2Q/JidCPJlD9KAzTFN6G0FGdlA1zXAW/DDP8QoXGlGzYdErkaHy7DdB1U9Mb1cKUEp6ij9huSpYqf50UynBI3yqyDmfgdbopOpOJfAvCKfs7U+LDNDQPE7Oar1LV9WpZ6q+YKgzTFJ5CY1bjCu0lOl1GJ3UVnqEUqMv4UsEw/TykRapK6rpdsuhkwxCYlLV9Ots8uDIpoTFFF0MwEfaGlE0MhZ0Mw7TrpmkITNkwlMIUuQ7JpsCVUhiGKXgGClDKsNsNgSNJXSnf0BgaqGwc0xRcaa+GUpNqeJg2vsIwTZs/kgxDl00MoTMFbtbg2HXhdwkmyOqHdWXLdZgGtno17VI3DM1Xw/04ps0XDNO0ZSt+mBTGJZkG1ejopGy4acsyaxqUYde1bEPa7bcoYVTLrUHz100MabuKl1GtgcBNWyezmp8puDYmhvA1TS3HqIZp+NVZ2TimTWtontX8TF0WVMMw/fYqqVz1KAzTlDYTwzBselNoND+HwKWIdqopZQ3nh6lajsY1TQMbX3CUzcsQfobwVQKRRxlSNzFtJIVhStk00K0a3z+/6hroulndbgi+UgpTcKUIQmGYpujsp0XV8DXQSdl1DZPsJ9Dgq7NSws+8lLX+flSFYWreus3AD+NHksK8qlFdoqsBK8NA66yzUQ1USgnMz1uhk7pEZ+NpoNJ0gmPTqKvxhd4wTbFd2k0DpZTkq/EN07TbDcMPN2w+XJW0btpmLdMUfNMUXhpD+JlSt9uMahhKPibKfE4I193cn7jz37Ho01W8/+GXHAnrzJyh7XApaVcmmlZno1qmYWhdDISt5Gp9dAXQOpgiS2cbX6lqesGnJikMQ9P5s6DgT5b/AtppsGfFh7y+4Swtu3UixmXhQ8n//FiXfpWmNzFtvRSGKWXT8OMJU0PKpmlgGtUwwJCyDTMNDMEREEoZmKaJaSiUlB0OByZSrtOaCX3rc2DN53z4+Ure/mIPqTeOZnCzEN2K06lpDAzT/1bK4DIfhWGa0mcGWowSuQ6pG1JR6jIeOglM0+l20xR8gV3GtyuX+EoNw9R8TQylawpDeGs6nf0wrkpK2jVvQ+TUNGiYKXx0NgybkfAxRV/NVwmawjBNqRvYZPKjcTUf0xQYCPwHdqCqaUwMw9/mZ30ZrmlN4Wv6G/DroXENoVH4k7LtrUahNtV6oNYDP60HjJ9W3P/n0pRMftoFIY2ZOGMi90y/kVbR/i6onQS1Y2pzrQdqPfDv8ICSPVdthv8ffMCPJEtfmmrjf6StFvTXHtAHFg11hNdj3J0P8OR9w+nSMEyDMGoXa9sP/+ofmaLkbt+SywFd+qe5/2QE/mFkUL/zIH73pweZ2qe+/Z93QBn8z9b8J3NRraBaD9R6oNYDtgdU9XrpqdOEMZPHM2P8LUybMo5pg9sT6dIoCqX0+yfKqvoc7q7HxFvn8ou5N9E8yhThimpV+WmSwjCQZJLSZQCzZoxhyughzJs2koHt6mC75Cd1jKhS+9R6oNYDtR74iTxgT38/kaxaMZc8IIcsuRDQ/xREvv1egtYWaj3wv9cDtZrXeqDWA/9TPaCUfZz5n6re/2i99D8jtdfq2sX6395PSin/wfvfLulfIMDew/m49C96/wUsa1nUeqDWA7Ue+L/pAf+5V38Mr8n/3XZeWtv/mxXR/tD/qY7L7/9mhWrF13rgn/JALXKtB/55D9ReQP/zPvsXUMghSw5ahnxuVf8CbrUsaj1Q64FaD9R6oNYDtR7413tAGQaGXqtrF+t/vXP/N3O093ASG7Vx8b+5F/9v6F5rRa0H/sd7QKHU1fm/W+VLa/t/syJKKQzJStW8qU21Hqj1QK0H/k97oPYC+v9099YaV+uBWg/UeqDWA/9uD9Tyr/VArQdqPVDrgVoP1Hqg1gO1Hqj1QK0Haj1Q64FaD9R64G974P/KBfTftrC2pdYDtR6o9UCtB2o9UOuBWg/UeqDWA7UeqPVArQdqPVDrgf8rHqi1o9YDtR6o9UCtB/6XeaD2Avp/WYfVqlvrgVoP1Hqg1gO1Hqj1QK0H/md4oFaLWg/UeqDWA7UeqPVArQdqPVDrgVoP1Hqg1gP/sQdqL6D/Yx/VYtR64H+2B2q1q/VArQdqPVDrgVoP1Hqg1gO1Hqj1QK0Haj1Q64FaD9R64P++B2otrPXA/1IP1F5A/y/tuFq1az1Q64FaD9R6oNYDtR6o9UCtB2o9UOuB/x4P1Eqt9UCtB2o9UOuBWg/UeqDWA7Ue+Mc9UHsB/Y/7qhaz1gO1Hqj1QK0H/md5oFabWg/UeqDWA7UeqPVArQdqPVDrgVoP1Hqg1gO1Hqj1QK0H/od74F9wAf0/3MJa9Wo9UOuBWg/UeqDWA7UeqPVArQdqPVDrgVoP1Hrg/6QHLLHKkp/aDD+ND2rl1Pr5/58YkOml9qn1QK0H/kUeqL2A/hc5spZNrQdqPVDrgVoP1Hqg1gO1HvjJPFArqNYDtR6o9UCtB2wPKPlV8lObodYHtT6ojYF/bQxQm2o9UOuBf5kHfqILaEu+xv5I/i+aYcmnR/nY/R9y+Udw/kMm/+cR/u97yY4XHTM/yFd1rd12FeRHKzYv4EcbNdDm86/xqS3L5qcZ/7TZlv2vFGnb8a/xyz+tVrVs26aa8j/NpJag1gP/pAeqY+2fpKpF/9/kgdo+/t/UWz+ZrvZa85NJ+/cLsu35wfL9g+q/X4m/I8HW7++01zb93/VARSUUl0Bh0X8xF1r/dR7/VR3+pfQWBf+BTUXFoPOP+85PX1BU8/4v+vdfaptfF627zj+uvx+ntq3WD/+VGNDxVV7m/2vvmln0f+vbXif/Jy3cP4Ejtc0/gZhaEf+EB36iC2glX2N/JGPhk4Pb39RX2rxen2D9OIZSwvPHm66CKqlZXi/e/88GnJj9DzwWXp92jPbSP4BejWL5xJ82XTXgp35ZPtHb909JVUr9aBxaPt/lGLNx/mO2Sgmvv4em2yX/NYqPyqr/rN5/ze3fAZFhd4mtUv+BnZcw/8GC5if5H8T+lwZqxo4AABAASURBVKHpPpbOl0fsEflKVb//ZRL+64y0jv6x+F/n9V/h4JO58r9zaP9N3SUw/9568DfpfoIGv8/0PPojwqpj7Uda/rUg8c8/ytD6Z+dP4f3P+P4/E8tVlVWX5+F/1JB/KZ71n5f/N/r4v3edFHv+Rkj+h26T/v7Pkv6HvP9BBJ/s/f7u/vDv8Pnv9ftlxZSSdeZy9Z8rSR/8kwT/+fi9JMji741zpcQedQnZLthVXxWVP/0GG+sHMaKUQtla/Sd+xN//cMzb8+c/jP2fUOafJxH1/3kiofifMlZElf/UI9t38vIgJxfyC5DL1n82W5fo8guhpFxRLBeyulwg9f/NWfujsERRJjb9LTvyCyzxnY+8v/KdRV6+haYvr1CUC4+KSnmXQn6+j/85/tH6e8nN5z/R97U0fysuauFXx4YeSzrGLuZAecV/aqr6O0SWf939ty4pFlVVXnuPoJRCHtHHErleuYeT4t97ZHGx9wX/Rv3+3euQUurvWGjh32/+HZTapn+5B36CC2iL0vyLZJ46Q0ZmFicyTnH0WBrHT12kAoUhQfHDmLYk2JFcWZbDigVPMe8vqyi17+yuwKwq4/y5ixRV2g38WLLsWxQfF47t4p23F/Lye9uQtfbHUP+/g1l614aXC/s28djPf8eLG8/ZPqioKOdve5TqicrH+ePf8NsHf8ErW2XXJ5S2q+X9734s2fhrGRXnT7D46T/xwIJtEkcCkYPIFdEhgKsfn7ec82fOShyeJk1i8ERaBoePZZJdUIkyDJTEm6YoycvhXG6JPUn7fzT0cvbLqCT3wgVySryXG35QKi/MJTunWDwsDXJZr31aVXCIJx/9I88uP+r3cbVMwfibj633WdH75BlOn8+jXDP6m9j/mgZVPU9b3jIZYzkUV/it9v9elmGJ/vJcBvwDpYqiPM5eLPL75R/A/5egiJK6jykvJCP9JOknT5Oenkla1kVKqn5o1b9E4j/FRGugc2Hmt/zhwd/z0qZMP311rPsr/+5fqzrcyzi46WPufugvrD+lZVryoUe/fyz/RDDpP+0fys6z8u0Xufuxj7FVkyj6qeadH7e0xmelHN7+Offd/zgrMzTm1T4rs+eCIv+Y183/rlwzcP8Of3EluqOLzx/lhV/9gt8uOVqNbXu4unzFyyaQetkZPnnhSe5/fg1yzkN2rZqNNFz9aC465xxcy/33P8nHB+RmQFCsH+so4W0vQxV5LJ3/JI++soEcwdWP5qHfP21WqP+kwPLqea2qhl5s08WCtJ386eeP8fTqE7oqfvP9qN/8jf/aX0us+QdC4seFCuF/1hc/zvAfhVroJc6yKsnau5L773qcpcdLbOJql9rlv/VTM2XmndjNH3/xa/6y0j9TVP03XI7iq+DiuQsUlP/tfcLfssMepNIHf7v9x1qU9PiPwf8ZWDHr3nyau/68lKxKTXfl4dRLnt73FNsNWPbghaIDq7n/4WdYtt+eGfD92FjXrP5l+XKMnNknsu/6I58clVsxiZmLcibIL/vP+Fs8Lv5W/5GOEoSW4FSeP8Qrv/8jv3t/F+VSl9PrTzautbgfy6L+j4H/JszfTT4upu/gsZ/9ghc2X7Rx/XC7+D/+R4egvngulu7XZe0D04R/KBuCJ9kwFE4nGCY4VQU55/WY9eGUumHwj/ES3H9I5k+Ip3V3iF3ekjyyLxRiVcvWdvp1tWzbHNY5Nn70JH9480v0+usxfYhLUPITEqKoKrxgn90zT50mLf0Up/PKCQgycBoWWoaf13+Hn0S+2OQ2izn6zfv84ann2C3H0WCXD/4H9ZthiD6yV7WsKpkjvChl2X7XfjPEh7IpwG6zvMhBlJr+UTU0ms7OXukTbNofbVP8jbaqS3z/njytT022dRAf/rWcal5ik9ZZ62vTCK5+G8pbbYvYaussbySeqtvVpXYvaLj032WY4NbQiC+0n5T4wHcJptu9l3xgCk+lfJflCT8lMFuPav0s8bium6YFwtMSHG2b3+c+dFm3/5DPlXFtiAyqdaj0eimrgFzZ1pbbE7+Y8S94LF8J33zyGrN+s5AM+eilWcpSo1//5WxpDt5Sdq5ZxnMvvMfq/WcoyMuVew6ZNCnlwJZPuOeBp1ifpRGtq857lq2EeK0kl02Ln2fubz+tPntZ4lmN/1/P/vXGx4UT3/H7h37BS1v0LAR++H+dv+ZgVZZy7lwOJZW6xmXdxT7bP1XFcu79kDvve4GvpW811r9Svub3fy//1y2S4fpfZ/K3OOiO9UkH52Xt4+U//Ib7XljOlu07Wb9pKx+/8ya/efYL9p6rQAkDHeg1WapyvvXhDIiiQ/MkVFkh/nsiRc2ml5wjPPPnl9iQWabRZf/nlYCVQSHy9AZY80IWUErP8snnXxHa+WaGd02gSl9UahzJGkdnm4GEpNZV138s/1ibn06C+QpeUqwBX34L8EqeNQ1XwuxydYNdvkTjB2pYjV12+VK7hR9+Bd4VbVL0N8ivpvNnn1iru76cguAmTJ0wiPLNr/NL6Y/dp8XXGld2c35cC/0WkP0omcC9XkVso9akRrkoKfOPaI1zVbaxr/7R7Vf78XK7brsy2y2ivN+2yzooZdj6uGJT6ZoSTllJudhiY9vwSzz8IIFJQfh4KwrZu+JD7rrvzyxct4MtX3/DylUr+cvjL/Hm+mMUKx2FsPOTV3lMDhVCheWVmJJZSMexXRef+P9C9TzvPv0Mb+/wz1RX6ijoGpVjqxbzywWbsNcSYaC8JezdepCIzrcwb3ATlOikEa+mFTs1ULLdLD9a7/2rPuCOex/nk92nKKqSeBM9rqTTNguJ2Gr5Y0FXJGu4zlK023TZnzVE+Ah/f92Sdj/MK7wrSipsn/pyDvCXP85nU6bPbvRJ25X4SinkkTZNfzkLwH4u41qyqIkTBHpq00f88qU12EcduS3wXqWDH0c0wxJH1tD77LIQ289lObrdBsmPLl/KUq95BButZG76Tl5e8CErtuxk2849bN/+HZ9+8hEffX0OnbTdl+i1ThooWcNqYrbmrWE667qgyGP9Xb9rPBv/b9ghxtrzW2j9FjSv46SosFJ46sdC013KGiT5Uv0KPQV86fmx9qtgNW6+RKELCqQ/LMtDi06tiZaDWPU9Az9G+9cwy+8DrVNN1mwl/xjuVTDBEeFX0dt9bsPlx1bNhxUQS9d2sh6UlOL3kHW1boL6w+cqOdV6aRw/XJdEssB1H1XX/kOegiCPli0U4jMIoFmnFsQY3ksbHGnBkvGi38dXvcOvFmzG3q/K+mPbJjK1Dlqufuvsh2uKyzppuD9fDbdrwsOm1xXpU19FKeWXLtq0fpezRvFnn/2XisGxTWhZP4KKIv/6ifDyy6mm8SOjx440iYkJdG8aRbmc9K8cs1SnGlrpRLwCi2rUjNTQKvLlI11Nm81H2mrqWnclB5LsYwc4H9qZO2YPIEq3yzi5rI8AfvBoek2r3z/MNrwGXwRe2W6DBeb3s9/Oq/B9lZSWSZxpRPHnlbRSFaiFJbrVwDUf6U6Bw/FN7/OrV9Ziz2t40Xy1n8IatqRDvJOCIn/EIvJr6O23Tf3XP3bbJVx/+9Uwv0YSKfxQp5oWJRdyJdqeasDV9H6eNv0lOWJfDa5s2Mv8my5ZCzT8ylxDe/X7av41jK6k02U/jcbVPrLf1T71tygMGVM+5aRum2YkBXopLvevP1ylZzV/fpDkcK/5RqS2o0WsQXGp3+9azpVZU+m6xtXvmnwlXJe1f3Q/+6VZ2OVqPWxaP5L8WqJedZZxLyhQdJL5ciny+SF7FyB7VIktabhSlhD9NU8tzFJ45YRbLnsQXRUBglrNX/PQgB9mbyWl5Zb0l7+hRo79tkHWj8iy/HylD/TeGIJp2bY+ZlEJFVpOdbZ7wFfIkpef5bXN9klV1nTZi5WcY/2uC/QcNYEhrcNFtoUSWbZMob3aR9JQ/eh2u03jiGxdr8kaXo3m101watr8cIWSj/peHCS0aU5ykJfCcgdYZ3n9yRf4YL9/X2bZ/VBtn/CwaeX9wz4UIXaTkpgvrZQ+smuX6WzZNkx+ZMMj6uKMbUaHlBCKC6vnT2my8YS//Zb6Xz9/zVPjXrJXaHXZqibUbZezH6jrNo7g6rKd7SYvFRVVVNaM2SvbpWyj/OCnZi8fndKKRlFuSksrbAybp9DUvG3gD35q2mredvMVNH64DRX3Wn+Vr7RBY/nxa/A0ROeaes1bwy5n7Sf9V7jlFRZKxr0EgC1H9+8Ps6gmbXAV3I5WMGQ3UVJahe5XlzeT9+Y/x4pjpXgkpK7CFwQ/H4sr4X4YNn+7LHhXtkv1cpsgXNkmVSxBuBKmyzZcfnT5UraqZdTgX3r74TZeNY4ue2XuMF1w7ruP+Ms7KyiUI58+d1yWB94qH87wOFo2r48qLqFS6IWbnPIQ+yvYv20Ta7ZsZ/fe79m153t279nJhtVLWbYtnSpToaQ7tKwrs6iN7k+fjD95pIzfX8Jbt/ku6a39yOX2q+C67Yr8t2iFxmsG06x1c0KqKtDfnv5aJ78MLfvKfLUelq3HZd9Yf0fn6jaRXcPjSr5XlQUHuSkNDjWJinQQKW+3QwlvrZMYJXcUwSEmkdIWFW4S5BbtZbLVZAEC1zQ1OVLa3XJhq336o20ObL4/bIuOchAaIHz13vBKeRE18iy/7aJOje4yNOwYcAeZfr21fjoLrzDhZYhNkVIOcilkudbo9tv0mH5bNK7OghMZaiAmy3oBLmmPiHAQJbJD5SOGEkOdHqERvCiBR+osdNGRJoHiC0+wSYzUbf/od7iJ9oHWU0hxug3Cw4WftEWEGLgkxrV/lEPgwissUNk+8fkUQWEmGkffJwdJP0SGG+h7aZ/42+Yj+NrX4cECl/lE89f9awjT0GoZkcIjQH/UkU2u/hcDVVVclfxx7/enbtB1nXX5yqxhNdkn41QZQbRr04jAqiLZw1+JqcuW2GBRg2+/NdjOV8P9IIua+dWnjZN+zdu7hg++dzF+4vW0iDHZ9P58nlyyV9ADadWhuey7K686u0iD/chyh74HUUGR9GxVD7OixP8Hf9Ljth7SEfbbxr7yx/prfWtwq9812NJDeOVOKSa1FamyDpVUr0M17fpty6imk5cGXc1fgGKmDecK3bT90kRF9i7+/Mc3+PasH8UnG3ebp67KXsJyBNOifSPC5ZRW9oM+pTb92zwgw/XfxhslPe+T5T2+eRe6pUbRsMu1TBp1M7OmjeeB28eSenE9v3vpC/x3yAqlLmfTYdiK2XOm4S9rgJIJVL+JasJt987kmiSPXTVME6Oa3jCq+eiWknzO5FcQHhZOnQb1iJCJUyl1CVcphT+pq2BKqav0MX5QV0pRk5RSV+DWQK94X9WuLjUopa6gk3J1i1JSvpT9QKUUl+zSZclK+fH8cOyklPqBHVxKSvnxlTIwDA0OJKV+HZKad2DutJvo3i6FJonRuKRJCYJSNfhKIHpYg1IGpmw8pCQ4tl1DAAAQAElEQVSTvYkpOEgyDYVSV2SB/fBRSmFIVqoGj0tJqRqY/203CMww/HWllA268seLQstFJ9HLEBylFEpJ1jDJUpSJyicfM6K5pmdL4mPrMWT8LUwaN5I75s3k1sHxrJj/Cou2nhFsaHfLZO4b2Q4lNeUwseVLWT/KMHAYuiWOcbfdyoQO4ehkCEwphVIKKaJT6rWjeWRKD0J1xTRRppuGPQcybXALAgSmlEII/PylrJQSWoXCn6R6We/ubUiqk0j3a1oid/4ow7iKTik/lVLKD8eflFIopeyKUsouK6XfNuiKeg3Mx6Ft61mxtwAlKCq8GXfcN5Ue9UypIb42UEpV5ypOpZ8gK9+SthqY/y0A+1HKX1dKCa1Cp7q9hvHzmX2J1BVl2PGjlKrmqTRUskKJI5WSt2TDLgvYfvwwpfxvGyQ/SvnrSslb6vqx7J2Rwpt7lJeffo/c+r2ZPeEWxtxyA6NHD+PWCT2IqvAfIE3jStsu81BKYUhW6vJbKWmXrOHYSWEYAsOflJKyZF1TSl2m1ziK6qQu2ayUgd6gIRdXlpQ1L3RSJkpdgadhkpX6a5iALz1K/XW7UlfA1CXUqwpKZAsaojAu09AvWcoVDlOhVE32kyhVU9dvDVNovZXS9eqMPylVXbffGnZlXcoahLqK3uaFTv74MgzpH6kaootp+MvgwLR9Kjw0b2n/4aNUddsVb42jlIbrEiilxFaFPym7rlT12w+8+remzX4bdpslC5USvUQdu65/VHWl8bXjeGRqT+yVyjQwBK6Un79R/VZKVcOxk1KKK9ukik5KKRuOTtVlJWVfyRlWLdnISbm4QnoNFEpdzlQnpQyc/mDDKWXTUNhJykoplKrONvDqH69PSSw4cGgch+OyHoKmVDWdYeBAkteHpUw8TofN0zANeQtcHqX8uIbgKsEJadiZ6WN6Ee+UTatuF52U8uPISyBXP0opW7ZSNTiX34aGUZ10+YpsQ6VuGD+Of2jTl6xNq0IJok9+lFIoVZ0FBlK+glbzMQ3s1PCakTw6vS9Rds3EMAxMoUXTOHRdYSddFrhSCqUkC1BHuLyuepSStkvZ36TUlTDlB6JQP9BJ4ed49ttVfLEnD6WqI0IKSimp61xNji5fmQXuy2fDp6s5XKakonkplLoyC/hHHqWuxFF+jB/ApGrDlVKX+tCo1p8fJhlTGIbg+RtUNZ5SytbHD736Vykl+ApQOE0nQo5ODoeBUupS1jCllI2r1I/DsZMSHtKOTspfrsY39JuaJDi6rrMIlReE1GPGXXO5sYm9C8A/Bi7j2ZSCaBiXYYbU5QFVxIal6zmQW4nCn5RSKFWd/aArfivZte5LvsqSdoHKmfoyrqYRGCj+Wpbg63ZD4TANdJKhi+F04XTqNgemtNktRhhDZs9lco9EjYZMASh3CNcMH8kt7eNsmFCgBF8pKUk2JCu75eofpdQl3xuGQqnL2ZAy1Umpy3ClVDUUlGGi5yEpYciYcvgqwIhn8p2zGNY8DJ2UYVzFV8MEgGEoefmzoRRSAbmE/OaLleyQL0gCkRGkBHxFFoyrHx8+lO0bdDIMNC+lqmk07K9yddsVOEopmw6dqstKlyUrpVCqJgtAHqWUja9UDVzeSLp4nGWrt5FdqaRiXUGn7LIA/+pRysA0lcAVTvGnKTylgu5vpRRK+bOG/TAr5W9Tyv+226vLSvlh8hI/comPUjVwdZUNSFLqcpsUBaKfK2G6rGGXc4V0eZls3wxT4ZLLKrdL7JCYdXsUbnk7HMqGewSuNJn86Jh2CtwpMKehMF1wYf8a1h7IwS3lCkcyo6bO5brUQPRFhCl8XJIdkt1CIyxQQueSsuav+UiVK5MS/h7RR8vSNA7DP/9qnBparYPmIaqj8bW+plS07lpf23GGQuPU4Np8LNA62XxFjm23U3MGTSt3ZTapQ/QLkIs9r3z1juswjNvGXUeoDztmtV7aJv/bQBP4p1oDJax8socIdJXx3ZrP2JLho/U1NzJh9CDGjRzIlAk3M25gR3J3fsny/TmYDlCit9ZT+0O/RW2kg/F4DNzSLiqj9dT6I0n7TNuk365q3bVNNT7TPOyy2Kd19Eh/Og0hlMcpftVttiwpa78JWC43LZSMQSUVKaH7yyV9puld1TKk6arHL1/ZuNqfWoTdF+I7m7+8tVxLqGrkan5uLVfaanC0PrbNgvfDxxAdKs8f5/P3FvP4swt4bvEa9pwpxyWxpmPBystk+Ucf8dSzr/H4gk9Yvy8bS9qcFLLlo/d4/JnXhe51/vT0q/zljRV8L7QedymbPxR+0vbEszVty/n+dJnEQBlbPvG3Pf70Av70zAJ+84fXeH/zMXyh4p3cDFZ8/LFf3qsfsW7vWXxiiyFBoO30629JnECAVcL3qz/lsb+8zpPPvMafnn6N3//5Rd7anE7emd08+4c3WX3kAsotVF4vhvj57M51PPPcAtH7NRv/D39+iWcWbeRUBQS5Kjn6zUZefVlseuFd3lt7mHLp2/TtK3nyT6/w+Itv8bTkp8RPv312MRv2ZLBjxQf87imR/5y2cwHPvLGc78+WY0pnuR0+Tu79mtcXvMGfnn2T+R9t4mheFW45YJdm7uW1l+Rc/9VpnKKf05HP6ncX8crHO0VmMevfW8Szb23krLgkQNpP7d/OW6++yZ+fe5u3v9zJuXIl+2PQMXLxyA4WvfEWfxJ/v/jeOvZnVxAgfVRZBSWlYvsVj1IKQ4JBXuiklEIppYtXZaWUDVdKYZgmOnllEBqGgdKVq7LCsHkqlKrO1KTqejXchkrZkKyUEjrhpyAn6xxlbjexkdEkxsbSd+Rk7hzSEp18FuixYwgef5UUSga5ErgSBNMQflIGA0OpH9HHbpSfK9o0nkAEWZ7LcA0S0QIzME0lVYXLMDEFXypXPUopwavJ0iSEStXUq98C9j/VdWm3/anAXact9943kQ51/BimaVzmZ5gICjVnOF32Y9X+/rs9YPy7BWj+lkxkJRVeKkqLqJKJqqqyEis4gSnzbibwyHdsOlQkASAb7o8+4OlXFvLk84tZc6RIk8riKJHmL8mvJZdy/vDIPbyV+a99wa7z5VB0hOd+9QS/e2UxL774PLc98DQf7TovU2op323+lrQz51nzyft8vuOs8Kjg+zVLeeG1D5i/4F0WLN1FoQ/Of7+KX/7heRat2spbz73K4pWr+N3vnubP80WnPz/Bnb99g882fMOnCxdw9wN/4KmleykRblDOrpWfit5v84xMql/svmBDLX35LhOKrpzatZGXX3qblxe8xV8WbpJNooZ6ObZ9Nc/Pf4/5by+Wyfdj9mmGpadYumgx89/6kBdeXMjSXZqfj28+foV7f/8K819/h589/Dt+88ZKNm3YyEtPPMHcn89nfVoxVJ3krSef4sHH32LBa6/ys1+/wEffnBY/SFPuARYIvwVvvcPvn/2EfXk+ijK38duHHufxt97jlY+2sOXrXew77fd72s61PPf8Il5Z8BpPv7eNXPGRElt3r/qUx6V/Fn26kp2nC0HpELLI3Ck2vvoer72+kGfeXktakb/ftB+Q5Ms7zMt/eIKf/+Ud5suF74O/eUUOx9o2aSy/yFpZbF9440NeeflNFm3KkKs4HwfWfiA2LGLtV8v59VMfcuhcpSBfCgmJGamKn3VEWAWHeePlRSx4610ee/ZDdpyVFc/fLL/6sSguKZf4q6Qwv0q+uHmRkKRu55HM7Oxk7frvyCnOY9OSj3ln9RGRn8fnLz7O3EffZmtmkTAoZssS8d0bGzmWdpSP3l7IF3sLBa5t38Arry3i2Rfe4u3Vh6mikC2fLeHdZbvJEQwqc+VA/wmLP1nG/Jde5+21x4U/HNrwMQ8+9Cwvvf8+v/nl77nvqc9JL7U0hd1ndkFKxSVlVFZWUFzqw6q4wEcvPcEDTy7i9fmvcs8Dv+P51Ufx+XL57IXHmftr0fek2G7JRuTDt/jD6xvIxeLYli95+qV3eealN3hvyymBWBzfuoqXXlvMM8++yftbM8k7tZ+PP/ycz774jJX7z3Nq3ze8/O5Sdp0stVUpzNrP26+9y+uLP+T3v36cNzedJVy+MJcf3MCTL7/Pq6+9wZ/fWIcf3SJz93peeOU93nxrIb998TMOZWWzc+0y3vz0G87bHKs4KH373Kvvs0Di+sUPtpLnlcXy2FYee/SPPPHGZzzzpye4/RevsCFdDw4o0zoseJvnXnmTZ9/dxOky8VdVNssXL+I5seXp56TvLwgT4e+zDJQER4bE8vaKpoy5tqEsMlVUVnnt/neENeO6Xv7DdM6Rb3hlwWJee3MxTy34goMyPihK4/U//ZGfP/8BL73wEnf+7C+8uXw7qz5fwq8f/i0PvrgaHZJFB9fzs1+/xIY08XtpGgsef5q/LDtEZUkmL/7xzzz63AfMl3lp3gN/YfG2M+J78IqNz8x/n/mvv8WfXl3BsUKxA1FW9EZfnMvbl5fBp++8w7Pz3+LpBcs4cNEn0GK++uwjnnt9MU8//S5rj+sY1GNC04Ov+DRfLFzo98ULH/N9vpBYxWz94iOefeVtnnxhIcv3XRQgIsaiZnxSfp7lC98ROR+zeMlXpJf4kDVaPiIWsXXpxzK/LeSp5xay/FCREJazY80Snpm/kL+8vJDP9+dTfGwTv3joj/xRxvBfnnienz35IfvyfTI3p/HqH57kxaVbWCh2vLryEKWlGSx89W2x6w2efHMNp8og78A6Hnr0KZ4W+if+9Gdu/fU7fHOqXPRUFJ06yBsybl5etIyP1h1B/2sFQ1qsgjQ+eEP4vPIWT7z6BftzvAIV9SxLxoNll8/s2cKzz73Fmx8u54WnnuXZzw9QcCGNZx57gje2nheciyx74xV+8fIae6xaBRl89Obbopvm+Tn7LlYJjuYp2efnWZSxW8beOzz38ts8JzF7TszUf4ElYm1c/49P+ln6s+wiW778jFeX7LL/cuDQmg+5/+HneEni9VeP/o6Hn1nK+q2bmP/cM7JuPcXH+wtsny147I888uxCWTde48Ffv8IX+y4I2wKWv/Isv31rO9ILpG34mPsfe5M92SUc27RcNs4beUc2+2l5FVzYtYI/vvQ+r8xfwDMffEeerbpF+jereeqZd3j7ky/ZdPgsVbL5Esbkp+3kjQULWfDmezw9/zN2ykFHw33VNuuykhNkpRyYvvzsPR64/3c88dEuSnVD6XHemb+IBTLO//D0YrkE84LTgctRxu41n/P0My/w4B/fZWuGdLQ3i4V/eV7WnFV88fki/vz2d2Rs+Yjb//geJwqVxFsem5cs5ilZn597/nU++Po04l4sn9d+U3GRle+8wF2/f4s3Xn+D+x/8I39atIHN65bz+B/+yO2/eYftZyu1Vhz/aiUvvrKQl155nWc//AY9xE5/+zkP//JJOch8zOO/f4y7/riQvfIRrejENt79cD1LP/qYdfuzqSo9w8cyNp+R2Hp8/uccLLAoOfktzv28bwAAEABJREFUjz38uOj+AU8/9RS3PjJf1l3dE0X2mFywbCf+cKngyKZlsk4u5N2PVrDlaCEO8Z1WKmf/V9Ini8VX78kYXcEhWViVNPhjx5KYAav0LMveW8gLb33MS8+/yHOrjwncEnuW84LMla/KGvuyxJ2e5gqPbOF3v/kTf5TD63N/fpzbf/kyG05W4rtwmMUfruWLJZ/w2dcnqPKd46PXF/Hqmwv549Pvsu5ogUiFqgvH+VD2BPPfeZ+nnnqLj7cf4eh361m4bCMLF65ijxz2vOf28+arC3n1rQ949uVPZFxW2LQ1Ouv+wVvApqXS/uaHzH95Pk987Pd32ak9Eldv88pb7/PcK5/y7ekqmcRPs/D5v3D/E4ukbT633/9HXlxxwu7fS/ORlqAdI29f9T7qyOYVvChx9qL05/OffIeeXhAqrYcd3rKqHtq8tNrvy9DrtlG9P8naKWuRjNdnX3ydN1YeodIq5Iv5T/OzP7/Da6+/yQMP/Ja/fLZPxqiPHZ+8ziOyFmeJ7Iv7NvDL3zzD8uMV5O1fzc+l/595ewlP/E5i57F32HneK1hi0tmDLHz9XeYv/IQnnnqVpftzKc3czWuvfcbm43k2TvrudTz7/CLmv/YaTy3cRrHlY//q92U+eJ6X33+PX/78d9z/7AqyhWX2rvUsXr6Gxe8vY9PxUnzlp/lEDtDPzX+LJ1/9jD3Z4kfh6qsen2f3bGLRJxtY8uHHfJ2WQ2nOQZlT3pE4e4vfv7CUYyUWuYc38csHn+Qv0td//sOfuPu377Bs83bef+dV7rrv9zyz/IjEGZgOh8TFId5/bSG/+c0TPPH+11zQcs7uY5HY/uUu7Rmw8jNZsvgzFr3zHo+/vJSDMsDO7l7Bzx58iudkj/CH3/2BOx97n/25fl31XxeKylTlHOal3z3Jb15axAsvPMudP3uOt2VdXfH5ezzy0G/4+WsbOF2pMX2kf7uGZ198l2dl/n9L9jl+cBGbP32fx1/5kPfk0uGgnAHccilUdmYv7y74mHXHioXY4vsNy3j+5cW8+PICXvl8v0QHHFz3AT979FlefOdjfv/L33L/Xz4jXcL54t4NvPPFJt5ftNQer76CY7z96rs8N/8NnnprLRlFlvAUm3Ww2SX8q7WATaD87EHefvE1ifPF/FmvuxfsUYF2G9U0ZYLzrvh0/sKPeUIudb7Yn8aGxa/xq1fWkys8srZ9wc9kj79ZLv0QbfesXcrTr7zNM8+/ySffXRBQPp+/+hT3Pf6OjJvXuV/H7Bf7Zb9ZyfYVK/jos9Us+lD7roLDX6/i2RcXydy3gBeXfC+xJgIk3kTdanWq2LtmKU88t4iFn65gR1YeVvVYyfxuHS/IXvrZF97kzTVHKbeJfNgvYYOviO1ffCh7kPd4dcFb/OXNTeQhPji1T+bkRRJz7/GXVz7iW1nYlZXDJy88w+NvrZS95acy5y3jow9e5pcLlrPio/dl/V/LOcvHATlX/EXm6qefe4dP5ewm7Cg7Uz2m3nxf5p0P2XyiSIOpifnyctAql5/dJfusJ3h20XssmP8Uj/5hPp9t+oaVX7zNb3/zG/7y6bcUKFmSynPZuOQ1Xv/gI1599U1WHS7El3ucL5atYc3qz1j13WGysg6xVC7wvkorJSAAsnet5A05jy16501e/uRr8qUvfXlpLH3vXd6QS6zX3lnGiTLhLd7R05RTeTmx6X1+9fx7bNnwOc+/8T7fZ0Og6cNnQJV8KFiy+G1eW/gW8xetJFP2r+e2vs/jEheffvk5Tz/+GL99czMl8rXaKMjk8w8X8brgvvTu5xy44CVQdDq5YzUvLXiHj5YtY8ELL/LWhuMU5nzPC3/6E0sOVRFUcZblbz/Nnz7cgc8sY/+Wz3lv+TZyAbezisNfCd07C3lV1rcl32Vhmdj/6sTuXx94ggwydmxg65kQbhrWh6YBF1kll5Z/+P2fefCxzzgb14BxfVM49O33shcTm7IP8sGit3nj3Td59cONnK20OL9nFb9+9Jcs2JhGVUEaC19+gc93nUbJJeTXn77Lmx++L32wgE93nJV9ZiXfLnmBX8qF6cLFb/G7x37HX95fw9av1vH6CyLzj6+zPasMZ4Xwee4Jfv/S2yxePJ/fP/kSy77PFp5imPSL/NqPU3x97OtlzH/7bV574y2WfHMKJTbK6JV2S1YMcKlSvl+/hAWLPuC9ha/x3Mfr7Xm3LG07b729iPckNhe8s1TmLi+OojTee+kPPCZr7ztvPMfDjz3Ju2u+YfPKj3niT7/jt6+tQh83TSXsax6JaaS/Ob+b+S88x1vLNrHnyEG2rFjMEy++xOaTFkHFB3lVxsZrS9fy/ZHD7N62nBeef4YPNmdgSD8f2b6G5Zu/4mB6JifTj/LVioW8/OEqThZ75by9luWbvmL/pbZFvPzBKk7Jvj1z11q+3LSNQ6fOcDb7FBmnMrH/U5OFJ3jzpWdZsGQ1u215q2x57208jiXzpwysGu2xpOSS8/6p/ZtZunYrR7JOkpWdxUl5nykok/XlKKtl3Ow7mw8OQRb/G/LOz9jJ8rXr+O5IJmfPa/xMTp69QJXTx+FVb/KErF9rvtvPof1fs/j1Z3np082czDnD/u938M13W1i+Zh1f79zBzoOHOZ+TS9qeDSzdsI1jmZlkph9i46oPeOntZeQ6LTm/fsRfnn+VZdv2cfjIXlYvfYsnn3sdfSw38tLZuuELFr6/mM1pPkICy9i7ZS0bvjtGpaucQ9vXs2rTHkoD4fTXH/P0i/P5/Os9HD60k88Wv8wTr3/BOblkPr/jC558/iU+3vQdh48dYOOXC3nqpfl8daqKICeUlovt4i3tLyhiy6IXmf3gc7JfF79QwFoZ6398fTM5lX48/9xlcVrW9+dlbn/2xTd4Y8VBmcPBUJZw0nj+XLMXKpI93i9+9TiPvfwBLzz9LA89+YHsz22GlJw5wNvi02dln/rk2+s5U25xbN373Pen91i3cilPvPIxW3an8d2BTLIPf8MLH3zFiWPSd58tZdHaI/ZYMKTv/BKv+K2GeWXNeutlGR/vfsF7649SIEu5IWhWYSYfV+9HnljwOfuq90E+n09a5Qx/aDO//8PTvLVqG2898yKfHi4m7/i3vPTiWzz7yhu8vGQneRayhlbwvcy9jz+3qHodykc5tATQC5XNzVfA1ys+4VnZgz4l92Ur9haA+Cpz1wbZD78nY3wRz76z5tIaXXH+sHwwWGjvxZ78ywKW7L3IqT1f8/y7yzhg75uqOLTuE/40/0NeeulV5n95QCJdWFKbfmoPVPf0v1eskl2CoUDJ2zRNHA5TgsuHFdGQ1KBimWjyOfvtFyxJC+PW2eOZ29Hk44XLyQEcSlDl7X+kgk+2ZhCeXBdX4RnS9cgOqktyUCn5AY2ZOXM2Uzt45ZJoi1xyBdC+eyuSYqLpOXAYA9vFcfqbL3hjSz79xwxj6oQ+eHd9wesbzxDVqi1B54+TVRlL715taZKSSpz3IlZiB269axb9gzNkgjpDl9GTeWBECgfXruVQIVTs+5KFX1cybvYk7hwUw5r3P2O/TEpKVLX0j+hbGVyPYRMmMWf69VQc3MBXx734MtfzwkdH6DR0BFPHDmVgsyhQFXz93vt8Vd6IKeOHMu36RDbKBdSWAoMWTetRnFtK6xvH8otbr6NYJtBdVgNm3DmT6yPPsGTlXnFWEq3CKykMqM/kSVO4Z2ACqxa+z9cy6EyHm7YDRjJj8kRa+w7ywfosgurWJ5IirLo9mDt7Au0dAq+eDJ0x9Rk6YRyzZ/Sl6Ns1bD4DZbu/4MU1uQyaOJJRg/vSLDYAn8ODlbNLJrZdNBhwC5MmDqdFhWwI3/vanlSVTKlewAirS8PACkojmzF16jRu6xXEx299xCFZMI/JovLZqWjGTxjKtKFtOCIbx0+PeWnaPJqck8fJi2jCgK5NCLQqhNNfP0pAXly07j9c7JtAR+cJlmw4JlDQ8rGTwtBBKBDDNLHjUCYxPV82bR5P2bkscpxhtIj2cSL9jMRYGNd0rk/RxWx8wUEyFwYRJgtsUsu2pCQnEViaQ0aOlpzJB5/uJvGGcdwxczDt4w3KCKZpokHa8WyxHo6v/ICPjgUzbMzNTB3dhZMr3+G9AxU0bhlLaX4xdXsN56EHRxNzegtLv8vFTpb9Kz/K1lspAyU7XOWKIjXaQ15FCCOmTuGBoU345vM1HLPC6dU9hZLTF3AGOkHwfY4oOnRoScCZr3lz5RlumDWBO8c1Z/fnX/D9icMsXXGIJsPHcufUG2gWDZ74VOrHRdK2zw0MEEDdBnUxczI5XaRArpqWLvyUM7HdmDhyKD3iFReKLfQ/R6pyRdNvzGhmTr+RwGPbWHuigooz3/D84t00uf4Wxo8fxoD28Th8ITSt7ybzxBk56EPl4VW8/HkGXUYMZ/LEwURlruWZj48RIWMvuDQfkroy7+659I84w/srD4Mvn4/eWU5FqyHcPnsyyRe38cGmdNnjbWa1xM+86WOZOSAVfQgQhUGrLePvXPppXAlxhDgslOwSHdX9byolX7gdUHyI+W+uJaTrjUwaP5K+YRlycF1DeVAy9cIt8irqMH7WDOb1DGLFl98S1flGfnb7INTeTXIxUUxQaiOC5dLoTIFEekAyTSPg1Okc6Ye6Mr+VUehJZqrMS/M6ulj7+SayEVMcofQYNppZ04YQd2Yny3fpfnchgYb+5+dIBG748FMOB3XljlmT6RN8jHc+30NJ5jd8/E0FI6dJv93SngiPz44x5TeWDLmk2ZCXzO3ii6k9k+2/tDj/1ed8fCiQGTJH3dY3ghUffcGxYpAQEVoF+Nj28WJWnEtg8uRbGHFDB+q4QEku+WYpiw+4mSXz8u09g1nx6RoOHt0hl1oXGTR9PHdN7EWcXOgHpiQRUF5KTJtB3HbnVPoEHZWNxgYqg+qSEFTCoRPltO7ek851C/nw9c+4mHK92DWV1qU7eH35CcJSmxFZlYenaR/uvHsOPTjKZ1uOiGalvPfqYs4n92X6qOu5qWsDPDIOnNKy7f0PORjaizskFq4JOM4bn34nXgPtQz3WK8/vlYuGTcT3HcL4odfaX78z0y8QFJVMvMrjdE6JIEfSpl4A2Vm54kEf295/n71BPdA+7xucxhuffGvzVCLP0vNHeRYL31uLu8sIbp8zlpi0jSxcfxbcbrBvG4Sl/ch41YcQTyRNYx2cyjwrn6WgceNYymTMJ/UYzsP3Dic4bRPrsuKZOmcOo5pVsmTJVizts5AqvKEtmT5jErf2DeajBR9xtCqYRg3cnLP//xMguWl9zIJscryBNGoaS0ydRgwf1ZPkMNE2pB5Dp41m9vRu5Hy9lq3noerUVp55fy8tB49g3M3X0yklGqWcUHmWN95eha/FACaPH8F1iRd4/a0vkGUDw7AkRmyDZM/nw+uOpveNI3hkcmtObFzLN8LXwqBJryEy946nZ2gWn607AqYTb6VJk57XcvciHIEAABAASURBVOu8WYxNzeUVuTjLIZ5OiRXsPZpD3bZd6NwklnqJcVTmZlPiFTmVPqJb9GDm9FHMvCaMLXJwy7QQPRH/CoIrkjaJgRQU+ug1ZoLo0Zb09WvJjO7KnfdOoZ1vP0s3nxRkC19EKuOnjmeuxOjFnWv5Wg598a2b4izMJ7LNQO68fyaNZOx/si6NoPqpJERE0X7ATfRuFsmWdxexP6wXd0ps9Qk6xisf7CIwqT5Rsl5WxXfm1tvncWOdLD79Yo9ERhBNkj2cPZFNlUwn5UfW8aLMaz1GjmbMkH50Sg6iSp/yK9N45e0NhPe4hcnjRtAl8CgvvbkeubdDSazIMELJzLju3XfZVJrK5PG3MGNsTxpFuMnet5H5yzPoOGw4UyYOJDxzAy99doTA1PZEl+djJHVhzl230y/8Ah/KR3UjqiF1Y6No1v06buwscVJeQf1uNzJzyngGJuXz+eoD+LzlfCQf3I6HdpI9yHAmDepCtGkQ3ySR2Oh63DiiPy2jcnntpU8pbHItU2QeH5xaxJvSjyerEF0t+aCL9L6XvUsX8/HRYEaOH8rUif1oHR9CRd4F3ntrGWWNrmXauOHckFLAW3JpmOWMp32Mj1xvOCMmT+Xhm+qxc/kqDmqeCvEnVyVlGFL3YUU1YsK0ccwb151suST7LtMrcEOGu090geIDK3n+syx6jdZ+H0CbeBde5Yay/Sx4fw8tx0zijrl9OLP+M9ZmO2nWKJicfA8DJ47nkXFt2Lt2NfvzDFo2DydX1tEiH0Q2SSWsKoczORZhzepi5uUT2uI67nxgFi0qDvLBBok17znefPkTchr0kvX9ZmZe21LWIIuApAaElmeTmVshevowI5LRe6pZ0/tTtmsNy9MNmjWPoiivlNT+I3jk/qEEH1/HJ7u9xDasT0J0AtcMGkiPBk42L/qYPc623C5rwcCY0yz8+CuKLDCUKCnc4xrXJy4yjh43DqZzcoTEm5uuN41jxuTxpBbs5v3NFwlvVJ8g+VBg1OvJXffOoqvrIEu2F3Pt2Cncc1My36xag/6vbbnlEo2wZG6eMJqH7hyCsXMp81dlQlwzErnI8bPFgMXmTz5ht6MVc2dO4MbwdF5auIvYlvXkwqaQsLYDufeBqTQr3cmnm7NEQ1Cy3/Ja4IioS/2wCnLMZKbMmsvUjhYrVu0ltucIfj73Wgrlon677CEo3CPz7yE6TZzAHXN6krnyQ9ae95K19n3e2aMYO2UoI4Z0o1Gki5JyE0+dJAIrLpB5UTbglIv/WzJx+ljmjW4tc9VqvsmDJh3jKT9XQPI1t8gF7jgisr7m4635RLaoS0xELNfceCPdUhWfzP+UnEY3iL+n0rJkB2/LXklUR12aDbGTHwZlRghdbxjG7Mljae7dz5JNJ/ztXi9eoaIii7fnf0JeSk+JkVuY0b85YYHBxCcFkHP6IqVAQpMU3EXnyPMaeNNW8876fIbJmn3n8Ppsfu8D9vpCaZEQTF5JIDfJPv6hMW34fsVaDlY6adQsidjEZgwZ0Yt4RyVmncaMmzqOuRM7kbVlDdvOiACl8MkkIy/ydi7j5VXnGDB+FKMH96N5XDCWvZffzcufHabz+HGyBvYia91SVh8vAWVg+XzaEg6vXCz+dzF84gimjB1Mu1gPeg15V9b1wtTe6LltSGoJr7/yGTkqgkb1TI4cuEhSmw70aJtCvZhgzhzLIKppR3q3SeT8tlW8+W0FE2SPcdegBDZ8vIKMwhw+XbyU7IQeTB0/nKEtfLwvH7eOlljoNQlJlqy3FVUQG1+XGHcJJUEtmTR7HtclXWDN1+k07juO24Z14uSONRzK9mHgJKFpHybPHMHwRlWsW72FynoNSIyOJKVtf/q3a0S95DjcJTmcLagUfAvLXY/+I8dy+8heVBzaIh9+CkiT89BB1ZLZc8Zxc9s64lNA9gbK8lLpNKmbGEvp2eMUhDflmg5NCPB5qVIGwUY+6z//jAtxA7jvjsm0ZB8fr9mHL6olQ0aNYUjnBPLOF5HYohUJgZVsWfYJxzwdufPOyfQISWfZup1kn9rD4i+/p1H/0Uy9eQDNgotIP1tIZP0UAiuLuFBQgjsqjobRDnLPX6Ay0EPD2ACyT5+m3AWlh9by3vYcBsicd8egpuyVy599FyzZJ5ogaxAOcBRksW1vJo269KaBWcjKlespTx3A1Bua41UO2ef5xOlxhJYVU1GZw6rPvqSs4c08cNsU6hd+w8frj1G3a396N3Bx/OhxTp47T4DMjf3bxUNFGSEp3Zkie5MRbUPZvnEduaaT1OR4igsqaHntOO4d14dcmd+POZsydfYMugaks/Krg6jIZNlPV1AS3IgRM6cytVska5d8IBf8FvrI4xP/G06xMX0dn2zKps/ESdw5uAn7Vy9hZw64TIXXB54Ai/Rtn/Lx7lKuuXkYU4ZdT/u6kVScS+PDJbK3azOYaWOH0zniFIsWraQ4MZnkYIt8XwxDJs1hSodQtq3bRkD7m7l98iCcxzexSTbU8t2O6ns7++10Wxz8agP6Dyn6TPgFrz//JH+edS1m1lH2H89g57eb+OpYAd3H/IxXnv0zL/zyLjq4Mlix4WvS852EBjip06w/D//5V7z2wp+Z0SWCY8ePcS63jKDAAJmO+8kH1Oq2rpEcOyFtsqcNDg4iOrklo6bM5e4Zc7jv7juZemMz0lYvZ/2JArqMetCW9+Kv75E9yClWbfyKE+IfEXfVFtbCwBXgIbROC0ZPnss9s+dy120PMu/6prgcDkLDwwh0ObhySnS6PQRGJsg8OpUH5s7j9nl3c8fk4TQrP8LS9Tsoi+7Cg398ktee/B1j2njYu20nAW2n8M7b83lsTEcMZxIj5/2OhS/+irEd46iUc31042v5xeO/4vWX/8DoTvHknjjE8YwzfL1pDWkBHXngV0+w4Nkn+fmobhSnbWX5lmzMwCDCwqNRF/bygZx3jhcHEBkRKn5zoyyFJyiEkLAIgsty2bRpi6x7qcz75VO88cyfZN+ZxNmjuzhy9Czbpf+OlKYw45Gnee2ZJ/jDzOvwpu9m1aadFLuBKp+Yr8QHllSCaNe9OeTmoQIDpe4hJDKIlFYtiHCCVwefofBd3MFrH+yn/fhJ9rp2ev1SVsr87HIKkmYjlPaj2UohqG5DoryFRLXsw6xbZ3NDxAleeWsN+d4KVi9aQl6TIegzUKOcLcxfc5aUxokUnDpGXkwz+nduQJzEb5cWdYhOac/kYV2pn9KQep5S0uTMI+xFd/v3ih8Ln/gIXw7vLPiYs4l9mTPmBoZ0a0CAzGOBVPHVB5+w39NB1sfJXBeeyUL5OFciHMQ88QdEpDYlrOCkXAqH0bdvF+pI/7+9eCtJA8Xm2UOpkj3Fx99XULp/BS+sOs/1E0bZ61CL+BCZT33CSdSSwWRIKe/AVj7dWcaomeOZN6oLcSZ483fy8qLvaCBr7qQJI2hZtpNnFu6U8Z0rH7s+Ijupm6yzNzPr2lb23FCnfgOJhQyytZKif0BiC8bNGsncUU05smYdu4tFkCl2y6v2+ek8oPv3p5N2hSQlgawjVUkwKVXE/gMnKS69wEb5crclLZ/yc+fQ21ZTVY/CK2h1UclKEyCTo0NHvPByujxEhAfhlEFct1EDwuRwpTd1hsMWgMPlxGF45YvaMRxJjWkQ5MDpSaBTs2CO70mjzCcTZ3gcKSnJ1G/bifbJwRiuIKKiInC6QmnapB7RATKpOZzEpzQmPshHaTlkHjpBthwAd2/cxqp956koOk/mBaqTnk0MGtQNJWPvdjau28OFMhNTFsq0Hfsoq9OUjjEOnIaLZtf0oaU6xTeHS2gsmzSXw0FAw2Y0C7zIdwfKRX8X7tBIYsIELhNKk6RoAoKCxN4wGjVNwFlSgReFSy8AHo9M5E65DOpJq5Acvj98ERVSn/CCnazZsp2TBaVUeqtQ4nyXO4DIsAChk8uNxnXxlJXIERiSooI5s/8r1q07SL7MB6avir17jolOrWkhOjhEP1MWKLfHR87BQ1x016F9UoDo46Gj6J9/dL9c5gPSN+gkb5fbQ1CAS3Cc1O3Qg0ZGNvuPZ7Lr8AXZfDcl3Cm+iGtNm7hKkXUKQybxsLAoUmRj3qN7a+rFBWlOKGW//D9S8UrJEZZAZMkese9b0nNK5WCsodLwdx+FoaSPDIkRy5BwVDg9bgI8TtuXYR37cE1kPhu3ZonMbHaeiaZ/mzAMibnAoEA8Ek8QLpvFQj5/YyFrjlXRomUjglFYLo/wcct+zseu708S26Q5MRKDzsgW4ifY+3262Bck/ggkPMSBK7AuqfHhEj8SVPy9pHA4XYSGhxIksV6nfhJJQT6y8xWRrXtKPJ1n3b48KD5Lts+kSdNoSvft4URhOQe2bmfN1lMUleeSVeCmbkAOH7zyERuz3bRuXE/sqcAnMWGYDkyxURlO9BhzOQxRKJ+Mi2XEJ8TjlL5PSYyhNPcCBdISVDcR48A2NmyR+C6pxGFVkLl/L0WhTWlfP0DwPXTs0IHUuoF4LQeBOgaE7sTuw1TENqJdhAOnM5yeLePJPHCIEsNDSEAgYRKXThmDjevH4ZFDsZV/jsPZF7h4YhfrNm8jMy+fbNl8E1qH8qObee6L7ZxLbkfrKJdwt5DQsN++Kh9K26OkesVj+Xx2rejwUdK9MXRuHCp6OGjXvgnWqaMcUQZBHhehUWGEiq8bNkwhLspDRLATV3wSTeNCKCurRDmceNwu8RmSDBlLLlxO7TOFU2I+LCxY+DppIBcYYaqMIsFy1quHWy7rN3y1mzNF5Xjt/3ajJS0Whp6zrGIOycV57rnjrNuyjT3ZheTLRr7QlUhI3h5eWriBfaGNaJ8YhpLItSQLMaHRUeR9v44XV3xHfqOONAvwyWVfJoV5OWzY9C2b95+jIC+P3GI9PpQmASuHbw9epFG7tkTIGDRMJ4b8z+mwOHRQaEtz2Crz28ZjFyk9c5YcdxghBUd4dcEa9uRG0blNpPjXIiDQQ4iMHacziAE9W1N18hgZ4sMAl4u4usm0atmQNvWDOHnsHBezD7BG+jAjr5gz2bkoOXS79fwaIvEi822jxjGY5ZWUFR7k0MUAiZ8GOCXuTJf4VTlkDi3g+2MXKbmYxhpZM47ImM/PPm8f5JXojqRzR/dyztmADs3ChFZJv7jR/aJEJ5f0lz6QgJI2h7SJXDTPHEpz0lktPA9fLCVPeBYjyQJDXpUXznD8ZA7nju9h/eavOZNTxkU56Fk4Mav7QNCueIS/y02g+EUDDbEhMDBM4siNKzyFJvWiiAgJxOl007xRMiGy5ba0fjIPhQS4URJ39Tp2IlVdYMepEpwyLtwuU7QWfRxOiTsnhgJlmhgCdUjsGMpFVHw0Bdu3smHzcYorvGicUwcOUhSRSjcZkxrPYRg43Ypi2ayeKAykfaM40cNB01YtcZ07xtFzVYAh8SHGS8nygccTQKDDQVhyKklyKCsqAhVQl7jKA6ze8i1ldgN1AAAQAElEQVRHzxfLntwLstm2lImW4xQb2nbrTnTRCQ7lVmG6g4ivV5fmdRvSp0s9nKaDAI8Lezp1RZIQUM7ODdtZLZcWKB+iPmKafajTBYfMfyHhUUTKqTO8SUM5bIcRGhSKy1WHxg3q4CvTs5IitV4QB7/ZxsbNB8mVSxpLxpie0zzBIUSEyZrniqF1g0gqSkrRcIesBcoZgGmc4/ujFyiROLBjS+a9PJl3BEn8HWDTOp0eGqTWxS0H6nJRTq9t2gY3kLFrP6WJTWkd5xD7DfT0aYiNVZn7yKiKp32LQNvPrbumQuZB9heCNt5nKihL59vDZTTv0pJg8bMzqiU3tE8i9+A+iiMa0izKIbSRdGobw5l9R8hXAYTImAkPD8bp8tAqNU7GTSkowTPkJXIdpoERkEiSSmP15m/Zm5UPTun3CyfYedqgU/vG6LEVKR/SerZLJbCiUggVjgATQ8bpgZwQ2reNtXEadG5BZN5RdmYBysJriM6+HHbuOUdC29bEO0VuUAP6d21GcOFe9lwIlDUxHqfA67drTVDhCYkRJToHEhwSTLDTSXyjesS4vRSL2vxIUkjgSRw2lvVj3zbpz68Ok1fhwKrS8YnEhT8+D+48iK9uM7rInkrvT8REiTUnvuNpchAq4uQ3X7Nm81EKi3M4db4SU/owNCwUj+gQlpRKUqhP5nNwuly4q8eYMhy4Za5wGEj3OwgIDCEszCM40TStF46px3z2UfbmeujWPhntx7DmnejbJFIU8+EJCMBlaP0M6kYFk633VOsPUCA2yRYVQ8ZwoByWw/UeICyZVFlTyoslohwORCSmjDfTuMixtPPkXUyX/c129p4u4EJODiXafJkrNHdlmujwMYWfKX0SFlEHz8VvWbvlG07L3rCqskr0N3G5Q4iNDsHliqBRahLRIR5ChKZew4YkBSqKKgDpUtNw4hDbXWEN6d0xhpP70vEpB8FBbtwuJ1DCgcNn5PL8FOs2fc2+7FKKz2XL7OUmWGyOEL4uVx1S68XgKykT/CsepXA6A4iKCrf3MPVlDNcN8xAqPgiQOaFpXIDsrauw0o5wIq+ctF3bWKP/+qqyktyss+w9eJK45h1IcjtwOEzbbnQS/TyBHtyGjhcPKRGw7+utrP7qBCXSB3Jet/0dEBxKaKgDl8Rp08RgKkU/ZTi02ZgBThk/GRzMKiDv9D6Jl21k5hdzNjsHry1D6d9LWdeqpBYWEwUFx21/p+npRy56BWzHpnQHFRlHJEYC6dbBHyPhLTrRo0EMHlN84XLYspX0g9vlRKpkHzjGmbJC9m3YxqrdZ6koPi/7diXzj5Og8FB73EQlJ1EvGPLFvaZTeBgGToeJUsE0iHRy9LutrN0k87/4Q9uOJK/scyUw2b37CM4GLWkb6cDhkKwMXG4fBemZnJV91bHt21n31SGKcgs5nacNQoa7AZSyfccp6rRqS4LINNzhXHNDZwKzjrMvP5BOzeJwCrxBmxaE5mayq9SHy+kiqm49WjauR/sOzajjhaD4+rSQ/UDHdk2pSDvMuaICdsicv2b/Garyc9mfdogD2YrWTevZ/Oo2byv7+5MczNQ3CFoPS4880UcebbczkKiIUMIiPdRr0ICYYA9BEk/xiY2oH+akqLgCd2QwYe4ytq7YLvNXMb5KL+XKkKlQoUzxgWmI70zcHg8u6RevTxGbHMPZnV+zescBcmWtK/WZhEaGcmbXct79cg806kCrcCjXX1YsPRLBUCaBobHUl/m0Y/u2NI6TdmmqzM0l7Uy27CkOsHr9Nk4Iw9zz5/E0bUH7CC9frVpBacMBDOkSjnH+DOlZFynIPcFKwT0qF9P5+Rc5uXsn+WGp4ksXXq8hfebGWa23y+VGz1OgcDhd4ncDnXTZ43LhdsralJ5BfnE+h77ezsYDmZTLXvBssQ9L/KDVFzLOn0njfEUITRsFcGb/do5XNmJA+zByz1wguG59IsXfPlkjLNnH+C6c54TsS/NOfc8K2SOeKsgn50IOuVUm/YaNoWHeGt7ZVEKf/q0J9CmUM4S6kRVsXvstO9MvUlFloF2n50F3aCSRsrdOTKhPg9goPEFBRMRFktwwHqO0giqJXZfLI3sFNxq/XouepDrPcSSzECX2WV4L04TTcumdXV7ECVkrVu4/TXnpeU5dlDZZr0QF3JUlHD6QTmBKG1JkPS1z1qFDhzbE5u7hWEmMxFyInB8dcg5vijP7EIfOGwQHugmUPUe46JeakkJ0aAiBIU5ikuuTFBNEeXGV+FB7W2cLS3R1mOWcknnDFVSHxk3jseSLYVjLm3n0ofsZ0TmYzBNncYUk0rJFPZR8WDEjWtClZQw5F89RkF+AIf1Zeu4ISxd+xmtvf8iGoyXE14mT87oHHZtl54/yud32ARuOFEtbLOEyj3rljFWWtZsXf/8zbn3gXu557Hm+zfZy8cIFDE8dWrZqgCHylHwg6dIylrwL58jPr8AwDURxbUB1tuQi0oHK28dzj/2cOx+6l3mPPM022Yc6nAZVsv7qv3i1LEvIJPtAzy+uiousevd3zLn3fm575FHe3HoBh8RcVomXqAZtaBJukVsRwXUT7+XhmSNJCUL2qOCTDzVe/cFO7iaENT7ZcFpK4b14iI/fXcqC1z7kW1kHg+s2Jswq5Oy5i9Rp2ZHm4YoCsad5y2bES/9cOJVJnjKwvD7iGzYgb8+XLFr5NUU+sUV4YoFP7jM07wrxc3ZBEZ66bWkdK3qVuOly82wevXc6rR0XSb+YT3DDdnRMMikotEho1YamoQY5Mj4vyLzrkHVFzJcRJzzlN6heB3rVL2eHjK1Kq1LwTNq1jZS5U5wDiIfJPZJGRkkhGRKfa2R/XlSSw8lsUUra0crZ78s/Sjlwy12X2+2QudBNx94dcMjHkrQs2aNnFZF/ej9rN27lZGEFuWcvyhhzEBoWKzHXiHbt2tMoxpBxpjBlnnc5TAzDkHlDxpHb5EeT9pEhLbL3+/6ch449GuAUWqfeUMka5ay6yBEZu3kX0mQ/so0DZwo5fzGH0iqhUZLlUUrhDImmfv1k6rXuQLuQ8xw5mU/WkW9Yu2E352XzUpKXxff7T+CRdah1hAP/OlTNQHjoR3vFHRmDK3sPL76xgRNlcbRrHsq5b/dxIageneu6cMqk1a1dA4pOpXE08zgHL7ro3N6vc0jTDvRpEoVD+WQv5sGB7gcPsdGBnJS1dd03WTKveCmvEGlatBYoxf/xz/8RBXWY/dtN8U9QYElge30+WTglS0erqlzOlAYQEx2Mjt7g8DrEy8IT06o39987nFSgwieIMigtPcqlXvNYwssnbZbALWnXZT15aexK2Vh4ZQAIGEvkaRyfjeelssxCuRzIDITMT5geE6uynCpdsaqo0AurxpW6T2i9MiEijMrlU7utuwW+igoJWksmE4siuYByh0SREBdFXHInbrt7Et1jRUvBQ+tQepZ3X32P7/KDiEmKJlBEa30Kyrw4ZLNsyoDQE3mVTGQ+byUVsqlwuGQiF9k+WUhcbks+Glei9feJLpV6kPs0nhddR3SrkAu2KllcLS1WdPfpiVzT+8DlMPCqSvavXcw72/OJi4km2O2UCdFC6+ETP3qFr+ZTWSEEMsGoslzeX7CYHYURxCWE4zJM4VyK/j/qcIvOIlh85xPNwRD6kooqlCitpKx9ZDjdcp72InOM0F1+bHm2XkIrC7TbaVBaXkZRlZLNkYntb9HfJbwqyssRdliiW0W5xreQfeuVzLD5ST85xfB9ny/ijS0XiIuOJEQOEQixgEFRnSx8wlvbacsRPbSuSB/lZufilK+h4cKostKH5mujEkP/Holk7NnFwZ2HcTZpSB2ncBDeEnriQ9GRMEbMmcTN9cpZ+sqz/Pyd7RRjyQVZFT5B0rxKKhUumex9tkxwiX8qq+0TJBkPoql8lKgUW5T0Y7XC1S/L1kdXNC/BFBKf0HiR/Rb6EO5Fib+rwIxlQOf6ZH61iV0nzuEISCbRDbkFlbiDQ0mUsRWb2Iw5t06kp2xCbpk5geuic3j/mSf53af7ECwchk/4eyVbIleXLVsWRNAsMYj0E+lUShAeko1AbHIKIbIRWPrq26w+5SFWDmEBToeER6Uds6Zb4kB84ZW4VqYSfpqXT3j7kAol5VUoOdyb4hftG5fbgaqqoNRC2sU+8YcUKNe7EUPJZFBJmeUkNiFW+jmKa0ZMYfaNjYhp3IMHZvbFu2stv/3FMyw/VgookYMkk4g6kVReyJO5RGFJn+h+1/IsiUFBkBiswOt04tT9KrogNjhlzJSJ7kiU6/GhfV0hB2BN69UV6a8qLxL/wsEfLAhzdLvuJ59P+88SHSzbf9qkComtKhnTAbIx2fjmW3x+3CQ2OoogmY+8wkMeLmWJ9mKJmyjZcNaRmG7ddxT3TuxMdJ3G3HHHUBLO7uAvv/oz73x7XhRQYq2IFyFRHQfx0JSuFG5bzqO/eoVt54olFr24wqJIjIsgtnkX7r9jLC0iDaGz0G6FIspEVkCgU/rOS5VPGGmO8iqTOSE4NI54md+im/XkbpHdMbkFs24bS5PyvTz3xyd5cc1J4SW0eh6SuccnPjRlU+BSVVRU+7BK5sxK4WuVlFIhPoiNj0Pb1XnwBO4f3gx8ZbZcX3WfV1SKcBmbvqIifHKh7XZZ4kef+FNECRyrnHJLERETS52YSFr2Hy52X0OgNAtYfqGqrBzDEyAXFD6h9UrfWLZ/7Ub7xxK4Dz0viDT8PCFceMZLLDfvO4yfTe1NsOBaWqa8q6rK8akgEvRYio6jz8TJzLu+IVZFCT5xpu57QbvisdD+0Fm3WeIbn/ioSmLIkhiqEHu9Utf9Xi4B5RM5uqxxdSxVaXxl4HFCpYwXy1LCW3gKnU/707LEJtFeHjHAlmWVp/PWS4vYVRZNbHyojGmZW6UfSkvLcIo/kDj3yrzqE05KyuXSN17lwCFzj5aJ6cQhbSV63Mm75tE6+bQ+AtDzjl6b3YEWmevfZ/660zImIwmTGNI8LYkfcQc+sVHztDBwmoasb1UCs6iScV4uuvt01jwlm26LkvRNPP/2RqqkT+tGekRVC3EVVyatg9bfHjPykaJSbPHaPrQolzFqyeWWVZrFq/M/4mBlpMwXkXhMny3XEnt9Gl/8JxEiengRVcU7lryVyNN4OrYMIiUOdGy10LE1tRvSyaKLZceMVGQe9EksCF2NDZqLVIuKvDL3ujFEJ6/uUw2XXFVaiSVrm4EP7ROfy41DPtaJ+7mUZCdcJeuhx4M9FmWqxSddXlUuesocpcRPXul35XGgJBb1muETm7QctIyqKiwdQ1jCUqHhGj9t0ye8uOwYMeLXqGA3OiLKK0vx4ZYLRdFIfCKU6L8uNDSpUFsiyyc6e5XEg3yM8krdEh1cRqXM74KgH9ENmc8qZJ5wBxriPy9eGbtKYonKCqp0XAlDTauUB4fhE1rL1s7uQ+Fhybzh1Z1gQwVQ/VjiV1sHU5xRksHLAjH6XgAAEABJREFUr3zEUV+UxHQEHof4Xtr9qFoJKJJLCnegC6X9Lv3rs9uVjJsKLFcIiUkxEqMNGDN3Frc0CqJcfK1jSdyJJfFYZdstutlvHQt6HfSh9dC8LPGzT8ez8EZ01eNVyWHOEh9Vynwn5qNtqsLAZWo+PulnC8twCPZFFi14j2/zw4iLj5RLUsT30iY6ah1q1hTdnwpJWgcp6Daf+LdclIuMiyFe1oJm1wzhoWl9kbtDQcT2nChpF7yim8bf/N47vP99ObESw0FOE0QDS8sS/aukr0Uyet+o+0WPL1+llypLBAoeEj9KtPPqfhHnKIfT71NNLzHgE5hFuazLDqKj6hAnc2XDvkN5aEZ3nKWlso8APcYQXpXCw1Jamqhw6bHwCR/tK21mRYWXKrsuFBLwFT6lVaBSz98BwSTGyjwWU59xt07nhkZy+JeLnMDgACyxwyv2arbaNkv6x65LX1j5J3jltc855YgmIT4MpxLeor/G0bp5tdHaryIXaZNWED19wsMncVEplxV6n+FfoyZy36i2MmaQZCMLuiUulywQpxix4+NFLP4mjzhZz0NdMg60LGnTj6aoKKvkyhiplL2HiBORQixI2h8+7VebzqJY5npHcASJdaKIS2rHnLun0iMcSmRsiXOx1Zex7hVaUUZ0QXhJrMr4svKPsuDVTzgmY6VOQhgyIqQn/HIEC2RvUSL6XN7LS5wLVIntZbIvdbgiSKoTLbGTytg7pjGseZS0Cr3eMki/l1YoAgPcaD2qZG8ngvEJvwrTgZhu961lmrhNL6Vy6SQmyZxQSXm1fVUC8FVJvUL8J+Ui6f+gkBjqiq2xKZ247a5RdJKPm8Vyiel0KHSM6jHkMgxK5HxGddLri4Qq2njdbzqedL9WypyraWQakPm9Ah1PTpfJhb1reOuLHThFTkyQS7iITfKgmQiyprVEH58uW04CKs/w6cLF7C0JJzEmkkDp6NJyi6RuQ5g5qCWnt3zK48++wddnfQRIgDmdDhn5Wh2fuMYr05+PMrHRa5i4nVAll16lPpPI2DjCQyJp3XssU2/qhNxncerAOr7crxh4XU/CRKdKGQeVsg5ERMUSHRRFo67DmDO8Ox65WPW5XDiUT2wTOaKvoCNSJevH8vtfw6uzT2JcHmm05HxbhStAxoTEaWBCa8bOmETXWMOGg7L1L8jLweuog3ynIuvYCdz1UgnxQdb5QiKjonC44WL6SYE3IFiCuMwr80BsrG1Th2snMeWGFpglFmZoPA0TPJw5fYZC8Zshe7iiIxt47ZOtqMhIYkIDMcUO7XPL58Oyx7P4SWKjUvrAJ2Nb94meQ7wKiWHJYpO2R89NPp+B22GIj6skopEwtATHknolZkAEcXHRhEW3YPSUGfRKVFTI/OZ2KZTwrpTzt1vWUGSeq6zwYch6Ykkse2WtM2QM2TJlrXMYFVSK7hrPH1/IvFmJV/T1ySCskjGo5y6f+IErkwLLkh/DwCdzrojE5VaYRiU58qHuolwWmjIvW1RJXGC3OWSPUlnhlTXYQCmFkrjxFp1l33fb+OyzJewqj+XmQYNpFO6jxDKxirPZb7ctlbYYBg+6haaRUCz7PHdcCybd/hC/e+TnPCrzZrtYqLTnVa+sUX55Il5i1IuSfZlShigMKK5ICiVzZFVwY6bd9gC/euhRfv/AdNrHB1Al6wWCbMjaoH3qdjkRd8lZo4oyM4xrht7BHx59hN88cC+jukZQLvh6vPrkctlnKjxu8JXmcvZ8DjJ1oNssi79KSvSySs+xd/smPlu+ku8viJ1jbyTVY0nPGfj0fs8Et/jW0uNemCjTgUMiQdvbrNcohrUL5qvPl7HzbCEObbQtRUn/gKgCCiy5U/Epv15UFJAtl9vFMv5Mw5DtTQW6K7UMJfrrKdgQuCkuExJ01j+WxANGMF07NeDM9/tISztCrnxUSHUiMaAQdHQqlfXF5wolITGa2OgUxs6bzbAmSnxUpZuxxAa7cOnHsmE+CSId+yg3TomVquISqmSvHiNjT6/B3YZO4u5bUvHJxzY9H1ZIXFuWz6a1hKclMav3zAJA8/FVz8d2m26XrEXqbhCXIJdbVIgsj8cnMSNZ8HW7T8ZJeaWcverEyn4kihb9hvGgnJPCTd1qYNMKL5+MZ/2fDtX8i2Vf4HMGEm+v5bHcMnUmY+TDds7FUgKCA8VBXrzaPmGh8W0dqoMiIKE9d90+goTc73j8t0+xaEcuZdLmk9hziH0+scvhdMg6V0VufiWVLgeBphKdvTK6lMAttO0+0V/HBsUHeeXlJWS6I6kTFyz9IrhoHMn6LbqLGrXPT+AB4yeQgVIBBHqceAKD5QBiYDoc9sA/tHoD5yNT6NQ0gQiPRQmBtGgqX21aNqZZajwBopxbZiq3202AQ0nt8qPcHgLcTjzSrnDKBKTLLpSguATfI4u0rDUoXZYcGKCkzUVEnQBKLuRhGYYsBooLZ4twR0fbl7IOh5vAAA9KCa7DJZOkW/i7hKOyv7p6hI/Mcxgej7R5cImAeLk8L69w0ET0btO6Cc1TEghzosPYttHKOc3ujBK69W9J8yaNiAxw4HS5aZAUysWMTM5L+DvcLtwi0wiKICqokvPnq0Q3A0MVkpVrUCchUGQJjtga4BJ1DJfY7r6km/aBW3g6pEmPHYeUXWKf4cvndL5JUrzJwW1HiWo3gFZyiVovKlAWC7fY6cTtcoktTqFUaL8FBgVQWXSS3Wk+ut/QXPqjPuEeA4c7hMRIJxdkM4FhYhqBuB0mhnzR1gu9VZRLsTIEblAsX1SrgiKJcyNJTyXyksfWTfrNMAyMylzOlhgk1KlHffkieuFiERpuqgqyc4qIjItFORUO8XVAgCG+kD4RHpce8VeAxyU6B4qfvXz/7THCWvalVdOGNJAv0i6nG3UJWRcUQYEeXOKbkDDT1tPpMKD0BEu+zqVD57bECE/lCCBA/OzRxKJwcuf+NCzdwpNLL9K5VaLIAiWTcoDLiScoGEUp+Z663DhpGs/eP5D877dwvEARHRYg/Sz+VSZ1ox2y6Sjw22dUcfZCAeHaPofCdLtwSxyhBNftFv1cXJ0UAYEeXE6nxKZD5IFL8DxuFzaZ7j+xKdBtgrQ269WNuDNbWbzpBPHtmstCDAGxYahKi+QmqbRu3ZyWjZIIK88jPzSVYdNn8fytPTn5zVZO5/kQl+OWcWoYqnrsOCXWnICTll3aYR7/mveXriIv9XruG98KszyDbYeLaH5tW5o3aUZciBPTE0pMQjSFp4+TIztH03ThMAyUUuhY9Yj+Likn1QmjSr7KF0ub7vtz8iHADI0hShkYTo/EpfaF0EjMOBwOVHgwwSjMuFRaNGtMmxaNqB8VQEVuDjGd+nHvow8zsVERqzYfQicli5OOvgYd+9CweB8rdl1EGQ4cpvA3RJ/yk3y59ihh9eoQXJjLBa8fXin8iowwEkWWIXOC1tdU4Ba9PeJ3j0sqymX7xS3zGoJXJf41A4IwDQOnEp3lgKaUwu1yis0uwRB6lwO3J5BglcP2PRdI6d1JfNaShDC3xIoHJXHlkTlNj0mlPETJBrkyMI7mTRvTtnVjGsaHYeRewGzShdn33stDA8LZuGGnvfkWYv1QlldEUvfrePBXjzK8zllW7zhDYmyQfAxy00b3f4tmMkfFEiixB7IJQ5KKJNpTRtbZXAzTxB3swukwcbkVdSKcFHudtJD5ra09L8dhlRThiG/L9Dvu5g+j6rF7/bfk4sGtLFyeAAzxQd75bMrckcRhYjrcEsOBOA2FCpUPRM5KeafQUvqwtfBMiQkE04nH9q8TRC+PR3RwOgiIisVZco7sPIVpGgSLT5wO8aMRQZTbS3lAos2nrcRCw4RQDC6n4KhQKs+doqDKEFoXgdJvMqSFO3irZLPh9AjcwCN8XWKvW4UTJYekCk+88GyE5pmaKD6/zJKA4HDcZgWexFRaNGtCu5YpJEQEYMj4dIv+AR51BbYuKgL0fCJtHokHJeNV43ncCqVjyOMWu11SRt5SlnZTGWg9nS4PHsPAKM0nu8xJUqz4yScHIjnwhYvOhnw8NUwXgR4F8vHUi4Nwl4HKO853GQbdr2ss8VWfyEAHDrE9JiqEovOnKRZa0zQJcJqYjiAio+MI9BWSW64wDQNv4QUKVSDxIR6uTG4d+5JdArTtcAYQHOjj8I7DBDS+htYy96bKhs7ldKPkwlLOA7gDQv08886So4KpFxqIR+ZdtycAj/jDkOwRH7jE1vAgRe7hA5xyNKC/9Gfb1Cg8TpdkrkputwsdK26hVaKPLnvkrZSS8eYiMCQYdVHWsNMWffs2plmrFCIkbjwyRyrxr0f3hVxIIP7yl12IB6mUmAgIdGGoGKIltso8CRIHMvZaNKZpQggoU+Q6JbtAKNzCxy38XKq6j0VX2WYQG+uh4PQFKgwT0xGC2+HELfp55FLHUXqRvEoD0zCoyj5PgSsSCVvpPwU+ICSaqKBS0o7IeiF95BRfaR+FxQdTmZsrPDWtIu9MHkREEyZx63C4ZY5xCrESOVKWOUe4iT1enB7tf8XhXfuxkrrQVvqoaaKGuYiOrEM4ORw/XYxhmrgcDpQSSh1LhoOgIAMjMZYgXx7nZU0zDQOVe5FzvlASY7CT8snLDCU2Ek4dvoAyTEynA62zMyqGEK/EVYmy7bVKzlBYEUBUpMJhaF+6pQdAiQ89orPbpbgyKbFL+zdIb6jOpbMn22TANY1o1qYBYW6H2OzCnyz7FRsVRO6Zc5QIb1PGhdvpxOVw4I4NB7nAj2qeSqvmjWnTvAExsqcxRaZH+lCLVRJ/bpl7PboiM2plFQSJbw2nS3zjkLneLb5x4na5pf8dgBL5HrHTQMnlVLD06wG9bxM/OkwDgaJkftF2BYYEokoz2H2iim43tKRF0/qEiyxtrxI73G4Xbi1XCX89FkQnmaHkYkIRFGxiqHDCAix8rjpC24jWLZvQKCEch+KKZKHHW1CIxi9gx65T1O3Sm1Yy5ydGeHA6tf5ajlv0don2St5SFllu4WPI2+N2iW0KSw6OPsNFaKAh/ebjfFYBwQlxOJXCJXw8Hk0fQqSM/QpPNC2aNaaDrE+N5LLTkHi17fE4RTeFW/hqnympXX6cIsdty1cC9IjNHsETthILbvG1C7fTiUv6zZSOiG2aSutWLWjXOImIoAhCAuBc1lmU+No0A8Q2Bx6PB6XcuCWWQmTvVXrhMIcvhjKge2NatI4nWPi5xVAlNvp1Esnib79cF3rwVfkMAsVmIyyWEEc5KjTVHv+tW6Si95RCIXjVj3IQIAq7A4Okfyx2f3+CuPZ9aCnjKyncg0vkaHyd9RAJiokguOQC+6tjxCmdp3SjjLVKr0GojC0jwIHhcBHgVsRFB1NRbtJQ1t02rZrQqnE9wkxw2PHnvmLv58KjY0cO717lIkb8r3KOsv9sAH17NxHbEwlxOcQvWhgotDYO6oS7uSh7eZ8eK0YQbol10xUsYypRZxUAABAASURBVDMUr1yqJbRoSMsWTWjdrB7RgU4QSuwUSr1og4zjWcjAQPvSEEM8UeFEVhZxrlRhGAaqKI+LlQEkR7hwuRzSPwEEGAqllE2j+ytQ9FZKUUfis0Que5rrfpbYbtawDjGyJoUbxVws8kkMGjJ+csiRc1Z8RDA6WfIjXQ0KDNOJ2+m2+TpNhcstZfGTywlOlwe3lIPcBmcO7aEgrBkDO6XSPC4Mp9OFDD+qZOA4PcGEBSncboHpWAkOwLiYwd5TFbTr3YxWrRsRHiBt0m9lhVU0vHYwv//lQ3QJSGPL3mw8lKL/IrNUFHO6BE9kemSOcRpQWiBnnZxyAoKCCTENfCF16dapMX26NaZ5UgjBvmLWr91FQv+xDGrroirvDMfzg4kMtqhyxdOlcyP69GhCy+RQIuRsU37+HMWyhoRHuZEtACISpQy73wyHh8BgA4f0gW2fxJJb+0AMdYju0aEB6I9ZyS1T6Ss8u7ZNIlIWLZfo7BKdA5xQVV6J1xVgx9jp8wUEhQZRkpPB4SyT+tI3lvhl68lAurZOIjzE7e/XyAZ069yY3mJTY9kDRIQoju/dB01uZHDyGT5YuR93hOLCkb1ku1O4sWtDWtSLkT2IA49bST+5sftN5DudbnSfuaUvHHZ/uux5x2lg74scYk+o9q0vl3MlJjFR4QQ6TZxyTnA7FbFRIVRVKJJapdK3VxO6tqlLbCCUy7713LkiKpwBREW7OZ92hkrpj5hoF0HC3BMThasyl/wyg5hIAyvnIgVGBHFRCmW4RU+36IFfN9tfyh9DoqfL6ZJ5gOok+DLOqrxuGibHUFWYxY7tuzldkM/xze/z5xfns+agj2bNk/EWZLJ103Yyc/PtjxCrdl0gPrEe0dFh0g9lBNXrxG2/+j1/ntoLT0UJefl5KPGPIZf0gXU7cqtum9aLAGnLlTaZIGw9HAEhxNdNpmGDhjRIjMSotKifUh+jJIuvNn5NhsjLOrieVTvPEZdQj5gohz0OQFGTdFwZhsKUeSG+bj1SGqTQsF4MpnxI8clc6ZI9VLmc+8+ey+XMuXPkFFZiyXxiONyEx9alochLrV+PcLOSqsgEGoa5OHvoazYfzKNI4umjN5/lhQ9XcVrWWgeSlInDYYr+Sir6MUQbH2ZcVx780595ZOK1xHrPc/JCOaGyt0hKiid75zo2HDpPoZzZ1m7exslCi+TGKYTJfKgvjY3QFG4eNYZWASVcLPMJf1sShunAlD4yIyJJjo6k9Pg21u69QFH+eVZ+9CpPv/oBx11xsu+LoejYV3z53VkKZU77bv1G9hVYJCQ3JMYJhowppVWVbChLOCoatulAbP5BPlh2kLoS53a7kl95BI2omDCUfNSIaZZKa9lbtm6WQmwA/viXuSvAU42okauzhYE7IBjDMKjMO0s5ocQkJRLgK4fIVFo2b0L7lo3EFg+m2KbXtYAA8Z/IVUrJWuXG4wkgQMpIDvDouhtTykpk2vjVcpXItCQTGUFgVS6nZd9lmoacM1w4HQ4C3GGEB/gu7UdaiQ2N5GxqakKqk0P4u10EBnhEnCIwPBKXXPKHiN9atWhO+xbJxAQHExvqJOfsaXTcmEYQbuHv8XiwWUkHKqUoKiwipEFH5txzH78cFMVX63fhSEwgMO88ucrAEJ/kXcih1AyjSXIEQSW5HMguxzRNHA4ThIcSnm4Zs8EyN3L6EPvywhkgc1XzlvGyN3DgljlIaZ+43OIrRW36aTxg/LvFKCxyMw+w+9hZju7exrrN37BuwxaWLf2IxXtcjJ86gsayaDS7phcRmat47J01fLHmK7YfOktVZTGHDp8gPS2d/ScLq1VVaJ45J45wMD2LI4fTKCg6z4mM0xw9lkW+t5ystAyOS87KK+fc8eMck/LuXcfIKbNo1KMPTaqO8NGa79i0cQVfnYtmUJ9Uys8c5FD6KQ4cTqfUC5U5WRxOyxSepygpzeXICSmnpZFRUEZ2egbHMjIFv4C4Ln1oVvodv311GUtXf8XXuzMpr9bUZ4EKiyMlqIBFr33JRpkgM8+d57v9aYR26k+PwDSefXEpS1duYu3W/VyoiKX/wLYUfrue9Vt3sPq9DRTW68qARganxI408cPhk8WUnjspsjM5dvQ0xeX5nDiWKT7KkEsKLw7Z1OVkpfHtrt18/slavI260yM1nsSEAHYte4ulW3awTxbek/J17uTZc6RnZHH0xFlKvMVknsjgyNET5BNNvaDzLHxlFes37eW0yPt+zxkadO9DwrktPPbuWlZ99RUH0k+z7/t9lDdpz/VJ5Sz57Gs2CfyDHQX0uK4HkTq69E0K/uRw+jiXfoxvd+7iw8+2EtK6N+2TAuksl6YBx7ewbMt3bPjiU44FtWFgp0guHMogPVNsPXIeWTtQwkZ/HZOXLOrn2X80U2w/Snq+j4ZNotj35dss2bSL70+cJuPEUeRcrVFBJiBveS67dx0mIzODTau2sWHzdlat28hr85fi6ziIKQMbCc8L7BMfnDiWxtGLJXIMVZhRKXRKDMQKDiUhIki+CHspuCDxeDyTg0fTyc/OYMUHH/DB5u/YvOc0SY2bEW3ms3NfBhkZx9l3toSuA/oSfvJrPhP7Ni7/iAOO5gzuGkeu9Fta5mnST+ZTWpJNmsTYifSTFPvwJ1vvAvbsPkz6qVN8vy9TPkqWczIjnSNpJzklX9FzT53kSHomx07n+2kimtO9cSA5JTG0iQcdg7HtxM+e4zz57Cd8JrG2Yc8pyvNP8um7H7D0qx1sOpBDAzmohgYEECMLy7crv5BNwhnOSCwckzGx/0gGZVYZR3ftkY1LMefOnOH4gV18snIf+a4kWiRUsuzVpaz5bhtHss7w/a6jBDXvRf86F3jlhU9YsmIDq7Ye4IJs6A4fzSBN9N+XVUhMl350DDzJ4hXfsvmrdSw9ZjJoUDvZgGdwWPr9uMRXSVkZmRkZHD98lLNWIjf3SWTL66+w+Mt1rFi/k1P5lRQd3chTb25ky/YdZJSH0755ou0LJZsnHX6exPbcJpu0zHXLWLLxW7Z+9z3bvv6GDz7bxLHcKlyJbbiupYvVHwuPrd/wxpoM2g7sTUJVHsekn9PT0jgtvj6Vni5zyUmOnCyi7GIWBzNOcvjwSbxGCA2jqvhy0SKxczd7Tp7mhMCzc89z4lQWR+Wwll9exsnMUxw/ni7xGkCjZAfr3/yIVd9ukbktm7TDhzlzLl3mnFMcOXiAM6UB9L+uNVnL32XBZ2v5ct12DmeX4ju3kxdeXc6mbTvZe86UC9D66HFhGyxz7YXvV/Hku1v4avtOsomkSd0kGna8hnq5G/nNGyv4XOaorQfPUe7zUwiJFELp178t2Rs/5I0vN7J+4x5OnMxg94FzpPTsR9L5Dfz6jZVCu4Xtx85LzB9j8YL3ZI7aybcnvTRr2ZhgqqiUy9ETe3fzzbZtvL35Al1uuoao8gscPnGKY8eOk1VQCa76XNenMd8vfpm3lq1lxYZvOZpbSUXOGY7KPH0kLVsO3nmcSMvi6JF0CowmDOoUwZo3F/CZ6L5+13HS0o5y4JyPvgM7cXblfOZ/tl74fMPerFKxRT+WbVZUI5n7Ei/y1gsf8+WG7exIy0PJhlGbnlIvhO+Xfcjn63aw7WgWJ9OOsz9H0XdQR7JXv8YrS9azfL3wPFWiGdo+1mMJmQ8Gdolk6auv8OGX61n51R7O5Jdw/kSajMlMmZPOUFIFSlmAwleax/4jJzgm88ox2aifzzpDWno6x9JzyM8/LX7JELqT5BaVkiFxdkx8kFVYjMtlcC79IN/t3MknH28nsF0POslhPqpOAt7jG3h+6Vds++YEp04e5/vjOVihdfDkHWLRkm9J98WRHHSORfNXs/GrvaSfyWLPziwiWvWgg+sET7/wJV9u+oadYvehfd9zNjSFmztE89UKwZf4X7TmIKm9+pAaAZYl3pJ5SL46iN+zxI5TZJVUkJd1khPpYnNGKYmpcRxbu5BPN+xmx9Ezso4c4nSeg6gQgxO7v2Hjlq95Y9URWvbuT4q7gP2Hs0g7foLjeRXCv4Kjh44Kr3R2HcohsH4DjIwtzP/yK5Z/l8mpjBMcPlUM4ku0Hr4ywT0pemSQfr4Qe/7T67TMU4VyGDohfj4qe4ZCdzwNnWdZ8MZavpI+Pnk2m++PZpN/Umhlvjxy8gJVxefE95kclXqRL4CE0Aq++uwTvj5WQe/rOnFulcSWHQfb+f5MGd7Ci4KrfXBGPsqUkCGy0mS+PpWXZ+8bjqcdY8epIuJlnWxSspNn3ljNqs1fS/9kcXjfUQqiOnBTa5M1733Fpq3f8t66s7QdcA2pLsTRJg55YdRh4KD2nF+3iNeWrJMPZFv59nguCR360CnkNJ8s+5ZNW9ax6oiLATI/OHKPceDESY5mnKW8rFDGTabEVgbnyhVJ0Q72rfmE9ftOE5tSl7PbPuKjdTv5+uBpMo/u40RVAsOvbcy3n7zJIplPl6/7RsZQIVZIHULLTvLJO1s5VNyAm3vHs2vJKjZu/Y7Fn+4jqXtf2keKsnJAcMhIswimx6Br8BxYxrPvrWPZ6i1s3ncGb2hzbuoVx85ly4X2Wz78fC91e/anZUgRe/TaLb47U1zG+VOnROcs0k8VClN5hK+SuMuRvj+akcn+vRmUBNejvpnFK2+tZatc1pyS/twt+x89ygwdF0LWrMc1NCjazZ/fXM3Kjd+IX7I5sH8fBQltuTG1hPmPLWLJ8g2s2XKY/IoSzoj8ozLmThX4yDuVQZqsvwdPFsqBtwERRXuZ/+Y6Nm87JOt4Jgdlz3dRZKZnpnMs4zxlJQVkSNycOHKE3KCmjLimDhvffMueE1bIXH0gu4yCU8c5JOv0wX3HKFSxJIdckD3VStkDf8/Js6c5KvNiVvo50iV208T2ksKztg7Hj2dQ5Y4g1jzHF++tZHeGRc9BXbiw5h1e+HSNrAXb2HuqwF7bJXDEcnnMUKKdeaz5aDk7M6toVD+QrR+9wxcyng9lniVT9nun5MIxQ/Y/R4+dIr8snxNHxZa0dNLzKzifkcYxmZeOpBfiCgrFXXqGr6S/1638nM358dwke+Syc8fZcyLLHq/ZpabsVdtzet27MleuZdn6bew7XUTp+fMcFXtOZJyjTPZc6TJGTsiclidTv2iJ0j9lFzgmY/a4yD9fVkq62Htc9DiaVUZRVobQZ3Dg6Ems+t3pnZDDS39+jyUrNrFh+0FyKz2yn+qJeeALnv1kHetkHB3Ue/9DR7hwLp0j6Sc5sPcIxe5EoisP8fLiDWyUj8xnzmTInjmHC+lnSBNbT5wqoiw/mzTR41jaKcqJIt6Tz8rFa9iZEcSAAc3ZvfB59Bq1cuO3HD1frjW3s7ahquAM+45mcOzIYbn08NE4OYJvl77Nsk07ZL46z+GjaXLhAco0sGThUHVaMKxXHTa9cTlGjpyvIKJeCs6srbz04Sa2fn2MkyfT2Hkwm5AO19DR2Mfv9flg1Wb0sfz5AAAQAElEQVQ2fZcua3ZF9d4vk5PSZwUyBx+RuDycUUiAXKRYp3fx7sodZBmJJBonePXtdWxec4gzZ09y8MQF2c8ih3JLRiy07NmLpJztPPbWGlZu+Yr96Vns372fkobt6Vc3h7/8+X3x+QbWfXNEYsWHP9mjjWsG9SMkbSVPLV4tY30rm3afpCymCTd1i+PbL1ezWfpk4ed7qNunNy2NEo6Kn47LfvzYxQoJ13IyZd04ejydQ1nFti6NuvWicflOHnnlSz4XWzfuzaIyNImbe6ZwYN1yNgi/D778hqgOvWmdJDOOzA2gZI0EtwuK886h/4/W0mWuyM4r4VR6utTTOXmugvOn/eVjpy4SkZRM6eG1sl/YxgY5I2RnHSctu5JYuYw8uvVzVu2QuvBIE78eP3qEi846pATnsfTd1SzfsIO0C+c4LfPFsZ3LeO29bWzcvotCM5bmjePI2fsFv37sOb7Ptcg/f1L29Okckb2S8lSy9f3H+fXrX4tNdejbvRHHlr/O84vX8uGy7zh8roQjG9/h0+/ziA4o5evV2/hsyRfss2Lp2bM95za/xTNyJv7oi21sO14sa3gfWrvSWPzaFyyVeWb/2RIcyofPCKBBHZOdK97mw1W72Z92msxTp8jIyBVdTkg5U87SucS160kL5yGefXkp7y3ZxIrtGeQVFJB2Ip2Tp05w+HQpplzWOMqL0f9CKdAsZfPni1i6fisHTp5kj+xXV32dSXLHrjSJsqgMiqdvl7rs/vRV5n+wjo+X7eK4XFof3bGZz7ceJ6F1J65tn8jh5QtYsC6L8MSGOE5t4oWPvuar/Sc4k53FoYwCLpzJJCMznTSx54KcqU6ckrqM64t5eZyUMX3qZDrnC704pL9zTx/l229388mKjThSe9O1fhVH9h+WOfUUB4+cI7hpT9oFHOPlZ5bw3tLNLN94jBIPnPn2fX75pzc4VO6kfe9+1MnZzOtvrOLTVZtZ+02mxHAHBrTwsG3ZJtZt+Zalm9LlvqAvKUY+xzNOkil74tPnS8nKShefnuSknLkunBc/y77+1KmTFFWBgT8ppaiqgIbdBnFjp7rsXfIEU2bfzs/e2k5il+vo1yqJRm36M/Ka5pxY9Tyz5t3GHX98mwtx3Rg9sCd1g8rIzcmVfWEZVZZBYsf+tHWe4tOlX7D7TDmW3ElcLChBfzRL6tCPtq4sliz9nF1ZpSjZh2ftW8/v7pvNuFtnM2H6bB5b+C2xfW9iTI9mZKx7kdki77bH3uBMdGdGD+pNvRCLMq/CUNjJkl9DZozSklLyMjbz6/vnMH7OLCbNnMyD735LWUUVZWVn+PL1P9i8Js+ZwW/f282FCpPyc0dY/Ny9jJgxm6mzJzPvsQXs8tVn+C0DaGAc5YVHb2fSnY+w+mwU1143iOYxUC6+81UUcv5iHqWVXmR4o+TjXElBLhcLiijzGXLOaE9qTAHL33qHnd4obrhpBN2izzL/sXuZOu8unlubTtOew7i5axhlRUUUFOSQm19KWGpHJtxyDSGy3zuXV4pl+CgWvnm5ORQ4Quhz/SA6xuez6I93M2HuPSz6vpJuA26kdXIUnQfcQp+GlSx5+n4mzruT332yhwZdBzKkd3OcsiR4ggxqklIK+69sY1PpUq+CfWeCaRflb5cmlDLwCXJAk070r5fPy398D302Xrf1ELnlZRw/cow0mc/3Hcux8aq7QigMHFYpJw5+z3fbt/P+2tO0HNCT5JB4evdowM7Fz/LuFxtkz/Mdx3PLZSydkvU1Q8b7OSQEka9H7JMz/LHDB9l9Kp/i3PMckDuOY0fT/R8+juu1P5P9R89SJv2gDNFZH5yjmss9TCjrXlvAJyu3yvn+BCfl3m3naSc95eyVvfJtXlqyhi/XbUOf52WZA00HlMkYPppxyt4XF5T7CIhty3Wy9/3wuTf5VM6Ya7bu5Yxs61v16kmdc9v4wztr5U5pq9y/ZHHkyFHyRA9hJpyg4NR+3l7wERu+3snu806aNYwnsUkHetUrY8lH29ny1dcs+q6AngPaExnekJv61GX7u2/zvpzPVohuB7ILOXP0OHovuWd/FmWR9YkpO8Sri9ezadNhMk5ncezwGbljy5Bxns5+Kes7QGWvTrYKtT//Jg9IpP2bOGu2MupMGUUWQfQcMZWZEhiWBKhPshmWyrQ7ZjK4VaTdzQENe/GzO0bRNkppShx6ZyEtkS17M3dkdwKqNVVKYRddsQyZMJy+yQGAk05DRjC2Wz0pQ2Tjnswcfw0xUrNCUpg0aSjt4iScZFYNSmjNzMk3kOyxZCKKYdiUcfSrHyCSwhk6cTS96rukLISuCK4fPZobmoWiAcldbmD60HZoTBXemMmTh9AyXPAiWnLvvRPpVdeF1txwOe23UlKTh+BEJswczzVJAg9Ilgl5HD3rO/B5Urj1nin0bxRkT1qmw2GP3eQuNzNzcHMpWzjqdeC2WTeSIHxcSe2ZM+5aEkRviGLguBEMbCq6iQopXQYyc2hLDNmYeTFBZOt/5hYoG/hbpw2w/dBl+CSmiv+Rb/X9xkxluhwMLcIYOHEUAxuHI64hpfsgpg5qSkBQAuNnTaBbggEhTZg+ZxLd64AvsTP33z6atuE+LF8o100Yz7CWbiyzDiOnDad7gim2mHS5eTSTeiSik+XvLSmKBMPEkMlNx0Bksz7cOrEn4QIOajGAOaM7EyhxYckF6vSZo+SQKv0T2ZoZM2+ikVOQhIO4QX4R83RfWiR0vZHZt7S0de86bCLTr20AOOkltk7vV9+Gg0IpA508ye25/dYRNA4CHYOWZdKk/0gentqPOg6RJ/ITO1/PrCFtcUpZ3AiWg/ZDZ/HL8R3Ec8JFSbbcEs9jGdnGgxVYjz6dmhEt+FVhjRg1/DoSXBDYpA9zx3YjVOABTa+RcnfCpOwLaczkmWNoGyGs5YA+beYttAjUPF1cM3w0wzpESgMokaNEb22529Z7JM2DDdum5O6DmHlTa0xptKJbMHXKjTSRNnQSfXuMncp9E7oRIO0aZAYnM+fumQxqGqarODVzOTD065BKiNapTkvGDbmGCI+i24iJjOwch09orcAYho8fQ896DqHzUK9hPVLqp5Bcvz5NG4Sx5/MP+OBbHyOnT+DGJmK0UYfRs6YwuKHEgacOE2+bxdCWEej+tqTvRRShqT2YPeEaYiwxMyyVKTNuoWWw+N4XzPVjJ3JLy2DBD+OmSWMlvgNte+t3v4m5g5sJgUm7IRO5Z3Q77GFgil5iS1izbvRr6MaSr96pvW9mdJdY0RcMaTOUkEktWcbIg1OvIdIUWVq2KBMjX1WHXdtQWoMZOHkMNzQReQJvPmA4c25IETi0vm44UwRH2OBM7sDcCQOJDxCeVoTE/wgGNpRgIpCbJk+1/0mRZSnaDh7F1AGNhd6izcCRTLymvl0ObNid2ZN7E2YFMmjyJIa3DQEVyZBpUxjZOliYuugxbCTDO8VJGZJ7DufhmX2JETNRDrQOZkonbmgpMSK2RrcdwMRrm+CPbiQpYlp2p0+yA5+vihai+9CWgag6bbn37ol0reO0eTidYChBl0dcJL+Q3GsUD0zsRaQl/glOZaKMwQ4R4I3vwIN3jadLHZlXBFMpg+jEVLp2ShEdfQQ37sb4YS1xygcKr+FEyeZOWNDuxjHM6p0g/afoNHgkY3om4hepaH3jWO6f0J0wm58Dt25wxzB0/Ej6NggQKDTrN5jJ1zWWsqL31BlM75+KjqPwZn2ZO6kvEVgkdh/Fz6b1J85WzcDlEHR5lFLyK09gPJNum8mNEveW5UU5HTJmfJRJU7vB45k3qJnNM142lHPH9ZBVSvMcycPTB1BHeCmlEBLBlkfKfj8H0HP0dO6+uQWCAoYDcYlME/GMmjaSbnUkuFB+/yolPoKwFr2ZN7YHodJkRbVg2ozBNLXNdNljfnjHGLEGIloJ3sjO9sbbUk5MWTx9PoG37Msd43siw4SQRn25a+71JBjSTzHNmCkyW4SLmMgOzJs1mEZ6YAQ2ZubcsXSNQ4QLzq3j6RFt4ZPxNueOqVyb4sTyKdrfOIopfaSP5PK11+hRshZHCNwipectzLmlVfX8YaDNQDo1uv21zB7RWeZpkRdUn3FTh9NKjGp2/Vjm3NhEhBl0kfE559r6WEY4N0p/XpsahE8mk9Q+Q5g5uKkQQt1rbmb6oJa4hUIb7khoyeyJNxHntAhp3Jc7p/YnWtqi214v5b4kGVLBQK8duhTWsp+sF90IlYrlrMvIaSPoGm+idWx9/Qgm96yDFV6P6bPH0jFKkCKaMnvOKNrrPYCjDqMnjaSLXlSFZ6sbhjGpT7Ko4eaGyRO5pW2YsPHR6JrhPDRtgFwCCr3gBTiU4ERy7bhR3NRUBob0Zf3ONzBjaBuJfQhrdA2zxvdCizOjWnP3vePoGCsq+Vz0GDmaMZ3qgC+IgZPHM6CR0/ZJqxtGMPM6GUciwjKU2IfIgPo9hnKvxGCMrEniOokHC4dsrKdMvYXmYdKPVojMlWMZ2iICn6xF148dJ3NlsE2cKAfeOUNb23w6Dx/PpL5JYo9FiwEjmDesNaBodd0Ybr+lBTq2mg2cwH2jOhAognyWgYQVSuJnyqzhdJSYsXxuegwfy/COEfgsGR9dB3HbqLZ4kCSBYYjeUiJaxuV9tw+jUajPxjN1nxkeug0fxfD20bYOdbsOZs7Idrglpuv2uJnZN7USzwp1dAumTbuJxn6mKKX8cCOOIVNG0auOODuiAbNmj6a9uJ6oZsyZM4K2cU50UoYWBiquLffeOZYuup9lbb9m5HjGdonC8kUxct5MxveQPhDeptuBjEyS2l7HjJEdCEGS8J88eTjNNf/g1tx6x0jaRFpYLomXqRPoL2uaRQKjZV7skWggrkDH2rT+9UD6uPvoKdx6S3Mc0mBhIuECRhjXjxnF9anSN2Yik+ZMpGeSiQpuxDTZU/VOQuK0NbNkj9PYLToQyIAx4xjS0gWORMbNGMs1dT0iy0dyh8H8bO4gZJsJwt8pDhZThEhJlseMZui0CVyr12HZpV43YTJjusaKnaEMnDiNCZ3DRVgUQyaPpG/9IClD416DmTaoOdqLVlxzZkweRF2phKR0x79/FvvdiYydPoZrkrSCJp0GD2dc97pCL97oNpJHZ/THP1c6ZK4UXQKSBX8EXaMddgx2GDSCcdeIoaIiysAOF8tFuxuHMq5XfQ0lNKUzMyb0I1HiHUcEN48dRb+GAdIWxeQ7ZzOqQ6yUwXS6UApiWg3gvrm3UN/pw1IJjJg6jv71nVjyIeS6saMZJHtqp8yzc2YPp6l0riOxI7dJWY8d3M2YPGsILQUODq4ZOpaRnfRKFM3YGeO4tqHwkT5sP3CsrFE97DkG5fDbZmuB3wYUTfsOZvK1jUQu9Bw9Ufa8SSjlYsDYCUy4JlH6TXAlNg2lCQPpPnoy8y7FiD+GQup1545bbybFI74OS2HqtLH4p4rG3HnPFAakBmpiHB7thHSVxQAAEABJREFUf0jocJ3sedv7x19YYybpOSEA3PW6MG/a9dQxhU9kC2bOHkWbCCGNbcscKbeLlTGEwhB9kOSq15m7bx/jH+NWkOg8nhFtA7AcsYyfO4txnWIFSxSXBVBV0ygldYEGpfbkwduGylyEPdadJuKDALqMGMWIjtHodTqp8yD0PKEpkjvdyDRZT4K0CkIf17Yfs4d2lDNdNSC2FXfdOZl+9d3SCg6HpvLQ9pbhjOom65P0Rx0Zq7eO60aEbsJAvwxxbGQolEm/dxo4nOuax8pHBYuYpn0ZM6groT4oD0xi8NBhNI00CW05kOk3dyOwQvwo56wZQ7rgzoMW145lePd6VFUhOZSeN49jQIpJvjuZ4ePG0S1WURHUgFETR9IuLpyY1C6yliAX8wat+42kTwPpy4SuzJw6liZu8Ea3YszwQdQPhKJSB637j2PGwBbygRAa9xrD7OHdCVPIGukA8akZ14UpE4fTOLjSvsQMT2lPs3BIbH+z7BuuR09zPhnzpuwtHSENGDN9Cl3qBVBe4cNwulFYVIrunQZPZli3FMpLoUG3IYy/rh2OYnAld2PC0H5EeS0qwhsxYfJ0+jQItmlM2TR5yyEypSdjBne3zwPhMYmY5ZlknFX0uGks17WtT8N2NzB7+kiahQXRuGMvOqZIcFVCaZWHNgMmMuOmDnjE35bpQEwCdxitO3cnUVn4knowc9JwksXmoOb9mT68H2Gib1SL65g+og9BUg6Q+B0/uA9RMgfkm3H0HzqCHvWCKMyHlI43MapfY9xVlXjFD0j/V5RbhKT0YsLw3sSKDG9gI4aOGkZTiYfiwFQmTJtGn1SxUXQ03S585RDTrB8zJw4k2guepI4y1sbRPNqgstKSvZboqULpffNYeqcGkF9s0bD7UMbLB1qv6NCo5zBGdW9Ild48xrdn7Ijr7LmyqCKM3oNH00/2OTp+bNuRpAs+8AU3YMzUO3jw7ju5Y+Yc7rnrAe6bPooWUVBgJnDLpNt4+O67uWvmbG6/7V5+fucs+reIpLzYzTXjbueOUddRx4AyZ0PG3X6/zB/dCBP7u4yWttHXE29KH0jb2Nvu59Yh3e22zkNmc/9ttzJv6jRmTZrB7XNmc0sXORtbkQyaeJvIu8eWd8et94i82VzbKkbsUhjKQFwJEmemJXxl9tN9+/CdtzN3ykzmTJ7OvJm3MqZ7KpF1uzDrrnu4d87t3CX5wbvvZ2yPFFLaDpAzttgzYwazBX/ujFtlj9SfeAPiOg4V++/nvnnzuG3WnfzsnruYeG1TAqSPKkReZPObeOTuqXSqF273V7krip63zOFnk/oTLjje8FTZYz/IPaO7oOM6tvk1zL3zQR68dS5zZ87jAdHhLjkLJylw1O3ItNn3Mbh5IHJ/TaPuQ7jn/geYcXMnPGUh9B09m9unDiS6BEKb9Wbe7Q/wgPjstlm38eA99zF3mMwfEjMBKZ2Yfev9PHTHrdw6bTb33Xk/d08ZRpNQcAWCx8XlJH3usHz4rGCaN2nBDQNbEmS3WohKIO0GkhxxjJs3g7Fd/XOs4XJJk4U7qTWzJgwkoXqitGkEHeEJhpxdEN4WTa8dzozr/fvGtsOmcN+oTjIDCqLpxKVlxLdi5uSBpARYAlQgr4Qug5h9cyvZnwhIAPV73MiMG1sirRCYzJhpw/37VYEYwkMeQQyg34QpzByYip7Tg5r14rapff4fe38BZ8d15f+i36o60MwgaDEztaQWM9qW2TEktpM4OIHJZJKZSYYzM2FwHMdJzCjLMsmSLLLFzMzU3ZJazQynD9T77Trdsuwk7828+2b+7zP3lmufqtp78V5rbWrbZAi/x4QlfO9LCzRmg2U5fDQfIX7ZaSy+/37mDUyMf2ujf7EOQh6bJT8UcVuLUGOLYM8i/kp7SmMyY+KRzNwHH+BOzcENkm0A9NKl9yDGj+4BWvNma430kPYCbDeTux/7FNO6K/u5NhNvf5BHp+YL2k/RPQ/z9btGENCc1nUdL2OQ2pUHP/0pxhiTZ43i61+9k8HJ4CuY6L2PMDZXDnn4sTu0pkEagsTk/7n+ey1g//eSN9QtsnoOYuaMScyfNZk50ycyd9Y0Fs4YTa8MG7M4tQyYJhnpPYewePEcbp07hXF9svD5Uxgydjzzpo1hUPdUA9VRRLPXEGbNKGLSiF6kpWQzZtJEZhX2Jd0J0nPYGObOHCv6QfL7DWXOzMlMGdOP7ESpKz6J+X2YNnU8M6YVMrwgRY6vGOzanxnTJzNVJ5NJDvgzC5hmvkf2ICkpk2HjxjNvyih6pCWS13ewaE5iVE9lISWHYN4A5i2cx23zpjBxaFcCnpQWNvErrWAwt942j+mFAxk+cixzRvXQ1NfFTenGzHlzuGPRDGZOGERuUI4fsykYPILZUwqZNXkE3ZIEJzJdB41i3szxDO+eQmJeb6ZLtikjC0jWQD9Ess2dMozcRL94OnSVfJPGjmHOtBF0TRBN4dtp3Zg+bx5Lpg5j6IgRTCkcSs8uXZg4fRJThnUj2Ulm0Dj1jejkJEJaz2HccdtcZo3pw/BxE5g6vCt+YqT3HsriW+exQH0yZXIRM8YNIs2Ea3I+4ydPYOa0IooGd1HQK+uJrwlir3/13t4UIr2gLxPGyQaTh5KtDQe8RpecfsOYNW08s6aMoV+mI2iLXOkxZ9YExvTNI6HDmJblIeBPy2PUhAnMnzaKPul+SMpl6tx53DF9OEOHD2faxBF0TxaZjtsJZjJ8gvpw9hT5RhGzpxexYM5U6dVdfRETlCWauYwsnOjR7JuTLFt61WTkd6Vn14z4tzRLy+3FlOlTmD1uABmpSfQYOkr0xkv20fTN9oE/nRHjxjF3eiFDuiaLiEtmnyGyjWCmjmVgjl91KC4GMGf2BEb0SCcxOY9C0Zw2qpf6wmv2fnzBNIYWFjJv9iSKhhWQqoXIwNETWTh5GF3TAmQpDubLRiML0jx4Y8/EnJ4MNh1vgW2ZahdfRndmzZ/D7QumM2VEd4K+AP1GjWXW1EJmTx5JzwyfASQxvy/zF81hhvy4W78BzJgxhamSKdh4nPV7W7n9K7dy961zuPPOO5g3KJXa6hoCuf1ZfPtC5o4dzKixo5g2foAGRPV/UjZFs2dx56JZLCgaRG52NgNHjGHejPEMK0iVqC6BzF5MnjqB6dMmMLZfJsZbA4q9yTOmMnloN5ITEuSXRcyTvl0S1axF+uBxU7jzltksnD6SgjQfjgaXyfK7aZPkp4qtgGLcQHYWSy+udlsScnszXbymjB/FpMkTmT1pCAXpAdOKG8hitPxj2pSJ0renNpUkvy+DEUWTmDVxCF1TA3RTDC6YWchQ5Qxj4+kzJjN1RA95BCTk9GLeLfNYoBwxesRIZk8dQn5mHqPGFzF3Qj/Sg4n0GDRSOXAsvWXrYFYv5t+6gPmKw1GjFY+K/675BRRNncTMQvFLkiV0mFAwdDy33zKHxbPGMTBfBvBnMk65bvpk5a/x/UnThJ2brmB2D6Z02GLy8G6KWQwhjb/9vX69VTlqfP88/Jbqb7pdDeK95Fe33zKT2UXqw2mTKRqUp0lPjJTugzSZmqv8NpUJ/bJxAikMHz9B/l7I9PFDyQvKVpaPhGASwwrHMnFSEdNGdsWJuVjBbMZMmsTsiYPplubHXDFtQPQfPYE7pNeCGWPke34C2T0xvlY0OJ9AMMOjP2fSIDJ9EHPSGTd9JnfMn8rUcaOZPWs8g/MSUeql54hCbr91FgvVL4OMfQwDFUsF5SUntRuzFs3lFo09U/tn43Mcr79IzmeKcu+SOYWMHjqM2ZJjQFbQo9lj+LgOmuLTJcmj5P3EieLayQwvmsqdi2exYNJQuqQmkddrMLPEY9KQbiRJZuTd6LKTMhgyZgILZoymX34meT0HKOaLGNkzi/T0LkyYPpWZo3uTlZJInxETWDxzlOyUQHNjI2kFQ5gge86aNITsgItH0gnSf+w07r11KpNGDWDC5ClMGpCFrX96j53EnQsn0Ds9oLwwjCW3z2PG8P6MmTiOSSMK8MsewdzezFk0j1s0lkyZNJ7Zk0bRNQgKIoaPL2Tm1IlMkzzJVpyfSbeWmpHO/UePZf704RQkB8jsPoBZc6YwpmcqVmKW4nwud84cybBhQ5km/+me5pDapTcTJxYyS2PVrDG9tC2mPOukMWz8OOZOGUnvjACWHZDeY5g/cyJj+mfjswIMLJzKXYunMH5gX4qmT2B4t2Qjgac+dgJ9h4/FzB9656aS1a0/s2dPZvyArqSmZjF60mTF2wDS5OAZvYdzx5I5GvsHMGrMOKYPzSOjax9maj5g/NiXnMNYxffcwv6k2pDYdRC3yCcnD8gVP5eC4YXccdssFsnfBmpw9qXmM1nzGJM/k31JDNS4OEfjZbfMDPXxOOZNG+fNJ4zfJXcxtOap38dTNHE8cyYNIM2RTf2ZjCuawEzloUnDu2uuoDpxs1TMHX9aXq5Zojx769zJFPbPUpNyuPLiFOHN1Dg5pm+8LpDVg6nTpzBtWFeCiakMHjuBBZOHka8+tTN6Mlfzktkm36ttzLQ53DN7DCOGDWLypEL6Z4isoqH3qCLuEK9bFAvDClJNJd2l++3qgyH5SfpO0Bg0XvE+nunj+pFiqR9V23kbmV3FekaPQSxaPJfb5k9T7u4qf5MlrGQPd+aUCUwb25dUg+ukMFRyztccID85gdzeQ5g7u5Ah3eO8LUuULVuL3IHMnDmFqSN6k+SgMXQkpj8njx7AaNOfw/LjPiFw71beT+k+iEWK94Uzx1FUNJE54weRrhwZS8hj0ozZ3LFwBrPG9yMtkCS/K9RYP1pzOpuMHgOZNWsSo+XPChO6DB7P3fKd6WP7M3biZKYN7UFOl55MnTmNCfLTxOQ0RkycwmyNdZl+iLkJDB43CTMuLZIuA/MSSJNvTtc8w9gs1W+R2mMot3tzqr4MHye/GFxAQZ/+8t8JDOuaSlJqFyZo3JuqeZ1PQuT0H8mSxTMY0zvdy0vdB49myS1zWTx7vMb3lA7djbHwroxew7lN/ju2Z5qmIN2YvXABt0wezNCRo5g6djAF3Qvkv5OZNLSA9MR0hk8sYu6kYd4YmNdvuOaXRfLfVPAlK8ZGMFVjzJyZkk05UKYlIa83RZOLmFU0hHx1iMm/3YcVcrt8Z7HJv3nJJOb2ZtbsIsb2zSExmMmoyZOZqfjK8PPRlZjNGI0RszTm5SYk0nvYaM3JxjOkSyIpXfpo3j3F8x8tLXGT8pg6x/TbdKaN7YeGTlzXosuAUSyRLedMGss0zR0mjepDruSbqvnsDPVZRqJD3qBxmqvMZMqIPporFlHYJ5ucHv1l7ymM6pFCQnouhVOnMmNUT5T5Se8znFtvmcG43pni4dB/zET15xwWTB9D78zAR/LrzZfWhcKiIuZMHkXPVAdLfTdjwXwWa849cMgI5k0cjJlHCxTLssyDGDf7yHgG5QXUyza9Rk7mnttmMGV0fwonTQAcktQAABAASURBVGHq4HwcGdfJ7suc+XNZYuJpeFeCypX9Ro1n4dQRdFeOT1O8zZtVxJgeqWAlMGj8VO6cW0hBqo+sfqO567bZGod7M3L8RIoG5uJgLgvLPFyX9J6DWag5y8JpY5msfp1ZOBh5Gm5yPpNnz8LEyuwxfRUrBuGjYjYjUjU2LVw8T7E+RWNLDxLVHLNTFNeFTFesz1Cfa2Wl2iTFxQTmTh1N3xzZ0ArSd+R4b/0wsGvKDVkS8/oyd8Ec0ZvGlCHd4rlDVAcqzmeJ3vTCAWQof5ps2WFOzOWTUvm9ujBdeXKm8ktWYrLGx0LmzxhDv+wAJq7naFwe3y8blySGTZvBvYuLGNl/INNnjKNflh9SCpirnDFnTD96FPRkimJwlvJcumOR3GMIt981l5ljBjN27BgmaT2YmddLuEXMlN9NHJJPTJtYTmYPiuTPmYrzrN5DmT97ImNkc6vdpsvA4UwckosVgdZokN4ji7hLueXW2aPplZ5Igdar87UOmDRulNakRdwyZxw9kqCh3aHH0DHcsWQut84Zz2DFYbhd40BWH+Ypzu9eNIkxGhttx4ctw1gpPZmjufsdc0YzQjE/X2NT726Z9NF8ZtHscQzrlYXVGsPJEtzC2dy1aDrTh3UnQWuMIWMLFf+FDM5JIrXbYIblx9i4/hBNXfrz0D2TGd0jk1FFY7lj9hgS2+upbnTBdERUm6OxRAaMm8zdS2azeMYIumek0XPQSGYW9tL4ZpFgxq3Z05g7tju2+r9f4VTuXzKJkUMHMrVoDCN7pJE/cBQmbw7Reiklvy+zZk7CzF9TEjKUp+Q/U4ZqnhXEJkCXPsOYMXUM8yYNJ8fv0hgO0ksbfnOUd8yc1R/VmjnN6DhXOk5j+pieBNpR/urDJPNHNza0tLoYn1t4y1zukB1maI2ToDlPq52hOcF45mjeMVV1AW3yR4PpyvWTmK/5aH5GIl0HSFaNlUO6p5OqddVkzT8mjyhQdENEZlFoaTwwYwJEtbkdCWQxsnA8C+dMZqZ8pEuiRUsbanRps1IYPGYs87QWnTd1FH2yE3SA4BLWwfKAiZOYN2EA2iYhFLXpPnwid88bTZeUFPqOK2J+0cCP2obF27qmpCqfFLJ43lQWah9loeZotyyawsTBXXCkc6tidPCoMXF+00bTNzsRswdzYdtSfvCDv+fvf/jv/PMP/5Xv/eDfeHJjKQM057tFa4b5kn2hYmzxwunMGJJHkvx9pngs0LrU5KBFC6cxcUC2l1/nzp0u3pM9fRdpvblA40WOH5pbXI3pA5imOdQCjWGj++dhy74ykQ5+1D9dB3PbwkL6ZCUSVX+FrCT6jyni1in9SbHUZ+0Beo8Yy+IFE+iXqvgQvSStdSdNmyReRUwd1Yc004eyrS+rgOkzpzOqS8Cj3RJLY/S0ycwq7IkdCjB4wkStKQfqwAVaZZckxfQkrZsWzJnIhKHdCcqHFGpETLykd1NeLmKR9J9Z2J/cBBd/IqQm8/FLOTvmONhWK5dCmYwfkO+1uxqvvJcbP/LPpHwmzTTj2gxmFvYlI5CINw5qbTyqbzb2DVi9aLPWCaYwWHOGCRpzZo7uQdCVo6nJlZ8MLJzEHbfOZIH8p4fm1Fm9hiiWJzJK/AOCITmLUdJ3vsbo4T3SSc7M89al86cOp0dmOgV9B2PWLkWDupDoMwhg6R90ub4Mxk6Zyd0LpzBp/FjFRSHDuiXixiwKlJuW3DqXRZofD+qSHMewLGFBQm4vpsre0xVDaUGjjXT2pzNu+gzuVLzNHj+Y/GSBSo9MybvI7ClNHcsUjUMzxg0kw6e2OEXsxExGT5zIzCnjmTZugNaE0l39bMuGhVrzT9OapUhjpiVaBismmwyQ39x562wWKW8Oyk+la7/BzNa8f5Lm3AmaW3YbUsidS2YxeXA/CicXKld301xMMLMmM2FgV5IcQ8kyP/9P+W+0gP3fSPsGadeNEY1GO4p5jxfzFzVK06rX6Ycc142ZegMXI96mVp16RKMffXcSvUFTCx9Xjhcz9IUv11RwGDrC0XdEx5Lx/ymFqwmIsA0fI4/aoiom+aoKIUkO8RY9QenbvenbJSY5IpGokllU9TGVKJ6McmZXdOL6xTD0PPybfm60Cy4mOlGPh5zbyGHkNkVtRnZLM4mY3qOmeHpbXhi63rfhKcLS1+Pn0XHFM0pYsG57LZdKr3Hh9CmK69qI6ljcyGgJ5SP9jIwxDH1jt4/ooAMm6SY+Ro5wOKZBOkprW0zFPF1Cmky16bul1XyrvuMZarcI6UQ6Dqv6NgNrYf7nSqaENJi0azKV2r0fBSl+0RNMc5S2UCeMhaHbid8mWh5eKBaXQd9xGtxE0xVOVO0x2kTf4y95WiRfJ62P4xj4DnqC8XhpcDHPkPTy+LUbmA6aoZt4SY5OmTw4ydMqXnFcgxOL6yS6cbh4XYvot3l0LMn6SRhks5jkN3ZwZVtXA2FUdMw7n9AzDtfq2RXRigovJhw+RsPIZkpI8rZKRvMeL5YH1yqZ430X59Fpp9Ybcn9Er9XwMnRaI55MreEkMrJzaa+Ny9J47SLlKQOYPrK/R7up2cDFBBsvoU6fuMFT8komw7PFs4srHTvkEv9Wrxi5TN3HbdHW9pG+7eqjVu876vEKdfS90TmeJ2KKc8/jjdffKJZt45p4k3+b+DIlDm9ALJ38u4qjmBcXXmzEk0I8JkxsKSg+ikHz4Xo5IOrFIHHaJo5VwuEw7coVruLUy0sGX2zi+DHB3gTfIU9UTwMfj0fBCN7SIBsz+UI0P5LVlUyxj+TsmNy4RjfB3dBL9DyaooN0cfXtyRWOCFfym/qbysd4CTYuh+A+md+kk6Qnqrza3h4mrGdUMjRXl1NSUsrR45epV59HjM7ia2Bv2EDkDEv7E3rFSbpEJX/Us6crHaP6jqkv0eLK1Xv8O9ohmwGTaPE+M3jiF9c9imkzfMCSrWNezo601XL8dDGXSyuokQ8Ri3j1nl09mjEPz9IGWUT9Z/4nQF5bh8x0XnqaiU5cJ8kkXCN/p/2jHzEXpG41RiNhxWpE9F1PHqOnycuuaTOyd+BE5TPNLTHBWmR0H0C3FNvz8VbZs+1GrnRvxL8XM2rrjPXOOGlTzonHWfQGfmub69E1cWLotZp4E26LYtHEUDyuYnF4tYUUv/HcgeI0XuI0Yx35VrDCN7xDiutWvbcIz8C0dtAMmfyhOo+XnvE8696Q38hpeBicFuGb3BlS37R68S36wjF0O+EMrClxeMkhvh4P4XpyGFy9twgvpLwQh+uwgWi2GhsIx9D8U/CGlpEj3mZ1yBnHNzJ4som+R8ejH9WiKoq6jabiYzz926VsudRKJCxcwXm0JIvhF5fJUq5UbjN1HSX0J+xs+uKG7LKlsUu74Ix8rR14rUYXr070xCv+7XbIHFNffzKXx+FaDD1DwzylQ5xXVONJVONPjDgvuNmXDEyrweko7WEbub2c+6PbshVrqjS+7cVNh097sWLqO4rr2kJyhR9VTMe8+HY7c5fiQY0dt2CUW0yO6Ywpk8MiipfOOO+s70AA5RsD0ymD9xRfF+umHCK+MdfjG6cjGVxlqZtlsPDkM7yiwo/TEY7k63xHFOI5QPiAl9Mkm9ductFNNKMd/IxsnTQ9XFN/M9+P0Y/b08CbXGHJbHF5Jb9HXwzE9+a7k76BdzVutLe3Y+aFMaODinsTfWmDJ4NoqVrvEflMuGNscKV/TO8qao9JTplWKrvqszh/w/2PZZI0ImZsYHCEILqCN7zV9NHtxutF29Axcns4+rhZRtRvxn9MmymGpkAwssRE09RFbzxd5dYO+VRn4FzZwLOfvg2sZ5dP2NuzgfQzvD6SQ9iCC3vjm+Q3cqqKmy/p6eF2tgne8DDyxDx+MXnIzQjyEdXEceI0DVtLILFPyBn1GmzpE7th75ip64CNdvB0O3gavW7YurNNMkTkjzE9PZ4d+CIRv2VEV22ezIILy1e8OQuSqIOu1ya8T6puWXHf9NrFLyYYQ9Q2NjE0O4praKkhJv2iglOzvpBvfaS/V/Fn6ImL9I9o/Al7fmzw3Q5aHl7Hj09rpgRfFNsX0+G1KiMmj+mQTJsIPo3noVBUG4MuwaBLTO/Nyn0x6diu8QnBBIUXUg4NRSSx5iZhwbTr3RcEHzFatFYJtUeUuyVL1MVnxyRTvISlu85QCGpzPKzNNEc7PgbH5M+I2oIJYMseBs68J2rzypK9W1qitIq/Jf6WbGPgw5K7XTRM7re0AZNsYIVr/keOBtakzoQEi6D4G3kbKis4fbEU85+XkkoE/VHapIeBjRmaoqWJLejd5O+Y/C+YaBN0YtrkjHo5v136BAIurnib8QXZK+pLYM5tixkQ2cvryzezftsJ9hw6x/4DZ1i/8QOOX6kkMc0iwR/D00l2Nf8DOKNTm3hasoUludtFMyAbBvRt/qd/xr5B8TIDZpPmOTHBGJvFZHNjIyOj0dEv/drVB2HJ5hd8LBxVbooRa6/myrUrXDp3kgtVIUKayzp+SJaNDb+Qxnhj54BslNDRp2a8a1dfBgQTEF1jY79kSkq0MHw8e8lmnh2CFoniF9HaNyQ92mW3QIJNguqMDG2q1/6i15+erJLb36GbkdXomiDaQRXz9IqRxe9qLR6lVfKZ/rX1nZiIbBfnF9MEwvR/m3TWyROJks3g0h7F8PSJXpLkt+U3zaJh2VGioTDG1jfaFBfNGp9tn3xYcIbXjSL9jF2Comv0M/mgrS2i/g8TlX/7ddDSZ+xMPvvop/nCp+/lcyrmuWR8F8KSoVVytYmvR0+02uXXfvlQu+pMvSmtqjc8HPmP9622TvhW2dLEhdHLET/Tt6YtqhgMGvtKvwTpZ/rH0DExab4T5VfGVwy+T74WkH8bP2s1zm7iQ/qYPjTxauiZ/vIFZFPZ1vic4aN9ezppRaVHu3wyKF6ufMrYz/iCkcsvudvbwjQ1h2X2GH75gumDBMVLwIkqXtplr4giKEZGhkWq2TxVqonfrveIRctZ/fYWNq3ZyPWEHLrnJSuHq83ymm/6sbCkezx/RhWeij+1uooHUxcziU7f5jZ1LRVXuFhyhWNHT1EfihCRDm5HbrXkg6Y/DV5U+AY1Fo0Q0tgVVT8ZGhip5R8mB3u0BRTr+Hb17nbKcgM+jmV+LbV7tBULJl4798Bcj2aMG22ugb6pdOKJZrzJ6Ox2wBudY6IQh4+EZXPFclTye/T0jOPE22VETD7z2k2b1ptIf1c8jEymRA0fjSPo8uZinn7iY+QWnCu8OO045ZjaI4oTD9d7umIT8+SLiZbI/D/3/4AF7P8BHliWjaPMHS/mPV5sOYxtO2qzsSSIZZt6x/s2baqis73z29SZYlkdsLaFZVnYjvCEbwFxOja2vn0+H34dkzsGjvhlWTaOHS+26r1aU+fRMBRUY1k4N74tbNvBJzqmOI5w1WYLBl1xfuKv+hv0VN9532i3bY+OY1tekyeH6DimqK0esApvAAAQAElEQVSjVjA2jq3i2HSAYnnfDt63ZRHHMRiWB+8XrBVtI3X4JG4ZnU2DBgDb78NGlwIQy+7AsbENLRWDbTsOjkcUbMdRENrUN0BtnU1tvUNNra1inhZV1VCt79o68636jqepr6qxBKc6tdfUxmErBW9KjfcM0HvufTwwvl8crsGhugZMuymGbpyXrXrLq6+qsak1PETb8DBwnaWq2qK61lG7TbXoe/wFWyv+1SqGVpXqO+ErPfgOeh3tNdLxZrgqD8aJ06zBk6FSNIwc1ZLBvJvSySuOa+Sw4zqJbhwuXlcr+tU1eHQ6ZTI41R20qjr0q9a34V0j+Ws+YTtTb3Brb2qr7tC7qkM202ZoGNlMqRLdGtE0753FqxMNA9vJo1ryGnlMqe6ArxLuDRi919T5pJtFbaQ/dz54K11Ctuzj0OAM4I677qBfAAxOfYOBswUbL3HZLIxOcXq24G62i+XZxeAa/vFieT72kX3j39U36VupPqrxvh2PVyefWtm6rsHWwsDGsuTzf+K2LFu+bmPL901xHL13wgrJVr1j2x5MvNrCdhwMnJqxTJu+bUutKo7eHVvvgGUJz3yr+HViHfA5WJaF7XTgA3F8W/V6t2zRdXBsG1vFsW3VW8Rp6h1zWWpz4nWO4DxWqtO7Y9s4ti0cvMuy9C1etuocx8Y2TxUPRRCGtyeXcoLTIbOqb7ot4Tg4ouHYdsczjm1wvXrHxrZMnYXj8xEI+JVbfWj+SqgxkaKFsxiQ2EgoZuMTLN5lYTuGro2HirksbNuJ83DseL0aHQNnx+nbjqN2G/NFZ5tgHdtWvSM58C6749txbNG0P9ZmACwsL287sRDp/cczb1yBJjNqsX341EeO+NkeDTtO07Lxqf/8funldNTxiUvy2I7j8XJsG32q2B3f1seB1ej4/AQDPtG3bsDZqrcs6waO+YuYWuXExiZbMRBg0OxPcc/EXoo1xZPitroGL14qFafV+q6Vv9eY+NV7Z6x3xomBrVZbrWkzcF4ej8dSVY3txWRNR32tniaG4nFl2lSEW1WD4lVFOaZCpVIlTtPuyLeCM/QlT5VKjd5rRcuDqRdMDcrPglE+M3VG1irVVSp+O+Wv9r7jcLXCrxadyirLk69ONGpFr1ayV1W7cd0lQ6WKoWfaqkW7SvoYmFq9V9dY1Aq+tkN+A1dnvg0d0TcwVTWWR79G8FWSpVbtdWqvEt1KQ0twxp5VNUh+h1rzLXrVNcgeFjWCr6mL27Jaeaiu0aGyrJK339tJXe5oeiUmUlnlUiW8WpUa0a7Rs1Y0DI+qGsvLWzVeva2+hsoalWpEv7NY4m3fgKsWTpVgqjz5bMlkimA8HIsa0a+RTNWCM+91sl2cl4FzRMcS7ThcrfhWS5Ya8xR+pWxQLT1qPRo2hkal6uN15hsqBVNjcDpKdQ3iiRZkfOyyFAuOFxc2tm3F2+Tjtm3j2PGiT9VbandwHBsDZVlqE54dbyR+WdgdOcaxDRQY+j4DZ8fhO+u56TIwjmAcpxMmzgPLEj8nXkTPULQ9OrZpUonD24JDl207GF6OHa93bEsw1g18sLAdR9+23tBldXw7Xp3AsaybcAEjWydN2xGcgDphbMsSgCXceD26OuG9Nn3bnbI4Np11qr5xW2o39E2bJfkDgQB+A6t6x7axLOsGfQsL23H0baNqvfuUo/w4tgVY2IJ3bBvHsfVu6gABOp04xC/bg3HicAasA8bupGPgbRvTFMcwvxa243g4pt5Su6FrUCzLUr3azAe6Or8Fb6vOwKuWG3xt+wa8ZVkd77bHz7Idrw87YW21W1Yc3ryjb9txcGwLc1kdtGzVm3d/wK/xTe2ObUANyEdFMLbj4HS2ddB1bBtbxav/CLrjzcJ2DI4pgouzxbadj8np2Bbmsixb9DtgO+pswTqOjdirxNtt84GF7TiCtzGfRn7PFySL7Tg4dpwmN10GxjFtKv5AAG/OonbLitP12oT3x5jwEa6NLRjMZVl6t8UrXjrxbFv8HRs1Yy6747sTzdT9SXpYOI4vPs9wDL4l+g6O3vnYZeH4HFKSbTLTISfbJi/HJicLsrNscnMccrMtsjMt791rE0xujv1HMDlZcZg4fCcth7xcH13yfORlW3GaBt8r+s5EdQbPJsd7t8m7wTNOo5NetmTIyXY8OXKFnyMZc/SMw9uSUyXH9uTK8mDtj8FmG/rZlkc/IylEr/GTWDwhm5QAZOc45Kvkil6cpi06Fje/Zxv8TpvkOOJneXYxMJ49hJud7pLWpSuf/sqXeHhiIo3112gNldHcWk2vwiLumt2f7umQKTpxepboiJZHzyYny8KjZ2hl0mEbRzCW954j/fNy7DiMeXbAe3WyR7a+cz1aVodsjvSySU5up0/RFG4dm45Pa9qcPB+5gs0Sj5xsu8NOlnAQHzv+Lfq52dZHdYLLFryHI/k9Pjd4GTyLXMF0FgOb7fWDo763RRdP7rislr6tOJ9svYtu9p8qWXGYvBxHsDY5+r4Bp/dczx6O16c3t+XkmDrbkz1LdHM64IzM+Xl+wX+yzfZod8IZfp0lN9vy6GRnWdLPEa5P/uxXsclSX3bv15Xx4wczcfxAlSFMmTSIMQMSycx0ZHtH8B8Vj1aH7Trpm6epz1G9ef94seNx4elgywaORy83247LpPpsUzpwc4wP6Dur0+45NjmC/YimvgWTJRhTb+xh2gy9HNXHaVken9xPfouOab/Ztt636vNy/XTN90vfOH1Tb0qO7N4lP0B+ro/0NBstD/hTl+3LoGtiPZfCOcyYMpIMOw5lxR8f/7Vs5TEnXpQIDUxnDrQ7EqWrp23bJCUlUzRrNsPTooRcC5/PvomWhe04HXRshKJv301jObosbNsRjI1HW0Cd35ZlCcdWm9ptiz+61O549G3RsMXb8YotWNu243iOLbp8/OrEE5zV2dJZZ+gJ16tXnU8GDfh9OLaNx0tPr+0mPFs8HFvtKkLxWizL8mSyVefczAdTL30cU2zJZmEZGPMtOHTZtvOxMddWvWXZHn/zLhDthZnf/0RRLuqEim9v/+mvzlrz/Dicqfm/Z7H/j6odrmHjsuf5yavbqIoYSdz/fKcb8D9TOv2h/MQenn95BRuPlP8ZyP9cdZyeS23JEX7/+JMsO1jjIZoTGO/l//iPFZcgsSszFi3moTtmMaJLMqbWc3TLvOkgXCet8RMsnaLFhKJ6S+dQHow+dRClxSo0N6NTOVBMYstD/vPFRXFNJ7zWjwpo0emgEdHJZXO7YDq+O+H+Tz2Vj6RjTJpHO0rMk92RfPE2t6PetMcwf51u9HPseL2r82NZqgPGtMN/tc3QsCz4c/yMLH/aPq76N6reM7LFi+tGMbRsnea6Omm2OvXoeFqq/0jeaFxunaAanWzp9FF7DB3RYvhaVgePaITGhjBhnRxiaOvUtKkpTJt86iOaMQ/H00U8P6InXuJt6HlteseT1cXw7rQZkuXmdw/+j+gIR3WfbDOnmS0tsbj/tvD/8YrpRNQVlCt9IpEIXomaGlX+f3PHkwSNlw7w+yefZ9neax4VV6ep/xeoejT+/I+r/ler207pkU387Gcvs/N6uyq4KY92wLTX8MGyl3j8tZ3UGgj1339drg5aRLh+fCs/++kLbClpM9TI6j2cOz91O3dPH0FesuXVGV/0Xv6bf+Kmj3J5/xZ+8sSr7Loc8jhGTUOnEEldmH6LcuOSQgqCMa+/ve722mUJAyus5tLjvPbSm97/QCeumYta1fJfvEXPw2uvY9+6t/jJU6u5/GfGuIZGbejVxAi1q9/kl5biINQSpTkkCuonz9cdvFxlSRrLxJ9iwMRSZ37Q+hvHh/JIPD7ibR/ha46nCVe8zeQHq5OOhXDA4yGaluFneCk+o4p59J7gh04+Hl3Vebld8B6evk3+kPSYdpOHzF9hxGQDX8AiKLkM3A3akt/Euu3h4fE27aaYv1ZxlFdjistYLILm2yQkWJi8YNqNHIbHzfyQrAbWk9fCg43Lor4TDa9N9CxbbeJpKf+YbnfsGF6b9DSyBANqF4wt+3v4kjvg6R63myW5LVwsPW/IIvgoSYyc9SkeXTgE858ucTVwmv64IWMHHfNt8Dz+HXQs8bL0bnRyJWvUKzF8kiUh2KG35HWRbM6f1sfg2pIpKh2NPp7dxbOTn7GZ4WuKB6tcj2zsqpinVySHabPEy/v+RJuhBVFJGvX+SsT8pVJ1bQzjuxLt/3e3fMYQi4Vq2f7mi/zri9toipga0yPx5/+//po++j8tW9x8Ua4e2sxPfv0iW87HB0PzF0J/LJv8yVTK5y7sXMG/Pf4Ox2rjxv7/n7mtEfB/qsgeXidGKDmyhV/88lk2xpM2Gsb/R4Tw2P+PcFI8xZ2FlvILvP773/HbdRc9zv/n+/6jfrhyageP//z3rL8oy7RW8sHyF/nJK3toMJJKftWat//eEm3m0Opl/PPv11DWJLv993L7T1C3lIcFlt6DubfdykPzx5Cj8SJeqfr/C3e87y3a667y1tO/Y3VNL+6+bR6L5kxn4ezJjCpI+Vgs/H9rfw9P/ReNxjyxY9Eo5t86i3myWzKyq3Em5rW5pk1z9HCgGzNvuYUHl8xgSG6C5riu1qkxD8bDNzDhCJE4EY/S/5afeL9AtKmGre+8wn88u5Uqo5xs6NnSvP8nSiedSO1V1ix9np8vO4gZIVzNO80Y4bphys7t4YmfP8WKE6aFj/X3J1l08jZ0zbzjxvcnAf8vfYe5uGMNP3jidQ5f1fgkJrr/NEU1uErWMsuNdlUhZ/H8pLPSq+v46Hy/dugDfvyrpWwrafZaDB3z0n7tBH/4zfMs3XXVfGLqO3G8Cs0I489EChct4XNLJtItMV5jWfLl+Ot//lfCG/pWuIFDG1fws2XnmXjXEu6eN4q8BE1gRemPyHbgEGnn1JZ3+bdfvcvJJq27Basm/f4n7z8HbAQSibbKc7z50jKWbz9Pc0edqv/Lt/EXg9RedZm3XniOJ949RrupMH1nnv+F4mrNIjRhuN66zlvL35QEXM+31aw7FgfE9fR0vRyDPCMmGDVrKRH1aBjV/sjGBqCjuJprGz4frSHjDaa3Xa0nIvEGVRpKeoiHgTd/vW6+DJx5xksnTPzr/06/Wj7996nryjEMdbf8KD/4h1/wQUmr+cQ4hBlsolYKBbkOF04W0+gNGjE5RLSjxNRlcg05RkSbXFENQhENMJGOASve6BKNhJUgXcKXdvC9v3+SvZVhpQOXSPEunvuwhAEjhpDtb2T723/ge3/Y7fEXVU+GqGiaEhFN02DkinbUmWRs6ryiBW64PUZyXgGBumucudKRoKTfDXjR+NNu5BKTs96A6wwA6fXJOgkl54/r7+kqvWMdcOb7ZpncjnqPRgdNo5f5jhg8BZgrI1mWRbixir2bN/DSq2/y7Ktv8cwLy/jdi++y9uAV2rTCtwQrtamtjxFqiwgrhkdHlWZQiUn+iGz/EX/3JrkPVwAAEABJREFUYzp57EXDFS8poKWwi91ewou/+gnvHW0iLUEDZwct18it4tnVcBJtwyuquj9tvzik+TX+5MF20DJ1f7p0yheTnDHpEvuTYDH1maXVeXKKo5NNh5QkW74jHEEbu+NYJHW0paXY+LWxIDExGwwGJyPN8fDS9UxNtnGkT1Q2+K+0GRqJmjyaiRh/gp+Xx+i8bn5aJEo2g2/4m5KZ4ZDoBydgk5FqE3SQ/nEcI3cgwb4hr4FPT3dIS7Qwew4x18IftEmTLmnCTfRbxNQhwUSHTAOX7iMz00+G3tN1ChzQKWxKmt9rSxeOKcYGttgpLJA74JMcqamOxzNZvE2lkcPIly5Zg9LXg5VdkwWXGrRwjf3Mu+QSKaIidIOO6pO0oeWqzrQZ33T8Nimq9+RSH5mNmPoGaGszEJ8srnxTdbFmTu9ZzXe+/j2+8W/P84cXl/P7Z1/iH378pDaOtT0r+iEzkZV/ROX3ES+ehPdHt+jd8F9XskJS995kNJZworQxDi1a8Rf96r3TfyOiLfOqUvcn6o1eqlJDTOEUw8DFOvl8zPfVdzJg1PLTpW8OTSUXudy5gSBDRwVr/CpqnMifTu+MMCdPltCs+ojRy3SwuLiCjRgd9TQ4ptq9mZ+pEByKDleDa3vUIrNPHqGrF7lYbaYNLhHZK6Qd1JD3r37xictVDEaJiK+h7/GKK+jBGf6m3isdvEydB3eTTB5w54/Ru4NeTDBg0613d5pLz3C55uMTL+MvljbUSg5u44knXuCpl9/htdfe5je/X8aqQ2VEpJfBcFvO8tzrh8gZMpze6TFObF7Od3/8NlUdPN2beJp/ldNVz5zZ8AJ/9Yv3qTCdJB6eDkYuyeQaXfypDC4IcunkBWoicUIeqPfq0qShpLmhnSsHVvAf//o3/Hj5Ri5XVbD7vaf423/8V17bfpTaMNpUhdarR1j68q/49dLNVLSq0gKxIRAMs+mlH/GPj7/BNVdxbPrO0A/Vc3r7a3zv7/6K7/1ujfSwUPjQXnWSpS/9hidfXsUlHSKJjAlNLKuJk9tX8O//9F3+45mXeU2Hw796+hV2XGjE1uansVFCWznv/uHv+dsXdxL2gSIWc0XDVex69xm+9w/f54lXl7Ls7aX8/veP8+QbGyltdQnSSunpD/jpv32ff/n9Wxy5VEFIhrBkUz1kSRe/3c75/et54bVXeWPVSlasfJPnl77DttM12MpHeFCGG8QiNbLRs+L3PR5/ZSlL317O0jf+wA+fXMbZq6dZ+8av+PY//BtPL3uD199Zzisv/Yp/f/lDGtugreIgzz/xz3z333/Fq8J79bXn+OXvX2DLmQZ8QR3AVp1n/Ws/4it//T2e3XoZJ0EWUryUHX6fX/72NyzbdIoWG08apSusxuO8/OQTvLSxmJjJW+p/dQOR2mK2vPtrvvKN7/CbtaeIKb/5Y+1UX9zKE4//iqUf7uOa5hMbXv453/mnf+Xp5ct57fUXePLpJ3nypVUcLWvFVoeFKs/y3is/4lv/9EN+/8abLJM+Lz3zS36x/EMqJVpT2SYe//GvePHdt1i+4h2WLfsNf/mXf8s7xyqFLzk/FmtgKe96ObMjb6crf5q8KrHxK0+neTne8fJ25xhhK0enCN6MNybXZmY6pGm8bNYqxPgwn7zE82Ox4LW7mI2CeH1M9vMq/+gn0t5OzEmhZ5bFuTMX0FmMYD6OG1MNbjPLf/lTfv7+JfNFVDksTjtK9Ob86rV2/NwkV0QKmxA1LSaH3MDtqHQVXCYHmfqI+j9yE82P4GMYcLmxYiFGJNLxrYqo6HvFAIjJn6RnnF9tJgA9WOFEOua0kfMb+e4/PM2BRgMAZo7UCRPtoBlv+ejX1VxVZiC7Vw9C185wQfNh0xqT7J24MdnA1CGJTfzFbIeCgmxqLp7jarNnWYysnfB/lpfs0wkTF8clepOdYspD8TwJFze/wrd+uoJrHvkoxvZx3BhGHM8MImL8w3tXZbw96umNLkMvIuVMfeQmPmoSjRim3hTzn5oSOi0nN/Cdf3yGE00ehOiIluxrYMTKVH6iWKJjYCC3X1daSi/dGEv+tP0+jm7kM7S90sHAlQ28b8O3o87o2Kl/xOjTUW9ktjRDiaivXH1Eb+C4XtxEVG84xvFjxPm5ktm9oXtUMJ79BOh+rH86a9Vw43Y1bofxZ3Qj2y3n2MUKr8WVPFHD2xS9e5U3/bii68mtdvOM/BmenX7jyWv09OAj2iB0vdg3OhqyUcWMkc69ETMxr69c1yG/TwGRsmLOX9dAGcxkQGo7p06XaDQxmK5nl6ihqxIzREz1nyudNhXsDTuJZ0SyxXFdz46RaCchfYfDGtWD9OqaTPHZMzQo37olW/mb7/+Bwx0Tg5jsEfVoGt+JeboZEYydbtTHGZjqeDGy3OzD5tvQ+AScZzvZ1yAZejdkFU8z52pXrmzTvMuLM8lm4G4uBueTMsTkkxHRNHKbp9G20/amLtwewZeaRy61HDxyHsMjFArpgDyMyR3hi9v4u394igM6NFcHEA5L7w65XSOX9JA6ZuDx7BnVt2dvt0MyNXqv0XoOb3mTb3z1e3znpy/xu989xz/99EU+ONeMazVwaMtbXtu3f/Iyzzz/Kj/7xRN874fP8Mb2S7S7Ih9u5PCapXz9q9/nO4+/gpnH/+7ZV/inf/gdG8tDHrNYh1wNZz7kb/7hOY7Xqlp93m5kllwR0wc3+l9tfORTntwd+EYv71s4nX6N6EQMrnT22vQ039676iOyscQ0RCXszXRjsqOpjtcZuFhHnwhQJpU9DR+VDvbK1GD2OtxgCoMzY5w7XUyTIWGJhnCjgjXF9I+p/ngRTGe7ZIypOGl59E9q4cSZq97GnyUOtpnTuD5ye/bCrirhQnlbnIz6y/ihoe/1Y7xW6kuPTroG3zJUpIHofwQvXQTvNp7gJ//wM945b9YLghHNOD21GzvdMJSAb7pjWm9EohZde+ZTe+kUV1sciJzhh9//GStPx/vYyNRJKyKDWZqrSBROr3yav358vZcrXDMhM/3VwcuMe504MaODeOb26kr4ynku1sRlVJWyMfhzu5PZUsqx4joMjom7G3Z2Xa/OqxedsNY/ET1Pv/8if/XLdfE+EiFjcwNjSkwyquqPbld2M+1RPV31Kb5kBvdM5NLRo5ypU96MylbiZ5pQVtr07K/4p1eOeXRc+a2rXBXGpldBKuUXLlDWEjdqTPQ8upIrzjous5HT1Icjhq4hIyrGcIKLmJzstvDer3/Kj969iLqXWOgab7y8ieCwYfRJs6i9uJ9//sfH2XQdXaJ5Ex9D2+MleQ2PeImhT8Ga2yWi2LPTutDHX8fRs+XSSPU30YiqrzwaptrIZL6lvKk3mglULS6Vl07wu3/5Rz77nV/wpNbyf3jhNX7081/roHK/rAKlx3byy3/8Dp/65i9573iNN/6U6MD923/3c57bdJ7WsA4Y33+Nv1Ae+u4Tr/Kb3z3PP/7ydXaUeBEmmQ03FJdiJ4qnt73PD3/6nJdvfvLz53j7YJnnJ7gRTu9ay1PPaH/tmVd5fsMJ5SlL09RLPKcDnV899ya/+/0z/PSVrVw1ix/Riskno8Y3IzW89ssf8tTOWsNEPL3H//of+79TQ5OOTNdZ2X28/5HDkCyt7gxDZTrvX3H2BejeLZ/spIDxb3WH4/15v+M4OI6tOhdLsD6fo2/T5sNn6o0XW3DpwBHOanVuWxb+/EHcfddMemtRhL7rrl6hnEymjh7IqKH9GTZmKnfN6Ge4I0YYuo7Hx/Fomoab6xzbMlWCNTI4+AMOgYRsuudmkuQthsG243I5Hh0buRqfvIT9cTiPrmqlVxzPwTF1rjAt+4b+Pp90ld52B5z5dgwcugT7R7KqDklgaBp7WRa6LFoqzvDaq6spcXrxmUc+xZceuY8vP/YQX7t7NFXbV/LK5ouEBNzaEiPi2mRk+zCblxmZDjkqiVoAmw3VnBwf6UkeUQWbRWKyg1mImpLgQwt3i9aK0+w920hOuoUVzGPqnAWMLEhUgIMTdEhPj5c0bdZaks4V36ROOto8NGb11FDbH93qc0cbmoaG4en1wZ8BdmWH+MaxTWKiTaY2TA2/m2mKHD4ttsMNFRzcvZ8Nm/ax69gVDVY2PsDwcpuqOLr3AOs37mHzvrOYObD5b2jZrdXC2cOaD3az7sNdrP1wN9sOFdPgyi6hag7s/tNtSSFN6Hbv9fDWC8fgrvlwLyeutZEgv3Wbqjm2L85v094zlDUjX5MLSp6P3Za+tIl6XHwM7/Ubd0vG3axas5eT5S3Ulp5kzeZDXK6NEpAyJon7fWFKTx1n3YZdgt0juXezRu+bj5cREUyCE+X6hdNs3rybD7cf4XR5Own+GJePH2T1hj18uGkPG2QHj9fmE1ypq+Hozt3S3dDaJXq72HqoFLN+tRzwi2ZN6Xm2bt3Dhs37OXC2kohjEfRD3bXzrJOsFxqi+PwuVks1+7bvYMeFJuxQIwe3bWPziUpi0jNBTlF35QLbRWf9toMc1cauozpXJgjo2Xi9mF3b97Fu8152nbhGu2ObfXwaW2Q3AyS4j24LW8HvWskMnTSO7gnQbfxtfPXzD/AXX/ocf/XpqeRqoEN+GZQCJtc4Ph8+E4eShU9cLha27eA4phi+Lk4gmaz0FNnO5o8u0Y3DOhjaVifAJ+ptfVttVfKj47Rqo9/klY/4OJj2TtT408KfnKJ4TEYh4lXZjt0hlyNeFlgOqWlppKWmkuk4+PwBbfhZMpKalGM8HfU08tmqtmwHR3Cm2KYCXS7Yjo+A6oPJeXTJTiHg/TWlJXo+gsEAwYAfsRbwzbeFoeMTnvf07Gm04gZ/U+8V8TKxadk2n5SJG5cEsWyPpodjGCr2AsmpivUk+bx1A9KM5JZlUafNwx89v4OBt9zLVx65h4cfVh68tT87X/wDz+8uxxGMVXGNyy0+Ro8fzIhhAxg5dBx3LhpHukfNxbqJp9/rF4seo6Zz/9zhaD4mqI9sZuSypQuW49k9PSXByysCunFHNLFuVsBEfAFGjx1N14BLVq9RDO2fT+HQPvjdRIaMH02XBGiPQm6foYwa1J8hg8cwKMdPxFT6IVJ2koYuQ0mtO8/Bsx25JBLVZmgGY8YMZ0D//vgubGDZ5stEBZ/RezAjBw9i0MBRDOzm18QqJplcb9Nv9PhxpBGjT9EdfOubX2Bxj1pe0sHlhQZIT3K5VFpHdl4XfGUHOVyJ+t/SBDyGPy2X8WMGYEcTmLT4QR777Gf4y4cXED68QmPMZVx/IgPHjvViLrXPBGYMz8evxOTKppae5r+5V7x7Ob9bdY4xtzzIo3ffwX2fup8HJ6bxwWu/Y93ZFgKKd9OfZjPIl5LNhDGDcaJBihY9yBc//QBf+PwXWTisB6ldBjJhaL7yTXduuVuyPPgAX/viI8zsl0O7NsPzhxQyolsisazRPPiA2r78GLNyq1m+9G1KQpDZbQAThvWnb0xT8HMAABAASURBVI9Mjq55i52lYXxJPgaPGMHA3n0pHD4YDYfeIlLzRyqLK8nun8K5w8eo0pot6FjEoi4Jub0ZN3YYA3tkUbx1OR+cacPyB+g5cjSDew2Qfw2lf9/ujB7UVbbPY9F9D/DFzz7Gtz73KYZwhMcf/wP7rraR13cwRf0yIaEbi++9n8ceeoBvPPoA0/pmyQcgbKcyavp9fO3R+/jsQ/cyMrGF9qzBFA7IxRdxcdW73q3AsnyCr7nGrq07vNxtxqH1Ww5y/GorSYku18+dYsO6naxTrl+nMWrt+h1sPFZGc2MlezbFccy4te6DPew+VSHelvef4jBp0+PR+WNZmBjwim1jItLVr+04HfW2vlw+ebnC8wUC+Hx+Cszc0Py75QJzBe3R8vBtbKOVlcyUBQuYOyLfI+Mor9wMY3h6DTd+XBD9ThiTj2wDpGpbMnbWO6qUqbBU5+ug6fNpHHCMzHiXrbY4vGQRDbkx53fuoURzKKGLjd2hp4NjKoT1J+kJV00IgTg9B590ty0LX7dh3H37NHol4l2Obd+A6aTpNXT+eHoIXzZKyOpKt6xU/PJFZCuf37mBa2gLtBNLT4tgShqZqQn4LH3qth3nBvyf42X06ZTZ9vAsnJvsZNsOfp+NBXQbPoUH5o8iw9YHDr4b9G2kKpZbzYc7zyGmWAKJqdLphJHeUgFD70/3h4tlfWQbv9+P0EnqNZL77piKzv9E0cKxHZwOmnF5+fjlguHhaIxLTMmnW2YqmvYLxlWf2Dg3cC0jjuo/uoXagdvBo4OBZXd8G9yOOttxbujv6aN6g29F6zm84zANji35LRzBOWoDC9sxODbmsp34u207grGwLEtP826KjUX8suyPbGILJl7b+etiWTY+2cofTKRHfjapZiKPLucjPMfupKb6jtvQ9eQ2cig+fI7h6Xqtps1RvVeEa2ptx+EjeB8+1VuCtqwWju0/RFnM4IMleTw8A69iYeFPSCFD86mgLUq2j7T0NFI71otg39DbJ3iR5f/tJRt00nc8mQUtnka2OK7l0fN5MWOiRt+yjyN/yOqaT15aAm5UXLsO5d47pmH+34/osm6yc5yuZP1kfZyBajtuI4tPtrghh4UjHZxPwNmOg8+xMZfh0ymr9+73EVCuTAgGbsSZgbu5GDiPrug4om0ks+04TVtyG9rGmy3ZwTEwKv6AD9vxkypbp6cmezyCwaA3t7Msi2C3odx953R6mXWc7eD3Oziibfgafj7REBjox9F7vNj6NNxBL3j525fBuJmjyRJc/+n38vWvf5H7+tTx9O9e5VRrOoWzR5Jh2mbczVe+8Ah/952v8sXZ2ax9+gl+ue4SdiCNcZNGkabpy6hb7uYvNI//+pcf5ctz++MnjLmEbh6k9hihXDCZbkn6tGzNJRyMXD7TBz6HDvFNI7bjeG2m3TENGgws2cr7Nm2qUxVYNj6D29mmp/n24FTvc2wsOi7LwnacDro2IgFqtR3H61/bjj876xzVmxKHky8K3/H58fkD5HXLIUPjooXqsXHsTroOtuA6rKzWztu6ibdgbLDUv+nq39Qkv6TgpsvCF0wmIz2VhI7BwLWtm/DtG/CWbeM4TrxYIaqqagkBH6tXu5HTSu3F4tsXMC7fh3dZVhxP7Y5jo08+eRk9bMeHTyWxq8azzGRsFID+Htx6xzxGdg14KLbwHcfx6PkUu42V16lX9/cqnM49c4ZioCy7lb2bDtMsWI+XdZPswjUy+pNSyUxPIiB9PcIdP5Y/icy0FDJSUz0e/qAfR0SMfEbwj3g7Xiz4RK/32KncN28EiaJhfMW+yVa26Hu4arv5tuyPZLJtB9dySExLIyM1SX3hkx1sLPOPbbAcRsyYy+1FBeZDxcZWrvLLn5NMrspIlIyqFiPHsXEkkylirUrL+zZymjq/z4nDGtqtJazbcw2fz8aykiict4BFo7Mxl91ylbPlNoPGDadwZD8KevVlyW1zGJxhWsGxP+JjaHu8ZCfDI14MzTisJft7sRdIoEeXLFIS/F6DazvS08Hx5LWxLSmgFtsx9fq2TZuNZeps+b/WDnl9RzC2RwJ21jC+ohzw1cce5m+/9gDj8xzaBNdz1FS++Jm5JFRdpSbsxyfBqmvbGTbnTh6ZO5CgP4Vxk0eRJtcavvAe/vJrX+C2vKs88cS7XI2AVAAkh2FKiAY7l9vvu9/bN3hsehornnmdE2IUKt3Fi2tLKHroIb72uanUbXuPt443Y/ks8ofO4q+/eD9f++pdpJxczbOrz4umhfbTcdx2zu7ewOo9V2hzpZRa/u9y/7dqaxYTps/qL5/hiDag6ltNegKruZwP3lnB6ys3sWLLcepcC1uAllvHng3rePn1Fby6+iC1WJSf2MHTT7/N2+vW8tSTz/D4st1URS1aS/fy7KvLeW7p++w4V01NyQWOnauiGRe37Tpb9p2n4tJRXly9kxOXq7h44QJnyhvwLnlU5Zl9vL58LW+99S7PrT5EUzhG5fHtvPjGGpa+8R7rDl8XJUELtvrsfl555T1WrN/Avss1uKpTC23l51j1ziqWv7mSZeuOUBOREmowCccslF29S1KObl3P0rff59WlK1i9t8SrrTmznzfeXM0b4rVi52XRhMbiw7z49HKWr13Hs394hl++uIkjZ86wfuVyfvLLF3nv0PX4sCo2lafj+MuWr2LNvlLirF0MT8MbV0Ctlax8cxNJU27nnmkDaTp/gFdeepXHf7uS43ZvPv3IVGqO7uN0dUQJx6L5+imWPf8qr6hfXnzmRX76zGr2n73I1jUr+fkvn2PZjssen+RoHfs3qZ/efJ+X31jP4esR3LoS1rz1Gm+sfos1B65xrbSEU1dKqWlqIeCD5osHeG35Gl5/YwVvbTlHmw2B9lp2fLDW6+/X3t/NlVbVKeF49pOVOm/z7QQtas8dZNkbK3nx9TXsutSEbXKWaewEJK6/P1bPrjWrWLpyH4cObOTZdw6i/XXPxwyoKxw7AG2l8o/f/ZT/+NWv+NXvfs2Pf/FTnli2g1rRjZSd4uWnf8a///Ln/Or3v5H9f8rP/vAyh2os0lvO88aLgn/yd/LBF/m9JkQ//MUveWVLMb5wMW+++Dg/+kTby9r8CUZLeOelJ9T2JL976Vl+//wfeOL5F9hSEtGJ8zmP37/9Is7vp7/6KT8Vv4MVURJs6eV1rCc9CguIlLPm5V/x4yd/y2+ff44/vPwHHn/qebZfrOfaweX86LfPsv9KO4nSRYf9JARaOfzBUn72s1/y5IvP8IcXn+bJZ5/ilY3nSAiGOLJxKT/+6Y/5yW+f4Ge/+QX//viTbDh+hRMbnuNfJdPPn/w1v1T5+RM/E+1lHL12gVXP/YIf/e4pnnlZNnj2t/zk5z/l5W0XQX17effyDnq/4RdPPc5//OxH/G7NcZT/KT+1jl/+5Mc8/saHVDsWwbpzLH32Jzy34zr+pqu8/ewPeGLtGQ0MLsX73+PnP/2RbCb+v/0V//6zn7N0z1XJDFcPr+Pxn6vtiSf4pXj88Oc/4Tdv7qZBtopp40UHwvzZSz5g2Ta6PZBYSyN29kCKRmXSWnaaZa8u5eUVG3nt+ef5ydPvcrjjLyqEhokvsVA3NHNw8wZeXvYOr6zaI/+1RCtGeyRmQPTecXtI6rKGYlYp5pcr9728aj8VoXh7rK6YNe+uZPmKVTz36gecuF7B/vUr+e1LK3jh/SNUtrRyeudaXn1rDa8pP+261OwhdpD13tFxsDlljnVUXju2R/llpeDfY5VyjpEX5a2WivO8/tY7PP6bV3lvn3KRRL5+Yhcvv7pGBzDHWPryGo5VN3Fuz/o4v2Vr2HGxKc5DsJf2beUFxd+qtVs4ocMOtHmK28CeD9fyylvv8/KbGzhS1ubBmxN9j29LOevefpPfLl3Lm6+/wS+ffoudxS2IHObn8qHtLBXu0tdX8+GJKiQmlce28/ulH3L44D7eeGsNh67HQJfZ2AOL9opzvPfWKl5e+h5rdQKtIQGiYSIfs72La4jRxPvv7sA/ai4L+iYT1cl7JBIlWDCOu8ensPW9rVSFmtmz+yTlZRd4fflm5d0yLl6+zPmSWlowl0WbcsI7b63kzXdW8vyybVxuqufcibOcr6z3/pqXsOywYSWvSd5X3/yQY9fbDSJh8YtqouTZwquJ/7SrWd2GZaPuc/V0pJmLQL1iG+eMgRsHl0+5He+u1+4KOhCEcydr6Td8PEX9bY4cPUqtYsqnTWRXeOH2EIk9pvC5O8dyavVrbC8Ok6CN3LifuKIpoJvumJjbmuQRDdEiW/YaOZa0phJKal0Sdeh1raacjMIHmJhezu6DJZg8anCMYELFdhzMWK6uICm/O91yEmmqa0Ci4grAkk62gKMx4pf8NWZZ+MMVOuA6SpfCW5lS4KMlFKWtxSVv1Eym942wZv0eWl2wDXGDqXeRw3YcLMsC3RGdpA0rHE0XLYzDETSJtb16R/avbAgwbPQQ0pMtwhrrsWx8to1SK+16H9C7wBuPNC/FAVrbHQbNup9b+9Ty2rIPqBE/R3KrCTN+6FOvFlb4CkdrenDn7MkEK45y6mpINhFfAVgqrW0ufYvu4YGxDiuWvuNtcPs6+sajIxhXwjvSy9WmdaTdJZqYx62f/jJFwXOs2nqCJgnkCMd2RNcSW8FUtCczZMRQcqVAqibehaPyUVdTc3IDr2yrZe7ddzMww6JNdrAsg4TkjmFpPGgr3c8Lv/sxP/7dH3jq2Sf5+a9/xk+eeo3TTSEu7HqLn/30Fx1jxDM8+cxTvLzpJLUVJ3n1Nz/SnOBpnnv5BZ76wxMaL3/Fe0eqRBjaJZMk07vrPWi4wrpV7/Pq6++wfNMJ6mJIy0YOblyvfLmCV1fto6y9Qy50yQ/0ixWqYON77/LKe1t4e6v4hh3UTdqgbGb/xrXCfZelq/dRGbFwK8+x/0wxFbVNBpX20mMse/N93nhzBW98eIrGDlHMw9gaSRCuuag522rF8Gqe0/hzojxsqrm4bytL31zD68tW8cHxaozJSg9v4+ln3tbccw1PPvE0v3lzL1WyJ5qFnd2/lVc1d3th+Qblu2ZqT22UzVZoPrBZeTFEzfk9yiOrWbbsPd7bewVzVR7bxpPPv8s7q9by1G+f4Vdv7KIinlxwW67z4UrBv7OG519fy4HiGq4XX+b8xXIaQgY7xrldm1j61mpeeX0lm05Vm0rUqboVx+ZLDld8aLM3P1q1+kOOV0Zw5B9gUX56n3R7l5eWrWXv5WbV4OHqN37HopgcJRfUt3gd2M7rb6ySjiv54FiF6nSrj1zzUMGKcWn/Nl6Vfi++uZHTdSLXXMKKF17gqTf3UNbSzqlta3jhvX3Uxlo5e+wM58rrUVgLsIF9H67TWLGSl9/eQan87syad3h26XKeU56+2Ax2yzXWvfceL7/+Lu9uv+jNO09sWsXTr6zinRWrefxXf+ClD04qs3vC0FZxlhUaD5a9q35dvonLDRGVm1q6AAAQAElEQVQdqJ/jxMUq6uV7mqDeNEZ9oD4LGUTifiGRzJd0urh3My9pTbFyzSZO17TE7aTf6yeN/VbyorFfaUe9sYcpwrW0MD2xfYNy//u8pnn+e7uKVQtnt33Ia2+vlh6r2HquXowa2frWGxrbVrJs+Tv86qk32XyuThzaObR2Bb/ROuPFVUcovnCCZYqdDaeUO6lly6p3eWHNMeXAZra99w6vbTjK4S2reeXDs9TXXGG11iBvvPMur6w+QGW7x5qrR3fz+purJPM6dl+o9SpNDo7rbNF2/STLxePNNVv58GQlEdeKw9SVsHbFCl56/V1W7rpMnJx8TLoagPJjW3n6+WW89s56nvnDi1pMH6TOG4A1JzuylaUa/16Xj645Uim9ohz+8F2ekk3ffXsFv/zV07y27ZwiCC5vWad56Ju8+PpWLjWGqL1yguXLVip+3+XNLWdo9cSJEonEMH5neHtzHA2a3rdbo7XJOt7QePzC8o2cqmo3IDf61PvQT4fYtFad4x2tl5a//S6vrjmCsWxr2QleffZNdl6LQHstG99exkubznv8rGgdu9apz9/ZxHsfHOR6yKdDUzR/PcXhK5U6eIuKOoRrinl/xUpeV2wuW3NQ9vcEp+biIV7XfO812XjDiRoPVsJ5z0j1eZY+9zJPv72LKy0xwsply15/lw+OVnntcf2a2L9hFc++ewgtjSg7sZtnXljJcTl0xfHt/EHr4nfWrbmRm6qjHqpYuCrmPUbpib0sU05bqnXX6gNl6o+w5vprePHNXRxVXz2zfBeNMYv6S4d4Y/lqXn97JS+9uwsjhSMrtFRd0tz0HX7y61dYq3mG0az8wmlOXKihNdLKme2r+cXv3mHjyWrDkOtarz+3bANnavXZXKYYXil/EN3VeylrM9gKAU85tZtbCcd2NMC57Zi/bBw8ZSQZjRVcKDONFpaSv+VGMT7bpvGi98Q7+as7e7PrjdWcMvpaYAnf1pOOq6BoEqOzkrwvy4o3lJxWn5VWYeYF0SuH5b9v8sbKDTz/zLP8+PkNnKsxxIQSbdQexBqt19fxqvYh1sqHEY2rx3fLNqtZKluuPVRmqmg8u4enX1vP3gMHWfneat7fto9XX3hDcfE+Lzz/Ek9pLVIREk1zt1Wxfc37LJOPvPb2Fs7Wmco6Nq94j2VrDnBgzwaeW3WCSFsDu9e/h4mhV9/azKmquFxWa4U3Lr68YhNvbzlLQ9jgg6XZ6eEtH/C6xo1XtRY/eKVNdXhGNjbTG4Tqb6K5iZNaU5r6cCSKN3czHx8rMa8vOvGdUDXb1q7hZa1zXt9wjI5/yZJqrcffeHsdb739Dr/67Rtsu9zm8T23ay2vKge8pvn85rMN2KJddfE8J86XUt5kkjHaA7rOB6vf5w3lxlff26UYwLs8ngpY4yKWMs+pHet5Yfl6Vq/cxnmlT3O4H7p8lkOXy6htihs3Un2J1SvW8ObK9xUTz7F0X408t5ETJy9y8VqD6EY5u3UFf3htBc+t2IGWyDRf3KvYXMUyjeHv7rwkTgKLRYhoYmp46+umO4blRCk9up1XX1vKz59ZxSFtVhjPCps1nfp0uVnTvbeH6xFpG27k+IlTXCyr06gAltXOqV2bef3tNVqPrWXXxUYMrpxacnomw1xV5w9qjF7BS2+8z64LHTBy2Kgmup0yGftYQo42VXP87AUuVjYaVCyrhcMb1/Cc1l+rNT8qaYiqTrJY0l1zm1eWie7bGqdrBN56hbdfX85zb33A8qWv8u9PvM7m8xp0I/VsWrqU515/k7e3n+JqeQWnzl3gsvmLcLeZ/duOUFp7hXe0Rj1YUkHJhUucLimjrkU0sWirusDKt1exTH3x/OvrOWNE09rWjCWe30uuMg8WIpUXeOeNd1i2ejMr918jqrmLR6Wtgi3vr2GZ8WfFyYVGo2wz21e+wcsbj+oQYT1L1x/GCx/NieUqmMu1bHxmfoyuWCst0QyKpg5GyyPPx9MGzeOrC7JZ/eJytu/fws6aXG6d2Y9ANCIqBsfF1ka8pZwWVU4aOWYkWTWXONvkevimD9RT6q8AYyaOYXBBorrPJbtLLzK0/q1vCXPl1Cma0/rQK8XF9eczroePwwdOEwr2Zv6s/sQkbIwc+nZJpKaiQrRc7bvZlJ88yLHWDMYP74E/1C5hwNUYF+soQvPq/jf+yEP/02r9lwEtN0ZUWIkpFke3bOZAeURfzbz7/Gtsr8th4ujBDO6Vg08WDmqT8vT7b7P6ciJzZ04mo3gzT664SFoXmyPb91GeOohZs4dQvuNtXttdhz8rkyR/kB5DBjMgP4XkQBu7N27lpCYhlj+dXgWZpGR2Zcyw/nTPScUtO8F7m0+o06H17BYef30fGUNGUFgox2ioo6mqAfPfJy4YP4FZg2HNa6s4GpYjlOzkF8/vJGnIaNEaRH5agKhZdWvh+ezzq7mePojxWvTa5z7gybcOeRMrSxPZGJb+iXFoxWssPdTO2HEjGdsnnyYFdXXVEZ58dQ+Jg0cxfnwfLn/wOr/fWU1CdjIlh/dxtCGLaTPGY59dy29XnKfbqPGMz6vntVdWUir/jJTu5Mll+0gfOooJhQWcev8NXt1TjnfJaV1xNkmqZP8mLiSN464RGdrQ2cK7Z2wmT+jBlYuXqGyI4aZ3oyAYobK8iZawRUZ+uhZOBzlyPYHxU4rIqN7DS1pApPQdjdDY9O7bHFVA+kKNNDvZjJ40gf7WBd57fxtNadmkOJDTcxBDemSQnZ7ClUMbtdhpUj+5NNc3kNx/AvPH5nL8g9UcqQtrQbWGPWWpTJ0+gYLEFqrrUUBauJ4iHT/yIfzgXtnPq+8fIHdMETN6wXotTsyAZDa35T4dwLK5PqxAItnRUtZ9uINqJ5lYcyPh9hgyiwi5XsJJool9m97TYrOVhV/9ES//9qc8UpjCvvXL2asR6vj2Faza38SMz/2A5598nB98egrVRzfwxroTtCWlkJSYRv8pD/Hr3z3JK4//LZMyWzi8ezel4VQyUtLpN/lBfv1UvG2y2o6orURt6Smp9B53Nz/+2U/41X/8jCd++EM+V+iw/YN3WbO/nqmP/ovH798fnk798Q9ZtnY/rdp8N7F0s10sJ0ByQiK9xt/K3//gxzzxbz/h97/+dx6YkK94SiAnO0t2ly2FZGHUtggmJZPTdQRf/Puf8JsfifePf8G/fnYSbZfOsHLVSqq7LuA/fvEET//tI3Rr2M+rKw8x8rP/xlvPPcnnpvQio/tgvvSPv2Ppr79NUX6a+ipBp4iP8vPfPMkLP/wH5napYuOeE1RVXmTVG29zJX0i3/vB47z4s39iyUCXTW8sY2dpi+TIID8/lWv73mPZhjLQyXNGejYZST5cnSqkped4/hOpq2Tdijc45R/Hd//tCZ7/wbcoTLjI2+9s4VLVdT5YsZRDkcF88x9/xcuP/5hPj0tn7+qXWXO6GXPKqfmVNP8zt4XcwSWkTcT6hkZO7tjJ0TqLpCSHQEYOtacOcKA8kaKpU8muPMTTb+6Lx7braqFuebgn3n+HFWd8zJ0xhfzqvbzwzhH5biI+xb/Mzo3LBKM+WlvrcLOGM2PKGJr2rdFGeq065hrP/H45F4P9GD9mFD2dMNXNIVKzAwTSujN+ZA9SNUg2tfoZO20CExOvaFNgE2a6b4mbK7o3365r6zNKWW2MwWOLmDUsjU0r3uWoWCX5FAMJGYwoHMu8CblsXvoyrx9qIL27j4NrPmDHVUizI9RUKx+GEhgtfpNTr/HaaxuoEdUq9dcTK8/Qr3AEo0b0Idf0l+XgtjTQGMyjSPADY+d4VRsQDYKXct5ASiANX/lJ9pxvpXDieCZm1/Dib17msPJr06E1PLXqgmiOZOKodHa89hIrL8VIzfBxZMt6tl4Nk5EcoaoqIoqyvVlpNBfz4svraOoxjjnyywMr3+KDS0qMifIfky8E6d2dxold42yVTfeuXY1I8jEHb66ivuzRvQtO7SUuWkH698whJTWTERqXenbNIKO1hPfX78PoTs0xfv2HtbR0HU7h2NFkt9RSpUV3QuQK69YdRubF1WFb2JfOZGOHlmO88O5eb2Jri0+nKEauzvdoFJSuTdV/vYiIa9sEmq5RaqXRp3tXJhaNpO3CcUoqXBICNmKLbbm0tUboN/kubh/YwNLlH1IRc/Dbf56lK+8OBJNJD9q0VZdpiaP3NIu6qmbCbTZ9R2QzaURPKk/t8/5CICAehhfCs9wwTQ31NLXWK6Y2c66tC7fOGkYsrFaLG9dHry74VK2F7lXtMOd1zSeidYUl3aQBbZK1S5d83OsXKVcX2wIVhn7NbWH4NWpsaW2pZs/ew1yvt0lPsqS7oESosbmJ6isXOXDkFO3JAYJ2DLUYSSVTMzWK/ZbqCnYcvUjW8CkMTMKLczvWTis5LLrnXvJL1/Dqlmp8CcE4XcNaClt+aLl8DregP4MHj2Ns9xgHTpTG+1w2ERMcLaybI8nMuuNTDGrdyYvrLoLoID81chhSN4ryhCX/NodnUTuL8cOyuV5aQm0zyrU2bjhEs/SpkC327T+LlRYkIG0cf6L81o8/XMUqLQgDo27j1tGphFpiGHo36JsXMbUFn6x8O/GOb/PqC7/nPz4zg9i1PWw7VkVaehrp2QN55Dv/wW9+/FN+/ZPH+eHnppHqD5CYlM7Eu/5Km89P8oe//wpDgxfZsOs07aJpfBnJEo1Z4tLGB8tXctoawJzpQ0mNNdHcBhc+eIe3TzvKl5PJu76L3799VBgCj8aI6gENrH7hVTZWZTFp9CCG9szS0O+S6Hc5teYt1l5IZM6MyeSU7dLGzVFIzafl5C7eP3jdw26prsE/YAIzpvXk7IbVrDsjw5kWL8jkE03FPKtNm9KkforhUeSmhKgub6Xx9Fp+u6aY/sprE4ensm3pi6y67JKVFdai6yA1WcOYM3sgJRvf5J3jrbRe2sPSzSUMnz6Jsf2Tqb4WIsn8R1gD6QwbOZjuaQ41Za30nDyRGcOT2fb2u+zWiik108epPXspTx2guexwqjZrsba/TBK28tazr7C3NZ8JY0fSr6tL8cVW0rNa2KTNwbNNAnHbKGvwMWHaRIq6tPD2m2vRUIrxMVcbh8bqFYdW8+u3z9FTeowa1pusoKzqBKHuIM++doC8yZOZ1z/EmzrYPNMGWBYx9R0fuxx1YxPV7QkMG1/EtP4x3l+2mhNGBvVWTH1leNUeWcvTH1xj7IxJFAZKePqFDTQFs+mf0crGD6RjUx3F12pI79KbJMtPMHKF9esO0WBD1a73efe4y+zpExjmr+M6PtKTk0hMy2bEyAHkBmtY+fzbnAsO01pgONe0efD2sRpy8lrZ8eFRAgNHavzKYds7b7LxvHQMneW3T62iLn+Y7DeK3rHrnGqySU9oYvP6HVxqBbepXmNUPpOUmwdEz/DqW3vkbUbxmGzgYum1Yt8qnlx1iT4TRjJ6RG/SAzau/N69vpun3jxKz2lFMtBioQAAEABJREFUzOkZZvnLq7jYIgTZz2Aa3GPvL+WlvS2MLhxJ4YBuNCluG9wIVeqziVMnMiG7Xpsd66i1Egi2XWbn8SaGjh/LjD6tvPb7l9lf7VCQ58NJ7sLY0T3Izsum+uRudp+vFaNkskLX2LTtBK1WkJTa0yxfd4DmhACxlmaqauvw5Y1i5pTR1OxazZtHpHDFYZ5dc0qHX0UU9cumtbqaiCipx0Fy03xGNltJde5wCkcMokdWUHZwgDreeuFdLqeNZt7MwVz6cAXvHq0GLBnRxVxpmQH58RGaFRfTZ4yibtd7PPHuSVz9U18Zoq9sPHMArNUG3umoTbbdwLadZ8gYOob5GuPXvf4uuytdcrom4/iyGa5Yz0mIUdvcSk5fxcy0Hhxb9S7rLxh+FtoPMGz/qLihGup8PaR3EV0ajvLye4dpE5RRT4+PboluPppaGwnmj2bG1KGUbV3J8gMt+NMDXDi0n0M6EMeXjK/uIpsOlEpbl13LX+Vtze+Kxg5mZL98Eq0wUR+kJUfZ8cFGzjXY6BSM1559m8uBfkxU3ydf2cpvXt1PS6yct17ZSnBkETNGZ9KgJG7OPUVYdgYnJZvE6hNsOFKBX+Oso/GmvqEJJzNFosY0PMT0DJIdKmHjjtOer2Z0dTizazdHqy3S0kMc2H6AqsyhXm66suVtlu+pEU78VkqQbM1UtVn0H1XErFGJbNQG4oEGhx4J5axatYXL0SC+WITKo1u0Ht1NktajE8cNIaPlGiVh8GsMi8QCmkeOZXxWFa+9uBqlRekfYseGLZxqCdAlL4GjW7Ypxiz1PoSVg+sjPrKT23jnmdc5Gespu4wiu2ovv3lhC3EJY54NPEkthGcRCCbjODYt5RXU2wlkpanVNTbQU0azLAvHiskuLv0GDKVLtJgTFYBwLI0ejXX11Dc0cGznIY5Vh0g1ExzXxe2gkR0IsW3DJswZkJ2ZStmxPZxsy2Hq9MkknN7MC2uOixjsWPoC714MMHHccEYMSKL8aiOhCzv57fKjdB0xSrp04eg7r7D0eLPW6ymc27mODeeaSE5xMJtHxQcOUZrcl6mKed+Ztfx82VHRjfLB68vYWJnOhMJR9LUv8IdnVlDuJtE9coE3Vu6mJiGFWHMzDZpjxRJzmKYY6lm7n+dXHPLw1+iwYkt1LpPHDGFEzyz1GwTVcmrVMpYebtd4NoLxXZtZ+swyra1jIHvRcbXXtxJJyGaqaPapP8QLK/bL5mDHZM8OmD/1iHpjucu2ZcvZWttF43YRsePreG3bNcK1Z3nm1c34+wxn3IgcLpy4iJuSK5ki1Dc6jBCv6Tk1LH99HaUSJyXXz/HNm9lZog/qWP7sUva3d2W89kW6NR7WwewHNLh4YsfkvBZwcfNb/GFjpfLkMEYNLiDNH1UOc/DlJHNq2wb2Gielnjeee4PTVh8KRw+h8cJZyiIpZPgTSGm4wMoPDgvCpkvQj5OWw8hRA8hNdKm61krBhCJmKjb3ai9hZ4UYBh3kMHr54zumJJSS34NJRRMZn1rC755axmkJbLfXEkkfpjXdWJoPfcDrW6+Ck0BqUzErNhyRvHB589u8sL2a4eNGUNQH3nthKTvKwmCJXcwojQ4DD/PCsn3kFE5m3lBY9dKbHG0FHPejWNGnZRmcGHZQ/lJyiHe3n1ctHHnvdV7dH2J8oWw1sBupUsWfZGvZ8gGvbL5OocbpscmlvPySxiB/JgnXD7PlbCvjJk5ilF2s+rVU2YmysZ+0rj0ZO6ArmWlp3iHL+/uLQeNO74J8UhPTGSYevbLTyE5vZ/+6zRyrBdrP84en3qUiexATxoyQj1ejfXZaqloJan48c9pYosc2sGyX7EMlz/9uOSXJg5gwajADuiTjRi0S5JUfvvwqm2uyNY6PoF/sHE8+tZpqO0hycynvr9mJIg1fWy1mT1xchWF+4yXa3kyNcmjd6T2sO1Gr+WoitprMGijq2oy/79PMTDzBT188xZR7ZpHnk20t24MRmGhZBJJScRxLcVhBg5NKbtDCtm0c29bTUpfZ+H0+LNsCy6K28hqt6b3on+qj7lqtYiCZJM9/AyRl+GmtrKcN8CfYnmvZVgPFVVF6DeyLpX/aKk6zbm8t4ydPINdq0z5czBvvbMfG9njaiI0o/O+87f8JtQIZuRTkZ5KS5MdVUth6vo1ps6fQp0dXrz5g2UTb29i77xyWFkDhSDtpgQinD54hkJ1Pz7wc+gzow+AhU7Q5nEHV9UbNFTLJSEoir3sP8tKCBDNy6JGfgd8o5CSSr03nBE1oB/XOIyMlSG5+PrlpiVhq369BvCmnkAXDu9Or13AevK2I7NwMeo8cxxBfiHZfEoFQI1q/cWb/PiqzhnLHuJ707F5AjhaxAU1MW84d42htMrMnDaB37x4smtyf0gN7KTVJQy7tOU2onA27LtO3aDqDehUwZPxE7l40iuLtOyjTImTx8AL69BnBgmGJ7Nt2CH9KDt3z8+g3cCADB41iQVFfUjQwD+rZm1kLptE7oZWmlijFBw9SmTaIaUMLhD9aNrHZp0lKvbSzLBc9gEb2HyplwJRxOO2lrN5ZR9G8MfTNTSGzfy/6pNmCiYHtw1YwxaIQSMkgJ0cy9OjLyLGDmFs4kIRAgD79ezB/9lT6JcWoaQwRze7B2CH9SI5oQhYMEGmsJ+Qkk5WaQHJWAb3zkkhKyaBLbjbJAZuwBrK8oZMYmdpGAwkk6jStXsk7Emmi5PJlqsNpTJg2m9HdoaVdIlkSreN2JWIgqCR+6iglrQ4pRDAbAeHSM5yubsZSskCpowM8/rADWjxm06VbD4YWTuTRO6aSGrTQeKt22Uc4blMjJWXXoUcRC8YXkJnVhVlLHuXrj2mjwK3lfEkpVq8iFk/qR3ZGFuNmzmBMfgqVl89g/jtUPsnR1lRDSUkJly9fpT4UJpicRYIvSkSr8VBz7Udt7RECps2JepLWnt/Mz7T5++8//SG/fG2HdG6itOQy0R4TuWXyAI/f2BkzGNc1lWrx02ErftnElfSdtzkhc31+Gs7t4Jlf/4J//eG/8/PX36csbBNw2wlJBjO2dcKbpysCEW2qvfPUz/nBz37Mv/3sOU0ibepqSynTAmjUtJkM7ZZFd8XZ5z7/dT4zfTApKRn06J6L5ulE1I/p8o8eXdNJtJE9Y7Q11nK1tFiLzatUNVpkZiYTqT3D8fIUJkyZzMiBWeT17M+0KVPoziWOXW4EdWqYJG1ghtj+/jLWn6rF74dwNIYaZT8tlewojU0lFF8NM6RoKqP7ZNGl9yjueeSbfO22UUQbL3OuuJUhk2dT2D+L7K4FzJo0ke4pTZw6WyKfNpSksCj+6dvC50YpObKdd1avZ+W209TJ92IympOYSTfFYa8+feg7YAB3TBsEtdXyXUPJRXtiEKvi0IELtCcn0y5bJweinDl5DoUnPvkXxtgG/KaSmj+CaUMC6m+L5IQoTW3ttJw6xu7KFBbNGkrvnj2Ye/ssJvbtTkZyAsmpmfQtkE+lpjGqaAxJrSHsxCTCNQ3U86cuC9syNnQYN3kkuYGw+syPrXza2CJbWOBPTKdfr14MnTCP2/u7bFfOTczoSvcuuQwcOoJbHrqNGQMLGKVNjJQOfqHaFtqJsXf7AfwDpjCtf4F8Ip+MBAe3PYSVXMDUkb2VM8PElC8iDY00A5ZN/PIl0jU/h64F3ektm06+6xYGuFc4fOI6xw8eJtpzAhOUH/vK7ybkN7Jl9yUSsnLVB10YMmYC8xfepkmZH1d945cOTSVnOVTaTHqiS0RMAtXXOHS5CpyANiRBmsb5dv5qAz+s4Hf8PjQ3wOqstywCflt+HKbN9ZGdl65Ji+zTvwuZaYlk5uSRn5FEouCL9+3nFD25Y3IfevfqwW33zGWExqqsvFy6ZiSDXNbK7ELhmKHQFsJJSKS5ph4zHDh/JJAI/ifvP4dq3MtJgJrLJVRUF3Ps4nmuaPPVqTst29QS9TlxPaWj7YZpdpOYf9e9dLu2mtc2X8ZJCGLs+UkxXGH51NvlsvG+PYdYteUs/abNYWS+xXUdIpy62kL5ibNct1NovXqaY5ciJCrHq2tkWwcnUs3BnZtYtfI93tHkOCm/B4FwM1Hrk5xu/nblR2GNExFsn93R0Ing4nds7Fir+qijqePhWg7+aCX7d6zn3ZUrWLPnGLUh4j5g+7EaStix9UMtBDZx4kodMeUdE9+ujGc5Dm1lp9i8aS0//cG/soUpfOP+CQQjUVzAsuUXoRZ8PUbx0C3DtSHyCtuuhUkKWsiVcOVICYEYx05W0FZ9UhtFl3ETHS4dOUC18oitMRVz2Q5WuJVIZn8evGMi1za9wvqzLYp/n+jEDMSfKJb4gxNMlF0iyH2xfH7CytO7NkmfDzZysqxOcsaIua7aowSUf05robOlpiv33DmF5EiMMJb++RPkZYeoiIZDbbQ0NdOiPGT6z1beMrrFwuWsevZxjQ8/5Qc/+S17K2LKabKHJgnNtZUa10q5XFZBQ4tDelYiQrvBxJVUEKO1pY7zpy8RSujDwjkT6Z5Uz7adZ3XonU5Y84bUoMvFgycpF6YleSzL0iHDBTafamPavEn069mFXvkZBOQPZpPt4KGzhJOTMP/mgi8hzPmj56hNSKNvj2zSEn2iAhkjJzE1p51QxE+K1UptXdird2UFkaf83D6OVGczb85gxXABC+bMY/JQh50fHMDXr5DxvQvoM2wGRXl1bN11juTu+XTV3LPfgJ4MVv30gWnU6LA+ZkNlcTHnymIMHjOZ2eOyCCYkkZGUTI++XciQPP0nTaCHGyLs8+NrafUOTxJMTuuSq/lUPwYPLmL2iAwaa5porT2OTMP0OYX06dWdadPmMbewK4nJmXTPzyRoSw0riSnTRxLQPMPS4jrS0kKz/AzpZm6IcmDbEZwBE5hl8nOPbmQG1er4aDh2iLOhANka79okY/u1Uxy5gne53u9HP5boYKVRpPlfli9EyA1iRZqobxGMjOjK71EfH9t7lGp/CsFwmMTUINXHj3M6msywOz7DZ8e08vSzG7CHLODOCbkELR+53brSJSPR8xW7vZXS4lJK2oOMue0Wxqc4JKWlkpScRv++eaTWFbPzfCWpKT75il8xWc7hU9Uk5BVQ0KUr/Yf0YOjkeYzNi1Hb6BI+vJtjbV25ZWof+vQsYPaSW5iYb5OYmas1QRoBxYKV2oupI3ppkyiMGwgQaWigQyUvliHCju1HCA6exLR+BfQo6EaGxgt/EK4fOcrlkJ80xUwkOUDr+TMcr2mTQUAmEWoVH+64QK8J0xmqcWxg4XjuWjiGBOk9adYoAm1hnEAC4cZm2vBrzaKxLa87fXv3YszCRRQGq9hyuorUrjmkyqf7aMxPSc2kS0665s8WEKBrl1yyxNvCR67GnJ49ezF+4jwevm0M/foNYcJAPy3tFqkJEWo0Xpsk2HC1lNPFzRSMK2TWhP7CRJEJhmLlwQMcD3XnjjkD6F2QT6MmlakAABAASURBVPeMoGQMQtVl9lysJTXJpj0SINhexqETVepx4RlEIDE7T36ZS88+3Rk4aDQPLejN5f0HuCzKQ2dOIL8tRESTdquxicqYRU5ePt10+Dt4cE+GzZrCmMwY15SoU/KySElKoWfffFL9ifQdOILh3cIaPn0k+duorm0H0eSPLsursRL7M29crvSOkqAFfpP6NOK1fPwnDg25PUZS1N+mtd2RncJUVbfhS8ogLy9T81kX7AB5snOONoHdljI2H6hkpA68BvXqSu8u2YKJmeggMSefHjmpJCZYuNcOsf9qkInTh9KnVw9matO++sR+LlW10dZYyblzNST1ncndU3vgt41cFnI4rGAmc2+dTU7TVWrln82hmPxiKFN6JKB0iOdXlp/c3Bxt9KhfhJqYlUPXnDT8akzIzqar5qj9B/TCy00D0pSbGgQF4uAVrFTGjBpK1+QQberLAE3UtNiYOUu37t0YOnYij35qGrXH9nItZQi3jCiQDv25dck8hvihLQZJiqE+8rVZc8aTF2vgekj9b/TPT9O4ZpE+cCp3T8rg1MnrWIqhkrYA44qKyGk7wJbzLoUzRtFXMTF99kideezndLmLZcluLh2XpXl4O9cunOTo8UO8vPoSI2fOYlyOmiNgd46j+sQCS9+BoF++atFiJlfY+JxWDm3ayJvvrWbpmn1c0UTekHfND/ErJbeL+iwF2wYrOavDf/syYOAQFk/pQaimntbYFTbtq2DUjDmSuTsjx83jwfl5HNy1i+b8MUwZUEBfjRNT+0TZsfMc/rQcuuV3YfCoMcyevZB507uRpn2I3n0L6D9gEA8uHE3tqf1cvHqePVrjjJo4UfYtoEg+FZTfHC2xKejRhYKePSkcNYnH7h1HVkFXxo4YSCQU0pgepFHr7Vj7STafDDFx8RRvXOzXNQ3H79dGVxW7dxfTvbCQ/rLxwGmT6B++wKajddx8BfK6UDhyEFHR9CUkaMypx2Qv+yb73Azvvct4rhOE2GV2HS3Dn5mgfOCS6rRw4vRlrmn9faU1j9FjutN7wBjGdXe4fr0S1/IzVnsO6Vo7uFqrROsbqBHBhFRjq0zllQAaiNl5KcqUaaNkjx7Mmj2K0Omj7KuNCVKdbOvhNiofnyF3zCQm9O5OQfcupAs1oomKk5KrvJ6puZhfgJc5cjnG0HGD6d2jD1OHd6G5vBQXP13y88lXXnPlOGk60UhKTKef9oNSAxa9ND73cUK02w5+yVrR6jmb6P3pO6aDtOScbvTt25fpty+hT/s5dpypxckZzYyhAeUgi7SkKDXVjaC5Z57GqbzsNJKlx75dZ0kbPo7h6qO+OtQdEbzK5oPlYmRJMrxSffwgJxsCmH2vkJ2kjctzHC8GNLdWV+il87b0Io00MHXrkkdWegq4FZqvFNNj2hRG9O4mn8ohMeAjGGnmwsFjVNip+CNRHPnMtYunuNqWTJ+CPLr16OHpc+uC0aS3VFKr/JeflURCWi698zNISkyga5d8sjQWgI8cU5eQQu9BXclOTiA5M4fuORkkJ+GNwUcac1k8YwBmDJ5xxxImZbkk9R/M6J7ZtGl/L2jZ1DW3ESo5wL7qDOYtHio5utAzJxHja1bkNFuOtzBm1nj6yFZFiwtJLD7ArmofBV270KWgL1M0fty7ZCZ9fTKDYl/pwLzgOBbN107z7uoPWaaN6jOahMc0mXU941lYci07kMvgId2IlF2hVOsYgxhvN28WttVOyZkzHFU+fG3TZYbfuohBdg3Hd+1l87b9bN99nGvNyN4xYpaDFavRuF/MuEVz6OJ3adCEzHJs0REMevocopofibVyuosjYav2b6Q4vZA7p3UFrWmOH7nG0DsX0TM5HVeb5KlZOTiS44TGZ8Nz045jXKhsMwTV7nrP/00/9v+IMlpwtCsADK9oUzXtVjKpibJ/LEYoHFGygGi0hebWGOHaKm0gXaC6yzgeWTIct6GZkOAi7WF1QFTvtteRwiCq+nYlVeNEbgePzi5qF92Y6lrDrvAg3N5ORJMhg1dfH1bwpGMcNKZJZVJKIr5IGe/oZGrV3ouatJXTpF0m+TTVNS2kZGQYYQXfpgkx4h+lXpssMX8yQUlv6PiSUhXkIWpCRku8pEJ7HU3tQczGrIFpi4JPC5Im0fSlJINmGrGYS2JaOm5LI64bJhKOqrTr3aUtamFrlAgpiNy2iILUr4V4lGZN3gKS2ZH+uknISMBtbvQGFawOC4QUZAryAf1s2s8cpTKzJ4MCUHO+FCuYQqrYWw2VNGtinKdNaV8M3FhE+kXUF+2Yf3W4TRuCloUGQ1ff7YCDP+DQWHqYN956j4PnLlKmE56I7ZNtEF5Msodok829BW4kQkxB5beb2KmNxlVa1JWUXqMxHFPCdhg6dTFze9bw2m9/yg9f2EBpk4vfcYUjVh23K0s6FjRpUmu1h7h66SJHKmyKFt3CiKyA5O0A/MQjbPrfFa2o+t4VAaNIB4y+cLVpGImGcZXIje4t2qALpuXTr19fcpPQYBvFUpvmtdLdJWo7BIwgWoQbP/L7berObuTn//yv/PVPX+WSrz+33j6Jbm4brs9H4zm1/Uu87aLdj1tun0w39VDIhYT0Ap1UjmHCuPGMVUL0KTYisjXilyjhWttcoo4jW9jqkyhqxhNfuHRelvpLOvjTuzN8zFgmjh/PmEF9SbYhhnXDDTrBvafs4Tqp9Bsl+LGFTBg7ghxtBIQVA5GoQ8DveH3dEguQp2Tfr1c2drtLqybHZuBHvh7Wd5uUiMjxnIQg1cfW8Iv/+CF/+7PfczIwggcXjCfdDmny6hM9H+av+VrlOrbPj+NE1V+uZAsTsruz4PYF9Go/xVsr13CxyUeC7Up2vMvSb9SNEg7bcTri2xKGzC69GdQ3V3EXkV3UFvQT0+m05hDYsrttW0r6EZT/ZQUR+bN3jHbLx6Dpt/PoA3fzrS/dxqgsv+LNEobkk/9EtHFrcovJXZYjw6oF2cB7RENayES0yKjg5LmL1KYP4/7bC0mgrSPPeFDxH9nd4F3Y9h5Pv7OL04rB6w1h6RWjvq4eJzGTJKO7fCsaTMAM1DEdWkQVPyHlh1jFCZ577l02n7nEeW08RWz7T+tm8onth/YqVrz6Ju8ol525Uk5j2MERfddI40Y00Y4RU6xnZqUQbW2mVXaOqT/F3tMuXHWW5599m43id+ZqDWHHj48mDbCQpkmV68G2E5bdfUEfjZpMP/vCKg6dvciV6w2EtWnk8TL8OkpYupj/OZTHx00kXbPJtqoqGpXzE5XLvHrl5/T0ZNm0DnW8R1+d68lkQtjt0LpZKw9XebKy5KLGihr6z13E4sHpaDDBUv93sPzo4U8jNQCNLU2i4GL61CuCaGxpx7USyVS7G4poTInQ1uYKBuWyMO1RcARXU9ukjZYM/MbGMlQsJYmg6sOhdtojMXwCarm8m9+9uJoDZy9RXNEoWWxB/PnbstSmH4s4P/M0Var1+CMnlin1btpdVVvYlmiqv4w9EpVrLtbU0d7i0HTlCiWtAQryHG3qn6EpDI5F/BIPt93FKRjHg4tHc3yVNlMvt2mSaskP4iDer1i4ImxbYeorr3LmUjm9Z32aL2uTI0E5oOJCCbFMh8tnr1LRnk1BShOHT5yhJWAj94JYmLC/K7Nvu4tHH/4Mf/fdv+KOXhU88/vnOFwLQQEZu+Ppa3TyuEpOC0cDUqL6rq01LB3VRkx6g626ViWQcDCTdMdgcuOyNF6GfN2Zc9u9fPkLn+PhRROUz2JaBoOl+HSzB3Hb7bfzpUcfZknRYMWYjV85zrIszHwh2LOIRx6+l08vGEGbNtzLNLm0TRvxy5KpQy0wcOY9zO1WzevL3udaJEFLm6jys2Aqz3JF84RYQ502W6+QmNOV5NpT7CsJ47ctFLoC0q13zb/pNvFu7hwS491lb3Op2UHnN8idBBC/jW06i0FubWjCl5pOMBEtHtsI5A1k0d2385XPPMJtEwfKF238GiccEQqXHef1NccYvvh+JuS4tEZsggGLP3Wpm4Vncf7Dp3jsK9/iB2/sJqX/bGaNyKWtPYLlJNNnuMansRonCkeTrwVIVIOQT7m2ZMdr/OCf/41/+v0b1HSbzkMzh6nvRdEyvCz1JeqkJObdcwfjnaP8x7/8iB8uP0Sj5l+h1ijh+mrF7AWuZ43k4bsLSTHgso+tZ3t9PS2+ANmKxZiMZ2RRwBKWTA0a/9vqKzl97gKNmaO4/46xJAmnpT1GTHkDQux+9w1eXnuCs+euUa284oquQHRbKmijt0GbD5kEzRwj1o7t8+PT4F+ujeqUtET1haEFqamptGnh7GqeYvQOi4frthPGIab5ZnKvQj5/S192vfJbvvXPz7PN/PWkFIiY+aZ2bdxoFatefYM3d13kbEkFDRrDfEYEtZuxxOQM1w0Rcm3lZTRNrNWGXTrJia50CWP+Wict1cZVbjE05WJQf4GXXniLD09d5tLVKuE6ihNPLYyNoJ3KxlYyM5PjeihmIq6aZIM68xdw0VZKzl7gZImP2XcvZmwO3mV7v/Ef1xWCz0ErOd546Q3WHr7IpdJKjQ+WNEeXq8g1ioQ1FkSwW5s4rZx/ItSFex+cQ08rqvYU5ozvT8Wpk7RqXgsx1eH1YVRzHIlF1uRbeHBEjNd+9ku+86sVnFPcObEYETNGSAa3KST/bae+rFi+UkLO2LksmZBPtKmRdsGFNP9wI21E5XN+x6a8uhk7PZVUBVMsGiUWTCFLarjtYdo1TjiaxDVe2c+zys2HlZtLy+uJOI4nFx2/0EBNY4T0zNS4/TT2axqLY0Wo18LS1tzz0pmLnLoeZP59CxmV4fPsYayhDqS+zU9WeoL6L4bp44S0FPzV53jhBY2jZy9z8VqNesjBFpbx54joN0uXmGpy0gI01TdrPuES88ZnF1fPsGIuZuyh0q4YiComhI55N/UafvUZ5cymFTzzzm7NK65Q0RzF0oEXuSN47O4xnH7veb71vd+yVhv4nq4dNKq14eakp8Rtprp2+botX0EbBG3RduqvXZbtr9Jl4jxun9hFfG66TT+ptMvXY9LBykglYGxdW8u6l99gxf6LnJHftERtHKGFw+1E1C9tGoPctjZc8VHXoUqimoeY+aTrNrHt7Td49YOTnDlfRo362Ix9Qv+j28w/jB3rzmzn96+s5fi5S1ypbMJVn3r98QkMmU81EY6vf5tnV+3n1LkrVDZHvDEbokSMneUHrgDbNQeICTra3Ei9a5OT5MPo2CL7m3pD39WmQrvGfMsVdl2jYjcJM7YZOJ/GKT+NNMR6cv/DMwnteo3vfP9XLNtXJqrqAfGwhWj6LqXPEAYGyth68LrWUdeh20AdNbiKGDse0sKIyM4RARvZXK1XwpJVbHHDEcx7uCM3hSSr13/CERfvpqWSla8t4729F7w81BS18LxWuMaXDB0Dfr2+neSsVKxYjKjiBa1lE9UgtvqNyRddLxdZdlwuT3+O2DmIAAAQAElEQVQ1WlH1pQ6oJk8YQsPxXZy6cp36pgg9eiQSuVpJOJBCos/17GcFMkjwhWhuioqmZ0U9zW1haz5TX1HKhQvlDFr0IN+5fxwJ6LLi/CSAbhdXfuqqulmHb23tAXIz9SHc9mgyM+++l89/+gH+6jOz6as5iuEgdAHE77jMpgf1beYNsmNEY5Kxa2tEdvE52g+qpcVNRqlEMkdpUwL1a4yoqwuRkJokX42p3iU1PY32Zk1oQmFM30gweY/oal4Ylv3CoTbBxfDpQC3JCnG1roZmN4GUBMurd229+y1qmpox/5kzI4PMCbZDzemd/PaltRxVniqWT9sBPy2a4zWTREZa1MM346Krvoi1t9AY8pGcKtnVdzHxSE2N0VTbKmF0G2PpUXd2N797aQ0m9xVXNGEpTlT9//aWtUFxSluLcnGU1pqrnNT4Qb9J3DezP5nSzR9poM6wilZSUhMhKy8Nq/EyLzz3NhtOX+TM1WrCroPtcYroYDhqVASjtz+BNAdPH1cTnFT5Rk19xIP0fsKtVGkumJWe5MHEZGtjI8s0umEvx0Zlb8gmMyFMZbXBjXL5Si2JWXle/IQ6YsegqKOIRcM0K6m7sVrWv76cZdsvenGh4R8v13S4hwf/yR8xdsPtcVm0Ps5I8itft3Hl6FqefmsHp86XUKY4cjxCEFE+lPugiZvUtUlJ88lNYsTcAKlpaA+nBXNZomuebRq7QxrTrmgteao4yoy7btchqlqklqV8obc/utvlv1FL1m1vprbVT4YGPZODYm1h5RBLOSJMS0uIUFudDsI0lrflcee98ygIuDS0RYlpPmJ8z8yhkE8YUSJRyaj6iPKUaWv3ckWctWtyoGweanU9f3cFZ2xsRKioaQbFRbrwYoKJaG8sIyHK+W2r+cNbuzijOVmZDB3Q/Lu+op5oYjqplnjFXAwPS+M4TU2aaySSmmjqY7gJKaQFQlojo72liOLPJRoXhQ6n6vwiImOn9Snkyw/czpe+9ih3jszEtm0sy5LNXWwnyoUdGyjNmcZX5qby6u/fpLTFwpEmHk1XJDV/qblazAUdwI6847P87ZJBuMqhO7dsY+3mHazdepjLTYBly1+a2f7uBhoGzuXBGT1EJaID9EQs5ZWwbOC6USIaS5ykBPFAKD6qzu1j2QGXex9eRG8/VO5ZwXPrD7F/9Ts8/eJrHLhay571K9h44CxnT+5n1QdbWb1lH2fK474isvxvu+z/EYWUxP1K8I7Phy8xFSfSREtYHS4HCfpV5/NpMZykBZOfbiMmsWD2dO64dY5Ot3vgk9Udv5+A8C3LwS9Yn95Rl7tYBBMSsCwLq4NHvA38wvGpBPxqs/QdCODzVuQOaUoGddr8cGwb2/FhrtaL+9hwBi3wpjJ/0Ujygg7GydOTfTTpZN04s20HRdfGchLJ0OTaijTTjoVpi7Q0EnYSyAziXa75DWaQosRarSxtYBI6FoSZSqrtzc1Ylo1tW7SJvqWAtCwfjvTz+31YlkWnrj69Wz4Hn+1gBqSkjAAhTRQjti18aG0IYaekkGR4eoz1omBuclLI1qK/4ko1AbVbqi6+1kBaTney9XHhSCmp3QcyINtRgIOlJOR3fDhesfDp6VOdz7Fw9G5rIyrFJJUD27nkDObe26cwa1QvTRZMu+kR8AUSSJSePqOHwQ0kYtedY9OBMoYvmsktCyfRQ4skR5sVsaQCbn3k6/z7Nx4k84qCbV8pvkQJpsGUjstSP0elU0pANkktYMHiKdy3ZCafun0C/dL8Skp4tuITl8/nePJLhI+3WKIYAyc5meyMbGJXT3CkPExGusXZ7a/xgx/+gs3VQXrm5xHRRvveklZS1VZ95gSnqjQNyCkgPzmmvS6LrCG38sNf/DOfmtgbv4RMyUzBZ+QV/czBt/AfP/9n7i9Sm5JsSpbapEfMtUjvXcRjn7+Lr33xU3zx7tF00aZbdk4e1tUj7ClujvM7e5KTFY0kq6+6JIPmu9LzI1Usy1Gyayel91geePROvvaVB/nKPZPpnYoGfZ98x8FnYsIPCgMc+ZnB9iV0ZeFDd/EXX7yHr39xEWPzfCQnZZGR1sLZ46do8VmktF1m2dM/5McvbaJZfekI0ZKvObaNLftZliW/U+rWIJs/9nb+6V/+lc9PzqeNBC3CkknO7UXPpBqOHT9NjZJ8itXG2dPHKGvJp2/3FCzbAi088kcs5LO3jaax7Cq1OnwIBmxkPsnq6OnohDuH3Jww50+eolaT5zSrng+W/pTv//p9GoPdKMh3OXvkMOWtlgaqKKdPn6KywaZ7j+6IAo78lj97+QnIRywtWg1IQve+9E1v5/yJ05THEkiVLI4jn7Ms+ZEjWzoeTbDwLn9Q+NBt6GQWzZzCLYvnccv43jiOje04srkM7wGawdrSWy0frj1I6oS7WDhzMqN7pmO5fjIy0og21dKkTRTbFg9boLrN4I/jJ122qtm/mf2N3Xhk/hTmTexDst/ukEWAnbd4+hybhJQAXDvO2hPN3HrXVOYumkCPNAtLfWcpl1qWjwS92yrVNW0kZ2SQaFmS2Y/fh6ddmfjtacjns4bfpH7KYRaQSHIwRoMWyoaWLdl82ixKT9TAvn075xOGcvucKSyeUECSfNwnjE/eluxp+NpWK431YVJ1up6sSXlbUxtevXJVY30LCSnpyOC6fSqOJ5OhZRnn0EuSDj5IyGDGoqksmDODO5dMY2RXBYnizidnD6hfBQbSC+kMXSkalEzx6fM0YWHsZEsWiwhHTheT0Hc0A42KPp/61E+gc8zwaNnCgMyMZFrraglZNrbt4Bh4wC8Yn0qyDRe2bOZS8kjunjuF20Z2xbFtHMHgONLDJ77mA48eusROPtBGq/o+KeAjogVEUrLiSnokpabgRGq4XgdJSRaJKo5iprGmDjstDUv8rMZGxY3FlLuW8MDimSxZMpsH5k8kdHYfSlsEg4i1+Kqf/NJJ6xUGzriP+d2beH/dXpoTEnGUk3RLGt0W0scmZqczfMpiPnP/QuaN7ElATZH2K1xoHcDD983h9sWzuPfO+dw7syeXTpykrBlSDC+fH78Mo/knSu9oL4ThQweTHC7D/IF6QtAveRzZIiDZLBKSLfzhNs4dPEpF7hBG5Sdy+fQJIgkWiZrIBxJtksL1nL14hZxBI8jxQyzmxu1ngdPBz1VdXZ1Lt4J+dO2SqIW59AgEcGzQmoNwMJH+w3qRXXaCPaXN2KLtk7P7rbAWzDB42m2MDpzk1dXHcJNsHTSCT53j8wdQqNFqZbHkvjtJubKTjcerCST4SZS7Xb1QTXqfsdp8m8ddC2dy7/0PUNQjxuED5/ElgW0Tp2PktNGCNpHZ99xPz+oDrDtUipWYgMLbg3Ecn2eTYNAiWXaJtZWx93QTQwb1I1e0sIM4omH0iTkBBk/oTezMOU5WasHld9m26i0qeyziM7O7q7cs0hPr2bf/POYwxcihyhu3ZVnEtGHRd+qD/OPffY9//v4/8TcaDwan4U3wbSeb2ffd7Y0R3/jybUzpFVAYuerTqA57vsBP/unvuWNYCi1uCnnadPNLCcc4kuGgd6x27C5DePgv/5qffHMONdvfZ/OFVtIyE8kfNJ4Fs6dxxy2zmDuxPykGJ96j+JOT8Wun3vz3ZG3RCQZ82LJLcqKPBCeRriOLmD9rKktumcv8wj4abeQPgCt7QCmrNl2i/5J5zJ05nUF5ybKtrVZzu+aHJOXacEMlrWEL2w4QTxPJZGk+2NzUimXb6Ka5sZmEtAwsnyUfc/DLXyzL8fop6EdXAmMWPsDPf/wtbu9yndffOUSz5jnGBMF0G6v8KO8fbGTuvdOYM6+Qnuk+ZG5ETLR8Kg6W6PklgM9xlMq0k9JarU1dS/z9+ExHg/j78EmgxFRoPr6LbdfTNAZMYu6M4eQkqt0mfnnq+UkNOtRrIevpkRjAL9qmpCcH8aX25Ja5U7llwUyWzJtE/4w4qmXFn1IOM99LTnFovryPbRcTuPO2acxfIF4BB4krQAvLy8F+khMcErsNYfGcqdy2cBa3zB5JvoGLVbC7Mpv7Z+azafl6KrGFg1R38Pn9KA2BncHCz36JX/3zF+lTvYulOys11juykY+gZWGlBrCtNIbPm+75yu23zqWodxoB28WnWPIpx1jyC5/Ph2Vb5GUlEdJBbq1lYzsOtmyGLkv8fPKN9MSoFsLbOZ84lCVzJrN4fE9kPnyC+ehOkv20MFffW8K3fQEChpbjJy3Rwc7syW3K64vmy34Lx9MrxWBbcfRAOmn+du+PVQzvzvGn/NAOdlRn8cjcScyfPpSsBDxbWEKzbB+Jho8irkaboZmaE/u1AMb2kySdLEt5OGbjBBKwLIuAz8bnc3DAe3rvooMs/MG6w2RPuZOFmocML0gn4FeD6zB4xm38x4+/y2eGh1j51jbNayxsW4MLkJKSSKSxiWbLxha/gPrOdhyQ7S07jVELp7Fw9jRuv2UORX3TEUWQHHReevcHbOHaRGvrIT2fgOavq09EuV3zjjnzRpGvtZPxG7/6ycjru6nfPB935bgCSE61sELnWbf1KmPumMf82ZPomxHEsh1xc6SvT8W8G919OOrTVLUc+3Ar1zOLuHXWJGaPLiDRAlv1YPHR5WKZz1gZ6zecpGDWEhbOmsGQLime7Moe3hrC12Fnv98WffFLTCJBg1itNnht28aMz458zu+AJb8yOukTX0YKTqiVUMzy6EVD9UTcRAI+V2cAc/mHf/sefzc7jTXL1nIxjGRRBFk2mDVOQnemjunCgQ3bOXHBYcSIFEx4mWY6LwNn+6SbhSWGjuMgc2J1+INfT8tyMPIYG6PL1fzLsqCx9BAbj0VZePd05i8aQ1flUYFiSQkTO8aDBU5eWoDa6nrC0tNRm20LWQ0Gxu9zsCwLyyeb6F3s9e7Hr/dA0Kc2yB47iTEJl/nd27sIZw2ndwDCyWkEIq20ebnWxm2v15opQHKyI8quiqWi22cRC6QyavYS7rx9IfPG9sTfMSc3ihqd/MGg+FgEAn4sGej40WM0dRnJGKVNbEe2dnA1NzLGy+jThwHdcohfHTz0Ee8zx5BECkh+Hz7palkWhodj2/iTMgm6jVTXR7FFN0GyQZBUrTNDyqu2YGzboqmhkUCSEmhANGQHg2/ReVn4/AHh24Sb6mmxUuiakaeYbqNZhyqGhqWN+2bZJVPjXYLoOT4fCg0gxJHNWynPGac8NYWFw/I1P7RITk3GrwPEhmbHoxuUHXyWTSCYpHlXRPt2MQxd22qjrtkiNTNBtMCVbii/HNe89GrmGO6YM4VbhueLl2QUhJHbJ94+vX/8duL2kQ1ITCRg2/QdP5dFs6dx2y3zmDkkj7SeI5g9wmLDy2t4a+VRBi74FHcMTabm4EZ2VGbxmQVTmTtlIGlBW7LFqdt66BwKkpNICYe05kFttqYLbTRFAuRoHBZI/PYlkOJH+oQ8GDvoxyfn8xtDWb64fI4B7c5tYJ2mTQAAEABJREFU8/twbt3bkmMdzf0X8JXFA0wDxl88Hc2X64LwEv0WVuUJVu+tYtqnpjFn9kT6Zvg19oiY48ODV5/yicug2/5AXBa3haoWm65ZYXat3kNS4Z1aA05mTK9MfJ7NhWyraF6KEyQlOUaTDjct2dG2whqjISUzSQBg6JqXYLKf5PRuyrlTWLRoDrfPnUi/THT5CPr9kkuvn7j9pt4RI81vk30hdH4fly8oeNUHNL8Myv5ZBcOV86awePFclkwbRkbAkr5+An4flmWJtuMVR/TNXNroafSwLAu/YHwd9rCMr/iEF7SwBGtiKuD3iSfkaW7XXldPlXBs+Y3PAERr2PTBfpLHLWD+rClM0uZEVBOh9PREYq3NtGAL15IcfhzbhrRkErQ2bGgz9bbGhCbq5ReZCjWPpnj5xPdP3QG/IxoWEdMY7MawnqlUlJzlRGlUPGxay06x6SzMmjORWz97JyNaD/OHtedwHR9+gyN8y0pl0pJblYcWMHtUN4wtApmD+OLffpsf/cM3+bfvfhptcwi6mX0btlHVfQqfXziUVNVYyH9752DV1tAiG1hWOxXX20jpnkuK2hsuHGLD4WZueeRORucGVAOBLiO4ZWo/gk6AtPQACcrlCXaMpPSe3PnpL/Gzf/krfva9z7F4eJYHb9uW9/zf9GP/TyjTWltFRXUt165WEc4YwPiuzby3YhPnL1+jpPQ65dfLqWoPMLUwj93vvsbWU5c4d6mYsoZ2wm1NVFTWUF3XolOOFqorq6msqiVMQBsdDZw8doZr1c0011cLrlZ8GghrlVZdWUVFRSXXq1toV7Kr1HdVVSW1zWEKJ04k8dJmXtx6RjKUUlJejzmRCbaVs+PgZYoPnqe0qoIr11sZMnYkwcs7eGlHMZeuFHO1vJrSknKs/kMZm9LEpj2XKL1axvo95+k2Zhw9Eo1FY3iJJdCF+UXd2Pf+u+w4U8yFyyVc1Wb0kPFj6VZ/gXWnr1N65RQbTjYxZupIYm11XK+qprJag3WknRq9V0jXulDU2/yolPzXatvpNW4sObVn2GXwS4/JXmFGFw3yAsHVRMhz02CAQLQds0Hu+qKcO7yPTTu38s6mk1xXkBw9dohjzVnMmzWEIFGCyRBWUqirraa2ro7m1ggNddVU1dZS3xKhpble7/quayMxI53WayfZdfSKTmpLqFD/1rcqcPxQcVEbp2UNNNXXUS1aVdKhxUom1d/M8X0XOXH4NJevV+lkroLDezfx/pZi2nwp5HXrQrIm+jGdWpqTzahGKlNiboTWlii9ho4iq243T799lAPHizlyppomcyInuIiKgTUl/h6mQToY/rWN7cRMl9woFrYmda3BTCZMmsaQlBKe++Hf8Pmvf4cfv7UPX/4wBg/syuiJsxmdU8krP/0eX/rW3/LtX79NvXx3gQaGrPYm6poaaZPl8vv0YsHk4VjVB3nzzYM02DatDXVanAfp0te0jcCpUdvy/dSrLdbWyMW9r/Glr36fR772XT79F//Ak3ujzJg5n5E5Vbz6i+/H+f1yOTVp/Zk/bzKZOlUPyaFi2tnxbCP5Y+rbNtmqZM/bfPebovUXf8sDn/8+z22/RkiJqrX8LK/85rs8/LVv8NlvfZ9nNp7DcaLUlB3ml9/9Pg9/5Xt85st/zXef2kFK/+FMnziG2j0v8dff/ju+8Pc/Y9PlKEMnFKI9XqKWRVgHLPVasGg+jj5B/dJUV0urnUpBv65MmTWTjEubeGH1fkJpo7VwmUDL4ff4h+/9DV/69vd4/sML9JhxK9P7pNDe2khDfQP1bX5Gz72fOwvzaZRPNoZiWKLb3FRHbWMLyTk9mDZrGvbZlfzT3/0tn//uP/Pm/hr6jB3LoC69mTl3DqnF6/mXf/guX/jmd3hizTGyRixi0agsTb5AIcChpY/z9Z++TxW63FiHL0RpqqtUrNVSfu0KJVfKuFR6lTN7D7J5/1naNEGsUr64XttAOBKmTjFQrjisl3xYYOuwAXIYO74PJ5WvNpy4xLlLJVzVCXi4tYGKqhrlnhrMXzeIa8ftI1MbwRcO7uBc6UXOlZQp9itgwEgmZpbzwuvbOHexlEvF12mQkQPJibSWF3OguIL2pDTM/7Rh87lSjp8spayihuqmdo+uplV6ul5+KBff6xUNRPyZpIUr2br3MqWHT1F8vY6y2mZvsI201FF69TrFJ/awvSygfh9Ce1M5ZdWSubqJSAyS09PF7xIbxe/UyRLKK8q52gSjC4dSu3e1NreLKb56hSuVlVyrbCSQmUbj5RMcvniVfSckn2jVtkYl10e3ZUFIG+3Xy8s5v3kbVxP7M25YLsPHjITSfey7cp3Sc7vYU57AjIl9aW+olh2rqaySPh1kDA1DNbXPEEamXOG3z2/lzPnLXCgu92zd2lBLhem3qnpPD7E03SVsh2n33snQ1qO89sEpSq+Vc/XadU5uWcPm8lw+8+BkglrAVGmcMfjXKpoI6XClWoeUlRq3rja00rOwkP6hE/z+3YOcv1TKpZIKmrVZVllVLTmrKG8Kk5KRRr3scER22HH2Kga3rqWNBvlPhWhV1LbiuY4kUgCREEQTlkO8t/Y4J89eJJQ9kDE90nFbIdhrNFMHJ7Ft+VI2Hr7EpfMXtYBexcn6PMYMDBJubeXkmSOcLg7Jz9u5XhOiqqKdQEo6/objvLf5LHUNLdQqN1dUllNV36aYcAk5qcy+XbYI1FOlnO18bAYQo6Gmipq6Gq5ePselshCN5i9Pws1c2vUhF+0UwjURahsiVNaFsDO6457byqaDF6lpa6W6okLjdA1l8o2LpWVcLS5lxfr9+LqOp7BXjCr5S1VdHVUVVz0/N3F36uQh9p8oJuRPYP6tC8io2MZbWy7Lxyu4fr0M8z+/ORUdwv3zhqM1fUf8ynoa4+s0HlYrB1WKbmNbDJ8mk5ZyX7StBTPe10n3K4rty5dLOa9N7C3bdlKihWREObhc84hajU+VlU00JXTjrlunUbfjBV7dfp1mbexXiHaF6NZ7fyniktRzDPcunAC11wkpj7uVJWw7c57WVkcbOVFqGsJcr46Qn+7nrGJkc3Ej0dYWjM6GTp3G0Fg0hpM/mLsWTyO5pYJGbTC57a2StVL9VElpcZnsco3iixdYsXwZFQUzuHNaP6I1zZTX1Aimiiuy66Vi+d/5YjbvPsB1jW/XDr/NC1vqmTBhCOGyMs4VX+PCwW3sOneVdtnjY11syXaREPUNddhZA5g8YSjjRw+kR6YPM3aGQ23KjWf4/T9+Lz5GfPHbfPs3G6nDpVm2bg9k0ndoHyZPGo97+F1e2nyBmF/+6s2pXcyaC+r4cNUGth0roy05k64FuSQm5TB1XFcOvb+MjSZfqk+ua27moksyRfWwu/dhQkGEFW9+yDnNDS9fqaKirJzySAKTxnfn0DtvstmbG16hvLZFGLpFwHINdhIZie0c23aKS1dPcvZKORWaR7ULxJLsAiNvwHhGJ5Xy2vJdmHx9SfZuDEHR5JHy40McNDno7A52VSQzZWI/QvKRiuo6asxfEEeaqVLerWtpovjcMd55b6fmiDFyunahuzbL/FowO6Fqjhy4QnUoQLJby85dlyg9fpYL16upqWkipDlUhWhWaS4bjbRQpTHlWnkNVuZIpvcK8c4b6zl9sUT57Cq1OpQz9i5XLi2vaMFKziLQcJkNZ0p0UHyBK+V1VNRLeOkn9fTrY8LEgVzft0n5uYTikqtcr6inVHOt1DFjyKvcw69XH+PMhRIul1bRbgwiLO/WuNhcLVtr/Cgrb4bENHwtJWw9Wsx5zYWvKKdWKd4NrGXF9LAYO3EITYdXafP4Amc1bhVrzhduqeODt7dQkTOYu7XQyr60niffOYH56/Vq0ShXDqyXv1bvf59nt5RQEUmie9ccspMTsIJB3MpSdp++Tn1yb6b2jbD8Dys5dv6yYvcaDaEITQ0NXn6vbYoSaW6g2pO3hsDYCQyKnuOltw5ixtCLGltrNVY311RRrvx8raJZY1Q6jZdOYHLzvhMlXBNujTdGWRojjE4JTBzfl/I961l/qoTi0hLKFP+lVxvIGz2KjOvb+bX56/oLxfKvGsJxR8c1dvTlMn9yLw6ve5ctp4u5oLnAdeV6X0YOTs0lbxw9ffyC1g51VMrhLMumvbmO8vJKLu3ezRm6MUWbOomK1bbqqxw6dV1zAJucVJsTe3ZzvricS/LNyso6altaqa2vo6KiWu9h9UWQjFSXs/t3c+7KOc4qbio1N2g8d5xlq7coh7eS1aUb3bKSEVsQj5iwuo8aRbe2s7y88hgXSq5rTKyj7MpVmlL7MaVHK0t/t4YTsv2Fi9doaIsI4+bbgmg7pk/LrhWzfmcFfSaMoGd+EFoq2Xb4MlcOX6BY8VKlOUWd8ka5xtba5ojXbxXqkwrNR/AnEGu4xrHDV6ltSyAt0MSB7We4dFkxXFah9UadNtfqlR+rKVcst4VC6vNqKuRLFcqnKdkp1Fw4yMlLZRw5XUJZTT2hthCrn/hnvvPCAbTfB/Jt00XYQTKSw5zae4jzV06plItODe2kkJ8c4tCWHaqrpPRKpdapV6j3d2HqkGQ2rtjA8UtXZf8KL/6va/3WVF2t3FLNletNxLqOZlxBC7slt1kHbt12nKzhExmYfpmlr27m4rU6UnPytL7JIiizdVrRsVxcbcaMmzgCWwebh/396acBLiZhbQ8oDpyUk0nk2inWHy7lWvFVyrT+rFYuadFcvEqHj9VebmpRX1RrvlGPpo7Cdr2U4CSlEtTh7w7NA0sOnVV/VFJd26QxvkG5p5oq+aLJnEOnjSe1eDevbDyDN7eRDdrCYeoUP9er6mjR+qNVPleufFWnnNOicbNC7xVVjUQkrxsoYM6YbEo09+s+ppfm8JAgu0zsE2XvjhOY+eaOLYdJGjiWIfnSS31i6yEnoklxWCV7Fp87r5wXIqQJqLdxqnVAfUUlhs+10mJKr5RxueQKhzat4K0jDg89Mp8uotOo+aTZWygvr/LWqY45QLA84rJD5x2lUbmnQrwqNOaE2xqoND4o34zIj2tr6rxYbI11Z/6YdLa+/SYHlCcvaJ5X2RBilOZ+iVWH2XXxusbog+y45DJl0kAiLTVcVz6vqJTdZQewsKyw1xfXrl1j7e5LdB0+hv7dejO5fzpH9+zH2OLAjl20dBnOyF5Qca1KstRQ3WJi2cFszlWeP8aJS9fYfeE6VWXXaPAPZHyPNta/toGz8sVzWhNUlpdR2pzOxKIeXNt3gMua01/atZeLVh+mj8jko8sWzRSqLxznuOalO89fo7Kmzvvr6yrNbU0eMbw98T0kl1BLrWf3Ss2/WmO9mTgyk00vvczuc5c8/6jUerylqphTF+vkb600aHPV0Wq4RnsrienZ2HWX2Xi2hPNap1+Tfaq9scNCxiEaaof8gRT1sti59ThXtHeybfNhfINGMzbbeL581wijdeXkcT05uWkD2zW+lJZeoxb3GzcAABAASURBVLyilvK6Rlrqa2WzasnYoDxcweGjV2l3YjTWNeP4XaprGohGw9SYMU0xUql8SFKQWH0p+0+X0RBOJM1pYPeWS5ScOsO5smpqqupFt1o0a5UXpJeRgc7LwucgH7qu/ivj8Jat1GcOYcLAbiT5I1w4vIuzpZc5o/lEuWyqabXSrIUl32rVpua4on7Ua7/ltPqo9NBeTrR2YdqYPI94J5u80RPIqdzHU6sOa4wu5vLVSlo1X2ysqcaMYcZvTax5SPqJat5YIX+uvCZ92rKZMDaHo++9z37Z6orGi3LFRUmdy4AJo2na8x6v7T7PWc0tSiobiUmuWtG9Lp3btIfQoI3jCtmpVv2amBygsfgsR69Wq1+bvTxRrrYW5dVGs45R7r16vVHr4gjNomFi79rVJpxR4xnuu8TLy/ZpDC7xcmlzJEBOqsP5Q/s5L/scLq6k/Hol9B3FIPsSb2m/5IJsdlHzvCrFS2N0EFOHJ3B440HZ+ToH1h2kufsYJmZH1ffV3jhQJR+T+h+727U3UaaxprKinAvKE8XylUsXTrJp6x4utbo0VJ3npVc/JHnCTPokusQCA/jUkkHsfuH3LN1XQn1rmGYz35c+F85c9vJQRLZXJ3p8XA3yrnJNRMnZ1eHDppee4/ltpUSbSlm1aj1vvLuVU/UR+owsoq9dzO6j1yg+e4hD9eksmDwYrmzj3379HtdwObN1I2+8vZ4PD1wi2GcYt9x+B5994BY+dcci+qX56Fe0gKL+aeojV+MD8eJJ8X/057+NuYn4/zbirmXjqPNqKhrpMXwwqQ3XaSKdT33p80z0X2LFBzs5096F8cPSuF4RYuDtn+crU9PZvWELG3Ycp1STuJbWIANGDyJZk/e2UB1pPQczKL1ZG3xZLLxjBinXTrDnVBnX5QADRvTHX19Dc6iJJn8eY3smeBsFTZqs1ge7M16bXxc0aUgaMovvfn4urcd3smLdVjYdvESsoIhHbu3PhU1b2NvUlXnTh+AqCTiDFvBXDxVRs+8D1m0/TdqQ0fTzX6PG14PHHltERsVRtu7YQ2ufmXztvnEEZE1XEwsbk1osRt/5CF+elsLudRtZo8A6dqEap2cRX31gLDXaFN4qml1nfYovT82hvaaFguGDNLA2Uh9qJJpawIheCVrsh6luthg+vBftVXX4e0zhL+4bw/VD+8T7Iv3m38vDk7oazmDbeupO7k63xCpOXo3Ro3AaY7PaqXCzufszC8mpu8yVaB5LFo4n22cc3SYlGaJNTWT0HEbP5BYt/tto1Qb68N7Z1GjQvlrv0mNIX6huptvoBczpF2H75v005o9jUl9NBCvCDJl+G0N8V9hyoIQrZZVk9R5JLhWURPtx961TaD/9AVtKYVzRWPVnHcna6Gq6up9VW/fh9J3JkqLuGid85GT7ycp04iUroAWKQ0LvCXz2/iUkXtvPui07OXSpAhJ95GZ1wHXCZ/vISLBoDHRnWN9s6pQc22UTW/1hekSWwbItIq3QZcwivvbVL7Nk0gj69RnMnFsf5btf+zT9ZcLUITP55te+xn3mv2HWox/jZ97JN//iGyxSY6Odw4Qps5kxoiut1S7Zw6Zyzy2LGZTRzLVwLkVTZ2sS0E1tMbKGTtWGwy0MUduV9mwKJ89i7sSxDOjRm369+jGgTx8y/THSB0/h61/7Bg/MHE1f8Rs38w6+8RdfZ9GgFGIa/bLSHNJTTPGRkWJr8yqNYZPmMa9oHEN696Z3T9Hq25ts2SSz91jmz5rFxBFDGDJgGKMGDyJXhwZdB45j3tzpjOzbhz69+jCgd38KspMJOZks/NRX+canb2H0gN4MGjmFz3zxr3lsbh+0L4f2vek2dApzp0ygWxJoPoqtjYgxMxcydUCOFtYxkuWTD96/kB6BNm1aWUy8/St897G7KRranz7aZL31ga/zt4/MJccHyfnDmT9/Mt0SXNqCmcy/6zM8sGg2Y3ulE0vIYMyUxcwakktbLMDERV/grx+7hwlD+9J/yDju/uy3+dadIyWTzYg5j/DdrzzMzJED6NNvmOg8xne+eC/9UyFRA43p64yeAxg9qBtB86HiYumfEGUXq+g2aiw9I+fZtH2P4mi/BoXLJBYMIrW1hkRtcvZOaadFm1mNiV0ZMzCDRm0yyXmQO8mbLEYs+hRfXdiDwx9uZv3WQxRXtXoDmU80+iW1Uq4FruU44ueKcxqL77uFXq3HWLvnGgMnz2B4YiXVbje+9LVPM9q6wIq1m1m//QQlmtDlDJnG7ePT2PfhERqGzeWhiWlsX7eHK9mDWTA+j4ayRrAswFaOjVJztZVeo4eQWF1Ca+5wPnv7KC5v38K+60nMnz2MxspKgv0mc7uOcE/uOsDG/dcpuvczLBnsp+6ay6Dxg0hsKNOkB7InzufBCensXLebyxmDWaTcUn1VOXzW3XxlcV+ObtjIxj0V9Bs7gkBdLQWTF3LrgBDrNu2nqed4Zg5IVM7QRBMHS5aSkNhacLVUl7H/4CG2XEnl/q/cz2AL0sbdwpcX9uTMjn1s3lfFhE89ym39bBrrowwePgBfbaVyPViWKFk2tsYT7Qby6FcfZgwXWKmT6C3HiqnXYVSNFrm9hgwjOSQc7fQ6tnBQkfmdnOF85zsP0rv5HJt3H2bn7n0cqErX4cgXmV6QANEQ10LJjByST7km/w1NLYrlFMYM70pNSQ1uzkj+8i/uIK/ysPppK5v2XeB6Qw01kWxGDUrDTMJ6zlnCbb3aWLvxACHF2sxBqdRU1VHSmsjIUQW0lNXQ7lqeLYwa6Mru2p3s1uO8f7KVmYtvZajyV1gzzZCbzuIHv8rdIxM4uH09K9dvocTtzW13zKOL8lNIvEvL63BSEmjQ2OkGgyREI5TWxxg4toi0houcL66gtDpGVlKIS9dqiDoWsbBLUtdxPPrwXcpXQYQCsq0pViysBW4tPUdNILu9jEvl7QSCftq16Xq2yk9OsIGyBpfEoE902jQu+Rk/eQSR8gucuVzFlXqbkWOH0HBuH9v27GHLtu1UZhfx5c8rJgNhLl+oIHvAGPr6r7BZi7G9e/ey71Qxyb0HkNrm4u8zja9/7g6Srh9l54HD7FP7uWhfHvvSwwzLsnWQ7EpMC8uxiWlz91pdTPxGEi47x+XqMFIPW7Zpqyqnoj2XohG5XD4mOfYdYMeO3Zxpy6F/1xTqSq8QShvMuF4WZ7R50dgG2SPncd/c0TRcPM4pLTxrZP/U1muU1ITw+yw0/6Z/0Z189pZxpPjgijaZ2lw/sWgzDa0WwQQ/oZoK2rMGMmFwFpdPXsFslFS3+Em1a7hY0YovYKN9evqOW8Sjd84gN2jRrMV3eTibwuHduHxwL9v37mbjjv1Ees/nW4/dSa9EqKu4znUKGD8oh+LDsu2BA2zbuoOrpNBNeb250WbsjCKSak6xXX69f98e1u4vI7eggOQgmH/dGnNZFmav1p/ZiynTFzCmm59a2bC5OUpY8RKJ+ujSbwxz585gVL++9OnZh/59+tE9J4PElC4UzV1MYfcEGppcCobN4b67Z5AVbSSgMcE2seaCbfiQSd8cH5eO7WW9DkIGzFzAjF5J9Jh9P1+fk8f+D7ewYfthzlW3YWEuCy+urRzu+/xnmJ52jXfW79JCLYdpI7I4V9bG4Fse4qtzMtn3wWbWbzvChco2Tc7F0FBww0A3PvWpGSRc2Mrm4y2MnTmZrqHrNArEkmwCxkrtxRe+8SjD2s/y7potrN19iqv1YXIKb+Pz87pwbPt+Nh+oZNL9j3B7P4v6uiDDxg4iobFOBw31ZPQeSM/EdvwpGfhar7JZfXDC7cf9dxYSsHqwZMl4mvbv4JTbh4fvHU/1ts3svp7I7AWjSLp2VXaDgcMHkqgNkHBbPUk9BmqsitEYC3LP17/MvMxK3nt/E2s09y2+XqMDDZuhY/sT02GXb8h0HpnZld1rd3HeLWDBlJ5UVtRJb7CUW6Um3afdw1/e0oMjH25Sfr7KgDGjSG6/TntGId//2kICl/fw/sZd7DtbScgzvIuZo9tyivLqCANHite1MpxeRXx6fh+Obt7BkdaeLJral1BdHS4Wjm3rCeljlvCdBwop27+FNZv3cEKL0lDVZa6EgvTqmgmkMW1mIQnVlzWPr6M6ksVIxUW11gL+vJ74Sg6wZfNWWvrM4cHJqdBrNPdO7c6xbfu4VJ/JXV98hIXdalmrvLdx33kqW9toDWUwbGRXYrUhWnQo02PQMPLsWtoDQ/ibb91Dfs1R5ebNbBZ8dUM71+tiDB/Vm8bSGnpMWsit/dtYpzlrc8/xzByQSG1Nu+R0sCzPGPSa/QBfndedo+s38sHuSvoWjiS9uYT2rpP5my8twLmwh9Wy394LlYRdS7jmds0Pw277DH8xO4v96z+UjQ9w4HyN/Gomn9a8fof67LLTi/mTu3PlSgOu46O1toxDh4+w/QLc+ej9jMuwcLoVcff0PE5u3U9pE0y/8y6KkspYqfGkKmeQ8kou1Vpot+T0Y1yPBG2iNwOZLLlvMV0bDrFubwXDpk6jv1NJfVoumeEqtmzdxYHaPO68dwZ5gnaVsywXrJwxfOuLi0ks2cNqjdvtXYczrkuIstZ0HvjyI8zOqeD99VvZtP+CbB/DXK7wvKdlY0WauHz2OHt26oBx7GK+ojHcyR3No7cNpfjDbexrzmLB/KHEzpVR7WRqjOxCS20bbXVhCoYOJC3UAOmDuXvBYC7v3cH5xh58+oEZWiNsZfNJW74zkdzYVa5erRf8EHKtGmrkg9VOLoWD0ikpb2HkojuY272eFVuOYPcpYmYfixKNQcP6F9BUXkJNFJCscbnzuONTi8go38sHB+oYPX0K/QIVNLipLPnULQzkNCu1IRbtPpJJBVEuNfqZ+9Cj3NG7iVVrdyiOg0wa15sabXReqwhrLTaIaOU12u18PvPYXfRoOMVWzWFq8qbylQfHk+pLIz+pge0ad96/YHPPffPo7gMvD+mBiSMJZvUaxtQBQ5hU2E21Flgq6Op4BIbM5JH5vTi9cTM7z8O4ySPJbCqjsj6Z4WMHKDfV0tpWR0avQfRLb6NROlvKd65IJPUYy0O3DuPijq3sqsxl4cxhIL8rbu/CBMVRw5U6+TEk95nJ331lDm2ntvPe2m3sPH6NOs1/7JzeDO0aoKI5THUokWGj+uBTzqmsbmfgiH5YVZW0yDWMqP2mL+YLmt+OShdjo5c/i3s+ey+D2s+xRXa5llbIVx6dTaaAXTNKeHqGuHq5mUETxpAXLuFqXRs+nw0IKNrCpXMNmA207PrjbN62l42btrOvIpXHvvs1lgzNAG0yXtWB2eCisaRVFXNJB02WwRV/Oi7LMvTClNZEGKbcG62uwOSO7oMG08VpJRRuIJLRk+E9U6hthUmf/QqPDLf58P2NrN56gKOX60gdOouv3D6E0r37pEspg2/9NA+MTKa1poU+Q4eQ0lyNpl0gu9tOhGvnzrBn9z6aZNeX7/gSAAAQAElEQVRv3jMcLIdZD36KqemVbJYtTrT35AtfuJMuVisVwV4UDkjT4UaL8qqPEQtvY35ePe9vPog1fAIz+tjKBYk8+PWHGZ9SzNvrd3Jca8zJo3K5fq2NYbc+yL3q1i07DrDtso/7HrufMZnSWTawRFHMGbLgNhZ0bVSePog7ZDwzB6RwraycquTujOubTFlFEzHJbsBtO0a9fDxr4DC6Kz6qW21mf/oRHhwdYOu6LXygselKXYSk9Hxyk13qG2q8g6LD65fx94+vpnbQfB6Zlsu+93dyNqkPC6f0oKW4QW7vYOkfHxqrrQzu/tw9jOAy23bu42JgOF/97Gw6XAcb470WwxY9yJemprL9/S0a0xsZOW4oVmsZ5cU19Bw6gox29SUZ9MxNpK2pgfLaRkqPbOKHP3+BTWdrqLazGD0wm8qyRug9kU/N6c3ZjXspcfrx6H1FNO7exK4rNjMWyn8qrlKpw8aCoYNJb6uhoSOOLBddQcbMncf49FrJu4e99d149Iu308PxM+OuJfRvP8HaPaWaH05jeGojlZq/+2RP23ZpEXbfWffw8MRU9qqPNp9uZ/EjDzKtWwAZBUtw6ioCeaP59tdvJ6n4AO9/uIsD56oI6xDoSo3NsNG9aNdBcihmebaxbIdQYzXNqX0oLLC9ue+0+z/D/aNh3fvb2H4pogPi/rReriZ7+EK+85kJlO/dwtpNezl1pQk33IiT14+heX5q2tqoiaUxalRv7S2F6TJ5Pgv7uWzadYpiHXraeQMYmu1SUddAaUsCI0cXUHf2Ck2aEF8vC9F75CCCdVdpDQzhr791P72aTvDu2i1s3n6MsvYM5t17C4OaTrFBG+A9Z0xhWKCRWrcX3/zLO8kqP6CxbQ/VWQMY19tPaT0seOxhJqdcY/PO/ZwI9+arX11CthuiJbkLo3onc+W6sagFrmtuz36NVVdpyRlCUV+Lndv3smnHPjZvOUi5041h2e1cPn6MhpTe9EsNE9O4bcfaiKYP4s55/Sk/e5ZL1S1cr2pj6MQxpNde5Wp9Oz6tMTwGIK9Fl+XxsoiQkJFDnnz/3JGjHDl9gZOnL8n32rCzB/PI/TMJndrDxgP1zHjgfmZ0t2lvs8nr1ZX20nPsP3aOk6fOce5KjQ5IpYMWYJGoSywG/cdNYHgeYut6vPQmvnTw53/lZf93amUZ4pZF91Ez+MoXH+KhBaMxA5A/uy+f+sLn+PZj9/DQPUv4iy88wMzeiUo7yUxZch/f/cajfFWbYRN6pZPeexx/8fWHWDK2OynJ3bj1kUf4yt3jSRfxgnFz+c5fPsqdU/vTb/Rsvv7VB7ln2iAykjKZdvv9/PWX7mbOmN5kZeQy554H+fbnbqWwX6b61SVv+ES+/NXP8u0vPcQji0aT7g8yesF9fP/bj3Dv9CLu/9z93D2xpwfbp2gef/mXn+fL9y/mMw8/xJfvmU43G3xdBnPXfbfz0H138NDCceT4XKMxUpn4j9CtJMYtuJtvf+OzfO1zd7BwXAEWLjlDJ/LQ/bep3MndU/t5f92V0G04j3zpET49d4QcPJuZdz/AXz68gBH5CRSMm803vvxpFo/Jx1z5wyZqwmbwb+e2ot74PdYWliXq5t3KoWhCHnvf20FDZn8+9+VH+dSUYYwaP4NvfuNBFo/uQqjdA0RImCu/Z18+/bXP8jktsrtnpjB24b185/O3MqpnGjkDi/ja1z7DneO640vI47ZHvsB3H7uDW2dM57NfvJuJ3YIEuo/iS9/8ohZzwxk4chQPffazfPGO8WQHHXpNWMh3//qLfGbOFO566B7umzWSgYPH8sBn7uahu27nvgXDsIqP8+ILr/Cb55bz1HPLeOr5N/jdc6/x+CvrOFwcomDERL7w5c/xl489wENzhlCxbx2PP71UMG8Qh1/O755+md+sPEm/Gbfy7S/eweQ+WfikXAzZRs/4rXcL2kPQZfAEHvnCZ/kb9e8X751Gn1SbcAwisk2aNtA/9eij/N23vsBfPrJYSTLVwwkp8d//2c/ztSUjCIYtWn3dueuxx5Top5Gf1Y07H32Mb9xu2my1dVPb5/nOwzPU1oXbPv1Z/v7bj/Hdv/wCf/OtL/GPf/U57hqTRYNWyum9R3DvI3F+33rkFgp7p2KHmziwdhn/+qOn+Omvn+Ynj/+ef/vJs2wsTuAW2fdvJbdH65tf4J++/wVuGZlHrzHz+PZffonvfvMxvise//qdh1k8si/Dpt3C33xX9d94jL8R/+9/58t8/a7RJMoOkUAWkxbcybe/qfavPMjt8n0nDBrzCGu1Onzhp/jOF+5kaAa0tIMvqSv3fO0v+OL8oSRHbWJJeRpYv8I/PjqdbBm8xU1i+PRFfONrX+BvFM8PLxpDbgCaNX50HTabv/n23YzMTqC9CYLy+8e+8Rc8UpRDRAPNXV/4Ol9bOABbk4B2xc+I6Yv55te/yN987WHunzWIJHViVG0hbVD3mzCTr/zFF/g72eFzS4rolQYpWs8GAupgwfWZspjPLxmNqsCycdAlmgP+X+ydBaAd1Z3/P2fm3ueWvLi7O0kIBAmQQCAEEkIguHvdt9u17nZ3u926tzgkeEiIu7u768tLnrtfmf/3zH0vArTb9t92KXunM3dmzvnp9/zO78g80mHie/ZhPqvNsYemTeKhabfz7DP3K6560zyjHVMefoxnJwzSpn8mw268g689NoEBLRPFDMYYDOCZVEbcdDtfVd9+7qHbubJ7Jlkd+3H3ww/w7N1X0TU9KCowxmCP1K4j+cznn+azd17FuImTeGLqtXRIAJp3586HHuYrzz7AU/fdwIBsFQaac/ODj/C1R8fRp2Vbxk57gG995k7GX32t/8+3XNszW4yg+QuYAO0HXMlTWjg+eMMA0hNc+oyZxLe+9BB3jB+txc9UHry6C4kZknPrzcq5E3jkoTsYN6glGsdp1WskTz7zMNOu6YlvstOMsfc+KH1Tufnqa3jssSlc37u5HE5g6LhJfO0Lj/DIHbfy2OP38+C4fmSktmbSI0/y9cdux+aDR5+8gys6JOMfjb57Qr5Vj8u49ebxPHb/LYzQBMxmH4FI9+HXcv/dE3ng3tu4SfFry7P7Xslzynd3XNubDONLwt6Msb+Q3LoP9z72CF955gEevXUkbVMSfAyetPlTMZkRjNFZcnvJeEhrz42338YDd9zE1Dukb8o19GzmYvURTGXwDbfxleemMXl0L33YUruPvYOvPH0H1w9oL+s9UjsN5kE7bj1zH49OuYLuLdpx1c2T+OKTU4VPM9y0Dkx54nG+/vht3HjNDTzz6AQGdWrDAG3Qf+HZ+5gwuD3JDjoMxhjdIaF5Zx56ehrPPXQzQztm2DUVqgQb34EsRt08ma987im+/vlHFMeX0zoJtNdONL094yfdyWcfupFBbdNxwhAOpNBrxA08/cyjfPmR8Qzr24WBV9ysced2rhnUjoD6szFG/TdAn1Gjubpfc0J1Vp3BANFIIr0uH8Mzzzys9r2OwR3TfXuSW3Xntgfu56lJV9G7RZB6yXEc4XPDzTzzmSd5TovrYV06crVi+gvPPcKT90/mwbsm8fgj9/D4nVfTJQ2qJbvroKt44qmH+czDd3DflFu5b+rtPPnkvUy7rg/BOoPdeHeadefWO29j2qQbmXrn7TwwcSRt5LPVaYy1UoaqwUwgk8vG387nn3uA+yeMpq+IwtFYXaBZV26echdfUN1j905S357Ew/feyeefncwQ5ZzU1n2559EH+OITU7j58p7YuURNOI0xdz3AV/SRbFj/foybPI1npl5Fn9YpaI6I1mU0pLTnprGjaSd/mvW9iicfk+1j+5EZcAgLk+RWXRk7+W6++tlHeWRcX1p36MS1E+7kuQfG0ldtZNderoEqWnCd+mX39CBuagfGT5vGl557kMfuu50Hp93BUw/frfFxAOla/Vi/k9t0Fyb3KDbv54l7bhPNJB68716+/MStao8kulw5mW8+dw/TJt/C3VNv44Fpk/mM8u6j13XHFVZRv3XBKP9FGyCx4yCeeO4Z7h3VmpByt+u6uI6hvj5In9E385VLxohn+cKdw2jdogcPfvEz+hDWHqPxwM3uzgPPfoZ/eHgIGUH8wxiDTj0H6X3FdTx47+08cPckJo7ojE9i0rn8linE5ncTubqXcoqojbF8Rk8QyO7G1Icf4WuaU9x91+3Kz3dzQ880PNL0MXKqsH2E5x6cwJXiNcbgOR6O4/q8HS8by1e++hSP3HQVk+6arI9ww8n2xRrZZXyahJaS/8hDfFW59pm7r6dPq0TwDD2vuF5xcisP3DOJmwa2QrDRqu8onvvMPUwY1Jb09I5MfvQRHr9lEO3admTi3VN54K6JPDDpGrplWtEBBmv8/OYX7uaqbu0ZePXNfPNrjzB17NVMu2cKdyqPtew2nOeUr+4Y2Z6ktI7qU/cpxkaS7Yg/pTW33HsfX/vMQ3zm3psY0qMd3Yddx+cUw1Ou6EJCYhpX3H4f//j5u7lFH5Ef0hxq0rDWYsT3zWCPRIaMnczXP/eQPtbfzIMPP8ATNw0gSd5k9bmKZ597nC8+cTdTb+hLuiXH6H96cIJ0GzkOO3++6+oeJLnJXDZhGv+oeeKUsZcz9f77NO9rHaMV5kYsAl6Y3ciXPvcon9dmwi1D25PWaQgP33cbI9slgJuJ7U/feHwC/du25sobb+OLT93F9X2yyeg2jIcemMw09e97bx5KM3QktGTs3ffztScmMqS1wUvpxO33Pah2eogn776G7llpdB0+ji+qPa7umkJGp348+NSj3H9dDxLkX0K7AZrHPYwdQx+dcjU9WibRfdTNfOHZe7htZBcyMtsy6dEn+dqjt3PrmGt5TGPUqPbJUgzGGGJHIsPGT+Yrn3+Yx6bezCOPPcTD4wZovuHRqv9VPPfco8JvGndf05sUN8ZhTIzXM0lY7L8sPD772GQmjGgvgjSunvIA//DZuxg/Vjo1172pb0vNIUO06D6UO24aywP3TeTKbunyQOQJzbhh2oN846lb6d88kcRWvXlc84UvPnILN48bz2efnMCwLu25QvORLwnXoZ2yxATpPa/Qgv4ZPnPHaI1vU3ji9pF0aNOGGydN4eG7b+XBO6+jX8sEn9aaay+ksUWfUdiY+NxDt3LXPXfx+ftvQntheBldueMB20ce5PG7rqa7/T9IEHeMDwxRTEorrhw7hkl3TtHHikGN8ZTMsJun8K0vP8DkMVdz3/136IN3TwZoDPrKU3dweec00roM8+cp917ZWYKSGTnxHv7+M3cxomMq7YaN4xtffoJHbhnJhLumcf8Ng+ndezAPaTx79Mb+tGvdmpE338lXnpnMVV0zcDM6cdcTT/J3D4/nJmH52AO3M6hVLQWmA7feMJQstZGnCY5SG/Zo3vcqvvSlp3j69iu4adJUHp80ihYGUrqN5LkvPMcX772OmydO5LmHJjGkhRBKacOt96o9nrmLe+66lWcenbL0kgAAEABJREFUu4eJQzrSa9R1fPbpB7hzTC/1LQhqvLlt6iTuu+s27tNcpE2isodpwfWTb+PBqRN5WOvGkR1SrAnQBCJGeUvK6ysJ9hzOqDYO9miyFdXjH6mMnnwP//Al6bvhas157ufO0T3p3PsyntG6+NbB7UhXLrn9kYd58vbLyA5aJkNMWiIDx07hH774IHffOJLJ996nNW1PBl51PZ97Zio39G1NkkxAR4vel2se/Rhf0tz/vvGDadMsk2vueICv3DeGLllJdBg2li8/O5Ux/VrTWc+fE92Ua/qQ4YpZp9u8F7fc0DuW5xudCGR2ZMKUSdx/10Tuv+0KLkBgGr1Loffw66T3IZ6cej0DOmTiSpbRhTbwh4wex2eee5jnHrlTefl2Hn1omnLBDfTVPFzNCoFEjVe3aI3xEI/rg+6QLlmWE85jTOORRL+rb+Tzmk/efmUXMtv156HHH+bhcb1JTWrBtZPu4gsPX0+XNIOnMWq0YuNrn32YzykObhjUGqSs4+DR2PX6/RrTbh3RQWWQrnz+1FMPcM+Ng2jRaLinDxrDx45lsta0D08YRnO/PfD7y5jbJvpjxoNTbqBvM+tlOpdPuJ0vaL44qksmtiTYrBvTnn6Crz5yK+OuHauYm8iA5hKe0Z17Hn1cuWEqd94xic8pf13bM12mJTN87HgeumsCD95zCyM7paDok0KDMQZ7BDI7c/dTkvnoRF/m0+ovQ3t2Y/SEqXz5yYlc3qVZDHef3qVV16E8+OTDPHHrCDqkgRdoztgpd2tsepin77+Zoe0Sydm9i5oet/C9rz3OVz/3GN/5x/toVX2ckyXJXDX1Af7xC9O4RWuHR9R24we3lC1R6XBJTJQv6FDfHa/+cY/mKw/dcTWdU2O2GmNEa0Rg9aYzeuLdfOMz9/OAPh499ugD3HtlL7oOGs5jTzzCIxMGk1Gwj111vfjnf3xS496jfOsfv8641mUcKUlSrriLrz19O1f2zJa8VOXi+/nWs7czoG2WxobxfPPrj3L3uGuYevcd3DOuP537XckTT0nXTYO1lyMWnTJHv5DdYzBTNGe9d+oknrxnLP1bJPjlad1G8Jxy9Oc0zx136+1+fHZKMP5H/6jjkoQOL5EB14zVnHiC4ngCV/fIVKFOCTexm36lo9cI9YXH+OKT05gypi8ZSYn01drh81o73HZZJ1ItdOKxxCnZHbhl2v186eGbGdQ+RcNxc264837l0nuZNuU2ntae2tQrbZwaelwxji+qjT732J3cNLQtblI2N971AF+8Z7TmsmnKZzfxxeemMKqTrE3tzLQnH+fzd17FwL7dNSd5iC/ffx1d2rSk36gb+cJn7mOabGuWlkr3odfy7LMPcd8Y9SPHw23Vh/sfe1hj9gM8Pu16eqRDWuehPKOc+8zUsYy/6RaeuX8MbQKQ3Gk4T2vd/oVHJnHn1Dv54qM300+dyHNbMm7SRB6Yeqty5/X0zvDASVE/uZuvPDKBkd0vws6gw5DdqQ93P/wgX37qXh6Zdrvy7e088uj9fO6eG+iancKgMZP5quJgZJfm+KlJ85s+o8bwrD76f/G+sQxR3uk+6iY++9mHeey20QxoL8Ot5EasFZA6jeIXMBlccds9/PPXn+Qb8utbX3mGf/7GQ4zpkgbqeZmdByieJvOI+uI1mqPKepJ6jOYrX3qSv9P1919+in/+++c0Bl1GpuQbN0BAiwLHSWfMHVO4uX+6r0tV5+8S/Kk9nb+GZ56298PhCJFIFNsgyppE9B6O2LIIti6qCqPaqMrsu62LKvF79k/fRRv7Kx6PiK1vlONFY7y27rwOCfIukhO175LTJNfKVMvSRG91RRrlNdFEZK+1LyLeGG1MT1i6L9YvIb49tiziyzAfgfMjPlmZqNTqaJQX0yNW+RrTG5UHHr49orEsTfb6tD5p9IJuS3CRahu8UdH0uOZWRiXt5ZfTV3OquJSS8grKdeUe3sGsuRsoiFzEJHrbgdxoGBOIEghAqDZCaXkEu+HhaVe2rCxCtTZmZT3VFWG/rro2SoVo6rVhYoRBeVmYcn0WDzWoXDTl1dYXCNdJVmmEStVV6/NilfhCklml58pqlVdFtRnTnmGXXc6Vwy9j1PARuoZz+fCRjB7Wn7aZARrqJLM8TJl4Kqo9mnXqz5UjRvp0MfrLGDViFKP6tsfIoBLZVaevkRYPR67a+8WX40BENlj7S63dlVHCwsGWO5ZBOxqV0mXrrM56yXLE46p1KuVbmXwzeg8Yj2rZVVoRxcZX9f9QV1wq7KTPyi0WJhZTm4SQPouHLW/ShxJUs9bt6NOzMz26d6Jnt0707tGFVimGGskouVhWcVibSx7hBmGtOivHXkXSUaN2s+1ZUnJBt+W1PiAfHPlQV9PIJ9yq6zysbxYG63PI1qk85EHAwUYwVdJh29rnFyY18rtYGGg9j6sByeorE02p4qZC7W43swNuDPMS2SSTrHs0xU2FdDqSY7Esq/Ea9Xt+HDbJqVTcSDnWJnuFFRM25ux/BhbVwigjCxKCnD88myMUl+cLGh9i/Sns555Y/431cdu/PNkQ0a5TWHz22e+HykG2mzWy+zcjuqa6sN9PRa1cE9Fz2Of1yc7/xHKZdKoucjFNE490hFUX0+MRaaK5qD6qvGFzlo2z84L1EPMnoomP7fkQtX5LXkTyrBzrF41y/HfJtmVG/SJml+X1JMmejbot/8X6RNvkr5Vh7bAyPCs3bP2yMqI+pjEfrCxdyinVVVUUFxdQXRciHApj7Zc4bFt60mHl+ZcYbbmnXNgkn485bL2l92nko7XcyvHfJeOjLJJq7bR+i97yWmxUZE3wyaNNmInfU0XTe1TviMqTTTY/+zokw7M0kmffrT8+vo04ROVTU3mTHIsVHz6sDOl13AgBLVyDil3bJkZ97JI+qT5kc2lE/DbubT+pUd60eULdnRg9hOqjyvHq4+qHDeqsDcq7Zeq3tepslsYIBtt/Q+o3taq3smyZvexzE3+FcpvN+7ZMjaV8H8HqFwu2zChfNGh8sH2vTLm7Qbmx1tqj/l6unFlZJXrpLVedzalWZ1hjQrnKrM223l723Y4FxvqrZGOEdbV4q6S/SvdK3Zt8tjZeuDwalJPKhIu11f+vMiTDrxemNeK1ddaWCj1XVEaw74JBncOjytqhq6bew+argPJVrWhsDre4WWyt7Roe8P2VbAePGuUxa48dD33b9W75LY3FyfKVCoMK5a9oxMPHRG1h7TOSYe2z40Wt+CwuNofUWL3yw9rqYyL8LCaIuEmuxaTU0lhf7CUaOzbb9rBja7HqKoW/j5nqy2RDea1nRSBY/bvE4ctT7Np2q1S+dRttsnX2OaR4Kf3wGKE2tPFdIZmWJ5gAaUkeQRP2842nmPzw6Sn+Y30sIpomCo+m/BFWv7EyP8zn9yHV2b5j+e1dIYHFyfKGFJSW1/6f9Rw5foj8qgCdu7TzxVjasPpf5LzuqFrMr7rwo9hokhsWDnpFwrH9NSK9/iWF6iYyJernsojekaSIZId9Hs/PzRGfPio6/MPaF1bOtH5F1aftc6TJFsnwlD9iZRYPz5cRtvIstwyJSJ6tD6vMyvDEa98j4rX6m+RHVB4RbcQvt8wXrvM0tl5XWLKQg02yLHYfx9dU31R3Xs7v0iWAoo0+npcp/yLSGTPLi7W13j351kTr+9VIZ2kjss+iYf2z72HRW34jHv9deIYtjYg82WLxsPUWdNseMXtljJVpaXVFztNHzreftcHShyU/crEcLj3O+y06q8vS8gfgJwti/kp/TIcMVsw0ybPxFVISiGosrK2uprSogHMaCyOREBH5avnPYyAZKtJrVPaHicmL+WLxizbaZp/R4fn+WLrohZiSAEsXEW1EePiYifbCqR7l80muTxO7i42Lsbe8tuwCH5rL11JZXkZeXg0h2dqgy3praazOJtx83VIcbYwTPTb6FCHiv3AeM/vqNdJZnTHeqJpZPtl+J1ttGzbJt/SqJNamopEPYV0hJfgWPQYyZkgXkmWQQQmO2OFJRtjKEh5Wflh3a7en2Lm0PIKVfykOwkd+WrtjciLYZ1+yAIpId+ySzYoXGts+VmblWU0+tX48ys/sZeuRAnat2k2S8leWHSAlR5UfOr3zGEVkf6TJBt/mJhs84RBRnESl9VL2Jrx8XmujHItKTlhymuLHcjT5ZMstrSdbfF6LkUxvqrc8Tc8RybK8/iV7IqL1n5t+JKPJf1un16aa8/cmWWHxWtnnK/QQszMsvyJErO26wj4d2PEKHV5jzMTKZajKPu5s0mNt9qyt8t8+N7WT5bfcRgj6fqs+LH1R66OUWf6I3v3LL5MWyfHxEm1Er5GGOioryijIK6BeZbbOylSV1MTa0edv9MGWN+k677tAivgxGiF6vp1Eacul38q0MuzdN0PzsKgwsGX2snJiuUQ8TaflPS8z4uNp6T6iu5G+CZ+w7LT2W0wijbrDWqNa3uryInILSyksLaektIxda/eQ0nEA3VujtmrA5rqwtb+hklNHjnD4wFkqg1l0aauNBTifY6zciNVjFXHpYS5qC0tnfbZt5lm5oQb5ESVUWUxOQTn5ZWWUypbcPevJpQOX9UlTvwnRYHOu/FcD6D2ClWHt94SZfY5IlpVt79Zvv8zac6kpSjVR+RXxr7DqLfaWJMYTxpZFhFFFUT57Dx7lSH4lrTq0J9US+W10gd/qt8Ufvny/GuMm0qjggp0fAkg+WX1h6bSkFiv/Xfzn77EKxVEE65dPa8uacJUfVmqTjliVR8SPlSjWTl+WpZO+JvusbTG+qC/XvoOiTv3Bp/dtiEqLUFdZuFGeX2dl0VQuu2S/Xy4eqZAUD/seu6KyQXJF/7tiVVVqG8/nCUtGjC8Se7e6ZGi0sa2tP5beXp7aPdxoly1vwiAsHh8HS/Sxl+fHUcjynr8istMSqxXkb6TRp6gE+darzNd1nj5MWHpkmmU6f1kfVXz+/f/Cw4XR+S/ordGqJ6CdJ1crHKPGsAHr6D1g3xv1WvDDSm44LpY24LrYhZPIVeRqQ8uI0qjM+EFq6aNa+bmicy2hwT9sYEUkJ6rWNdLrqM4Yg707roOj56iCz6/Xs8+vcsvuuK6v23UcXNnnOgZ7GMelySbX0riObFCNEZ3ebZnbVKbiS0+D417gd87LvIi3sQyjMl+vld/IJ15bbRzHt8G1LyBS0arOtVdjGRcddrHsBTKZ8Mgj3NYlxMJZC3l//krmLFjBxnNBrrzhCjrZGZp4jK7YaXC185yW4tC8GbRp7dK+rUtbDSptWjv+c7vWhjatDO00kNi6dvpyb+9tz9MEaK+ytj597LlNK2jbJibL0reTTHu3NPbZ8rdr6dBBX2JHjuzJVZd3Y7S9Rtp7D64Z0YHenVxaWpp2ASx9G9nQo28HrhrVPUbbRD+qJ6OHZNOxnevrjNmLbP74y9pg5bX3/XFoK1vbNF2+D670xXQ2yWot3T69/P1HF2UAABAASURBVLS09j2GhyM9JobN76nr4OuyMgN0sFgI09ZWp/TF8LB1Lu1V3qxlEiOvu1pf/2/hwWk38eC9N/PIAzdwTf8U6RK/j4eljz1bG9v67RYrs3Y26bBt0OEieltn28r60EY+tfPbyPK5tGtj2xnpiF2W1+Jk8fFttfTWj0Y/fX69d2jrNPIYH39fh3xsL7om3rbys4PK/PeWkq93n87XafCx9J+RrI/KaWOxarzaSq7lbdMqQFaGg7oPFx/GcVXmXFzkPxu/PwVifcr2IV22j9v+ZdS7XVd1rqMng+O6Pt1Hu9mFuli+MhhjcC29z8slhzGO5MTkXkLTxCPjA+KL6TEX5FxU7/h2uzgq46LDNJa7juOXOo4rXa5kNOYJx4B4XNl2/rJloGInRtv4DkZ8bmOZ498d8aLDcRvLm+6OwRiDq7wRUJnrOI30aJahzXD5THUe5yIZZEdK2JNbhRsMoP0+EcROa7tree0lebbUiC8gPNzGd1t28WXrLY9P4zoYVVo5/vvv4MHaaXWI3vK6uqtInLHTcVzfdlf8xhicxndH7+gwRljKJl+HK52WRvLsu6NndDXh4DiOL8uWO41yXMfwkUM8juqTElyyMgwts1HMN12Gi/tkUx+KxX9TnfKczb1N/aGpL6kftlX+aOv3aRebF2J80LoV6pvOJWVNdbZv2v7k6zovV7rUX/2yRj1t1P9jspUv1AetrottjT27NPHYnBGTrTLJsvX2sjmlnfjbNMmVze1kczuVtWu82zxxvr6J7rx+yRNt2/O2IvxMI26qk672kuNfeo7JUr2efd3SZ2W3lrxY7nVoqzKru72Ve14fjXJVb8t8nF3pcVRu6+xl9O4Sw8/QpkmO3xZg9djL13VetiGm1/K5+HplWzvVW9rYJRrrg8rb27u97LMu649tB5tPbV071dnrvA3W1g9fvu223YwfCzEdTXHh0v5jxwhz3i8boxmK1cTEAK6jfvCRoAbbF131Df86H/cGx3X9fmFzhWMMHzlUZnlsn2q6Oz6ZaeR1sLwpyiVrl6ykutsN3DkyAyUbXL9vxmxypcd1P8a2i+QHbL0vGxz54fo8rnwykoe6s4Nvh2+AwfVznKNyPTfRuvYd/3Bc16d3pMNxYs+uo5yhctcx4nP8evsMBlflvg3oEI//bn1wHRy9G/FerN9x3UZ+x+d1HSPGS0/HdWM0uvvyXMcnaJIVsOUfw9dU7zbWOW6jHNlg5TSV+8IafxwnRnNeponZFRNhcFzV6zJGz420jp6NidG5qnNdB4M9jO+TleXYAtG5qrf+ByyNyppsPF+v9nD9F8DKtNjpcl3J9OndGBaiMcYQaz8X13H8chXz4cNxG3ma7qK1NE26Ayp3P45RRI4b471AY3DcWJkjnmDQxYmWUFieTLPkMvafqMB1g7iGxsPgNtLLXDCO7LTzBRfXcfXs4hiD4154RodxHNVZuhiuAdfBGOPTuaJ19e4YPnLE+FwCPk3sLjbEjKsyi70rXr8M9TCtt3Qj/1Q+yS2yKDh+mMqwIUGY+7sNqnRcydG768RscaXYcWJlegTjyFYX138Bx3X9d/tqGulct4nXkSl6VjsHJM8Ycwm9KnFtXSO99SOYmEm3Hu1JDcheA9iL2GEkI3AJveSryhhHNlyMn4u1B+lzG+07f1eF8eW4531oorM0rmyJqTQ4rmgaL0ey0OHZXRYMAepZ9e5brI9044ZBbdGOnCCMcXLJYXw5fltIr+tjazDGIVZmAIOr8sB53Zw/HNclRufg6tl1DI7k2DKn0SZ0NPlky13HwRiD44rXtc/QVG95mp5dx3D+MFa+c/7VfzAmplNyXNeRTD5yNMmytjuiv5jAkR2x9nLPy4nRXaAyjuv7Fys3Fyo+9GR8WZLjGNkhW4WXq2cwOK6L5TfYw8TeVR9QuePTgOV39e5fjWUq9HUblSvc9FGhmECrLGpPHCWv3vXraDqM8eX6/K5DkwjHjdE5qsceurt+jLo4juPLcAxgyxtprQzbTn45RnTueXwc0fHhQ2UXZLr4fqnMcfUsPz/MY4yDq/KA62B8WcaXb3UGlKwsfZ9rb2Fcm2Len7eK+fMXs7WmM4/cfyOtBYRxEvBzneQEEpKJlp9i9vz99JowgStaBFDPxBjjy3Rlg2v1xBRx6WFwXBer1226OwbjOAT0FdwVX0LPMTx0bQYrZy1nwaJlzNtZxY333sfIbBecIAlBF8cYwJyXZd+N4+LLdZyYHbob48TKJNdw6WHrXNlgr4DqnUYCW+7HqApsXWqSQ86W5awp78x9twzGlRjb5R3Jt/X2cnx7VPGh04jG2mQv14kpMI4bs6nx/TyLZLiyJ6ArVmV8P3xelfn3WAWOE5Ph0/plBsdVmetggCYdsSqD68efg2P03ESnZ9Non+uYRj7nUtuMc5ENjbJV5uPjNtbpbgDjl7tYm1yrI+AiFYDxZdgyV7RShT0cV7SicWJEtuj8ZYzxeXyfRec2XgHxqwrH+ShvzJeA9DtYmaaRxvI4ht9zGBzXJWgxOn+5NPEY4/i2uKJxzhc6wkm6ztPr2XUwXHo4rouKLy38lL85f3X//AZyGsE3OK6rxrlwNbUZ/mFU75xvXHQYx8E2buySHKNCncY4l8jxg/FiYap3bDQCji/DyrnAr+K/4vnXUGXQ9xgpSqLv6Bt46rF7eOye27h/2u1MuXYArZJd1av6k3Iaa4jnf82yk7QPX7a2sfnsI7Hn/4Hep/zb/vFh0dT04/AgVvm37WDc+k8vAkb51XqX3oHJjz3Lf3zlLkZ1bxYLWycevBaa+BVHII7AH46Anf9Z6uZ9hvPIU0/xxMRB2O1nlFXiGYX48YlFoDE6A624+dEn+c5XH+TGvvra6NvbWOc/f3J/YnNuaH/ZOL75rS/w+clDaK4NF2txU519/l+/tOPjyYhPIqrGxKxK7XAZX/67z/LMzQNIdWSszV+xKvsSv/6GEGiayjbvfTXf+Icv8oWp19A5NeaAid0+9Ps3/NoYvyRlM2bSnTx5v/YU7ruLx6aMpmNazK8mPGL3AF2Hj+Wr9p/lGd4BP9QV6zHK/7/fGLZB+msz/OmH7+DeaXfw5AMTGN4x0Rcc0+8//uV/GnFx01py8wNP8pWHx9IzO8HXa0zMUv8l/hNHII7AeQRi+eD861/uwd9Ak/i6M9t46c0lnLb//Wm4hLm/+RFf+Zef8E//+gO+/J0XWLy/QlQQ0aZbtOYcH7w5j41nYmW29OiGZfzs12/w4mtv84vXl3DA/3+agNzdK/j3b/0Xf/edn/IP//ZT/v6fv8cPZu+jIeqLo+bgRpbsL9FLPduXz+f5l9/hhbdWcLTcTlVo/BN6VX+qzlji87wo/l99R5vuMZ8/ea4ajPn46+Nt/XhaY8zHk//NlpqP4PI360rc8P+DCHjKP/Y/i4rqA9P/QffjLscRiCPw50VAm0yxOc2fMJf581oSlxZH4I9AwI6FUSKRKPa/wvwjGD8xpHY9EbH/mfEn1QHN/80nBq3fYYjyl8Uw+knF8HeYHS/+PQg0tqn9L7w//aOSR/T8foJymeL4d/kcyxeiET6/B70/ucr+V+8X2/IXUvMH2hfDJSJs/nft+APNjZPFEfhfROCvtwGNwUTLmP3aW8zcnEOddbqmhtKqAMOuu5zrRw9j3FVD6dEy9vWq4vQh5s6cw7tLtnGiPGypdVVx5GyYoVdczcSbx9LP288Pf7HAl1VcUUPzDr0Zf8Pl3HjjSHqmBnAT5J5OjxBbdhWQkuJxdPVcZu+H8bfewIjUE7zwyhIKo+jLXERb3lLxKTyNcbB/9X3hMp9CL+Mu/a0gELfz/xoCRvnHxXUdtDYkfsQRiCMQR+D/CwElEsex85r4XOb/C8c4818ZAaOx0PHHwr/qX+jx5zuMsfa7uH+rDvz5oPjTJSl/ua6LE8fwT8fwk8bZ2Ka2X3z6RyWj2HUuugyGjz+McXBdB0f4fDzF/1+pcST7ousvpOYPNNLgyBbXcfjftYNP7BE3LI5AEwJO08Nf7q4vQl4Ux9SyZ/V6Stp1p3eaNpmt5ohD655DmXrN5Vx7/RjGXzuMbnYDWvRp7ftw2x3jGNI2Cc+zxLLQS+aKsVdx5YAOtGzRnFFDehDNPcDhOo+uA6/igfsncu2okVw9vCvNe/TlumHdsf8RhKk4ysnEVvTKDLJh0z5aDhhOB/EPEq2bu52t9s+xjaMvep6UxM84AnEE4gjEEYgjEEcgjkAcgTgCcQTiCHxqEIg7EkcgjkAcgTgCcQTiCMQR+F9FoHFn9y9ng/aStfnskLNtPXvD3Zg4sgOhhobYZm8Azh3cwgsfLOGtt+axaNsp6n1TPH0x00M06tM1bQt7XgJZmangxf5auaS0HJq1ppljSM/MJCXN4Ol/Z3bsp8TLpkdru/0M+YeLyWiWQXpaNafyQ2RnpohK8hOyyDA1nMkv04vh4v9jLOJHHIE4AnEE4gjEEfizIhAXFkcgjkAcgTgCcQTiCMQRiCMQRyCOQByBOAJxBP7vIfAX3YD27D+Co81hr+gUO86lcPPYPrRN0nayk0BqwEBac8ZPncQNg/py2WWdObnwbX425yCe+V1maXtZm9Jhx8VQxtrt+Qy+/ho6JGhPOhrRD5iGcvYcyqH9kKGkYI9KDpZUkdGqC0mmjLpQAgmJAfGrLhAgSU/12hDHP2Sbf4//xBGIIxBHII5AHIE4AnEE4gjEEYgjEEcgjkAcgTgCf9MIxI2PIxBHII5AHIFPBAK/a6f3z2OcMZhoBQs+mMOBSoej23ezdFsu1ZX5bNpxmmovidZdutCzSwd69BjAxKuz2btmC8frDb5hH9kP9oiqJmga2D5/CfkdruSBsV208ez5m9bSRtnpHRyu7cYV3QJ4QLSkjMrKEO26pmD/vNoNRAiFtFmtOiIRGnRP1EY0XpRwxPN5VBQ/4wjEEYgjEEcgjkAcgTgCcQT+TAjExcQRiCMQRyCOQByBOAJxBOIIxBGII/B/FwF/n/cv476HsYLDDglBl4oTW5i/dC0rtudSVZ7D6g2nKC0vIa+ogWhUG78eJKankOCF/U1hy2ovY0xMjl7sX0a7Tj17V67iQKAfj9x5OVmAMY00ppYtK4/S4drhpAEGKCw+S7nTie5Jeglk0zYTKqpq9aLN6VA5FV6A1tmZYByCCa7PQ/yII/DpRCDuVRyBOAJxBOIIxBGIIxBHII5AHIE4AnEE4gjEEYgj8OlHIO5hHIFPFAJ/wQ1ou/0rXxPSGPvAM3znm5/hH7/2LN94YCjZrQbx5WevplX9YRYsP4Ld9fW8GrZvziG9Zx+6JWpz2LOb0lFCDSFCkSh6xZhqNsx6hw8O1dGvZzZnDx1l3+EzlDVEVGeoObSajZFe3NRDAtChzexzR06Q0qef/39GSGILRvZvT86+/dRp0/vc/u1UpvXhsi6J1B5YwQ9/O5djNeLD6rb3+BVHII5AHIE4AnEE4ghEqwqVAAAQAElEQVTEEYgjEEcgjkAcgT8VgThfHIE4AnEE4gjEEYgjEEcgjsBfcAP6Arj234K2f+Vsr7BJoWWLdMLhKMH05ni5q/jRi+/ym+ff4UD65Xzhvsux28dlx/cyb+lWyl04uWk1i7fnYELlHDp4gkMH9vDKL5/nh7+ezk9/+wG7ij0gzK5dZQy8sj9p2kBWAV64lLzqllzWJwhe1P/nO4bcOplrmp3h+Rff4L1Dadzz8ETaJ0BdTRknc4uoifqcjRLsc/yKIxBHII5AHIG/eQTiDsQRiCMQRyCOQByBOAJxBOIIxBGIIxBHII5AHIE4Av8rCPxVNqCNMRjH4DiGFn1u4p+/cSfdAw4mtRePfvFZvvTYVJ5+8iE+d+81dEh1fCCadx/IrXfcw3/859/zT09O4qbLOkKwHQ9/4x945Uff5Ef/9S1++YN/4lffe5Zr2wbEE+CKu+5m0sAW2jw2egcTbMn4qTfQyb4aBwdwElswbuq9fObxe/ncY5MY2dH+2xzQ7LLJ/PifH2ZgmohE6Rh7j19xBOIIxBGIIxBHII5AHIE4AnEE4gjEEYgjEEcgjsAfi0CcPo5AHIE4AnEE4gg0IWD3ZJue/6L3C/u5HvYvopuUffjZ/i1zU52923r7z2/YZ3vF3mMymp5teexSuR4u6NLLxcx6tafla7pfqBbvhRdbHb/iCMQRiCMQRyCOQByBOAJ/6wjE7Y8jEEcgjkAcgTgCcQTiCMQRiCMQRyCOwP8qAn+1DegLXhr/32tuejfGND365RfeYsXGGJXHnu2vMfb90suWxy6Vxx4u/Ir+wkvsyRjjPxhjLpJtn41fHv+JI/DnRyAuMY5AHIE4AnEE4gjEEYgjEEcgjkAcgTgCcQTiCMQR+PQjEPcwjkAcgQ8j8L+wAf1hE+LvcQTiCMQRiCMQRyCOQByBOAJxBOIIxBH4MyMQFxdHII5AHIE4AnEE4gjEEYgj8IlAIL4B/YlohrgRcQTiCMQR+PQiEPcsjkAcgTgCcQTiCMQRiCMQRyCOQByBOAJxBOIIxBH49CPwuzyMb0D/LmTi5XEE4gjEEYgjEEcgjkAcgTgCcQTiCMQRiCMQR+BvD4G4xXEE4gjEEYgjEEfgE4VAfAP6E9UccWPiCMQRiCMQRyCOQByBTw8CcU/iCMQRiCMQRyCOQByBOAJxBOIIxBGIIxBHIL4BHY+BTz8CcQ/jCMQRiCMQRyCOQByBOAJxBOIIxBGIIxBHII5AHIFPPwJxD+MIxBH4RCIQ34D+RDZL3Kg4AnEE4gjEEYgjEEcgjkAcgTgCf7sIxC2PIxBHII5AHIE4AnEE4gjEEYgj0IRAfAO6CYn4PY5AHIE4Ap8+BOIexRGIIxBHII5AHIE4AnEE4gjEEYgjEEcgjkAcgTgCn34EPtEexjegP9HNEzcujkAcgTgCcQTiCMQRiCMQRyCOQByBOAJxBP52EIhbGkcgjkAcgTgCcQTiCHwYgfgG9IcRib/HEYgjEEcgjkAcgTgCf/sIxD2IIxBHII5AHIFPBAKe94kwI25EHIE4AnEE4gjEEYgj8L+IQHwD+n8R/P8LquM+xhGIIxBHII5AHIE4AnEE4gjEEYgj8H8XAWP+7/oe9zyOwP81BOL+xhGIIxBH4HchEN+A/l3I/DXKvSiRcIRI9A/7swAvGiUs+j+Q/K/hQVzHH4yARzQSIRyJ/sEcfw7CaFQ6FTN/WIT9OTT+cTK8xpj+/X8ZcwG7P8gPCftj+tUfZ/HfJrX3CY+Dvzqq/x8x4ilv/63k4b+6rRZXP8/9/p76l7KrKcf+fu2/O9qitp/I/j+V/3dL/mTWeI3590+dU4gdwRW/InwSMfhE2WRj5ZPZC/78VsX71aX9wW/7SDnLZ85h+b5CrXlUH/Y+UfH5v5HHLC4aMv/8AfjXlCgHIgLvD13H/jVN+2N1Rf/Xxn/1Ba3TPg0YfhjzP36u98dj0bS++VPnMR+2+X98tzH/B7RXk10i/x9F/uEETevhP3WW2sQf5U+V8IfbGqeMI/C7EfirbUB76oFRjbbnL73HzFJnUPn511jhX/ZXyqJ/QKbyZFdUtL/PmBjN76P4PXXGwQ24uM4f9mcBxnEIiP4PJCd+fJIQMDiuS8B1+GsejiOdihnzRyj9g2Ja/SKq/vH7B7DGvv17dDfF9O//yxhzHrs/yA8Jc+Wz+0d0lJjPv98b/ujjf2bwMfyzqv04zD3MnxAH/7P1f8MUxvDHxgiNhzEOF+fhWOzwiTw+bOtf3EiLq5/nPq6nXojN32dXbK7g/V5TP5ZGOclxXT/Hmt/L/bsrHccVv4v53SSfqhrzJ80pPOrqohSVRCkogvzCS6+8gijn8qPkFVxanl/oXSj3+TzyRHdO9E08559tmZWru5V1rsD7HXo+Wm7t8eWJ179LR56V9aGrqS4mXzaL3j5b2gLRNtXbu71s3bn8Rn2y55zk+pfls5feL/XZEwYWh9hlfbCyrX0WiyaZeZIVK7N4iUdyLO3FNLF3fHm+Tp/mUpsvpr9UJh/CTjoa7T1n7/aSvEtt/zDP/9+7xbO4BCoqtfGozfpPVSf6GGf+tH4FDSEoK4fColhfOXdJbNg2uNB2l7Sx6GxcXFKmGLYxESu3vB9/5TW2f4xOMaVYuFSvdNoye1lae+k5Tzpt3OY1vlueC/HdpMv6EVGeqGLZm+8z/6hLWlI2RcoFeYWGfMmweu1lZcUu6WuUmef70CSr6S6Zqm/SmycZMb5YfZ7qzum6cPe4uN7X2VR/kR+X0sRwuKTsY22J6fxT6Wy/KCmF6hrQ8PUxkfQ3UNQ45rt/xHz7w17F5lC/f8z/MM9f4t25ZPy/MF/5S+i6WKanGYf7R65ZLub/JD+bD82XL7bV86JEP7QX86dgYWy7Cb//jxC82Kz/+dnGvPS5H6dQHbnJpya7RP4RmR/n+4eJPr5fGBzX1TzV8KcdppHf4U+VwCfxaMTd+8TYFjfkf0LA+Z8I/lz1xhgcLXbOX3rHP4xffv6Vv8IhZc7HJY4PqTaOgyPaDxVf8mp8mkuK/seXpg5Sl3+Id9+YxfKDxTEedaDYw0W/KvPpI3Wc3LWO195YxpEqv+Rvd8JykXt/rccYYn8tbRf0eGo/+xZpqGDz4rm8sngf9X/pP4KWTt/fSA0HN63k1XdWcarOWuH9QTHzB8W0+oXty79/ADOxvs3vOkIc27qWl95byclS32Iu+eDT5EddCRuXLmTG/O2U+qKiH+tHTALUFRzm/Tc/YOm+fPwyyfHZfs9PzOff783vYf+Tq3wMf5faP8Dujyo2l2LuyzAU713Db99YwsladPxhcSDCT93pwyGvQoVHFSOzWLwnH787NlWo7nedfiwRoeT4Tqa/PoctuT6YxGLnd3H9b5Q3tW+YvKM7eHPGbDafCfuGfGiu75f9WX4a8fMq81kxZw7vrD15HtcYblaLaYxNj+JjO4ThPHYXaceFSxffxhjR/a5OgX8YYy6h8dWr7Oi6hbwwcwvaw0FSY/1fT3/Q6dVxaN0Sfvv+egqrYxxN+Tv29un59dvEi3B612pefGMFhyti8fGH+FtTb6itd0gIaH4kSAQ7F19JKQ6Z6Q5JCVxSblxDeobjl9uGcfSekuqQmuyQrCtDPGniTUpysPfEAAQSHNJtebLBdTgvDwNWT0aaQcVcrN8+J0qGL1f3tDQHK8uWX3wFEx1fdrpsSJF+e/m0riLHQJLK0lRnbfNtkh2+PtW5QUOa3tMl2/LZy6dNBFRv9VgaW54sG+yVKh+sHegwjvHlW5+TEg16xecJGJKlMy1JZcLH+mFpUi2NHE1otNnqSpF99rI2JMhmKzNZ+Fl6K/NivKzs85eUWd8sncXIyrDyPtJejX408QkV8Hu1zZj28nybjfEay5vuF+pQQ3viiepqCEUpr1Tf1yAeinV78X1yT+vNH2udz6NkdHbvel6asZR9JfW+iD+oX2k4sZv0djMy4hk/vmy7qxkacQbbZ2zbZaofpSgmrHDbPm6C6BU3ySpT856nN3qxfS45AQxgaT98XdwPbKynSk5aitEGByAm18ak4jxD1/l40XOKjVGgiT9N8R104RL50p+c6pKWnExG97F87qlbGNDaIUF2ag8F19qtfmTzhdKJmHWKx8a97Yt+XBuVXXQF1PesHX6fkq3JsiOm08M4sX7r16vf2di29RJ53q6AdDbVW18tZuYi+fbZVT+7xKYP1VuapgvFto1xe49dssOn94i9x/pD7Nnz7aCxX0S0AVenBUFpmUepBq2oJRXX38KpMPfNrC7KYcH77zN7a57//ofEuk940Y/x19EC7aKyv96jF1Pl1bB37RJenbWW3BpbZHBk11/UKoFotZvwOWa98S7zd+srHYoOlev2N31av6wDxYc3x3Jh8UdzoTEOju2cllAMOjGUsPDtd5i5KdeWaq3nqbf4jx/6aSyPhjm9YyUvvrWa45Wxr5t/Sgx+SPjHvjY1S23JcWa/OZMlh9RpLWVThX1WYoj55HFy63JenLmGXI17tupiu4y5yHdb+THXJf1COiw+UM+W+W/zyvLjWo1Yplipffq9VxN/XTHrFy/gjQU7KUOHFxXGuv+tn424/0X76986Rp8w+zVc/2UtahpQz+1fy69/M4PfvP4OP/7Rr3lt1TEapNorOsBPf/ACa3O1Q6aOEA6HiUSiuiL+P1egPiOqi0/vfJ39z37CkYg2rS7UR6XQlkckI8Ybo4+oPBwKE9EKPG/LAv7tlwspUq6yX5jCltaKEEMkEiasO1Sz7NUX+eXS47YGxB+T3WSbp05bzfwXfsMLK8/5NFHJjorO6rbXJZtpPkXjjxZ+oXAEJyWV4v2bWbk7P1ah8oj88S/J8gvVqRAuUSdAs8wGNi1bx6Fyz6+yySwq260ue8VK/aqLfjyZHvUxO08j/y7o8Px/GsLiYuX55b7Mi/G/QGMFe/5/phQ9PyhEfZ8j0qGyRiMsrlafvXwcpNPy+r58HE2Tvz7RR388YXDxP6vgv8vOj7BJT8wH2SO7rCoVYZOStck+I8tjuF1EI9qwj32UsNomLNmWlxiDWKI06Yr5G/X99X37qLk00cTuEYybSEpVDgtX7aIm8lGGmD+yR3qjTYqkO6L3iLXLluk9xindjc9RtUWM5oJ9liaqOI54LtkpFaxdvIFT/oQKxb94fZmitzItsb0kz5/7eqV88PxveWn9WZV6RLRSjMmP+LjYOFEFFUdX8p0fvcPBavsWlb+e8Ijoknz1YSvay9/JD/77NTYV+0hepFt0lgDZlxxh48rVHGvcgPZipFaof0Wt704i6aEzLFmyg+JYaaMsyZEvTSzGtqteAmnplB/eyvKdsQmxjIvFuGitLyLxpahRbZVuDax8/Xl+vuCEXx4WdmGrV5eNhSafLdb23TddlJ5iJiKZvjwbn6K3bfVx7eeXnXdOGFkhtXm89tNfMWufVuSN8qJWpr0kF2NUak9P9keF7aX+2tj0aqVyfgAAEABJREFU8RFtk13hor38+Puvsumsbc0oIekMFR9k+uyNNDRvT1ZQ7qosej5uIn67WnOspt93eY12WR8jevb9tpjr2eJgryY50Ub5Tf74d9nZdLd6PJ8vIr+iNPHF+kHM16gtlK2+PvHae1h3W2z55QlRX4alvyAjKt3hxrawuEQuMIglis29JKdRcWQby3eclQeS9nG5V8UXn55khsIeiVkBjm5Yy6YcjWBeCW/9/De8uaPIJ/UutsfX68m/iC5rYwRrl1/sU/Mh+xsRPW9/FN9++RyrQU0elayIf0V9QZ5iQ+/SG9UV81Vxo00hz3NIz07m8Po1bDkdm/z7NJIXkS/RaJPURmN0s/ZbGyONNBHJ9Km86HmdNuZirF5Mt6UVXVQ0JKYRPbuLBRuPEbZ+hGSbdFkZ3tnt/LfNB6WGlGzYs2Yd28/W+nLDoolELBXkrJ/Nd36zggo/T3qNPkdjdOGo3j1Or57Jd15cid9ztACxcVN9dBMzFh8kpX17knxfvAv2SX5Yl8JJNR8+PSLKWZGoQ6ZTyarl68ititFE5JfFKiIffxdvVHW23tLFcGlqpyh++flC+27xiN3D1h6rpkmw7rE2kT2+zBhtDBWf8CPxIhZb4Zf7dCq4YE9UWPnVH/nxlOO0H0hGSojtK9ezvyS2I2jb3/oRkf4msy9mrlcYnd65hVd+9d984TszOFgexg1Yfz20Z0YwWs2q6d/nK//1Mgt2nkMhaCvR9IVQ4Wb+859+wrz9xSSmQ13+UV7/1Y/4yWvv8dorL/LP3/k+P3zpLd58azo/fvFtNh3M5+CG9/n2v36PH7+3gdyKEHaTSrCRkljJihm/5Nu/XUWl3VyT37KAqEI/gSq2LH2f37z2Fq+/OYMf/vINNuRW47jWFKEkGk927l/1Pv/579/j33/zGi9Of5MXXn2V737vJdbkh8k0xSx5+0W+98vX9dH/Xaa/+w4//N5/8U+/XkmtE+bMnmX88L/+m3/9yUu8JN4XX3uV7/3818zcWkRK0MNIV9mJncyYPp2X33mfN99+i+ffWsK+vDoSE6Gh9BRzNOb8/b//itlrDlIlmwLCseDgZl777S/42fxdlBTksHLea/zTv/6QF5bsp6Kugl3LZvEf3/ku/yWbX5rxFi++8lv+9cdvsDu/lmjFEd566Zf8/Xd/y/ur9lBYD3Zt751vQM8fUqIVx5n75vP8w7/9mJ+/+qZkvMIPfvUay/YXoSHZgnSe4+KHhETjfyhIbtz4TtRGoA0wo43yJG32JWsD0tb5lzYFre6geOxmn90ITE1xtBFpiCqHlpaDplkXi7/k2fZnG4e2P/h3z1Z72Nxj322/9ItQWVMf1d2WRW3eUZBYurC9XxTInmgiim17RRUzVqocJvqhcltlVGnp7bNP08hneWNlIvjQGetXUVLTPPauXseufDWCaKwca4/lvcgc1cTOOpGVV+hZgt2AR6D+ML/6jx/wytpTuCnyUX7YPlRVcIz503/GF//xB7y7rYCEZPEod+VsW8SPfvkibyw7QJWKjHAxisFo+U6+/y8/YuaufAKi9eSDquWOh+2vJlTN/rWz+Lf/+C++86sZvKx4/ekvfsnP31rByQqPxCAUHN3Gaz//Id/679/wa8XcS9Nn8P3v/4rXpKs+KczupTYm/5sfz1jI7nO1fox58sMIwAbNPd59WTH5779m7Z4tvPe6+uNPf8L3391KvRJQ7o5F/OC7/87Xfzib0w0QdDxMZQ6LP5jOD37+JlvPlPt92saZROLKp3P71/HCK9N5TX3qF795kbdXHyUqO61iEypj3fz3ef71t3lLm6IvKL5nrT+OvplhJCAgbM/sWamYf4NX33mbX/zyN8zckotSP4YoUQ99WIPc9TP48vff4WgFJLmoXBU+cB/9sRvwNr79uFffSNIGt7UX9YumsvN946J+YXnsZftFqjb867UEtjEgMz+qpKlEY2xTHNl7WO15gd7j0jiOMUXVH/y4a4zxD3vi+TKjGqsiuqJ4+l9U8RaxYEiEJ35fj55pqpPeqOTZKyE1i9pj21i07UwjBeftsPURybKiPNGHxefrl9ERjUFKBeKpYskrL/DrFSf1rFN0TXzW7hh9VLZFJNeL3f1C0drTlxWzPSJdYV1RKYxau5VkmvyIkVo5setC/8eXGYm6pCdUsnrpRs6EwMvbwL9//3W2+5MM6ZXMiOxvui42wcr+yCW7fBwbeZroPfkXkxEhHBWX18DmDxazpSiZ7m3USYUxikYaj+hF9Od9keyYjJgvMdmyUb7HyiVbvkdlc9Ty6zksO2J0Evyx/Co/f3q+FUJBay3L5XGJL7ZIFBfKYvqsfU36ItIZkbyUlCj71q1lR9NcT043TvU4vvY9vvPCSoqsPC+Mp7nc/kULWHY0RNcOGeKOSm9U7WOvCHY+fKHdjPq06o1DVko9m1eu096I1YjixNLHrgv0Etd4Rm1s+FhJpr0LJ1sV6wuKs8Y+IZhAP02YRi2WUhFMTqFg70ZW7S6UbZIhX8ONThXuXsx3fjKXfLVts6QQa5ev4USJtSWitYfstYqUa3Z+8Br//e5OlPqRkxfZLDrlRk9rkg1vvciPZh+KcQgjT3bnbFvKexuL6di5maJE8mR72Pqg9o3oHtZdpD7Ph3/89nKSSGs4zZJlOyn1CaJqY13ijfwOXs/3O0YTFR4+m34uLj/Pq3r7HLHyZJvIaKKLNsqx9sWwjsm09LZMhMKzsayJV20RtrJkm5UZ1j2qOuuLjQcbc1ZH0Z6F/NvP5nCqwb5ZXBrliNfS+6XCLyYr1mZNvDLZVkt9lJgdXqMdVkY0VuZTXPzjNbZZxKe9wBd7j0iv1SVTL2bSs/hUd163MAnLJ8vvnbcv6sd6RHW2XEyfytP5S3plg86RhrqTG/n1zH30G3sL026/hUfH9yVSWUqZOqhJz6Z3ny5kJwVAiSQQCGiS4ehyCbgOxvChw5yvczUbCeiyE13bSDaIHCm05a5reW1pjN51HAJByRZxRqsO9OveDs2NMbbc0qLDGMkOENAdEunQoytdW6VgjwuyHdG4ss3ItiQ69egmmmRLoiA1OJJnddvLkZyPBJ8EGeMSDLgkpHegf/e2pCU4IG4c2Sd/XHs5BpGqHAw6TIDMtm1p3yoDO7+x9J5oHNfBbbzEIcLzp//gaZbpOJbG9el8GtnlunoXP5LuuLFnY4xo9OzG6AO6x/66xeC4KvfpwTgufh2yQhA7F8s3KpAvxi+zchwcY0BXXs5JThQYvaN8G8VcTOPIMsvKxx/GOLjCzBWdpfDfXYfGV1sUu4xp9MHa62iAAuvDmb17KZI+VROxmIjX9X1qpFFdwL6rPCA9Ad19V8Rw6vhJzpbFdEUiHo5oXdeRHkf6DR9pY7iIxhWd3t1E2nVsR+uMRNV+6PQ82eiIztI64jVCUDTSHdOjcuuo3ivOnObwuSrNbQ2WyHFU5zridWSLeHRKmt4VS4FEWvbtRqeW6di1og0o65fbRC+Z522XOLGCSaGzYrpzc7uFY3CDQclydLlYXFzxeEBqdnsG9OigCZte7KlytxE/NxDwbTHpLejVuxPNEy0BuI6VYy/ZLHohSVa7dnRskUbQjdFc/OvJX19mQir9+3amTWYSjgg8zyHgOvh1ujeihT2sbYGUlnTp2JJ0m1NsoexyXFf0lsfhAr3RswhMAu27dqVH21S9INnKAT69e4nPjhN7l+k+nWn0x5dnrGxXOlwcEXhqU3z7Y+W2zL7XFZ7lwIliXNEoAdC9T1faNcaExcNxRG8v14FQLVW1YekyOHp3XRdXd4M9PCw+jhsrs21jRQbSWtC3d0eaayGEjqBkhb1m3P7sF3jupn5kuVGZIR2NfK7uTbwi/92n/Gny1/K4joMRtecZHD27rpXp4KjT2DZwHNe31bF1uvy7q3o9u7rb4DX+cyOdFYaHMY7P58ouxzrkYxijsWW23Zt0fFS3lQqO46oNLY/u6suulYM9JN9x/NwbTGtD/55tyUhw8Q8nIL0xHkvveX7phR8VOK4r3gCpzbvSq3MLtOcCJo2uvbrRISsBe8R8ciTLkR1GRUbPVq7j3wOSYc2x4iVSNLY8djny1Zab8/Y72LZxXQcryXpnjKV1fVmOYyPP4Lh6dxwcXa7K0BGjd0ht1oZObVtoU0yFOgOuI157uaKPUan4/GkkI2Dl+XQuruNgorLKXOBxXBerxsPguC7WPld3x7af+mrH9q1okZlCguMSSAj4beEBJrMlvXt1pFkiJGe1pEObFmRlZojfFa6So9hBR1bbjvTv1pKAoxeBZIwjGnu5wsPBGENWu0707tRSo7RocHBkZ0NqJ5742he453KN7bLZqMyVXa7riF+8eharZbjk8hTDrnKW6ybQpndPurRMwfoXy5euz2tl2HHkEka9qDfh+PJjOpr4jLHvjZcKPdmDX2bl2fJGe7QQwRgiBYfYdRYcS4uRzhiNr5fY4anccWx57HLEZ0wt+3YexVO5jYaIyi7Y4/BxNnue9LgB5VyXrJ496NEu4/z44LiudNvL8TEQaUy5fqPyobQSWnXuQ/eWhqPblrN899lGHR62vSqL97F0xRYKvGz692hFghYPHrJMaaw8N5dQQjlbNu+nSvIiNSFadh3KnffdwoQru1J8tpgOA2/iwSm3cWXfLKobshgysAM1+QWkdRhEt2YBf9NK6ydqT56iJinCucPb2HXOI9kFrQGxi929y2eyMieDGydMYPLk27mhUyplZWVEHPktvdhJfTCFfn26EyrII7PfGO6afBt33TmVW/q1IaSNEeoraUjvxg3j7+DOSROYMKwDeSdOkNihC2lOgM4D+5Fak09tsxHce8dtTLl9KpOv649bUYerjdjS/fP58atLaTZ0PHfechO3TJjI2K71vP3Cr1l9oo6sjh3o3dblXFGYPv16kCXbQsKoTZe+dGmRRIuOfenUoR39uqZRkF9N+x49aZ2ZRs/ePYRHHi0GjOXuyRO5+65pXNOrLaGqKtI6KS+1CJFbmsiQIX1onehproPQp/EwaH1DYstO9O+cSG4hDL/2Nh68Zyrjehjef+kldsiehKCxod/IE7sZr47ckzkcOHSKQ0dOc/BIDqcLqzAJhlBFCUePnuDg4dMcstehkxw6UUBNQwMFOafZe/CU6k5x4PApjuaUUK8YjYTA/qVvTPpHf41x/Dh0HBO7G2STwXFd/911HOyGoo2tWJ9wcR0HkeE4LrEc1niXDBvzEoARjeta2Q6OkZ80yv1Quao4t28veUYyDcLR4DTyWf7/qV9ldulOzw6ZJDhilnLHdXH9y+qVTi4cCkcqq1Tm2TL9SHnFyROE0hrYvXkbuXXGH2vC2pRIa9WZ/h3ToaGYJbNmsbswSkA7pD16diU7M5MeXbrSPMHai/CBCuEfSq5m26bdlISRPZ6yNCAdRooj6gcD+3WF4lySe93A3ZMm89DUWwgeXsgL76+lMgDtupSkZbEAABAASURBVA6gg1tBEW245Y6J3Hn7ZKaNvYqWKqtWXxjQvwd1BWdI7TKEwZ2SiGqObIzBdqO0Dj3o3SpKTlkyl199I5MnSv7d4+iY4FEbchTP3clunkr5/mW8tXgPoYAhmNmOnj3akZ7WkT7d0nG1YaUUjZEtodz1TJ97gF433M6Um29l4vWX4RbkUutAUriQWS/8nFVlbZl4+63cdMN4pk64kuot0/nVrO1E0w1VR1byxorjDLxWuWHiBKaO7kZdRSnVwsZVUykMqK2toVAfdNzy/Ww8eJaI8HQ8PvYw2jQrzDnF/sZ+YeP/RL4aU8KiFYXqL+oLtk+ozxw6dIJDp4qp02ZDcU6O6i70iyOniqiLgroM9mPExyqzhcbBbYwjew+4jpoyZpyHwZEDrmtpHMW357e147ji0Xtjndzk/KH8bIzj17uNcg0GxxWPY7CHcVx8Pfalqc6N8TjSEExOp5PG/KyUgE9h575Oo67Y3cWKMio7L0fx4WoMUnOLJ4kO3bvRtUWKnpFEaOJzXUcaPTBWn4sjQa5s043zhzHYMle0ruoCuiyd47j486cmYs/DGCsndjni85A+BZcrHtdNpHPfnnRWPNr2Ns3a0L9nJ5oliciejpGeGK/rOhjfUn73IfmO657nidF7WBwsv6u6gOLEqNE7XjOF73x2gvpKsqQajImJlck4TpNOyZINtsYTges2lTvq6woejHS5umy56/vuiN5x9B5wL2pDxcWH+aWVpsOzD4ZwWQH7D+fjSgaS7bhNsh1i+c/guC4xOyRfOiyt06RP70ayklu0plPrLJo1zfWC4mnU17xdF/p1aYG+y4BxcZwQKb3G8p/fvJeh7dOBC7a70mXb0zFG5RedxiGjTXutc9Ni8xhVWVrXFa8uSy8zVHrhdBxXeLi4VqYu1zFoeiO/LI+LI5mu6+jdIyp99tm1dCqzhIHk5nRs35LMlJSYjGBA8mLy01t2UNy0JlEyM1u3p33LTLKyXJ8uQb47tlFxNI/SmrNdJn6vkYGOxc2N6Q8GHel2adulG73aZ/iCjX4dxyWtRT++9u1nuL57M7yogyO+QKNtru4BV7yN+Irl/BmLGRc3IRW7nm6VniQrEKUj2x3fPvd38BrpsHWu6+IID5krRg9zUbnbyBvT0yjPMVh3m+icRnqVyj9LYy9Xuh2sfxLoP7vS41pewBhH9rnny61/juoc1yWgGGuiS2vVkQHd29CYhiSqSbaLpfdAd7dRlu6NvBquMKae3XtPEzIO1rYLbW5lxMrEfsnpeQbHsfWub5udj2CMnmPv1oeAbJSpXHoYHFc0jRUWm4DrYADjuI32OY2+qdwazqfzcP6Sbnk+pFBWfI6z+RGat88iIz2V9D7DmXhVH7I8qCosxdWmSWYi1OcfZt6itaxev5kPZn3Am0t3U1gXs/D8V5eaAtavWs3cFZtZtmgxb87fwIlyfE02KeYe2MGCpatYtG4/JWEDtWdZsXgDu46cZNuWzWzYe5bi6noymqX7G7kFh7Yze+U+KjQhjpQeZ9HCtewrqEHZl4pIKnbDUHMTjAKk+OR+lizfwMKlq1m99xyV5eXUqDNnpsRgFAlW/6IV61m0bAtHS8LEYkyOgt8RMYb6wpMsWSwZGzew+Xgxnk9kiFac9X2bv2Qd20+WW9JGJjHr9DRQ2b/e85QywOA2lLF9w3ot+NaydNMRqtQh0OH56hqpTJgTu7ewYMkq0RylRnx1BUeZN2cxy3edoaammv0bV7Nq31nKinJYuXAFi9duZP7cRby3bA+lUWEYLmaTMN1wtNTXfGLHBuatO0C5MJM7nDu0k4WSbzEv0mQZ6ag4e5jlK9Yyf8kG9pytoTp/PzNeepUXZi5mZ26VOqBD0Ym9+gq3nsXLNrBXNFYWMeNpOppeK3MOyqYlLN9ximr5V5Z7hKVLNnGooN4ntYtj+1BXdJo1K9ewRLpXyz8bAvm7V/Dz52cyff4uTpc14CrZ7N+8wcdk+Y4cInLRxsGsJRtZt349b7/9AbM3HtdCSfF5cjOvvPQaL81azt68OiUXw7nDu1iyfD2Llkt/YUOsjZsMtUbQoMX5ZhavEs3Kbfr6GYmV1jdokSzj/bfYj88mx+uLTrFq+SrmL9vIPvtXJLa6rojNa9ezaOkaf6FfU5LDu2+8wQszFrLhWCladbJvyzoWr9jA4rV7yauxTGBMlBM7NjFv+WZWL99Dvr+JCRiHwuP7RL+ORbZdztXEbPdb1WCjOFxZTI3JoJU6ZDRUxNrFq1m+fguLFi/ktffXcKioAUUquWqvjNZZJPquOTj1JWxZK9xXrlPf3MrpygYqSmpIy8zS4kkAE+LYnh1+rC5cvoXDhfWSo/AWJvV2gXExLD4oYKI17Fq/lkWrtzJ/w1Eqo/iHMQ0c3r7Z7+dLtxxTPFj5kuXX2p8Q9Q1hIpro27dw2Vk2rlrDkpVrWbH1BNUxzQo1z0JCpKqYCjeFzDQlIbXdvs3rmbN8K2tXruTVt+az6lCpOBrYv2k17y3cxL6z1RIbIe/ITuav3ElBvcGrPMvaFasU7+vZeboCYwzRynOsW2OxXsW6Q0Xa0yjkg7ff5DczFrHmYAE1hQWEk1qSpY0CCcQxdRzduZWlazaxYNESps/ZwNmasHJYgexfxzLlvSWbjlLlY24wDSVsW2fL1zJvyVZOVUQoLyzDNG9OiiM+HKIVeew8eJyTe7ayesdp7AAbKcth5fIVLF2/ncVz5zJjzlrlUF+oHwnWlosvvznkT9XZwyxbvo6lyn+Ltx5TvkF+1nFg60Zszlu8ejd5wsJ4texV/1q+7Tgn9m5j2Zb9bN+yg1UbDnH86C7mrz4klA1lp/axWHljofjOVnmAobbwJKvVTvMXr2PHyQrCFbksU+5ZsHIzi+cv4J1lMbwNYEyUU3u3stDmW7XXiQqVEebojo18sGyzsF/N9LfmsmR3PrHQMYSKc1i2ZDWrNm5iw5EibUoZDDqq89m0erXaby1b1LfkrgLK2tR4U0H5yQNYu9Zu2Mh+dbZgIEKkspBQMIPmyf40knDJGdYq1hYsXcfmo2WEa/NZs3QZCxTDixYs5u2FW1D39nVa++14sUT22z5xtCTil5/evZG5yzeyZvVa3n7rAxbvPE29TES19aU5yhOyc+l69uWHMZEqtq5Zyer9OX47rFTOa0CH32j23kBDKExjVyDv0G71wXUsUs7dfaZKBDqbaPVYcHAbcxevYdnqDbw/cwFLd54hqvGpPv8oS1euZ9fxHDYpv+7Ma8B4NezcsM7PJ0vVtmVRVxKgPhyhviyPLetWMf3tJWw+Va1I9CgsrCYjS/nAAh4NqX+GOLxpObM/WMisFXspFxVqvwLFcVpzbTxEJM44RBQDa6V7hfTOW7OfkuoKCqoc2ujDld+umgTWFp5gjxb5h5UPNxwsBNlcee4IS5W/Vm+UT+9/wNuLt6uvSub50/Pj3RjlJjumrVQfWasYrnNRc2N/zh7awSLFqD+2KbbR0RgVepIaati3eSOLV29k7pL17MurxfLVFRxn5Yp1LFmmOFDfN7KnJvcgi9R3Vq7fyrz3Z8uebRQ0qI+W5/LuK2/w4tvz2HK0iEhtMZuU95esWMvSzUeptDgAhjoObdvIklXrmLd4IwdO53Fw7Vx+8cIs3tl0gKIag+tVsGP9epYoD9s2qbDjN4phXfb09GOMx9n925izdBPrlm7nRGkIYwz6oeTUAZaofyxYtYMcbTarVHkS/1CqVptBQKvzYJu+jBnSmZwtazgtXAJqA09temrfETIGD6etJvcJ2hjzGSUk2nCO/SWduee2Kwid3M7RcxHSO3Rh2NDhtEpIJiU9nZTUFNJ0JWpDo3vvkQzWJmnESSRVZSnJSQRco35sSE4IcSCnig79JnBD11q2bTmClywlniEYbeDcuRwqvXTatU+jeVoqvUeNYXinFii0sD7iH4aExBRStIBMTU0jIyOFoJNAt+FXMqRFItVuS0Zcdhnd2qSQlRJk6/L5lHUaz8PjuuCFIGBt9nlTSU9PIVkfarM7DmXUwJaqr2TpwrU4vccxYUg2CcEUEhJS6DP6Rq5sW8L7CzZTr4VGakoyKZKRnBhA1vtWucFElaWSkpiAG3BJTErRu66koELaISEpxpOamk6m9AZMCgOGDadHm0z18UCM1soUveDiYw8nQHKyZKakkpaWQqLoBwwYSjsKOFFSh3HRYSNFNyFunxIix5n+k3/lK9/6Jl//53/ki9/4Kt/43i9Zf7qemsOL+Nd//ipf/qd/4Ov/8vd89Vtf4Wvff42jeeeY++K3+dy3/oF/+o9v881/+ju+8u3/4NXlh4kmgv2Lz4jfga2e2NWUiirOHmX5UsX4qdOsWrqBA8UNGFPDbuWbBYrPNXtyNZYZjPLfnq2b/Lncsi1HqQw1cNCO3+qLK1eu5M33l7HjdJV4Jd8YaiR3meZT8zUHOVaq/OkXN3BM86XF6jPzF61jd04xBSc28svfvsPrH+zguHS70r1H83Xbr5Y0rS3E22SvxUjiKTi8gzmaW61duYUjRSGMIyJjKD9zyB835yvHnCiLYlTcxKv0jN2sFZnykUPAVLAzN5Nbbh5Hi8r97DtRSzDZRXuFOG4AJ9iMkRMf5vqMo7w6cwPVAUNSYhKpyYqNRMUJ9pCGSBG789sx9darCeTu5PC5EIFEybHGWhJ7SWlCYjKpioFU9QPb/zLadGZUj2zK83IpjUJiIJFkW694yVDMpSQkktWxGyMG9yctDIGmGFU8J8gWK7bpMm7A500Rb3pmKlkpSZiULlx9eT/0KH8MSa1H8Pg911G89m3m76skOdUlQf3L2pQQcPAPgeW40HDuBDm1UbJbptG6VSptOwxizOgBpMjdgp2LWHM2jfETR9FO9ibJr7TWnbj9xhHkbviAbRoWKsryKSyFrLYZtGuWSoueV3D9kK4kS0lE+cORutqivRR0uJm7R7Rk/469lNeD64rg4lP2eKJ168tZ/OI/+jH+dzb2/+GrfPXffsTKoxWU7pnF1//+G36f+Nq//CNf+/sv8vVfziK3upqVL32HL33r7/jWv32bb/3LN/nyP/wLzy/agVIuIbsTfbEuPUudfqE+76jWWqu1ttjIwrnzmbn6AOV2kaNaQ4PmxZtic7FVuzirtZjRh6P9WzaybOtxTu3byqKNRxvnjx6+TOMQKjrByuVrWLZiNYvWH6aktoRtmsuuO1yieISTOzcyd+1+6ZESzfO3rVmLHYOWbjioNXYMGH++bXfwROI6jvrBbuarD67dsIXFmpudqghj11izV+6nUjETLjmutfY69hfVQqiUymgqrTKC2KFOJpGzb5vfp5doXlGp3lJ97jBLl23iwKlTrFReOFhcR9PRoLXTymUrWL5hOws+mMOMBZs4craIfRtWM+OdBazYVxAjVaw3FJ1k5bK1WmdJVkG9JIMxDRzRuLrA7ims3ImWJtjcWZRTRWrLZiT5/cWoLWu5AAAQAElEQVTgyveta9exROP68s3HqMZgD7/aPjRdPrAQrcxj89q1LNWcebnWHVV2oSmesjMH/XnBwmXr2V/YoAzb4Pu77kip3yZNUq1cmUz+kd3SuUHrnTWs3X8uNoxp7r9Vc3+bj5ZtPEK1Ba2mSOuE5Sxat4NlCxbw2qxV7M0p4ujOTbz9zlzmbT6lCLFGGkxtEVvky1K1+/Itx6mRXbam0XSoKWThzLf51WvzWKa9gfraSvYq9y5etZ7Fa/ZQEDJQV8KmVTF9yxcu1PpwhXJnIcd2buEd6ZvTuIZGCS6qtdzBdSv5YI5iduVBKtXXJIA8zXmbt8zAKM8gZ/OOHuXYqaMsX7sHQQOal69ZsYIlWqsslU/TZ63ksPKxtdUHy3/Qj/ZGGrSOjKJOST3Hd21RP9jAIuXb4yXKwyKxH0Y83a3ew8r3cxavZ+WqVbw1czEbjpahKRqlJ/excPEOTuQcZani4WS5wakvYN0q245rWKm5eNjPcQ00hD2q1B9XLV3MjJmrOaK8jo6CklqSszMJKpijDRE8tdX65Wt5//15zN1yinr5SaSEgtok2jVP9tvTJphqyVqu+eLi5atZqngpr6qmwiRqnA9iDyMDi0/sY/vpQnZon2BXbg12z6lI69DZ2uNat34D777zAe+vO0qNH2v4/bcJJxOpZue6tWq/rf5/oVgdA0Ow12teucnvb8uktykWrE7bLChqcvZvZ5H2vhYJk8ON/UaMVGlOu1T9aeky2bzpmB9HpjqPDavWsUBruG1alFl3T+3ayrL1+zh89KDmwDsowRDVOnSV5vQ2Bhes20ux8kLt2QPaN1qnvr6ejeoPijLKju8k5t9GZr47h9nrD5Gbe5q1SxfymnDfW1BnpXEuv57MVlkEJAccGkpOK6+tZv7SDezNszFQz/m1/aqVWtvPY+XBYvV1j9zNC/jVy2/yxoJtnKo2OA2lbFu3nqVqt2V2ze3HK41QxoAzWjeckF92PrJUNDVytK7oOMsWr9L6cjvzZ8/hTc31ztXE6M/3rXAZ21euZs2BQj/f5ezezNw1+yhTrJzasZEF2ldarbXfm2/O0b5PruY56PBibamnT9Npe+tfzB9HCx0LfYseA+gR2M+//N1PeWneFnKrUsjOSiPoKkw0SZr16tusyKnHCdax/P33WHK8gaysFHbOfJOXlx31gTca3PxNaCfI6XXzeHP1KTL0RS16dA0/+MVMTsuL8l0L+O3cQ2RnN6dy7zJ+8f4eKQhwbOX7/PL9LZwqymXHgTJM/m5+/vIi8sQTLD3KjLcXcrwKjNvA5jlzmbOvxL6wf8n7vLD4qIIbSvYv55dvrKc+PY3MQD0Hd+4kTzOiI4ve4PnlJ7DH2Y3z+PUHe0kWTUbdMV7+zTvsrbA16myeRQKihUf41W9ncqwhjfTUVJIcNYF8wqtg9oz32VWeTsvsGma//hZrc5XBjCHSyGuMwfaAqN8Zypn/+hssPBHWpn4a+Vvm84t3d/jJzAh3SyOtnFgzlxmrcmmR3YzCTfN4YcERXE0oSyz9zF2EnHLWrjtCrQmqvIa1s2ex+HCIzMxMctbN5L9f3wYmwNH1C3hr5Um/LSIFB3j9vZXYf8Kk8uAyXpotn4V59JAwn7kHL5zDG6+voDSzOc21iXokt1B8QarLq0nJTCdFDV98cDW/fGsjdRlpZDiFvP3iDDbZ/+5JPjb5i3+o4wk6J6Ge1TPfZua2ElzhEaw5qUlVLk5QmIhORfaXnNO5lNYEkTkseWsGS0+ECLiVFGtTwU6eEwKwb9E7vLOtwsdk34K3eXVLKYH6XN6eMYd91Sk0S6th7vOvM/dwNa5sraqsJVV4JAcdzu5czK9m78SzbRzK4bXn32RPYVS6zfl2ikbK2X8gnxbNs6g5sIJfKfYsheOITEjYX3t5dkVhH8pOMn36fHJMM1oE85j+8kyOVEXZPf9tFuckSI7LgUNnccIhGqpriKZlkJoYoF4z6VNni8mSnvIt8xT7e600jix5m18tPExaeirpiQk40uMkQf3hNfzizQ2EZXtmQiHvPv8G67QoQRHuCcCocDZumG3z3uG1NWdUHODE6rm8uvIEmRmZVOxewE9eW0WZ6AOVB3j5+dnsrUJHMTNfms7SHEOGZIfzdrPmSATXzWX6b99mXb5IvCL2HC8jvXkzvLNbeP6N1RqEwDix9uWiwzO2TcOsefs13lZ7Z8qPVDWczQF2KM5Z+T6vrRW+iulTaz7ghaUnfW6FvX9H9hlQVzF6gsK8s5xTsNq/tNk4ewaztpep1hCRw9ZvnADHVrzHrxbs98vr9dHhhXfWUJ6WTlb1UZ7/6VvsqXNILj/CS/L5kDZLPRzKD+5my+lyXE2gZ06fxf7aLFpmVvDOK++xq7icDe/NZltFJtkZtRw5dpZQNKBJaQWhlHTsX2c7Ti2r3n2L93cVSy8cXjqbl5aeIlP+Vu1ezfJDNbTNTqK+9AQHzwUVB6lsn/M2b2wpF30p7780g2WnIj7mFUePsUex4BqrfzobctSY5DP9hbfYUphIUnIC+xa/y/PLTuK4CexdOIu31ueTkZVFzsb5/OjNLZpmgFEcWE4aDx8fA9XC5BevLqMgIY2MjHoOHTzAmQrZvOAd3tggOWr36KkN/Pz5peQbh/DRtfzolYUcOneafUfOUXJkB795YRY78grYu/8kObvW8co7W4iqo6acWc/PZ2wSPoXMnLGIU4HmZJtqjp7Iod7aumgm8/aUk6E+WLhxDt9/eTUWgdxV7/P8wiPCK42UigP89pfvcSRiSCjYw0tvLONcIJ3m0bO8/vPprLMQV53mN9pU2F+drNybRrLjgNreoZYFb7zHxsJk5d4wC6a/yVL733EpDsORCLpRdWwDP35lKRVJaaSlJZMg1ojdcNVG/9r33ubtHSVCrJI5M+exvzpdbS5sdh0m7AQ5sOQDZm4tJiMjg/I9S/nuryRH1Gc3z+U3cw+QKOwya47y/K/f43AdBKpOMH3GfE6G0miWWKo+NJ3lJ+ohksuMF2dzzGtGy+RzzFDfOhRyKNu1hJ+/sYLTp8+x/9AxCiMSbqKKf3s3+kHPju717N+XQ7BZFmlVR3jxtfnk1KhYJFH1BT1h6nJ5780FHKtLI6tZmMWvvqL8mI8TDLDx3Vd5ZflRyk4dYd+ps6x8610WHKj2469st2yYvolqCQkq39Q3hHDS0nBzt/Lz38zhVBSCkdPKl8K5SESOo58IJphCVlYGp9bN5ruvbiKEIaTNn5++tpgSTySVJ3nhhffYXZlEenoiJzSBPXWuQVBs4Gcvr8SKongvv35xHse0q5WUEmHVO68zc2e5cnctS998m4WHQtKRzM75b/KrRcck1OLhEZVNhqja511+s/gEKdpcyUgOYKIREhOh+vByXp61jyTFqHdoBb9+Z6siReyRKBF/Y7eWtW++IV1lZGijM5h3hG05pVRrcf3iy/PI8WxfibBC49B7e0pxEzxWv/eOMKsns1ka2+e+w68XHgfXpVgfRgNJqX5ej+YeZm95osawRHbNe5+3tlgvo2yZ+QZvbCqUrjRMcT67D5+ltqGeGsVYRmoygUANq2fMYO6hBmweLtmxmJ+8sR2FFLZf279MlPXkbp3PL97bhZORqg3fRFwVuklB6s9t58U31xNWXk0t2cELry2lwLaBP02GSBgsZo6JUFObwODLR9C8eh+r9laSLLxq83cpT3bhyu4pVNeL2PIqFowU1J89SGV2b4b0HkrP1BI27S/Ckc2Z2pSKKrSjEhy7IoTEmpiSpTaDUCiq2PWkV/eoDLWLv5JCSr0IWe27cs3g9uQf3M7xakg2YaoVA33696Fy66v88/feZO6mk9RpvGyTIQPFb02SFP/0ZJvVGdYGTkIgzL79uzldlUDrVMWgm0rzrHTSkiBn2we8t8dl4uTxtNZ41SA5QsK3KWJBkX/FB7awtzBMs9aJePpYc7K0gfZduqP9H+x8xgt7Wpwl0KljB2pPH9SHOhTpkmL9vsgoT/nX2mTnunrEs/X+hX/E3iNIHEFttp/Yt4Xj6qst0hKICjfL5/PLxovE+rwX/8RoIkREF3Tg+MEdFKb3YmDrFKyt+NZx/jBOgIRAMp36Xc/X/+Nf+MEX7yCzcD/L1h2kOiVDm4bNuHzSF/jZd/+d//rO9/jPr9zPgCwJDyTRqvcNfP2fvs1P/u2LXNWqlg2LF3GsFhzlHKv/vBI9GM2PrN9usILFysNvb8zhzKHjHNMYvmXB+5qb19OiRQqb3n+HOYeLObV6EfN2V9IqO42yM0c4XWkI5mzgt29vpkEfNZrXH+e3v3iNdQUSXryHX766ggrNQZpVHOLXGs/KoxH2L3yLV1bn+n0mUFHCzt0nCbt1lNehXJ9CYjDEhndmMHN3tU9TvX+F5kEbqJJIf64diSFdYHOgxtGI5rTpChwbqm5CgvYa9vPS9JVUZTbD9peXXl7IGbUVjf0qrGcfB8UjQQjnHqA0owO9ug5jWHuPnXuPUSUVAeV0qSTaUEt9Qgcm3T0RZ9d7vLe1hgTNj8ISYtsfAWgC0HBmP+XZ3RnQ8zL6Na9iqzavGgyauUiYFdR4edJr4yEifltj/xhiw+k6BowcRQcD9SqMxZ3aU++VJQdZv6+Y7OZpikEUQ57fF6wMqW6U2ngTry2Pqp944o0Un2HHnqPQPJVExZ1Cm4baWloMmsi9IxKZqw2UY9WQ6EaJaOw9L0+DcDQEKT2H0TW8mx985+f85oNt5CqOmrfJVt+HI8dzcTI70iHdU16UYuFbV+vRrGV72iSVseNgA516DqZN/Ra+/91f8sK8HeTWp9JOG9kB9WuFI5q6UHikgBa92jGo/0ASC3ZzsCiC6xoLa6NTF92cAIkJQbJ6jeGr3/w23/3qU/QN72bW6j2aTyVpbG/Pzfd9iR9999/UL37IfzwziY7pymVuAq20+f3M3/0rP/72P3FnrxDLtClxNC+Ma6wua/8FPUaTW1tiTD1rZr/P6pNh7HzouOb6P3p3p3TB/jlv8fq6s36Mmtwt/OI3izgnh7wT6/mxxqP9Z3PZd+AEBRVWrmxQe3iFe/nFb+dx0kv3+Y5v2k2+NmRPbFrCW8uPaXQUbdEhpr+3jLwGxUJFPic0acjWmuPAknd4fU2uCMDGnY0R+1KofvBjrZXDGWmkpZSxQOvajUUGu9Z+/Z1FnKwGo/61QRs08w6U6iWguelMXlx63M86ZzfN5zWN01kah6p2LuLXsw5qLK9j8cvTeWdzLrkHj3DwXJEyBdhGMfpQt1tr9TfWniUzK4PTa2bxvReWUBhM05z9MC/9cgYbK0VbcZTnX17E2WTNn8JnePnVeZypjXJs6Xs8v/gk6Zp3pycn4Alr40Igepo3nn9H82fxUsYHL89geY7j45S7fg4/n7lfEQZGgWPbhsbD9if7WH7ijC8/u7n2MjTWv79DvoZP8rZdG2cpF2g+tP1QJUbKDq2cwxurc0BtqBzb6AAAEABJREFUAh6eP7+A3O0L+fXMHdj1Zma0gn17DlKojrDy9ddZfBzfljyt63/yzm7Qx56cjQt5af5eXO2NVB9cyfd+9h6HapJobgp577evMO94GCjlvVffQSlP/Omae33Arz7YT0g1Ri1u+ymOS6i8nPqUdLKSg9RWF6p/lWHXtGfWzdK8W/Mo0ZzZsoSX5u7GSF/t4TX890/f1fw3gWZOMbNeeJV5JyRV/cPGhklOJUvtc2rVW3x/xi5h51BzYi2/fm21LIKqQ8t56b2dmIx0okdX88s3txFS/zoubF5bfgo7fy7etYwfvLaWatmKMZzHXc8Wt6gwoKGYw/qI6ChGXY0Fv3ljA2WA8RTzaitwCJ7dzotag5bbMcLJY/qvX2bxSbW5k8/MV99kzq4THD6Ry9mje5n92ntsLEkSVmnYmH9+oQhJkO4wdZrnpGVkUKQ4/dH0DdQj6ed2azNzLmcb9Kz9CS8CwZQ0mjULsOW9V/j5EvErQedtna952Hafp+HMFn7x0mL89ZU+wJ3WJv6xqEPBhln87INdkgoVB9bwC/lSn5RAilvAe8/P8Me1QChfc/bZ7ChLpFlWiMUvvc77u4t9HqJRPB+bBla++Rrv7iwlQ3PVNM3nERZBUZ1aNpPp64uwe0THV87ipeWnVSo0m5JvbSnHTp0jJDxTC7epvVZipYdOruPHL62gTL5lZAT04fcAJXUFfPD6bLZXpdIis44Ppr/L5rwIiRzjlZ+9xaoT5eTsPcyRMwd46cVZ7K9LJyPd5dyx3ezL8yg7cpSStGZky7+3X32PXcpVbrSEeW++w9ozEbLSQ6yY/pLm64cICPfyvQv5ySsr8du3aCfPvzCf4wo5ak/w+ktzOGnU1xNzmfH8uxyqd3BzNmtusMZf2zevPs4L/tpe7dPQoDEjQlpWKomBMuZPf4clit0Mrc3ObZ7HL2buohYUclE8z2CPk2vnam8kR7g159y6Ofxm0SmcQIS1muN/oLlCpmL94LJ3+dmc/ZZcvJ6FHIzDqa1LeGPxIcKAU3aMN95ZxGl9MAxU7+WVlxeT66TR3Mnl5d/OYHVOSFT446z/8Cn6cf6ivhhHnd4jkNmdz37jC0zqFmHd7Df43Bf/hZfXn8GT8pQ2nejTJZug4xBsbp/b0WvQZVwzZiwPjGlLwanc2AJKgwL2SGpGn27t6danLyNGjeLeZyaSmbuTrSdK2bNxmyaXvTVx6MPInpns1XtlYgv6d29L2+59uePmyTx9Rz9aa+HSvU0qGGjWvjPdW2nSLWOcjPb069qKZEcVbjN69+5A85REXGDLwtXUdL6cW0cM5Iox47j35hF0TM+ke7eOPg3UsHzJJoL9x3HN8EFcMeEmOlbtZdFWO/NV8ETASOyR7SvZW9eJqbcOY+jA/vRonYqriuqifazeX0fvkf1l/0Ayq46xbk8e/hH1f2M/noeT4MDZfSxWkF97yzWMHD6E+8b34eTa9eyyvQSD30eilaxbuxOvvTAZ0IcBnRx2rN1BSfOOPPClJ7gisJefvrqTYVOnML5fS9KzWtK1a3v69BvK6KtH8YVpIzm7cQMHnQxh0ZGsBCPJ0LFHdzq3TCVRmO1Zs5HizG6MGCAdvTI4tnUHOUXVnD1TSDitM6MmTuSOEZ1Iy86mZYtmdBs0gl6tktm6bA1VbS5j4ohBjLruJoYlnGOuvqBJJK4So72jwxijTBgltfVgHpgwFMry8NQ+9ZoUjphwFT2bJYjKw04QkXU9hl7OjdcNoX/PXrRNruX02Tqye7SjeUY2w6/sSZu0fBYvO0hmv8uEszBpUcOaDUdp3qU7PTu1Y+DlQxh3y53c3NPheE4FyYqP1s0z6TXsMrpnG9Yv2YDpNoobZfcVN42jV8MJ5u2ITRwc2Y0Ox23JzVNukA09GdKjJaW5+f4E0ajuwukpkTlY9/KPbWbLmUQGjuzLYOHIuX1sO1XEuXOFlIcSGTzsSp6bOpykVq1p36wZHXoPYlCHdBLbdGDc+HEM7NOdgXo/m1OERzmLF+2g9ZUTuXZEf4YO7UK6BuXEhChbV66mou0V3DJ8MKOuvYUhyadZsOZEo0myR09uSmt692pPetDFCWTRu3t7uvTsz6hRV/D43aNIyD9DkSZH7fp0pWuLTDRfg6MbWXjQMP7Oq7j8ssFMumsyYzolkyr7enZuQYpjg78tt9w0msF9ejKgZytqivIoj1qFl6KixsYvqTzA/LVnGT7hNkYN7cuIfm0Iei5BDQNrVuwhoetgBvbrw9D2hq3rd1ItUfZsihv7rPQjPKBtr6Hccsso+vfuSZdMj5wzdrgipkf9yU3JlJ+daJVmgKD6dCe6du7EFcMv47ZHb2VAYhEHzgXoeuPt3DowwIkzdeKtJ8e05babriSzfBsrD4Tpf3lfBg3oR0rZUbYcPM0JTZKrvAyGXz6eh28ZRFpmBu1bNqdt134M6dKcpJbt6dO1NWmJQekNs2PrQdL6jmDEkAHcMn4EzWrKORfySGw3lKkTBtOrVx96NY9wtqSWhuO7mX8owq23XcOIYUO45/4JXNG5OWmtOtO3QzNSkgNwZAOrz6QyfvIwLh9xGXde3ZLtizeQl96aQWrXbn0HMOqqq3h24hDqz56hlI8eUXlqNDXdtWINOZlDuPuqwYwcOY5HbruOzhxnzorjdL/udmE1CNv+3sHVbMpJZEDvjnRo05YR4ybzuXtu4OoBLWnVrh1DrhrH154eS9HuDRwx7RiteB88oDXn9CX5aEEN+Tl51CS25vJxNzL1un6kpGbTU/m+pxaDo0ZfybOPXEX1rk0cKChi/ZpdpA29xdd99aSxtCzcwvK9hk69u9FFGz0jrhjKzfdMYnizas6W1yplrmVHWSumTBrBMOXe3u3TcOVfTflBVmim01N9elD/QbQMnWTdjlwfDC/i6B5R31lJUYvhTLtmMEMG9qVjVoI2x8BNbUvvnm3JSBTeXohTx8/QkNRKGI3m/knDSUpsTvfuXejduz9XjB7FE49fp/6ymZ25pZK5k0D/0bHx4tYb6FK5m4XK6e169KV7x44Mv2oI4ybdybVtPE7lV6nNt7HhTJBhl/dm4KD+BM7uYMPpFPX/brRr05lxd0zm2TtG0drIZOuZf7fP4PibF4lcdcsNjOrbk6F9OumbZxElNbbe2B//atG5h/JgRwZrnLvuuok8NDqVdUu2U9e8C/26tKdz9/7c9NCjTOmcx6L15xg6/kZGKZ9MnjiIgq2r2K+uZUMvIbMNlw2+jGkPX0f7mjOcqjJkdelItw7NSTK+Kk3GXLoOvIxrr7mSz981lPwNa9gVdenVswedspNJVrfI3b2KbcWtmTJxGMMvG8kjU8bSp0M2Hbp2or0WdkkSlbtpDftDnbh7zBAuH3kNE3sZFi7eRXKLbvTp2Jaeg4Zx1XXjeODqzhTqA6VYdHoIFKjLZ/Hyg3S6ehzXXdaPIf27kpmoam3G7Fu7kaLMrrGxrU8mR7ds5bhWGMYJawxSpsrfx9z1eYy47SZGDRvITXdP4faBLTi7eQV7Qp2ZeP1g9btrGN87wvIle4m27Ev/Lm3pM+QyrhpzA49c14m8Izk4aW3omJ1Oe427/TpmkdBtBHdfP5D+fXvRPStCQUEtXvUZZq89xeAbbvZz7C13jOemUQPolp1JZotOXDWgC1nlu5mzuZxRE8f4NHdN6k/hxlVsKQHU/mGNm4ZSVi/eSmDwGCaMHKCxpTstU10StAw6vXUjR2nr+zusTzvy921lfz4YJVKhhT38u30P15LUaaRyToBdG9ZTmap0s/kQLQcNpDlR/c9Ycn+MCyZ47N9brA/PhZys9WjTKpFDO3ZSFfKwm1PESH36ph9Pm086sePj+TI9aC1Lfl4xpYUVhKJF1GrO5hQfYt/xGpJSXEKS33bobXz+yTtoUXWAmS//gG997xW2F4QJ2C6qfC8x/ul5jspq2WM/smpB/96qnZTWRfGi8jIaJawbNTm89/5aWl81met7JlJfF8EWexgCxiNvjxbgb7zDmwvXc64qhN18iNbVa9MuSjDR9WkRraIFKzdBG4WBcA1Vdkjkjzw8hKtDIhXs0ofXl6ZPZ9b6PZSEZbMF63eI8+SzWC/U6sVzE7TwOcGyhe/yo3//Z364qoFpjz9Av0wHram5GPcmRvtvejqJmXTq1pnenVuT7BrC4RANUQh4DZzevYy33p3NW+/PYu1JQ6sUQ0NYeDlJNGuRTausdBJcD+OoYxtoBEcPF522XK+pLdvStWNrOvcaxn3P3cctXatZuvQIrYeMZFC/AfRIK2OT4i2nMI8zJS69NT+bMvlm+mUFad+5M126dOOKyy7jxmkPc0XKGVbuOM25vdvYU5POqP69NC/qRN2B3Ww6doy5a47Ta8zNjLpsMDfecTMTr+ijTclmNM/MZshVvWkfOMqcNfkMuWUsl4tm0h1Dqdq+BnV9WeoRNgZDFRuWbqKh99VMunwgg4f2pE26S8CEyduxQQvtlowY1Jsh/bpQdngbu3OFg+1HF4HgadKelASHDxYLzxJtrpaS1Sab3L07UPon4DrSB45wD2nTNr3XVdx3fQfWvPemxjVH/UuQevYyJCTCgf3FaqNiTtfW0bJ1Osd2bae8AVzlAV/Q+R+DY0IcWT+L196ewU+ff41jpgODurWCEP4R0Ny/+OR23pvxDq++v5B9+dU4ikaFnl//4R9ZgSdbfPfcBLyiA3ww+z1eeGeW1mrFRL2oFtQxLkOE2oYAo267l0EN63h1wSFCwRQc9S8roonKyopm9OXxZ7/AjR1r2bBoOv/6b//OjI1ncZM8KmtlbDBBMziDoBSbwT6YYJCEoEdteRmmRW+eePZzXN2qhvXzXuWfxf/utkLNdUUrDqc+h50FDqnlRZQntaCVW8SWXXk4jiq5YI19a7q8SAhPm52dO3fV2NmBTNlSrw2NaMRTO1azb/Ny3nlvJjOE26Fz9aRnBAnVy1Y3iUx9DGnVIpME42DUrrZtvJjxTeIvuSe07ETfrm3oNXAIV149mqenDeLMxk0cL8ll9fojdLz6Nn8+NG7q1QSOKkZPJtCvdyfNxdowbNztfOnB6+mUAVF9qHckee+alewx3Xn4hiGMGD6MR+69gZ5J2XTSnKuZ8pdI6KD1XtcWqWjfk8SWfZhw+1X069WT3q0TyNWaw9LYxo56AT2GWL1oPZFeY7jt8kEMGdyXrq0zCAZcmnXoTNNa281sTz+71jYGAs3o3bs9zVKScCRh65qt1LRSHx3Qh4HdE9m7diflLbrSp3NLOvcewX2fuY8JvVurz6EWMQSzOtCza0e69R/KlddcyxduG4LT4NBX/kx+/Db6J1SQWwZl+7ewpTiBkYN7MmhYV5xj+9ig/r9QuHW9+mauGt6fywZ2I8tRn1ZOy+rahZ6tm5GUKKPObmXx/jDXTLqCyzXfmXZzd06sWc0eu/AQveeJpvE0jus/NRs4jJuvGUnffp3pnupxsiWmvEAAABAASURBVKAML1LGweOlpLboxcgbp3HvqGxMQqbmPJ1poYmTL0bCNDRhvArWLtyA2+8qbhw+kCtvHM+94y8n48wW5u4OyZbRfj6659ZenF61ij0JWQzq2ZmOPfoz5krFxr3XkKUO2m7EYMbeeRfXdoiSq3EwdHovyw83cMP40YwcMUTzpm4cXLuK09YXxaHUQ1JzurZpRstOfbisW0uyWnThlknX+e3ev1M6506cBdnds3tnOvXsx3VXXMmT919HtvS1UY4cO+Uuru9iOHVSEyblm0g0SWv/oVxz9VV85o5hnNq0gmPhBHoP6EnHrFQSHNi3dgP5aZ25XO0+uG9zTmxcz4nkbAb16EDXXv0YOXo0n5kyEqcgF/9vvizKPmD2wV4GR32RhHZcP+46rurTXXPz1tTnnaPM0kkHihgZTtdunejcsSsjhw3THPtBbm5bwoLVB0nv1IVu7VvQaeBonn10MgMiB1lwOMikycOF1VDuG92GLcs0L8QlEPXIaNuNy0aO4snJlxHKO0ENqL/08vd1jJ5RngsHMhgyfAhjxtzEYzd2YM+yDZRpT2Vg/060SE0kQXQHly/nZHI/pl09mFGXj+a+ydfQPSNZdnahVXqSKDw2r1gdW7+rza4YcwtDk08xb8NZsrp0p1entvQfOYwbxk3htgGJnDpVKB4w8tcA0fL9LNiQz0jtx9j19GV925Cg9XSAGlat3EdSjyEM0of7wW2jbF63C7uNZPl8yJJbc/XYsVxv8ezTjmj+OSokc9vSFZzVmujOK4YImyt54sHrSMjZx7LDlQwY0odBg3vTsuQoqw8X0qZfNzq2acPAUZfz+BfuIGv3WjZWdWDq+BjvA3feQv80Q9urbuLGQd3pN6ALGZUlHFVMpnfupvlGBwaNGs31N93BHUOyibgtGTXySp6edg3JpWcoCjt06dudLtnp/rohdHATG84lMmxkbwYO6kvw3HbWHQ/Sp29XtXtHf25wq9b2/ROLOVJqaNWpvXJxK0Zd2Yc2JftYuKeSMTddK7+GcPfYPpzYsIrjyiHgEEvPFcJtB6ZzfwYN6MXwDo7iYgdeVhf6dm5Hj/5DGH3ttTxyfS9Kz+RqhBNgOv2+5WbQpWcHstXfVUT7Ht3p2jIVR4NM+15d6NK+k7/2u/Gu+xjZvIaTZ2xUGbAxbBk+RZcfX39Rf+wAo0ZLbt2Dez/zBZ7/8dd4dFiA916bz36Lq2Y4tZrx+th69ZqIhAnX12qAjFJnggRd24UuttCjVoN3fX09dvESdZrRslmQyjNnKa+PUn1mPzMXrGBdQTLXXTWIBHWliuoIwQTXF2LHhnpN3hqUKG2B11CvCbdGm9iLZIc1rfJfqNNCImIf1fKnK0K0apXl2xUKRbSh2oIklVdp0hORf1BBUbWheXZqzC4vjVbZiZQVFjdKUAApDEsKyrWZ2labrFHRVVEnOxwjX/W1sbamkr2rV/DB3M2k9buSUV2SfV7FpX+3P550OgFoKCmnOjmDFprQWxy89EwynRrOlXqWTIlHN+FUVlOF/acuPli4nO2VLbn+mj44YdFk9WfCIFi3NY9W7bKUJyXZC1MvnpDFXwswrWxpHqgjr6KOUEMIzackVLrr6nzMtPagqKKK8vxjzFuwnOXaDLn2mp4kterD1Bs7sfan3+Wz353Bhtw6iIZpCIWoq6kmKj3nhGdGi0zpjWJVtWmRQmVxKfXyL6pPhn5H9bWBH0IyubeSSfTUPnZVVLPnjEOPFmn4dKqL2gctgg6tX8xv3ljIPH0MOKbEkhRU/NQ1EI40UFUdxasrpbwuTOHBjcxasIxTyb24cWgbojU11KldG/RFPBKtIaxJs1jlrHhDYepr6qSrnjytHJs1S5fNUaKeQ5vmSZQWlfsxY9SOMoVQyRHeeWOmFqWrWbPvHBEnAdv6/I6jSv7UVRWxaclKZq84RMfho+nTPItrJowlbf87PPWVH/Dq2hxsZDaEGhox9KjO3ctrr87kfcX72sMluHb1QiGl4RTatEonImCj1fWEjEOgvo7CshAZ2Rl4tjwaoFWLdCqKC7j0CCnuQ/LNlnrU1oWIhOuxMVbR4BIIBmyFcKxXDITBgYg2ysOpLWkRFCaRsPzNQmM41IpGceM5hmjlGea+/RZvLFjJqu2nqA8kELBgefbHF3nJT6ToHLUJ2bTM8nzdtp956rzGq6SkNkzZ8e1qv+Xsi7Rn3OU9CVzCHXuxoo0ez+xYyW9en8OcRes4UBgiMRjLBapqPD3qFPfqiv677fehcIRa4RRR7Bit5oKhWkVmC64d2Ir9mvCXFJylTBsobZoHqc4vpKamgh0rVjB74Q6yB13OwI5duHHi5dSs/A1PffMXvLs9X7KjNDSEaaivwY9XtaXNfVFNZBCQqdqZq6nSiCvKiqJCKuVVhoKwaO86Xn79PeYsXsfeczUkJ0cpLSjECPOsQJRoNEJEX2rtgObV1lAbimWtSJ4mvKlZpGsSFJUvwVZZJDeUUNTgqT+ECIfqxRulQZttQWFiW8ITXcS3R0bI41jg1lFcWEezNpk4qo+qTdMyUkmOFFHUkESzDHw5nptN84yoaEOEJNMzLi46DITDDUS9xneVllTWUFV6mgXzlzNnf5Qrr+lPSnZnJt3an0Ov/5Bn/+0llh6plPoINWqDurparA+R5FZkpUUpP6NJZUOQ5s2S/PKol0VLtUVxXileOOz35TptJkWidRBMIkExXJZfSnLLdvqQERVPFbUNEYzxaKhQLq2t5uD6lcyet5ZAryu4qnu6DNep8QeqKCppILttK8LW/2g1Dcqhfl7Sp6U6jV8RLTgxzZk8cSQnPvgFz/7Dr5izx+b+CDae6jXORMXrZTSnpRYjRTmllNW4NNMYYcujXrLsN5TmVVKvcadBG0p1NVEi0hUIuKgLUV9SoUV2OVuWLGfWwkO0v+JqBmRGKKtuwAkEcWQugUQCDvbpwuV52L4jASyb/T4vzlnFwk2HKY8ECRrvAp2eQsoT9crTtZVRImrDlLbZuJUVVCj5hzRMuq4rKvCqKqgNpJGR7AlLVSS1Ij2xnpJScAOyRDEXkr/ROi9mm9R49Q3UKzb16MuwP6G6Kl8PWdlkJzZQUIn6iOgiEBRBWX4Zic3bkCh5Ua2AkzMySFY/qa6q9WNMJJwtrCSoD5zJVp+uli0ziVaVUKJxJqJYCDXUEpUv9Zo5Bq1tlklGOPZeXUahJq/tG+OoTlja8I8qGRQqL1c0jm3LTiVyzXWDyLQ8GAK61xaVUZ6UTodkg23DsDYhMpMSqMmvINgsiwTZEpWwtDbqIOXFVGjADEfChBvqfHsi1p6gtSKieApTX12NzQtn967ntVc+YK4+Fu3NrSaQEqCmooAqL53WmQHFRJSQGyArLQFqamlQP66SHq+ghBotbJqneUSl2xOmzQKVFNgwlL0yG+orySsztGrf2G8q61AaA7V5VXkNFSVnWLxwBXN2VTH0ulF0TBJQYjTYO3rCP+x7KJLB6CsHqC8eZOvGPRyOtmVIezs2qvGMT4bnOLjlRzlWVcdZbQAu1AfcCjeZSO4u9hSZj5nfxfhQ59LZ+HLhFlC/KC05xon8SnYu38CGw+VkJVSxd/9RKsTgYnDcIF0HX88Xvv4v/MeXH6ZrzRbeWLSTsGyy/YjGw2i8DoeTGXjNBO679wEeuOkK2qS6uIkiVJ9JSQ6zc/7bbI/2457xg0nU/DKaECDJNr7qw2q/NgPG8dDdd+pj0zh6NEuUbofElERsv6pXXnCwh8XOYKS8QXOJcCCFtFgFGAOSZSmwh31QkX38yKVyRzONejIYfNVEHrz3Xu4aO4o2iQ6BBHmuej58SF4gYNB5oUZ0RvOhSGY3xt82hftu7E+0tIAGbSC5mk+K5QJt45On+AomByg8vJivP/Yc077xK04E2zB0SE+aR+sJ42AUc6FISP05rJisRl1duSCB+pPL+IfPPsu9X/wuq/NTueqWcfRKRvoQV2Osflip5nL16oPWdnQ4yv8VtXXk7lnDLM01S1r2Z1Svzlx23Rj61KzmC1/8L342dxf2PysO2fypMapK9kSiQdq3yqCm9Bw5mnM66kdL561g1uYSBl89nFaRIorrU2mblRjrV7KoWfNUqK6jIRKmRjnY0xy+2qTTIiNKVDK9jGY0T6hGQ7AsM4gFItXkFUdp0S7Fp4nafhUFo/xTVVZNZXkeK9SvZm0uYsCYK+mqMUC1nD9sDCg+ApX5nKosI/fIIZYvXs/pmiCJ5YfZYXeHXOc8ueN41NYZht18FyOTDvP6eysoVb9yvAhRkQXKT3K8spqz+3eySHKKvUTI28fOvCiuq5A7L8k+eES9IL1GT+K+Kffw2S9+k8+Oac77v/0xi47XkpaE5ixRsrsMY/K0O3n4zkmM6qxMqL6d6PcFK+Oiy9Oz4jooPboJmwZMi776WH4Hj06byph+7QmKNykoOp3GGDz1i0izbtx9xw3kr5zOwoMlJAYDWFEiOX+KlKx2Pbjzya/w/X/+BpP7RFg9ZyZ7yw0tUxy8hmrlMw/TxGE8POW2kHJ8UmoSjnHI7NiXe578Mv/9T19mQvd6Ppi5kNNVHgkphqqjRyhuKGT7lrX+GBlMSeDEvu3Y/xJEsH7EHoS3l5BK9ORyvvmlZ3jgK99mdWkHxgzvTaI27RUumMZxKKL8X6c5X1hx7SQmU5ezkR984/Pc89lv8K42R8aOuV4bVy51Gmitn36seU2ONN6V62sbwtRpnmfrnfTWpCXUU3bmLJVeCln66GHLo6Y5mupTXFCvfhjBMwFsU9nGD6iPg+MLPJtXRbPWrUGGRpVbg62b+XS1mnNFFJOWqMFf70Vw1J4Vxzfy4muz+GDRKnaeqSHBNrIlQoZaAiopqoRWrbMlMkpUa8n6sPSr2tMcyPbpGHkD/tw39kKt1hlRK4MoxdVVlJw5wBytWdcXZmnjrg/BaCU1EaONbCMOh0AwyPnDa8DKiij3WN9rlZcTFDthL0pUPnl2TiS2Mq1jPc0L1qgfzl6dQ3dthnVPLCG/KgG7/rS8lTX1ymX48eNpj6FOcelZqAq0Ng1m0ixJMoWVpw8HmdFyCsrA03tY+ZHGw47haK1/dvdann9lLvMWbWL3uQYCJopJHKCc3YJ53/82X/rvd9meXysuz58jXixD5qJCcpTQ2rTM8LEMKS7SM1NILimgOtCMZimeXx5tnk2WqaBAa/9QpN5vb+uLJ/oEdbKoPw+uJ+ImEnAjVJSWEE7IIF05xNIFMrJJCddwribChSNCvebHoca1StXxHbz6+kxmLVrN5uPluMEEkTbabTEWBl4ogq+v3lMutfqCPo1+sPHcUFOlsSFKML0lzd0QBXLd328Qr0RQpPlWZeEJ5iu/Lzke4KobhpKuGKyqD4mvwfe1TrEUVNsqnKzYSy/ROsqRoarTfDDjXd6Zv5KV23NokK2quoQ2pLlog/KCv8aLQps2zagrK1J3DhPRhKhp3CkJCu7mAAAQAElEQVSpqCGcnkWW4tJild5ca6hQBYWg9jRq+5BvV3VD1B+DVUxEc+g69XH7bC8j3tqqiHyIktK8FUFPc+ewp5waRjdLwsmiGrLatkDCNBULaz7bmkzVVGkNHfEcPdVRWB7m4vV7S63xq4rysfPqOmEfqglJRzVh7V0ELODiajpDheeoS8imZXosZqpsf3NcTLSSUu1hlRzZ6o+rh0wnrad74DYyWpxDVbnMe/Nd3pq3gmWbT1Kj/mQtytVaumWbFjI5qlgJk5iZiqmqJqR57C592JqzcCeJ/UZyecdkqktriCjnByyjpNdo/pbUsgWpavtoQwgnJYvsrGo2L5rLC28tZ8GaveTVOiTKEE941iqWw7pHtI8UdoIkOp6wj1LVAG6Ci5G9/rgfiWLbul5rpaqaMjbbtdLiI3TQWmlgloZ17YdEtF72210xYBQbNh3af942or5eJz1VJcU0BNLJ0AaFbXMnLVt21pKntYHU+LqIVFFZGaLsxD4+WLCUzfUduPma7uDVKH9HCIfqfFxqo4Zg0HfasjZewkv4hxtzRiy/Rv0660NInaFOc82IZBjFegwzv/pT9/NhZP6sDtpAUItQkJfLqZyqmOzkNkyYdhsDU0o4Z4sCAVxNvBzbYYyjZwdHgeq4uovDGBNrcD3HTr0bYjSWzqnX5mCUZCVhVx299ZAbuPf2G7lHE52nJg8j0XIb0DyEpsMYFTS+GL8+gNoZjItr9ToGVO66LsbEnjM0epfXNuCoPhhwiR0mRu/TJJEYsIEVwXEcHBOmThtlCUnJXDgcErRYaFBScXzbre8Oxg0QUE9zM9ow/q6bmDrlNp588Hau7dPcZzXG+HcZg+WzLwEtdBOUQO2/J2TLjDpPfSRAuiZRtt6zP0GDQwo9rxrL1NvG89AD07j/pkG0DBjC1YUUBrozslUB7yw6jJHPRmrshW+bA1ooh7QoydDETnkCNxBAJPLZxXUc7Bwg6CbRduCVTL19PA/cO5VHJ11BK7XDgFsf5Cf/9jQ3ZeTwq9dWUN0QJMF1NHEI4BiXTNlWr0mOkRxHumt8rBIISoMTCKIizh/GQXMKAh1HMKqjx/Lpiyjp0Ja2zVMwaljrqzGyrCKH9+ZupcuYSdxx+wQGtwmgfILYQToT1JNNQqL8SKD/mFu5e9LNPPzA3frq2BFHA4brujiOg+s4kisWa4TEqlB2yzLjKikZJVvFgWgcY7Q5FiE5OQFLhhKKvR9eOo+1lZ15aNINTB7VnRQN+jEbHYkyiI2LD7lLQovu3DlpHHdNmcQzD05gSNsEEruM5p/+8+/48uT2rH79LTbl1pGQEMSVD1b31vnzOJA4kHsn3cTkIW1APhhFfEAbNXZAch3pC7gEMJhEydPCOlQXxthyx6O2toFgYiqXHg6u6/h2Ij7HNXq27w56xBhil2S4joNecdIS8bSRVIeD4wZwRWT9xdZ7LikZDuc2LWb52VY8OOlGpt3QjywTIeqApXFEZ/3R2/nT0cTJKAHXa9fA1gdcF0vjmCQCrkO3K29l2qRYzD0wvj+JltPg22N/HT07QVtaw5xZq0gZNpE7b7uJER1TBJNvHRcOc5HPyCT5IQGO7HIdB2McXMWrAfqOuYrOZZv5ycIjZLToSdsg2gxyCWa1Z+KdN3HXHbfz9EO3cXmnNFppc+Lf/utrPH1lMnNefodD5SESNVg6wsgRRki29cWov4DD6OuvJuHYSt5bup41J5O5dcr1tKGSeW+vJNTvdu6aeCOXd2uGF3ZISdEmjhYhYePgOC6ubLOnkUzXMRhj1C4JRLXgqTcOjso9tXdI/TVV/R9iZbbcEa1O7GGEc8Ax9lGX7j5UCWi9RK0GXAnCkQ+qhOQkEjRd1/xBxQ6GOm2eQmKKq2cwRvzEDmMczotVUYIbpHmP4dylNrx/2hQev/s6OrtRuo25kx/8++e5q3MVL2sTLr/GIdViL/sdXURrCdUZkls0I2hC6otRbLlj6pVvI9KdglXrSpnjOriOtUsKXZeEJEcprU71jngCuCpzXJdAkoub2pKxd9j2u40nHpjEDQNbAF6jzQH8v6ASfgHJc5ygeK0MIxoH19EV0KPOzldM5Pvf/TL3DTXMfukdDtcr/jWRkSTpdDDaDK0LBcnMTiZB40WdFgeO+O14oWGBRPUl13FEa3Q5vmxj7LPBTXBI0yb9HZPHM23q7Tz5wG0Mb+1SF1GdQ+PhNd6bbtZGj0BKEqHDq3lvZ4i77xzHHbeMoE1yRK1n65toETYG+z8nIN2uQ1QbMW5SinKYUaVO3bCHcp6jBV9Iuh3Z67dLyPHjxFY7Fn/HkQ8GmR+79O46KjOWwl5G9bYdZLwWubXaJGmWimgdmnJIYmoiddUVKnREG0SU2MPqdKTDPmclBaW+johjaRyt20KYQCKpvuJYmeM6kouuRuWNNwIJBOyETxNXR/xB+W2Mg70HnURaDxwdG9vumcojk66kQyKa4DqKDEgQBgEtqssFueX1uxUqTw0Q1gQzJHmOYwhpgu/p42CS64BkO47DpfbIGA1wJpCCYyqY8+5y6gbcyJRbxzO6V3M8TUYTk1NxtbiUWD8mgq5kYQ+D6wYISo9JlS/RBmobc6bR+F3rJZCawoUjECBJ/ay21uBYO4KusHZwg2oHB1p1H8q9ypP3aKPpyTvH0D1LtmEPg9SIBx0Ga380Giazzw1cmXWWl15eRFrP/rTOMMJH0W4kTLgkpkHu/jNk9r2Khx6dwoNTJvL4o/cxIKOUzdvPkKiPCXaeaDF3jOTKJt1oOowx+HbaewBhWcXZcw7jH7uLhyXrwQfu5L7xvTm1axf6dkNGsIrjJ06TX442njya9x/GHWOH45WfQetpJIamwxhHeDsIEi3aXfr27UPnDrB7Rw6eclj18fW8uzafqydNZUAziIg+lH+IPeeiJAYdjP7nitlokZHZbxAjWieRf+QEp1N70E/9O//UKbwkhK/BuIZkbZScPpVLcpc+tFIcBYPJaGcUu3EelG+uizCOUlVep/xusHMuRzpjF/5h/HcXhSkRN5HOvQcypEUx2w8VoxDGdQw+vYPuhkAClBw+wCktngSt4laNAhi96NSHQUO7IROZ0LOSN99dRpHiMMkVdjEyzh9G9oQ90lv14RaNcfdOe5QvPv0Utw7LJKJ5XNhJZOhtX+S73/4iP/nPr/KNO7riKT70Q7DNCJ798me5bXgnxXAy3fp0JzEq+9RhHNfgyBCJ55LDOPLFwTQVJrq42ugbduPNmrfdwqMPTGXyyPYkt+zPZ/7xW/zDw5dxcuG7vL+3giT7YUb2ByTX1TynurqWQHJzMpVvE9oN5AHl+WlTJ/HYfdcxpE0zTLhWHyM9X19Q9vgqpdgYl2DQwSgHuYSoUX7zbdWYWqON7dRUSylF9ua4JEn+Jf1K+p2Ai6u2ze48gPtvv4lpd0/hybtuoG8Lx3Jh9D9bbzeUbTgUnzpLNKs39z45lYcm38pjjz7MdT1g6+bD4ILFKdZ2Lo5U1yV14K67xlO3ZyFL9pWRpMazcs4dPkNSl+E8+PidPHjHRMl5UPFZzeYtJ9CaG+3N0XQYYe1IsPXN9f136DT0arqQx6ET54gm+aqx9cYYUjO6MfqKthw/cobCsgiuPso08ctlSDAkVJ5iz6li6mRjwDE4ctJVbAWy2zBoUE8y8o6xp1CYuy7Wn0DAIVTj0W74zUwZnMjC95aQT5CA+KPoUJJQc1B75hAHizV3DIGT3opb7rqVzk4F54qhb5+ORMrOcbpCfS1oFOuGRH0cLCk8S359NkN7p3L2zBlO66OexBJt1kE59ha6uIUU1EF6YgO7TjRwxQ13a/01ifs1xj49bQyBwl1sOQ1p8ktTfBlz0WkcHH2ocLN7ceMtd3Dv3Q/yuc99jjtHtiPJq6Xaa8a4+77Cf/3Ll/j5f32OJ27sgqsNZE3iSGzZm3ue+SyPXdmBSDCVzp270FJY41j5hhjeXHoYg1GJrbOXF64mGg6Q1CKLgOb9WhL6fI6dDwn8xOSgTy82Yofn44J+7XuaPm7WVtYgJhzXjam2FbqcQABXd1flrhuUPxHWzFpIRecbuOu2cVzTM0txZJEEY9vYGCCBoBuitqYBa58TvCDTWEtMgGBAZMbFdR3RGPuiZ4MxTuzZJNN5xBjsmvXB++7m4VuH0NypV/4NnLdP4SDaxtNIjpUl/Y6jZ6NyXVaeI7tUjCfRSS4EWvbioSk3cdeU23n8oRsZ1rE5dp3REPGwvAHbXx0HsWF0d53Ysxofo492dRG92zJtXtWZROVpRBcgwe83CFaPqJHycCFzZq7EDLmRKRNv5tpuaYS0PoMAo+56kp/882Nck3CQX7y4gloMQceVHKMnydDpoyrs0t0oZVqzWduCagdVYVIScPTRry5sfJtdbczVCffUVMvv4JhYuTEGexjZ6zoO9tUzrqbtadoPaSDsGZ8ffdQIqV3Sles4f4hXp6MBxMrbOGcuJzIvZ9ptN3DjgFaKi6gojfhdXQarwxiDPYxjcB1H+gz2mcbDsTnAdaSulhonSJZi3TEGxzjIVYJOAq36X4G/l6D51mNTrqKt6sPCzBWfI5mWXkWSzYcOA8YlNRnObFjEyqKWPDj5Ru65oRcp2nDE4ZLDGNFjsDJd1dVW1+AmZWD0jPTYWnSkJwZxGxqU/YWrysMNIcImSLp4LbGjMv8yBp0Y8dh313Fw7IveUYUbcLE+1NdWKRbTsPHiiMC/RJOl8a1KH+mt7mBQcd4Y4I7r4mAFJaAlPKG6MMaRbCV/+8cSwcRU/92VLOO40uH61EbvEnv+dDVHRu3cEDU44g9YucY+JxJwAtojuk3j6nh/D+f+G/sqmtCq1PiyzmxZyqKTqdytfjNtfD8y9NEtCqRpP6GmqtaXl6hcoSISAw5eejsm3TWeOyfdyhOPT+LqrpnY/RXHdWhMOwRSXRrE68/ZlVdVA2e38cbiU4y692YmTR5N92YB/P0b8bmO4+txHce3Cdlu9Gzd1KN99esdW+aCm+CQ2qILU7S+tGP9E/ffxoi2+GulgOv4tK4jWQZ8GRgEHrYuOSkdx6u/qH/Uqf0DpAcD1sWYC4pfo77Ra9SNitdbtLc2hftvHUii1qSe6+I6MR2+bBlofM6LfzwcYWYluk30InKcRj7XwdWF5dV1Meen6dn5SzoT60OG8lN7mLdkO4W1ddTU1nJ4+14irXrTNxsitdVU16hcCT2iBW1NTQ01+uLlaYZUr0Vptf0i6WfjSy2NqDPVqb5g+3bOOG0Z2Ls1vbu35fjqD9h6toqKykpKqsN4+vpcJ73VdQqo6KUy7AQfuzqryNNkqYKaukpKS8v9BWxEk+da2VWtxGQ7wfARXTixagXbztRQpbJK1dl/o6xWsmurKwlHMxg5qB25e7dRorKaU9vYV57OiKFa0Uita6xyQ9e+/eD4BlYdrqBadOUVFdgvs6nt+tPJO8Y7Hxyg3P5Vsb681elLCrjlKgAAEABJREFUnFgbT4+wFrs10lupL2Gmc2/6p1Wycdc5rA27t+4j0rEPgzMtuQDTSSCLy/pmsXnOQg6VVFFRUSn8Gwhrwr158Sqq+k3g7x4dxv7ZbzPneANRO0qrezUIK4vtzs1qp4796K8Zq91HP3v4GPnakKgsKaO0vIpatdGQwe05vmoxW3IrJb+CSiVI79wu7P85RkVyG8Zc2Y82CSHqvSAJ+spakF/sT/YHD+5B9eF9nBQGNYVH2JwbYdjQ/rglW/nX/3iB1ScbrCNEm2Z81h8yuP7K1qxfsguy2pCdhOp9ssafAK4mYQX5+fo6dZxT+rpfq8HD08AXra3kXF4tDV5nhnZ2WfHBfE5bnMsrqApFiYYbqFa71jVE8dT2dTW1VOuLoqdE42pz9Vx+ob5sJTBsYCcK9+8nz9qdc4AdJUGG9+uCcgcRGruTk0CoqohCTeyO5eRRWlVPg77cNtRZmXXUSx/iMNiYgHbdBpJVso23V5+mvLJK8VBPxKtgzfzVHC136XflSAa2SSasSVKQWgrzS6ipDxN0DVVlhZRKz/6cIqr0hbk+2pZBnQNsW7ZcbVVDTUU5FdrAKdGm3TBhXnF0B6es7cX72J4T5bLLuhM7rAfg+X2wlmrthoW1MVMv2mptvNm/hghpolOt9m/QRCpUX0+1MKqqjmD6DaWnd4IP5h70+0ZVZTW12jSxfdTS1NSGsQNNuLKYHNUdP3FG2NRiv+iHrcymPu8bErPDtO5Hr7RSVi/fJZm1VFo/qiqoDqcxrGcGm+e9z2Eb08Kr4pIv9xd8sLkmrF3ugPpeccE5KqvzFRO2n4doCiur0tMEpVY2+LkmEiWkflZl/RTesbioVcxGLSk0H8i1ncNs33FKC/euflmzToNo13CQtxYcoVwxVVpZR7jhNAsWbeVMdTJDrxpB1xaJWsB6mng0UFxYTLl2kiLheh/DGj1be07nnKPWJFBeXkKkZWd6t0335TvBKOUFuWrHInLyCikuqyOlV396O6d4Y9EBbD6q1pfnumiUiGyvVptVVtdDn0F05gw7DtSo31ezZeMJ0noNoJOJKGZqqdXGQdTzaKgXvpX1WsB47PxgOt/+zWpKfc2eJqqenhLpN6IPlVuXs+JIufTVUaGYjib3YHiPBPbv2C/5deTu2ExeQmeGdTfyQflBdtQrVtQimvvUq3/VYd+txIFDulG8ZTlLj5VTYXNgbQiv+BBzVu0kL9CMa64aTJe0qPqcwVFI2HizOenI5l3UZvegT8d2DOzRnFO7t1MlPRWHt3C4Nls5OJkGxViNymw/8xTPtTXVlOlrekdt1iSc2cKqfaVUK75jubeWpOx+dA/m8O6sPX77lSv31jZYK438t+2eSt++XSnctoIt+TXytYoyjS/VareIFiXVip3akEeospzVS1Zyyktn9DWX0TXNUGPw7be529p/YsNeilI7MqhbK/poAMzbfZBS2Vpzch97yrIYMTCLaH21j3GT/TX60FChDaRkbXJlFe3kjVUnKZf+0rJKjTueNjtrqdI4aRdRXHJ4RJTXarQBY2PMaDPSNFRwSv2m5HQO+aX1ykWRSzj8F+W/BsVRbW0xG7YV0H5IHzIjNfK5rjFmIKllPwa1qWPXztPCo45DW7ZT36IX/VpGKSuvplqxFVJQ+zlAfak+FFGfqFcM1Pi5S2FH1GKnuhrlkS3r9+F26c/gQFS+11CrNiutj9C5/xAyC7Yya+MZv7zKTlptXlL+sWNhjeKr29ABpBceZrN2EWprC1i1J5/ugweQFKmmXONlneYTNs7r/fxbL8xQ9kU5FkhvrfEgwMqlWyi0OFVXK9fUUK1mHzK4Ayc1tm0+U6kYrcIuEPyo0IRQrhHo1Ish2VXMmbWJ4qoaqqqrqAt7tB/Un/SSI+zPqxM2eWzaWUznQb1Jj1YJw1rsf8Hlaexsmt9EPIcA9RTnFVCjzQlHeb1cY1hFdREnzhZRUlmLyejGqI4NzJm7mnxfV7V0RXEToLaihHPCMdy+PwNbVbNj8znprePAur1UtezDUE28sYtN+YTbiv790tm/ZD0nJKdGY0dFeSXloQCd1c4V25cy93AJFcqr5cIj7HH+sAsg1xVu2oCr1TzOxpVJyeaKwW0Iu1l079yKxHBIfauO2roaatU2gZoC/9/FTtXmOnURyjR3KQllKbclcWDLcg5WhbUpGaFWOahG8znbb0PKu75SYdTQUEeN+leN2ttTXBaf3cGhqmw6uGEKyhuUCyNktu5LWuFmVhwqwyh+juxYzbr9uZqf1FEtTHceLqJzr75aHIFSpC8aPOX5Wmpkpy9fvpaW11Cau4PlO/NJqS9k5lvvUdrxBsb3S6GkpJY69cPD29ZwRJvbJlInP8Uv26rFW6WcUZB3hh1bN3Mums4tt1xF5PBiFuwu1cZDnRZSYU7tWMnGwmwm3TQSR+m5RfcB9M0q5P3Z6zlZUk19dTmb18xjZ01XhrRHY05ENtb5NtZqvPfUaUI+TjU+JlZvmdqw+Og21h3MEbwRGpr8UdxXya76hkp2bdxFnuJKYRXz3YtQr/arFd51yruV2iW/dsLtND85hxkrc2gQNkZXjDj2azSG1iqvuorDW+66kXsnj+O6oZ1JVEcI64rWV7J59o/55rd/wle+9T2+9v2Z7CkOYqS/LtCcgVcMZdK1l9M8vJ/3Z63F/ifRdupN6V6+/18vsfJk2FcUkSz7EFXe9ttF80m/KLUng1rXs/D9lZzTGGtzdH24TGPaBrYerafD4JGM6NmcuvqQFpeyXvx1ipmavJ1sy09gwMCu9OjVhcjJNbyzq0D5s0pXPdGMjozWeLVw3gpyhWWVxtEaOz8LBIlqPXAup5b6zN4MaVfP7k2nsPF5fP1uSrN6of10UCZxIoBpRr8B2RxbuZZDioUazSXLbb+qN7Qf3J/6fat4d2+hdEqv9FgVNB4JAXC1+ROor2Lf6VPCvxlpynHFiu+CaoduHdqSt2cZ24pDmHCYWsWhtcPiHtE41bzvGKZc24fqc2cJaVGbUFfKjtP5BNx0AsqhZRUhiutS6NU+g2Pbl7FbH8IDRGMtbGNKH0VrlBst3tWKG/tfGx3buoIj0Tb07doBqkPUaJ7WVF+pHFxdeoZNO3aRV+LhROvVF2rw67WJUh+q5eyhHeyUDRHNNeqtvbafKM9XVlRTLv/3b13LkdIQnsYAO77Zv+o1xlATTmH0xDsZklLO2eowdg0eg8nDuFCn/r9wzS5KbexK3pEdhwm17EPXbEgbMIHrO9eydO4G7Py8wY5hBTl8sGQbrS+fyOhOLgVHt7Nywx6K5U9E/Lv3HsRt25cuzaDm+F6O1kSVJ1xqyiOUlUYIaUzrTAGb1m+nQg1tlEC8mEGNv4ZQbRl1qV0Yf+s47rvzRm4a0ZVkoVuvtYQTymPhjP/m6//6M776re/ytZ8v4lSlUf+voi6QRe+hA7nt9nF0rDrOoqVLOVLh0Uwf/Rry9vDj//wly07WYg/P7wT2CUWcR4NysG2nvZtlf8de9GrblQGdEzi0czc2NvJ3bOZsoAOX9XYoV16r0XzHzr1oPIwT82LoyAFEj65j1p5CjV81VGqsiWg0Skt1yDtyTG1QR4Vd76nPqStijNH7Oc3/Kjl+tpSqugblt7DySR01Gv/C0RQG9mnNiQ3LOFBWQ41ipd5iFpVi2+HLz2mtXUXTWrvWn0OFqVPcVWvsDWmD7DJtcO5ZtIg9BXbcrdDcJkxU/aFW9XZv4CIoJJTG9VqNP+eISleD2rZK42GD5vJRG1+aS5RXRGjZtxep+Zt4aX0udp1Vrnl6JKE9wzpGWLNiA8U2X1aWaeyrbcy99YrrOqo1RnldBtI3vZjtW0t8fHeuP0C4wwAuy4qy/NUX+O6MbdRZa9Sf7E1AEVTOLC7I99cdh8+WaA0Y1Th3mNkLDhBK78gNV/QjVeOV0bjm9+mmOZwwVvfErt9HDmnL3iXL2VdQQ7Xitco2QtdB9M8skS2Fvi271u2noX0/BiVG5VcdtXX1hAVSWPFvcajX3CuqdXGd+mB5eT3J3XrQO6GCrYdi/Ns0f0/o0o9u+niMr9h6YEhwQpQVFgnXEGhjrLosn3K15/EzxVTUNBAWvg3KG3buGZY+u/bw9elLb1T67JqqRuOLJ3Ge8nFNbYPsrWXz5j2kdBlED61rqqqq5Jd801xhyOBOnF6zmPU5lVRor6Kyog5Pc4A65Y9q8UaFbUg5vUqx0nDxxER9zfpqc1eN1aeNwfqKUk5rvDpyNJdi4RrS/FxmXHJ61kZhVVN6iPXHowwZ3oeoMKrSfLwuFEYukT1gAF3CuRpfq5R3a1m95SiZfQbSzgtRoXipVfxbupBizuq3MWfjz47NtcLB+m7/CKha+aqmppTN+pLVdsBAmmm3wI5hNWrTGs13Bl/el8qda1hyuIwq5cdKyY4I33qN0zW1lYQiDpfZ9fuxHTSt37ediTJsWDeiDcJP8V6nMd7z97tqqbZ4+d4aoQOBtv3pkVLEquV7qJLsyvIyKrSerolkMKxHKhvmvc/RsioqhFlFbaSR07/hakxpqC7jjHL3SeFZJIyq1V6jLu9J4dblrD5h+2g1FerLqZ170oWjvNK4j1VRXoVdB4S1X1etMbG+ISJ7HDpdNoTUk5uYuy1f9tRTae2td3D00e5sThXVp8+SU1Sp9YnaQbnO8vpzIPWVOmFi129hAR9SPPi4K8Yb9Fwj26q0H5Dcb7DWDTt4c80p9YlKyrTPE1L8hDWfrBJWDcK8aW1fozFSmylQWcJJ7RFF23enf1ot2/afU7zWsWvHXmjfh+7ZDn7/sI1qshnRJ5V1C+dyuLhKOiqorJJv0Qh+3GseEpW+BrV7tY0/m//EbSwvDqlpQY1Fx8ixbaE9x1Ktmerkg++PxUnxHdVcOiYrLMzE/Ck8LaJ/MbdMDG1N/vvQhmNMf2Me77z7AYtPZ3L/o7dqEeFxZv9JounpVB8/wtmTZyE1nfriQg1QBRTUJ5Di1WlhqNZzXJSNfFsDGjwLT+xn3vwFzNhUz6Sn7qVvwNB1/DQeuzyJ9198lV++OocNx0qJKBFVBDIIlp9VoNRLhB8BOErymjdBmz5MvqYrW954iRlz9+F2akc09wxn8g5zLpJKWn0ue/NDdL/pPp69Lp3ZL7/Aj37zNvM2HeP06ZMUaMKUXHuO48UNXDb1Pia0L+bNt+by2qIcRt59P+M7BZDhuJr9q79o/+p6nrmjNxvfeJFfv7WEwuSWmPxjFJh2PPXcFJL3L+IHv3mLNzVpOldrxBslgoMTqef4wTyC2SkUHTlImduBx54YT8Khlcx4ZzYrSzvw1GNj0dxFPoKrkPX0e/mke7irdwWv/uplfjl9iSamReQc3MiaHJfuLZJJzOrEZV0c1i3bTKmSiv0rjhN7NjBn9gesLOvMM4+OIRHDgKvHMiRpL9//jXSdDdKtWT3bj1TQ9rppPH11ErNefpmfvzqXlXuLMP3JlCgAABAASURBVC1a0HB0K9NnvMfLm2q5+dYxNE9PY/SVg8hfN4u3N52l29gpTBsG89+cy+uzd9L15ru4c2ASBDMxpUfZuC8Pe9hObO+eBiJPHbpNt0FcNaI97Vu1FCoentpRp+8zmR244+ZBHJ73Oq/PO063/p0pOnOc2sQBTBiezooZ77L+ZD23PPk4N6Qd51c/m84L7y7noJJOcWkV9i9nSnLzqS3LoyYhHUryqHebMe7qPhxZ8i6zd5Qw4Pa7mNijmlm2jRceYcjkaUzoLbvVuE7jbLn/9WMZkXCE7/92LjluG7q1DLHhwBmOF9XTLCnEniNFeHLKGEM0KpfbXcZnHruB0jUz+fFv39Hm/V7Ko+lkmQLmvj2T376wlsyrbuTaTq0YdOUwAgeW8tryQ/S+5WZ6l27lR68toLBNNzo5xRwo9eTfQ4wKHObHP3ud6ZuKaN4qhVM7c2g95m4eGGqY99YcXpu5i07jFRuDMyBmDa6B6tzjFHqpJFef5oj6QVkgk2S7ENMiJa+onvR0h2ItLHar7ZOzApw6eJpwYh8+97nJJBxYwvd+9iq/eW8VR/MrOHs4n2DzZM7tPkLGgOu5ts1ZfvLr2WyvbsuA1g0cPFbC0aN5pGWkcub4CWqj4DpGbemB05oHn55Ki1NL+Z76w+ITHi3Twxw+UcnIh55gUsdiXv7V6/4/t7JNA5acEB++DzXnTpDvJROoOMWx8iATbx9N2dq3eX7mTlr36kp9WQ6F9aKVw0YTq8r8ExrUk0gLlXL8XAHn6hwyEyPknqmg9GwZbkYatbl5+goqLZ7L4JsmcMc1V9BHIeJForjN+vD0MxNxds7jh7L17SU7KI5kk1F9nPfVfr98dxd9x97I0Bap9Bg5guyzG3lx3m5OaYJfnZhOOO8U+Q2QlZWgDZAcjuw/zt7NK/inb/0Xb+0Ic9vUMYR2vsfPZH96l15klO7ntOnCZ5+bTMbRxXzv56/xqzdWcbCwghNHT+GmZFBw9ADVyQP47MNXU7BmDtPV5sfTLuOzD11OoOwUZQnpOPUllFZWcLrCIyMtxOnSelq2CnB40x5ywvJV/V4hikKbDqNu5blJ3Vjz1qvy8Q3eWrGXslAKEx++i/61O3jt7bnM3u1w55P30ydYrYVUgOzEGg4eKZGgBk4WQbOMCEcP5VPvQavLb+VzE9qw8o2X+ekrs1my9SymeTbk7OOdGe/zqyVnGX3LeDpnGi226sk5sJ15cz5gQU4LHnrqNloZh8vuvp+xzXJ4XX1x+ooixjz0MFdnhjlW1EBWmuFcThkV9k+jMjKoPplDYufRfGbaYHa++zK/nLFAC7RsAiWnyA1n8/hnp5F9Yhk/+PWbzFi4idwaI7t1CgCFJT3HTeKhK1J479cv8MJ7Wwk3b0Uk96DGgONUBtPxCo5xpjpKtDKH96bP4tezjtD31tsZkuBhN+Lyj8n+efN5/3AiDz89iZauy5Db7+bW9iW88dY8Xl2cw+h77uOGlobTZ4tJSQuQl1dG/bl85a80QspFDYkD+PIz4yhf8z42zmatOUKJ2q+4wSU5WsLu05XWYGxHiHoGY8KcVYyF0ptReWIPlR0vY+ogw8s/f5Plp1z6d09i79Fz4kG0xA7HhdpCtq5dxXtvz6Oi51ieua0nobOHqU/NpL7gJHmVIUxCK+56ZBItctbw+tsfsOJcSx5+YgqtZMepumQynWpOl9WSe7aShGapVJ06xfEjxSQ2S6HsyCGqw625fuxQaveuYoby8traHnzmyZtIaShnz+lqWqTB7j3nCHYYxZcfH0vxyvf53i9f47cfbCbn7BkO50fISq3n0MFi3F7jeHpSF3Z+8AEz3lxCXZ+befa2jtTm5UNWOuHSc1o4VVLaEPTtOq54R/FjNKbgpDHhvnsZ5e7nh794k7c2F9GuZYCdR/Jpdc1dPHNtErNfeZmfvzaHlfsKY8szYevYp0AbHnrmPj/+f/izl/nVK/PZcLySjF7X8/St3dk+9wOmv7mCcL/xPHWL8k7uMRpSM6ivyKe2soizmt+kJ1Ryqg6uunEUgYOzmb4mn6tuHYOz411+NXszwS69yCg8yMlwIlOfeJhRzh5+Jl0/f3Uh646UkTboCq5uW870FxZqY7QFjz59B5knVvLaO3NYnt+KJ564ldaOQkL+BkxUmT7A1XdM49ZOefziJ68xY/kx0tukkbP7NJmy8/N39GLTW6/x05dmsmRbDvpuqbgQnweOYyyc5J06yhl9BD1zch/HyjzaDrydp++4go7NDecO7dVCJoVWKZUcOJzHwR3bOaoYzi0ooFzCElMCVOUcp0ybki0C+SxbfYS83NNs33OarNbZ5OlD0s6zdTgBh0BdObv3niE5uznluQc4eOQEO7cf0OZTKafO1BBMScDUKTfk5tOic2tOa9NszXHDsL5dKNm3kLc/WMDbs5ZS0XEc91zbG1eTermBHMGEati7/wiBFq21B7qcN9+bxftzZzFd8eW2bkFNYSHl4Uw6N6tg+bxZzJ43W/30ZRadTKJ76yiHd+xTfm1FYvEmXn9PvMpNM+YsJsdrTcugR3KvW/jC/ddRtm0RsxYuZeZr/83fPb+F0fc+x819kohosAtndOf+Rx+jP3t4/cXf8uMXp7O9tBWTbh9DlgyNlOexS7vd2S1TOHX0MPnllYr3oyS2bEXh7sVMf3cWsz+Yw2uL95CY1VzjyEn25xnaZ1ayYtFc3ps/h3feeoed1Qk0T07y87jjGs2vz7D3eC1tsw17d2zleHGUtC7DmXzTcPI3L2L1/ny8BIPPQOyIakutQ/eedFGsVBdFKC4NUVkTJqoNo2BaS3r16EHLhDoKisooLy2ltLSMCi+Rtp170adDJlWFEZr1HMJ1V15OesUJ1MVJT0WpKkwkHCEqf60mTfH8XFRxpgSTmUb1mcMUaVMQ0pj05EOMiO7h57+YzkvvreBIWQItqGT9ovd54eU5VHQZzZQR2YRrG/Th/SxLFi3nlfd302fSA9zZLYDbfSxfmjaEfXPe5KcvvM3MjUeoVf+f+PDDXJ9+hF//9GV+rj68en8BmrBw08gWbH7nLVYdd7j36Xton7fO71cLTqbz8BOT6Biw9hsCTlT9ymXExLu4s08Fz//0VV7XJlNy60zy9p8iqct1fOmeQex5fzo/ffFdFm7WXEfwohxi/bZ//ZyZAfmHd2lueIaCskIKy6IkpiYQKjxLXn2Qts1CbFizQ33gOMfyKvl/7P0FfFzHlu4N/2t3tyTLtmRmZowxzGhmZoodx2HGEzjBc3LCzA475rDjMDpmZlmyLFmymLm79/fUbsmQc2buvPd+M+975zc7u7qqVi2qVatWwbadgiO7NNalBPyG8kroc9E4xp7bAalEsj7K7k89xrHsLHKLDdGxAUrTjpBDHE1j8/nhh51kl4Hf7+BUFrNjRwLhei0p3fc1H676hGXLPmLVjgqGT7+Sy7tGkbhnB6nBujRw0/h82WpWfPUp7y35jAMFhnrxZezYtpdA4+bk7/mRpZ98xupPVrF03RHq1q9PMC2Z3ceQTxby/Zer+VjzZNmyZXx72NCibpCD+xMp0MXWnv0H0X0vPl0MUK8r06aMpnuDGMJhPH/QL+EgxHXoT8vSHaxY9QWfaL7+dDSOsZNG0Ub2LHEaMHLWQi5smMbqTz9nzXdrWfbFr9Q6fRILR/VB3yNp3bE7cWV7WbXySz5Z/Qnr8pszftwgmriF/LZuN9l5WSRnFRDUPi8mxuXIgSPENG0O6Rv5Zms6ISObabSr3RXX+GjUujvdWtSVz4XIyg1SUBIipDXCH9ecntpv1nUryc7KI1/zIlsXMhVBP000Lzq3aky4OExVg4GMvKIf/rICinSB79McjdJaS8Z+/tBZAT1G/VOm18GvM/HBrev5fPVqfi/rzII5lxJr/Fwycxr9Qruwe7EVW0OMmj+DXlGlHCxwaBRTzoEDWVRhMN4kc7BZbI9B3DqlN3s++0B7n4/44IvNpIcMfXXe6x+7j6df+YTvU/20q1fFzpRCzht2ObX2fcULS34htk0Hnc+z2H0oiVw3lrrBDPZpI3v6xKmM7VLG4mcXs/izjdprGxxdptC0O2POb8MfH7zFh1/sIdC2BaGUFI4e2096uA6xZansyQrSc8RUZvULsuT1xbz03hrWJ+WSd6wAp15tio7sJ8f+rTGj2SPnsHYJ5h6l3FcbpziTYzn5WpOCxNUKcfRIHscScxQb46hMScJtcQ43zTqPtB9W8OwbS/j45z0UE8vw2bPoX7WNZ17+iOUb82nQ0MdRnUd27ckjun4M6QmHxL8DV88fgrtzLXa/s6G8E9fMvZRaUqBhfJjdf+whEz2qa5kFX1OuGHEuwd9X8/rqjdTq0ZHojBSyS3xUpGzknSXLWbwtzOTpl0DeIZIrYompSGFPerllohQmjI9+I6cxZyCyxZvSeTlf/JFIeaAVV84bhj0Hvr/sU34rbMuihUNlvxyygzHUDedzMKOIxIxiYuv6yUo6Rn7mMUK14glkH6Qw0Jb5sy+hase3fPDxajYHu7BwxiXoaCefMPIQiddvp3POoU3hVl78ZAvdBw2iTfavPP3uNwSbtKVBVAG7UzIoU4yPDeazXzHp8LFCasUFyD6cTn72Maqi4ogqSKLEbcqlF/WkZMePvKe93ga3B9fOuYSYykx2HqkirnYp22XrJhdO4JpL6vLFO4t54Z3P+GFfLsGKLPKpRaAynwxdgCbnBalbx+VoahH2ce18DAdJTEgjKi6aZMXb+H4Xc179ZF56fRXbaEnnuqUkHi4E2dOxTq+S43OoLMngp69/0Hiuo8nlU5jXP4q8w/nENqxLrvYaOVVCrN+bK2edQ+6vX/DR0tXsje7PjXM1IMXpFAfqECjPJKOkivT8SurXdjiYnMUu8agnu+/bnUawXjuGnNGGfTqDvfvBJ6S3OJ9rJp2Gk5fEPq1f9X15+iBTQvzAsdw+viO/L3vXm4fLf9rD4fRjHCkOUM/NZ4fmXtvLJjCtn8MXH3+GPb+3HjKdqT385Oq8EB1Xi3ydXcsLj1Hkq4O/JB+FInAcjakLvubMXjiOuMRv+MfrH/NdiqFxnUoOJBdz9tz5jGiWyVsvvc/rS75iq/ZW6jki9LLW/S/mYt1rvfHKMsWbJnRrFNY5Lp/4c6dy09Cm/PDB2zzzxjKWrdlBYVxnFmhtrH1gre6xPuCdtVvJLK8i82g5sfUdUg+kUxGC+I7nc8Pcc0ha8zFPvfwh72rfldGkv/Zpzfn5jXdYsb2QTr2aUqgPhMfS8vHXjaUgNZWi/BSy9JErqiqbJH0USzlWRl3Z+mhiKlsTiqkT5+r8nA4NT+eWBZeQ89Mqnnl9Kat+OkBJVZBjhWHiYsLyn0Lyjubhrxevs9FRaN2DK3rU4vP3VvBHZlOu0lx39v8g3/iE30rasmD2IBQWZBKDLAr6PWPCTMa3K2Dxq+/y6kdfsym9iHBBLlW16+B0xwYZAAAQAElEQVTqvjFPl8vZZYZ6vjISckOiMeAFB+hx5mWc1TCJ519axZpEl/aNYX9iOklHpV/9MFkppRQePYYTU4fKwkwqAL/v+JCo9t/jdf4zu2GMDC4BUfXbMXbuHE3cccyaMUnBZyi9GwfUYmh7xhDuu+9GFo0aQOuOA7ntnlu5cUQPatdqxsgrF/HoTcPpUCeipjERflVhhxbdzmTc2DHcuGgyl3SqI156ffW4eOJMHrl3EXdcM5VhpzXGH9+WGTdfz/3zh9KjcRTGGIzmY0h5tBENtTlv3Ezvr31fOX4wV11zNXfNPpe2LXow67preei6sfRrHhBiLGcMm8Rf772Oe2+ezeSLe9K+fUdm33Q7j14zjC6NY8DEccnYiSyaPZ6rFkxhZP/mRB4ryKDzGxBF78vG88j9N3Lz7MnccOO1PHDtUFqoi/U6ncXNd97Ig7fO46px59M+Tug4+IxyXwxdzh7GXx+8mQWX90JzmeiWfZk9V4Fz5iSunzOMHg2snnh91A+WjNjmDJtxFY/cfS13XD2Wc7u2oH2vi7nj+jF0qStDNOjGorvv5vH559FQ5OXBKHqccwUTJk3kxnlD6dUoCvtEN+/Forvu4e83TWLS+FHcc9dNTOxTT011OH/cHB655zruunYqIwY2g+jWTJ47Q3aYoEuFiVzUJV540PXySfztoRuYdX4rMDGcPngci+aMZ8GV05h0fif8QHmV4Gedwem9GqvmYiJGw1EgNcZQZurSp29vuja0zeGIbdTTCJqPLtqI//3hW7h68hVMm3cVD0wdSKyvNpfOuYan7p3BRR3jcWq1ZPLV1/Lo3Vdx87zRDGxdm8b9BvO3v97ApDOaEyubXHXHzdw9/XRipFWf4TN44sFr1SadJP+CkRNYaMd4/lTGnNFa0qWLFDDGqKC3QRcW3HY7j988kRFjJ/DXG6cxqHdbzpswj3/cPYfLTmsSoRG+uiUCQ+v+l3LXPTdy381zmDt8AA006H2GjOfG+ZOZf+UMZl/WWXjQYuBgHnn4Vq4a0pOGLU/jxvvv4MFF4xg7eiJ/vWMKfe0g1m3PjOuu59E7FjB/6lQefPB6pp/bTvR+Th8yhkWzJyjATmPyBR3VO4GljVSxBeq06cfVt0qPKwfTvU1bxl9zLQ8tvJRW0bH0GTyVxx+Yx3kdmzPgkjE8+uCNzLmwvcejTrsBXHfbdTx4x0L59VBOa1OP1gMG89eHbmHhFd2p16QNU667laduncrkscO59ZarGd69Ed3OH8qD993EvMFdidUcQI8xRr9Qu21/rrvjFh68aTbzZ86SvKu4onM8+BozZt5Cjd9Cbl0wnou7CiYKY4x+RdeqN/NvuokHF46ga30fzQYM4dFH7uCmGYOYMPNKHp13EU01XVG/0RPXvBPTr7uFJ24cRZdWzRgweDpP3zebc9rG07DTefzloRuZfUkHAsK1JPU6ncPsMb29fhufY6E06XYet951Iw/YuavLmKa16nCuYsF18yZxtfXvc9p6eA16XMT9D93OTeMG0KH3+dx4x43cPfsimjv5HEgPM+qGO3jsL9fzyF/v5aaL4tm6YR+x3S7l4Ufu4u4ZlzFxznzuXTCU9tFQS/a5+uYbePD2q7hl/lD6Nq1HpzMv5/77b+PaMX11ZIcGXc9WLJqkDedkFk65mDaio34H5t10G3+dcQ4N4+I5fdQcntXc6N/QD/HNuXD4ObT3tHVBNpVro0lN70tGcd+91/GXm+Yxf8QAvHBTpy3jZkzjqlnjWTR/POe3ryXcOoqpc+Xrs7hQY2xp+wyfzN/un8+wns2JMUJR3B0wdCoP33sD91w3gwnnyz6+JoyYPo3r5kxg0YLJDNE8QUtvpYml28ALGTdhIjctGMWAZtLTsohqxKAJ6pfm4tVXTWFo74aC+ul+2WSeun8Bl3dtQHyrgdx2/81cO7yb2gxdLxzNQ/ffxK3zJnPtddfw0I0jaRuFfG0AN9xxIw/ediVXT7yYTvWErtdR5x3lOPFcMGE2j993HddNn8Qdd96gS4VztQb057o7b+beWZfQvll9Lho7QzFlPFddOYWxZ7cQZZiwCdC2z/mMGzeWmxeO58xW1kZq8jcUvuw2exwLvfWimYDQ6fxx/P3+axjZqwExrfpy4923csfYvkSrtVnfS7jn3pt44JYrmTOiP03i47li9vU8edM4BkYWDE6MWYDW3c9X/2/njvH9qV+/KVfMuoZn/3Il48cM49YbrmLOea045Qlro1S3BRcMuoLps2ayaNyZ1Nd4Rbfuy/V33M49086mRVzAI4nVx9upc6axYNZErpk7kr5NZalAI4bNvoZ/3DSaLg1iaXPGcB5/+FqthR3oMmCQ4sHNXDWoJ3UCMfS7bBjXKP5fqfh205wr6FRXgqLqc9GUufzj3gX6aBjRrZlsd8ud1/HgrVdx09QLade6LRePncnf75vPEG+dgA5nDmHRlROZJ33mjxqI/cM8tVr24+b77uTGET2pXzeOQXOu5283jKBLPclRDxxH+ir3N+rEbBsvb5vDlVpPH7j7Bmae1RxMHc4bOyeytl0zheFntsaHHvmEMTW0XZhx9UIevnMRty6ayMVd4oQA7c+6nGukz/w5U5g3cgBWZK02/bn1nju4ZXgPYuOaMFR2elJ26iB3aNhvGI89ejvzL+1B9wEXct/Dd3H71KHMmD1L82M4HaOAeu2YdNU1PCRZdywaz6XdG0CgFbNuvY2/XT+a7o0COA27MXPeFK6aOYFr5o6gbxO/CMHrsXS2uandUvHzGp7Q+nflzCnce+/1LBpi1xc/fa4Yx0N/uYF7rp/J+Au6eDEE7UEUBrBPTDS07nsW8668ivkTzqOBONdp3oEzT+9JVBnUbtOP6fOv55Hb5jKkTwta9RvMLddfzYwrelFH9g7pUFe7RTdGzLpO43eb9jLdadqqPZcMm6a91c0smngZ3ZvWwoQhGFOf/heN4y/338nNowfSpl1nBk++mnsXDKdHyzhc8TKBWLpfMJY777lX69QY+rWOo2H381hw1TzmjB/LzOlTmT64D3Eyg70UMsaADWtObXpeZOlu5+4FM5g7dQpz9SHi+mtv4OrL2un+oAfX3fdX7pk7gUkTJjB9ymTmz1/EvTdMp08DP816XMotd9zGfdfPxdLOmzqZaxZdxTWTL6Jx2FCuaRTXvh8TBZ8+fjhTR4+gR2wmX3/2CRsOZVIuHDcI0U26MHHWVdxz243aVy7iqnEX0KYOhETv1m3JJSNn8tB9N8tOPagbE0efy0ZrvOQbx3WexLU33cLcC9po39KRCXOu4+G7ruHKKeOZMXkis2fO5Y4bxtI5xkHfSr2+O7XbMmzafB7+y3XMHnY2res4lJY59B06m8fvvpJLezbXBT0gP9ePRhgqnbbMvv0O7tXHoMYhHwQC+P1+wpVQr/dl3HPfgzz1yL08/eh9PP33R3nu7rn0aduMEfPv5slrLqOBZk6R04JxC27l2ftncH5L8dfra9iX2yXzkvYaINX9Pke/4tl2ILdqb3PjmP40qR2BRTXszOzrr+PhO6/ixjnDtDeNpf1ZV3D9NdNYMHsq80edQQMDZUFD3SadGTNyCFdfPYvJ57T2+oB06H7BSB6Qv99741zmaB9d20qr24qx867mr3ct4o5rJjK4j43DUVw4YyFP3jebK7o2wB/fnslzp0bm1ZWjOb2FnZBg3YnqeaWbSobNXMgT9yxkwazJ3HX39dw4qgcGh+4Xj+LBv2iMb5jNpEt6EmfQ4+CZWBjRAWg38FwWar+1cPSZNBQgLP+Oqteai8bP57H77+IG9a9Lly4MmnYNd2gv0KNZbWy49uZKfBemjh9G+3j0IelSbtLaNndoX+IDGnfxqdW0I4OnLeJv993OtcP601Bjbn2M6Lr0uGAcf7nndu6+erp8eRLzZs/l5qumcblia1D3YQ07D2DmNbfw8G1XsUDzYN404che9185lNbxor9wFH+5905u1Zo3XXNl5qSp3Hzb9Uzo15yY+A6Mn30tD99zvS65pjB32hSunDmLO26eyelNY2neZzC3XHMl4y7tSR3NS9cYKnXabjXgCqYO7gYl1kwOxshgiglOwy5MmD6bq6aPY8qEKVypGNmjkY8qzRcTdglH1efsQeM8/584aiRzZkxh7FkdiRWboHy1VpPOjJ09hwVTxzJp0mSunHyF1gOHIPGcOWYGdyyaxeCejZAqBKv8tOl1Dgtuucvr+7DerYhyUJvx9LF2D0XVYdBVD/L0wstpEuvDNX4CPp83f+v1H8XfH75P6S888+hfeOrvj/PireNoH1ebixbcyePXjqFDrENpOJaLZt7BO4/O4oKutbB3ZBVBH21Pv5zLutaV5i5WHxX0hqg0tel37qWMnzyFm+YMpptdXNTikx+PnjaVq7y92AQu6lhH0FjOHj2Lf9wzm4t6NCUgiJTHKLcmRaUOZw/l7rtv1Nljns5N59HKB74m3Vl41908ccskJo0bwT1338T40+oT1+Vc7nvoLu7W2I+eOptHFwymb9fOTLj6Zh67fiS9mseIZUOGzrqKv92/iEXTLqZNXT9h66jEcsH42TprX8+V44aw8NqruXPWObRp2ZM5mtcPyR59mvoh0JDLp1zJIzpT3qn5eGnPJjrrDOD2B+/UHOhL41oO9nG0ntg80Kgjs2++nYdmn0uLxg04a9Rcnrl/Jue2b0iLLudx90O3Mv/yTtqvG9qdMVi+fj333XQlCzQP4sXK16ATc266gYdunSvbTeGB+25g4umd6HfOYB588BauvrwrtQzEtO7HXJ3HFmi/s2jmIH0gMRqrCuLatOcifbxrhvUN8ImnirQacDkPPXobN08bzrTps/nLwsto3aAjExfM1FyYyHXzx3Nu61hiGnZl7o038cg1I+lXvTd01DePjb8eF4yZxkOyxd03zmLSRV2oJeZRrfoyR/NtwcyJ2kMNpaf1gVqNGbngRv5+3TC6N4+jy4UTePKhhYzs15IGzXpz7b23csukgcQDdToMYPa8KczXHcS106+gaz2foGCMUcJ74jqcxV0P3sHtk86kZZczuEVz/IFFIxkxcQZ/u24kfdo151LtPZ+6aSQ9W8TR8fzx/OOhqxkzoBUNmvTkmntuE21/6kTF0O+KkVytffpV82Zw08zLaG/dOqoZQ7TX+9s98xl5WkPQ2foc+eoj917H3ddNZeQZLQjENGWKzo2Pzb+Y5vF16DVoKk/fN5eLq/e6Xvx0AnS5YIzOTNcz95KuNKzXhlnyh8dvns6UYSO46y/zGWzPjejxCMD+P0Fi6rVj+KjBLLhqNrMv6YRRc8OeF/EX3QPMv/w0dF0kCDTudi7zr5zE3FlTuFYfDFpFgVO3HXNuuY375lxI8zoB+oyYw9P3zObcDk0ZMHwKT9x/LePObk0gup72qpNZpDP3wvmzWTT+bKyLI5+beOVVPHb7dM7vYOepj24XjuT+e67nvlvmMWfY6XRsUefaAwAAEABJREFU1YKxC2/iH7dMYEC7eOkSxemDR3u8rpo/jakXdPB0btTnch554EZmnNuKWvHy5Vtu5N4559HILxK9jsZUGXXbD+RG3Ws8cONs5s+czaMPLODSDnHgb8q4+Qt59O6F3DJ/PBd2rmvRMY6J5LVbMFXn47/fPpupQ4Zyx18WMrpnA7UZTrtiAvfrDHeveM4bPYD6gtbvcjY3336Dt1+/bsL5tIyNouM5o3nsoeuZdFYrYjxXc2g74FLusv29dT7XTjqP5rVjGaj99rOPXMPMEUO4/sYFLBjSixbdzuOBB25m3hVdiNP8mXn9Dfz16mF0rldXe5bJPHHvfC7t0ZZzhk7isQeuY6pkoKdl/0u5994buf/mecwZOZB6UQH6DJ/FM3+ZddLZ/iauvLgNOA0ZMf86nrhjOufrkOxv0ZtZ86ZqHZ/EdTOH0LNhxJjGGLAJPTHNGDVngey2iNsXTODizvXw1e/OdffcyW3j+2uNjeWi6Yt4QjG/ZyOfCMDOa1vwNerEvNvu4snbpjJ57DDuvOtmpvRv5e1lH//rtQzqXJt67fty81/u5Pph3YixRBhqRHvV/wY/zn9NH1zC+loZthuE6rxmQbV/qjVsYVp1TykLFgwGCYmmBve4ruEgFZXlCv6uNgkh5cdbcEXn8VMulmpwFWyCVAVDlJcWcyQtm5ziYmLqNaKuAgm4Hk1I+GHJClfnglbrHBaG2Oh13fBxWFjMXaUT+ELQe6r8f9JcGJJ4Mp9qedUNp/L3gCd+jsuTXA+qPCLf6mU19qB/+hHck1GNI5UifARHDq3ehbU5qAqqQfVwqEIbwEosTkgnFgv1GP5Jlie3utE9zj8sOovtnuiH2kRqgWoLazzDWNtZgHuKHSLMKgpLaXLauVzUtpZQDI50gkqSDh+jIK+QhD27cducRjyIl8HqUcNfIE+GhYWrxzKkXXZkbCOyI7iuRxeWbmHhuQKGhWd9RFWxqWmP6ORKT49HpHqSjBN9EdFJbw19mBrbuJJhy5aPiifhRvzB01P2DludqhFcyfXqp8CqeVscpePtFkfJtTD1Jah0SpvgVqjXLjyvTbCauoq22SpTbRuXmrawjGK77kqfGv29smQENa/UfBJdWPSW1oJclVX3mEfKlt7yC0sHC3b1Y8s1PhFRovq3pk24Ho5ygdQY4XUqTOCaV0iRNpeI3m5EDylaA69BjeSR9pDaRap+h+VbYeVqFcCjUa5a5FU5LNxIJfJ7vB/SsaYvrrVXtY0sbw/Tow1jcdyasuVVUci+fcmEomtJbhi3PIND2YYOXVrjCC+oE5aVGbb8hW/7JUQidekrOSH5z3GeojlF3sl0skqEzuMiNmGClmdlGSWVDbnsst7ozCByGx+UVb/HeVteNfyreYUUq60v1ID/7OuutYXoatotSwuL6BHG2sODCedUmMHVrZX9K/oh+VplVVD6WsxIsnJq8Gt4W74h8fHq+vHalVsK2+bV1R7JXfVALWqP1KWLtYVAf36tLDtPg7avsnXY4h2ni/CJ8HcJK6YGvZhKZP0pr5LeLkGNk0iOs3aP6xH22vWjw2uQoPrq4ekn7OHUjJXlHRZ/JbVZRpbH8f5awEnJFc4JepcaXGtvCw/V9OM4jUuosorKSlcyQlo3I3I5hU818nFYWLiuRfEajsuQ3tZeERlh9StE0MoTnUU8oVs1vQUq1dDbdlX1uuIdrk5WHyX5U1hJjdXvn3EsuAYmfK8aJlwt21ZPpBq8cLUM5TWNkuFWJyKeUtNSnZ9KWw1UdjK8Wr7oI7yq6+J7Qh8XOx6ucKwcawNXurrCcZWLoV61enXpp1wAvRHYyXxctUVStRxhnfrW0ET4eLieXGEdp1VbDUzgk9/YGJfatcIYx8X4pG3I9fzFLtWuymVlYYpLwlTqgjWsOVBSGqZM/uRWM3E1byqFU1gcprTClV+4VFSEKSoKU1LuolCncRDfMFRVw4sFD4l3RVmIgqIQFVW2D8IR06rKMMWiLSqJ0Np/lqBE8ktFU14epky53C7CU/jWnDZVSbalK5Z+pcKzydJZHWw/bFuR2iwPm0pV9voVgpDkF0v/Ik9OGEtr20vKXKSmFIvglEu25enW78nV1yygb+1cfl+/n+yKSGwNWfsUhygsCno2s/Qn+i+7SK8i9a1Eumo6efYoLAhSIBpPptpLpUOk3aVcdi2SXrat3LYpWT2rZEvbZy/J/hUWT3w9+6vNDk2lcO2YVFRJ/ZPs5NHop0R8i2Rj2z9Vj9szrLG1OhYWhimoSeJtx7+8JES+dA2KKMrn4nfC+ANhnMh5TmJdz++t/1r/l2qC6ZWASN1VpeatxpUhwkK0LW4NnoXZQRaqcUOyUzmVtk3rRUi5wN5r8UOKwUHF8JB4eED5edjSV6cadFf1kFKk7kb0VD0suggsQn3i18XShD2ccAS/GtHKPQ6vhp2gi5RqRbvUignjzSsTGYOwZJWXBskrCEXmhvzOjl2x/Oy4n8gQrvCs7wTDENZgF8qfSjSeAmPFhWV/S1eocYn4Ch5c0x07f4oEt75t/aZUvmF9trwygmPnsPWxGr/ycDQXrC/Yfy6nolz+WBjCzqMyySxXsr5i57ynv/hZ/h6d2mxufbIyRGQeyX/LrX/bfihZa9i5Waa5Y6tW/+NJ/bDxxepYWiqZ8i/Lx7ZbWSEZoFx0VoblafHKxFtklq3WYpcy6V4qnHKri+xYQ2/939rAxpaQfCkk4wVlb9sX23cLFyhiNylmZVr7lUv/QjsvZHsPVt1m54W196nzwtVeCyosjeaT5Rejc2iMP4zjC8sTwehmu0xn1ZYDzmBgm3qCGcEMkcdoP1SpeFohPcI6/4bVHmnRaEV8rtr/rC62xZWSIcEidTeCE6nYZvGphgnH2tCNQCN41bCwzW2D6LyyFHctzObVsJAte7TSJBSkyrZXVcq/gqjJawl7dg0TFiBs25W71fRhW/awRO+1Wbyw9BOwBkc5qG5flcMeXo3+rmcL9+T+1uAod5Xs3Ldj69GpbtlImvQJn5rUZvFPxrP9DWpfFvJkCl/EpryUYtOSwZd1R8N40jiJq3h49OpXTW41jJRFLz5CERe3WrYb6asgJ79W7nGaCIFlXk1j+YiumsDihiQvMlRhQp4Mr1aNb8tCFp/jPKvxBT31PY7jSi/3OP1xGWJ1vCxKWw4q3tqzhBQ8jq+mk+j/lb5htVssUUnfGr0kXvAwwZNitXvy2EZIvF9XyB6dcnHRPA9R5dGFPT08sIcZ+THiU1VRvUZoPTi+RgjxBJ8IrpTweHjw47Zyq2EygtBc8YvYWtKry3ZNUxPWLhHasPY6IaUwbrWcGhoPz8Kki+1vUPNEVU/GP+HU2MgiRAg9vIg8N1IW/cl0Fk1CvbaQJ6NGD9tSTVPNt4atbYmkk9rVaPuizGty1Vdb99IJoCfHg1Xby1WbV1fuEernOMyTW21Hla3eti81+CfjWd0j89f901xXXbrU0Iq9uuue0KNarluN41X1UyPDw5dsWxfYDuIJ2uo+WJxTk8vJY+vRSSvLIywai2vbrU6R3llITXI9/rbt1L6Gq+es8MQwbHVSrtp/y9f5r+mVwd78O/qiUpMfX05NdZtyY04qOw72T1j4RKMll5CCiYuo8hLZl1VB4eF9bD5SiD/gw5w0QEZ0TnUSOxCN4/MT8PuI8YfYvXYpb20JM3Lo2cSp95bU0vgcRzoaJUcULtbJXTV6fuTiPcbYNuubrhxPCUNElvSyjq2JbZ2JargxgvPPjzFW1smpGk/4EX5qU7kaepyBMQavXTn2Ue7Vq3X/M75FAYPtXwRPZSOIMeJj1IIemzuyj6HgSDKZhUGO7NvI/pwgPp+DTID3eDTSq1qWY3PjtXCCv+zjwYz41+DWwMAYB5/j4BgPCaO6x8fCnAgsvl1X+nVqojHFe1xvAPyUJm7kmWfeYivduLRnbSzY6mfpq9l5+Cd4Gmybz+eLyJQMn+MQwY202XZHco1RXXjWR1QFjEdr29BjjBPhEVERo3qE1jneF6Edf+0my8VgcYwjHCVjDLbscyI6hOUrIdsJwBgHT0+fg6N2FHRsm4XbupeMwT7GGA/HUY6S1yaamtwYtasvfqUamJcLjh5jTITe0qhsTKSuDO9RwcN3jNibalyVQXUHnxPR3xiVJcOveSVU20iEzlEufGNBRmXVxROMV/Y5qovA4lqwMRG4o5w/P4JZvJOTQMIyHq8I3MhcIapNifcIqaZNamCMqcY/kXPKY7x2n2OEi5JzvJ+qeG2OeFDzqOw4pqbm5caYCJ7j4KiMHmMc7LhaG1neAoHanGocYwy2bIx41W7F6CH9SfriQ95e+jmvffAd9L5cX7lb4QN8AZ9wjZKjZDDoEZ2lt8mnsfD5HIwxaheOcuyj3LHyvGQwFqbfCKy6ZhxdDBiMvw5d+vSkvW6fXf75McYQoTuJfzUvn9+P7adQsI+RPJ9j9bE1MMbB55yoo8fC/szP0nkwMXKUKlL3kpQfJDvhIMdCPqIC9ubihHbH8U/ibfn6HBu7QtiDZw0/9Ng2r672SK5+C45kReoOjlMNs/CTkpXlk53tuuT3VeMdpzNg1wAdQo1Hb2QPQ0VGAkfyy8lM3Mn2oyX4RW/XlRq2lqfjONhkjHgo+bRe+TWvVOSEXmoDVY2Ha/EdDwEsD59zqm2pfowx1fiW3lCDa2ktD5/PyhaOMdgned8hCiuK2LJpN6Uhh4Df4K0Banckw3EieNjnOCzCQ1UL5bgMx8Gn/vp8jqeDT2W/LZsID2OMB3eE54hvBAo19MYIIptWVITIL4ScXIesHEOmPsxk5jhkKz+WESQtI8SxLNRm220ywkHJVMOMyqpbGo9e5eyTk6nGs7TVqaZdNFnVyZNbAz+e19AaMjLDpB2TPl4KkZ7hSq7lZ5SjpDzL9XTNrKa3NOnSPz0jjO1TlvqUqZRV3deIbNF5+OYUPTNPgmUf79fJOIYIjpV9cjIen+zqfnkyJNPD/VcwT85J9FmGwmJHlx8nfMMYjRV6lEfGs9ofa+rK1eq9jsoxMQ6N6js0b2Jo2tjQrInKTZVUb9YYwSLpZLjFa97CR5tWPlq3sHR/wmlq8GjFo3kNL/FtpnrTk3jWlC3cw/NwrWwlr2x5G2rarA42Ha9bXuJZU28uGV7yaKFRgxCNG7nqg1G/jMenUX2XVr16csOtc7lh6pn0aSO8RkhfQ6uWPvXJT+vmki++Nfo1/ZNdmkpuM8lo3cpPW9mghcon5JpqeeIhuLVD44ZhGjd0aSqetm7pI8lIrwhec7VFYByHNWuCeP05Ga8fzWXjGvzjuXgct4Vk15StzGbNfLRR/1ppvBo3MNSLQzFBodLzBPtjIjFAPuH5jSHyHK/XACzYRHAdG08MBjDGVMMMxudAOJfNiXmESlJZt/0olQE/PmP36niPMUi+T7FZcEf+Gw5rr+8e52Fjs0tkfTkRhyypqcaplm34F4/B0kRAsrcAABAASURBVDiefhZPyRgPzxhzgl5l/uVjiIl2aFDfyH/wxqCZ7N26pd8bbztWzWTrZp6/GfkOHk5kHAzN1WbLzeRHbeQjrVs4p7Q3s3R2fKrxLK5Nx+FemyM+Sio3O+4H4q16c5ssD5tsWbq1aG6w+rXRGNe01/Br5skxx/2quaVTaia/b6L50djOE/m0pWvm4bre3InMH0RnffrkPtpyNb9mjuT6PN9qYfUUvxbqdyv1uaX0am7liGdNbvsZSdX0avP0tLl0sG1eXf1qJnrLp5X61lztzQWzqZnKFu/PqZnamzV2adggfJK9pavwLd2pKdInS2PhTWSLBvWhbh1HewOHam8hvmVXTu/aGJ980cLsGmzzouR9HC0OkrxtGym6fPcHHCILNHrMCR+TD9a4mTHyQ/E5+Wzq1DQCxhg8n1Xuar21ssBEYOLjONVlq4BwPFzBIr4u8ZpDruA+C8M+mkGOH6ta+u4kssqqOJpwhAo1HeclXMeRXsqNMdWyjKTiPRHett1BzdgfD9+rEHlU9mDiUZMbtRjj4HP+mc4Yg92D+HwOjtod1fEeE6lbWE1SmzHVcOXosTr5tS/zOZZeknSGs3/ivnff7rSIFsKfXmNMNd8TuaGmbHlU63gcplbDPz1WrqevlSueHoLy4zDHiIMHxeL6quvGOPgcK8Oo0eCo7KgN+xhTXXeUGwz/4jmOYzDGCK8a13HE18LgFHmC+wN+/J4McxwfPcaY6rqjXLREngi91fFEPaKnhbmS62D32z6PJ17d59g2TnmMMeIr3srB4Ph82rv6padgx/FdwsYBitiUkE2oIoMN9p9Dcnx4a4QcP2R9WbmrPFxzqDQiEczOHzs/wmqTZjjia9Wyl6Jhi+sKT7jGOBG5ni5g++jhCt8nvXw+B2MMjq0rqYh9jDE4Pp/XX7tPVpV/hWPQnJM+VkcrOyh9xBDHEmBEYzC+iA7GcOJRxZE8nyfDh8/nCDty14VxcNRmkzmZxqM2x9usjJNxjDmJTjrZe7qgLr9dla29EC+9GFPNQznVjzEGyyuSLBYY6eBzxNOYSJtyY6rLgvuku8+ndsdgAGMcfI76IZyasqMyeowxOGrzksroqcHxqvo5pa0aV2DQj9fmwQyGf/UYrL41eMZYHFMt06tg232O9OPPj/HwfI6D1dfjYQzGONX9AVSvgfPf9HH+v9mvKo4lHWDjzkPk2lULg08XHI4B6ndgzm1/4bUHpjGgbbwAer0G5f/q1YKak5LA+q2HyCGOIbMXcf91kzi/cz1xBWMsU055jJzC7zm6D7/PEY6NLELRpLJtPq/NwYqNtLjCcYjAfThqEKoI/v/7uv//ZXecm5G+thLfZiB3PfkQf79qMN0aBSzI66NX+M/6+ReGssELExHo+GzBoeclI7j/wRuZe2kX76+iOyZMYdphNmzbT7ouzS32P7EKVZKeuI8NO49QFLIY/zq5VUUc3LmDbYnZVHko/2eWdnw+fNU29dj9ix/HV4MTlK8fZMOuw7r4iSA6vpq2SP3Pv/+yXq2y/feCdm7fyd6jxR7aP9nEg9b8hMlNTWLj9oNkFYc94L+P76Ec/ynPTWPz5t0czvMm6XH4f23B4JO9/hfm5t981OFq0/2bKP/7DSFyjySwfnsC2SX/tn29EGQcWg+8lGuvnsKciSNYMHcqUy7pTh2fpAvBzgKV/vUbtj4kP9+RTPVU+Nd4/yuoNaK1hwzy78r7E59wZTGHdu9kszZ1f2r6d6qujkT/TrP6bFujWvbltr8+yMPTepK+aw/pOnQZ8x/TznH+N+aRFfrvpPKcVDZu2U9KQbXPy1Y16EZj6K0XAhjH0S9EN+3Kovv/yvO3jaJPqzoezLF29kr//FORl87WrXtIyCiJNGo8IoX/hN9gCYcP7GPr3lRKPfYuHS+cwLPP3MfNo3pSJxCx83/Q3B6H4z/yyQx9JF4vnyyyfyotN1WxYo/sVnkc5T9SqHIdfUTwE+VzdOEJ+g59PFWFDdG1/dSr6yNKNj257b+6XBUyxEiXBvX91K9Xk3zYzyU1ulRVuZiAj9goQ5UWmrDjUEe619Ukr1vbhw+oCp7axxra/0/lWkut/kHl/zvuac9qFXID+1GhvBzF7/94ChZksH37bg4cK6qmc6vz/zgPLRf/iTSGqCifDr/mFBl+fcgxXsQzxMZGEeWnut3udfezadch8uQTPt//oj+Uk3pgNxv2HkWuUs2jhteJ3O93pIef6H+hy39u/zlVJ7eUZC/GpFBiAMVIn+Ngi5z0lOUfY9uWHSRkVcfVk9r+Y0UT4ek04JKZ1/HW0zcxuk8rYjzik+UZqMxn19bdHMoqkzqO9HU8LMTBJ+M4Rjj8v/s4UkmqEK4oZP+unexMzpee/IdSuOgYO2z/Mour8f8XPuX7j/G1+vw5lWWnsnXbHg7nVFTL+l/zspemdo7YFNC8OMHTyGd9kaS15wT8X/AMV5CSsJdNu1MpM+ATn/Jjh1i3eT+FqP5/0CdfuJADu3axOzkH1+Pzv7af7VN0lPMftsHJfftndzPyxMglE15Jv9VIdTufy4OPP8Sjiy6nTZ0A9jHG2OzfTcbx4XMMwZJc9uzYwZ5/dT4QH58UE9q/y+vURoPjE2/RcvwxRHg4tOx7BU+88AA3DY+c2xDeKdpqAXH533vCxVny813sTyv0IivVv3/m5vF3Q+SnH2aT9m4ZZeE/o/wH6mHyUg5pL39QZyUtfBiv3449n/4f9IH/3ef/DZn/lq6egaGiKJfdOnvuOVrkYUpFL/9/+mPZeXcAsrGNZev/2E562X+Mi6WFMCU6m27dspOkXC2qIo3ADQ72qcsZ4xfw5nO3M/mMDtT2HNLBGIPP+nJ1cuTEkT4Yb6z91XCfI9wIQ9xTaASnuoF/8YhZbqp8aGsCWfbf6DoFJUIXLs1n/+7dbE/MwXrZiY9LEWRrFyP5J+tp9XKkRzWGMoOpzGbjhm0k5XtcxMaNaBau9O4bNm5P9PYb6rTXZ8eI7P/w9fTy+3WB7vN41ugV6dn/IXNpX5R5lC1aa5JzKz1mMqeX/8/P/70WcP4rVLeT5uRkZZ5cP+GgmiT6muNqsShK286LL3zA5lxhlyaxavX37M1XWW9YnmfpNav0nqBW00mveOny2X7RKs/YycvPf8TvxyKbW/vlyH6xsjxOUFt8S+6Ssv0PPlr6FctXf8HHX28hq6p6dmqSZx7cyiefrGHpF+tIKgwRaTGUZR1i7eeCf/IDm44UI1SknGVYnVxVT6SaPlgdIrpUo9msun+27YR+YGWpyePrtani5eipKVfngniv114NO5mX16hJ/c/tJ3S0bRE8K/IE3MJs28l9kAgLjiRVbHskRUD2N1Kv4SOIVcgzlPiral+LU5PbZlv+c7I4NlUUpPDOq2+wZk+Jh1Lz5c2Or5c0/qWHfuPJFz/hQKmHoh5Hcvtredg/rRzWRfW+n1bx9AfrKPSEupzSN4usZPFrUmTM3ONjGpYse7BGy8beX77jy03HRKF+nWKLsMdXSzU7f/qONdvThROmNGkr9n+AtTtbVSrY8eO3aovQS4DeE3LEziL9U7J6hTV3ghXl/LLsLV78OtHDsTDbFkkeyPuxdWlHRXYSr7+8mJ+TIkHdzhfbFkke6j/92DaLFyzPZPXrb7FsU1YER/JtWyRFQJFyRP+wDGTrXos6ElbyyhoVC49UI7iRuuvZyxuSCOIJW0iW2MmU6az57Bu2ZIQ8DMuzhjbCT71U4VT4SRzlf8bK96gjuDX01aCTMve4/Aiwph6pnfzryh9sKs84wMsvLeb35H9h35MJbNlTy87ySMWV3pJQLdPCTujn1Wx7jZzDf/DUiyvZU2yZuLq0s5Qe1in0FuLx9WgtroXYJHwLs0XZI5JVwyzcJg944sfysX+SPxysJPGnlTz23q/UbPdsW006QVFTsnKN4pn4C1SDV5MLVK2z2tW/sPSpLEvm/RcWsyah2Guz/me5WFxZxYPV0EfgYfb/9h1fbEoVtbDkLMfbbV8iSGr4N14PR/Krm8PV/hYuS2PZ64tZtSPXk+npUc2rMHkry77aRFbQEom2hody63/GgqXNyXp4IPtTjRMsz+eHD9/ljR8PWyiEwpE5oHZLFwH+869tq0mR1n+WX62mzBVpQ5fEWbu+58mXvyTVa5Ss43YSWoTRP/3WyInkkeZIWXxlp5rYW568gadfWM6OAlcXKcdY/srbrNyRU0Pg2a+GLgI89beiLMSePzbxwfvLefuznZRot2I3ysZIWZWjqvL47csVvLb0J7YdKdLG19Lbw6XaZWc5GJreGFs+KXkwy+M4DCws0uNqWq8tArdtnkwPZtv/BPdBVGUeP322nH+8+D4vvPYhz736IS++sYY9BZXVekFMtOHojt/5dls6UXWgNHkny5Z+xpJVa/jo099IzHep5cd7IjLx9LJlnTmoST713drB6ntymy0b26ZkcXRewuYeXM5XQ29zr80Htuy1q282t3VLE+FfLf/fazMRHOxjfVS55VOTNMiiFlCvB1N+8luos2pJZPk+GfzPZfG2cyhYUcSvH7/Hy2sTIjj6uGHhET+KgP78G2lzpYpN1a3idwJeDavO1OSVTrSLzoP8889xHPm9/dsWBPP55atv+P1wsYccmQteUfKJzGWvKp6icbXXLUzdygvPL2FLtm0IExSj43wtyCYpFQ6HsCl7+w88+upnHKvZ00Rc0mJ5MpDFMw5uY9WyVby74jt+2Z+JprXXfvxH/I7LUPk4vKYgWFhEFsfmYdVt2SZbV7UGUzLdE6kGKoSw7V+wiqzdP/DkC59xRHqGj+5g2WfrORa0iC5hybB7sKqKAr585w3e/T3TNggePsFTdB7wTz9Wl5pU01RT9/SVb9bArU2kEm5FLt+sXsvG3bvYokv8xO0bWfX9LjwXDBVq7Nby2yF7hYl0cE/S4bgSJ7F08eSIcY3cE438S9oavEh+MnakHIGLr+ziyn4hra07v1zCP5Zvq0GI5Pq1uNZ+NbnVRWSEKgv5/qP3ePO7RF3FCFFriMWpSYJY5U7Rz9J6cP3U4EVyAf7Fa9vCWpurSvNY+fZrfPiH57xUaT7atkg6mdCVV0bq6Xs2sGzlGpZ98j1bkj3Lew1uWRa/rv2apavW8v3OYx5MSh6n8wDW1nYehIIU7f2Bx1/4lMSwWvIO8eZrS/npYCGVqlv5NbYRiRD0qmDhkaT6P73S0Q5zqIpda1fw7JINFIkmHNS8s/Bq/Ai9xXWxMiw4U31a/u1OymxF6WQcsRDkn99/hWNhYRHY3Iq0+XHKGriXR6C2/eRkoSfXLS8LO/jbt3z6x2Et+ZWsW76YF748KNO61PxNXyuLgmQ+//RH9hfKgLK6pa3hdUpZTmbhFotQAb+v+YZfkyJzRkz1useTle3hejpH4JaXhcsJsQucnaZqxj4W90SykH9Otj0kHcJVJfy64n1e+3Kf9zEO+aPlbdttilCqZ2Ll8aBDAAAQAElEQVQeVn8qixJ467l3+f5IudckMBbvePKg//zjiq9NFdmHeO3lt3RWUvBys1nzyVq2Hq3C9sH6gSuGkYSeSF8jdRdPL0HtG4HZkixQTROpnah7ONVAr1yN51qY/dFiavc1tuiBatptbgF/SpaHp4Ntr04nUNxT7FADtzQnUg30RF7TVsM3VFXBulXvyrcSPCTb7hW8n38l41RYDb71Bw0vlGfx6fsfsHJDGmXW5FZvNVi8SDphr4gI8VPM1EuwMpdP33idjzbm1TT9yz4iYZaXRarIS2HNqs9Z8sk3LNG6uTmlBJkZgoVsWLuG91euZdnyz/l6SwpB0VkaU5rG91+u0V3RV3z6835KiTTUjIvFscmVD4WUKrJ289rzH/BzWsQHw1LWVb/CNf3SXUTiD8t54oMNkViithpeNjfGUKR94+rVa1j55Y988tlXvC9ddxzVRkqCFBr1W8xPS95j8Tf7KaqQTap52PXEFUJp2hZeeOYDtpUItSCR1Z/8xMEiy132FMjqU5NUPf7WwLz8OFQ0HmmYY/u3snLpKt7++Es+XvkZi1f+wkF9ALAW8WisHtUpQl6tWzXM86NIg35PtIVlGwGoKs9jxVtvsHxz5CLQxgAL/5/0f68FdFT5z1feGIMxJ5KVaMyJurEAJfkh9iuKMTF0PrMf3ZvGow/b4JSSnJBMjneH4yqYm1P5WULRn/xakDEOPp+flv370qdtPD6MN1usDEcnLGOMIK6WpgilpYFCNm9OoW6LtnTv2pmKXV/zxNu/YUWXHfyFVz/ZScOuHelgjvDm4i9IDYm2KJF3PlhLfoOO9GgLX36whHVptsHo/sCbnUKSLCuvOjnVuTGGiC5E9LBKCGZMNX4ESmVBPhn5xQiM/TGmut3mwrFf4ow5AbMTXmChnoAZ/vyYf2oPu6fCrDq2B8acgFsuxhhO7oOqCvCoE8JWxZgafIE8sHuqLCOgUn5aBvbSV0WNK6fiqAMupz62X8YY4flo3H0AA9rXJ8pzEhfjcwRXm2NzbTn8MXTs1Z2OTWJxTmWjmouQcUTji2nImWf0omXtgGDSV02n9K1aD2PEuzo5zomyMQbHODjWWDgUpieTmFmKfU7oa/GFI1wLz087wqGMYhWj6NCrB12axlp1VIe8tBQOZdo26aIFyhhLW5MEk34e4vEfAaSP4zjENm7DWb3aUs/+XzOs3tLLmH+mNR6tj+Y9etKzVTwBXzXE50gPU50ky8M76Ud9NMbgcxzqtOjJmT1bEuugx+rgVNNZetEKZIwtR5IjHY0xET9R7iiJUG+kPVKtKUdyi2OEYV+JPsHfsbYU1FSQcugwWZH1/ES7mOmVBfBgHh8BjDFe3fKShhTlZJJbIRggU3ttxqiuFFFUDcffCNwYUw0x1fjV1eosMubWFgFaDBhInzZx+B2D5Wf9zRgToZN2MlE1lTLvUoxI23GcalyD9xhjvPbqCt4/P+SLol2PXvKh2phIAz6L55XBGKOE99h+G2PrkWSBkQU+Ujcmkks1LJExkboxyjn5keaCOT4f/tgGnDmwB63i/d6B959kWMDJpBiCBRmkFxqVwBjlJyX0GBOBORpnVYlvrdjaoRm1Y2I8fJ8j+3pK2tYIrjGRPAJ2KM44yoG0EoxQXG3+jDEYU5Oww6GWf+OtwatutnrYIYxt1Zl+HRtRu1Ytj5fPF9FD1iBcnkvCoTRKbUXy3Boeyh2lCKsa+ZE8AhOB2i1O7ebdOb9XS2pHOV6Ta3xYuDHV+P9ky0g/jIm0G2MiAAzGnEgej4hhUAMGPVHx9OvThZb1YvGpin59jsGY6uTBTv1xJd+Y6nYvj7QbUw2z4yI/Dssn28onuymuhcU3tnlXTuvSlFo+4xGIzQk5ov3zYIR0CZdbZGhcP4ZMXTS88/5HbEwPES1FQ1paA1GQlbyZj95ezG9pLo3io7AXWY7fISbGUEtJRY+tL8oQo+QPKBfcJzu4GKJVrqU4GeWPmCxKF8T2ktjS1441WlcicI+J7GLhJ+Mf74gDpugYB4+kEYqpR726dWkcVcDupBTK9fHayjMBqMrcyltvf8ivuhCMiobMgzvJjGnHaT070qhkB2+/v4JD2mgETJiwyymP/dAT1EVISJdJekEy/Vr3jLAibWH1Svqq0eJVeXmYoBhpCUDqe/axbR4P2TeoyyJv7mswjHj5xNOOr5pAZb/sYvv+77WFNBhBj0CKaGwDGh+xw/K1XfBZvtLM8rXyXAP2Mt/DEW1IhUItdZU6x/NvPi6ufMRRim3SifNPa01dO2jCP3V+qP9WqODHX/E3xkhmTbI4QvoTzOKX5eSQXVIhXFtDeQ2NckTDqY9MewJHfq/ug1NF2uFkUgsqPGQjuERFyirYPhjVXNcQaYuhy5n96dY0DvtX1tFc8YuRMSbCW7j2dTE4jg/HX5u+Z/ShUyM5EP/8iMyLwbUb1OfYhh/5JqGUVg1rY7S4uSejC9GYahnK/6l7gjlOpN3mjurGnKir6HGz42pMBG6M8mpGrqevg4mOZ+CZPWhbv5b0B6cij/2KkSWW2hWWY3B8DnFNu3J6z3bEBfAexxGt5ecljZkHPfEjUowxx5NtOVkXx7ZJF9c22KSCQLgZu/lhZyVDp09iwrntcQrT2Hs4h6DF0bzLPJJMan7Y1sAxx/kbU71vibREfgVzlIw5gWd1sI0SdwqtnUcWbswJXBUt6ERSp4yJtDuOcq0h0XUa0a9XBxrX1sSymMb+RJIxBg+vOneU26kY3bATA3q2oo4mo4fu8/1JFxBArzmeLC16rP7GnIB7c1XwU95qPR3jEN+mN2d0ber9rQ47Ssb51+MWDhuPRZbOU89/vJX6OkP1aFLKisUfsv6YbSris4+WsbmkKT26NWfXF0tYsk0XDdLFxhKLYVMYg7HzILo2PQb2pkOjOkT5oLjY5fzJ87hz8uk08rsYY3Ac4SrXK08AAfWegPFPj8bYwqIb0q+3bF4nltoidhQIxapmCE/iYXC82YbuYY+x/0hepFZtH2NOyBKIPz/G1LTb3FoPj7dTDa/JI7SRGGiMxbXpBL4xth5Jfx4/ywM9pTkpHEwrJCq+Gb27tKFh3WhPll+LpJHfh4WDW8KhhCPkVriqGSytMRG+p5SdCMzGP5wQ6cnJpORH4p3V1ZhIuzFGSrqeHEdlY8zxMh6xoSrrKPYvPahJ9o3gGhPBUxZB4+THRUzwOQZ//Xac17899Wr2SoqdjoiMidBbXYSM0ei7xk+TLt3o0a4BUQbvsSoYYzCmOkUIvLaan4g9HeH4adZvIH11j2BthgmSlniYjBJrOenkVPPweFnqk+smYstq/sbYNrzHGFs2XlkGkBxzIlm9xdqYk2FCNVB6LJU8u4aoaueHMeZUOsFPfo0xER2UGxPBjagjvyJSNyaS2z6jx5hI3RibC3DSa2mNsXDjzTPCYWIbNKdvt9bUr+Xjnx+DMSeSuiaUE3VjIuXy3FyyCku98S0tqaTd2WN45PohdKhbreef7GxMhE7MkADp4mD/i29m95uKgXYPg55/sqPlJ7j3Gu/3aGoSKXnR9OzagU5xubzz4rtszBBhZTrb9pbQpVsHuneqy69L3uGdP3JEk8tHL7/H+sLGuivqRP6mz3jp04OC67UGUmZfa09jHPXJR/N+fenbrh6eWsJxHOtbhkju4qvbhIG9OtCk+m83UP14PFRO+nEpjyxeT3SbjnTr1IauXTrRKpzHpm0HKVI7sg8FBThdBvPEPZM4rakPjMFARIa/Fh3O7EXXZnH4jICa8wkJKdi/faWaNyzGGIyJJE1gwjKBVD0OM0ZtwhTYkgjuKu4Z6jSsR+q67/kuOUjPbl1oWbaDvz31IbtzhSZkY0RXnbAMOVE3xkh11al5VBbMGCO9jdQwNGjTndPaNyZW+3j+5/lvYQHn/3Ev/h8TuBRkpHI4o4CC3CySUrP1hTpE3rGjHEzUJZzq5eEIU2MMpdnpHDiUQsLuw+SrwSpYXt6E4eMH07uexTP4tehlHxVOYjJJafmERWdbapJ8HWMM5Xnp7BWvw3sOk1EcjDQLXlWcS2LCYQ4mZ1IaNhi1uDaY2wLR9Ln0Uoaf14Oe3bsw6pz2pO3cRQFhNv/yB6VN+nBet84MHDyQ+NSd/K4v+Mf2bWRPURMuOKczvfqexYBaWXy97gBijY+wpipUFeVwODmVlLR09u3Zy57kLErKyzl2OJHte5M4VlSFsXpIP8oLOHwomQOHj1FkP7VV5PPt6lV88NkmUnJLKcw4StKxPAryxfNIBmWiNFXFHNHBJ+HQEdLyKzFG3PRWFGWToMv7g4fTKfA2FprLkmPfqpI8kpIyKCwuJDUllSydChxTxbGUwxzQRiSjUDqJh1Ef8iTzgOx96Gg+QR0cc4+lyX5p0vMwO/cmYhdio4sH18qtKOSwFuf9SekUVoKRBYwxlOQc05incFA6Hissp7zwAO+9vZKv1h8gqzikAKTLV8lJkJyElBwkXbSc8hhjCJVkq09HOHwwgfRiO9oWRYrKbsm6jDyQdIyiSkcXwuCWllOhg7bFOJ48EiPeQdk/mUPJR9knO5QLrheJoDAzDavHoSNZlGNxq8hIVX9TjpFsx2y3xiy/nJKCDPbu3qfNZw6VNvhXldP67MsZdVZzT5xdWMKleSQmpnBIY5qcVYz9HzV0EM6Y01vKMhCUH1QEXbwFIVRF+3MuZ6zawuJgdHioKMzmkObKwaQ0Cqw9jRqsosrs61r9QiUcSUwiUX1JyiwlbJ3PNfhNxfHxzCwKYvtmaWqSK9llVaHIeiBgeUGOfOIICUmpGhPhC1bzurZgGcjOBw8mc/hIAodzSpAYtUgp+WBK0mH2HzpKbpkrWSGy0mSzI+kkJyexbVcCKXmVgkNxTobsnk2FKENlBSRLXnZpiFBZPkmJR0lNO8q+vft0iSMcV0h6reiS7DQS5B8HDqWQWVypC554Lh8zlNOb2kghW5bkitcR2SuVY4UR/fMVfxI1binaKO/ctZ8j0tnyKk3bz5J3P+KLDanSt0oLnfTKSpfvHyY5o0j9Up8kt1o8Qc2Xw5J9OD1HcUNzurSAI9L7WKHthRCJYBpjqMzP4KAW9sN7EskqBU0NXMGD0i9JseeAYk9ZyHhw7OORGigvlP6H8fqnmBDSXDqSnCIeQghXkHE0leTMIs1IS3QiueVllMvPrQsGFd8O2TmgOqFSjoo+La8McZDtw+Qp9h4S7GDiAX76eRPJihduVQmpsk9iUrL4l4BUqSjIJklxqqgkn2SNYYGdINhHnFwjlCCZGteDyWnsS87G9kctGGOwNj+QkESKDSqqe1Rq1IuNSV989D5Lfkogq6CIY+kZpKbnU5ifSeLRPMWXIDlpqSQcPkKi1ocKDK76XlEZJCf1EHv3HfDmW9jCxThUVsgR6Z6gOXI0pwxPXGURTQZcysTzWnu28uaR+FucE/PIaiMGJ701EG/OHc6gWCd6N1hMStIRu0eB8wAAEABJREFUjhZWQrCSiqpKCo4eZv/+A/LPHIISaCQlHN+dyWPPxbqia3w4mg/Jsqf118S0PMUzxSPNnRT165DVNbfck+xi5EzFihGHZe9DHMgs8eD2x2iGpKcc8Xze+iSS5dqG48m1IAoy07D2TrYfroQTKsrm0OGjpB49GplHGp+KsOSIzlWKvC6lZeXYy0oP5paTkZpKouZw4tFcT98I3olfY6RRQZbmVwoHFMeP5pZ5jV68lI0SkjMo1qWrI6hbVub5pIq245SVB7F/ysHWjRCO+0i2+iu+Fu7poYLCEkHXoV7DJnQYcD5nNqli4+/bKI9yMXJyU1TAYelau2N/OjVtSpP60fh9juZdNgcPHmFfQhoKzwS00y/QWnUoLZd8HW4S7byQ70apr6nSd8/+I2QUhUUb1EH6qOJnuuJ0Ipt3JOkDSRCf6LGBuSyfQ1pH9wk/vTBEICAlvdeg8zvB6KZcPHQqcyYPZ/q0EZzbszfnXnAWnZsEsP9UiL88kx9/S6Je86bE14rC/pMajbtfyJhLeuvA05kLL+mLSdvJrhSI0Qbb9XjjzdmA47L+o7+y4KbrWXTztVx547Xc//LH7EgvJzoKNrx/F7P/+jIH8iH566e4+qZrufbWG7ja4t50Gy98up4SHxxc8yzXCHbVDYuYd/3VzJ4/m1teWEmaMZB7gKVv/I3rb1jIvBtv4pG3vmB/diUBXdJX5Bxi+Zt/4wav7UYeefMz9mZXUdefyKsP3s49z60kqxb4S47x7bIXuf3Wq5hz7dXc8MjzfLUlhbAu9p2kL7njtut48IOfKPIboslnxT+u5va31pAVhlB5yOsrf3oidjCYYInWhSSNz2EOpBcLt6alnDRdGh7QfiVTewjblRoWrkURwFVMPaz9yIGa/Yhg9p82OKxYbvciGdrnBEsz+ezjFXy8dqsuICulUAWZ3h7ziPaYeZoLxmNrWXoF/cgNKdB+5aA+NOzXepRTKv+ujOKswUO4tHM8bmUhh5PSsHCo5Jj2WEeyigiKVipQUr3XPai9bkGFoFaE/DLtSEpkDmpvK6iw8eZ4vj5s70866vl2UZUGlH/rMToMtqZDi8a0aNOW9o1qY2Sxk7GDNmZqv2jjUFpeBUIA4UT651KSl0WSdD+alsIurZkJurwqF83hhAPs3J/i7UNEgDEGGysTtKYlpOYoWqkTMrzA5KWncEDr4+6EdIoVe5wQFNbtwpSx59PckTTjYGSjxAOHSTqSSKrWtRCiF+Mi7Q8SNT4Jh9PI177Vg0aUUysYLaj5WscOCidJa4bcRzDpon22p0tKNhWa55ZO6uA9VWUkad+SU1Gqc0G69sNl1O58BtOG9KG2EEIlIQYMHsqlXW1N+pVq3fNidQrp+RXiL6Tjr2yUm0FC8lEOy4579xzUXqhcOLK09DQa5YzUI17cPnysEDWA1tiUlHQy84rJST9KitYqTn5ktKD2B97ZRPvOsrAfpHxJWQVBewtwMq7Wm8KsY5E9uNaY3XsSSNXm0PuIIR8qKas8HmupKNYckS7CO+LtayBckqt4luStEckpRzWvMimTLGOM9mbHpPcR8c6gTONmbSjXOCFdOFTkk3josNYrjVt+FV677O1onqZq/TigPVye9gseraV0wGgObP9+E6Vt+3NZz870PPtC+gRS+XbbEary9/Lr7irOGtSXXt17M6RbFL98t5ECwCfmMqlKrndGKEg/wv7EVJIOHNX5SKKrXNxadalXN0CZ9jwWtzg3k+P+o3OkETWhMm+PY+d8avW6ZcH/nFzKKkOUFeWy+1AiO/YkkWt5WCbhIHb/ZPcmh1KyKNM6j2CBtv2YOqQvMZaZ7ONqnU+SvQ9qD/jnuGRRpDVl+VnSMYWDmtO5pS5GY5qbkU6i1u1k2XCn9o7pdg9r5arVxsAU8YzYFkHKOaq9V0ZuEbla/w8fK8YYQ6Vd+3XeOKDzhu2nG6ygSf/LtRdqL7GuzkNBSvKPcfDAQbbtT6VAscQnG5fRhJFjr6BXAx/higKOyPdT0o9xYN9+2SGD4opyMlOS2bE7gaP5lTpbIDyHgVcM4bIu9bCPUVAszDyKtxfJKEYKKc4dk5+kkZKawu6d+0hQXAsLr6roGMvfWc6K3w5wLL9cqIbKwmz5XjIHdYaQCG+8qX7kntLSYDSOhxXPkxT396cVEFKfLYpR9Dkm/exacKywSvws9ERyK8sp17i61SCf42oepnryDmm/U8OnutnLjDFUFWRix/Hw3kNk2q2KPgyFi6M4f8wwzmilOYp0kr0Oa093UOtAdkmQoM40SYp9yUdSOXDgAPuO5BIWL3ROS1HMSM8rF/8wuelpJGlvIvcCtVdob2Xj16HUXM0W8TV4fpKgsTyoM3yRzmrlOQd4540VfLkljdzSKhzZMnK2PsJB7VlLwgbDyU+YAp1rDspXDst/IrEiiMRF7Km9tV1DExRLj2i/bozBrpnJOjPl5BdrHUz17hRO5igUisTzgOK+3Ze6Oqe6GqCy8qqT/tCdpYhY29Ve+OiRI9o3HsHKwCpYmqe4n0F+QSFpikFHFc++XrmKD9doDdb89DlR1G8UR3lFlcKgQd307j4S1Ac7B3KLy8iVDklH8wlh7ZTt+ZndVruKgaXSJRI2FTcUf4q13nr7f3v2UqyyKois+nVp1K4XE8ZdSu9uHRl46cW0qTjEuv3pENuKK0YN4owenXS/cyEXtKzQnc1Rwhl7+CnBx8WjBtCze2cmX9SG/b/+wkEpY+1je26TMfLM/GPsTUjhsGLJsaJgRKbgQZ0pbRw9YPfNIQd1lJLSilPjvfzNFW44ayOvLNtImyETGdK/Mz26dKBb185cNOQizurWSGOJbBQiS/xbtIgjKz0PzQLP1MYYSrLSsPuUpD1HyK0Ev/Qspyljxl5O97iISo7WrSyt+YcOp3Do0D6+/34rGVVhRE5htf0SZL9y12Asie2gzVWr06g17bXnaNuhI716dOVy2axFxnZ+3J+B0eCVZh9TXDhMop3/xmjPV6AYk6o7sWMcVIzZpX2CrgA8buGyIlJOOtOJvUxTprgc1LrmofzPz38DCzj/mX2wAcnyT9n4Jfc88hKrv13PiqVfsEMT8btvf+GPHXv47P13efPbRG/yFOz7jede/4Tftu/XREmjwF7KBdDXvk088ciLrEmy3ELs+WYVr61cz9ade/n8w49Y/FMKYTVZeW4k4lCYvIlXXlvFj9v2s+9QOvllYXzRAcJFyXy4eDnfbNvFL18t47VVO9AyKWpx8GZUDO3bNlA98mYcyyGmdUfqUcThtDLimtfTRHBxnXq0qB8kPSGbY8k5uA0aU1sBOOz6ado8hvyUzAhf49q5Q0VuEq8/8Tee+OhXNvz+C88+8Sz/+OAHHXS3s/qdN7jv2U9JVUAwpaksWbyMz7fuYt2Xq3lh1TZKi7PYs0eXnCkp7NOCm7HzB+5/6FmWfrORT5eu5o/kXL5dsoQVv+xmy7ZNLH59Cb8eLdMimMrK91by7fY9bPruU77cWYp9rI3CKlTm7+PlR57mpU9+YsVnX/PNhgQSf/yMNz/fyqYtv/Dy66vZX1RJ4aF1LP7oWzZv2803K75ijy4SUn/8mJsfeZdfduzk16+W88jTH7O7yGDKs1j1wVI+W7eTjT9+zvPv/0heyFC490eeX/w5m7fv44/fvua9Lw9SejSJHdpI7DuQzJGCSo5sWsNLH//I1h17+W7VUt74fJcOEUTsLdu60rkiex+vv7qEzzbvZU/CYW0IgvgcHcaCmaxevIRVm3ayUf7x/Ee/U2DAGIPh5MeVr7gClLP1i+W8vPxXtmljcSgtn0rhRqslY/sPvPz+N2zZvpefPl3KSyu2aywrSVj7Ibf+9W3WbNvO10sWc8/j7/Dpuj3qzzc8+sgzfGD/vZjKYta+8zwPfrRVnCCYfZC33vqYNdJ327afeWXJOkpKsvnqrZd5cvUeb+G0Wxjru3ZMCObwxWsv8Y9Vu7220tRtvL54FT9pDLf8soYXF68hrdiyVj+sTdQVE8rlq/ff5+2vt7NjfyJJuSWidcBUsPGTFbyzZjubNv/MS5pbe/KrkEVlU2V6jTGefcJG+Fp8Etet5ePf97L118944pU1pFQKyZMj7STLLUzlvTc+YPXvu9mty5gMjRt+TdJQMd8uW8qyH2T/9Wt5ThcL6dpVpf6yjFvvf5Ov9RHnh5Xvce/fP+JgKRTs+4a7HniHHeVQVXyUt//xFK//kUdVURIvP/wkT3/6BxvWr+O5vz3Nm5rf0oKs7d/z0ntr2KR5/9vX37Pi2+3k5ifx4sNPsXSXVVSXLD99wxdb9kj/r3nylc9IFfjY5i+586+vyCd38/NXK7j78ffZlh+m8Nh+Nu9OJ8n+D+50eZGz/zdeeedrNmnuLX37A1Zsz7NiCYdcz15VhSm89djD3PfuZnTXQGXuLsWYz9mhuWYRwxETUZy6hZdfW8ZaxZ69Ogjk6HTsC/gxZSl8/NYyvty2k/WfL+PFldspNRoNbTBCloEOdV+v/oRPf9vDxnXr+PzHfVSUZfDO4//gzfWF2APU1k/e4u5Xf4r8lWE7LpZOScNoGaEdCFXZW3n8gZdYc6QU+9fHPn/jBR5fsdeLsanrvuLlj37UIWIfK158jRW7S2jdIJoCxctln25j2471vPjsO/yUHqI0YxOP3vss73/7Gx8u+4Kfd2RLkljKHsi3dqxZxgvLfmPL7kMcTM+lUvNcnsCxTWt5/eOf2KR+fvjWR6zZXyA62VAGMiqV5SSwcWcaRxIPkpqTw+HfV3L7/YtZu/EXXnn/J5JSk1i7+ju27NzDx6+/wfvr8jBOAGP/1MmhA/zxx2YWv/oGL645iOV3ePsGvlYs3Lp9Hc+9+AEb7b+BUZHHd2+9ymMfbtJ8h5LDm3n17U+0tuxhy49f8Pw735FWLuqTbCjVsAPtqlCSup6H7nuVH4/JgSoLWPnKczz1xRHwxxLlq+JY8iE2btzMh2+8wguf7KdSMy5l/Sfc98RS9smnTXmaDldLWKmx3LJjG8s/Ws12scpXXP3k921s2/Qbz7+4hM3ZIUwwm0/efp+Pvt2py50k2aRS/XWkRZDNij+LFY+37djt/Wmxjzdlen22MdxLqmVu/5aXtZZYe3/w1gd8ua+QqvxEnn3sSZ5bvZEN637jqUde4N1f0zwfcENh8bavkbvIBhZqs6L9LFu9nm2aq++/tpgl6zOxj41NEVnITzbzuuz4i2L0pp++Y8lXG8kMVfLTyq80pnsVF9/hxc92yhqoD0pUP8agF7m6B0jbsPaEj7y5hK+rfaRmd1nzJ2LDurQpcZtx0VkdyNq7jh1ZhngF6WPpB0mrbMmAzrEU5pcQ8kNZ2jaWLv+ETQe0Nqz/mndXfEN6EIoSf+SZ55/hi1838c0nK9meXczO71ew8tft7Nv+C+8u+4IjxZUc/uk9HnvpQzYc3M7Pn7zB04tXc1T0UaUprF6+jO/V58ku1PoAABAASURBVMQ9v/DhkuXszA5iL3tc26mw3KZuA1o3a4Q/6FKRnc36g2k0aNaBeC1NUU4xm35Zh9PpHPq1qENFVZW1OLFNWhLvhigoDuuOqAICtYmri+KNZ6JTfsIVRRSZegy8cDhjL+lH8bZPefr9zzhS6uLXgdxeUtlp6cpehaFoep01jGnjRnJ+e8O3n33KbwfLtU5WkF8ZTb+LJzB/+hRmTp/OqPN60YA8vnz3VZZvzKDzuUMYe0EXjv7yHs9+sJZcXSj98N6LLN1wjE627cKupP36Ps+9/61sq+sWrXclim/RgSrWrXyT17/YQVyPy5g48graVu7jjTdf5ZsDZUQF1C8dQvf8/Ckrf8vARMfoAFJKuS5eXfW0sgoMf37UopeqPNa89wEffL2LnfuSOJJTgalerzZ/spS3JHPT1h958dXV7Kv+6+NujaMVH2PlB0v5/I+d/PH9Z7zw0W/klZfw27JlrFi/l82/fcmqDRmaL8fE+6AuTJP1AaiMirTDfL32W28vsvyNxSfmgvaXrmKG1TRt01e88OG3bJFfbPhmBYs1v4Ilqbz5t2d5a2MObtlR3nj8WRZvPCb0cjasfpsHXvkJRXKtfz/z3Guf8Lu3102nIITGR2glu1mqObtt5w7eefktlm3JFRBS1n/Os+9+g/23ovenZOpiUPjGa/rXP24F5VVBqioqkEsex5H6Koc4vGcdq9fsZuv2H3n6uWXsKJCh9XprnTAKk//g4Xuf5s3vtvDbT1/w2MPP88pnG9mydTOLX3iWB97/Aw0ZZek7eOvtT/hVsf6blR/xztcJhDQnkn//nOfe/ZrNuxK0/87SPs7oI49Lzs613P/we2wrA1OazpI3PuTjn3ex60ASqYXlhI0cRRb66btf+U3r+C/aWz7zwe8UaI7JgxQepCSQt+9nXl/yI5sVF1d+/B2Hq1zKs/fy9lur+Fl7+29Xf8zbXx1AoRg0x7x5Kl89kpZHaXEOu7cf0qVXPru//Zj7n/2KdPEM5iXx1rPP89bPaarBsQ3f8t7Pe9m66RuefGE1e0qsbPe4Dvl7fuLeB15g1W97Wfe7cP7xJt8klmL0RWrL5yt5Wx9+tmjf9sl7H/LhH+m4VYWsffs57nx5Det+WsWrn+2j0uuXiyuJQcXsD99arn3DTn6TX7/yyQ6qZEtHbcdfIUY+4oXJ3PIVdzz4Kms27uZ3jdHf/vEuv6ZVgvHjiKON2dZFMpN28cVnv7B153Zeff4NvkuvonTLj3z4/Ua2HzzAO089zo0vr9Me15C99xdeen8Nm6T371+t5uWP11FQJRNafpKtF7ckhQ/eeJ+lv+7WHjCRo/oAb4xPcgv45sMlLPllp+LdVzz/9nekV3oURJ4QJWVBasfF4Gp+ht0oWrasRV5qJpmKl0W1GtG4luvZN65lQ9Dlko3f9k+SWL8EQ9pG8X1nLRt2JrDncI4+cILPF6Zozzfcft+bbC1yhJXPj9/+ctx/nv5AfZPj71y7jHfW7pEP/67zx26CYfRY/ZT96fXpI5n9A0w7tNf/8bOVPPLSJxwuEm44gy+Wfc2WnXtZ/fabvPFdMoQr2b/2A+589ktyhaKv0zpjLuWrjVbWr7z2+kq26mMeng1dPLH6qL3rp7Ws2riXrd+t4Im3fiSXEMf+WMUt973Oml27+PW71Tz21AdszJZyoXy++mgZy2TzjT+u5dn3hF9WxsZlr3Hnkyv5fv0PvLFiCwXpe3hLc+DH7XvZ/OtPfPT5OjIrS9nw0as8+O56kD/5Fb+KM4+wZfMWvlz6AY+9+a1iuaEofaO3b/w6NawPFOm8+48neOidX9i0fgOvPvscj779DRs039Z+9A53PbGcRM3hUHEK7zzxHK8r9qEna9v3vP7R92xSPLB76C8PFFCU9gcP3f0k7/+4n99/+Zy/PvIKXyVVUJFzkI0HU0jQmeVwVhkV2o+/8eZynSml+29reemtNSSXimm13az9jPZiaz78gPfWbFG8PkxKZimuzn8B4excs4K3P9nIxm2/a/+9nC05IRG7hOyYqGSMwQhPrqdaiAM/fcYrK39jm+4hvlr6EW9/c0AjICmyj507rrBK0rfzyutL+XrrPvbqA3SWbsgCfj+h/EM898TTLN1aJKxC1n64jFUb9rDpty9Yvj6TYHEybzz2D55a/QebNm1SzHyFV39IEfMi1r7+Ao+v2iW6EEnrP+O+x5ZwIAihzK289sZqxdLdfKU+vvVzhr7zJPDRx5/yi2Ladz/8yM878ylN38fGQ+kk6NyaroUjL3E9r7z9uWLhHtav+YSXPvqDnLBEqR+RWBEiY+MX3P7ga6zdtJvff/ycx598hz8yQhi3nJ9WLOUDfXjdsn0nS9/+kM9252LKsln2wpM8sPh7fpd/vrFWfi6NXfFURrZi3yvvrvXOSh+LZqXOSsYYzWvXNh9Prvb8tpK7cxOf/7yebds28dpL7/NjipynKI23nvoHj3/4E79+v5qPvtzO/oOH2J98xPt/PFRm7ODRB5/l04MlGOOSvO5LXv7oO8WxPXz/6Td8oTPowQ2fctffPyVTQoozd/DkQ8/xaWJY+I4gskHY6mPI2vUDr7z3jTdv132+ihc/3kRkq+AK16Ia7b0aUC/eELY6l5ZS6YshLra2GmvTrl0d5fYtJV1rZYt2LQkX5BMM1KJONB5NVMvmxFVmkpRh8QSrdrzilK28+tpKfti2j30J6eSXufijozTHUlmiu56vt+zi9zUreHX5ZkpkQ7/66kZYeL+u+mB7k7p1D0erWnHxAN0Waxzs2NrxcOs0okfHNsTJtw9oHXhj1W9s2rKDLz76iHe/TaBKXPIU15974xPduR1gz8EMirVe+rRnLknbwGM6G3971EoMsue7VbyychM7d+/m7adf5avMWjSPcsjY8YPOGGvZornyy5crdW+yMfLxTzItpUSA9hwVVUEqy8sVW8PgxKBvgvIJQ7nG8tW3PmeD/PiTd9/n/Q1ZWkeOKXb8jb8pPm9ev4FXn36Gpz8/4LE6vHMja9ZuZKvOo8+9+D5/ZAQ1Thqb48I8tP/5+b/cAtav//O6oEkChi69T6N1nVp0PW8Q180fRocmTRk8eTIzRg9m7MD6HNqepG+XLmtXr6G08xXMGXsZwwYNRPe4VFZCg27d6dakLo4mDKW7+eDzfXQfNZ4JowazcFhLNqz6gh3FkqSNn9xUEov5ceXXpLe6kKvHXcbgIQNoE+/XxUGQlHXfsrm8PQvGD2fuqJ4c0mK+M9fSoiCCN53spM7c/RsvvPAKb++MZvr0iwlQRmFJFf5oB2MMhij8AUN5YQnF2jibgI+A4I420L4YP5UlpbrOAwSzc6ZO29M4rUNzOva/lJlXLuDOS1uQfLSSi0aO4f57RlM/fT/7SiBXm91vshoyf/wwZk0dQNoP37Hf15z+7VvS7cxLuLyXePToSbv4WNr1v5irr55Mw4PfsGpfLONnDGXiuLFc0vQYq77YTlbqAf5I8DNs3BCmzJvF5Z2jsY+jAKfwQO2WnejdLo7o5j24ct4MRjVL5e0vEzlr/BimTpxAh5JdfK3DU4oumROjujFl7FAWXnk5bWpF0617Jzq378CIMSO4+uar6Vu+jVV/HCP3wK98l1SHSdNGMH3qBRTu+IUNyWl89cn3mN6jmDzmcmbMnKIveG1o0LYdbZu04mLBBrTIY+mydTQ7bxgT5BdXje+tTd0a/siqkgmNF8T0y/avvmC705ubJg1i6JAL6NoAwv4oKvd8zzep8cyeNIJps84ib8N3/Jqt3vrVbgdARfvaYG6MoerYHj74JpFzxk1j3OALuGxge2JwcLRZ+2L1T8QMGM7EMYOZO/k8Mn5Zzfq0Wpx7bleaN2/P6NFjuP3BKbTWATPY6mLmLLiOuQNj2b7pINRuzMCuramrDayVt16L3Xb3NBZNuIJx42Yw5/IexNVrQd9erYg1FiOSpJLnf0S3oX/vltTy+QioadMXX3K4bj/mSpdJM0fSIHkdSzelIaNgtIO2dIXbf2T5dodpV49m9BXnc077ujqDREPhfpb+eFTjOYqpk8bTtnAbn6+3S7UR+UlGARxvrjq0P3cEt04bxITpFxJ7VJsuzQ0MBEMGK2vfz5/zU1ZTrpw1lGGXX0iflnXw+R1K0jbx5bYgQ+eO0LgPIyrxZ9YedOl3dhfZrA1Dho/gpvtm0738AL+lQMuenWjfNFYHZIhp3Ik+XZrpIsUQ06Qjp3VsSic7T2bP4frLGrJv2xEqg6WsXPkdgT4jmKx5P+fKiQzu0ZIW8uFerRsR7UT60+asoSwcN5jx0j/+2AG2FkK3fv3o0LwFl4wfyjW3LWRgIIONCUU069ed1k0EH3sxPRqH+GrVdwR7DWLqhOFM6lLFl5+u06EMeYWrsQlTq/lpTB1xOnV04KgVBbUqffQafCEXd68nC7pYAxkq+W311yQ3PIdrxl/GkEFn0a6ui9F45m36mt8LWnH1+BHMmNiLw4o9WwssmasfoOgoP285So/LhzJt+kQmnduW2Hpt6N6hhXyzAgL15BsdaFrz13FFcuI12ky6VIpVrTYd6Nk2DqkJ0c3od1pb4gMB9aOC777ZRHSfyxkzVPNwaC8Cil9SgTpt+rHg6lGMHTWBgXUy+HVXMfW7dKVzi1jqdDqL666ewvmdYjUPXYzjQNZu3v8ikdMnTmXS0PMZ3K8NMX4fvsp8Pv9iPXXPGMHU8SO5rGUeX3y9nVIMjuKzC9Rr14n2zZtw+hVD6NehneZLKxrUr6P1YQR3z72AZo1bMmruTCaOGMzIHgG2b0sUlUMo6NBDF3BzZk/hgfGd2PDpWnZrH9vstPO48srxTBgzkg7mKNsOaDGo24Zz+7Shtg4LflH//umXpDQ6h1l2Hs0ZS1zSd6zamAXGaGytVniP0QpgY2OD9h3p3qYuIRnUxLakX+821FXcRO2VVQG6nHsZ06dP4aHp/dix9lN2FQXoc3YvWsdGYS/bcn//XB8AFI+mD2Hi6FEsmnAeDUJQt9dFzJ86knETB9OxMo0dGUUU7PmRzxJimbFgBCOvuIyzO9Ym5AbkDzv48Ps0zpoyivGjhzLr/Np8u/wbkqqQ2mGtL3bMs/ly9TqiBg7B2ntU+xI++WoHMa37ckanxnQ+81Jmzp3HjRfGs0cfRGQuHPXjRI/RY2y3oHYHps4dx9iRwxnezWH7jmT1FqxN7AWOcbWuLv+CjOZnM3v0FcKdwsRzO6g9wMVTpzFl5CBmXtSGxF37vHmjBo+emsd1cbWphzw+lY/EnTnS0/nSFjnykR1aYY1IrPWljhQUOsYYjXuY1gMuoW34MBu2pKLJQJI+ZMb3OYPGigsVTpTibYiN368hJa4/EycNYt60odRL/ZkvtxXSpucZtKlbixY9LuTKBdPoXLCdVZuzOHfYCOZNH0nTjA38cKCcvgO60kSXxoMnjuHO68dTP3sPB/MgfdMXrMtvzhSN2ZRp0+hr+2BWAAAQAElEQVTn38enP+zCRIFdT7CP9A3qpi8qynA0+QC5ocb06lIbR/D0fTs5GujK5Wc0wwlWYrReRflEGwoprjrUqu1wUP2J6nwuA5tDWQiNkWVanWQDQxhTtwUXXjGUK+fMY/rlXcnZtZOD9k+ZRcdglxuh4QgvHNOI0y8ezKTxgxncpzWUllBSVkLYF6XLGgiHqgiWV0pODM3bdqVpzm5+UjxsN2AYVy8YzewZC5kz9Eyaxwd0UbqT9clFtO5X3TZdbcPOpnmdMEWFVQQ0502gFk7OEdbtOUztzpdz9cIJzJ4ylmtmDaNJYTpbdyRS4EhH9Ts6nMV3q5bwe3IZMTFROCbSRzvWkdKJX9c1Gn/I3fE9K/b4mXz1CEYNupjzOtUlZFfH0r0s+/EY504fwdQJk2ibt4lV63PEwMiuEdo00f6QUo8pU0cwc8q55G37hY2H9vP7thx6XTaYyTMXMv7MptRq0YZuLZrT75JBXNIlHkdxd8KsOUwYPYiRnXxs1oVlSJwdjZlGAsqT+WDpOhpdNJFJo65gxsIpXNquIVEN29Gna2McITvx7enfuSG+sCvKOPoO7EzjaD9+jdHald9Q2W0ws7XXHT5oAM2irM5Ci+2i/eFYzcGRDGpXxZY92sRUZvLxio00vnAs00ZcwKhze1DfX6UYIfx/6zUGY9ts7hVsBcGsLoZWvS7gmoVDGT9mGK0rE9li/x8QwnWlm+1f8x5d6dymGX0vGcVV19zMuC6VpJQ0ZeyEKTw27xzyD+yjRBFo97ffc6hOX2aPH8GVFzdn83e/kFyYzWerf6XxxVOZMuIixlzcnTgdgyskvX2fTrSMjyamFmSsX8Oao/W1pxzOiMsupX+behj5Jprg5w0dzYzxQ5g9rDcFB3frQwtg9XOt/nBowwZSYrszWXHxlquG0C6mil1rv+FATC/mam+/4PI2bPv2OxK0FBjHaJ6GQXuzCwZ2pFGTTkweex5dW7SiT6+2NKgdheUa3aYvAzo1wu+LGKxh/8u5eeYgxk8cRIvCRHamBAG1eToYWnbpTvd2LTh/+GDmXrmIiR3zWKVLlIK8faz6Non+YyYwccwQpl/SgB9XaP2Jas75PZpQK74Jl46fw/VDO+MArvVz5Qk/r2FTRQcWypazx3Rm3/ffsFciY3waFaugcKx446ov8qJOXbvStV1LLtW+fP7VNzC4QTIff7FXWFH4ZCuv36rVbXMas6+exoSRIzm9YREbt2YSqz3AbQsmMvbspgSdZsxaMJTGppCvVnyH2/ViJmudnDXjAgo3fMvaxGKMMbjhMOo9ST9/xXeZLVmk+T38isvo2zRAyB8DR9ezensVo2aOYOqsK/Af+Jm1+8ulgWg9/aPp0qMpmTt2kVzl4BiXsvIKKvVBrii/VHo4REmOo+SLjcatKKXSLliECQlGVSofL/+D+hdNYMaoixh2UXfqBaood3yK7d3p3Kw2jjeSsZw3bFS1//SiMGEfabqs3fTHPhr2H8Sk8ZO5dnh34Uo1r0c2PzkZ7Z2qqNOqJzPGjeH6O2bQRpezS9algL8JI2dM095EfM5owJ7Ne3D9sfTr04umdXzSH5J+/4Jfcloxc+IQJmm+9Pft4aOv9qkXBtsNBz2mFr2uGMstk65g4oTTCSfs5GBlgB59O9K6RXuuGDGcq6+7kfOjD7D8x0TKkjawancV43TGmzb3Enw7f+Kn9CjOPb0dsXXqc97gidw6sSO/L1/F4foDmTfmCqbMmszU89oTE1ufAb3bERftk2Cwf5q+UeczmDRlEnffNZZau7/hk+3lNOnejY4t43DLw/gbdKF355a07X62xnIG94zuSEpKkew6lFvvn0o7zYeNGVUEGnWmd9fGRGkNhlK++uJX/KcNxu5Fhncu5fNPt1PvtIF0136vv3xl/qJbGdmujN+1/tZp14nWjZtwzqDBnNW5Pn98uoqEumcxR+e/yTPH0Dj1V5auy5DOxtujGWPI2fsTKzdXMHbeWO2VLuS8bo20xjpUViSx+tvD9J0wnmka387lu/j0lxSPFi/+qqjX+rAbiJGqSXz86TY6XjaK8aOHcO3ILmz9/Gu2FsrHJcdVMopv6z/5ioS4M7h2wuUMGXouneqhD3thAq1607d1nGKFH0jmO607fYYMZsrMqxg3sDExjdvQt2Mrupx1MVOnTuXeCV3447MvSAs247w+LanliIwAvc/oRus60d7fZNr11dckxPdnts7+V1/UmM3f/MTOxH1sPRJg2LhhLJwzkfM6xtGgUxvaNG3F4LHn07O5zikffElZ92FMlt2mL7iYso1f8tW+MoyR3bw/bBCgS/eudG3XmsvHD2X+ohu5PC6JFT8cpODoZlb/XsAlk0crVg1nbF/4bNlPFNTvwDldGxDdsBVDp87jqgtbejPLGANuEV9/9hNun8HYs9L4TuV8+dkfivDgc10Pz/bOJmMcm1G30xnMnj6BcWNHMDA6l3X7dbBs2pP+beKo1bwzo6fNZv6wfnRo0oTTzr+MS3o0oW77rpzWpj4+fzRUpfH+qnU0PX8sE0YOYeG8MVzQuzO9OramWb0oT3ZjxeNuLeoTVaOB4mTIREl+Pp+v+JFQ7yuYOGoQM+edQ+G6rxTXKkH9kcrUPOEQODrfZB/YSk58Ly7oGa8mV/GgkE3frubvj79EUtMLmH1hQ/zN29LSyeCPrTkeTUVZFcFghWKaSKRDEKP/SvnlkzUcaXoui8ZfzuBhp9Ounl9+EyJ947f8UdyaKycMZ/aY0zjyyzfs1HlVyxEnuaw4RWyYmV9AuHZ94hwFU+ltjPjbpH56l9GlB1j2yXY6XDGVSfLpeaPas+nTr9hfXMz3Wn+sj8wacynDhvSjWbSL3Wc27N6FToqbGMkIpfLlmj20umg4o4cNZtwF7XELSiFUrDPGj/h7X8ZE+dicaeeQ8+tavj9SijFG64L0sV1W2djc8eGXDY+s/57ttOXcbg3Z+OnnZLQ5n+lam+edWYu1S3+mqmEX+nVsTpueZzN51gzuGtONRH3MLxe7pr3O5sr547X/GkknJ43tB4vF2Y+9m1Phf97/JhZw/iv6UaEDUCgmnvp1A0TXbUSDmCI2fL6ad5d+zqo/UjCxtagM53I416FT+yaEtMkvKSpH5znk07jhCiqCIYxdPzOOkmca0qax6+H5W7WiqckhMTPSE9f2qDKPpIwQbbo00GErpK+VpVSFjS4Xy8nLyifz2EHeXvoZb395iHodWlOrSh5vtyPaEBn0nzHUa92N4cOHM/mceL5ftpacUkPtKE0AqxT2CUsvF1/ATyAgxcJUB42wZLr4/H5xsnjVyS2j1JMjfdSXQJ3a1NUmq0p9tX/qIzomRsEbjulAWZVzmPc+/JTFn+4lvnkz/MFySiqqqCgtxv7D65XlpVQG6lI/LppA7Xjc/Ax8jVoSq+gZ0oLTvG1Lio+laK/Ug4GND3PvLU/zyleJxOgSvlqbSCa7lukSLb5+XWrri5wPl7ySbDZ9sop3P/6cNF88cbGxtO4/kPg9K7j64bf0NTJIPdnY/omrSskrVBQLhuvSsVMTilMSyMwupDj7CMtE/+7H6/E370BccRKHCmprbBsTVH8rq2LpoUOeW1aui8VKysrChLOzOBauTftGMd64uvFNaBVTQcKxMk9XGx+hnIOpBbTShZwrPqGiUvmFi89xyE7NoagohRUffsJbS3dTp2UTooMe6Z9+jDcu+bJPaa2mtK3vSl6YotJKjM9QXprB0eIAbZrUFjxEWDgt64Y5nJpPWVVYvAzBsMawKpr4+NrEmEqCugiOqV2XaHsLIBuWV1QQNH6glMSjpbTo0A5PX417p24tJT9MseRZbkL601ulBaxKXIwHT8wsp1mrJvIpyQzXonOL2qQdzSSkVmMiHJIPHSXQojWtXeFo/EsrQ/jll2TmUViRy/qVqzWeX5IZXZ/GdQKi/OfXlUQLTd+/mQ8Wr+bDD9eRGfLZWSGwWo3Vp4KU1Fwate9ItLVBqEg2cfERpDAnh8K8DNYs+Yx3P/iOioZtaarDQUgfbdAKXyH8YNhPbO1Y/GFwKyt18AiJt163kjL5t6si+opaXiGEcJnsqv5Ex1En2lBafIzU4lg6t6mH50NOFO27tBCjIkplf9eb+GHyUnfy5jur+fhjHYSLfdiLnsqyUmkIVcUhjV0F8XVjpFJYoiqp0oVQiTadYTeP7JwS0nf9LFut4nNt+rq0rE0lYLsuFxONS5cB3fAd3c7GgrC+1hfTtEE9b97qdIFjkJAiEjJL6dChaWTMCkqp0pdw25R2NFc2SuD9jz7hzS+SaNS2KT4rAEM4LNoGbbmsV20WP/Agj77zPZmBOAHLKausEnvLwaWsvFJ9cAX/16/FoqqS8kr1z0NxNb8qRW8rYeUhAnYDo2q58EpldzsK4czDfPbRx7y3bDXbsiq8DbFbVUFlKEA9xYc6sbWp16C27B1G04TCI6kUxDaka8380ebL6JK9Upud7JJ8DmmD996yT/g1M0Db5nU9+0cMJBYa70p9KS8tKsZunErKK/FHxxJfK4p6jRtQO5zPT5+vZPGyL1mzp0C+HIV97DiEyvIJah5Ft25Dq6hCDmvsyNrHu++v5MOPP2N3jp9anou7lJZXqL/WIiUkHqukRcuGkTEJx9Nem65jKfqQYzdv0kVDxMmP9c+Kf2lDMMZQWVLsxQfTuAPtapWTnFMlswep0kBaiQcSs4hr04b61u8rg8S26UyHWi6FWQf56N2VLFm2hp1Zhjr+ECkHjxGlday+N3+LsPPX8fu0506lNKYZreLDnqza7VtRrzyDw3mACRN2gJIcskvKObxpLe/I3j+l+2jdog5ueSF26lFV7tnLrV2HGB9e3BDVn14XpHSoJFeHpKWy+xd8tycLJ9pvwdjH8+2yIg7mhOTbTTx9qqoCtG7XisZuATt+W8vrH3/F0l8PE/TVOk5naU9Ojo2RZcXkVPvI+9L5t6wo2shHqiyi9LDZiWQ0ZkH8dTtx2WlxbNu+nUPJ+zhY3opzOtSSrcI4Pj9OaSlZWVU0btGAYGGIfKcurZoFyNacKykrI+SvS3ztaOIax2sfkE9ZYS7rv/6UNz76irzo5jSJDZJXXCm95TeKE/nqWy35ZMDAkfQ84po0J0ZrXVaRKz9qRklGKvmAT8lV8l5NDKO4dnDPfuJ6nEOrgEte+j6+WZdEk3YNyD+SRmZJUB+ts8nIr9BYuETHGrJ3/MiPGc2ZNOpsagc1P+RfxmP4p59QJQV5lRQXVRHXtAm1dEFTWFUlH6/GkyJhJ5qY0mTe/cftTJ1/J4+u2kWL3v05rV19jF0XgsXs+P1LlqxaxceffcaGxAIqS8soVs/rNGhKrbIKUnNdBoxcyI3TLqWpm0de2Edcw6bE2LYctY1S25QraFensvpPIDpUFJdQojU5tnELGipqpmi++WWz5tEhrXUllCumV/m1B+h7Lu0rtvPOks9JrTT41FdXBIk0LQAAEABJREFUeqPH1Vy0a2l1VZDIeyzhKE7zVjTy5keZxjyEUawh5xg5iu3rtV9558PPyIprQbPYUITIWAu6ZOUWUpKVzFK7H1m+mUCTFtSLb805/aL44L6HePTdXyjRXg23gnLZsrS4SDEJXSBn882KlZpTn/LZ7gKiowPVdpZ2BhDPY5UN6NIuGrseVYWbcFrn2hq8cvU1FFlNFefKKkIePzVQVlpB2DhUBXM4nO/QsXqvW1xcht1WqolQSTa/rP6Yd5Z/zo8H84iuEw15aaSFYujeopbmXZj8kgrhGykBns205ksrr/6/+jHG0jkUH97Bh+8u5/0la0kscYn2hU8hdcsrqAwafOFyglIuum49xWdXczFElVOLuNoxhLTPyc0rIiNpq+z0Ge/+lkWTDi0xGQfICDekU6tYT9+Ckkr5upGHSV/xrQgZrUGQmJxF/TZtiVeMDIVKtc5p7GygCRtSNn7Nmx99yuLPt1PsjyLgqYd4GOzT8+yziN21jGv++jqfHipH2pCbXUhm8k7Fr89Y/HO69inNq/eANlBaKpcS7buCWvPzrc3keOVae0Lhautpz2HHS2Ahu+Qe2cOKxav4cNmPJJeEcXwRPq5a7euq/xXiU5JfIRuFad25PbrRISsxiZJajWmiD8/2LBPbtB11yeGY5lWF+la3Tl1qaS40aVIHv8cywjHtWB4FWQd4d8mnvLE2lWbtmtgwjzrNv3qqPPnqU16IoBawdp1bU5qZKlQZS2Zy1RFlkHeYlUuWY2PthrQwMSaIE1eXGLeET1/5iOzOFzOsQwC7liQXO4rF8Rq3EKHoerSvb0g6mo99PNdR4VBSJg3atybOjpviSZnONX6tV6UpORRVZPDN+6tZ/NE6wo0aE+fzNMDRjAjrt+sVE5jYo4LFz7/N2yt/YHd2iKiYaAIBPz4XQuJv33AwhLFx3WdrLsbaKSeF1PI6dOpo51yYoOZByDWeebyx0D7QWPQwpG5cW+0/OyjS/PaZGC44uz2/vXY/977wOUeCAeyeLqyYZW1nyU5JRpx0mZRj23UuOa1DHNnHcghqT7T1h894c+lXfLLlGARiPPnl5WVUuT6i1M/k1FziWrTAZ+0TcmnTrilFx1IjeyA0B5TUyJEtv/H2e5/y7sqtFPoCmBCE5Y9B+VSplRty6NqpBRVZiSRlFeucd4xPPxD+R78Tqt+Cev5KcguCxMbHER8IUK9eDAczK2jXoVlknuqSv2WH9sTjUqD9md1rWdHqGWHt/UKSEfK3oHPLaLKks+sGKdca4XXIrVAcq8JxgoQ0Fm5ULPFx6p2lkV7xtWvjQ4ZWjCuvCIKdt/oYnF2Yz+HN32kf+Qnfpji0a1VHc65EtjGKFyGCiiUxih1RdrBFaz8+FBdXEHbzdWavoEmriO6hcDwd2tQh88hRq7J64Hh5RspRopq2p4ECQihUorUgiFFTeWEGOfnFbPlK9vlwBSlRLWgT55FgTCSv+fUFRJCTTZYTR4f6PqwdXK1vzfzF2rtXRtAsjS7eEjJKaN++Wid9JKnSGHn83HLK5fdh74NZJy7q6/DOPY/w+Hu/URoVEI8K7T9DhMqLPP61W3SkmVNAUn6R1sww3p+yFVa5/SciZUZryqOZJRQd2aG18RPe3lJEy0Z1adj+NPrHHuCmm5/mza/34osXb9FUavwKi8QnlE5KoY+WrRp6ckJOc9o1cTialCXuyG6ul1d6cTdMUW6IoHyyvWJFZUYyWUmpBOOaUT867NE3aNWWQPkxcgrDlIf81IurQ5S/Dk2bxopPhBfBArKzS0jb/hPvfbyKr1Ki6Nwi1vNvx9pNmDVvNQWVRYdZoT36R8s/4Y/UILWiNQayYUmVn4bxVkZt4hv6KK+spFx763BYlJXywcoQfr+4FRwls6qe5NSW/iGqYuroXBEPOkNUWVwXXK1VFRogFUWguv1VDKEqi/TCaNq2ruX1MRTThHb1KzicVGwxjidXe3fHZ6jK3sOKX/IZMmMMds/uYnACsXTqezbjpoxjQNRhPly5nVDdnlw9+2IKflrCE6+u5LNNyZQ7tbD35Va68QHBfJLSq2jdpaE3J0PyocowOBq//Mx8sjIO6R7sU61zB6mrmForiFzBEeHJr+tVYhQjnHClZ+cIxAODJoBjDOHsFLKpR/NGrtdPR+fJRjqbpCckklFRS/v2ph48VLPfAG17KqiQHyuMqhakIuzTVJYSqlXIZ+xscMsKSCkN0K5ZXY8+HNOQdvFhDh0tFJb64v3qR0oFYlzN/1955Y33WLYniitvmEvfBqUcSimm4NBG3v14NUv2BOncOo5KjX+FvVNRHAgpxoR8McRGGzDSP2Mf73+wUme6T9md66CjIfZRq8285Cq+BhUrvcr//PxfaQHnv0RrTQ5Xgbqi0sUucutWL+Wro/WZNHYIQ09vhVNRRVhO52jDZCeCowmPJpV1NruJiuhoaeXhPgsNextwY4x8VWXha/8TQbO/4qVXwddyEo6jZNQgfLvhaN51ANMnDGX6jBk8dNMk+ja1jeBTO5rgldrI+OM0yXS47nNJH5yE9fx4OEDzJtFUFFVSpUDlavOWX2Kooy9uDZvW8Q7ApSZMWAt3SUEV/oZx2JAtJSTYvkZauuq/i8/veBPZmzyOD6MAGtKi4EppI972TyPOmTqMaVOn8uBfptG7UTRSCcdneUR4EQ5SZQOHbGb/mh6aiMao3Ti44mcvPB0tLDPuupPbx3Tn6Dfv8NgH270vhWq2TCJJMoNVQentEhbPUHQLLtfX0CnjRnLb3Tcz+6xGxLUdyF8fu5GJ3fx89spLfHCwgrhaPuG7GGMUsFxCCvz+QC2Mgklsq55MmjiUKVMn8dAd0zhbl83l2ljZsfVjFPVChKS3ka6OozHS+IgJPh1AI3AjPtLHBb/6zPHHIDKvZoxwUDK2KkRlsS16MXnqcGZMmsxf7lnIZc0kShsey/6EH1H9uLgyhDFioNfKR8xdE4WEoyaMMRhAQ4LP8eGo7EpH1/hUdr2FUMPm6RjUxszSiIWwRKWCqw6ri8ILYrx+uNjDjit1jeWsgl6JFUBUrldRQW22bKt+Y+W7OMagV37j4nNUtmgnJ+mFFWYQXsROrhMiHNOCQZNHYcfz1rtuZs65TYg8QlTBkyMndfzqd/56XvtoM22GDmfi2AE0jTLydatbBFfoOIQJq1+Oo5oxSBVA7a58Xpdxo8cNZ8qkcdx310IGd6tNRVmk78b48EtOSH4qy2P0n+Pz4XMi/HyOgwwhDFCTLeL3+7CLjLUZxue1gyM+xiuHZSCrnW73CWtxpnArL73zO82vGM64UWfSLs7xFldjDLafiIe1Z0hjJQYYY3Ak10i0rQedGHqfO4jp40Yw95qruHPuhTRAj8URLhiclv04u42P9Z/9xJGYWJo3a4ajORSWSthHeFaWK56O8I0xGGPUol7LbvU6DGDalOHMnDKd+2+fyVmN1YSjA7nanTpcPPNa/nbtEGIP/8Dfn/+UvGAdomVkRzysHay+Vld1nZMfT6YANleGkc5+bfJlbhyfg6sNT5haXHBGVzK2/8bm3QfZmhhk4AWn0cQt5qO3PiGz1aVMGTOIXk1raz6HQWobjXfQXoBLYFigyAABRo2uAJKDHuOoLjs4jiufqcvAQUOYOmYYi66/gVsn9CNOOOB4v8YYHNHZhB6DwcblyrBsIJ6/LP2QHwvbMk26XKSv5264yhNrjPGwHStLMatKG+PaxUm8887X+HtezsTxV9C9saHcBkvxdDx86yEByUN+62KMUVJZsgzyKeMQiA7gTU9OPMLCOA4n21BOdFwPYxAf/QSDVOAQo5tK1bCPHQO/Y7yYjXigPgUVRNzyQ7z91hroO4jxYy+mV5MoyrUBi1hFvx5TRGKwPIyUci0tBmMMiEcII52slOokhw6Ho+h/+TBmyN4Lrr2Oeyf1xThB1EVPX7/fwQ2FsbFKXKoJI5nlL7ZEubB+yRJ+Kesquw/mvO5NcdQ3jbCHqGZQn8SJoPzYp7IGjbCEJH+/kg+2wcRxVzDijPa64KiSLNfrg6Vz5TuRhGBK8pHKcJznI1Okc8RH+kZ8xPg8eZr6GCNc0Vo5lZpQnc+8hAaZG3hvyc80HXg69QhLDhZJ/TQY8Q1KH9cYHNF5a4lsqBbQ2lYVdAkphbVxNXXacPmgkYwaOpYF1y1iRO8m6MSNvdQ30kGjQUh4rjg7iv3hsCwhvijZtRXhRDSl+gkjNIrSN7Env7XsF01JmdGFYgbpx5JYs+QtnnxtMb8mZXNo/Res+OUovrp+Cg+s48udVQwaN4qeDRzNHbea358zwZ0AcfWiqFM3QHFmNmWKP3UDUTjSEfsYMLJJ2BdLh55ncGa7KLJKo+nd/yx6tnao1CVJeWxrZt3+HCs/eolP3nyCqy+NJ+zzUUs8SgqyqYiNplVDhwM/fciry38hy61DHROiKD+HStvWWG0/fshrK3/gSHEAfaPAxvRofVisJT7luhQu8EXRqmkU4ZwMMvSBu3ZMbWKjwuj+mg79r2D6FX0p3P0DG46EiPIhySAXxRhHMd9B3eCUxxVEyRjleh2biyqstSNQuzH2T6xPnTiKW26/TutbM49Uw67cYLSfq9W6F5O9/chkHrlrJme0asz5067n8esuJz5xDQ8+/y1l4YBisJSRjSHIjx8v5Y/ydl4MG9ytPlWVQU55fA4oLoXllz5pbOQfIelUg+P5uwFj++RDvuRiLI1rPcrFTgR1SWMnJP26WkCiooOs/+gjNnIaU0cP5ezODUFz0FW7kV9jLK7laTCSJZEY8Q/IeIZTH0++xFg51ndDYVQs5WhmIeXZB3n3/R+IHjBUdjmf9nEBXYpb5BM8IvxcwlLS50djHCKoAGI0xrjhSNkx2PnUvv95zBw3hFlz5/LQwivEL4iWfUTqMTTGYFSyOilT2cWWjWwRllgbIhEOKkfFRlGw+xve+iGHi8YNZdIVPakn/6tSG/YRnsio1fl8Hn/4esb0CLD6hRdZu78Qn+PQps/Zni4zZs/h4etG07meJQJHdGCwmU2OEynjPVYfFWRLozUMJwqCB3nj7e+JPXe44trZtImLokp7XGEdf40RDyltjMFxHNmiCp+JwomSwTQ4rgxgjAHZyw0ZjFzGCD+oMbV7F3vJGumWwT5h+UCjLgOZPmkos6bN5P7bptNbgbks6GDnuBtBtqheMsZg+aHcUQoFtQezugvP2hfHL2kVLH9vJccan83kMSPor8vGCq2PUonsrWtZmVyPGaPPJkrzpLzQEOWgMXfF0kgmnt/6IpOJmse1BauMYxAiNkOSjGznr9eZsdNHMW38eO6+5zqNTwz2McYg1jjRDXW+mM09N8xg1tiL6KJL07r142nWLp7oyjJKNM3C8tWq/BKqasdRPxZQXZIiuYSHNeds3RFPm3t9FZor//QLv2DH2lP8p74u3Et1Gdtx8Hyeu3MiXcM7+dvfPmRXGTg+H/4/9RTGYGMAABAASURBVE+ssP3C2Mxg9zQVcsBadWux+/uPWZ3cgGljr+CK3i3w67xjfdgYIRN5HPELSxcEM8YQlr1R702kGZsX7v+e1z47RL8xQ5k2tBfxJqx1FWyfPDTROQbtwypx7aWWG8TfuCtTJ41gyoQJ/OXB+Vzcpj5h7c1C2s8GXRdXk9z6SVB+54jYyFYhGVNNnkxU0IsxYmxf5YYqbN98UVE1YoUmXramdpHj+H24WguD6odrfFgZNuYjZhJhMbG5Pb9WurXpc+kV2kMMY+Gia7l9cn8amlJFVCvQwa+pYfX1xkz8HceRfdFjlEuu7OYIbhxkN3G1BfSIXL+SHZY7GIxjUEf0Ggx6ZD9TpyEXjxjJ1IkjuOXWa5h7STs1gGNxVbIyJcHzb7kQjiaBjdnGGKyt7Dj6NXZCjbyCuwJKCxyVjRGeQd22XISisoyuQi2GLbyJRxZeTO2EL/jr899ToT2Z37F4BmMM6KNXUJsLn11fVPf5ZMVQGBz137Me0suh9WkXMGviMKbPm8sDNwzRB6A2zLnzLu4c3YWEr97nH8sOaF7U1hnIFV+JNo5yyRErYwxGvMJBtcmv1Xr8NSbSZn9tX0JVilVap52A9LCDLJ8xRtQqu9JTaolXGBurrN3CGvvjzCSjSj7Z57xBTBs3gnnXXsVdOivVEjxo9XFd2chiu4TEk2A6H735Cdltz2XC6MGc0SqGCl0sS0tszKjSXt7KcK3eEuw4xhKLmzIVbRuOTwZSJzH4lFyNnY2hxhiM+upo/F3j4DhItpXvqqDX6mIcS4GmMMYIX5yD6q9Pe2eqH1FgRFyadYgVXx6g55hJXNYuWgwM6ENUZdhPvUZN6di2Pef3b8CeX/5gd6lDs74Xc/MNC7hx3hjG9WmGLzqe1g3FVPyNMjFADiuJVgcjGUpGDUphzdtmnfsxTeM9bfo0HrplGn0auBTpnswnCqkuRDC2DLTVx6hAYRqHCoz8NYydg978riohr7CMUgLeHYprZRsD1kYhcHQONK4r/1JZ3IyxuqhZMLG1QByNAb52XHFWAw78up7tu3ewPacu55/bCXwhHM2D4/aWPtZ1T5krYuQKXlluaNvvbObNmcYtiyZzUde6GLR30LrUaeAlzBw/nJkLruS+Gy4n3v7hPTvWGirHrx6HgoR0D+HXx6R33v0K0+0ynekG0b2Rjwqdk1zpK1RsLnEEAn7FbscW/yf9V1jgP0HGf8no+bXqOAogAV1oGW0mC7W5CNeqr8OLHEtfsCo18WNNQzo2KGPb9oMYn0Pt6BDhsMEfMBjHj8/4lAw070jLQA4Hk6pwRJe37xCZ0S3p2VzWkXcqDkGgEe1bwJ4/EnB9Dk5MQIuZgleturTo3JysXds5VOEjEBWFfeTXYCerrZQk8e2GZBszbA2dHsmpMsS3iOe07k3JSUigSnLNscMcKq1H764NaNmtIzF5SaRVSpbJZ2diOd17dyJaHMKuo1+9Rn2QLtYWaEpG+Xw4Ph82BpqAH7tYhRQ42nRqQkHSdnYU+AjIbj7s4+IGyxVA/TiO8eCObBLt2dPQrkNb3JzDZFj5TpiEA0do1L4jblEqOw5Cz/MGc8e408hJSVUoAMd1I/3TmPiMwS87OMpjW7egSegYv+3I8WT4jGRrg5Wxfwf7fS24ePxMJvT0kZxcgU99cXGIiXFwTCb7Eoto0a0zrdq0oSJlJ7szHfGIxkFPTDt6NCrnt1+2EBJdlL4QByQPbQxDuhgLapydhq3oFFvJvuQC9dGhKi2ZlHA9ereMFQMXqaw8mm5t67B/+14KHcnVAd4NGYxxaNq5FWVJu9mR6yMQJTtFeogJ+DHGEAgY0dtXvJTVa96WuJJDbDysvohXjCwTCvuIj2+igOdyKCkLR3CyD5FcGkuX9vFEaxNmfAFkdo+vz3Hwa4wkgIDPp8xRMl7Z56ByLXp3qk/C+t9ICzn4hBtwjOB+4Sj3KTfCO1lHEyAgwxvHJzyNXdt6ZCQcIShZTjCbnWkVdO7YGrFXD0VMZGEqTd7P1nyDz3GQCxAKupgWrWlYkc7vu/I54Uuc8phAwJtXAWuf0kJyK6Np1MSPX5cVxcEwds5aAuMNQDSt2zYlc/dm0mUrnxOtcdE81ZfLRq07Esjez9bkKsmKEk9Lhco+HOPz/BzHj0/9d5QIOAQlL69M+jpQqQ8UJhClPvtEa/D5fdjH73ckwxAX15yeDUv56bddhH0OUQG/+BrhR/T3xxgoKiBPc6BRYz9+XQ4U6RQcqGUIaGwcx4fMD0ZtjiM9/CpDpT5++XyOeDWlS5MQm7bsJizEGGsPXE48BrsRgLpcekEbNn32A6mmPm0bC6rF2TEGZeCPo2uLKHZsPUCl5VvbT1g+6vMZOnRuRva+newr9RGI9otfhHvEIw2VGRns3HeEFro8uvG6oTTIP0qa4kcMpeTnlUhnh6DsFMaPp16E3Ps11h6OwRtH6W+qisgqdEQjvSoqICoaRx5ToTGLdQvZeSABf8+LmXJRezC5ZBZUEt+wkWwUpFg3RcYfhfH7MMbBXx0f1EVUFReo06YV8aVp/H6wCGs/OyeCVep+vUZ0rF/Fhk2J+KRHwAa4U+yI91RVViIBOMZofAyO4yPKMahKbm4xUfENCEh+cXE5mCiMeFTqsGWiYoXrkLH3IMVxbeheBzL1UbBB47r4fRUUFoeIVnxBjysqFNchitM61CUtIQWjsXcqU9l7tJyOvXRAObaLpZ/8zOFiEei1bu4qFzNMZRHZhT580itUXo4biOhhN8wmUMvTI//wXnICzeja2I9fOjpG+Bqcbr1akb1zM9u1WfRHRxEluaaomNySEPUa1xb7kHStwMTE0rprMwr37+Gw5oLPqY2rnbJjDP7WnWkUTONQqvFkpe9KpLheW7rWB+whIaw8prl8MMTWdftw/dJB80VQMH6s3j7ZUBX8Psfj4XDqExXwYYwfO31yc4uIqhuxe3lxCSEngIdvwNqFmPqc1iaaDb9sp8Q4BNQvxzHkZxUS0toaL1mV5aVUhRyipb8JSAflftnDyvBLBwOY6EZ0ql/J+n/HR6KjASH7AwEC4oPWxsZt+nNW81x25DSkd9vaBNTXgHg6QvTF16FV6zpkJx/FjXaIKc0kId2lTYcm1JJ8rH/JScNBQ6Omrahdkcim5CLq1vfj/aUQDbrf78MxPpRh7WZzuyZ3bd+Mcq1HOY5Dg6hyDiZn0KBtJ+JEE5JsA9i9SsBfwbZf99Bg4Ok0EbBS4xPb5kKuv/VO7rr5dh684zaGdG9K1wsnc9WwDuRv+5kV6zLofvqZNHHySDlWoA8SIJNxyiPju8YP6tMfP3/PB7qk/PDbvTTo2pPOTeqA5lIYCRSRsR/6A/U5e9B4blg4k/51C9i6ZSOHsqFWlINTqfqvX7N05fcsW/0ln/64n8KmfTmnbQxJm7/ijXfWsGT5+7y6bC37UvKI6dKHM1rUInnzl17bx7Zt6Rr2ppcSVSdKsS2M3ZuEG7XhrK6tKTjwLa+89SVLP13Dq+99QXpsU/r0bk+8PsyH3BCloTjOGTaJy7vXp7K8CledtVO0Vh2H3MPbRbeBjAq8p+ag0aJrC8oO7SWhyMHn1JI8F0f9dZp0pF7ZEf7Yli9fCGj+eWTVPxoclex+pDx5J3tyHOFEiQrcsiTWa4/TosdZXLNwMDH56RRWhjChcq2zMTjGoTC3ECc+MhcKCktx/TX8ZWfLunEnOtTJ57cfEjA+B+s7fsvdCM9n5D8B1fz43FKy84LS28GpKKfK+Ij1N1KMtHvdCG2d6BChkI9ovyEnp4SYuIbS1aGspMybS6ZhM1oGCvht5zF8klU7gPzNR+3aUJKyneVfbdG6jR4X+58KGBNFwAfGr1x+61PFMZn8smk/6fnFFJe6NGgUi98pp6g8SEDzjJMfOb9PdvAH/Bj955dcn8+WwW/bJMkEYmnTvh6HN28n3fHjj4qKcGjYgeYmi+0707D6xjjaIxhDQMkEfMg82HHv0r4ROQe2c0B7cZ8j2pArHn7Ki/Mpd2vRJMqPU16sS0lDdCDC2vqE2JC8ZRvJsc25bPxsJnQPcOCYS+tOjTmyZTtH8ROIihBo6kQIq3+jJN8YnxefQDqpL0b1gCHyWAL1m3Ah2WV+GjTReJpK7N9A9Nf0L4JZ/esQVTsKxwmTsO8oddp0oFnHLjQMZZKUERLcISthL2W1W9K2vtF8cfEF/DjqhDGmmod1KOjasSnpu3eSWCn9o4XjrbRWRwfjk+1r0KupIplDtOaOo3E8cCCLpp06gHFkY4OjMTWEyM0vo06jhvj9IYqKK/Fr3IyTxdLl2+gxcjJnNxNuXgK782vRq1k0hxIycBzJzD3KwcIoerZtiH2saWzepWNj0vfsJllx3ueLRtMabw53boM/8yAbkoPyJx+ORSbStwhtiIxjR0g+FiLg9+Me3cHvKTGce3obamv/2DqQof2/tRns3p5Gox69aCYeYe01PTaNWtMiJo8tGyLzwHEMrmsIGIMJWPv4iIk2lBTlneQ/RZSE/ET5yjmoPVl8+97MuH4CXcM5ZJdWaG/+PSt/P+JZGk+IBOoNazMTkh/FOY58MIttKRX07NqSqswcwnXqESO/KSstk3U1TkaU0sPRJXEFRvvzFpQcPUS+Ea1Tyj7Zs3nHDuImPG+TqJBdWEiR9fE6fnxlJYqLhkAUQnD1Y4ixcp1idibk0ah9V9q3aUY4ay9/pIY82/mEZV+/1kBHvuG3NoiJ47QOddjy61YKJdsf5cfnOBhjiJK+NT4U1OVjWNrYMXY07glZtejRo5GH57d01m+MD5/ofOKt7uEXveP40LYdo7HzOwZHuTEBAj6D958vni6NYPNGHTrVFrBfF9EjWp/l6zeqGHw+B+NEemB1cV0/jomnR9t6pCce0r7D0RkmlT0ppXTs1hb7GDdsM5q2a0cweTsHsw0+JxYTDhJ2/MQ16Ki9Ujrr9+Tg90cR5SBbKp30moAfv/FhNFamcWvaB0rYc6QEx3EoSU7imK8JvZsG8AhdZU4duraMYee2/ZQLx9FYhbR/CAQCGBOQfSAQXUvoKfy6NY9Wvc7hugWDCOQdpTwcVrvBF1XL459xOIGC2FZ0rh+DX+tMruak8TlE63KzSvqIPd3b1ebgjm3kOH5iAj7sk34kiQOpYXpfMIxbR3fz/uZhpc+nPWJQNnTEuxXdmvlIOpissuqFB0nI9dOta2ORuxj9p0LkNU51rChl/4FsGnXsRLNOHaldepRknR0dKZFy8ABOo7Y0UUyxl+8+2dIYg17x0HzTL4EmdGkcZOOWvVoX/dLVQYENMAR8wvX5sfg2VmOfylLyiiqo07ABfr9LgWKQEx0Qjh+fEH1RUSqLznHVL63BBHAc1TVePuUuPqjXjk61c/ledzrG5xCl+O5YWjWVFxZQ4jryoSAVWsd9Ab/4+fH7DH7NaeNvRsdmIZL25Yivg5N1mAMFdejZpa7VTjHEBY1UtcJbAAAQAElEQVRBQcou7Yv+oN7Ac+lfv4LMrDzs364tOrqdH3aXerj2JyP5GOH4RjQJFLBzbwbGF9CcrOSH73ZSXx9Au8eInViasLD9DenQymHfHwcJSm8nJkBYHzJNTB2ad25Bzm75sj0PWhsgIgzRfp94+gkYvMf219qybq/zGdHbZfl7v2Jji1/8fI6DydzFpz/spLjhaXSqnU/CkVKvn8XJe8mjPu07d6ZpdBm7t+4VXwfHF1bcAr8EmIDGAAd/tB8ok451iHHT2bXvCK3OHsyYXvGY6Hi6NnQ4mBi5EzHZKSSU1KJXm3qioTp2gjFR2CnuBKIISDfrEyEv3sXRvZWfXVt3UOzzy198mBB6fPiMwSeYAfx+Hz6V0Z1QbnGQeo3j8M50OjNFHZ9zBp9iC3qObP6F1T/tpURlDSLWerb4P+n/Hgs4/6mqanJY9zyccIj0rHQ2bU2himgGnDcQZ9tSHnx1BesL/FQk72JTZphB0yfS5NCX3PXEm7y2cgfZhVns3rWPXVsSSMnNUb6LQtOZq2aey7G1S3jjveW89WsZo2aMomMUckAffk0Hl1gunTCCzrk/cMvjb/DWsg1kl+Sw/rdDNB04nIk9K3j50Wd57IX3WfHLAUqMtYJrfRhi61Cw+weefW0Ziz9YylMf7+XMydM5v56PZueOYnDLHN58ewnPrzjAgLEjODPOR61WZzP5wqZ8/9YSXn59LWbAIKYMaAh28hkXy74s7RAp6dkkyBbZ2RlsP5xGxtFU9qfncXhfMpn5uRzankrtPsOZdXqAN594lkefW8zb3+yVzWrT87TWJK35iNe+2Mn+xESOZR9j8/bDlCnINep1OVPOjeXLxUt5c/GHbHZ7MXdcX+qUpPHNsg94/YNlvLElyNAR5xGjroaNQwAoS00lObuQxJ27SbGzOK4rcyefSdoXb/Dgk6/x3Hs/kFLsUpF1gA9eWcLb733EdtOTMefEUa6vdCFdun+++iveeGUVpb2HMqFfHLU7nMusSxux6tmneOSZd3jn6+3kVtVh9JxJdEj/nnsef5nHX/qA1euPQlx7+rZ2WfHsK3yxPczEeUNg21e8qXF94csjXDB5FH1kd9cFR6PrYug3bBznRu3hL4+8ystvf8+xwlK2bttGsPNlLLyoNh/942keeXYx7325h4LKMnZu3S9bZbJl8yFsFx1Hi6jGJdC0FzNH9WXLB8/wyMtL+Hx7FnmpO1iX5mPY1NHEH1rLa+8u54VPd9NvzGQGNixl/e6j5GSmsDsllyN7EkjNyGZ/QiK5mUc5kJxGVlYKew8cYufhbLKS97E9pZA+w6Yxtk2e9z8Be/jZt3n7yx1kH93HNuFk6PJqW3oBu3bsIzM7m23bkyhI36O2XDITd7A1vYreo8ZzRvR+XnlnBa+/+Q3RZwxmfL8GEd/yaSPiyoynDWLmmTG8//en+ftrK9lytIxjBzeRFO7IdTNOJ2n1qxrP13nx/R9IKqjSyCNrGuVVJGzbRmp2Hts276akYT8ubJPD83e/zLu/pOMECzTnEqiQ3f06YMlsdL5oBMM75PH0A8/z/Hufsy+3lH3rN1PcqB9XjunCz288y0NPLeaNTzeQVRHkoC5rMjIyOChbZB9O4qgW9ZR9h7SA9+Wyrg4fPvo0r67+kWOlVWTu3UJiSjqpOfkk6yNKXlUeiYkZZKYcZH9hFKNnT6Ll0a+49/GX+NuLy/lmWxKH9u8lQTz3btpCXr3eDOpUxav3vMR73yTg1qpk78+79EHoMOmZx9iTlEd5ShIJGTk6ZCVRFWhF31YVfPjca3y9v4zLpk+mS85P3PXYyzz51kp+3JcrG0HYdlwlV1azG4Gm3ftp8Y0mrlEHaqN219oSWUmDoQh04Zix9Cxax51/f4PXPlpHXnEBv8sPY/uNYHbfSl597Bkv9iz9dh9lgBF/TWNtYirY/tPnPP3OSl75eB+dL72ETtok9Du7B+nfvsXDr3zK+tQS8jIOs1mbDLR4hx0HU1XGrm17Sc/JZps2OhW+1lxyZis2vPkMT334JXtzq8g5uJPEEoeK4hz27j3E7u17+enzj7j+3nfYmtmYoRe35sc3HlO8+4NyDJkJG4WTRk5BHns27ySzQnoqlrn4QIcBp1Ev5ow5je0fvcRfn/+YT7dlU3JsD78c8Ssej6Bx4pfc/feXeeqNz9iYVIh9RCZaW2pEv25x/PT+Syxfv5ttiQXkaVOzeVc6VZrp513Yn/yfFvPwa5+TWukjO2U3B3KiGdivNYk/fsabb32geGaYMPVSmjZpxUXaJC1/9kleX7IRExNWXNxLUGKifJFxqVJ54KhxDGQnzy9ewWtvf03sOaMZ17sOlGSydtUKvtxjowNaPYzGQwSxHbloQGN+fOUpnv34Sw4VBMmRfyYXRdOrZ2uyN32tC7kPefm3YkZNH0v7qDx+0uY4Iy+V39cfpc6AMSw8L4Z3//EsDz/9Fq8sX0dGfHcGKUaufuIZXl+xifLaYXb/ugNfjyuY3q+Slx9+jmfeXM62zCKO7NrFMacrC6f0Zc+KjxQTl7F8by2mzBxEEwdc4yOA9ci6DJs8mtbp33G35sWTr69gXXIJlZlHOKz4lHjwCMVlmexPzuJY6lFSC0Mgn3HlO5Tl8IfmUHZuEr/uzaPvRWeQ99NrPPjmVySV+ilJ2cmObOvTEihZYaI5f+IkzvXt4sGHX+Lx595huQ7v7c69kOZHv+H2p5byx9FKqkrT+WV3Bod2HiA1K5s9W/ZzOGEfB3V4P7RjE4dLYhkzbTiND3153Ec2HfcRKw+iow11o0Ls276VPft3sX7jDjKiYunRfzhTh/ShsRayA5vXsTOthIKjm/h2exFnDBpPX98B3tf8eWnJ98T0HcGQHrVIOSBb5mWxe28i2VrPolr0Zvyg/hz44mX++tTrvP75b6TmFZKclk12RjqHU0tIPZxMltbk5INHqTtwNMM7lrLsg+W8+s6HHGlwHhMu7kioEoxnmhBGl9vB5C3srGzJuR3jqZQD2ib0OI4Pn+PgKPlUDvj8+HQZtPHHL/l92xZWv/csD//9SR59+U1+3F+A7rOIbNxFXP0afwzR5Rn89OUSFq/+CbfLYG6QDXUPrw/TPvx+n2atkB2VfUaXeBWUBjoy5Pyu5B7czuY9eVTWiqN2qJAtP67kzY8+5sOP3mfpd+s5Gm7AmFlzGdKtFlvWLOPdT9ZRt994rR1DaBoTx+Uz5jOiV6zX9s7q36nddxzXzhhCm6gKqkwU0T4oD0Zx7ti5zL2sC+l/rOSN95ezJ9iOWXPmc0WPWMorwgR0aPSFy6is05TRk8bS0/71Zw23zqL4FOeChUdY8dan/J5Rpo7g9ScMxJ92OTPOdHjrsWd48o2lbDxWROreXaSEOrFw5tkkrHyZB558nVc//p6ESKjBGBS3oUHPy5h5UTzLnn6GR7QfeU/7qcKgYd/3q3lZ+4yXl++j72UX0EQfV7p1b8S2Ja/z3k+J9Ll0IHlfL+axNz9lv/pYpEvE/YVVIDs7NpAFmjFj7ghidq3knr+9whOvfsw3O7MoTNrDnvR8UnZuJLm0FmdfeBqZa97mkTdX83tKGSWpe7SuuwyZPoGGBz7jbu11X129S2tEqi7Fs+lz4QDSvn6BhxevIa0qQGHCevaVN2bWtEsp/vkD7nv6PRb/kESxjTMJuRTpAnrFV1vJqZSh9LphowgaJnXH72xJKyJz52+89oEdj5W89I8lbEktp0HrHlzQy8f7ik2vLE+Q75Sxd9ceimRsn/ZHjuZ6dmIKaTlZHNydQGZeCgcPZ5J65BAJ2aUcOJCE3ev8sb+QroPGcXmTozzx4Av87aUP+XxjClX+VsyYfgkF37/FA88s1l4ikVzx2r39INt2ppBVcIyNvyYRd/ZYJnYp4/m/Ps9Ti1eyO7uIg1s2U9nmTHo427R+vsfy7eXUqkxhw167Ftu+yWGAiux9LH5tCW9pPu6I7sml/ZvS8aLhDGuVyZN/fYHHX/yQzzYkUyE/sOtV2DqE9hPrdiSTnZnE2l8TKcxOY9PuVK++aU+Oeu3HEZ5rJ3ZULy7t4fL+Q8/z5ucHdGCvZP/m7RRJtqNVQqYC49PlZCY/rVnDB+++wybTh5nDuhMb254pkwaS9OUHvKk95LLtPsbPGkGTyjS2Hskn89B+9mSUoICA0ebWGCPZ0PoCnU26FfHCI89pf/ABK6VjXpFo9qdpDTzElsOFgHC1X8A+kk/xUb75dA3vvP0+BxSXrhzWnvKjOyUnm5R92ziQF81l53Vk3eInefb9tZQpNiQeOciGb75mw9FSwhk7+WDJCh59/nMOmIZcPGUEzTJ+4/X3VvDix1vpOmw0F7eJkQldjM/x9Gx34WhGts3liYde5Nk3V5OYV8GBXZvIrX8GN4xpx/cvP8PDz7zFGyvWk1lprKaom8oNZdrffvjme7z54XJeWHWQM8ZP46KWAfB1YOrEs0n96kNe1vq+s24/rhrRHZ8l1D7XbyVHtWLatIso++1d7nnyHRZ/f4CCwiy2aZz36ZyYkZvBup8Tqdf9LLqxjbv+/l7EfypS2LI/k4PbfuHZt5fy+ms/UOfsyzinYYCsA3/w+kffk12FHqMUeRt37EqjikQWK4698PI3xJ07ihHd6tJjQH8CmvP3vbCavYUuhVmH2JDhEmNvPI2hXI7R/OyRjOlexhKt5W+8tZQjjc9nzuBOduRAOOip3/1M+sYm8OhDb/LetiJdQmeyXXsoomoRLknlq0/W8ubrH5La9mLmXdCIQMszmDe0Pd+8+iwPP/0mr322hTz1/UBKHplHEtiaWiyvjOassRO5pM4BHtLYPPbsO3z8S6LWtBQ2HTxKZvIhxZ5SWvfoQZ2M9bz13hKeemszHUZPZnDbShI37NY5O5cEfQQ5mpZCWmYuaUn7OJxTrHNOKpkZaWxPyCf7QCIHc3JI1hko68gu9qTlk6LxP1gQxeApo2h37Dvu/ttL/OP1T9iQLP0S0ziac4yEg8coyk4l+WgOx5IPkk8zzu1Rlx/eeZmPf0/itNHTuCB2Py++s5zXXv0ao7V+ysD6oLH3yW+t29fvdhHTL6zPsn88yT/eXMI69Tt113YSK5sxZ87FZHzxKvf/403tn7/Tvtf1aEM4utCu4si2PSTl5bJv00YynJbMnHEBhes+07l2OS//kM3g6SPoEut4vqpZJlof544eR9/KTdz1+Gu8+v6vZBcVsk1rz6Ft20jOzNf+ZBfZhcXs/fETXtF68pJ8esDgS4n3y5cCFez5/WeWLl/Bm5urtF8dilyOVvrgXD9hDXc88xGrNeZVhUfZsjOPjsOmckW9RP764Ev87eUP+XzHMQJVuaxZ/gGvv7uMd3f5GDL4dGr7m9OjRRVLnl3MN3tLuWLOZNpl/cgr767glcWbaD90Ile0j1I/DD6HyGM0g4pSWWtjheZXUrMLmHZha2IbncaMER3ZvOxD3nznY9akNmHm9AuJbxqb1gAAEABJREFUzU9k59FC0vbt4VBeOcZxsLFKTEH7wMunjKODZN5lz0pvruRXxabynETt5bPISNrP3vQSfD6fLhpdiG3DxTqP/PbK07yw9EdyaxnStu0m7eh++VYBh3fu4khREHz16dmrGbs/eZ831+wmcfcukrLz2LthC9luQ2bOGUnUruXc+/dXeOLlFXy3N5vo7udyRt1Unrj3Fd77fKc+ppaQvFv+oLN1gvwyef9mDuXXZvjMMTRN+ZnX3tM6+OFOeo4cz0Ut/XIt6WeMpmWI1E0/sPLn7Xzx7mvcfP/T3Hr3M7z/+xFMrWi2f/keL72/ijcVZ1cl1WP2nCE0CxSyac1nvLh4Ba+9+TG76p7D9eNO8/bl1l4+zUhFBi4cN4oexb9wm3zozY/XkVWSx4bfDtKo7zCm9Avz+mPP8uhz77L8l0Pk5Wpt3JdKZkoimw7n4Z3vPBXlkb4GjLlqEeObp/LeG8t5Z/lXfLx0Fc8t3YYbV4/GteszbspFFP36Ma/LFxf/VMSQ6SNpHxPL5ZqXjRI/05r6utaznWQV5rN/0x4O7EjgaG4WuzbtpNCNpigrg/27D7B1+z7WLn2T6x/+mL1l8QydPpwGR37WndsKnl++k96jR3Ney2hcTUpjDA6QuXcDuzKKydy3lbX6+Fzl+PA5alDrmeMmcIbZzn06NzzxqvxsTzbh4mPYM0rK4YNkFCv+HE4lSx9c9lXVZ1D/hqx89h+89vF6TJTLjn27tcZsJSUrnwSdjex+J2vPOlb/uJ8SK0Ixwsv+5+f/Kgt47vGfpbHxGDt0ungKrz5xK+PPbI6mPI16Xcojf7uT66cMYtrMBTzzyGwGNHSo1fp07rzvZm6fP4FJk6bw5FN/YfbZHejS8yIef/av3Dq0C3V80Kj3pdxw9Tgmjh7MwmtmMLxnPU+SMWBsoJQzxrbox0333Mp9V01k/ITJPPHkPVx7SVtMoD4jF1zDo7fOZuGMUQw+vT210WN8+ESPryWTZk9l/pQhjBs5lCsXzmH6RR2JVhBwoxsweNIkZo/TJfHcqUw+u73XH9eJpu9lI7lqhg7Hkydy1eiB1PWLwDE4xsE+MU26cdPDj/Do1D7Ur9+Ii2fcxNuPTKd3k3ha9bycJ1+4j9lnNsP11+WKmQv5+x1zuHrWWMZr8+jHR9dB03nu0euZfHFXOp8zhhf+cSfTz2tJjNi7gVqcM3wii6YNZcK4Mdw4fySd6gaIaTaQa2+axqRRQ5h35VRG9oy3quBIL6NSTLO+3PXEX7h7/Fk0ryWAvkS3PXsUD/3lOq6bN5lZowWvHU2bc8dw94LhjB89gkVXjqZ7LJRVBYmq15YLL7qAidNmcePUs2msPru+Olww4Uoeu+tKFs0ey9jzuxMveEyL07ju9uu4c+FUrpo2isv7NgVfPcZefzNP3z6Ni7rVJ77t6Sy6ajITRg/myqtmMeH0Vuo5WhwMGIMBkXTkqttu5q/XTGHq+BE88NiD3DuuF1EaxfOnXMXjd81j0axxjL64G3UDMXS9YgavPn0zE09vg9QWB+P1HwL0vHwSj9+/iEVThzP3mht46ZGrOKOJoVarPsyXnpPHDGb2vDnMvKAtUW4t+o5dwBv/uJpzmsfTost5PPLMQ9x8eVviGzZl/I1/4cUbh9KxfRuV7+H1eybSrXkdtMIz6qqFPHDDDK6RXuMv6kb9pl2Yc+tdPHvbWHo0qUuXi8fwvPxz+oWtqNukM3Nvu5Nnbx+vNj/+uDZMmjuT2WMHMWn6dBaNPYO4gIscS6/BGK2hsvllM67ib+r7gsmDuPque3npluG0knO0PHsMj9x/rcZzEtNHn0Wrun7sY+mQ97btfwmPPfFXbhjRmdjoJky/+U6euH0qY64YwUNP3s1Vtu8iMFpM5DY4tZoxftENPHLLTGaOHsndD93Ho1eeS5x49Rkyncf+spBr545l4mV9aBDlo/MVs3jzmUWcL1vUb3U6DzzzMDcMbqMxqM2Ia27ixQfnMuXyS7n+/r/w1zln0bp5V279+wP8ZVwf4v3xXDH/Vl56YBId60JMyz7ccPuN3L5wGgumD+a8Hq1o0+EsHnjqEe4f15P42o0Zc93NPHvXNEYPHsFDj97O1Zd0oes543ntyVsZ1jWe6Ob9uPtx+czoHvhNvMbqTp66YzLnd6hDrD5K3Hz3zdxz9VTmjR/EmR3rq+dIV+PlxjgYxZZgoAlnn9WXAV2iZfwwxhpGGMZE8KKa9eLGu27hvvkTmTxxLI//7QFuHNQB11ePIfOu5dHb5nixZ+i5HYkRHY6Do9xp2IbJs+cxT2M9deYUZgzqqq0etL5oKs/+/TaumXIpo2dfw+t/ncXpLWqJAs0PA7qc6nLJVF5+5jZmnNNOcyGKgePn8eLfrmPuiIuYfd2tPHXXaFpHFVAU1ZyJc+Zw+3VzuOfuhdrwH+GL31LoOfxKXnj4auYOu5Cr7/kLD00/i87dz+HhZ+/just70TDKijPYLjrGUSVA90sn8rf7r+HamcOYvug63nh4AQOb+6nd5izuuOcmbp8/lbkTL+W01ho8URjZydJDgHOnX8cLGvvBfTrTf/hUXn7qBoZ0aYJfeI1PH8k/HlN/J1/MhAU38Ppdo2hfrwEX6kPgrbOGMWH8SG68bhqXdqqDa6K5aOYinr33SiaNuJyb7r6Lh2cMwK+Da8g1GAcM4GvQkSlXzmD2uEFMnj6La8cMpI6Byqb9GHvpADo28TqIMQafaBDf86Yu4MVHFzF72EXMu+VOnrxlKC1rx3Dm2GncOncUE8eO4PprZjKkZ30M8Zw5fCYvPXUbk/o31VjX5YJJc3jo9jlcM2c8Uwf3o1FUNBdI1xfun8OkwYO4477b+cvE/tRy4hh85XX87Y5ZzJ4wTB8F7uGZ6y9TPDW0OnMYNywYqZg4hGsWTeHiDrXR7EdqgrWnanXa9ueWu2/iLs2LeZMG079lLIFmvbn3sYe4Z2xPYmMaMuyqm3npL+PoYBdQDLaLxNTjzFHzFB9vYYguTVudNZInHr+d6yZcyPRrbuSpW8fTrb7BPo4EWproBp2YfcMi7rl2mj4Ej2Vw/xbEt+vHvQ/dw51zhzJ86lxefmAuF3ZuRJvzJ/Lys7cz7ZwOtGo7QIe4B3lw+vlIPep0OIc77rnxuI/0PslHrDyb4vVxt3nvy1g0fyEjLuyGKYZ2Zw1i8MCOmCDUa3sm829+mEevmcBpTWsTrt2GEROmMW7QIIbpMDtjyADsn1Ju0mcUD95zN2POaE2UY3Q5HKDzueO57bpFzBw/iTEXDaBBbB16DVmoMZhDzzq1aNjxEu64/wEmaiwrFd3OGzWN6cMGc8XgyYoNl9NWB9WgBkLsMMaHE4Jwo4HMUvxtHFBZbTIZBvu4yl3KQw7nTLqBqwZ1pzTPxyVzH+Sp+27jhkU3cuetd/DA9Vdxcbc4SivBJ8aWXhlB+fGZU+7j9Wee4+WnXuDN55/nMa0n/dvUpkK4Z8x4nMX3L6RrPWgz9E7efvQuLugQTWG54bTRd/L+83/lim7xdL74al585gVeefp5Xld64+XFPHfjVNoHXHxN+zL72vt44blXxP9ZHrh6An0VY4LlLrEtejNj0X3Vbc/x4NWT6N8imuJQO666/wkevWEMjUvV5/hWDJl6o/ZtL7P4hZd5/r6bGXVWewIVLm7H4Tz92GPMOrclBQUuddqcyx1/f40nrhxEl4YuYSCubXcGjzqLdjYIyK9lNM9PjS+Oy2ct4m93zmHexKEsuutuXrhlCM2j1N9zR/PY/ddz/bxJTBl2Nu3qiJFeYwyOAwTqcNGkK3n0zrnefmS09lN167Zj+tXTmTZ6MNPmzGD+Je0wxs/p4+fz/F+vYvQZbelwxgie/PstLJxwKdMXXMvz94ynYx0pJr7GYyx9O53FbXdfz60Lpmi/OIzzuzWgTqv+3PHI/Twy7yJa6qDd5rxxPPvkbSyacDkTZlzN/4+9twCQ47jy/z/VPTM7s7yrZTEz2DJJZibJIFsmmZntGBLHAYcvh/+73F0u5MRMYrLFaIstsJgZlpkH/t/q2V1JhsS5c/C3la7p7leP69Ur6JXzn//wAKfpC1Koyxl886VneF5r3Vtvvo1/+/eXuPPULLqMHMO//fR5HrvhfO588jn+v2eupUeiocPQy/jR957m6Xuu4+a77+WX0u2y7mnknnU7r//bPfTy7DZxm3HI63c6z/z4x/zyu3dzyzWXe//nS+Nkx0t3jSAtIYEL7n6G//z23dx61UU8973v8cJ1/UlykB8cjP6X0fdC/vk/v8MD5/agQ1o+d734Ev/xxGV0zQjR76p7ePlfnuLC7sn4Ejtyx5NP8oOn7+DBcaO5aEi+l8Nzhl/B97/3NZ68+wZuHfcg//PvTykGu9H/nLH8z39+k3GndcJJyOCah5/gH75+J/fecBXP/+B7/ON951DQsSePf/tb/OjB67j+xrH8w4+/ro+FadhiWnzf59KxPH/nKMaOuYYnHxhN/xSDG8zn1iee5Idfu4OHbh/NxUM7evMnxsFBReuJs264n1//29OMPa0zyRk5XHDzw/zqnx/lir6Zshpixgg3ApqZRz/0Nf79O3cy9pIr+NZPv8mz18hHakEYll8s2kwkkMUZZ57NNdffxDMPjaJ/houmHnqcfhlPPHADY7WGfPjR27m4VwpofXXzUy/y3y9cR6+sEBJF/Ec3VBJyuU599KPn7uLhO67hcu1N0pJyuP6JF/jlt29iYMdkIYFxDLbEomFiifmMPPtcrr/hVp699xK6KC8l5Pbl4W99l397chTd01zN7Xfzsx89wd3XXswDz73AP995OkPOGcsvfvlDnrj+XK4ZdTmPPvs4N/T2EcoewAMPjuPm6y7nDrvuvFhrEBlrZVqpthLM5ebHnuQfvnY7d4+9kue//z3+6d6RpGnlMfjqu/ip1oCP3X0jN10xDHvghkq82xy6DbuUp58ey9jRV3Cv9hijT8vVXC0E+bRgyAU8ct8YbrvxOp64+3K6Ku6tfzxzjSeZXI2D7ymunhXeDXc9wC/+9UmuHdKRHmffxC/+4wVuO6Ozltx9eOI73+KHD16n+LlJ8fMs1w3uxBV33MH9N17FzbeO49EbhkrbGH1OPZMrz+6NzyYgqeHJ0j1n0AU8//Tt3HLtFdzxwJ08ct0pGh/KhQMu4R/+4QWevuNSrrv7EX790u2ckm1oCCvHi9gVbSyQxmU33cqDN13BTTfexFN3X0Jn9QsYjDHY4iZ31TrrRX70xFiuv3IM//j/fZ1xgzOIKaH7kgoYcdHZjL15HM/ddSEFwRiYIGdccyc//faDPHbPWG65eCBpyZlc8sDT/OKlcQzPS5QHIZDWnds1r33nydt5WPvHq0/vQkZWPtc+8g1+/f3bGJATose51/LC47dpPzeK+x6+m/su6qFo99N56BX8y89e4onL+pKb05UHv/8jfvrAuXRKT+SMmx7nVeWu07umktH7AhqAKOcAABAASURBVOF9h0cu6ElmwVCe+9FL/MODF9NNIZ7Y+TSe1Rr6hYfGcf/NlzCkYxodelzEv/z3D7j3zGySMvK479s/4l8fGkkqAc67+3F+9t27uPrUTgSSs7nxnju5Z8wV3HLH7Tw29kztAVCxPjPIveBL4rxbH+QfvnU/9v+87Qm7V3rxejprg15w2tX86KWneOr+sdx69Uh6ZcTpXN2M46Ng4MX89P/7Hs+MGkKWCxkDzueJB2/kBs0DDz9yF9cOzUGoGGO8ioo/pz9PvvAMLz18C7eOvZ6faC3/zBV96dL/NL7705/w0rhhZKT0497HxnGb+Nx+t8bEeQUQa6KpOUiPoafo0PhyntJ8c/mANHGEtD7n8aN/fpEX7h7NqGvH8a//3/Ncq70JSZ24+2tP88Onx/HgbaO1Fsgmq+epPPjondx8/ZU8cP8tXDowRTwyuOW5r/Ovz93gfewOZfTivofu5LbrL+e2+9Sfl/VVfyIbaCs2V5FYwNnnnMv1WiM9c/dFGl9SU9l60AXX8MQ91yiXXs0TD9/s/YunWGpnHvzmd/nXJy+nS1oCGOAEv4Q6nqID2qd50e6Vxl7OaV3SSEjvzF1f/y7/9dwoeuYkYotrna/1/uk33st//uhh7rjqEp74xvP85N6zycntzVMvfY8f3X8+BUnqEOlyyrX38LMfPsrY83rTWWu/f9D5xHNjTiXDgbTeI/j6C0/x7AO38sBtVzDS7s+CBTzwnRf51+dv5bqLruS7//x9nrq6P507DuRZrX+tnC5aBCbmDuZB9eEtymu3P3AX91zUU9EnDY2Jx5V2Uv1HP8Tbv3yJH77wBP/4vef42b88x+1nddaefChPP3U74669jLE3XMsTj47jvO6JxOjEnY/dzh12TzH2Rh4ddw65IVSss8Ao6dmnkNbiT774HC89dAtjlQ9++q/f5unLeoAvlavve9TbDz5y1/VceXo30tKyuez+53j5R7dzeqc0HFSMwdibJBrtcS+68Wbuu+UKxlxxPldfdRl3P3gXd1zQR96LkT3wfB5/8GZuViw+8PBdjB6mmNaElNJrBN/49nN8Q21jb7iDn/3HNxk3ohfdB17Gv/7su96YT26uojG1N/c/ej/feOo+vvPt+xjStJkPVpWS1HEo92s/d5P8d/f9d3Pn+b0ISClvXpB+qHToeRrP/MNP+PnzYzmnbw4+teNpDr6Mvjzy/Nd4SfuG+2+5mvP6ZeAkduOxH/yYn9wzgqxEP2fc8iS//eEd9MlJ4axbH1ZeeIBbrr6Up158gR9on9S9z2Dlmh/zw3vOJF1r7FPHPctvvn0N2agYp0WSntuvvxkPOH8OTf0JQRJDQUIJvrYgCYSSyUhNJuj3EUwMkaBEZf+60ARCZKSnkpYk/FBINAECCQmiD5EYDGAnIY0n/MFEUlOSSUpQ4rIATiwGD6SDmYyMVFKTQ4SCQRJDfiFpMschOTWNjLQUkoL+Np3U6F2OP4jlnSL+qYkBLLOYAWOZGh8p0tuDKyGgoiY9GUJJyaSlhJTKRPIprsbnJyR7EmWD67okSJ+kxAQCroNPBxOJiUGCnn9i4uWQmJIq/VJJCQVaOFlYClau3+KLVyjB77XJWtEYQsnJ0jtJvpR8a6YGZUIokVTBU9psp60YT6eg/JKAzxHYiE43XzCJTNsHKaEWuEticjIp8keSso5lHQwECISCZKUnkWrxRBeTNmIhXSBk9U9PIUX+c5WgbN/iSyA9PU12JavffKIA4wZItf2Q4Fo34yaExE+yrL7W33yqWJgTIM3TLxHbr0nyadwHDp7frFzRO8YQCMo++bbVVydyi+klkJTi6ZMcSiAknwaVNeMiQnE9Ev3CEqZ4JSQkkCheCT4Xn+wPKW4tX9f1ERRtYjCBgN8ffw4FSbBOtcxsvKWleXGdYv3h80uW9BJOQP0faBsffhz5IyR4omqCa9Qhku0ESJbvU5OD4iSQ/MwJxdqOWpLk8/TUJBKlh6X3O8IVuT+YHO/P5BB+x5xE6W+Rnej5UE2Sn6Y+StG4sOMupPuJFF4n4SNF9qSltMiST1yRShRB60/1TWpiAq585m/zmYPrCxBq8Zk0Ay0KLZ/UpCDWt9Zuv8/n+SYkuY5xSLD9FwoScERhfekL0hpDoYAfvz+BROt79Z+DiuMn1eqvcROwfeTdE7x+C/odjHzv4beMHay9aakk+m38yYIW/taPIb8jhpLr/UapqKojKp1q9n9CScYgevjUFjM4hpOLp6di3eaelBBB2ZAU9KvX7Nh223JPsmL006Q+9Ueq/Gp94rZxNfJrsuI0iWBCQPYGSVCcHm82eHEeCmL9FufpeGMhPSXRo0mS332l+1m24TBdhvcgu0MGWRnphJSHO6SGNGYdklLTSU9OwO8PYMdVIOAnJJ6Jig3XtElre5C3CCSmkJmWTFJIstUPIb+iUQ3GHyI9I4301ETp6rTRtD0YHympqSSLd0KLTcGAKx/Jp0JKSEohIzWJgN/v2avuAeMSSkoiNTVF+cMRYszDR7ySFY+pSQme7sHGBpqMoY4wfvHxAdFIFNvXKa3jSF/uY0Qp2b+bQL8LuLSbX1ic3JeSlyQd05ITCdpYUpzbIY3gwcRE5YcU6e8SHxOO18+J8kFigk96xeRT4/WBnc9Sk4MIk+O6Bj1dEzVO5DFQa4rmpHTZbH1v4dbnNpQSEmWz9E6MDwLxFnrbJeoYYpuAHReWPkGxYVy//BYiUXHnSN+EYJBEjRFPf1qK4EEPHiJkeQucYPtTfWb9HgyFUJcIevzy8rjxK/+mKZ9Z+31eo+vNGckEFTOWZ0iENrckyWchjTWfPyD50kc6KEQ8lxl/iN8XIxpq5OYFycsJaV4N4MjOqLoxEjGEdUbk+IJYXwWDIS/GmppihE2A5OQkUpKCoEPqpmaweEGNq6DfT0z0OrehqQncYDIZGvspil0Tc3ADCQSDCbiOoy4O6DlEwOcj1hyjKeIS0hyYqlhwpYfOCYjrAja0ItIn6vjxS+mwZLTC7D0aNcI1wjM4yjc+5fywdHNt7kpKIU3xnJKse0oSCWprtrwsD90tX1txE0hQLg0oDv0+v9Yi6PBZ9grPuNJbcGtbzJHeera+sj6KxFwSZJ9fNiH9EuQHj4c/QEB4fmuf9GvSIXFYPvCJt4Uh2Y0ezNBs71GHtjbJ9NosTD4LaA1nZYfl/0bZ5Sj2/PK1q0i1fJvCsl8ftxOE65q4H4x8mJYRpGOuD7/P4EQjFO8ppsuZpzGkIIRXhOvdETKOlzfTtfZKVB/Z8eFzwI4PfyjJm99Sk0NYWJym9ffEcZjiraccNbkt64zUpKC0FMBexlUOTBWO377RloPko5Di2O8YD976Y2Ubf5AML88lY2Pe8QcU50GvyixsaeVjx0FQedjO63YcmUAILzdIh5BiODHBtegEFRMZKSECfj/BkO5S2MqKr8tkg8UX3MozjqN+cY7bQLz4AnEdEpMSlaeSvJom3yUl+OIIxoede9MSA/jUL4nBAE68xft1/QHNmSFCCX5cx+fpkRhKwO8a/IqhxFBQY10UVrEWXnY9nSj+hni/tOkr34UsvnglJCTIN+Jr85LXry42d6cr9hO9XBTEFb1RbsjQOi5J/ALqq1DAQtXQehmXxORkb01q5xwPbHURtbXrRF28NvtjHIJWhvogycp3ffH3UIiG2gbsV62IBmxiajq2RN0Ez0epoYDGitX7ZB8Z+TiQECA9W30i/UNKDFYFG7b2HlBc2n1Eks2tFmD9aOWHggQ0zq2Mk6rFwUey5gGrf1LQh2NppJ/1d4LfOQndHwhg5Wfq41tqahIJarYsjC8gHwe9GidxsOvitJQgfn+ApGACwZBqMEhSUhIp8mN6SqJ0iveb8QcVL8mkJiVInh0/up14xYV4vklLTfJycJL4KTS8Hg0mpbTEdQIWdiIp6reQZFq/pIR8LQLjGJatHcuWp7YZ3tiOtxz/tTi+hCSN9xRSWuNKfgrYmNR7ovrVYpvPxI+DI7+kaB5Nla12Doo21rC7KMDFF55KZoJyjGVuiVuqXYulCD8tOajxZf1gvBZ/KJnMtCQS/H6NB4fmiNqijTiaT7yPOJ4XXJJEm6q51Mr6FGuPj/EFSddaOdnGeCiRUIKLCYinP4HslCStc5IIgPxg5UqGntt8q3WD47gkBINePyf4HLziCfKTqvVQRnqK1ic+XMV5yMaQ/JPgWl6GQDDk9XFaWx8bjesEj1dIMe1TzrO5JzGYgM91hB8kKRSUzQ42N9h5Pajx7PoCHo2NTxtrnnjPrjTsWiQovSx+osZcUGPYkS5xvgEcq7DjV7ynSE+/NRT0bn2eKp+7alev6PfTl5Fv0zg+FyiujZGfwKcx5+1dk0NYfY5TGnw2z1k/yCbrBqurLyHR80OyYshjcJwg/mSRlAfS1U+pKSEv1hMTAp6vQpZXKAFX84KrXG5jOlVyjaU0CSS4LqkZGd74Sg754vbZNlWbGzPSk0mSD4PiE2zNEcYf7zu7Jw64wjQEFRuWd7IX23GP2DGeKpxE4di5BF+CZ0dqUgAJ8iJQD21XwMsVCWTYXKH4T3CEJVZWV91ISEz26G0utfyM+iGk/k4MJeCXfW2MWh4sDr4g6dprtfazcf2EQiHFQ4JyieHk4pDoraVDBHx+QokJ+Oy9RYavVYZx1bcppCYG8Hv9FfT4tfaX8Yfw5luNv/icEMPI9jTpkSKeCerbUFC0/gAhxVyi3q3+Vl/j5bUkj7es/4yPHMVyUnIS6eprO1emW/8muJ4ZAcVVWopotVZL1ID2+CkruJoDbN/Yce4Tpg0X3U66PJgblN6ppCYnEvLGrB+rAxoFyfJLhmQlKQYd18XGQ2JikASfI5wTLyMSmwcMicnJiqtEb61r1zDWP0gfK8sXTMTqlByU7hbQMjYcxWjbeiMUJGj9ZH0sPyWFAjj71rHwUIQh/Tvi7Q8z0wn4E8nODHpDo9XWFPWNFOHTxbW8xDdR/BIDPmlzAobVwwmQpn7KSEvG7rdxfHjxEgzgOoaA9YvstkMBXOy6IDU5Ab/6Mkk4fvEPWf6KSWuvcVxsbjJ86dKO+FfmAeevSR9jvlwofQbtMwD4HBDx8uVkxHFbfsXMo9K9BdJy86De8/En75VPv8ehX+bX/C9oP0vzGVU90cb7/X0/vxdDSSSKg4nU8snWXezeto35qw+ivbzyUayN7efxMObzoG0k3sNnUD4DENrnwQRGXvvDEjipfBH+Z0V8EeZJ7D7/5bPMPh/vi6CfQ/9ZbT4LaWX3OeStTf+7++9h+MVafJ6oPw7bcjDmj6exdF+2GvP5/GPaYFgeRVuX8S8/+i/eWG84+6yuWsAo5jVx2raT6hfw4X8Ro3wVxY5bqWoyu3N2v1QWvjqV96bN5vXXJlPaZQRjRnbSgVZMiwwh/RHyPt9b8IXm8+XKF/H9DPWnBMV0iGVxwgc/4Xe/+R1zjmVy5QV9BJJtjqP78St79Y6sAAAQAElEQVSmfrNycvqdxiWnd8Ee2B1v/SOfPqVHnNpg4g9f4veLMT/D+jMA+BwQf6pizBfr+sfI/GPYaE+A9gRkZtBW9f2EjHTQejZe9dwh06A1MzpT9qr2fnTI5DiecDIzxEMwD54GWvujvToWrnNELL8Mi6dqn9PTLb7ByrM6WN623dJbmpOqcNPT8XBPgmfg8bc8MtRuq6W38iy/1mp1SU/n8+kF9/SRzvYe5xHXyz5bmCezBc+zPQNPrtU7Q/BMVYv36WrpOmQaTmz36AWzOnttGXi+sbQntlkbLCxT7RbP4rfC0tOR/40q2HaLl2FhGfH3tBQwrcPSGDoNG8HFA/K09bB5yGBoLcefWiGtd5G1Pn7B3ZzA5wtQ/pfgPyz7ixkbY7648XNa/kj0z+HwJwJ9gWJfAP6UEn+cDz5F/NnXLyf0JDrvw6Qgx9Yt5uf//SoHk4Zx+dA0bz2rKUItn3N5cqIc3L6ZHbv3smTxJqoUsrGY7NFlKTwU+9BaPwNobTjh/mVw7KrDGzTNbNf6e9eu3SxctJNasbGHIv/XYP+sCuazLD+LJOnxy8RvX/D7OeATeJ3w6CF++t0CPw9m4f+batwUzrjwXAbaP1v0+u73aW9O9kNMHS6h4doyZr32C36xvJFLLzqVkGAtTXo6fv1+vWPY/4MvqGPzxp3sP7CNxav3UO19JJccY/mYk+Vb0OfV3y/o8ygE+5K8hfmHrv+V+Famn0NsWtu+xP1zyH8v1WfwPwMQ+efBBP6iy45BO6XVH9nBhn0H+WTZcnZV6outgiL2h3j9oXZP6Gc9YsynYaYlVmLEvFzRxNat29m1a6dyxS5FmU1vrXFFCy5txRjT9vxFD8b8YZwvov3fwj9f5JfTw5hP45nP2P3H6GWM+Vz0zwN/HixO/Pk84m1f8Ctmv49KzScTtgBabie3tbzZmFWqgU5DuCC3lrdem8Z7U9/nd7+bTmzolVw7KEm+iqm2EHi336eFh3Dyz+9T4GTM9rf/hzxgc+X/Q+a2m/p/8oCSiKOKE6T/FXfy8x8+wLm9s/BZpoL/kSnJUrXXdg/81XvAcWxkO/Qceha33XEtN44+j66JaEI2qvz1F41Nz4RACudedwO3XX0mZ59xChdeeiV3jzmL3JAB4RijO3+7xeiLuNU+2GcY1119JfffeT3ndLJbQqMv7JxU4pY6+oIOUbv6igP4uy5/w8b5NMkkJEAo2F7/1n0QVB/6/Z8KRm2Ubfqx/01BaB+MtJc/iwccN74F6nnG2Vx33Rjuu/0yuidJtILRmzP1+OnLGBufhqxhl/HP//A8t5zdlUSBLFi3T6N/xe8GxwrSZ5pOI6/jP37yFKNPLUBDCmMMhvbyZT1gfOp7HQx6h79/rOPkaysnmJ7LxaOv5c47buTqU3I8/xvzRzNr6dMEupx3M//5z09wRe88Qo4RP1utpPb6t+ABYwyKKgKZA3jy+y/yrZtGkN+SHAx/7mJwpA/46HzOGOx/wvPqYfloGYUxBkN7afcAGK29HBsMiblcc8sYbrzkDM45cziXXDmaO68ZSqoLKF5oL+0e+Io94HzF/NrZ/R4P/N00GZfUzCw6FmSTlR6iPYj+bnq23ZDP84CdnAV3g8l06tqZvGQH7VsE+Vu7YuAGyc3Po2NeDp3yMwma2N+oLb/H976QZ2NuRpAvY1z8A8Pv4dfe1O6Bdg/8WTxgvJ3Qn0VUu5B2D7R5wA2lUNAxjw5J7peZMgBDKDWTjvk55HZI0REPf+bikJTWQWvwHHIyk3Qc/WcW//ciTgcrjur/2hzthdJyCuicnfwV7INcUjLUp4qp7PTEv0BM/a+90E74KQ+4wRTytMbOz8kkyW+81r/cj0NyuuKqoD1X/OX64G9Ast3UBpLJK8ijIC+Xznnp+NH+kPbS7oE/jQecPw3bdq5/9x5QsrL/dENHWn/3prYb2O6BVg94Ma+g/7/sWVp5/fnv8YVw3AYtLOwY1kb6b9OW3++9uI3C+Xs0Tma1X+0eaPfAH+WBduR2D/xeD7TOGV9+ymiZQ7Ue+L2M/1SNdv726p9KQDvfL+OBeNx8RUHg9afi6ssIbsf5q/bAVxoX/1dLW+PqKwrT/6s67fR/hR7wJj7lnrZYscFitEP8K9S1XaW/Cw+0H0D/XXTjX8AIJStj2pPTX8Dzf6Mi/z7UNkYxb/62bTHGYExL/ds25Qu1N8ba94XN7Q3tHmj3QLsH2j3Q7oE2Dxjzx84ZFt/WNhZ/3gdP37+g/D+vtX+10oyxfWC+Gv1aeH1F3L4andq5/K88YIzx1tn/K+KvmqhFF92+as7t/P6uPBCPWWPi96/OtHZO7R74rAfaD6A/65N2SLsH2j3Q7oF2D7R7oN0D7R5o90C7B9o90O6Bv20PtGvf7oF2D7R7oN0D7R5o90C7B/5KPNB+AP1X0hHtarR7oN0D7R5o98DfpwfarWr3QLsH2j3Q7oF2D7R7oN0D7R5o90C7B9o90O6Bdg/8v+yB/1cOoP9f7uN229s90O6Bdg+0e6DdA+0eaPdAuwfaPdDugXYPtHug3QP/r3ig3c52D7R7oN0D7R74K/NA+wH0X1mHtKvT7oF2D7R7oN0D7R5o90C7B/4+PNBuRbsH2j3Q7oF2D7R7oN0D7R5o90C7B9o90O4BaD+Abo+Cdg/8vXug3b52D7R7oN0D7R5o90C7B9o90O6Bdg+0e6DdA+0eaPdAuwf+/j3QbmG7B/5KPdB+AP1X2jHtarV7oN0D7R5o90C7B9o90O6Bdg+0e6DdA3+bHmjXut0D7R5o90C7B9o90O6Bdg+0e+C4B9oPoI/7ov2p3QPtHmj3QLsH/r480G5NuwfaPdDugXYPtHug3QPtHmj3QLsH2j3Q7oF2D7R7oN0Df2EP/BkOoP/0FsaiUSKRKLGYlRUjEo4Qjb9YwF9VjUYjhK2uf1VafbEysViUsOfPL8b5U7d8OZ/FiEb+cr6NKQa/Mj8pdiORMJGoF9B/avd+NfytzoqTr1rnz8ZfvJ/teI/+NY4l6wfF4Vfthz+qk6wO4b+x+PkjDGwd538EyV8W1fbHXzom/rIe+MtI/wv5vTVnSfxfxu7Pk/pXMI9/nlp/NbC/If+05r+vfHWggI38pfPUX0iHv8q1xJ9wcMS+yvXqn1DPr4J163j5Knj9NfOIeuvhSMs++E+jaevc9mX317FY1NuPR77yZPWnse/vnWtbjPwBQ5WG0VSgvT//T1WlxT/gmfbmdg+0e+Cr8sCf6QA6pkkxRlQHaifOQzFlOa/+Mda00LSRiKFxHFzXwRiIYXB9Lo594a+vOI6Lz3Wk5Wd183wR+yz8LwkxxsHn+ZO/WHF+j8+OK2Vw3C/2LX9s+XScfR79CThGMXiyn2JezH8e2R+EKXZd14frmD+I+qdE+KPi0eqsOPmqdf5M/MWM18+u6+A47heOJf5SxfpBceh6ffdlYiCO85UOe6uD74+Pn3h/t+jTEtsW9pW4soXfV8HLceP9/mV4Wf29eedTDrZwW78Mj1Yciy8zWl+//N32h3SOx8SnyMTQbubivGPxOVIwi2Vhcd1jXi6x77batvb6JTzw+/z+Jcj/tyitOUvi/5cs4v19YlzYfv9UCH853q1YxvmLz+P8NZe/If84bjz/mT/Gn8opXgx96n4SCwWsK95enmrBO6n9My/xOP0/xeWneZ6ow6fbPufd2tSaI09stnCvakcQv5/Y+tlnx3G/+rXEl/LhcV2snq222Ofj9TjOV/X02fXq7+cc1+X34/y1tjpuvG//WvU7Sa8/MmZOpI3HsIuGkAf2+sx7+j0/f6Q805Ynze9herzJ4rt2X/Dl0I8T/rmf/kg/fKF6XxUfCfD67ytNrtAWI+L/eZfUp64eyiugpBSKSv50tfCP5F1YHOOY6h+iKywGi2vvrfrb5zgs1tZ2Ip9i2Xq0CGrr4l6xfog/tf+2e6DdA38qDzh/KsYn8zUYY5T8dMceEutHlzF6t1XPX/qy+Kpt+CbKgY+X8OqExewsCWOa9/Pqz19j7tZyDyWmQ2/v4a/gJxatY9286fznex9SqiRvVTox0Rlj/WGhf/ka1ytGybY1vPy791i8o9pT6s/+l52xBjYt+oD/fGshR2s8FbzDmPiTfuOKauY4ytxJE3ll5no8TWNR4an9f3t5fWF+P3UbTpTDm1bzq1cnsXJ/QwuN8WK+5eVL3VpNqSvdzcRXX2X8qmMtdLGW+5/3Zoy14ffLbNW58dg23vzt20xfd0xbPtG0Nujxj7+0sbVEsQiF21bw619NZOWRlgFjalk86R1embGOtYum8p/jV1LVbJGVV/4ybjpJeLTmMLMmTmPq8j3UYv1n4u1f+GswRvUL2798Q9zlMWpKdvLeK68xaU1RC/EfdozFMEZ6fE5tYfJ/u7Xw/d8zaYkJmlg15Q1+PnMTDR4zwa3y3vNnf4wx8XnHcFIxxnh+Pwn4B16MsTR/AOmEZruBsK8NR7cz8a33GL/8gH0FzUmx+BNSAsfja3mbFl0Nthhj2t6NMUKNV/7GSputfya9W/1eV7ibSW+8ydvLDnuS7YGu9/An+omPP01Fe5bxbz+fypaysCepVR/v5Uv9xPv5xLgwRrAvRXsiksZGy5/0NB1Yw//86l3l0WYPoVVX7+X/4Z9WPzQcW8/P/+td1h5rySqtDX8tvmkZRNFwDWtmTeG/Jqygqimu3B9U1dLa+Pmcajm0xmes8hCzps5k2pJt1Lbg2vYvropJi/fFCF+6xapokSNa+4x/7W0mrtiPN3rsOs42fEE1xrTlSE4oxrTq1no/obH1scVxscYa1i+Ywn+99RFF0Xhjqz7xt//lb4sOX5baGNNmizEtent3+L8tZkXfcsXkT/tYt/0j/lPrqk/K7NsfZm+M1SeO+zfxq76Nd2UjH7//Hv856ZO29cJftf6en80foaJyvIddx+alH/A/b85mV5UHiK8X4o+f+Y3JP16Mf0l5Hq64lG5fwa9/O5mle+NCLB+BP3VJJ48gQsn+Dbz2mzeYu73Ww5FY7/5X9/Ml/fAH9f6q+EiQMUZ9qIf/8xVr2ZPVsGHJ+/zizTns9bojZpeibdy1LKW6BioroaERr00qeDr8Ke6O4Y/i7fMbEoMGV6dWX6iP2vx+CAovQfdWPH1fJCHBEBCPgO4JAbTu5iT5rfrwFZZ2Vu0eaPfAF3vA+eKm/3uLNwdpwXN4/QL+8fvf565vvMInlTGMTYc1R5k35U2+85NXWLSzxFtsxiJhwpEIEVttNpQKItcvRMLNWoPFOLpsIt/69+noIxcCULp+Nm+ur6V7QQp1FQdZPG0Bh5K6M6RjEvbwOSYGkUgUyzMsvnYCjHn/VCm+PBESJ/6nE+L4EQ8/2qqDxyMOazuAFSPL06va5MU8LU/+iQrutUtuRDoYk0BuqInVK9dTqgQfx44R16SOeb/7Bf82c7v1DtHmZpo9uoinX0S8PH5h+SgcQeJlf9RrD42YjgAAEABJREFUC4t3RLhh3T24OET0HmnRP9pir9XRVivX6mOfY+Lr4Z6Ab9uRzWHxS8rN1iH0aj4+UOeBPR0srq2itTy8hhN/pETEttsqHNsUszpIbwsPWxvE+/Npo57v43gRMH7yUmH9qrUci6sg6ziphC1ffyqZsWMsW7ETb26VVy2P1triipb/TEfEk+HRSb+oasTqZPm0/FuxHfPe5jv/s4i4yBgnxkWz1V02bv/gDb7768VUKaIzc9I5/MlqNh5pkm4xtrz/utemuRwJJWJlWH+oRluVEeZJlw5cm5oi+BKzCRfuYvnmwnizZLXaEWmhjX2BP62e1q44WsyzM06jZ9kXlu4R6RDHicX56zfapl+YsIhjsVLe+vef8coyfRpWe1g0li5eo8f7IBahqTmCk5JB5Mg2VmwpjrcpfuK41tcn4ItX/Ip5ulm+Mk+g+HtcV4Ox9MYhrWMyRzauk1/twUCM/cvmM393jGGndiPbVLNy9UbKGyLeOIjIBjEC0Vq+rTbJHMFa+Fs7PIDFjKlrIp4eEfml1RutdBbWhmrRW6sUPsk2vVvaaLSMaW/MojIzhw7+JvavWMgP/uVddnpnPpIlvFa6cHPYW+A1H1zNj37yG9aUhj3ux2VHvJixfL2GE35sH7fyiZygoDHWRkMwJZfI0R3qi5I41QlyrU2f5qlmRbBy65aV/OZXr/Pz1ybw29fe4T9+9iv+5/3NNHoE0l8x1yq39TAvKpiNJQsPawxZ/p4NNtZU4/rF+GTqy3z/d8vwXBGNtvhcNrb53epu322NHo8h8bD8wmHBpOixDfOZuQ2GDe2MX2M8LHqrQ0T92uoKoXl21x3ZxC//5Z956IWfM00fBJo8KNQf3cKbv/pPXvrFAvZVNBIWbdjKaSWUdPvPdi1PO+/Y8UD0MC//y7/z+roqcYlh8S2dxbHPcTttU+y4bVJIF/6MfBKKtrF04/HxjHwgbMrXTufJ577H48//hKe+8SMeevIH/ODlBew8uovf/uQnPPad/+Bnv1Vf/NfLfO+ff8vUdUdltQ1nr1MsC73In9I/LF9ErC2qVq7XqLFgYfEalWWCCmZ1t361cNuXMeljn73aRizctit28liRr6yPwpJpUSy9x/MEteJwFFuINkqc7Yl8Ii0wtf+BOIrTwkmxL9mfEodXpFtY86cvJRtTuIWlm4554KiYROQbr+rZA57w02ZDiy8sioV5+KKz9BY9anXVu4WHbcxbRM+zMcK1h5g4YSVOl74UJLlExSsqPa1v4viKbw/fcjq5qlsEiHBw8yr+4wc/4snv/5z//u1b/PP/9z/86Jcz2FjijR48ctlo+XnVA4hUl5UXkTwLj8aURx2HaNMRJs9YQXP+AHpluMSkf8RW2RCNyoPiJVLBo+Kt/pHOYfGIttyF4bVFhG+rJWnFt++22hiyMCGeFP8e7Pf8nNifYSvTCpMvbWxFPiXP87vi3MI9v1t86RgRLKwa1ysWlx9/waMRnmVreYZbeIa9fotigzPWVMyMSUtp7jyA7unKKuJpbY+IzpOl+3F60aA4FE7YytSzvWJ6t7i2tvriZJjFUlUnWxyvtugo6AlXi/6enlFaugZPT/WZcYNkB+pYuWojlV44qL88XMWV9Pw0S4/eKOcd2cE7r7zJz3/3Hq+9MV6Hj+OZoVyiFRaWJhYpYvzbCylLzCMvoZE9Gxbxw398k10t69SYGHk6S5ZntxzSXLiOn/74N6w4ohfZ1aQ5zfOZfBsWnkiw5UQ/tOVJ23BitfTyZywxE1/pLpZtOOLlObS+aJXbSmt1saSRhnIWv/dbHn/6h/zrpFUc9P7yQH0TqWTxxLd56R9/x4K1y/iXn/6C2fvj/WZ5HOcXJWZ96g/SJVTPmpWbKG62nMVD+rTiRa2DFJP23drl3aWr5RXHFv4J/d+s9ZA8wuaZr/HSb5bpk6nFiioH2j6yVc8WwYLlJO+xoZCFU1/na8/+mO//7A1+/qvf8aN/+m9+/ItprN4vw3SS4sWV8G0cWx0iJ/S3hYXlcwu3/RORPlHVsNcXUURmlfR82ly7l/EzVhPN70eXZIFla1R+sLSWzupjK8IOy/ZYrIk5L/+Mf5+5w2qsLolieXv4nsy4b0+ERUXnIZ/wE5OM8Al+szLDsiEuqwVRitrxbPWIiLdnc4vv7XukhW+c1voyork4rDHfopO118oQUyO6wo3LeH9DPYNP7YJPdqKBZPOxxIDePZ6SE2nTI3ZSP4kNtkTlyziu5LQCbUNbPZEuIv/EkWInyhAPD2ph0jEsmRHJDutu+VtWBxa9y7f/axYVqFg8tUWEY2uL6Wo4+Ypp3xyJ+Mjq4LBp+VoOaczGSlfxgx/+iqXe3yDEtJZUbeETFk9jjDwBh5aO59s/e58ysZQ4/SLdo1j/xKRvpIUmKhrbyyk5GZRvXcfag/Fdllzs0Xg/IorjRyXP+sAhJTuL8h2f8PG+OL5nv4cc/7F2x2kiomlplSJtMMn1oK28pZON9YjuHo7u8XcPS7qLj6dz3AYr5aQ+aOVnG1SjLfu93Qve4Tv/vYAasYm1xqn4hG08nUATlTxPrtpsf0hVcQG7TrSxWrx6Gt/618kctXy8FvnT0lv91ecRSyS41TksuB6960S+dsyJiiVv/Jx/nLTJa48JNyK9IpJrawsbz96w5St4uEVXj0AdE23DjwpPCpkgeR3gk482cMj7e56YsOLY9rdWXVRTGxNuRP0vP8pWUakpDrNyI20wCxZccuN6RUWjdytTOJ5s3YVFTHePVjbE5AdbFX6oQbKshFY6yfRwRCU8S2P9Yu1yEmIcWzuNn/7PDEqawO9Ese3HZYtG1sSizdRVHGP2W79l0trDxPyCG3Cbilgw+S1+8/Y7/Pp3bzF7YwnGpzbJ0W/71e6Bdg/8BTzg/CllGg3usA6RCgb1pVevAoJH1yoBLFGSN8SS8hh2ei8KOvTktJ4dcJXIjOvD57q4tjpGExIYJ8bWVZuo9PkxxpDRrT/nii7RzoZ6P7B1J/UZ/Thv5KkM7ZlFr/Pv5Ft3nUtuakDEBse1/ByPp+UtEozjSo6DV4yDz+fiGFRikhHHdUXnCBhPlsdhrmBCJGaMx9PiuY6jA7MYny6O4F67eLmuA8alQ042OWmhFnmcUEL0HnYKp3bPwKri6DOe36NzPf1cx8FRdX0+790eNmEc79nnOrjCtXcPLg723W3R1XHEw3UEBVN3kDVbCoXf8m55ita1VfieFeo3I7hPsFBGV3oWZBLUl0OI4fhc0bZUx/lcuz/tG5FJVenQQuuzNlh9PGGcUASQTZ4ukm37JYZLRrZ8lp6I1DsBN/5oZVk8N5BEn54FZCYFPDtjOJ5vWnk5OpwTdxxXeqhauEcnGxzVVr+6Dl7J6zWQ84Z1xs5RWH+YuI8tnV9Ixhjy+g5i5OBOBCUxlJlFflYqQZ8Rvdr6DGaE2kJ6i6nddY7TOzJELNVy4iUsxyUQUA2l0rtLHukJLcqcINsV7e/zpxGutcuigYn3lfeiZ5+Lz3U8WBzHYIvVxXHicNf14RO+MSkMPf1UBnVMtCi4LT6L3x1xtuAWnf0u/qRcenfLJtnv2AZw3BNoHOHH4vC2X+O1+8RXrgRhuHp2JZu2YnSY2kF+TSPodwWNEOpzMd97/laGFWSQnp5FdkYKGSmu19c+17EhCsbBJ15Oi02WpY0Tj7/grgewqAbHdT09XNeRBoJJTaeFzsJs3PCpchIv19JpG28MTsNhNuxpZuCFZ3HOaf0Y0LszZwzvR6aLSgxjTIss6ev3YdXwZ3bm7JGDyQ9ZJEVtm2wX13E+d3wZ4+C26m2ZxDip+BJSyc/LIiXoxuEn4nv6HieI5zdtRD58l++9vII+F1/Hg7dey7jbbuTRu8/BX1lCjVblsZjBcdw2uY5ssVwszKe4cqWPHdeu6wjPwRXM5xO+g4qhy4ChGg8dvfEUM2oXvutVByMM9Bt/dyXD0Zu4WzzxceQHK8PKTMw/jW+9cDsjO6UpMzheP7fSOXFGSDXsgj2kA7frzu9NzaEjJHTtgh/1r2owpxMZ/iROv/wcuqUneDxa+UuqhpfBcV3poap5x44HnAyGn3kaA3MDgMHi2xizsu2z62hOQ0XCLcxWn+uoj2O4wVQKcjNICXnZREi6WnQNN8CgS6/niQdu4rH7b2PsyHxC0q9nXg96d4hS7evGnXeqLx66izuHRXnzf95k6ZEINs+37J3AOLg+V3boLr19qlIHr9g2vbtedTAWKJiHI79auGMMpuXZvruOsVgn1ZgoHdeN+8R1MMbguK4nExVL7/E0ejnhMg7sX7uOY6KxbCNRg+O6LXxcLCwGOI6L50e1fV4cWTyhSW7cRld4rutofFjoiTWGkS0+v59AYiq9u+aR5o0tMMJ3PToX1zKMcVLx6NTuiN7iWRQLs8+2OgJYEseRrsJzVT1dBVfQSDcjfyRwzn1f4+lR/VBoyS4H13cifly25XOScL0YR/EZc+nU/zS6BmpozhzMg3fexNOP3cbQ6uX8+8tzKIuCEwsTMwZX8r3aIh8xdRzJc211cUwtxZWNhBtTuPTuh3ly9GA6hAzGcaWn69E7ltYYag98woZCB0fPjuOoXc8tdxvZRs9uizzHWD1jnAwTEBUZ0YrnCjEWE+wLr5h85nh6WBqf6yASTbcGx43rZ+EeDHAclz8cIybOzxJBnMZ1MPZZPH2qrqpPaxDXETwmWzSDn3vbEzx17WAylDON4BbHFZ1rcXU/Tu+IE57tVhfvTUYep3FxjDz2GRgqEmacuH7i6zriKpAaTrjM8XbXQaw4sRjjI0tryOzUIKJWk8H1uS00jmRzvFgdhBQ+tpaf/vNbVPU5jwfuGMOtN4/h/tED2PDOL/nvBQdwpIepKWLL0XoGnTecM84cysCOXTjzrIG0zl1IEdfqrOrZLb7+9I6cPWIwecl6MQ4Bv096OLjyrfWzSDxdjCOY6FxbJcsDnvQTE3vRyw5fKIM+3XNJ1TrIQ3H8WF5ttDGEa9RpGggJGZxzxbnkR4sojmTTOQXswYxxU+SjRPL7jOCiU4dw1vBBdEt38Irke7ysLqo2j+L4yNQaMzs9hOshxXBkTyueI5oYRra5bbr4pKvrwUEBi3Ecr93S+P2usCG/72DOHdqxhaeD47otOHo2orOX0YP6iWAOI8/oSm15M4MuuZEH7r2d55+4lfM7HOCfvv/vTNpWiyPcqDg77ol8YpaLx9vXAo/rJhnSyef1hYO1M4bBNQ6+aAaX3/soT40eSHogBoJZvb0qGmmEaShj7dp92LnPmAB9Tj2VU3tm4hVjcITn4XsyHWw5EeY4xqYjC26rxnGJ62Y8mGPfXUdaea/xH/F2Pd86nq8cvceE4XpyXFznRFq9C25tdF0nrpO116M3mhtipHQZxnPfvIsLuqXjM83s3rKFwoiD2IJxcEUfr4LFkM4Gx3Vb4A4GFfWP47TiOrSooIbj18n94uJ4SI9U12IAABAASURBVDGMcVp4ubiOg/ECVDDp6HN1lyx7b11n5vTszzmndifRsvZo4ziu8DyWFn5CjcUMjtbsrhsgt2c3OmUk4wImozPnnDWETsl4xYjY8rDVymuqLMX+y8Hs7v05e3h3ZUAwOtTboo8wlVZPGW50t/jx6mBLQGvuguxUQq3rfAtsrcYQx3XxSV+DISExk/ycTJK8/SMnlVgMnBNlSEc7ljBOGx/XdTAW0Zg4TPiO6+I6LTi6O659V7x5/PRs3y2d9nxej34eP+JFbL2HnJ4DOe/ULvj0ZhwXL05de/fJFkeWqMHj3yJXbVZdsWbH6k2Uun4cMUvv3JfzzuhJktAN1azQGJKyGGNw1eeuY7DFcV2Pr3225jmywxXMVjvmwKHX0FM4vU+WRSEmQa7j4rbgWDZSB8dx+bSuHgHGa3Nb8F1LIOs65OaQk55EW3dYJkBTM95/fsIIL0n7qfQ0l7RkRxRgYzsx2SUjXbAkR5rFvG5CuMmpLumqGWmO8r/B4qUJJ5gkuOgtoi/okC5ai2Pl+pPkmd2fsLtC41NzR0y6ejLFJz1VMnXmg89gdUgMOaSk+EgKGFIL+nLGkB6InXRyOJHG78ZAPo5UFbJh7Ycs/2QbhRWN2BIMhlk9820WFeZw47gbuf3sXJZPfZuPi2P4pZAoLVp7bfdAuwe+Ug/8YWbOH0b5v2EYkRubUFL78ujD15GwfgavfFSoXGGwCTUhwa+7wShRFm1fy4z3FzJ9+gKW76oUPMbhjbP5xSsTeHXaCnYdraCwpMpLlpqVqD22m+Xbizi2+SPeX7GdQ4eq2b1uHgu2lGKTijFRdq/5iIkzlzB/0VKmvL+CA9Vh9n+8mLfmxv+yr/bQRiZMXMCmUp0EoI3YkR3MmjmHiTMWs/ZANcYYGop2MWvWAqZOn8OSrcVKfmAaS1m5YBGTp89l7tqDNAkPSbVy7YSCsPZ8vIxpsxcyeeZi1h2swRb7l1nhSNQ+ejWGwXZCrL6QgyUNBOSHKGE+WTSH8R8sYfasOfzqd5OZv34fu7d/wvg33uWVaas41mgIH93ClMkzmDLnQya8N4k3PviY0qhBjmHRzOnM214NsSa2r1jAO3M3acFRw8rJU/jl6xOYsHgrNdJx/8bVzHh/PlNmLmLNvippA8iW8j3rmDxtPvMWLeKTwiZcbDE0Fu1h7uz5zHx/HrNX76NZuLbFq3HDMc0VrFq0gMny19w1B4lIpT2rlzB+2kLmzFvIK799hykf7aJecCkor4naozVEKw+yQDZPmj6fZdtKsSiR5mbsXx0Lq+2yB2f2xdSXsnT2XKbPX87MlXuoF4WlMaaeT5YvYcqshUz9YAV7qo1Cpo71H87n7RkfMW/WLH71ymRmr9vPvi1ree+td/ntlFUcbjZSqYmDx8qpV8cohMA48Rj4YCEfzJrLhPmfUFpTzsHCOnyOQzMqEemoL9Ax4UIVe47U4HcNTWoymqgPb1rFNMX2tBmLFFd1GImxk7OadYvpJtzi3bw/dQ6zFy9n2fZjhD0kNdUcYclc+XPaPBZtKgLR7l/7EROmLmDu/MW89ru3eW/hVuoEr9q/kSmTFuB1fW0RC2fOZM6WMmJ1R5kz9X1mLFzBtImT+OWbs9lSZGNe7GTkwU2rmTZrMZPUZ/M/OUplSQnH6puxX5RRCRfulO0LFStz+WDVXuqtylKk/ugupk+by5zFS/hoRwkxLRykBtQcY+n8+Uz7YAEzl2yhMupBtUH0CInVFrPw/Rm8OWMF+8rDxGoK+XDuHGZ/fJiw5KF4iGOGadIXfq2zIRalcNsyJi7eJj54NVx9kOlTZvPKG5O9v+iSSjQc3MiEGR+yacs25itWPy6MYWoPs0DPkxTTSxVXFs80V7J2ySImfzCfaQs3IzWwOWPfJ6u82J0yby1HrFOlj9Sxv6po7JexYsFCpiu2pi/YQEnEh4nUsXX5JxyoLGTOhA/5ZO9hdhXXYIM77A13BVPtUZbMWcD0uQsZP3MZ+yqbqSwspKrRJRq1Vmt8lexj7hzpM3MBc9bs57N5Ra49sJFp7y9i+sx5LNx4jBbXcrxEaW4OE9VHPQuLlB9g8bx56rsFzPhwO5Vh2xcx7BgyRs/hA7z27ho6nT+G83qm4GpDk6CNnD+1N9dcOISUIPJLhJ3rljNNY2DKBx+ytbgZQ4Rtyxfy3oyFzFF8/ua3E5i5Yid7d29j6jvjeXnCEnZXWf4V7FGsGTmxGTCRclYtWczUmXOZvGQrVRGgrpCls+cxXX00bcFGysW9/th2ZijG1+7czZzpi9igg+RDB3cxe8EmtHaFaC2fLFnE1DkL1YfL2VNhGSG7wChhRXHIPWUEI7s1MWfRLnEUHGgu3MyOaF8u6h6gZOd6ps2Yw8T3l7G9pCmOE6thw0eLW8bDAlYfqKKpvITi2iYwAfTC0tkzmTh3Oe9Pm8YvX53Gyv2VHm2s+igfzZcdivvpizdRojwtjWhsiqiPY7QV43iP6QPP5ZZLhtK3Ty/69M6guj6Tyy/ojaP2QCiBYDABry/8Pnqc3p+UxkqKyqOidYhF7R2aND+9P3UGk+d8xKQJk3lt+kp0liQcqNe4tfE0Y+Z8ZilfywLCh7doflvKhi3bWaRYW3u4juJdGxRP89Unmnt3lnu0qL/iD8i2Rrav+kh+ni856zhWW8nGxfOZ/tFubBY5tnkV7878UDFtbYwRn+LCHNm0kP/57SR+N2EVO4rrcR3FzMql8bwsn+9QRxoa2bx8AW9N/5D5c2bz699NZOaavezfvoGJ77zHbyYtZ19NvG9rDu9g9gfzFIfzmavx0ajwsv71pHr6GppLd/PB9Nl8sGgFS3eWKko9JIzNSXPnKsfNZcmmQrycQkz/wyvFO9YwcfZHrN+6nVnvL2ZXWQOHPlnBJI21KXpff6hOfoBdK5bw3rQFzF0if0yexpQVh8BIhvh/uGwru9YuZPrizVRaAY1HmTdtNtPmLWXa9Kn8z2uzWHuoFmETs4cRfLYYzSmBhARC6nu/HYeBNM46pRPVpUWUWkMdHxHF2WLNexOnz2Nxqy3acB/etIZps5cwc+b7vPzqXHbpQCvaWMhywT4+Ui9bDTWHZZ98OFFjauWuMurK9vLGb97it+/OYt3ug2xat5Kps9axY8fHWoOs1fg0VO7bIP1nM+mDZewoCWOMoWzPJmYqd06csYS1+yo9QyIVh5ivcTxJen24vUx4FmyVtvfj1eYeMESrD7NQ4/f92QskawV7K6IYHRRtPSlGLGYDm06IkV/ZGPl4r2JkPRNsjExczsEGoLmYxdM+YIFyvO3Z7aJ5d/4mqrXD3rJ0HhPeX8oczeG//u14Zq8/5OXOhqIjrFo2n48P1IoBmFg9G5YsZNKsjzS/qi+V40vrm5Rr5jDxo31AlKNbV/PelI/YV69X+aJ453r5R+3yz/bSZtkg/+z7hGnK41NmzGfNfstb9lYeYqFynJ2HPtxeCl4gnOCfxmLNgwt5X307ef4Gihvxihfa3hPYvwhr1qQSsymkqZzVi+14WKQYW8dhK0Z4NrTiNBFWTpvF3oxTuXNkZ61JXFzXIZQ/kJsv6MSKKXPY39DI7o83c7joKPOnLmatXVOXav3TbLB/1YeUNLWFfDhvPjOVL8ZPW8buyjBlh49RqmGpPTz1+9czfupsZs3/kHffeJvX3l9NYYvuxTs/ZobmjekzFmBjAq/EbW6Ng6aSA7yvtfTsxctYtPkYYeloXYNd+8yZh43zhZ8cJT7XiVb50ZGRruao687uzaFVizggXbTUItpcwd49tQw6rx+Ve/ahFKM5xxOKq7bVWtPauXvavPUUNvi8hojmS7suj0Xtq6H+2E5ma108cfpC1h6qV0xUsET7gilzljDj/en8zyszWLGnSp4Rvvq/fM96pgt/hnCmf7iLmnAjR4oraYh3guKynDVLF2stuoDJc1ZrH+IJorUZcbIfzbx8H/Dj9/lxgpmcM/YBxg1qYPzr87BZ2YlWs/ajJUzXfmTK3NUcqbdeqmHNvFlMmL1UeWsWv3xlKos37mfX5rW889q7vD5rPSUNBmMMsYZSVn68ntUrljLv4wNauwreVMZKzceTp89m5rIdVNfV8tGMd/n5q1MZv2o3lccOcaQypg9fcjBg5OSi7R8zTXPKdM2dM5ftVY6NKu+tlG/mY/c761tynGeguksOYJfWCq+9N5sVO8uI0cQOO5/MWU+pFyceErXFe/hg6jzW7NzDfI2dlQdqMNEyls6dr7w9mwUbi0DSti2bwzvvL9VeZJb2R+P5YO1e9m3byOR33uXXk5ULaqJoI8K+LZ8wa9FGKvV6dPV8fvmy/DFpBfurG6mWrFlaQ834YC6zV+/HziVGc//65Us1D85l0sINFNWBHMeRbWuw+z/bd/ur4rqe+OtQx8ZlS5im+BivdfrKvZWAobFwF3M+mK/YWMB8+Tsi3zUe3sLUKTOZ0rZvW0NxxIVoHXsLa4hK1yZsqWX9Rx8yw+aLD5azuzxsgZ5L4z9gTJPWYEuYJF/Mnr1G835UexIo211ILSphVQxOfTHLFi5i5rzFjJ8wnjcW76M+0sj+Y5U0Rx0c9ciRT2bz81cm8tpUO76bqdi1XnrP195wAR9tKxYGEG6iWetxDT29xK/WGK4v3cccjYGZyq/vL99NfOncrLVohBPxESfrO2PgkNYNUz9YxDTlqPfXHtD4NkTL9jFv1nwvluas3INdA4dLFRfvz+bDLXtZPm8BS9dtZu68JXy4fpfW8HP54JMyLL892hdNmj6HaQs+oVgxD4ZwxUEWaN1t96wfrNitfpZg8LSwsUykiUOFZdQJN0FNRVtW8ubkBcxfsIhXX32HN+ZvRccGoPn10JbVTFY+nyx/by9ppmznfPlsAq9N+pDthysoKqmkIeyDaJg9C6bq7OI93pqzjp2HpcPUD1i2vw5oZL329xOX7lB+AKP914FNq5im/ddk6T5vwxGa68s5WNqkMRdD4YDjNLNt9YfeumnK+8vYXhbDaPRuWbGIt6YtYf7cufzqt+OZuvpQfP+kPLHZzqHKu5NnCV/rdVSaGtUfWpzF9Hzi1dwUwy5XTFM5KxUn46cqZhdtweacxFg1a5Ys4b3Js5mxbDd1QvQHIFZ3jEXy61TlgXdnrmTP4WLWLl4onB1sk14TP9xDRPvBkp1rmaQ92viZK9hbHSOi9eqEiRN4d8b7fLitgkCklI/mL2Ky+Eyau44iOxBLdjN52iJWb9zJRwvms2BDOWWlZTqzQLEKpqGc1YvmKzZFN2cth2oMfmRDcicuv+pqzuqRQcwOJBcSpOf23SUUDBhKGj4y+g2iZ0IRazaXyq+iEZ6dh1rjmPbS7oF2D/xZPOD8WaRISHNDLYl9LuSp67sy97X32FwF9qtoJBL2Djdrti3kP8evI9ClgK75YT547TXmHjQkOTEaTAJ5BdmkJgbwF2/m9feWcEwrUX9iKhkpCQRTs+iSk05ycogWUcuiAAAQAElEQVQ9WphMW3NUyRl2LRzPL2btJq9LPh1Tq5k7aRZbGx2C5buYpAm9qBl8Sc2s0mZsZXEEqvfw6lvzqM3uQf+casa/PpVtpcXMffsDDqf2oHfHKPsPS/FwA4smTGJJcaIODjqxddYkJqwtlZUQjcS8O1rI7N5fTVd9lc6t287v3plPBeDXAkS3tssQ8yZnoy+oh9fM4815O6W7A1XbtFBYRX1WJ3omFvLqL99gyUGXHr06sG32RF5ZuBcnMcSGOXNZXZJED8kpXz2Tf3n1Y6L+RA6uWMyM1UfAyN6KXUx+fxVVjh+0eDMJmXQqSMcXK2OvDoayu3Snk/8gb74+i0Oa7SL7l/L//W4pTbkd6ViQSaJPCwgj2uheXv7NVPb6O9KlSy57F0zgN/P2iSdEozFxto81LNbh05KiFPr1KWDH3MlM3FhKUlIx099bSElyAX07RZn+1tvM2l5vCbD//C1qjFQ7zDsvT2NvqCf9uhrmvDOR5XKr3+8Kr8WvepLLMEb4mmQnv/I6C48G6Noxl5zUkNYIMU1EsHXqe7y7ppJunQvIbtjGL/9nMnuNj9SyLTo8WEZTVmd6JBTz1i9eZcFRl57y6+4F7/LqXGuPlmOH1vK7iR9SKzGUbOHnL8+kMJhP585ZlBzeT3EZmMI1wlmJ7VeI6X+GWCwKBAgfWs0rE1ZSo7eyde/z39O2kW5jO72aib99g4+OCk82RDX5gYSoj375i6nsc/PUN7mkJQWIWJ9TxeQ3p7KxOUexls7yyeOZvbuBlPRSZr43hwP+fPp09zP/vbeZvLGeQHIdC2fM5WP7V5K+EMWbP2TyRwfBF2T/qnlMl0+6du+Os+9D/r83lxFBi8AV0/jVzB1kdM6ne5rhwK6dVCUksmvBJCa2/HeEqw/J5qRuOiQLsVwH2HPsP2mVzv/96ykUBgukcx6ZQR8x+djQwJRX32N1WTJdO3ekbtNc/vudj6mVLBOLej4y/hDmwMe89cEG6oMOMfXx4U+2URlMwhGejQeju73kJmIRaWocmg9u4J3Za2lWo6MFIfJ1dufO9O2WwNI3fsMr6ytxk/x8PGsi72ox1qgF1YFtO5n+9gy2mwL69Uxk0fjxfHioiUPLZzJ5W4z+vbrSeOAgleJ3bN0cxcB+uvfpQXLRKh3KLEcjXmpEiMYkVFbMfu1NxUyibCsgumsJP3t9BbU6FEpPTyEkP+d2zqNDahLJdbt46805eP+MOXKY13/1Huvr0xSrBSSX7GRtiSFgypn23gzWVzpQe4DfKf4PObl07ZLFvgUTeXneHi1AJdfGiW6oxyoOH6EmW33RqZGZb0xhRWkMWz69gGp9rzxYRE0wXX2Xy555U9SnhUIXM+tTPXFoi3QM0qdPJ/VNjIh1uIXjJycvw/vLhp2LJvHKokPkaTx1co/w2q/eYUe1Q5r/CFPeXsSxxI70y6ln8u9eY9qmerr1LKBwxfv8cspaYk6AyM6PeHnyx7IFtnwwkff3BujXqxPVJaXU1oSpri6nol469OzOwaWTeHNREW5iTIcnU5m8+hg1JSUcLq8jXL6V96avoUzqxxrL2Xe0SX3aDXfvUn49dS1R6W2w/nAw8lnMl8MVI3pJl0VsbcBr2fjRDvLOO53QgdX8auJaUnv1oIvZx+/emE1xE3w86U3Gb6xT/+bTIamKDR8fxUlwWTtjCtO1ENYL5VtX6kPIfvK7diW9ajP/9fIcSiS7/mgplU4ifXp1oejDGbz14SFBDQoeT7ZeTrr8aemkOhGs3sWrFnM4uz8DU4RvsdSB0XAzjTocaWqqZ6U2MkkDT+OcPn7QOHIc3WSqEwqwVYfBHx5w6d6jO01b5vHPLy+iTjzKdx+hMTNfOSKJVVMmMWt3HY7Gx4a5E3lr2QEam0rYs/cYh0trSc3tTs+0Ut57fTpba0QsjSNRjTs9lqyZw+vLyxjQuxu+smLKGpuoL9rI5FkbvRyX6NawaNY8NhVLIdQD0h0cEn2N0iOkeSSHtKCfbXPH88ayYmxezo/t4eWfv8d2bb7Tq3cxUYdf1Wmd6Z1ayYRfv8IH+yJ075nL4aUTeHnWHmmhXHW4BDc5lz7dklg8+V0WKhfahqg2VcYYjaFd/OoXk9np5NO5IFcyDZGoK5RKpmpzvSHcSfNSCssmT2LWrirBRaM40QMhf5gPJ77DrE0VRMoOsftQMcfK6kjt0Y2ukR387vWFFAvx8IFSOnTpSBd/MVPfmcWeSJKgpbzz6gRWlSeQm5dHzcY5/Of4T8BNpHrTEiYsP0qXrt1ILVrBL16fr/UL2I1n1LqLz5aY/Bexm331fayhhPlriuk1eBhdrClNZcx8exIbm/Lo3yeVDye8xfv7IbpzIT+buI505fG85gPM1/yfnpeME2hU7MzS+iGMaTzCWxMXUp3Rg755CRzZcwgnwU9Dg0NGTi5ZylvptTZvzWRDSQ3HjpZwZMNSXn3vY4J9etI7uoOX31pKcdFu3pyyHLdjD7qlGo4cPKYYrmTS7yaw3elGP8XowrfeZeGhsIyz8aBby2VtA6O+OsSrv3mPjcqJXTrl01h1lIOHqtm/dAqvfVSMjZG8aEuMRH2kV+1UjCzBxkiflAom/OoVZu2L0UMxcmjpeH79vmLECXJowxIdpB2UBIO/5gDTPviIMs0d/vptTJi8mmhBR/qnlfGGPpitKARfosvqOTNZvKMGW5aNf523Pq6ku/J4TuwQUycs4ojrEN27mvHzNtIozolOGXOnzFfsQmTfSuWRdaRrzuiimH717fkcKtmvvP4h4a7d6Z1exfbDjdBUxNsaW7t8HenXI8SC8RNYuL8esSO+DhBKzREO1mTQu2dXjml+emPxQWwxGov2frzGsMu5SGk5pTpo6dOrG82fzOGV2Vs9FBOJELWMKWPjgRqyOnUnqJiKqtWgXz137NSZlJp9bK52Se2QSjAhoHyfQ1Z6iMTafbz7zmx2Sj2iR3nzN++wuiqFLp064is+ys6iChISmvhA6+DVR6IakzGWzfyAj8tS6Nk7n82zJvPqAruWinFs5xESenajT0o5E9+cxuaamKdFLCY9rI6V+/nNr99jD1l0ys+jg9b5RrnDTxOz357I2tpsxXkGK6e8y9SttSCaiPQ39q/egAGXnE5G9U7mae63bqo5uJG9bg/O7AD+xDoWT5jK4gNWZgPz3niTWft9dNU85j+4jJ+9ugibBRwRWn0UZlC5g99ojFYpH/bvUK98OJXd1T7MgVW8u2AvOV26klu7QR/KZnBQbJt2zuffX1+Fo7iy83bhhk0UOS6RPat4eepKrYYgVn6E/fVBequfYjuX8MqMTYojKR+L6Cd+xWRTa7UQEw4TjSVwxpA+OEU7OBxuZvWEt5myrUn6dySpZB3/9ZvZVGuejpZt4r3JG4gVdKaLs59f/4/6qyREzx4prJvyHm/aj2REWPjOOyw5lkSP7lmsnzGRGbtK2DbnfRYdTKB/rwLNr4cpavCTogP0SGoWXXNSCSQlcnT1XN5cuMuqRen6D/jZe+sJKQd2LUjn8MbtlMXKOaANVYHm79zwLl5/awFHwxY9RlS+BYfkhFJmTJjD9qqAHOJo7t/LZsVOggtRJcJoVP0VjLJBY/G9pQeoKqvimOye/vZkVldnK6dksHri27y/N0Kw4QiTpy6hKqUzfdJqeO+XrzJ7b5huygWHF03l5dnbJNXFlG3VwfVqL28nBsPUNyfSsXMOyf4oJSWFNAQUl71CLNZHrMVFcGzZNCZ80ki/3hpPWpNUVjZTv3cpv5u0hQ4a352qN/CrNxZRgYoU1rSjhwaWj3+bSXbNoDzWOVrO9kMl1FQe4BV9qDgS7Kj1XCZbZ4/n5aWHcZO0b5s7h1VFicpd3alY8wH/+toqGrV2TjiygVcmfESpuMaaCtlV4qePfOoeWslvtX6qE9ybNzQG9CiemhsXHaVjlzw6ZafiOshuSEisYNY704j/38jUM+ett5h3IECXjvkcWrOaLTXJ5AZdfEWbsPvpIvFL1F682a5fhZOquXDn3jI6dO6ucVvJ+LensqVSEn0OsZjuJ17GvkQpLz9KlemkGE9j9eQJzNjSoIZP48fwhr1aDq+cxs9nbCNVY7FrXoQ9+49RXHKYN1+ZzG5jfZbL/kWT+eWcPTjBVPYsms4bC3bRVFHMAX0cPvjxAn49cwO1WuMfOlLOvuXv89biQnqqnxL2LuaXkzdKSqHW3ePZHMuhi+L16EdT+J8ZOwTXJTu8Q0rNDRz+RHu5JdgD1xBlzNVh6G6TLd8ns0JjZsb2CuqL1/He1E2k9+lO94RCth01JIhNs/GTU5BDuvZsCeXbeeOd+RwTz4SoBAQSKCjooP1cjM0fzmfeJyWicIkUbmbi3A1E5Dvrh1/P3EmG5u1u3v5rB2VuArWfLODV2VvUM7Dr/fG89mER3ToX0JF9/PZ/3mV72CFV66YJOtitz+5I/8xapvz6HZaWysd1ZeyviNFR+SataA2/m7CCGsBvokgrPZ18NYUNQbeZLUvfZ8Vh6Ne3C07xXuw6+5M577Fgr8OwId0oWjOVaR8X4oYrmDH+HTZWJ9OvT0ci9Uc4oAFvStbw9swlOvyvpLConMLtq5g8az3JvXvSuXkLb09aRLXf0XiPkpyRR05mgGjJQY6EU7X/6Er15rlMXr4PX2oqB1ZNYaI+GDQ1HmNvcQx/5S6dY8yjLAKRygoqGmJ079eN6I65TFyyhWjIYGRWc0M9jc1KJvZNxsYcF+NEaQxHCATADaaTmeKjqqQEn76eJoYcEkUbTAAjfNpLuwfaPfBn8YDzZ5EiIUZpr64eOl1+C9dk79CB1ErKtDnz6VDHp7a1C5dTnX8Klw3qy5DTLuOcrGJmL91Jerd80pMzGTC4JzlpieR07ETX3GREQiA1m87ZSaR17M2gHrmkZ2TQrVMuqaGgJFYxb/5GOpxxOWcP7k2/Qd3p2CEJv5JfTn4BnTsEUR4jISOfHh2zSAtByd61fHIsQLdeeRRITqRwJ5/sP6rNeTkVWqT3OvUK7rikJ07VXhasO0SODvIK8nLoRDkr1+wiqoTnogxmJN6kctFVZ9M1K4uunbJoLC/3JgDj2Ea1n3CJAgIZOlzuSEaiT1wcOnbvrMPOLpxx2gAuvmk0AzooYef1Y/hZl3DLWQUUHSzG0Wa9lxY8vfsN4NThp/LIzadQsuZDNvtS6ds9n7QAKj4KunSmY3oCfk1qeTnpZOR0Zrg2CUHTgXPPPU0Hwlnk52bpi2YpVQ2wdekKirJO4eYz+2nh34eOyQ5GE2l042rWVWVx7UX9GDxoMDeekc3Hi1ZzRFIcu5A26uXyQyzecFgLl07k53ciV5v0lR8fIalLD/m5I4NO78uIy27k7IIoh4/UilI0Wsg5emrcsYmPDjRocZxNfpcuJFXt4KPtDRjXp9bPXo1H17FocyPnjz6fIf16MKBbBr6YT5NIKfOX7qbLOVcxbGBfRl53PmlHYqZD5wAAEABJREFUV/PRjoB83IUunbvJrwO55NYrGZLhkthxAMPPuIQxIzpTeugoaCHYqWs3OmYkonmJnR8vYWe4J9df2I9BA0/hjusvolfHDPI6FZAnHKs7rcVbnQXp2qMjOWlJWqDEWL1wFbEeZ3GeYnvoOaMYFjrAvOW7PIpIWFGjkDiwYSXrG/K5adQQBvbuQffsZPw+l+Zje1m6rYzO3TpRoM1ZVt1hlm0+QmaPnnTP78jA4X0464IbubiHy0FtNIMd8umU18GTS0IK3boWkB4w6r8Muuq5e8/eDD31FMZdeRpOdSmNDXUsX7CWpCFnc+7APpxy3sWMvXgYBSnpdNOBebriEZXMU87jmsFZ5OQWkO3WU1ldz/5PPmJ7c1euuWwgA3r3opvGos8Yaks3sGRHVP18JkNl81WX9uXwx8vYWQ7GcbDrMgLJXDD2SgaGqrSYNDhuKfUFZ3PdwHSsP23lxGLjCx9dunahQLHsjRm7kg114NRTBjBCfX3jKQksX7gZf3pHenbMpYf66qrRN3JZ1iHmaPXcs0cBBR3zSCvfz4qdhZRVllNUXEuHTj0Ze9cVdHPr2PDhOipSC+iSn0v3vER2bfyEw1q5Ga1MlKq0Q9nA3E0NjLjqTIYotq4dPZiqjz9kVWWA/J55JCemMfD0Xso16eQV5NExO5VgEMIbV7D0YIirrh3OkAF9ufKmaxnZyUcoK1+6ZqJvJ5TtXMH68mwuumQQQwcP4fLTM1i3dA3a18lvxst54NLprAu4qncGuToMyAhXcLTc84bNPJxYjGO818whp3D+0D7qu0zygnCouNKDx1shWt1ERP0SUrAbAXV57WrB/suDWKScD5dupsPgEZyh/jztsvPpUrOHeTvKyevXk64dO3LKyL6cM2oMZ3Zx8WV0Y+jpZ3P3Rb2pOXKUJieRHl07kpMekvba3xeVcLSokswu/blzzDnkp/pIye3HJRcNISc7m46Zfo4eqiSQmkfnjjl07XMKY+65kasG91LuzyI3I8mLERPqyOVXn0VOhyy6FaRSUVhG2Gpu7A/IJFCn9Tx3BJ3Ce5izpg6jA7SPSvK4rHeALetXcqC5A900rnp2zqZ8zza27P2EeauKGHbxlQwd2IfzLxrNdSM64kvsQG+N6TR9jNMLXbWZ6dqjB6cMHcrNsiGroZQDSmeJfQZx/umDyMlJpyDZ5YgW4VYbx8sL9ulTVTEciTk4kULmbQpz+qld5aN4f/pch5pDG3jjnan86pe/5d21dfTr35Uk0aBZwqii4kvJo7s2Jr369OOUU4dx/+0jad62guUlUDByJCP6dCK3IIsOsUYOlDfipBfQoyBX42MYl4+6mTFn9WDY8OEM6yXf5uQQ0AFnsWIeBYMnSjLqy0s4UliBm9OZ0bdcxoDMbPHMITsloFZI7diZbjlpBFzvVaTWBod0yU1PSqXf0G7kphQzb8EWcs+8ApuXz7j6InLK1rFks0tHfQTq0qkbp505gAtuGM3pWX4Scvpx6unnM/bcntQUHvXm635nnM6ZQ7qQn5VLuttEYUmDBBr1c1R3KFq7ko+r87n56iEM6tOdXtkh3EAQSnaxWPHauVsBBXldyGzez/INcpCoZKZ+ITmvI11kx4Chp3H17bdxycDOnDpyBMNzM8nqmI2rjW9JE5w35nouHtaXqn17SRlxC4+dnUHjrvV8uCfChRedydDB/RmjueLAyqUciabRVwc7Xbr3ZtiQodxywwhSK4oo1loIqc1nivUbyv1Rbd7W8Orbb/P4Uz9lbfaVfHvcMOV1tJbYyocbq+jYtwsFuV1JazrC+m2H2b59K9VpfThvQG+GXzSS3sF69lRCQOOouz6cJvlciDRTdPAIFc0J9D99BNdfNoSg+qdDZiode/fXeiqTTnnZdNZmuP+I83ni3sto3r2WzY1pDMzLpWP3bKp2rmfDoXrKjh6lIpLKqeedy6hz++LowG2JDoe698vReqIbaQ17WOptuFHOjx23VI/W58d2rGDVkXSuHnUqgwb258ZrRzGieymz5myh4KwrsDFy5igbI2tZssml04kxcuNoTrMxktu3LUbKDx8DN4Wuds0YcDx5BZovOmaEcPwuBd070UUHdMOGDOCcMdcyKLWGI8XNBDI706tjJonBBIjtY7Y+Gg24dBSnDOzJ0AHdyE5NwBfw0bFjPnlpfsWhIa2ggE6Ki7SEKDs2rOJgOIuueXn07JRNidYxuwsL2X+0nIZwJgPOuZ5xIzOp27uZ5Xtq6C79CjrlkV6meWibdFYgODFPXQIdhnDtRT3pkJ1P1w6GQo25eMvJv8YYoo0yN78HF5xzuvJNJp0zEyk6VqJjW8QxZlMf0EhTJEYgECAqGgHil579fh+uadYBjo/srvmkJ6dpnPanS24K6bkd6ZabRmKioXbrehYc9DPqyjMYPKAf199yBWd1ySCpQwG9O2ViXR3o0Ek5KJ/+yoennXkBN56eS9GhImLSZPBFF3NqbgfyuuTir6rkaKNdeUNMuVlqcHjrMtaXd1AcnMJAHR71tB9NlOHr9QF3iT745fft5sV5VuwYK9cdwBYTjenmiD+YDqdxVR+XFYvXoLN4dq/cT5czhuAXSmJeJ3rqACgxaKBmE3PWVTH8inOwc/eV155C48bl2D9y0JIPndkT8EOZ7F1XYuijj8AFOtB09mxmbVGE7r06aR3WkzMGDWHMTeeRXVuCTT8fL1xGecfTuXpYPwYPPpXbx55NvuOjkz5Ods4MoaUDJmcgV589iPycbOWuENWaq9R9WPv5oiKV7Uf2UCgBJ+Sn8sBBFn18jCHnnu3pf6n6I7ptLSsq/HTv001jthsjhg3kSq2reqRFyOjcn9O1LhozLFNxpETAXpasLiSnTy865nXV2ryMj9fuobCqjINHa8nuMZhrr76QnpkBOmRmkp7VmdO6ZRNKyaS74iMzKUGaxlgydwXhXmdz6eB+DDn1DO644UyStJe48ILh9NA+oiAvk+aqEiobhN5iYAyXvFOv4pphaTqYKgbr8HCIC685lWSfYsHixSCQkkWXTjl07TmY6++4jms71bFUX4kK+nUhP68HebHDLN1YSqfu3Sno3I0zRg7kgjFXcWaOfJTbk1NOv4CxF3Wh+kAJzXa92KMzHbW4suzT8nPISE2j37AedAiG6N7vNC4+PYfsnHyyArUcK22mobJC+5IKggU9ufmaC+id72fDkhUcTcilX14uHbtK/41r2VolE/ThOOaok8p2M3vZYYZceqHyRW/OGj2a68/oQvn6JayrLeAKzQVDBg3j6tOS+Xj+OqrSu9NfB8a9+g3g1OGn8Ogtw6lYu4QNjQF69OxIfodkeUv8Az0YrbVgdla2xmQSlSUlNMpHqFh7iBQzd+k2uo68lDMH9WagfJTmA52zkaQ1Wfe8TIJ+IXOQ5Z9U0OfMkQzu34fLz+zpHfpFrH+6dKZTViJ2RKbnZZGe0oFBw7qTnRji1PNHMqhLtmIlF19jFeW14mUc/Zx8yQMCOBT0Op3LR+STrdyVm1TPkSI7yTlEiam95YrFiEh5EytniXwRHHQeFw7qw9AzruCey4fB9o9YVtKByy/vx+BBg7jmnGw2LlxNUVIW/TR3d+3ej/NvvJlxF59K77w0uvTozbmX3MQDl+SxdtlqajM60UX91CnPZe/mnaxbs5n1JUlcffkg8evPzed0ZNPSleyVSlbvaEwPGqsdu3ajS2YC1g8peZ3oofzcf8hARmj8XN4/yJEjlTQ31bDrQDmxUL7Whzdy7WAfSZkd6JDagX6D+5CbnkhWQSe65SRhHEcf87JI1Tpp8IAuWvtmKF5zSVX/gI9OXbuQn5mEr7mBj+advP+66ZJTyAwk0rV7R7KSxYsK5i3eQo7OMoZpz3H6VReSV7GeRZsMnXp21hq5I0OHDeDc6y5neFYjB7WvNsldueS80+iV3YHc/HQai0uw3efaZMRni02njnxRV3qMQ+VNpHTqzaXXX83QZs3h6w+S3KkT2fn5FCTVsXnrHg7t/pi1hwKcd+EZDO3Xj8svkuwB+WSk59CpoAtDL7yWB0cNVy79SL7Ook/HXPI6daB8x3oORPLIS08mr1t/+mqPZXKHcvEpfcjPzpKffJRrzRxNyo73Y48BXDR6HGPPziQ9uxNdNDaizWh8dtce/kwKUjPFK5FS2VfnAzuPWt8bA2AwUWgI5jF8WC+K181nxdYD7Nl5kJLaZmJOE5VFhaxfv531G3ayfa9yhouKYkK/7Ve7B9o98Kf1gPOnZX+cuzEGu/mJkcVN916PWTmBV5YcwB9MwNEEdayiidT0FOwXSfs1PD0jnfryUmJNUWKRZmptwlBeaG5qoimsrGJZK2HaL13hpgZvg2P/eqCxqVmTiDWrgoqmANkdUojqgDNa10iz6ERCc3OTx0MaSV4TDc0RjJVRXUdjXQUbl69m4ceH6H/u+Qzu0odrxpxBxfxf89gL/8W0zSWirae5tomj2z5h8UcfU9ftdEad0VETrZQyUlJXtHIf49+cxKQPP2bVlsNaOPixuc3KF9bnXGEdBkp3OxOotamhUQuJCA06AYhKltFmwUSbZEuMsA5ZE+zCJ9ZEY3Oz6OoFjxLRojGVBsorY1pMh4UnRrqaGmW7NiFSS7pHaG5soDYaI1Z9mOn68j9Bh8irNu6nzvixi/Yj5dVkZKfJN1GiTY2iieFoQi0tqSKWluEdgFifpmakeAsT+88wlevVjxCtEe/mOo5u38CiD9fQ2OlULjstj3BVDY2RKI31MelaT9R18dlukn6tV2VFnTZstexcs5KFH+0h//RzGdklgP0/bGvFOfHeWFJMODGLtJDlGaWuMUxMeqLFUkUkQLoOgKye0ViaFp4OFSW1iqWw7InG/Rpuwrg+nEiTdIrRGHVwfcYT4fksitdnFTqkDHXI1uQWFV5YC/IkfC7UKabCsskj+NRPY2OT+s8CoxRWRkjLSBStpTdkKs5ryspto3wWd0JxSTmhjA4ExC8ajdCouHdMlNq6GqL1jez95GMWf7iJ4KBzuLR/Bo2K1SbFdaO+AkejDURcx9OJWDNNzWGNKPV2LEZjYzPqejT49BwmLJutT2qbItqU+mhWnB1Rn+RqMWTh4XCM5LRU3FiYOuHYTRlayq+fP4NXxy9m6bItHKyO4tPGtVIHmaGsXOFGZVstjc1RjMxpLC2nWQfMib54v/hCWYScOqqqpRNGutiqW4dBDE2rZOWGI1Rt3YevRx4B6ayWz7niNI0tsdyGoIPpWukTVTyndsgg0lhBzH4Bjxha46uuWfGmr187162RD7eTPvx8zu6eyqDzruQ0dzMvfOMH/MO7a6kOh6mXr6uLD7Doo9WsK0/jsivOJNtnpRlPc8oqaNAhR2vMxVLTSfPVIJOJKa80R8WjJobNRWHlokbxtLFSrAPXaHo6WcoSUfVbNJRGXoL4anx5fhNdTWkVTkoaftuumpSWhdNUiVQSomnp02qWTSpfDrwAABAASURBVJ3M61NWsXTlDooaYxjXqP2zVyxq4TH2rZjPr96ex5Jlm9ipE7QEHcaciO2khSQzoniOSm+86rXrgMCvuDKyq6gB0lOD6ucoUeWf7DSXstIaauojhBUk8XEtJNdVPESwNjbFXBJcn/RGsddMNBZVdoIzrx7NcLOeb3zjR/zTe2uokq1HNyzgl29+wOJla9guYf6AI0WavLHqODFaS5PGVZPkqZWGQ+t55bUpzFm2lvV7yohJlofXim6D0UpPHcRlA5LZun4lW1cdJL1PDzKFWFFZr43yMVZ8uJpFu+o5+5Lz6RopoiySSmYaRNUnYeWErLxkYTcqvps1loyerT1NsjssnKjirQnH59NmL0rRxsX86o1ZsmM9W481EPC7/L4SlY9d5fKy9ZuoSC2gf16SBEc8EptbUruexgN33MDjTzzGv710O87KN3npjY+pN9JD/oyb2uTp1mTnQfkympZBqhOmvrqRfUtm8pv3lrJ0xRb2V0VJ8Fu6Rs15MXwmTu00FzF7/Hu8M3clyzbsoTLiqs1TASPd7FPncy5nVJcK/vFbP+Jbv5jL0YYoUc09YSUXyyVm41g7308P31hTmIh2DHXKMbHmCiqafKSlJXh+i8ZS6ZCuDYfySEzjxM7rdixEYzaOfLgt8119FIz86+oAfd3s6bw8+UOWrtrM4Rqj+MIrMauEngqLyvBnakzGpF80RoPNo64DlXWaW+r10WyV5pZNJA0YwQUDM0Vx/PJsUN5wNEbRaI/U7GPi6+OZtmQ1qzcdot7nSm+1KB/U7ZzLbxfXM2rM6SQg9uUVNPtTSG7NeYnphHRCWFhbq76JYP+a2Y4J+4HXVUwY0SClo6r28Xj1WrRWMeT2P5v777ydscPSKT5yjHqNZ4veXF2lua6B/RvWsHDpWlKHnM9FfZIVfz6aaqSHZVZeTFmzi86/UGLSnCAdmnQgkNiFW689nZ1TfsHj3/4fZngHxDGticI06WOkzVtNXp8ZWoddmT42NtSWslIb9/nbY5x98XD69unHzVf0Y/mr/87jL73M4oM1RGrrqNHaYs+6ldJrKx1Ov5ALeoeQAl5t6SJiJm5jTXEFJjWLkBuVXxu1FvTj9zdSXOeQ3hYjKRqLPipKKolFwsoHERq1jvtsjBh8NrYlqUl5V2GJlefN4xEFkF6aGpo0ZiPyY5RorWJM/elYVbSOatA8GUVx0lhCrUkhS3Fp+6tWybc1xpuam2i2jCUjpnWojVWnuZGqmiaaqo6x3OaRvU2cfeHZ9O46mLGjerLm1X/hse+9wvLDtXjjs76e+Dy0g4zh58k/6eKmy+qhW/GmJbz85nQWLVcePFqDo1gR+DOXLMIfgKq9q/j1qzMU06tZf7AKV+vEWAt2fHinkKhcWltfq/5Uiy4bd9Y59bK5KRogLUU9ZH2jfFdf26jmmEKmSeNF87xcUq7JzU3qQIrXTxGiSUmkJ7jEZEuD1s6WJRqbjXpuVv9bv0UBR2PWUMW8iRN5/f1VfLh+D+VNDj7xRCWmMaYb1SUV+DLyCBr1S7SeBq0jbFNzTSW14ndok+Jca8mE/udx+ZAsS4IxxrtbO8DP+ZefSv3OTazf8Qmbol31IUNxZ1EiTTQqnjV00MKYepNMRpKVEyWWnE6Gv5aSMrFyIRoz6Eal4t3UVbNBB1oLVxXR98ILGJzp0/qlgaj2CQ3KsTId4/fjxuBIWZis7AxigjcrBwazs5B06hsbFa9R7JKyYt9qXnt1Kos/WsPGfeVEA34vy6AiFvr9nEsNMelUXVsve5O0NqylVJwzQw6ej3VI1SEUoahM+by1/6RDtK4Zx8aN5smocmjE+EhwZFlzJTWK22PbPmbhhyswPc/k/AE9OPOySxjctIJvvPAP/NvkdVRHUL5q1rq7kZqYVSLszd8Rz4mNFFZEdciY7unQpLwdzMogULGfCW+9x2SNgdWbD2mNpPx9gklKx8RiiZx+SjY7Pt7I0Zqj7KnMYUgmgsdoW8Jo/dqoODKtyae6gTqtWe1/qmHh0o/xDzibS/onUVtTR3MkSmNdlGhYY1lxbzTPRKMxjW9X/jKef9vGv3Sxc1FYOaS+NqqwaWTdnGn8duIilny0hWN2PmuEbuddwYUd9vGDb/6IH736IcWCV9TUUFd1lKUfKoYPBLn46hEU+KzeBt2IVFZT5obIT4r3S9gkkJLgp1k5yySnEpTxVq+ErDR89RVUyWcRxVGj1qlR9Vc4I5NU02jDE2Srzb9G+tYd+YQ3Xp+ow+01bNhdTFTxFh/TYNtpKqW8wU9Wh/jeokYxIPOxQ8PLT4pF232QTEogRkWlDETxerSEWGIarp4btZ4KK59afjHtG8LhZmrro9BYxtxJE3ljzmqWbthNRbMfv0WS50R20hWXEWbb4pn89r35LF62iUOVYfxtnXoSOlg+4Roqqhw65IS8OIo0G5ISA0SrqoiEUkn0fKax0yGNhMYKyptjXu42Di0lpnEdwXFaAMIv1/xTXbRPY2wVn1TncpUOncNFxTTqkDJV7dbXSempBMPV6lexsXroZq94nMQ81WJNjcpBEZrV99FoHWHHR1QxmaqPTLeen8jEf/kpz/7LeDYUR1Dw0Syf1Ws+sX5o1vhqkj8tT/ufwYs0N1NbF1OMR2hWHmqZPrDywjFo0v7rcD2cvP9Kwy8GtQ1NRDyNqrDrpvSUBM9Xdt2Ume6nQnkzpj62chqUM6OxJqLSNRBwqC/bwVuvKnaUN9dtL9TaxP85PSchLZc9mK6LBhh00VX0b1zDP/3gR/z3B9up0BqyUfNe2cFNLFiwior0wVx0emeF3lGioWxC/ijl5c24vhApqdCg+TLmKLIico38W1VTqzFaxMcfrWT5foczLjyLPLeOBundpCSqbqVw10dMnDyTJavXsutoFSjOrV2N8qPrgNIYlmWz+sV+TNVQp3Lvat6bPE3nNKvZeqQaV3PN5xnoSJWmOsPAi27h5rOy2LRyFesOHKYm7JKclMDRbcuZMGk879p/EbdyJw2BuENi8Vv7b7sH2j3wJ/SAhvefkHsra00Srg4m/H5X6TRGSs9zefjqLix48wN2NFokh9REn5e8jHAdx9CgySSQmIIxUWLGR1ATup1YLR9XWcmxmgvg02rWp+Tj6NkYg313HWU+ggRooEaHt46Q7YLMYLDFxETs+Ago2RjdfWo3egkGHfyZ3bn+6gu5/ppR3DP2Ivpn+SkYdgU//Mev89TIBCa/9j676nz4k9I469KLufaqy7j9tqu5dFgnfKCJBqQGx1bOZ/7RDO4ddQHXXziI9GAYm9Ss7l6VCpxUXDzd3XiDK3+5enaNg6O74xhcR8+6O7YKZgUZwHV9OI6DW1dDs5NIWrIhrNnNdf1qBb82Wh4v+6ZZ0rgBUsSjbPNS5u9K5KbrLub6q4aSZTfPLiQFfN5BnNcXerZ6GeGnJQcxdXU0GQcrr7FeE6QvSKoD1jhrnxN0MU46p159BddddQnjbrmeywdlE3TCOD4frhYlltYRP3sXZduVpBhwEvK5bMyljLn6Mm6/9VpGdnK8dp/r4jreY9uPLzGBWGMtTVoYO45DwNppDE5CIkETRnOhp6fjNHiHayGrv9pd18E1Do7ujiN8xyV+17NxsMW1vBwHo5dQSoDaynKM2hzHhyOYvVzXxbU87Itw7bPrOPZNcFfVCNclJdGRP5tw1KaLeh1cJyQmY0vMOk4PScEAjdok4kovx8X63PH5tXnU4ifUgUuuUazJp3eMu4pzemfgalZ2T/Sn7HIsc61Qoxj82uAgmOXjOg5YTVxH7N0WPRwwkKBxlaZdVEnLOPHJbmwxLj7rG39QbweYNH0jna4cxegrLmFwXpK0dgmqvxq1wHCMg+MEPJ1dxyGQGsLRoqW5pV+iWmw2R/wEQ0a8YhhjvHECiYw4qw/l6z/g7a0pjOyZjhpb2mgpDq6nt/QVxJV+3ruevcs4BLTgcqRrTVUtvmAaxu/DdQxWL1QSRBNLzuHK6+XDUVdw57grGNEtjWhyDx544Rv85InLqVo6iTkba0gK+ijQ4dSYKy/ixhuu5bbLhpEVFJOYwehGShCfDpbqwgbHcTCN9dRGE0hRdxrjYuPUp5W6MQZX8eFzXPkKMkQXka8qcTw6UeIVi+MK5vcTtAtMLcqiRu/i3dRQQ8wJoSahRnHEk+JPmLjwMKeNu5TRV42kW1oCJmbUfuLl4DoGEwhpXFYwc/pykk6/imuuuJRzuiWjdV0cWXJi9qnjYAamhtm8cSfGGHyOBaoa2LFlnw6VonQIGI2nZhzp5ciiam1SUpITSNDu2riObDVqc1Xt3dHdVj2LnwNqd3B9fuxzLHsgDz73Aj998nzKl77P9HUHWTRvGab/5Vx35YWM6JGqGDBgXFzHwTEOrcWnvnT1niDA9rmz2OQbxM1Xnc+Vp3YhqDgWFXg/eCX+aDjrynMJbZnMPy+q5Yx+nb02n/op1GkItyjfj7nuGu685gy6dc4kqg9Y2vPjOD58TpwDuPisndLHEruuG9dN78ZWXwLJ4UoWTF9CZMDFsuMSLuqVhvbIFt3DdUXvvbT+KBdjDPo6x9Jtx+jab4iXSyMRwYRj8a34+JsAoQIuOSWT3Tv2UqaOM9qkWxZg9D88fR3p4ujwpMGXQU7zQcbP2ESvK69mlPr+lLyg+t4I3drlYIyegdJtK/jgkwhXj72Ua68dTl6C4XhIxXHC/gLGPPwE//bCbWTsns87yw4R8wVwNPckiIex/eL5JI4vkHeJExgfCYof409UH0U0z8c3jo5poK42Qsj+lY90sfba6hgXxzGq8burZ18wRKzyMJPnb2PIVbLn6ksZkO1DswonlkR90A7rY1N8jjL4fA6OeKNchS+L8268lOs1Z4+75Vou6pfeQhrX2Uh/n6tnybcNB1YvZOGRNG6+9mLGXNqfNAv02Z8qpk78iMwLruPSLhpjdVVUmFRvvdGkvnMch6gOe5u0jUyTPq5xcB1ri4ONIccx0gkwth8kj88WV7q4TkQNhgtuvY6Cg4t5eeFB9RkEEnw4oQzOHX0x14++kjtvGcWI7ml0PesihroHeHXGfH34Lue8W67htFRAInw+l4DfAb30PGc0//SPz3PHsBjT3nyfg00hQho7jj8BYwyu66gapDa2JGptlFQwgDGjLmHs9aO4/brz6Jzo0u/im/iPnz7N6C6lvPfGhxz1pZGWlM5F12j+Hn0F4268ivN6p4mFwXUcDPFi4kFLMDWRptpymqMOjpOAa5sVIyFXMaLNvyMaxzRSV9cSI+Lgui6u68h/Lo5jVON313t2sMUe6rs+v7DBL7tdSyPh3t11cB2njc4RHcZVnLi4jgMB5fdwDUrDwnEIiN5YprZKT+P6CerZ+HwevlH/JgRiJHYeEs8j147mrutHUJAcYMDFt/Bv//AUYzsX89s3l1Ih/zpJmofGxOehO+w81CND3GJEcXSvZ+HUhdT1vJIxV1zI2X1UyN8WAAAQAElEQVRz8LX4So1tlyM9HTdAsony8ftzKcw9kxuuuphRA7MRo7gfxc8be6QyvE8mZft3U2aMbDIYV0FsYN/e3dRl9mOoYsQYg19wn9/FGINxXXyug10qp2h8hrW21ApG9K4445VWHNdxEIGH74rG0btjYrgJyUSK1jJxZRVX3HoJV48+jc4pLnFbaSuBkMZxXa0Hd5yA+sIRLxe/1neu5vMRsu16zdt33HQVF2otiYqx/WbvkhPTPbHP2VxUUMbP/+N9/MP6k63cbuG4rng52GHtzd06KK/VIbjV0WiM1ti5W/ZjHA/PgNZcDk5GN2/cX6/xdddtF9NPH+iNkqKreHAdR2wddLNkpCUaqitr9ezgVzuWCeD6HMECygsx1nwwn73pp2pNfCFXndYR+4cGjnBOvFzp6joOPp8PWxy/D2Oa+GTLXug8gIG5CSREmmnQpGL1d/RsbUlL9onG9XRyHenlqso/rmPvjmx3pKsBNwFfIIlTdeBs/Xn7zaO4dFAW/qwBPPTit/jhAyM5umAKH2yvIiEUkO4uQWPAuJ5vXM+JAfQdmqrKOvF0CPh8GOCgcuWSo1ncPupixl42kAxHQFt1QxiOelc3eg47g9zqPczSmiQwfDAp2GI52LuqcfC16I0tiT4CgWRGXnMZY0Zdzm03X8vFvVPRua187eB6tro4xuA6ujv2bnAcBwdwXVfVkQ/AGEWE8Wnd6mCadjF11jb6Xn0N1151IX10iEukEbQmvPPJ5/jX56+GDbMZv+YoodQ00rsO5fqrL+amMaO5ddRIuiaidZeDOOIoTkPhRkqajCfXJx1QCWjfENVHiEbj4AgW0aIikhAiUX6MitB1fYI72rdV02QSSUsVkXGlp49kPW6dO4ctZhC3SL9RI7pj/+WTcfCKyMEXIsE0Ud8Qxtob8PlwXQddGNntE3+JEn4Bo0f14+CiKUybNZ/diady/7XDsMVVjHk0erF5ORYzJKY4RPetYsq6Wq7THnjU6BF0So6hqU1YLh6+FaI3LdSkrx601pg+YwMFF1/PdVdexoC8RAGNqvQRrus4em65rPK+IMGEZqor4msB1x9vD2hNaQ+A4z6THvJZk+aEZJ+RHAfHxPHAeHo48iu26B5wgxQMO1tj7GJuveUGbji3D/3So8S0EWwWneM4NOk5bIKkBURk9dDNXq7GreuKv16M42Jzn2PfRWNlusq16OPPubc8zM++dz9nRjfyy4kbiCn/GyVJf4IfY5BOrqqDa/nEJEB9ENR6ysae7XOf368W8EmeI95B0WX4Inxm/yUsmwdcy5REgm6Yhsa4rxxj101hQknJkmkkz8V1HByLa1yS9VFqz5J5rAv34JZRFzHm3B4k6uTbAFam6zro0tvxK2DzpU6Dg9n9uefZF/nGredRtGIic/fUkB5KoNcZo7hj7CXe+BszohPBpCDhhkqaIw6JIT+uazxmrut6ejjxVxL8AZLzBjNGtLdr7Nx+3Uh6JTfTKJ/5Aor5hCZWLlhAZcdzta64iLP75WlvG8WRf1zHkX22YsNMMlyM5t8kJ8r6xXMpyjyDe2+4WB/fs+LtDiC5VgfXcTx8a6eiiLA+EA2/6HIeffBG7rigPwlRPzkdO9FnxHV8+1vf5rvaBz48ZgTJDagYy0b39qvdA3+jHvgbUdsO2T+5qs1aXFZUVFJRVUdEQzuiwT/4mhu4qkuMIxV1xKTBaaf3I7J7I1sqqigv3MSygzGGnd7XyzyR+lL27C+mVgeedfoaXVFZQ019M/aviqr09blCNPVNYcLNjVSqrVKHhU2RDgwf0IEtC2extbic8rIqGjQD2IWLLzWJcNE+Vm0vo6KkhNKyMkrKmunQYxhZFWt4dc4OjhWVUlhWS7huD5OmrORwlaHPoH7kaOZKzOzC8I51vPveXPYXlVAo/rUNzbLi+OUkJBGtPsqmwjJ27TogXvWezlb/yqoaquxfdB9HJxaux8JtbdDXwfraWs+W2qYo4fo6PVdTWduI/QuM6uoaz5fNmuDsJqO+pkoLwko+WrQdX+9BDPBDQtDhyNZ1HCqt4khhKSXlNVQ3Rgn4HaqOHuSQ3pu1CIjVFrL5cBn7t+7jaLn41EcYekovSjcsY8HuMkrLVctqKC2pwD94KN200Fi8rZKKilLmrt5Pp6HD6GwgahzkXkyHzpzeqZlpr89mr/WN/Finr8eN2tFVVFZRq41lVP1UrX6r1FfjqHxgNFvZe1LvfvR29/Lqe+vl/xIKj5Vh/2q6rrbG802VDkltrIjEu+zhUa/AUebO3kBxWQVFJWWUVZRRF8li5OB0dqxZSbli49DK5Rz2d+WM/g6VZZVUV9dSZ+Oloc7za5UWOfYvI2qqq6msqaMxHKG2qtrzaZEWd32HDift2Ere+2gvRaXllJRW06wD1hrFoo03+9fETTqYr7Q21dQrLpuxbVVVlVRq0XDWab2p3vIJe6VL+YFVrClKYPjwXp4N3kpFTz0HDCC1aB0Tlx1VX1VQKj3tX82S14vBaUd55d0Vnk+KFGsNOkFsbqjH+rNG/RUNN1GjmKjUAWdUS9cUKvlkzVZKKmrlw1IqZK/9il1bU02F7GtWRzVqQ1lRXkGNL5GzzujM1jlzWbO/TLaVYf/irbmpQT6vkowamjVeg4EIB7Ydorh8D3uOFlJYFaVT/6GEDq9k1tpCSjW+i8vKKVKsBHIGMyi7mo/XHqBcNn+8fCPB7oPp3UGGagAaxQveqIduw0eQfWAZ6+oS6axNHigSvHbhCifcWCcdqr1+aWrpl4pKG8sREKNIQy2liquykh18tKWeU88aoDiso1x9USVb9R2GlK596ZOwn5ff/bjNh41akG2bN5N5+ypJ6dKbgZ0zaPSn0W94T/bNn8r8XcUa16WUVtcjVE+W1IGOQzi1oJ41H+2nXLatXbSRJh1iDs+K0ahNaLXipqKqgbB0tXFbpRgpL2siOGQYvdnHu1M3UVRS7vm5Sso1KzdWVlVRVNpEZu9T6OQcZM2WMvEuYsXH++k8ZDDad8otUbyiMeuP1bF7UzGlB/dyQPHQOo68dv1EbS6R/ZXSpVFkyQlRDu3Zrb45yOaDZVQrRiVamLqUQ3DzuPuuS6hb/T7T1h+j3PpT9OVFm5k5fz2VCZmcObQTB9dv5IhsLt60nm3NmYzom01E4832R63iMKINfZXoKmsalKfC3hioqK6jvqnJey5Xzgk3h1k1ez4rd5WS1Fl+79pBmyhDUHmp8MAuCkuL2SUd7ThsaKxXXKvvxUMhLx+EqVbutHbVaFz5giHqiw9wuLiCbXuPUqp4r2uWwScmCcWSICT2PI3TM8CkdaBrgY6QhNNn0GCi2+fy9prDHCsupai8nkhyP87vGWP2lDnsUz4p1nivrmki0lzvxaE3fsJhHZzWeOOqSYcBtg8rZHdVY0wfMAxH9+2RHUVsPFAiX9fR2NRIlcZnheKjUfi0lBhGi3Uo3LqGPdUdOHVAMvav6CzQ/queigrZbmNDY6usvJKSw1uZuqySoaf2I1d2oQ88GgLES4z62mqqhL960SYadWAxtEsAJ9bEvp0HKSnew46j5dTYXCdbrD6Vim0bB05CAtSXyIelFOqw44hk1dTF57OYcoXlf2DpXGZuOgKZPRjap8D7lwqhtCQq929n4+EKKovLsHFdVdfkDRNL41VtQKivYN/uCmqa8hk5NIc9H6/Ajp1j65azJ9aREYP91EtmlfJUbX2YsOLI5tLWvFyrOK7UmG+OBUjU5mvXzkOUFO5g77EqqmWP7V87h8QksNuQAaSUbmL8soOUaM4vVT6y/zyzKbsXQ9NLeOvVZRzRvFRUVE69jRXRtF4RO89aWRof1uxAKJHmykK22Dl86wGOKvabdehzbMEEJu5N5fILOlNdXsry+atpUg45NbWe5Z8clG2VLF+zmVCvwXRTqFXINhvPzbEoDRrvtl+bNO7q103iuZ+8xdY6q0EUK9M+hRX3FbK3SrFerXVONH0Q917fnSVvvclCbQjJH0z/5GO8984K2aI+s7aoI4t376Kw2SEqm6v9qeSmJWiDqGEjf1qZVZrLGosOM2v2IvbV+Rg4uC8dU3w0u676s56je/ZTWdeg2K7XvFjjrVesb/sP709k6yLeXXOEY/JdmfJb5e5tzJi/QvISGTq0L9mBML7uQ+jlP8Abb67hqPAKVeuVPCs3z+Ol7/2CVcVR4iXmxUh+n+H0jOzgvfc3K3bKKC6ppDHakbOHZStG4nO3jZG9LTFSJz9WaRx9boxobrMxo+US6ak+DmzezH6tfYrUdyWaS6sbw9TX1HpjuE4b7Yh8bPFrtKZs1hxTKb4VFRWETS+GdYGPZs3jiPJ0eVk1TVLaxlZSeiKVB7azurCCStlWUV5OcUWYPgMHE946h3fXHpZ/SimRf2oVg+9NXU95cxLDBvckye+S1aUPfQJ7+c27az28Iq2pGrU2QnmAlpIQMpRoTB0rLWHngWOUa0wo1bW06mb/xY+11capPt4EE3yUHd7HMfHasK+ISuFbnwuTeG4wDL3mGk5zdvLa7G2UKbfb/F68fRUTVlRx5a2XkqfeaBS/Cq/WKWaiNGrNVaX3kpJGUvprPvfv5/Xp69VP5di1YK3mtyYby8p7lRovng+FX2nHv8aIXY9UyKdRjVm/Psxt3VNC2bb9HFAf1+gDfEwKGuLxUNB3GBll65n10QFKtAa164hi3Z3E/gzOKGPCOx9xSGvYIuX56gZLKeK2y6BhBf5szj49n4rGZIZ1zUJQYcTwdJRPKm1uTx3I6V3DrPtwl8ZoFZsXbaAmdyBnZEept2NGfi3Rujy73wCyKtfy89k7vH4qLKkmLJvqNJdXaA/TqDVMo2yvku0V+jgycuQAyj6ey6ytRRSXVmDzTjga8daQFcrHtU0RgjpYq2zpp827jlAineo1ZqVk/IqGtd6sUv9VU1paSrlivaysgnVzJ/L+/nRuG3sWiSkdGd4tgXWrd3j671i1nrIOPTk926WmXDGpfFGnnNZs85h0q9J6OapDVTtmbL4NO305Jb+eae8sYL/8eayokvpwKcsXLWHFtirSevalX/cOnj9DwShVhUfYozm10Rsftd7YaQgbzh3Zm0PLZrNoVwn2X+5V1DQSCwWJVB5hc1EZO7bu55hyZW1DJG6b/bVrfHWdL7c3Z3WpYeriMgb3SVfkqdHOZbrZK6qD3Krqaqo0NzXbhJg/iIGZFYx/ZTEHNeZsTrE2NtVbfWqUoyI6N66jwtIoDiPyY63irlL+bdA8XWP7zPpC6xPcAJGaYg7sq6Cu2SXgNLB3+2GKi3ex72iZxnkd25YtZcknR/Dl9GJIz2wSgMHD+1Cxeg4zNxdRKPtKtS/0ItfapAeT3YOzejjMnqCPDFozlJSVUaVBmz3oFAqa9vLxjkr1VyEfrirUfmkQ2fqo1hhG+ahKfV7JskXbcHoPZmgwQkl5NdXWFtH7khKoLdyvNV4F2zXnFdv+ReHNpQAAEABJREFUbZITW6Ibf0dO753AivmL2KtcVVmiNaHyVbUOWRtq65TDK6msbdS8Usq2rceIBtCBbzVpedkkGSkg79cp5u1cU92gd+WSWF0xuw5UUh9JINBcwabdJZTu2M2h4hqq65oJN9aKbw2VGgdKo/JO6+UjwdfEgZ37tI7aKX+WUCUdGpVnvXGi/miKSHclJcf2q+nA6Wd2Y+/cmazcU6b1cAVlWmelaU/R0xxi9cZK+ayUj5YfJGfwYDpGG7X3kGzlcC9lRpvkpxrpUkujXVO5SZx+Ske2zZrB8n3FFBaXKHc2kTJoGL05yqL1ZVRoHTVv5W6yBg+jl4usB0fqxzRWa5S7rB9q5Aeby+x6zq6BopEmxWINdQ2NFBZ9wqT3t1MdyOGUgd0I+g34HUx9iXc+YecSu+a3fCrrm/ElBDQXF7LzsPwZDpKRFGPn+k0UqY+KNf5KFSvVBDnz9C6f2n81eHtIO27tWUZDOIORw/LYs3YlZRpXRRuWsydawIihAWr1bvFqFS+R5gaqZEdlVRNuYog6zfV7tD/YtP0QhRoH9oykzjtXqFZcNKMlCK3FlwDJpppty2czR7kgt1s/+uUEiKV2Z0D3EMumT+SjrSXs1znMvuIIOV2H09O3lw8WbOBgaRk2VzXK5nqNy2pPVsTbN/XVmUn9tgVMWnyUA0dEf6CeBidAAvUcPXyY0qpmAqEA5Uf2sONgBbsPHKVSe+kGnRfUag6qVn97caMkX6f1rTc2JCdBsVqpsbHlUAVbDxdrHVhLgw1I5ekqu++WDvZeUd2Aq1y28cOPWL7hgNYlZcybMpOjWcO5sE8STZam1Qnt93YPtHvgz+oB508pLabJxtUis2jHIWq0YNu9ezdVWpM4+hId8xVw1wM3c4FOpcJSImvkNdx7fjpLpsxj0geb6Hn1Hdw2yEcsvR/XnJPPx+/PY9XmA+ysjFKQHeTwnmKqyg9T7csks+kwGw+WUnp0H9XBXLKjJRyoiHD+HXcyqls1b/1mAuOX7KDe+HEawfQczk1nd2T5O+OZsayIvME9aNyxl6aMQTz18FWEN8zl5bdm8MHKXVSbHOx/L3L61Fn8btExLrzmErqlpnPlnXdyTsIefvfaZN6du5o9kqfplZhxZDHknXkBo3uHeeP1WWxpyuG0fils2nKU3UUNdM5NYd/+QuQKbNE6hsaSg5SYTPICVWwvqaa0JkTXPB9Fh2opOVJBek4ezZXF1FcepTQhneyEBoq0MPT5I+zf9jFz58zhE3cgT9x7IX4pcuqVV3Fa4kF+9eZcNtak0b9rUAcM9XQ97RzOyCri9akraOh6PmNO9THxzZksL89jRL9k9u6tIPmMm3jq8lwWjX+Pt6esJbFTD0zVXqpCg3ny3hGULZ/LlOnzKOtyIY+NHahDDjD2i6MsjznpXH/POM5P3cdrr03k7fc/Zr8+Mmifri+OSdQcq1UtJpSZT6i5nLoo+OxnSi1KYom9eeSR68k5+iG/fnMqU5Zsp7i+lr1HqyjIS+fI/iM0g2QZhI4JdeH+R28gde9ifv3OTDbUZNA1PcrewmZG3nUX5yftZ7z0fH+Lyw0P3slgfyM7a0N0yg5QuL+ckoOVJObnEtPCubyyhFonQxvyMPuLK9hf6dI1y8/m7cX4u43k2fsupWrFB/xSNr2zYKM2Jkc4UC1eHWIcPFjO4X1HSM7OI1quCbTsKAdrEumcHWPn/mryL76ZO4Y7zJ46j4kL9nLKjXdwfd8ELQBist1Fcyv+7ufw+LhzKFw0gVcmzackuRO50f3sa+7AXQ/fTt/qtfzqjSm8t+ATjtWHqSwMk9U5lYaiCmpLinDT8klT3NeRx81jz6Z500xembmKcHZPOodK2HmsCF9aLmlOJYXauJY0uHTvEGD7sTD9rrqDh85N4v133uHlN99n8ebDHN6/j8bkAtLqD1BUn88tY8+gfMkEJnx4lJ7DBuLu30q429k8dfsZ7Jr1Hq9NWUx1RlfSao9QGM3kzvuvJWX/MiZNn8tWM4iH7rqUDAeixtXyGa/Y/ZeTmcug4SO1gM/Bp3btr732mHFw5ZiiXYUE8nIx6qMj2pAdqA7QLdNl075qgj2HMfrUTH1Bn8u7+kiUddFY7jkrg4bDxwjl5mGKj1KmEwmT0p17H7qFLiWrFFdTmLBkE8UNhqxs9e+8Obz37jzCgy7l6kHJdFYeeuzKAj6cMJ7XJsxlza4y6Sx1Y6rox8ni9vtuoKBoFZNmzmNNXTcefvBqskwjuw/Xk9cllf3rd1BWXqH3JvLVR6U7DlOfNIBnn7ye9AMf8otXJ/KO+mZfcSNHDpaQ0TGHqh07aFT+eeSOkVSsmcfkaYup7HoxD48Zil9yrd8kHTKHcfOVPdkydTzTtkQYMqQ7zYd2Uy/1HCG4boza0n1UJ+SQFT3C3qpErr7hIhI2zuWdOZvJPm0gHaoLKQ6LwI473exCNGPwpfzga1dQv+VDJs1ewvz5C5k4YxPZ/fuREUxg8KgbuKp7DR9oPE1eU8Fld9zGGR0M+w6E6do1hcojFZQfLCbYIR9/Qyk11SUciSSSn+1j/8ED7A0n0iUtzI7CKvndZf2iubz79iLCg89n9PAuXHrF+aQeWMwbM9aSM2AQHfzF7NpTSmbXfEzJXkoapGhDMTtLfHTJd9m9t4q+l43m7MQ9/PLdRdRm99aCuZnNR4RojPKDnCESbDR5jyHOuO0e7jt3ACGjOFTeyRh0CU/ffjr75k3llXdmsUiHh40kMuqBB7k87TCv/G48v5s4n3UHSqk4doimtDySGg5ztLiMxmA62aFmDlU2cKQK8vMSOFpmOO+6S8nZt5Q3319N0tCBdIpUsPfALkoTc8jV2NtVLv3kd2OdLj2kCRXi0fusM8lTB0ZiBtc1OlTawcFIDr3Sq5g2bQ7jp7zPa5NWknXZ7Tx/wxA0M2LHiMdCNro6aD68cz3z5s1leX03Hr/7QhKCnbnm2jOpXDSFyR8epudZ/Qjp42OxckEoT/1UdgT7f7qY0W8k15+dwZx3pjHvUAYjBnWg+EgRUfH1SSdUMvKTOLRqCRMmTGVv+mmMObcr+X1HcM2phqmvT+X9rc3075VFWdERwsJ3HYM1kbSBXH1eFzbPns7KvdWMHHcnl2UXMkFxNH19M6Puv1vzVDO7qvx0yg1SIl+X64NoIDcPt+YIZdXlVDnp5CVUc8zN5ebRw9k/bzJTlxYz9LTe1JQe1Ydl5DPHk+d2Poev3TGSoqVT+d2EefJ7Fwp8xzhQn83Dj91Cn/r1vKw8On7uBo4oD0pVj87ey/WxNDUvm7pj+6mJQpfTLuKqvs1MemMmH0c7M7xrEkUHjrDpqMPAPjkcXrWUKdPeZ8neRlJzujHuritI2r9COW8O23yDePyuC3AipZRpLOYGmjna2EhxhY2VJKpLmmhOSCMzPZmAsdLB+suYGMV7t1CZ3IVObiEf6uBA3zXoeeFY7jw1xJJ5KzjclMd9T95J78qP+c0b05myaC2H6w25WUlEayrYd/Age3dt4c2f/Qc/fXeddC7Ezc7FV7yXUh0I+pSjZ06exXvLqzj3hivp4bqcftEIzK4FfLBup/oikW6dEjm6vdDzbXL/S3j+9qHsmj+JV977gPnrjionpxMp2sXkKR8wbbPDFTdcQF4wk7sfvpVOxauUZ6cybfEmCpsNabn5+Er3sOFIfdxQjU8NUNzMvjz+2E1k7FnCL16bxJsfrGBvWYyzbhvHJVnH2mJk9AN3M1xjbZcXIyFK95dS/qkYqSadTikNHKiF4RddyZmZ+/jVW/NYU5nK4M5+du4upqgqREG+j9JjzVTo0D5T8zUNhRzdtQ9/h474qw5S1hjk5kfuZLiznV+/OoWp6w4RdXzo/IP00y7kmn4uU387lTk7m+k5qIDiDYdIHHA5z9x2KrvnTPX8s/CTo8TU58HyLbLhfd7d4nDTmLNJTuzEXZqHOhWt4NdvTmGi9U+bS6JAiEuvu5ycko/47cx1JPccSjdfERo2aCiC4xIN17CnpJnu2QE26YPxwKuvYkDNBn6r+be570B6BWo4qE29N5mCF1NOSm+e/vrd9KuVPjMW8cHsBUxeUchl9z/KHaflQLRZB0Y1dMhN5/D2nRRW1LP7aDV5nTIp2baPxlAPHnl0LAXHlnnrnzemr2R3WTUHDhwjXeO07tg+jurQJzUvj2h1MU01RVQFs8jxVVCWPoRxl3Ri+euTmXPIx2nDC6jUfiAq3RzFgV3HBTudxhP3XUjx0ilaR8ylMNSJnGgJhxpC3P7UvQyu38Bv35jG5IWK89qYKLHh493tj1GmQg7qOuhCnn3oYgqSHdC7oYmDO44RzM8jUrydyuZ0btOeo3v1OiZq7v6wLI8HHx5DXqSWLYXNdO6YzJEtR4jkDufpB64ktnEeL789jemrd1PTUEVlIJP8xKjWX40UlobJ65hOjfyUeNpNfP2G7qybPp7fvDmNWav2U1Jbzr6qBHpkRFh/oIZTrriCIbHNvDxlEdUFg+kXquRAWVhquhhjoLGMtZsq6NY7j2Nr5jJx6mzeVa79qCiLJ7/1OFd0dZXvk7jm7pvEZwdTZs5jwaFU7nnoenJNA0XVIbrlwNFjDRQerSIrL5faskIaKgqVSzLJdGooCfsZ+8T9nOVu55XXpzBh7koO1CTRMdjIuiUf8PZ7i0kYfJHWQamk9j2Lc3LKmDD1Y/bt2U15IINct5pdZfV0vvh2nr4yh48mviv/zGDmiv3kn3YxV/Vr4u03ZrOhsTMjeiWw/3A1YFBmBt2JRIg5qQzt35PhmovyfRYasy1oqYfCm4ayCgKKqUD1QQprIuBkcYfWv0MjW/mtYmjSvA0crtb4rTd0zkrgiCbfauUCX04OgcoSqhtKqIqm0SUjrPm4lF0lrjfm924vhw4DufrsfFbPnMa6onxuveE0Di6ayPgPqxlyak+ixYVEggF2rV7Ee+/OoqTLSK47syOZAy7mazf2ZO2M8bw6cQ5LtxZ6cxwxqWcUyU4a1993t+a1o7ymNcOrWk+u2lVBQsFpPHbrGRz5aK7miSU09b2UR67vK3sbMaaJA9vWevu29aYfT9x/CaG6CraWG7rlBdi2u5wBF1/NWWnKY+MXUZzan1NyGth5tAkwmKiEaxVw4bh7uSzzMK++NoUpa6vp1D2DysP72bmzivTO2dQe3kOD6UB+iqO8d4Qd+w7zydIZfPtHv2TOlmL2lofpmBviwPZiyBvGtecUsOLdD9js9OC2iwqY99YU5u9zOeOUfI4cPEr5nsOY7Dz85Ucosmtr18FTxd+ZMdeNoGLVFN6dX0SfoQNJrd/L7v0H8WXlE6w/pH6LYMe8neOiOAy66mbuuzCTueNtHE1j9sf7IWsIj955LhVr5jB5+gLKO53Lk+OGYcr2E+6QT3LDMY5Uy+d1RdQn5dAhVs5+rali8km/y27iofOTmPPueF6fvJiPd5eC/PbAvRfQtGEBUymEx60AABAASURBVGfM5nDWSJ5QrnaJF+O6+oBRoXwGXbMT2LuzmOLqJrLzM6kvLaZR69hYWg7J4XrC0QCNh9cwYdp0FlTkc891p2CC3bnqgm5smTOP5RsPsKuimY75KRzYUoiv13CuGhhiztS5bC4Ocdk1V9CreR0/H7+Y3eQzMKuBtYfD9NdZx8PnJcX3X2/NZOGmQipKjnIsmkJBqJ5txQ2coXXTlblFTNS6aeraJq5UvJ2e2Mz+ah+dchMpO1KtfXU1yfk5NBVV0WXEJZzXQXHx3lz2p/ZhWFaUg0eK2X6okdxO6RSrL5uiDuo+YrEYfhcS05PQNw/26OPS25onfH0u49z+WQy/fByXdKxixqSJTJ2/XPvrekxmd269fRy5Rct59Z0JTF60ir2Hi6j2pZOX2Mj+/VXYdUt6/yt4YFR/di6exNvTZ7Naub/a14ERI8/C3T2XScuPMfTiq+lctYI3Zywl1nkoXYzW+fv2QmYnEmsLOVwWJdZQyz6tmTp2CLJTe6bu519Fn4ZPeHvSYpq6DqB7oJpjxyKYhqN8tOITalM6kVC7mzXrd1La6CMzsZ6Naz7igxlz2NjYkztvvZIcP+pT49lvfRCjvbR7oN0Df04P2JXan0ye8Tg7dBx2IU89djd3jBpGhhKdMUbTBWT0Hcnt1wzUAYtFDDDwgit5+O4x3Hf3LYwd2c2DG5PCBbfcyz88dysXntaL0y+7hR+/eBdXDC4gI7s7tz36BN9//DrO6JlLbpe+3PX4k3z7/svopcM13GxG3/UAP/zmAzx46+kkN9fifUAmjYtvuYufvvSQ5F/KQ4/cxxM3DiNRauQOOpdnnn+Mbz19F3dfOZSMUBoXjL2Jh++8gYfuHcsVg7KEBYEOvRj38EN877kHePK2yxicF/RschyjqRUI5nHd/Y/y0+du45pRV/LsI+MYo4OWYZfdwEvP38u1Z3bS8gGMifsimNefcQ8+wLceHs3QvHROvfJmvvuNu7igRwp5g0bw3Nef5IHLepKU2YUx9z7I9x66VAtHqNIJbu8zL+WGsWN54vaL6Z4MGPDlDeLhZ5/mB0/ewOjrrudbX7uTq/olQUYfHnj2a3z77kvolp3JReMe5l++fge3jLqQBx97iJtO7YCoOe3qW/jBCw/z+D3X8vDjj/CNm84iTXzT+4zkvntu5O47bub+684gyxW64LowxqB0jpPZi5vvf4iXnnuIp+++jP7ZKXQ77Spe+vbDXNk/hdTO/Xnw2ad4fPRAvL2CqKzfjFildD+dR556nO987T4eumkEnZJTNEFdx3e+/gA3ntuNgHCMqtysX60vup/GE889yYuPjuMeTYjf+cbtjOgYBKcDl6vfHrxjDA/ecx1ndwsJP8hpV97Cj79+GyN7Z5MnW57/5uPcd/FAsjPzuPb+R/j2PZfQJ78D5469ix8/dweXDM0XHeQOGclTzz4qve7n8bEjtVnpxiU3juP7z43j/J5ZdB86giefk03XDyEzpwuXqe17z97BxX3SRR/glMuv4eG7xnD/3Tdz7fACwcAYgy3Gsb/Q/azLeOHFJ3hOB5wPPnQ/Lz50LX2kdkB9ef+Tj/LdZyT75vPplhIgb+ilfOfbj3Hd0A6k5PXg7qee4pkbT8V2f/4ZV/OD7z/Hc+Mu5MY77ubFe69mUOfOXP/Q47x453l0SvEx5Ipb+cHz4zi7c0DCEznz6rGKt0f5xpO3c/1ZvejWu78OOZ7ixXsuomOSnx5njuIHP3iaR0ZfzO3338VTt55Fqii7j7yK70jnr909loceuo8XH7mSzj5IyO3HbXfewn133MADt1xID4ss/NZ+s6b7rPn1tSR27aYPNDnY2DF2RSQ824RxKRh4Ds98/XEeufp0uuV04MJb7uHHz47jvL7p+JMLuPKGsdx32w08cv8d3HZ+Ly9nJComnn7mMR4ecxo5CXHnpnYZxkNPPcJ3vnY/j95wLp2SIO/Uy3jq/rHcfefN3H31ENIkF5PEmVeN5aUXHuX5R27h8lM6ejwxRpexGAqt3tx0h+SOG8ODt1/GgHQLDjLgvNF898WHuf/yIeRkd2DIhdfx0jcf5MazuqNuJNhpGA8+8TDfffZ+Hr/zCobkB+l65lV86xsPcvcVQ5BKdOh9BnffdRP33jmWe645nSy/5W1wrHxscTnlytv4yfce5Z7LL+Cex+/lgSsHerRCEoIhJbc/dz7+KC8qD/bLSCBr4Pl8+3tP8+QtVzB27C08f9/F5Ht88WwSa7QGJamgPzffdiP33TSKsWOu4f57b+H2S/p5eRFfOueNGqP+lM333MjF/TMBQ//zr+NbLz7I1QM6kNVzCI888zRPXz+Y1PQ8LlFf/ejxUQzp2YtLxt7Dj56/mdN1qNH79At46P5buFd5/u7LB5EAJPc5mxe+/SzP3nU5o268je/ceSGD+g/lvmee4kn1Y16ikBLzuXjMrXzv+bu5rF8Gvg49uedrz/CDx6/jilHS49EbGdnVIiJXGBHEL8d79NFr4CmMHJThAR3X3lz6jLySb6qvX3hiHDed34dEi5uUx+i775Gch/j6gzdy3oB8OnQ+hcc0vp+/+Sy65Odwya0P8sOHrqBHZiJdR4zih9+8j0t7ZpDW40y+8d1neOaOqxkz5ma+/sAl9Os1kLH3Pch3H7mKgVlx/ZDTjVUBh77nXMWoYRl6M/hcCzXkdB/CPU89xg+fV7wqFh6651aeeeJu7pK/Ul2hYjDG3m2NEXVC9D3jIsm8gSfvuhK5Rw2G3udczQ++9wQPXH8Zt99xO4+PPZ38zoOVYx/jceX0/AShOSmcP/Y+/umb9zBu1Hnc88jD3D6yozSzbZ4wMvqN1Px3C/co7h9U/ssPqC2Qzaj7nuCfXriLW0dfwMNPPsn9F3SPjxcMcf2CjFQu/ckLd3Bx3w4iSuGiMWN5QHn5gXvGcEGvZMH8DL7kRn70jTs5v38u2ZoDvvbC4zx0xTBy0rO58s4H+f5DV9El1U/3c67lxy89wX03XsTNd9zN82OGkeiIBYZ4P0PXMy7jm994nOcfHMPdD97PSw+OplcKmNzB3P+Ycrjy6GPjLqBnuh9bTAth1sDzeP75xzWeBuD5OCGb6x94jB8pj998ySU8+XXFXe+uXDLubr7/7O3ces1V3HXXHXzjSekmRoGOA7lV/rlP9cGbL6SHNc2Xq3XAA3z7/ovpEgwx4ILrlCPu5dyuCaQOuFjz1jX0DKHiEE99hvx+w3nomSf53tPjuPLUzoQMoAPNm554SrZcQvdEveYO4H7Fx3efuVvz5MWyr4n1648xcOyD/EQHVt978Wl+/shZFG/eBr3O4mnl0CeuP4OC3GwuGXMbj901hgfuvp6L+tu4g84jRvOD7z7OLWcP5pQLLudbL9zHNcM6tfjWT6+zRytPPc4Lj9/B2PO7E0rJ59pbb+dh9eNDd43mrG7J2JLcfRiPPK25SvP3A2MvoJum4ooGl56a2y7o7hOK3eYZWn2e0m0I9z/5CN9Vnzx9x+X0y7Z9kq5xfmNbjJzf0/L2M+TSsYqROzjPi5EzeEYx8uDlw+IxcteDfO/hUfIDmKy+3P/Mc/z4iTHccO1VPPv0Q4zSGB58yVi+rzXV2Z0DdOh7puifUJx3p8vA03hceeaZW0aQI31Nak9uf+wxXnr2Hh64elDcB1bthI6MffBhfvLNuxh71RU8LRvvurAHtvQ552petHnk8dsZe053kpPzdVh4Gw9obnr47uva8lJa11N4+KlH+Y5oH7nhHMW0a8kxxvHuiT3O4vkXn+XFOy/l2ptu4dl7R9Ev3TbJZ7o5/jRGXnOrctPdXD4kl9T8wTz+4jO8eP+1XHP19XzzyesYkGEHJxgjGoNXjObKK8aM4cFx13DbTdfxoPrs7F5pXhtOgL7nXcNLLz7EfVcNp1OHJAZceCPfe+FebjynLzY8Q50Gc0/L2HnqnssZkpNOrzMu5oVvPME9l/enS9+zeOb5J3jgwu4kpHXk2nse5vv3XURuQjJnjrmTf3rpAW654lIefvQu7riwN57V0i8+9AydThWvF57kmXtu4oH77uFbT1xLT8W5r0Nf7nniUb6rvnjopovo18HxdI7TeY8YJw5L6NSX80/vT2ocrN8EfSy/gGel4+OjhpGZIFBqd24cN5b7x43hoTuvZHAHwfwpDL/qFsXG3Vx7elcvf+XafcBzj/Ktp+7h/iuHkZ7UgYtuvYfvPXYVvZJCGlJX8a1vPciVA9PFAPnvWr759cfk/zu5/Yoh5KVkccm4e/nxM+M4u1c6Sdn9efjrz/Kt+6/jxtGjefZrN3O6PQERtVEllMOFo8fy0nee4LmHb9E68SYee/AuHh93Kf2yWvrT4qV0ZvRNY7ln3BitJ0dzSp5tS+Rs+fg7z9zGiM6J8uUF2Px35wVdSOzQg1sfephv33MheT5w0npw+yOP8NJz94n3ZfRND9L9rEt55MGbNbffzO2XDiLZysnsy/3PPs237zqPvv2HMPYezV+PjmJQjjoFR/n6BuWDx3jhiTv0gaEPoWA2o+59jH985lbGjrqEhx+7n2sHx32DMZYjrs/FEKPO35ELz+jhreW9Bv2Ylg5NzBnAAxqLT489i06pPrVAQGvJux99WOPxAR657WJ6ZybQ98Ib+MHX7+byARmk9zqLZ557jEdGDyY9MY+r7ryPbz16FX1zcjlv9C0aK/dy7bAscJM57+b7+QfNM2d3T6P7yGv5yUtP8uj153LTfQ/x8BWDGXjqmdx7323ce9fN3Dv6NDr4rQohBl98I9/5xmN8/ZFbuO7sFt1llzHx2NPiidG3381Lzz+kvd5NXDKwgyUka8BI7r/nRu65cyx3jRpOBw/dgC+ZQWdfyg1jx/LEHZfS3Q4yxdi5193BD79xB5cOyMKf0YO7n/oaP3r0Oq67dhRPPXo753W1QQymxV9OUgE3PPAQ33vmHh4YdwNff/4RbjqzD4OHX8DXX3iChzQ2E6t38kltF77x06/x/Rce4yc/fonru1axdn+Y4VffwY9fuJMrhuYDiVxw63384/O3cEa/rpw16nb+6Tv3c9M1l/DgA/dw3/ldyO4/gieeeYSntDfrmOCIBlpUodOZV/PD7z7NEzeex41338fjY0YyoM8Q9eeTPHfzSLqnx/vTaLzGKZM448ob+PY3HuEbj9/Jref39vbEqb3O4N57xnLvHWO557qzyDZgsvtwl/b637z9PLqmizq5Mzff/zDfeuBy+mhNJRTsGvXsa2/npW88ynMP3sAlw/KxJa3n6Zqnb+Su22/igRtGkuu3UDDGo8IX6sAFY+/kx8/dyfmD8ykYcCHfeeERxp5eQCi9F3c+8QhPjz2Njnn9uPWBcdx/6408NO7KeO6Qz86+8S5+8vztXHpGL4ZdOJYfvniP9nidMFo/jL7vEX741I2cJlXcTqfy9Itf53sPjeL6Mdfzjafu5ZLuARCPM64ay3el9zc0nm4Y0ZUOeV247u4nEHyxAAAQAElEQVSH+N4T1zEsPwlMKhded2PbnHhR7xTAz4CLbuDHX5feyjFpnYfztPr3nou6k6g8cYdi5/uP3cTYK67ka8+N49xu+Zxy3ii+9c2HuG1EFxJcsdBljMHo7gZdBmnuf+y+sYy75SZuvmwISWGIJHfkmnH38C3No4/eeQ2ndUmmuUFadxqgj0MP8+JTD+pQ/zL6du3IiFF38t3HxzK8WwZuBJr1wbvPOdfxjWce42sPjuP683rj1kCns67ixW8+pvVGT3K1Hn30+ed4dtxorr92DE9qvTak9wBuf+ARnrz1PO3VXIybwhmjbuG7mjcvH9aR9LxBPPjcM3z97msZdcX1PPfoGPql+TDpXTj/0qt4VGcWz99zPZeNGEyabOs0/BIeuOdWbr/tFu694Tw6h6Sb9DMGjDHxSntp90C7B/6cHnD+1MLs19GS0iiFRc0UlUQp1YfospZaqi9bxaUx2t6FV9xay2JtuCWlEY+2RDDLq6gkgn326Mugokq1AkrVXqy2wuIwxR7fGCV6t/hFRZ0ZPe52ehD1aIslx8Lt3dLYe4nVpyTs6XpM+ha26Gtl2vYi8S0SrMzTP0ZxC2/bViI94nBa7JHsVr0lq7gkSnFZTDpGKSpuxvIu8/jE8Vt1LxJeqeDF0uNYUVi6WruinqziUtGXx2j1R2l5GufecC8jMl3Bop6PSkXr8S2LeTRF4mf19+6eji1w6VRafpxXsd6LZU+xh0MLvwht8NIYpeJdan1kcb0ah5VZuGpltaGmxlAuvpZXkeUnvFY6+14i/qVlcR2KS4/Tl4ne1tY2i9vaHtdfupR9MX5xSdTTtUj3Eskokw6WrljybY3DaLErKr+Kl7XF01HPHn6EIuGXit7SxnnFvP4sbcEt8vCjlEqXE3Hse7HX1sorSlFJVHLi/WtxrR62loi2rMXeE++lLTIsTrF4FYm+VHilZXF/tcn2YJZ/xOPf2l5camUj3aKSHfH8UWxjsDQqWAxvLNhn0Vt9LP8S2VrW9h6huCQqnjEPv9jq0IJfKt2K9F6s93gVjuhOgqu9SPSlHlw6t+FGsTAr53g1nl/LqtLp2v0U0v0+770NT+O5qhrq66OEwxGiSiT2kDQajRCJRNGrUleMaDRKxFYLiwOxp6mRiPD0bs8NhHgc5sGjWHgsJlqPLio+FuJhEvP4id62WaFx8PFfwdrkCreVso3OyhVO67v9b5J6xIJ5erXqIMK4DpIlGosTE85x3jFPTws/sbbyjdt9nLYNx/KwMqS/RBDz7LR41s6o5z8Lb8PXg9Zgno9aZXt30Udb9BKKfCRa2WvlttrUqotFi0muZ599kebRlr6y8OPPVoz4eLytPi2afFpHybF0rfziWLG4DrItLiImW2RXC6/jcWG1/Wy1/Fr1bm2NSY4nw+MRlyIN2+TEecbkwxZZEuzxabVNjFp5qEmkrXjRFh5RjzYqnS0vuUgUJ1+W/jN6CTGul+zzdIzK1qh4tup4nEdMvmtqbMD+s/ew5DRpvIjcQ7C8PT7iEVWNSMmYGuOwmHrJQxPfCJbWwxGPqPDiLS2/Ho2VH/XGW1yLGNausOe7qPSLqC3e0kLl3aItvmq1MerpYfHFrwU9ZmGWj+ScrF/M0y2iNosax7NyRGv5flpPSYzJHxHZYGla75ZWHRHX0bZJngcTfuvVRtfGU7ItrmR7OuvZ2hB/tvLj1crxeEl3r028I6oeTB62PmrFadXfE+HpGRVGqwbxe0x8WvW28lqgnq9b+Ry3JUxTc1Q8wuzadZSaunoaGptoaKhh5ZZj5A4aQAfxC4fDXt9Y3ifq2Mq/TS8p1vYsurhshbXsadNJOIKoX6LiGa8eyCKLJo4XUTxJr1iYxrCjjflIeqcmCMNu9nRrvU7A93wWd9px3tb3LbC4XlGsztYOT44Ex2R91MaCcD3UNp7CbdHbo/GeBbNILThRS9/yHLHPnl4xotZf6u/ayjrNQU2E1QaySWPL9kHU4xUh6sHljZb3eJsnQG3R3+ufiGgspiey9ceLiYhHF1W75fdpnDhcsqX38TiQXa34rbxOusfa9PHohduiuocV03ubPyWw9T3aiiRZ8TiWbtbPFqdN15jUiMXHlocfI44bVc/EfROWL6MtMqIejie27SfWyku8PT10lwgRx+J8Rf+5/mrjoAero6qe2q42vm0ypZvVo6W2orfZ2wJoo/PkeprIfy22i3treyvbVvqI9Lb2xWR5tCUmPRzxbbUramULTyBxar1i4h9tsVX3FhzP5rj4FsRYHM+2q7byOJFnm25WsBBO7As5tKVvZIulF9c4frQl5lqFtfjd4pzIo6U5Jvhxeyww5vENy664LjY+xVx+sK3EKtiwpZDqw9vZqP1PQcdcfYSIKm6MRTpeJSvS4vNYK1SwuA0n6Nwi35ooJnG/xV/kH+FJj5ila8OLc4uqT8Jqs6ixljbrY4+/gJbG9qGFRex7iw5tuJZW8BbwCTfZ38rP4sTFSbWo59eI2qxfoqKISYemhgbs/2eJ1cWbrwX3+kZ4Vr7NVyKO22X5tcE9xBN+Yp7fI57Poh6+pY3Z8RRulmwpUnGYrYdrCDdoXmhspO7IJvbXpTO0fxZWl8bmCJbGMo1Kt7DkxeS7mGSGxTeqe0T3iOy28LZnS3BCjVmZHl40rpPoYuITx48qEk5Abnm0Mrx2yYyKvwe2NKKNeDUWpxMs3ketfGJxGaJTk0dmf6KWxuogeKtNCMHCbbU85RGLelL12lpoYi12xOlb5Ei3mOUjHMvDVr16PE6ibZF/Eq1oPFz9HLc12tJXHgtO8oPwUPXsbaUVmifH8leVOoIoYvQcEY73LhqPf/ylzT8enYcTo1VOXD+PRduP3QME/VH8viiWprFR+LZVfOtqo1RWRajWvSmMDmwlOxxTDEc8eJXgzXpvrIvqPCaKxUHFju7mhmgcpzpKXYO8L2C4UbDKCLWSERFdlZ4tjzrRVwmvuTlGbXUEC1OzOEGT2iqkQ31TDEtTLZpKya3THrGyKoqWQZqiYzTovUp4lTVR6sXfjrlwU5QavdcKv0Z8LE+p4fFt/2n3QLsH/jIecP7kYpVvGpod/Al+nJijjZEOlRrjtaHJ0ebJKEm0vAuvqbU2GRpa8BqbXZq1iWkUrFHtzWGXxka1CxaurWHv3iIKK6I0NSEameT6cBDfBqNEKFrJCfvSGDK8O51THaLNhkYLE59WeQ3CjUi/QIKPYNBPKOTXF0LHS2YNwm1SjYlvwBVMX//qJb9J9FYXy6NRsutb9I3fJaNVb+ncJJlN0r+h0SHmirevlU+L7WqL84vDo4700KFck3g2SLZtszwaJLexhW9Do5/87t0pSPHTKBnhsGiF78k/gZ9ta1Zbo6dji0+EfyIva4Mnw8PB49cs+9rg0r9BvBusLqL14C0wK6+xIczRQ8Xs2FdORZ0hLNpm6WnxWuksv0bxb2jRrekEesvD1tY2i9vaHtffVf8a9S9t8XIifpPss7Kadbcy6j0/OViYrXFYq10OjdKhwdoiPeNyDI3St1m2NUjHRt3jvBRHrXYLt1nV8msQ/Yk49r3JazPS0YiX0xKzcX0trqWztVG09eL56drQpo8Tj1vZEvedaXl3PXviMEf8XRql68myoUF8WvVsarNJOkk/zz7Jtvo0i3+j6K0e8Xfx92BGPExcpvxworwmvcercMSn4URZlr/o4/iib8N15JO4H6yskyoh0jI64LaMqUbxtO0Neq+p1YelSoeqalcLIoNdIDmOi+s6xP/qwuA4Dq6j6jp6NnhFiK4rPCG1QLDEHsyDO1i4MQ6uR+fgCJeWYhxHcFfVwRGvFvDxm2COxWmplpdtNPbddXEdgzGqLe+Onm27gOJp+aqqzYKNceIwx7SgGBy1uY7ggsWhnFSOyxGOZ8+nsMTYcSXDdbAtxrTiOTiWrxuHn8TUvlg6267qqMZ9YznYRuK0Fq7qCNdCW3VxDBhjTrDFCN/Vu+PBHcdteUbvjvfsWD6WEBVjYcLxYHrW3RgjPAszGGwxWBpXtnlkre2u0wLXPY7I5xVjDI4qJxQjOZaf6/FoJTYt/CTbwkVjjDmui54dJ95mKVp5ON5LK5508Xg7GGNwXFf0jp75TLH0jnBObDDGCN+N1xY+rmt5WiF4JaaNhtCoPbibsohL2d4t7C5tIuBzsZsli2R5u1a2eDiqrmOkg2nhq2fixXFcfMJzLI53N/GG1l8Jcl0r38F1HOKtxrPL1woXnSv+fKo4jovrOjjiYZsc0buO0wKzEDBt7wZjjNpE4+gZg+Po2XX0BHE8F9dx4nDh8KlijINrdRFN691YHGNa4HF6D8bx0kbXxtPguMIVH0fyLC9HPOLPDvZuq+s6eLxa2yyuqgdTi+O6kuvoCVr1d2yjcdrgnFCMMYK7qg6OnvGKwXHjMEuK4K737lN/gyGRq2+6iNDuD3lj4ge8/tZ0NiedyRNjh+IXrs/nw3UMxhgc6eY6jt6dNv7GvoufY3EcB8vbES4tpbXddR3RG0GN7g6uE6+OBaEiGld8bLVxYYxLbrc+DChIVEyq/dPXCfiu46BXD8PRs+uIt+vQytu0vRvhGawMV416ky6u3h0MKmLitbmi9WhcHMGM9yyYRdK7xXEcgzFGtC6ufSZejPWXW8/W7cVE3RiHtK5BR2au4+C6juQ5uru6G48gztsVzGmBGd0dXCdexRqvtMqyPlJbnNprif8Y4be0OWq3sj6NE4e7OOIl5XEtvuvgfAF+C+N4u3A8PHs/gbHRu8fHMZYlre+O3j16Y3Bct02WXoXnxN+FY4xpewbTguvoCSwvn2g9ud7d8OliTAsv9/jdwzImzld0rhPnxxcV4TqqJza38XU8bmoyOOLjOpKj2opudbT2Oy2ANjpPbpzWcVzp4mDfjBG92hz7ArTSu66DI6DB6O56+HoFY/Tc8u44enYsiOPFCN/CXa/Ndeyzqu4i5XgxcTzBXcdp4+Ho+f9n7y4A66rPPo7/zrkWT5qk7kaFFihaKMN5hzM2BgwmDBsuYwwt7lDcYYwxbGMbrsWhuLvUhbrFkyvnfc5NU1q0Ern33O99c3KP/OV5Pv9/wniSN/hz+20dx/r5sfkT2w03FEqP6ch/OXJD/rUd1se/19ze+ti16/eR/3Ksz7I2K47hd5D07XztTnrccMiV67rpvsuHsn8HlBPW7Ncf1qX3vaNuG2yi/qWh5f+bTiu+HCfdN+Q6crTsZffcUGjZfTd9v2V+aybZ85D/vPnC5g9ZW9duO3beEkvzaK4bUjpGR2oZI+S6ckMhhay/4zgKhVyFXP9w5Kj5tbytPXOtXfPdFT87cl3r448TctXSxHHcZWO58sdyJS2dPFFL7XvJ3IkfacbShH3/Dtn3Rs+eOHLTY7jW31qnYwnJj+eb+/rWy5Eb8tvYke4bSvd1HFehcCR9rl5j9PutS/X03Q/q/v8+on88/rlG7vlb7dQvKjkhxSLNCNx0vgAAEABJREFUfWQv1w0pHHLlOI4cGy9sY7v2HrL3kCXlOI6Wn2vll+PPmW7nyg2FFHJdOY6j5vZ2ru++/DnSz0PWx8ZPt/D7WN+Q6ypk9xz/pt1zQyG7dpW+ts9uKKRQyJU98lukD9fvs+y+2/LA3v37/hFyrX265cqf0s9Crvw+juPauKH0ueTIDYUUch05jn9uz2yMkOvatdIv1879OFx77qTPQ/LPJae5b8iVPZL/KeSPZdfu8nZKv5xl16GQq3Rf6+CGQvKv7TTdxvXbLDtcJ31Ljn8d8vvYteNYe+uTfugsn9td3sZRc/uQXMfR970ca5uf76qik6uuXRx17Sw7HHXv6qpHt5B62Hu3Lv49O+x5d/+ef6TvO+pm7z26uVrexvo33wtZf9fGaR6zW5fm8brbGN3s6LFsjOZ5XOvvKD22jdfNxvDjaBnnh/ssi8n6LB/Pxvb7d7P5/LFbDv+eP2bL0aVSFptUWNCs8gM8zQ/5jAACrSLgtsooPziI/QTNvs9FElX68O0PNWVJkyIRyXU8O/z3NT08ORZ5fmqhXn3xaT390ot69b2v5BQ7KilytGDKZH0xa4ncmOTYT+8i+Y4Si2bouadf15OvfKI59Z7yokq/Qo7k2KcS67t4+hd66ZV39PLr7+rFl97S65/PlZvnWiHas7g9LZj8qd6we15ICq1mDpGIIwtZEZt34eTP9MYnc21efxzJdbTSEQ3bXJM+1ZtfzJNnXvY/T1Z6vmL7JvtJov1wL/3ccZR+X/F525/bWjhSJNSgmR8/qquu/5s+rZYK7KeosvttM/9P7R+LSSmb3j/sfKU47Hr5s+bnLTE6dl92+O/Nh7Vd1te//r5ntnXS86z4LH2+rJ+/1/2+3xzej66R3y7df1kcsp/GN6+rxZK+9+3+zfdX7OPomzb+uZPul7I4vea57XlL+xXf0w7WtuXe8n7Lc5Fc67v8vn/uSOl+9vXQfL95jm9cmq/9Z7L2zW2VjsW/59hP8OP2o2v72U96nHSu1k4Wh2e5+z+pr2+QliyV/VBDvBDIVIF2j8uxf1nwJy3qs4GOO2usLj5yRw3pku/fkut/oaXP+BR4AcdNp+j/mYTfH7S/Dtl/Tx160G906B4bqSKcftSBn5z03J797zD/e3v6IuM/OfbPJz/IIm28++907bijtfvwcv+GHL6u0g58QmDVBOxryfFbFmmngw7VeSceoJ8NKJH/HSvX/hnluH7WUtmgzXTSuWN1/iHbqH+5/Quh8bhOGsnOWvcjPawT0cjtdtcxh+yj3+2/t4445NfacUR5+ntc+nnrTsloARHw94a/ZXPp8HMOyPKRBgIZL9D8T8Q2C7O5AB1tWqAJzz6rj+Y3pAuwruvIfrinsP3LUdiKuUoXnGRvnvx/Kqaf2X3XonPs8K/9c9nLv3btu0TYflK19LMJen5Gnvbe59faemQXNS2Yqzeef1S33f53Pfb+LDkx62D9vQUT9b+Hn9bXTZ5S8z7Svx58QjPrpajrKWlN8lJL9MKjD+r5T2Yr5TpybJJQRJr36Qu66+HXNT/uKL/A0eyPX9L4tyYpHpJC1i5kY1t3WXP5MTr+hZ+C3ffz8u/5t8IRT4sW1iplk0Ut7jmfvKin3pxkc9lTK7LZ53R/v72fZ9Rcvv7wJT3pzxWRHPNxbM70mDa2Vng5Nrk/r/37nfy+fhsLTf61Yw/S18v6+M+Xx2zP/Pn8I93exmx57t+LWAz+fcf6frNO1miFfs33HSvyp5QKF2n4qA3UsyjPGjV/GIX8sdIxOLa8zbeXf/b/pVR2v6WNnTbHbXP6cfrx+8/8/Fva+dfhkCP/uX7gFQo7tl6uCgtdFeQ7ipqd3zQdzwrPCgtc5UUdf2h5jhSzHzYU2b0CO/xnLX39fj/0LGUDR5f18/v4R5HNm2d+6TWwWPPsJ8p+LP6z/Jgj39UfU9/z8sfy27UcxUUWo40lw8i3uPLsBxlq6Wzvro3v3/fjTvexuQttr7r+2OmcHKXvW9+WuUMWQ3G6natCPzZ75l+HrVMoavfsusCOln6xsD9Y8/qFzSv9zO9vtulnFocfn5+nP4e/Xr5LJGZj5Tn+I6XzsnnTIzlS1M6/GceVP7eWvcLLnhVZ7kWWSyyi9G9619RZDP5cy9rxhgACvoBn3zc9+T+s8a84clTA/oHj/zN1xSNTJBzHvulnSjCrEYdvmUp5y/+RuxpdaYoAAisI+F9LzccKN3Py1L6f+N9T7Pt1e6Xf7G7z2pzN5+01c3vNwzwIIIAAAtkkYCWntgzXldV4VRvuqj0O+IO26luk+tomVVslyf97lQsXLtGi6rj8Ipr/ryeOFdOcRIOWLKnWoiU1qm9Kqqm+XlXV9aqrb7J/CfDk/+2squoaJeJxzZq7QPFEkz2vVX5pJ5urUP0HravBXQvkuFY1sypYXr70xdvPaWK8r7bbaXPtvetWKpv9np7/dJ48K44VOkv07CMPa2p4oHb6+TbabrMNNXLYSG204Sba/Ze7qG/1+3r0zamqrU+q3+Z764+7bKhoQqqrq1V1Q8JikhINFmNNQ/pvEPnTJhrqtHBxlZZUNyppAMmZb+lfjz+r6UtqVVeVUl9/nN1GybWCtBdyFXKSql5arcWLq1VdF1ddY0r9tvy1Dt55A0UbrejmWAGuptZMqlVlz+WuvGZ+Tc4vBjfW1di81aqLewqHLK7Gei2pqlNdQ1zJZFwNZrnU/P32yXijlprz4qV1sukUskzqay2nugaLZanmLaxWQ1JK2hj+Oi2ujZupzZtsUnV1nWqqa62ovkQLqxrtvitbOhu/wQz80a2dHIUt99qqKi2wvP05rPbrP2g+rFnIOjmJxub1tjiM1fpITXV1qq6PKxH352pQ0nHkpuLpeKtqazRnyid6b+JsNZmLY3Fr2cv+t5XCYU/TPnhOV194no756xk646q7NWFyjZyQFAl5mv3Jy7rhsvN09F9O1wnnXaf7J0xSkz0rrpuqe2+8UEf+9SydeNqZOuak03XKpXfoxekJlcWnf+fZyfbshWlxlSdm6f6bLtGRJ43VsX89zcY9VYedcJpufnG2Fb+l2pkf666br9Dxfz1dx55xqW555B0tMNeVLJblEHIW65GbL9bxJ4/VsSedpqNPPkUHHX6abnl+slILX9UZp56rm56dKLdASsaTCtv7jHcf0zmnnqpjThmro6zPkcefpGPO+qcmmUmkbq6e+fetOvm003TM2At1zQNva4n1e/+Bq/THY07R8aedo5PGnqsTbJ6DjxunF6cu1Bv/ucLczkwbHHvy6Tru7GvshzmzlTKjqNegz195WBeec46OtnxOHneHXvh8YTqeBvvBzjWnnaTz739btY6jEi3W+H9eoROuGa/FtQv1v6tO0om3v6J6T/b1s1RvPna3xo49Q0effo4uuet5Ta1K2tpJ4VStPhh/v8456ywdZXOcdvU9emNajfwfCDU0evb1bonxgQACKwg4chxHrh3ilbsCtv6O07wXHKf5vd0wAjqR4zhyXbMULwQQWBsBx7Gvo/SxNqMEoa85+N9TzKK9snEcm3Olo71mZh4EEEAAAQS+K+B+91br3nFsuIav39QtV92g52dUK1z/lW6//Bxdfs8DGj/+37ru+mv0wNtfy8mTEgu+1AP/vksPPv+UHnv4ft332Cua9MXjOuvU03X3m9PlhZKa8fq/dOV94zVxxhxNnDJbi+d8pWdfek2fz25SXkmRuvXqrYr8kFKplPz/t/5IU61mzVykvK5dFW5IqdopVq8KTzOnLpBrc3752nP6ND5Qv9hppAqrpujRB/+nO269RKeef6e+cou109bratFXX2hR3VK9/tANuvKBNxQPpfTRo9fo3Nue1KKU1PDV07r4sqv08tf1UtWXeuBf9+jRF57Q/ff9R58tbtJn776hDz56Wy+/9a6mL6rVu4/fpMvvf0VJ048ll+rlJ+7R3Y8+o5df/J9uuPdJzV44R68//Dfd9ODrqrMCenzJRD18z116+uWndPttN+uZL6rl/+apX3D1vJQi0ZTmfvS87vr3f/XEU//T7Xc/pC+r4krOf183X3KyLrj/LSWSi/TYHdfqf+9NMxdPnz97t/41/mk9+Z/bdMvD76jJa9K7j16rsVfepAefeEi33HiJLr39Pj0/4WU99K+bdM7FV+s5K+RGE1aMveZ8Xfj3f2v88//VjTdcpXsmTFbc6v0hefJfflzhcJM+fem/+vt/H9dTj/9Tf3/gFfmFV/9Pl3jWwLP2qpqmR/97rx5+dryeevhe3fnIG1pqG2beBw/ogivG6cFnnte9d/5dr89YpNetYPnv8c/r6Qdv0wX/GK+6WGeVRKRkyp9R8tc7kudp3odP6IYb79Dbi6Lq27ub6ia/oKvGXa03FyVsaZ7XNdffqldmSb379VBR3WTdf/sV+vsrCxQNJzRv1lRNW5xUzwEDtE7fci384iXd868nNM8mWfz1FE1d9M2zRV+u8Gz2ZE1d4mjQiBEaOWJdjRw+RN07Fykx93P94+Zr9cA781Xes6c6x6r0/H9v0LW2HvWWv//nYZqjb/7suEktmjlJ05Yk1GOdERrljzVimHpUFFole6GmTp+muUvr5di+8an99yYr7k6dMlmJ8kHaYKTNbTEMGdhTnWxfPfOf63XjI+8rVdZDPQoTev2Bq3XZv16TSrqod6/O9vWwwMacpWRhZ/XsXqHCqKvquZM1aV6tyrv319ABvRRa9L7+ds8jmrI0pelv3qtLbnlQ0+JFZttd8Zmv69qrrtULkxrkpBo0d850vf7EvXrw7fmK5rlaPHe6Jn+9WAn74cfCrydpso0bcTy999StGnfPi6qKdVWfinx9/sytOv/vT2mxFcc/ffZvuuwfT2qeW67+PbuoauLzuvKqG/WmfX2HHEeJeLMVnxFAAAEEEEAAAQQQQAABBBDIRQFyRiAbBdy2DdqzwqYV+wYM15DOMVXXpVTZfZCGdeukLsO30Z+OOkxHbd9Trzz+iKZbofadp/+tz8Mb67A/7q0D991Xo/t305DNttdG/QqVUKHKSsLKj3bWpmM213rDe2v9Ef1U0WeU9vvlDlq/R0QNVpxKNjUt/y1cv0CnRJPqahrkRiMKhyzdcFShqKNGK0YnFy7WOxPnaNimm6qyaYGeHv+WSjbfU8f8YrQqe3RX56TFX1qukvoqNZWUa/jgvoo01kn5rvoPGqxiNarJhuw1aKT6lYXkWFVwzvsv6uPEMB166L46fM8x6lwaU//+A9S1z3Dt/vOfaUT/Yg3q20uRZH36N0envfmQHv8soj3220sH/v4P2mPzISot7qph63SV15SQ1WrlxLpo21/9UceYyw59GvTmu19IVnz1Ukl5rqu82ml65InX1XWrP+iEo/+g4d4neuSlSSpZf3MdstfPFJ0/SW9/PFVlG+6lA7YbrEhC6rH+rjrkT7/Ssb/YRIs/nKAvwzGNHDZMxVaM3H6f3+vCo6d1rtwAABAASURBVPZWbPqnquoxRieecrJ261er19+ZokjlIG3Qu5M6DdxcB/3pYJ245wh9OP4BfTjbU0HMlf8fxnIttvjst/XwK19ry9/up+P/9GtFrEg//rN65UesiGhF48JoXO8994DeaxqofX//Sx15wP/J+exxPfxBjfqtt5m6ql4FfUbrj7/dTbEpT+vJL8PadZ899Mdf7KaBeU221kmFrFit9MtLO8Tidfrsgzf0VWNf/f6ov+ic047Wnw/YXYNLazRzylx79oo+q+6j/Y/4q87661E674Q/aoNOTXrruZc0IxVTnu2Pkv5b67g/H6gzTj1UW/QtUvWcGZrTELGCf0gl/bda9uwwjelbrOq5MzS7Iay8aFT5JT00Yr1R2mjk+tpkk//TrhsU6fOPX7f91ahtDzjG5jtSF5x2vHYZWqapb47Xe/Nk80m2xfTNy1E4ElFhWRcNHzlKGw1fT1tstaXGjOyqqBVno/n5Vih31fLy03dCEUVjeerRf7g2WX9DbTBiA+2029YqXDBNr775qbqM3len/vVoXXjSMdpnyyGqnz5D5dvsryvPP1EHjO6nvIIS/fzAE3XV2QdqdI8yKZlQpMf6+sMRf9SpJx2hfTbtrdq5czRv6Uy9+uIE1fYcrcOPPVHnnnq0Tvnjr9Sn6WM98cYkJbyI8v34Ugv19H/v04QZTSop8uMNyXEcyytPBXlRNVbP1esvv63o8J/rhD8fowv+eqIO2mVj5S2YrWnzZ2jCS2/KW2dHnXD8cTr3tON00m92UnnVO3ryremyLy+1/JkB/+tCvBBAAAEEEEAAgY4TYGYEEEAAAQQQQACBVRT4ppq1ih1Wt5ljHRKJuBIpyXWshpSMy2ppcpKNWtyQVHmfkeoZWqTJX0/Tl/M8DRjUT5HqlGqUp3VGDFZYZdpkaGdNfv8dLW2q0ocLwhpYXmL9PTVZgTblF5itsJ0eU/ayYpdNYyf24dlhH45jd/yK1bJrv5AVtSL0gnlTtLixkwYNiOjrD1/XjNh62nqAq88+m6xo1x4qtH5Ofa0aC0pVZEM0JZKyapolISUSVhyWY//nn8etkOhZjiF1HjhM4YkP6czL/qvX6ytk9WY1NsWVMoP6+pTiCb99Up4TtQylyVNmqbjXcFWEU5q/MKlB66yjitKQ6uvjsumt4GaFykhKMz55Trfc9bRem1ZjhfSQ/FT8ArTCnqrmL9aC2sWa9MbD+sd9j+nLqqQiTlKLF6XUY/Tu2rJiuv777ESN2HSwovWePCsCJxtm6KF/PKG7XvxIVYlIerx4vFFeNE+uLVYoUqTKTp0UDXlKWCBFFeUKW7U0kfCUtvaatMSK+IXdR6h/fpWmza2TF46YTSodd8PX0zWvtkbvPf2Ybrv3eS2NFMhrsDZeSkmbLVxfrZkzatS5bw+FbJzaWKUG9gxr1tS5qvX/tElehbpWlqp7/97qHK5Vwi2xvGUeYblenaobGqWQml+e0nMmzXnpkiVKdB6oASUpzZkTV6fhO+kvp5ymPQbFNG/efCW7DdSwypDmz7U1qeiufpVl8urma16NFImEVTflWZ112uU68dRr9MrMBvVad0P1L4ybQUSN057T2elnV9uzens2SgMKk0pYIE1z3tYNV4zTheMu1gU3/UeTFjeovmaBqt0+GjKwVDUL41riVmpQ70o5qcWav8hTyK+gW+xa9vL8PRqKqXH2x7rzhnE657KLddHNd+rdOZ5ilmsy5SmVSimZTDYfdi3r4zopvf/oTTpv3OW64LIL9c9Xpqk2sVRLqiLq1b+XQraXZjcUa/v9TtBZR+6uysakFixNqta8PFuPhpqkFi1JqdH2qBfKlzvvfV13kRmcfrnunTBLfUasr77FTZprZt179VF5flJz7Lygoq96d8/XgrnzbP/H7esxT0NGDlNx9Se65/6HNbk+ZPsw5Ydoh2dr5NneWaAFS1x179NT+Ypr9pKk1t/5CCuQ76t+scWavUDq3a+fisMJfb0goU5d+6pbRbR5Dkc2hmwsMzAHT7wQ8AU4EEAAAQQQQAABBBBAAAEEEEAgkwXcVgnuJwZxHCddOEo3W3buqbkwJSuAyQp4SSukJT1HITckOfZ0WYEp1SQN2nCUSuqm6sUXpihaWaGSTgVWxZXkaNnLkQ3rV6bSBTrPxvJs3GTSSlSRPJWU5UsNTWq0wp0bb1Bdg6eCsjw11SxVPNxZFUWywud0FfTuozzX0cIlDaqoqFCB3Z/+5QyV9BugspgNb7M5dvgfjuPIdV0LP2nlVDu3B3Er6BUP3lInHHOg1ovO1X23XKn/fBJXgVUPPWuvdFwWk//hn1sB0d4kWwWbVimL2T/S92TzWbtISHrv8bv09LQCbb/DphrerVipVMIKflKsIKrCmKOUl1DcLbMC8xiN2WRz7f2H43XwjkMVTTqKJxx16t5VzoJp+mh6g8L5nhIzXtEd/3ldFRuN1vbr9VdxRFbQtMnkyDP3lL2nx0wmlLIYLRIlrfietEw9e9acipe+8tv7CfjUKQt82V3rl1SksKs22fJn2mrTbXTgYcdqrw3K1eC5Ki12LWVr6dl8Npid2ViWsCG0/FkKz3KMx1OqqfXUtd/66urO1Oefz9UXkz9TQ9EgDelRpIQVrmX9tewVCrmKxKyYXrtUNYqovCIit3qqXnr2Kb03u0FFhQVya5aoKhVWp/KI8hINqrJ9ISu6Ftr6+vHLjagkP6R5X8/QYvXXbnuNVidzMErJiajYfzZ7hhYm+2m3X2yhcj9O+2FEfv//0zW33KK7br1Td112jEaWx+SPG/OWaon9QKW4LKLSaEJLa+qNM6b8fMvdk+Ro+ctxbT811Sqv/xiNveIW3X/7P/SPS87U1r0d1SUk13INRwtUUhJSaUnUYnGsu6dE0tW2h12h+267Vffcfo9O33OA/VwipIjNV2PzRQoi6mx9Znw6QU88/7blZc9CIbnWW/Zy3JBCZufYtWPXckKKxaJqWvSVPqvtop3+bysNqChRKJRSbV2Nkm5IlWbrJGpVW9OkvIJCe+Yq0RhXj4321AE7DNXMD17RG7ZekbyIPNsXspf/5jj5ikWt+F1TJycasR9yRLV42tt67KnXNCceU1F+StXVNVI4rMqysJKNtaqriyuWX6iwIznpw7UfRriyU/FCAAEEEEAAAQQQQACBdhZgOgQQQAABBFZTwF3N9q3S3PMcRWJFqrRiXsPiGZqfKlbf7n01oKRJn372pZoKQ+paEVZhvqtUUop020ijuy/Sf556V6Vdeqtzgae4lZ9isZgi0Txr58iVvaxCVVhYqDwrbEVjhSoucBSPFKhf/26q+3qa4nkhFTYt0JT5YQ0c0EXRRL0SoajC1nnugmorKEvzJ3+kNycl1XdAN1VP+kRvzK/UVqP6WDnT6oYWt82S/vALYQ21dYpZrEVWYE5aUbPYxq+ZOV1VXYbr4OOO1D7DXH36xXyFXMeKhI4KS21+KzzGrLgXtcqym+eod48KLZj4nubGXXWtjKiswFUk4igWjSoSy1N+WJo9a44Kew3TwG5lirqy2lyeFZ49Tfn0I70zsUZlnbuozKnRzCUh9RtYrkG9SlQSDam4yNGsj95WbbftddjuvfXiQ49pviXrLpihualSrTuik7oW5VmB0LUioqNoJKpINGp+jp3HlBeJKGrXEXONxSIWV0wWtrWXIrEClVu+Xu0szWkoUO+uhRarm24Ts/iLuvdQuHq25jeWaJ3hlerdpUCFlm/V7Kl6851pqs4vUtduUS2atVCRopDKvBpNm92ozr0qrV3ExolakdZVyMzDBcXqUR7WV++8pbdmxbT7fgdoo+6uGq24brSyrSC/Ip/MK1C/AeuobOG7+s9jL+iNtz/SQw/epb/f/7SmuyUavM5IlS15R/c8OF6vv/+JHn3wEb06eYG6rruR+lvhsymZUn7P0Tr2jBN0zB6bq7j+K73zyRK50bC8ZFKxHpvpGHt2rD0raZhozxbLMRDHCsdqWqrJkyfpqymTNHHSZ/pkTkIDBo5Qn6LZevy/D+uFtz7WS088qMffnaxY9w01oqfUkEilQ1fLy/PkhGyuJlvLaZP15dQp+vKLzzVlTq3tU1uLkKclsz7TKxPe14uvvqO3Pp+nuK1R2HW0dN40fTlpkiZO/UoffzpTodKeGtyvUJ+++JjGT/hYr734lO666+965MO5Crs2oSM5jiPXYrdTpV+OKyfVKK9iqH5/+LE64/BfqnfTLH04caoanC7a2L4O5n74sh5+8nW9/tZHeuSJR/TunCJtst4g2xchWWc1ecXaco8DtP3AYtXVxhUKh+SP74ZcyZPyC7pr2NAKTX3rOY1/4SO99tqr+vddt6X/jngsv482GNld0958Wg89847eePM9PfjkU/qyqlybrtdPFqrtS6lm9iS9/fE01cp/+T++8N85EEAAgdwTIGMEEEAAAQQQQAABBBBAIBsErCrUxmFa0WnFGTwrKEaicU39+A098PCTuvOlqRq13S4aVBHTJjvuri5zn9LF4+7UzXc+pCfenKIGi9ANF2jE0N7y/6xEUbcuKkhJjUsX6dPPv9QMKxi/8u5U1VgxLbVgpl56cbzenfK1pnzymsa/+IVmL/U0YONdtFmXRXrknod1/X9fVt7IbbXtkGK54bBCjUtV0yR1q4zqlf/drsfem6Sa+vl66bEH9Nznjdpoq03UuySiuLXxK2mOY8XvRmvfb6i6xt/XDVf/S/e+8L7mLpqrj7+YrrmT39K9/3xAd9/3mD6o664t1uuq/PIeqlz6sf5275N65f1Z+mLiZM2a/oVe/2CRBozZU9t2W6w7brxNV956n+5+6j3NnD5Nn3w1TTOmfKyPptRr6MhhmvHcTbrsjuc0dVGNpkz5UlMXLNJbj9yuf46fLKeir3bdYQNNe+pmXXjN3br5vhf01eI6K/I9q3+/9KVKevXXqGH9lZr8lK6/8x3V9RypganPdc3F9+me96aounaOPnlzkqbMnKqZ0ybqs6nVmjH9K305a6YmfTVFs+bM1qRJMzRr2heatSSuaJ6nWV+8o0ceHa/bn/xAA7feSxt1qdU7b3+smXNm6P2PZ6q+y2jtObpSz9x+lS663vJ69HXN9aR5Hzyqm+96QjMaY9p0+13Uv+ED3XPPE7rtnkdV1eNn2nmDMs358n1NnjVDH306VVW21g1VCzTFiqtTZk7RpC/e07/uuFb3vTpL/p8S8auani2M1WBVHw9ryOZ76re7jdDMZ/6m0865WP95r0Hb73e4dhxUol4b7qKD995c1a/fpbHnXKhrHvtYfcfsq0N2X1fRpgbVN9Srrq5GCxd7Wmf9DdS/U5MmPPKgPq6yGZpqVWPPFvnP1hulARVxTXjUf5ZSKNmoBZNe1OUXnKexdpx2/nm69Zlp6j5stH53wN6qnPeCLr3oQp132xPy+m2vA3//c/UOS07YVSwihcOOInYd8vdWIqnFk97SLePO06kXna8zH5HjAAAQAElEQVRTx56nfz73lRrDYaUaFunj5+/ROeddqLMuuEDX/XuCFlvfeG2VJtx/hU497zydecHZOvvSf2pitJd+8ZuDtXnZDP3t6gt16vX3an75VjriD7uoi+vJ/431RLxBtfZDlLgZO2p+xRvr7F6tqmo8FXXfVNuvG9Yrjz2ht2c0aLO9/qQDN++sNx64Vqece6H+9U6Vfrbv4frVxuXy4nWqt7HqG+vllXTWL/f5hQYV1WlhdZMN7Kmprtb86pWKFGibvQ7VboMb9b/bLtKp427UB/ER+tNBv1C/siJtsceh+s36eRp/zzidcv6leuwr6ecHHKFdhpeaUSptNfOVB3TBTeM120aWVbW99DufEEAAAQQQQAABBBBAAAEEEGhTAQZHAIE1FHDXsN8qd3NC1tSzEpEViGUFNimpZDKsTt16a/A6I/Tz3X+jX43pK6dRKui3iQ455FD9YuvNtfGoURrap6vyrHtSKcV6baQdRq2nvp2lpqTkOIVa/+cH64zDfq1hvSoVtilChZ3Uo98I/ergP+vYvbfRwG7dVGiFvXhepXbcfW9tu+EG2mjLXbXfTpup0Np36jrQ3mfq88nSZnscrGN/u5u22GxnHXnsMfrNdltoyy021IDyAiX937QNeVbqcpT+bdeEFOo8Un884s/ab/tNNGS9rXXEMcdrz1E9VbHuTjpgx40tt/W06y/305geIcUr19chRx5tBdkR6lbaSev+/FCNtbj7dipUMr+rdj/gMP1h12212UabasNhfZVXUKFNdj1EpxzyC/XJj6aLpH8+ZH+NHj5U2+9ztE797VaKqUS7HDJWf91riBWQHQ3afG8de8hvtO0mo7Xx+kNVWRBVUa/1tNdev9CQUk81xaN03Cln6rdb9FWoYoQOPOIY7b/1phqxye46+cTDtWXPLuo7ej+NPXJ/Da0w9a4b64jjT9DeG/WUlyjVNvscqz//ZowqQk1qSoRU0rmXBq0zXNvutK/23XaQIvGYegzfSX/+89HaYWAnJbwCbbHnQTp6/z01ZsNNNWr4IBWaW58tf6fz/rK/utgahjuvq31+/UuNGTFSI0fvpN/v/X/qGZUK+2+lPx9/rHYYXpH+beuQW6gB62+hXXb6P/1izz2087CYXnruOX1dK8VCjvyX4zpyrJCqwq76v32P00UXjdN1l47TFeeO1WG7b6gyW71kXrm23vNPOu+8y3TdxZfr2gvP118O3EP2swgtjfXTb4++UOOO2lmltt7xshE64uTLddkxu6i4sJd+fdRFuuLoXdLPmuzZ4X+9zJ7tqsKCHvrFYWfrxksv1bjzL9GVF16q6y4Zp+N27KMqMxmx9a912thLdK3du+bSS3TW0QfqZ/0KVVddqzkz52rW1/M1245Zdj5vab52+9PZuvqiS3TFBRdb7Jfqhqsv1xE7DlV+tzE6/ezLdc2lV+j6y6/R7TfeoLG/21Ejh/5cZ1xyja6+4CJdecEluuqiK3TlWYdqUCSkzuv8TMecdJ71GafrL75M5594mLYaWKyUHCXqpXV3PkKXnjNW2/SS6m1tPMfVFr+7UNf/5bcaUOKoztR2PvwS3Tz2QA0ui8gr6qO9Dv6LLrc5rrM4rjz3LB35i01UKilSuYH+bN4Hje6jRK2nov5b669nX6XLD99RnYqtIH3CtRp30FaKWtvCXqN00DFn6kpr76/Dpacdp9026KpUwlOs61Dtd/gpujI9x5W6/KzTdOCOI1RpW7KgwJH/GrjLobrpnP3U17+Qa/+XPuETAggggAACCOSMAIkigAACCCCAAAIIZJOA27bBemqqT8mNRtRgtaOom6dQKKpIKKbKHn01ZEgv9e/WSV6TtbOCZLzRk1vUWesMHaxh6/RRn/KYGpusIBl1tXDGTOUPGqmyRqnB2nqujdGtp/r36aFu5UVy/HuRQlV27a4+vXuqX6/u6tGlVBHHkT9uMlyk3v36aFDvzsqTp3orkoUrBmizoZWa8Nij+rChRJtv2k/9OsfUq39XDe4a1aypszS/zlMq2WAFVUeRVEJuKF+OKzXGHRV06qZ1hw9Qn+6V6tatu3pXFikULVTPvr01oF9v9aqwArPF35QMqaRrTw0d2EuVpQWq6NZDA3p3U0VxzAqBlnuoQH0HDdSIIQM0sGe5YnlF6mLj9evdXZ3yQ4p7UXXvP1DrDu6hTp0q1L9nZxWGI8orK1eljZGKS/VxV2Vd+2jdoQM1pH83FYXDKijrrD42XsRzJLPp2qOXBvSqVDQlxTp107rrDrAYO6lrt67qVlGskvKu6m9zlhdGFC0qV78+PdXd7kfCBerSs4eNVamiWEwRN6qKrr01ZJ2eGtCzUq4VDuv9HypUdlEfv095oZQy42TUfiDQXyOGDlg+r5tXqi6VJbJ6vur99S4sV//+vTSwX3cVhywPW988i7ufrWG3ymIVeY167403tLhiY+259RBtvslgbTqkn4ojrlKWR9Lm8f9MSzJpU9q1v9a+V3nnLurdq5u62xhu3IyTjhK2zxpTYZX4cVqePbt0Up79UKTO1ihuJX1/Xfp1LUnvpcZ4WKVdutn+6qICs+7UtYf6L38WSj8b0Kf5WWmX7hrQt4ftu+7po1+f7upi6+LPV9/kqLC0XL1sP/a2fVIUtji8lGZ9/oxuvPJKXXnLDbr65hs17tpx+seL01Ro677SWP16qHOp7dhQkXqZSV/bN717dDPnburWqUh5sRL16ttL/l7pYzn1ta+HPrYm+ZLqzDdcUKYePbqrd48u6lQQUqPd893itmfySjurjxkVh6W4+SVTjooqe2hAj3JbY9vjVoiPFldq0MAe6hQLqakxpbiTp862X/pYPj3M1jan/P8epGwPd7c4Kguj8uxro/mHFD3s67tEjheW/wOnfl2KZdVva+9J0WJ17+7H1dX2cNTG9tJ/pqbJ4kvaWF27d1M/y7VP1yIV53sqtK6hkGNZybZyqbpWFCmSvuJThwowOQIIIIAAAggggAACCCCAAAIIBF9gLTN017L/D3b3/N96lqs5rz6ua//9jBq6r6Ot1ilQavFX+mLqVH3+wZuatDChaF5KxcWOSq3AVFpiBTArNkXCKblhT/l5i/XphP/qqlv+rS8SXbX1qHyFo546lToqKZLyY56idp2XJ5VY/5Z7MbuXvh+Tiq2dP25JoadYxEu3L7T5OpVI4YijzXfaVXuOaNKERx7QP//7nB4bP0FPPPOqnnv3QzW4SSuWOsoLT9Sjf/u3npkubT5mqDrbXEVFVhTL8xQOeYpZHHl2xCyO4pZ5bK6YPS+xefzcCux5xO7lF3wTt39earmUWp+o5Ru2w4+7qFBWWPTSsebbeWmxpzzr6z/PtzHTbSyvQjvPs0qjP0eZzeN7+G38eQrteZE99y2KLV7fJm+Zi3/t903Hbm382H3DAjv3x/bjKi5ont+/7xvmRVLy+7uN0/XFlCn64uM39cXcxub1s7nKbA4/Nn8+v0+JGZdZ3L65H5M/rj+vP64/n39etmy9o5abfxRYrmWWhx+3395vFysIa9iIftK0F3XvYxP0yIPjde+bc7XtzttpSDdzWZZ/ic3vH/5a+16+hT+mvzZFNo+/Bi3P/LVoeVZY6Mif03+e7mPr1DyO1NKuyPL7sWct7fwxWw5/Xfz5/LHTrn6O5l9ocRZEXQ3b9P905gWn6MLTjtd5px+ni889XcfvPlQl1iZs1i3j+O9pT9tvvq1/7dv47/7+KrS94xv718sPG6PI5imzvIvs6yndz+7l27kfU0t+Lc8KLD8/f/9+OlZr6695qb8W1idi+zK9D0tc+XvV3wf+XL5tsc1RZu3S+yviKT9f8vej3zftssxzRb8y6+Pvg5a48myOUrvnx+C/p79WLQZ/fD+/EttL4dAK32rse0vz95cV7nGKAAIIIIAAAggggEAOCJAiAggggAAC2SjgtlXQjtP824o91h+pbUeP0kEH/FwjuklOaWftc9ShOma3oepW6Fqxyo7i5kKxX/Tyi00lxa46WdGpoKRcozdfT1tb0XfXXTdSLyuUFdl9v51/NLd10sVn/9o/vrm38v1i67f8mY3jX5fae6SwQGN2/aUO2mM9rTuoi4YMrtCg/p21+dZjtN3GfVSeL3Xu1k9bbzNEO++9p/beootK/EKzxbh8vOVjywrejsXzzeHH5B/L29qcK577z/xYlt+zsfx7y6+tfXHRN+O13C+2+83nsjmbj+ZrJz2//7xl3PT58vZOuv2KbZvPle6XPre2xcvnVHP7ElelFlteWal2Pfgg/eXXG6hnUWjZ+jnNbex5uv/y9XS+GdOeFfvj2rvfJn1u1/75N4fS47TE7RcqY3khDfvZDjrolxtpeN9K9RvYQz/f+xe2Dt1Vmm/z+uPZOC3jNb87K83rF0eb78vGd1Z+tjxWLb/f0rYlLv96xfNvX7c8W/ldNlfz8Z37Vmgv7ZSvbj3L1KN7afPRo1RdK920ccl39pY/jrM8vpXGW75OKz/3Y/SPldqalX9v+WHX6ecr+KWv7X5Lm5br5Yb2bPk9//w7ffWdvP2xWvr45/7Rct3y7t9bfvjj2lFY4ChqPyTSt1/2vcVxnG/f5RoBBBBoTwHmQgABBBBAAAEEEEAAAQQQWEUBdxXbrXGz/Mq+2njD4erdKZb+e75uYVeNWH+ERg0fqMp8N33Ps9E9+7TiYbckz1VF78Eas9l66hbz0m39+yu2W6vzZYP5f8ahtOdAbTxqhNYbPtSOwepVGlYqZXP6bZwiDVxvfY2ywnTEJrRQLTb7sBO7TMcV+Hdz8HNUrEJDR47QRiMGq2thuDn3Zc/856192NBKeSF16z9EG4wYovVGrquhPUvkpdfGf5qt6+DJ318rHl4u7adVybV5efmMAAIIIIAAAggggAACCCCQ8wIAIIBANgu0eQFaVlVLpVJK2XvzLy36hTe7XlZA9O85Jph+t5OWd7sl2bXn+W1TNowj/5ns5b+3ymFj+YOGXMfG/yaulB+bFchcu+/4bfTNM88mTt+zT3bqd8+Nwxz8fK3cq5S/nnYYUXPuy575z1v7sKFlyyDP5kvZuqT8d38v2U1bAv9xcwx20dpzt+14jvz9teLRtvMp+5zECwEEEEAAAQQyToCAEEAAAQQQQAABBBBYTYG2L0BbVc11XSsiWoUwHZxjhTe7XqGAqB95OY7f1pUN8yOt1v6R4zhKx2lxuXbYpb55OcuftWShnH05yyxsTdR+L8f194HTPPfKiyNeCOSiADkjgAACCCCAAAIIIIAAAggggEDwBYKQoRuEJMgBAQQQQAABBBBAAAEEEEAAgTYUYGgEEEAAAQQQWEMBCtBrCEc3BBBAAAEEEOgIAeZEAAEEEEAAAQQQQAABBBDIJgEK0Nm0WpkUK7EggAACCCCAAAIIIIAAAggggEDwBcgQAQQQWEsBCtBrCUh3BBBAAAEEEEAAAQTaQ4A5EEAAAQQQQAABBBDIRgEK0Nm4AR/PfgAAEABJREFUasSMAAIdKcDcCCCAAAIIIIAAAggggAACCCAQfAEybCUBCtCtBMkwCCCAAAIIIIAAAggggAACbSHAmAgggAACCCCQzQIUoLN59YgdAQQQQACB9hRgLgQQQAABBBBAAAEEEEAAAQRWU4AC9GqCZUJzYkAAAQQQQAABBBBAAAEEEEAAgeALkCECCCAQBAEK0EFYRXJAAAEEEEAAAQQQaEsBxkYAAQQQQAABBBBAAIE1FKAAvYZwdEMAgY4QYE4EEEAAAQQQQAABBBBAAAEEEAi+ABkGSYACdJBWk1wQQAABBBBAAAEEEEAAgdYUYCwEEEAAAQQQQGAtBShAryUg3RFAAAEEEGgPAeZAAAEEEEAAAQQQQAABBBBAIBsFKECv3qrRGgEEEEAAAQQQQAABBBBAAAEEgi9AhggggAACrSRAAbqVIBkGAQQQQAABBBBAoC0EGBMBBBBAAAEEEEAAAQSyWYACdDavHrEj0J4CzIUAAggggAACCCCAAAIIIIAAAsEXIEMEWlmAAnQrgzIcAggggAACCCCAAAIIINAaAoyBAAIIIIAAAggEQYACdBBWkRwQQAABBNpSgLERQAABBBBAAAEEEEAAAQQQQGANBbKoAL2GGdINAQQQQAABBBBAAAEEEEAAAQSySIBQEUAAAQSCJEABOkirSS4IIIAAAggggEBrCjAWAggggAACCCCAAAIIILCWAhSg1xKQ7gi0hwBzIIAAAggggAACCCCAAAIIIIBA8AXIEIEgClCADuKqkhMCCCCAAAIIIIAAAgisjQB9EUAAAQQQQAABBFpJgAJ0K0EyDAIIIIBAWwgwJgIIIIAAAggggAACCCCAAAIIZLPAqhWgszlDYkcAAQQQQAABBBBAAAEEEEAAgVUToBUCCCCAAAKtLEABupVBGQ4BBBBAAAEEEGgNAcZAAAEEEEAAAQQQQAABBIIgQAE6CKtIDm0pwNgIIIAAAggggAACCCCAAAIIIBB8ATJEAIE1EHBdV6FQSP57y+FfrziUu+IF5wgggAACCCCAAAIIIIBAxwowOwIIIIAAAggggEA2CPgF57q6OtXU1Mh/r62tVX19vZYuXbpS+BSgV+LgAgEEEEBguQAnCCCAAAIIIIAAAggggAACCCAQfIE1yNDzvPRvPs+bN0+PPPKInnzySY0fP14PPvigPvvss/RvRLcMSwG6RYJ3BBBAAAEEEEAAAQQQQAABBDpQgKkRQAABBBDIFgHHcdTU1KR11llHY8aMUSqVUmNjowYNGrT8uiUXCtAtErwjgAACCCCAAALNAnxGAAEEEEAAAQQQQAABBBD4CQHHcdJ/esMvOo8aNUq9e/fWFltskS5M+78hrWUvCtDLIHjLRAFiQgABBBBAAAEEEEAAAQQQQACB4AuQIQIIZKtAy9+BHjhwoEaPHr28+Ow4zvKUKEAvp+AEAQQQQAABBBBAAIEcFyB9BBBAAAEEEEAAAQRWU8BxnPSf4PC7+b/57DjfFJ/9e65/k8MTBhiwB9gDmbQHiIX9yB5gD7AH2APsAfYAe4A9wB5gD7AH2APsgeDvgVxYY9dxHDkOh+Ng4DgYOA4GjoOB42DgOBg4DgaOg4HjYOA4GDgOBo6DgeNg4DiBNuDfi1lf9gB7gD3AHmAPrOIeiMViikZX/XBnzZwhDgzYA+wB9gB7gD3AHsiMPcA6sA7sAfYAe4A9wB5gD7AH2APsAfYAeyCT98D0aVOtnjxdM2es2uHW19WJA4Pv7AH2BV8X7AH2AHuAPcAeYA+wB9gD7AH2AHuAPcAeCP4eYI1ZY/YAe2A190AqlZL/Hx90XcfeV+EYtM4QcWDAHmAPsAfYA+wB9gB7gD3AHujYPYA//uwB9gB7gD3AHmAPsAfYA1mxBwavo4GDBq/y4XpWseZICQMM2APsgWV7gO8H/HOBPcAeYA+wB9gD7AH2AHuAPcAeYA+wB9gDwd8DrHE7rbHruK44MGAPsAfYA+wB9gB7gD3AHmAPsAfYA+yBjtkDuOPOHmAPsAfYA+yBIO8BV7wQQAABBBBAAAFfgAMBBBBAAAEEEEAAAQQQQACBVhagAN3KoK0xHGMggAACCCCAAAIIIIAAAggggEDwBcgQAQQQyAUBCtC5sMrkiAACCCCAAAIIIPBjAjxDAAEEEEAAAQQQQACBNhKgAN1GsAyLAAJrIkAfBBBAAAEEEEAAAQQQQAABBBAIvgAZ5pIABehcWm1yRQABBBBAAAEEEEAAAQRWFOAcAQQQQAABBBBoYwEK0G0MzPAIIIAAAgisigBtEEAAAQQQQAABBBBAAAEEEAiiAAXolVeVKwQQQAABBBBAAAEEEEAAAQQQCL4AGSKAAAIItJMABeh2gmYaBBBAAAEEEEAAge8T4B4CCCCAAAIIIIAAAggEWYACdJBXl9wQWB0B2iKAAAIIIIAAAggggAACCCCAQPAFyBCBdhagAN3O4EyHAAIIIIAAAggggAACCPgCHAgggAACCCCAQC4IUIDOhVUmRwQQQACBHxPgGQIIIIAAAggggAACCCCAAAIItJFABhWg2yhDhkUAAQQQQAABBBBAAAEEEEAAgQwSIBQEEEAAgVwSoACdS6tNrggggAACCCCAwIoCnCOAAAIIIIAAAggggAACbSxAAbqNgRkegVURoA0CCCCAAAIIIIAAAggggAACKwp4dsEhBc2AfLJvTe1LkY+1FKAAvZaAdEcAAQQQQAABBBBAAIGsEyBgBBBAIOMFHIuQQ8IAg47eA+K11gIUoNeakAEQQAABBNZcgJ4IIIAAAggggAACCCDQItDUJE2ZLr33kfTuhxwYsAcyYQ+8/7E082spmWz5SuV9dQWaC9Cr24v2CCCAAAIIIIAAAggggAACCCCQfQJEnLECfnHrqynSjFlSXT0HBuyBTNkDtXXS5GnStJmS5/8NlYz9LpK5gVGAzty1ITIEEEAAAQQQCLAAqSGAAAIIIIAAAr5AS0GrqkZavEQKhyXHkVyXAwP2QCbsAf/r0f+6nLdAaoqL1xoI2LezNehFFwSCI0AmCCCAAAIIIIAAAggggAACCHS4gP8b0Imk0sVn8WoLAcZEYI0F/CJ03IrPqdQaD5HTHSlA5/TykzwCCCCAAAIIIIAAAu0twHwIIIAAAggggAACuSRAATqXVptcEUAAgRUFOEcAAQQQQAABBBBAAAEEEEAAgeALdHCGFKA7eAGYHgEEEEAAAQQQQAABBBBAIDcEyBIBBBBAAIFcFKAAnYurTs4IIIAAAgjktgDZI4AAAggggAACCCCAAAIItJMABeh2gmaa7xPgHgIIIIAAAggggAACCCCAAAIIBF+ADBFAIJcFKEDn8uqTOwIIIIAAAggggEBuCZAtAggggAACCCCAAALtLEABup3BmQ4BBBDwBTgQQAABBBBAAAEEEEAAAQQQQCD4AmQoUYBmFyCAAAIIIIAAAggggAACCARdgPwQQAABBBBAoIMEKEB3EDzTIoAAAgggkJsCZI0AAggggAACCCCAQBsLeG08fmsMnw0xtkaejIGACVCANoSc/CBpBBBAAAEEEEAAAQQQQAABBBAIvkAHZ+ilUkolk0ql2qfi6lilKxyRXGf1EndCUjgsrWY3rcnLsUn8GP33NemfMX08W1tb3/ZZ2YzJmkDWQMBdgz50QQABBBBAAAEEEEAAgdUUoDkCCCCAAAK5JuBZZTKS76qkLKSCfKu62nWbGtgUycYmVVfVqym5GjNZv0R9g/Vr0Op0W40Zljf1i86JpgYtWVRrMRqIzb38YTadWOhuxFVRsauweCHw4wIUoH/ch6cIIBA8ATJCAAEEEEAAAQQQQAABBBBoYwG/+ByOJDX3iy/08gvv6uOJS6Wo5N/3p/Z/MzqZSNp1SslkQgk7X+m3pK2h/5vTyURCyW8981JJJZMpNY/R/DwRTyhaLE2851Tttc0ueniSVBBOKR5PKuU1z5Eex8Zd3s/mjcelwvIGPX3ewdp1m+P1kRWEIzZ+PD1vwuJqPpLLfoPbWz5WojkGz8/G8rI+fkyplM3lx7vsfvPT5ufxxoTCBSlNfvZqHbDDn/TCrDrFYv6zVPq3xJPpOZNKLZurua9n10l955nl4d/7pq21s3xa4kylz21cPy5/XN/L+qSSy8b69hwt9/3Ylz9rHtOfI/WtceSmVDtnmt5+7QstWl7tt/bL2iVa5mxOoqM+M2+GCFCAzpCFIAwEEEAAAQQQQAABBBBAIJgCZIUAAjkn4FkR1SpOebUTdc+ZR+joI/bT2Ivu0ZykX4O2ZykpnO+qU0VIsTxXpeVhVXYOqajAqr+ygqwVb52wo6JOIVV0Cati2TP/qdVQFSsKqVOZq1iBq06d7XllSIX5YcXrpO5b7a9TzxmrTbpKiZA9t2f5/hwVze0KYo6iy/qV27z5UamxOqz1f32Uzjj3EPWx+SOlIXVJz2t9bPxKOy8tdOTPHY65KvXHsnudOrmKhpvjDRdYTHadX+Sq3OLNj9h9G8v/sHTSMfu5LO/jP/APe+j4v0m8LNdyi9d3cPxn/uE4yi8O2ZhhO0LNRtZHIUelFltxvrW0a8/aFXUKq1OxxWn9CsrCKrVYCvy+FmtZiatwZJmp9SuxfKxnc5CuowLLudzut1hrhTGLrW16HHvuu7tWoC7s4mrOs7fp5KOu1nzrWxyVknKUb2vjj+OblZW6stT8ocQrtwXc3E6f7BFAAAEEEMghAVJFAAEEEEAAAQQQQKAdBFIpR7FCacrbr+mjyYXa/mdjVP3lu3rr3Sr5f5IjYs+mPXeXTj7u73rjhad05fEn6fDfnaU7n5wiq2EqHJPqp32gO848R4fvc4yOPOgs/fORT2T1ZRUUN2nCrVdq7HkP6Z2n/qUzDjxORx1xpZ56b5HyC6SaOdP0/hvvabFVVxe/+bDOPvwKPT/hOV191J915MEX6dG3ZujTx+/Wyb87TiedeJPemZFQXp6jRVM/03vvTLYq6mI9dsmZ2v9XR+uIfY7XUQecrMP3O1KX3fWZYjb+/Pef07ij/2rPjtUpZ9yl92fWq7BImvbITTpt7N16/u479NeDztQzX0kFYc+KslIkWafX/nGNjv710Tr1jLv1yeyELEUlk55CdlI7+S3dceoZOnLf43TsoRfo3qe+VFyS/3ep3br5euaWK/Xn/Y/RMQeerTsf+1xenpSY/4EuO+BI3fzkVHkWV37NVN158vE69aZPlO8t0f/OPFnX3P2ynrzleh2395E6/4aXNHvSh7rjjDN0xP6n6Mb/fJqew41IbtUMPXH1xTr2N0fr8APP0l1PT1TSCsp51ZN1x0l/1a0PvK0nbrpKx+1vOZ99v2ba+k596n5de/fbCmm6rj3yEr04QyrTAj3/95v0l98eq8P2PVEXjntUk5dI0ZDVuT3xymEBN4dz77DUmRgBBBBAAAEEEEAAAQQQQAABBIIvkJsZevJcV9H6Gn3w/BOaWral9j90Hw2o+ZFWN5YAABAASURBVEwvT3hHC5OO8sIpVc+frjeevEaXXPWoagrLlJr1pm497TyNnynF5r2n6085Ubc/8rlilUUKz/tEfzvzz7r2/q+UX+xqwcSP9OQ9l+qau95TfvcyzXvtf7r2nOv1caPU+NVrevzxRzStVkounqE3n/+7xl3yoGoKCrX0w6d12VGH6tI7P1BReVgfPfoPXTnuIdWUuPr6rWf16EMvWwk1rJJO5erauVI9euZr4YfP6uW3p6u8fzdVfzFeZx54tB77qlGduxdqygNX6ZyL/6cFdVJ88VS9eP8Nuv7mBzV9UZNStvieVdNjYU+fPnCZzr3sXs2oS2nxJ6/q/juf1WLlKxKNqHH6+7rupD/rtvHTlV9erNSc93TTKcfqhqdmqDhSp+dvOF0XXveEavNLFa3+TDeferRufXqeJVejj958Vp/NXCovLIXjVfrqzRf11mcLFUo0aPanE/S3iy7VU58uUmFoqcZff6aOPfJifVofVXjJx/rnuefpyclxlVrGD15yui6+9QUly0qVt/Qz3XrKSbr3rUbleXWa8t4LuunsC/Tkp3WqKGrSm/8ap3E3T5BTUqwCeUokwiquKFdFQaPGX3u+LrzyX/o6WajKwlq99I9LdP7Zf9espBRxUtbaUPjISQE3J7MmaQQQQAABBBBAAIFcEiBXBBBAAAEEEGgvAU+KxKTqOV/opec+V7/tt9Q6w3fQthtKbzz9iqbPTigcdRWK5inPLdMOx5yh8649XWecuI+6JD7SlFn1mvjOS3rpy7j2vfR6XXnjRbru3su10wBXr97/qCanwiqIxlQY7qffnHWxLrjmHP35gI3UNPd9TZ4u5VthtDRWppgrOeE8xUKF2vA3p+ui287TYftspFiTp62Pu1hX3n6hfr1FP1V98ZFmNzoqKC5TeUmeGkPF+tmBx+vSW87WIXuNUKIhojF/OFUHbNNJTU3Ser84RVfde7Vuv+8inXjgKM16/xMtqE0pz+ZNOYXa4rjLdNeDF2uHASnVW+FV8UWa8OATSg77jS68+wbdcsc4/Xa77mqyEnV+JK4Zbz6qp6dW6JDLrtKlt52va2+9UDv3r9XTD0zQzBlf6flHX1bP3U7VpbefryuvO197b1ysSZ/Pst5hFUXKzCIkv7LrOSHlFZWoqCAizwkrFouosP8OOnHcWJ1zwYka1WmhUkP31Xk3nanT/nKA+nifafJsV0umfKTnn3tLww6+QtffdYFuve1cbd1tgZ568A3FI3nKz4uqfN09dNoVp+ni68Zqjw266KuXPlKnLXfSbtuvIze2jg684FCt1/C2Hnz8dVVs+xfdcOdFuvgfN+ncQzbUtNef0IT3GlWQ5yjpe4hXLgq4uZg0OSOAQEcJMC8CCCCAAAIIIIAAAggggECQBfy/RRxxpFkT7ter9RvolztvrN59I9pin1+r0+zn9NxHs5WwmqmTalIy1U19Bxaotsqq1qGoFaYjcuM1qlkwR/XOcA0fVKDqBUlV53XX0EGVCi2dqXmLJUdWxM7vo159pCULU4oU5MkNS1aVleellPL/g3t2Kc+veBba/J1UX5WSF3aUX9BF3btZ4bWqSdG8UPpvFDs2fXM//0RqiksNk9/XVadepzn999Shf9hUXnVKFevuqN//fpjevupi/W6PM3Tb+BkqKAwrkfLkNdUr4QzQqJG9lKxLqSEhOVZ1SyUWas68sPoO6a/CUEJL4yH13WA9lfolaAuvbsYMNRQM1vCuhWqYl1S8pEL9Bg1S+OuJmlw1X4tryzRkvV5K1SS1JNxHf7j0HzrzkBHykk1W0PWU8uxIWW6yV/rck+NIyXiTKvr0VrHF1tRo90IR9RjQW26tp7gXletGFHISqlqySHXxIi186TYds+epOuzYa/XRAk/182bJaJWKx9V50ECVxD1VJxxFYnmKmHGDFfKbmpLyUnE1WaV9wfxqVdc1qP+odZVn1/MXJjV43SEKO3HNmzvX1seC8hfIwuQj9wTc3EuZjBFAAAEEEEAAAQQQQACBHBEgTQQQQKBdBazQadXcZONcvfLYe4onZ+jOEw/ULqP2018vfkZxLdErD7+hqkbJDTmSPGvjyfUrpnbuWQHVHsiJRBVSjRqSEfn/8buCmKdEQ1KeG5U98rvJc1KKpyTXdeXZ//kf+t5XSslEUo6189vYSFa89fs58ufzVurjpUukhXm1Gn/9NXpxUT8devox2rhHxOZ1Nf3pG/Sn347Vq7OjGrLhBhrcrdDGCCkaC8mVheVITiIl13HTRWB/PjlRxSJJNdTFFY6FVVIUUaqxxsrPzW2cvDxZcmp0wsorCSk/5CjeWKdktFAF4YhCTqPq61OKFYRUZA5ff/iq3vlkvo0fUtjx7D2mwmJXNrQSjQ2yG+lpZS/P8k7Jkf/h55qyYrEFZ5eeLFpr5yocCckXLB80Ultuu4k23mob/erQw3XAXhsplkjI/4GCASrlONbOxrKu9pG+77iOHDeiaL7FHXXlhsKqr21UtNBitVwSDRaPpEjEcvQ72cx2yUcOCrg5mDMpI4AAAggg0O4CTIgAAggggAACCCCAQOAFrIDshB01fvaMxn+e1KCRVtDcdLD6DR6skWNGad2BfbX4zcf1xmxZwTQiN+zK6q3NLE5I4bDVYr1C9Ro0TJ299/XQP5/T5Glf64tnHtVTb0xWwToba2CprJAdViQcktU/030d1+8bbi5v2nkovOzcsfHtPLSsod8u/cxJd5MbCilk4/hX/jPXDSmvwNP0Z/6lOx/7TIN+/mtt3r9OEz+fo0WLlmrKhPGaqEHa55Dfa+cxfdS4aL7qG2o1f84SJdLxh+Qum8sf00tJ4WhXDV23RF+8/oxee2myPnz5JT36v1fVECmUbL7OIzZUj6Y3dP/DL+pLy/XDZ57Ri69+puINNtKwrgPUv4/02iOP6oNPvta0t8fr6pNO0j0vz1M0WqJorEofvfmWPnv7cz103/16a0ajigrDStm8bigsPzfHD8Rk/LxDrpu+sqqx/MJzKuGqc8+B6lGe1OJEmTbdeUf9fOte+vSh2/W/Zyfb+BELMaRQyLURmrumx/WvbWD/N81T8SWa9eUChbr106Duxfrw0Xv1/IczNffLz/Wf/76oZF5XDRnWRenfwnasU/MwfM4xATfH8iVdBBBAAAEEEEAAAQQQQAABBBAIvgAZtruAp5TnKC9aradv/7e+bOys/c+9RJddf67Ov/Y8XXjTuTrruF0Vi7+s+/75uuq8Ri1KLFJ9ypNfl/Sa6rS0foEVepvUa+MddciBe2r2/WN18B77649/GqeF/XfUoUftpE7xJtXXLLV2S60QrXTfRN1SLaxaqEYrvKbqq7Soofnca6rV4sb5qmuyOawCFrd2i6oWp9u5rqf6pYu1aEGVUvassWqRFlbHlZo7Vfdcc5O+bGrQ9Gdv0TG7/U4H/3ofnXDly+q3za80wnlXZ/5uLx14/I2q6jNG/WvHa9yVD2tuXY1qG5eoKWFzpeus9skK8slQvsb88ThtXfKxzj/ql/rTSbdraflwVcZnae4ST9032UNHHjBGE/9+hg7b5QAdesKVWjT01zr+Dxsr1qmn9jr6BA1Z/ID+svcB+v0Rl2rhkP106C9HqKCin7bdayfVvHyefverI/W/j2vVp0e5quYtkRxPdYvna9HSetmSSKm4qhbM05LaJnsmefF6LW1coKqqWoV7DNP+xx6hwtcv1x93/pX2/dXJ+sgdof0OHKOCxnpVLZyvJdWN8l+Ok1LdkkVavLhKdQlpyKj1VRF/UZccdbEmxNfRQSccqhGpF3XqPvvr97v9Qfd8UqhfHn20FfFlayBZ3dofhiMHBexLLAezJmUEEEAAAQQQQACBdhJgGgQQQAABBBDIDQHHisGOmuojGvG7s3XTrRdqTE9Ps75OqKYqoXnTkyoetbcuufFeHbZdX3Xf9Le68tpLtWWXQjVUS4Ujd9E519+t346OabFXoe2OHKur7/mbzht3iS665TZdcfVZ2mHdQi1eFNIWh47VuBuP10BPaqiX+uxyvK66YZxGV0oV2x2uy6+7WluUSwWjfqULrrtNe4yIqXqxNGTPE3TZDRdpdBdp6dICbXv8hbrs2sNVYc82PMjmu+lo9YxUaqdTrtHV1/5NZ110tk668FydcdnFOmLf0eq55QG66NZbdenVl+usyy/XGRefo4tvv1WnHr6j1tn2SF173Rka2SNPDVavdZxmj0RcKh+xi06/4e+64fpbdcm1l+u0i8/TxVaQ37JHVA3qpB2Ou0g33H2zzr3iEl18y9901ZWnasv+Baqtd9V/+9/p/Fv/oUuuvkTnXn+Trrjqr9qsV1j1bpG2O/wc3XDb3brupht17sUXaOw11+v0gzdSU7hMu59tff6ygxX8pWTFMB15/b908n4jlDDrgqE7aOz19+j3m+drSW1Yw/Y4RBffcYfOuehcnX719bryxsu05/rFqinop99ffpfOPHQTWe1ZtU2V2u3US3XFZb9RyRKp13a/1fm336cLzzlMgwo8ddlyf5110x26xtZi7LU36/o7rtGh+4xQ2Dy0wm+G58bXA1muKOCueME5AggEWIDUEEAAAQQQQAABBBBAAAEEEGhDAau5KpHM06DNR2n0Fv1V6Dly/T+BYUc4FFIyr5PW33pjbbZed5VV9tbmO6yn7vlhJa1IGynvro233VSDu0eVbPIUV0y91l1XP/u/zbTlluuqZ6ew/D/jkIyH1GXYMG2x9WCVSEokpKK+gzVmm/XVJV/K7zlQW2y/gbrFpEjnXtp0+43Vr9xV3IqgZf3X0ebbjEy3a2oKqcd6I7X5lgOV3yR1HjZcW2w1UGUFxRq25Whts8OG2nrn0dpu5021/U5baMv1KpVMOuoyYl1t9fPNtOHQCps9qgGbbqxN17d8bN6tth+myoKQEknJt5C9HDsSCU9FfQZq9HabaNSwSuUXlGm9rdZXz5KwEg2eEspTT4vlZ/+3qcZsOVzdS8NqiHvy+zaaTWlf67v9ptpqm3XNIaK45ezZmMor0zqbb6SfbT1E5flRVQwerk3WK1dSEfXbZBNtvn5XuSnJyy/RiK1Ga9TgUnk2XqRTV2203WYa3DUiWawNcVcVAwZry5+PtjlGqp/hxes9JSNFNv6m2nhYuRxPiqdi6jtqPW2xeR/lW794qFCDNh2lLceso8p8R7V1nkp79dVG226ibXbcQMMGVcppSvlTmAIfuSzg5nLy5I4AAggggAACCCCAAAIItJUA4yKAAAK5KOAXXhuqU6q2w+q1KxE4KU+1S5OqrvWUiFubJSlZfVJ+H7+gWmPP6q0Y7DiOFTw9NdSmVLU4qaqlKTVawdVx7b5jhdC6lKrtntVh032Tjc3XNqRSNmC1P64VTD0r4tYsSVpfpdslVmhnU6jJxvfjTK04psVYV5XUUuvnz73U5l9iR5XF7FgVNm5z+/drrNjq/4f9GmqSzfnYvEttXj8Gf2yMfL84AAAQAElEQVSt8HIcR8kGi9HG9Pslk545pNRkCaRz8jw1Wiz+uH6u/p/xcK2PP4RrcyaW9a2y8dMOFm86IYu1vjop/37cxozXp1Tjx2kdG5fFZaeStasz21orKsv6pq0tlnorRvvX/hx+3/T8NkeD/QCgJS5/fD9mfxzrmo6zxcyRp3qbp6oqZcVpyXUd+ca+ue9WZ/F4jutP4XfnyGEBN4dzJ3UEEEAAgdwQIEsEEEAAAQQQQAABBBBoRwHHdeXa4Xx7TiuquqGQPXPkONYm5MppadPyrOWGf21juKGQXGvn2vXypun77vK+jrPCWCudO3JDIbV0dRxXro21fAp/HDtkr3TM6WeOXDekUCgkN9R8pM9dv5ej5nZ2364dp7mtmz53rY8rR9//+k6/kLVtaZwex102n3+/5YE/lqPlfa2P6zj+zebDzl03ZP1cOY6jdDvXST9z/PvLziXH2oTkxyn/5ThyQ3bd3FSy5477zfyuPVf65Sg9vru8odLtrK3SL2fZc9dGUPqVfu6P7R/W7pue6cd8ylEBt/3yZiYEEEAAAQQQQAABBBBAAAEEEAi+ABkigAACCCDwjQAF6G8sOEMAAQQQQAABBIIlQDYIIIAAAggggAACCCCAQAcLUIDu4AVg+twQIEsEEEAAAQQQQAABBBBAAAEEEAi+ABkigMB3BShAf9eEOwgggAACCCCAAAIIIJDdAkSPAAIIIIAAAgggkCECFKAzZCEIAwEEEAimAFkhgAACCCCAAAIIIIAAAggggEDwBX44QwrQP2zDEwQQQAABBBBAAAEEEEAAAQSyS4BoEUAAAQQQyDABCtAZtiCEgwACCCCAAALBECALBBBAAAEEEEAAAQQQQAABiQI0uyDoAuSHAAIIIIAAAggggAACCCCAAALBFyBDBBDIUAEK0Bm6MISFAAIIIIAAAggggEB2ChA1AggggAACCCCAAALfCFCA/saCMwQQQCBYAmSDAAIIIIAAAggggAACWSMQCklhOzwva0ImUARyRsD/ugxHJNfJ0JQzPCw3w+MjPAQQQAABBBBAAAEEEEAAAQSyQoAgEVgTAWdZQaukSOpUJiUSkl/sSqUkDgzYAx2/B/yvR//rsmulFI2uyVc5fShAswcQQAABBBBAIGgC5IMAAggggAACCGSdgP8b0IP6S716SAX5HBiwBzJmDxRI/ftKfXtJLT8wEq/VEqAAvVpcNF49AVojgAACCCCAAAIIIIAAAggggMCqCsSi0gArdI0aKW24XjYdxMp6BXcPjBoh9bYfDPk/JFrVr2XarSxAAXplD64QQAABBBBAAAEEEMheASJHAAEEEEAAAQQQQCDDBChAZ9iCEA4CCARDgCwQQAABBBBAAAEEEEAAAQQQQCD4AmT40wIUoH/aiBYIIIAAAggggAACCCCAAAKZLUB0CCCAAAIIIJChAhSgM3RhCAsBBBBAAIHsFCBqBBBAAAEEEEAAAQQQQAABBL4RoAD9jUWwzsgGAQQQQAABBBBAAAEEEEAAAQSCL0CGCCCAQIYLUIDO8AUiPAQQQAABBBBAAIHsECBKBBBAAAEEEEAAAQQQ+K4ABejvmnAHAQSyW4DoEUAAAQQQQAABBBBAAAEEEEAg+AJkmCUCFKCzZKEIEwEEEEAAAQQQQAABBBDITAGiQgABBBBAAAEEfliAAvQP2/AEAQQQQACB7BIgWgQQQAABBBBAAAEEEEAAAQQyTIACdBssCEMigAACCCCAAAIIIIAAAggggEDwBcgQAQQQQOCnBShA/7QRLRBAAAEEEEAAAQQyW4DoEEAAAQQQQAABBBBAIEMFKEBn6MIQFgLZKUDUCCCAAAIIIIAAAggggAACCCAQfAEyRGDVBShAr7oVLRFAAAEEEEAAAQQQQACBzBIgGgQQQAABBBBAIMMFKEBn+AIRHgIIIIBAdggQJQIIIIAAAggggAACCCCAAAIIfFcgaAXo72bIHQQQQAABBBBAAAEEEEAAAQQQCJoA+SCAAAIIZIkABegsWSjCRAABBBBAAAEEMlOAqBBAAAEEEEAAAQQQQACBHxagAP3DNjxBILsEiBYBBBBAAAEEEEAAAQQQQAABBIIvQIYIZJkABegsWzDCRQABBBBAAAEEEEAAgcwQIAoEEEAAAQQQQACBnxagAP3TRrRAAAEEEMhsAaJDAAEEEEAAAQQQQAABBBBAAIEMFWjFAnSGZkhYCCCAAAIIIIAAAggggAACCCDQigIMhQACCCCAwKoLUIBedStaIoAAAggggAACmSVANAgggAACCCCAAAIIIIBAhgtQgM7wBSK87BAgSgQQQAABBBBAAAEEEEAAAQQQCL4AGSKAwOoLUIBefTN6IIAAAggggAACCCCAQMcKMDsCCCCAAAIIIIBAlghQgM6ShSJMBBBAIDMFiAoBBBBAAAEEEEAAAQQQQAABBIIvsOYZUoBeczt6IoAAAggggAACCCCAAAIIINC+AsyGAAIIIIBAlglQgM6yBSNcBBBAAAEEEMgMAaJAAAEEEEAAAQQQQAABBBD4aQEK0D9tRIvMFiA6BBBAAAEEEEAAAQQQQAABBBAIvgAZIoBAlgpQgM7ShSNsBBBAAAEEEEAAAQQ6RoBZEUAAAQQQQAABBBBYdQEK0KtuRUsEEEAgswSIBgEEEEAAAQQQQAABBBBAAAEEgi+Q5RlSgM7yBSR8BBBAAAEEEEAAAQQQQACB9hFgFgQQQAABBBBYfQEK0KtvRg8EEEAAAQQQ6FgBZkcAAQQQQAABBBBAAAEEEMgSAQrQWbJQmRkmUSGAAAIIIIAAAggggAACCCCAQPAFyBABBBBYcwEK0GtuR08EEEAAAQQQQAABBNpXgNkQQAABBBBAAAEEEMgyAQrQWbZghIsAApkhQBQIIIAAAggggAACCCCAAAIIIBB8ATJcewEK0GtvyAgIIIAAAggggAACCCCAAAJtK8DoCCCAAAIIIJClAhSgs3ThCBsBBBBAAIGOEWBWBBBAAAEEEEAAAQQQQAABBFZdgAL0qltlVkuiQQABBBBAAAEEEEAAAQQQQACB4AuQIQIIIJDlAhSgs3wBCR8BBBBAAAEEEECgfQSYBQEEEEAAAQQQQAABBFZfgAL06pvRAwEEOlaA2RFAAAEEEEAAAQQQQAABBBBAIPgCZBgQAQrQAVlI0kAAAQQQQAABBBBAAAEE2kaAURFAAAEEEEAAgTUXoAC95nb0RAABBBBAoH0FmA0BBBBAAAEEEEAAAQQQQACBLBOgAL0GC0YXBBBAAAEEEEAAAQQQQAABBBAIvgAZIoAAAgisvQAF6LU3ZAQEEEAAAQQQQACBthVgdAQQQAABBBBAAAEEEMhSAQrQWbpwhI1AxwgwKwIIIIAAAggggAACCCCAAAIIBF+ADBFoPQEK0K1nyUgIIIAAAggggAACCCCAQOsKMBoCCCCAAAIIIJDlAhSgs3wBCR8BBBBAoH0EmAUBBBBAAAEEEEAAAQQQQAABBFZfINsK0KufIT0QQAABBBBAAAEEEEAAAQQQQCDbBIgXAQQQQCAgAhSgA7KQpIEAAggggAACCLSNAKMigAACCCCAAAIIIIAAAmsuQAF6ze3oiUD7CjAbAggggAACCCCAAAIIIIAAAggEX4AMEQiYAAXogC0o6SCAAAIIIIAAAggggEDrCDAKAggggAACCCCAwNoLUIBee0NGQAABBBBoWwFGRwABBBBAAAEEEEAAAQQQQACBLBVYjQJ0lmZI2AgggAACCCCAAAIIIIAAAgggsBoCNEUAAQQQQKD1BChAt54lIyGAAAIIIIAAAq0rwGgIIIAAAggggAACCCCAQJYLUIDO8gUk/PYRYBYEEEAAAQQQQAABBBBAAAEEEAi+ABkigEDrC1CAbn1TRkQAAQQQQAABBBBAAIG1E6A3AggggAACCCCAQEAEKEAHZCFJAwEEEGgbAUZFAAEEEEAAAQQQQAABBBBAAIHgC7RdhhSg286WkRFAAAEEEEAAAQQQQAABBBBYPQFaI4AAAgggEDABCtABW1DSQQABBBBAAIHWEWAUBBBAAAEEEEAAAQQQQACBtRegAL32hozQtgKMjgACCCCAAAIIIIAAAggggAACwRcgQwQQCKgABeiALixpIYAAAggggAACCCCwZgL0QgABBBBAAAEEEECg9QQoQLeeJSMhgAACrSvAaAgggAACCCCAAAIIIIAAAgggEHyBgGdIATrgC0x6CCCAAAIIIIAAAggggAACqyZAKwQQQAABBBBofQEK0K1vyogIIIAAAgggsHYC9EYAAQQQQAABBBBAAAEEEAiIAAXogCxk26TBqAgggAACCCCAAAIIIIAAAgggEHwBMkQAAQTaToACdNvZMjICCCCAAAIIIIAAAqsnQGsEEEAAAQQQQAABBAImQAE6YAtKOggg0DoCjIIAAggggAACCCCAAAIIIIAAAsEXIMO2F6AA3fbGzIAAAggggAACCCCAAAIIIPDjAjxFAAEEEEAAgYAKUIAO6MKSFgIIIIAAAmsmQC8EEEAAAQQQQAABBBBAAAEEWk+AAnTrWbbuSIyGAAIIIIAAAggggAACCCCAAALBFyBDBBBAIOACFKADvsCkhwACCCCAAAIIILBqArRCAAEEEEAAAQQQQACB1hegAN36poyIAAJrJ0BvBBBAAAEEEEAAAQQQQAABBBAIvgAZ5ogABegcWWjSRAABBBBAAAEEEEAAAQS+X4C7CCCAAAIIIIBA2wlQgG47W0ZGAAEEEEBg9QRojQACCCCAAAIIIIAAAggggEDABChAf8+CcgsBBBBAAAEEEEAAAQQQQAABBIIvQIYIIIAAAm0vQAG67Y2ZAQEEEEAAAQQQQODHBXiKAAIIIIAAAggggAACARWgAB3QhSUtBNZMgF4IIIAAAggggAACCCCAAAIIIBB8ATJEoP0EKEC3nzUzIYAAAggggAACCCCAAAIrC3CFAAIIIIAAAggEXIACdMAXmPQQQAABBFZNgFYIIIAAAggggAACCCCAAAIIIND6AplWgG79DBkRAQQQQAABBBBAAAEEEEAAAQQyTYB4EEAAAQRyRIACdI4sNGkigAACCCCAAALfL8BdBBBAAAEEEEAAAQQQQKDtBChAt50tIyOwegK0RgABBBBAAAEEEEAAAQQQQACB4AuQIQI5JkABOscWnHQRQAABBBBAAAEEEECgWYDPCCCAAAIIIIAAAm0vQAG67Y2ZAQEEEEDgxwV4igACCCCAAAIIIIAAAggggAACARVYoQAd0AxJCwEEEEAAAQQQQAABBBBAAAEEVhDgFAEEEEAAgfYToADdftbMhAACCCCAAAIIrCzAFQIIIIAAAggggAACCCAQcAEK0AFfYNJbNQFaIYAAAggggAACCCCAAAIIIIBA8AXIEAEE2l+AAnT7mzMjAggggAACCCCAAAK5LkD+CCCAAAIIIIAAAjkiQAE6RxaaNBFAAIHvF+AuAggggAACSLzvTgAAEABJREFUCCCAAAIIIIAAAggEX6DjMqQA3XH2zIwAAggggAACCCCAAAIIIJBrAuSLAAIIIIBAjglQgM6xBSddBBBAAAEEEGgW4DMCCCCAAAIIIIAAAggggEDbC1CAbntjZvhxAZ4igAACCCCAAAIIIIAAAggggEDwBcgQAQRyVIACdI4uPGkjgAACCCCAAAII5KoAeSOAAAIIIIAAAggg0H4CFKDbz5qZEEAAgZUFuEIAAQQQQAABBBBAAAEEEEAAgeAL5HiGFKBzfAOQPgIIIIAAAggggAACCCCQKwLkiQACCCCAAALtL0ABuv3NmREBBBBAAIFcFyB/BBBAAAEEEEAAAQQQQACBHBGgAJ0jC/39aXIXAQQQQAABBBBAAAEEEEAAAQSCL0CGCCCAQMcJUIDuOHtmRgABBBBAAAEEEMg1AfJFAAEEEEAAAQQQQCDHBChA59iCky4CCDQL8BkBBBBAAAEEEEAAAQQQQAABBIIvQIYdL0ABuuPXgAgQQAABBBBAAAEEEEAAgaALkB8CCCCAAAII5KgABegcXXjSRgABBBDIVQHyRgABBBBAAAEEEEAAAQQQQKD9BChAt5/1yjNxhQACCCCAAAIIIIAAAggggAACwRcgQwQQQCDHBShA5/gGIH0EEEAAAQQQQCBXBMgTAQQQQAABBBBAAAEE2l+AAnT7mzMjArkuQP4IIIAAAggggAACCCCAAAIIIBB8ATJEIC1AATrNwCcEEEAAAQQQQAABBBBAIKgC5IUAAggggAACCHScAAXojrNnZgQQQACBXBMgXwQQQAABBBBAAAEEEEAAAQRyTCAnC9A5tsakiwACCCCAAAIIIIAAAggggEBOCpA0AggggEDHC1CA7vg1IAIEEEAAAQQQQCDoAuSHAAIIIIAAAggggAACOSpAATpHF560c1WAvBFAAAEEEEAAAQQQQAABBBBAIPgCZIhA5ghQgM6ctSASBBBAAAEEEEAAAQQQCJoA+SCAAAIIIIAAAjkuQAE6xzcA6SOAAAK5IkCeCCCAAAIIIIAAAggggAACCCDQ/gLtXYBu/wyZEQEEEEAAAQQQQAABBBBAAAEE2luA+RBAAAEEEEgLUIBOM/AJAQQQQAABBBAIqgB5IYAAAggggAACCCCAAAIdJ0ABuuPsmTnXBMgXAQQQQAABBBBAAAEEEEAAAQSCL0CGCCCwkgAF6JU4uEAAAQQQQAABBBBAAIGgCJAHAggggAACCCCAQMcLUIDu+DUgAgQQQCDoAuSHAAIIIIAAAggggAACCCCAAALBF/jeDClAfy8LNxFAAAEEEEAAAQQQQAABBBDIVgHiRgABBBBAIHMEKEBnzloQCQIIIIAAAggETYB8EEAAAQQQQAABBBBAAIEcF6AAneMbIFfSJ08EEEAAAQQQQAABBBBAAAEEEAi+ABkigEDmCVCAzrw1ISIEEEAAAQQQQAABBLJdgPgRQAABBBBAAAEEEEgLUIBOM/AJAQQQCKoAeSGAAAIIIIAAAggggAACCCCAQPAFMjdDCtCZuzZEhgACCCCAAAIIIIAAAgggkG0CxIsAAggggAACKwlQgF6JgwsEEEAAAQQQCIoAeSCAAAIIIIAAAggggAACCHS8AAXojl+DoEdAfggggAACCCCAAAIIIIAAAgggEHwBMkQAAQS+V4AC9PeycBMBBBBAAAEEEEAAgWwVIG4EEEAAAQQQQAABBDJHgAJ05qwFkSCAQNAEyAcBBBBAAAEEEEAAAQQQQAABBIIvQIY/KkAB+kd5eIgAAggggAACCCCAAAIIIJAtAsSJAAIIIIAAApknQAE689aEiBBAAAEEEMh2AeJHAAEEEEAAAQQQQAABBBBAIC1AATrNENRP5IUAAggggAACCCCAAAIIIIAAAsEXIEMEEEAgcwUoQGfu2hAZAggggAACCCCAQLYJEC8CCCCAAAIIIIAAAgisJEABeiUOLhBAICgC5IEAAggggAACCCCAAAIIIIAAAsEXIMPMF6AAnflrRIQIIIAAAggggAACCCCAQKYLEB8CCCCAAAIIIPC9AhSgv5eFmwgggAACCGSrAHEjgAACCCCAAAIIIIAAAgggkDkCFKDbai0YFwEEEEAAAQQQQAABBBBAAAEEgi9AhggggAACPypAAfpHeXiIAAIIIIAAAgggkC0CxIkAAggggAACCCCAAAKZJ0ABOvPWhIgQyHYB4kcAAQQQQAABBBBAAAEEEEAAgeALkCECqyRAAXqVmGiEAAIIIIAAAggggAACCGSqAHEhgAACCCCAAAKZK0ABOnPXhsgQQAABBLJNgHgRQAABBBBAAAEEEEAAAQQQQGAlgUAWoFfKkAsEEEAAAQQQQAABBBBAAAEEEAikAEkhgAACCGS+AAXozF8jIkQAAQQQQAABBDJdgPgQQAABBBBAAAEEEEAAge8VoAD9vSzcRCBbBYgbAQQQQAABBBBAAAEEEEAAAQSCL0CGCGSPAAXo7FkrIkUAAQQQQAABBBBAAIFMEyAeBBBAAAEEEEAAgR8VoAD9ozw8RAABBBDIFgHiRAABBBBAAAEEEEAAAQQQQACBzBNo7QJ05mVIRAgggAACCCCAAAIIIIAAAggg0NoCjIcAAggggMAqCVCAXiUmGiGAAAIIIIAAApkqQFwIIIAAAggggAACCCCAQOYKUIDO3LUhsmwTIF4EEEAAAQQQQAABBBBAAAEEEAi+ABkigMBqCVCAXi0uGiOAAAIIIIAAAggggECmCBAHAggggAACCCCAQOYLUIDO/DUiQgQQQCDTBYgPAQQQQAABBBBAAAEEEEAAAQSCL7BGGVKAXiM2OiGAAAIIIIAAAggggAACCCDQUQLMiwACCCCAQPYIUIDOnrUiUgQQQAABBBDINAHiQQABBBBAAAEEEEAAAQQQ+FEBCtA/ysPDbBEgTgQQQAABBBBAAAEEEEAAAQQQCL4AGSKAQPYJUIDOvjUjYgQQQAABBBBAAAEEOlqA+RFAAAEEEEAAAQQQWCUBCtCrxEQjBBBAIFMFiAsBBBBAAAEEEEAAAQQQQAABBIIvkL0ZUoDO3rUjcgQQQAABBBBAAAEEEEAAgfYWYD4EEEAAAQQQWC0BCtCrxUVjBBBAAAEEEMgUAeJAAAEEEEAAAQQQQAABBBDIfAEK0Jm/RpkeIfEhgAACCCCAAAIIIIAAAggggEDwBcgQAQQQWCMBCtBrxEYnBBBAAAEEEEAAAQQ6SoB5EUAAAQQQQAABBBDIHgEK0NmzVkSKAAKZJkA8CCCAAAIIIIAAAggggAACCCAQfAEyXCsBCtBrxUdnBBBAAAEEEEAAAQQQQACB9hJgHgQQQAABBBDIPgEK0Nm3ZkSMAAIIIIBARwswPwIIIIAAAggggAACCCCAAAKrJEABepWYMrURcSGAAAIIIIAAAggggAACCCCAQPAFyBABBBDIXgEK0Nm7dkSOAAIIIIAAAggg0N4CzIcAAggggAACCCCAAAKrJUABerW4aIwAApkiQBwIIIAAAggggAACCCCAAAIIIBB8ATLMfgEK0Nm/hmSAAAIIIIAAAggggAACCLS1AOMjgAACCCCAAAJrJEABeo3Y6IQAAggggEBHCTAvAggggAACCCCAAAIIIIAAAtkjQAF6TdeKfggggAACCCCAAAIIIIAAAgggEHwBMkQAAQQQWCsBCtBrxUdnBBBAAAEEEEAAgfYSYB4EEEAAAQQQQAABBBDIPgEK0Nm3ZkSMQEcLMD8CCCCAAAIIIIAAAggggAACCARfgAwRaBUBCtCtwsggCCCAAAIIIIAAAggggEBbCTAuAggggAACCCCQvQIUoLN37YgcAQQQQKC9BZgPAQQQQAABBBBAAAEEEEAAAQRWSyArC9CrlSGNEUAAAQQQQAABBBBAAAEEEEAgKwUIGgEEEEAg+wUoQGf/GpIBAggggAACCCDQ1gKMjwACCCCAAAIIIIAAAgiskQAF6DVioxMCHSXAvAgggAACCCCAAAIIIIAAAgggEHwBMkQgOAIUoIOzlmSCAAIIIIAAAggggAACrS3AeAgggAACCCCAAAJrJUABeq346IwAAggg0F4CzIMAAggggAACCCCAAAIIIIAAAtknsLoF6OzLkIgRQAABBBBAAAEEEEAAAQQQQGB1BWiPAAIIIIBAqwhQgG4VRgZBAAEEEEAAAQTaSoBxEUAAAQQQQAABBBBAAIHsFaAAnb1rR+TtLcB8CCCAAAIIIIAAAggggAACCCAQfAEyRACBVhWgAN2qnAyGAAIIIIAAAggggAACrSXAOAgggAACCCCAAALZL0ABOvvXkAwQQACBthZgfAQQQAABBBBAAAEEEEAAAQQQCL5Am2RIAbpNWBkUAQQQQAABBBBAAAEEEEAAgTUVoB8CCCCAAALBEaAAHZy1JBMEEEAAAQQQaG0BxkMAAQQQQAABBBBAAAEEEFgrAQrQa8VH5/YSYB4EEEAAAQQQQAABBBBAAAEEEAi+ABkigEDwBChAB29NyQgBBBBAAAEEEEAAgbUVoD8CCCCAAAIIIIAAAq0iQAG6VRgZBAEEEGgrAcZFAAEEEEAAAQQQQAABBBBAAIHgCwQ3QwrQwV1bMkMAAQQQQAABBBBAAAEEEFhdAdojgAACCCCAQKsKUIBuVU4GQwABBBBAAIHWEmAcBBBAAAEEEEAAAQQQQACB7BegAJ39a9jWGTA+AggggAACCCCAAAIIIIAAAggEX4AMEUAAgTYRoADdJqwMigACCCCAAAIIIIDAmgrQDwEEEEAAAQQQQACB4AhQgA7OWpIJAgi0tgDjIYAAAggggAACCCCAAAIIIIBA8AXIsE0FKEC3KS+DI4AAAggggAACCCCAAAIIrKoA7RBAAAEEEEAgeAIUoIO3pmSEAAIIIIDA2grQHwEEEEAAAQQQQAABBBBAAIFWEaAA3SqMbTUI4yKAAAIIIIAAAggggAACCCCAQPAFyBABBBAIrgAF6OCuLZkhgAACCCCAAAIIrK4A7RFAAAEEEEAAAQQQQKBVBShAtyongyGAQGsJMA4CCCCAAAIIIIAAAggggAACCARfgAyDL0ABOvhrTIYIIIAAAggggAACCCCAwE8J8BwBBBBAAAEEEGgTAQrQbcLKoAgggAACCKypAP0QQAABBBBAAAEEEEAAAQQQCI4ABegfWkvuI4AAAggggAACCCCAAAIIIIBA8AXIEAEEEECgTQUoQLcpL4MjgAACCCCAAAIIrKoA7RBAAAEEEEAAAQQQQCB4AhSgg7emZITA2grQHwEEEEAAAQQQQAABBBBAAAEEgi9Ahgi0iwAF6HZhZhIEEEAAAQQQQAABBBBA4IcEuI8AAggggAACCARXgAJ0cNeWzBBAAAEEVleA9ggggAACCCCAAAIIIIAAAggg0KoCGVmAbtUMGQwBBBBAAAEEEEAAAQQQQAABBDJSgKAQQAABBIIvQAE6+GtMhggggAACCCCAwE8J8BwBBBBAAB/mjZ8AAAycSURBVAEEEEAAAQQQaBMBCtBtwsqgCKypAP0QQAABBBBAAAEEEEAAAQQQQCD4AmSIQO4IUIDOnbUmUwQQQAABBBBAAAEEEPi2ANcIIIAAAggggAACbSpAAbpNeRkcAQQQQGBVBWiHAAIIIIAAAggggAACCCCAAALBE/h2ATp4GZIRAggggAACCCCAAAIIIIAAAgh8W4BrBBBAAAEE2kWAAnS7MDMJAggggAACCCDwQwLcRwABBBBAAAEEEEAAAQSCK0ABOrhrS2arK0B7BBBAAAEEEEAAAQQQQAABBBAIvgAZIoBAuwpQgG5XbiZDAAEEEEAAAQQQQACBFgHeEUAAAQQQQAABBIIvQAE6+GtMhggggMBPCfAcAQQQQAABBBBAAAEEEEAAAQSCL9AhGVKA7hB2JkUAAQQQQAABBBBAAAEEEMhdATJHAAEEEEAgdwQoQOfOWpMpAggggAACCHxbgGsEEEAAAQQQQAABBBBAAIE2FaAA3aa8DL6qArRDAAEEEEAAAQQQQAABBBBAAIHgC5AhAgjkngAF6NxbczJGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTaRYACdLswMwkCCCDwQwLcRwABBBBAAAEEEEAAAQQQQACB4AvkboYUoHN37ckcAQQQQAABBBBAAAEEEMg9ATJGAAEEEEAAgXYVoADdrtxMhgACCCCAAAItArwjgAACCCCAAAIIIIAAAggEX4ACdPDX+Kcy5DkCCCCAAAIIIIAAAggggAACCARfgAwRQACBDhGgAN0h7EyKAAIIIIAAAgggkLsCZI4AAggggAACCCCAQO4IUIDOnbUmUwQQ+LYA1wgggAACCCCAAAIIIIAAAgggEHwBMuxQAQrQHcrP5AgggAACCCCAAAIIIIBA7giQKQIIIIAAAgjkngAF6NxbczJGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTaRYACdLsw/9Ak3EcAAQQQQAABBBBAAAEEEEAAgeALkCECCCCQuwIUoHN37ckcAQQQQAABBBDIPQEyRgABBBBAAAEEEEAAgXYVoADdrtxMhgACLQK8I4AAAggggAACCCCAAAIIIIBA8AXIEAEK0OwBBBBAAAEEEEAAAQQQQCD4AmSIAAIIIIAAAgh0iAAF6A5hZ1IEEEAAgdwVIHMEEEAAAQQQQAABBBBAAAEEckcgdwvQubPGZIoAAggggAACCCCAAAIIIIBA7gqQOQIIIIBAhwpQgO5QfiZHAAEEEEAAAQRyR4BMEUAAAQQQQAABBBBAIPcEKEDn3pqTMQIIIIAAAggggAACCCCAAAIIIIBA8AXIEIGMEKAAnRHLQBAIIIAAAggggAACCCAQXAEyQwABBBBAAAEEcleAAnTurj2ZI4AAArknQMYIIIAAAggggAACCCCAAAIIINCuAh1SgG7XDJkMAQQQQAABBBBAAAEEEEAAAQQ6RIBJEUAAAQQQoADNHkAAAQQQQAABBIIvQIYIIIAAAggggAACCCCAQIcIUIDuEHYmzV0BMkcAAQQQQAABBBBAAAEEEEAAgeALkCECCLQIUIBukeAdAQQQQAABBBBAAAEEgidARggggAACCCCAAAIdKkABukP5mRwBBBDIHQEyRQABBBBAAAEEEEAAAQQQQACB4At8O0MK0N8W4RoBBBBAAAEEEEAAAQQQQACB7BcgAwQQQAABBDJCgAJ0RiwDQSCAAAIIIIBAcAXIDAEEEEAAAQQQQAABBBDIXQEK0Lm79rmXORkjgAACCCCAAAIIIIAAAggggEDwBcgQAQQySoACdEYtB8EggAACCCCAAAIIIBAcATJBAAEEEEAAAQQQQIACNHsAAQQQCL4AGSKAAAIIIIAAAggggAACCCCAQPAFMjJDCtAZuSwEhQACCCCAAAIIIIAAAgggkL0CRI4AAggggAACLQIUoFskeEcAAQQQQACB4AmQEQIIIIAAAggggAACCCCAQIcKUIDuUP7cmZxMEUAAAQQQQAABBBBAAAEEEEAg+AJkiAACCHxbgAL0t0W4RgABBBBAAAEEEEAg+wXIAAEEEEAAAQQQQACBjBCgAJ0Ry0AQCCAQXAEyQwABBBBAAAEEEEAAAQQQQACB4AuQ4Q8JUID+IRnuI4AAAggggAACCCCAAAIIZJ8AESOAAAIIIIBARglQgM6o5SAYBBBAAAEEgiNAJggggAACCCCAAAIIIIAAAghQgA7+HiBDBBBAAAEEEEAAAQQQQAABBBAIvgAZIoAAAhkpQAE6I5eFoBBAAAEEEEAAAQSyV4DIEUAAAQQQQAABBBBAoEWAAnSLBO8IIBA8ATJCAAEEEEAAAQQQQAABBBBAAIHgC5BhRgtQgM7o5SE4BBBAAAEEEEAAAQQQQCB7BIgUAQQQQAABBBD4tgAF6G+LcI0AAggggED2C5ABAggggAACCCCAAAIIIIAAAhkhQAG6TZeBwRFAAAEEEEAAAQQQQAABBBBAIPgCZIgAAggg8EMCFKB/SIb7CCCAAAIIIIAAAtknQMQIIIAAAggggAACCCCQUQIUoDNqOQgGgeAIkAkCCCCAAAIIIIAAAggggAACCARfgAwR+CkBCtA/JcRzBBBAAAEEEEAAAQQQQCDzBYgQAQQQQAABBBDISAEK0Bm5LASFAAIIIJC9AkSOAAIIIIAAAggggAACCCCAAAItAsEtQLdkyDsCCCCAAAIIIIAAAggggAACCARXgMwQQAABBDJagAJ0Ri8PwSGAAAIIIIAAAtkjQKQIIIAAAggggAACCCCAwLcFKEB/W4RrBLJfgAwQQAABBBBAAAEEEEAAAQQQQCD4AmSIQFYIUIDOimUiSAQQQAABBBBAAAEEEMhcASJDAAEEEEAAAQQQ+CEBCtA/JMN9BBBAAIHsEyBiBBBAAAEEEEAAAQQQQAABBBDIKIE2KUBnVIYEgwACCCCAAAIIIIAAAggggAACbSLAoAgggAACCPyUAAXonxLiOQIIIIAAAgggkPkCRIgAAggggAACCCCAAAIIZKQABeiMXBaCyl4BIkcAAQQQQAABBBBAAAEEEEAAgeALkCECCKyqAAXoVZWiHQIIIIAAAggggAACCGSeABEhgAACCCCAAAIIZLQABeiMXh6CQwABBLJHgEgRQAABBBBAAAEEEEAAAQQQQCD4AqubIQXo1RWjPQIIIIAAAggggAACCCCAAAIdL0AECCCAAAIIZIUABeisWCaCRAABBBBAAIHMFSAyBBBAAAEEEEAAAQQQQACBHxKgAP1DMtzPPgEiRgABBBBAAAEEEEAAAQQQQACB4AuQIQIIZJUABeisWi6CRQABBBBAAAEEEEAgcwSIBAEEEEAAAQQQQACBnxKgAP1TQjxHAAEEMl+ACBFAAAEEEEAAAQQQQAABBBBAIPgCWZkhBeisXDaCRgABBBBAAAEEEEAAAQQQ6DgBZkYAAQQQQACBVRWgAL2qUrRDAAEEEEAAgcwTICIEEEAAAQQQQAABBBBAAIGMFqAAndHLkz3BESkCCCCAAAIIIIAAAggggAACCARfgAwRQACB1RWgAL26YrRHAAEEEEAAAQQQQKDjBYgAAQQQQAABBBBAAIGsEKAAnRXLRJAIIJC5AkSGAAIIIIAAAggggAACCCCAAALBFyDDNRWgAL2mcvRDAAEEEEAAAQQQQAABBBBofwFmRAABBBBAAIGsEqAAnVXLRbAIIIAAAghkjgCRIIAAAggggAACCCCAAAIIIPBTAhSgf0oo858TIQIIIIAAAggggAACCCCAAAIIBF+ADBFAAIGsFKAAnZXLRtAIIIAAAggggAACHSfAzAgggAACCCCAAAIIILCqAhSgV1WKdgggkHkCRIQAAggggAACCCCAAAIIIIAAAsEXIMOsFqAAndXLR/AIIIAAAggggAACCCCAQPsJMBMCCCCAAAIIILC6AhSgV1eM9ggggAACCHS8ABEggAACCCCAAAIIIIAAAgggkBUCFKDXapnojAACCCCAAAIIIIAAAggggAACwRcgQwQQQACBNRWgAL2mcvRDAAEEEEAAAQQQaH8BZkQAAQQQQAABBBBAAIGsEqAAnVXLRbAIZI4AkSCAAAIIIIAAAggggAACCCCAQPAFyBCBtRWgAL22gvRHAAEEEEAAAQQQQAABBNpegBkQQAABBBBAAIGsFKAAnZXLRtAIIIAAAh0nwMwIIIAAAggggAACCCCAAAIIILCqAtlbgF7VDGmHAAIIIIAAAggggAACCCCAAALZK0DkCCCAAAJZLUABOquXj+ARQAABBBBAAIH2E2AmBBBAAAEEEEAAAQQQQGB1Bf4fAAD//9rOeEMAAAAGSURBVAMADBQlWGrY1lsAAAAASUVORK5CYII=", + "created": 1772207236966, + "lastRetrieved": 1772207236966 + } + } +} \ No newline at end of file diff --git a/docs/pipelines/anonymizer/pipeline.png b/docs/pipelines/anonymizer/pipeline.png new file mode 100644 index 00000000..61ce789e Binary files /dev/null and b/docs/pipelines/anonymizer/pipeline.png differ diff --git a/docs/pipelines/datapublic/README.md b/docs/pipelines/datapublic/README.md new file mode 100644 index 00000000..53b4199c --- /dev/null +++ b/docs/pipelines/datapublic/README.md @@ -0,0 +1,76 @@ +# Datapublic Pipeline +Language: **English** | [Español](../../es/pipelines/datapublic/README.md) + +Workflow-oriented technical reference for the datapublic information extraction flow. + +## Scope +This flow extracts structured information from paragraphs and supports document-level validation persistence for public dataset curation. + +## Diagram +![Datapublic pipeline diagram](pipeline.png) + +## Runtime entrypoints +- `POST /misc/document-extract` +- `POST /datapublic/predict/{document_id}` +- `GET /datapublic/validation/document/{document_id}` +- `POST /datapublic/validation/document/{document_id}` + +## Step-by-step flow +1. Text extraction (`/misc/document-extract`) splits source document into normalized paragraphs. +2. Prediction (`/datapublic/predict/{document_id}`) processes each paragraph and returns predictions; cache persistence happens only when `use_cache=true` (default). +3. UI review aggregates document-level validated output. +4. Validation read/write endpoints persist and retrieve document-level validation payload. + +## Technical components + +### Pipeline configuration +- Source: `resources/pipelines/production/datapublic/pipeline.json` +- Preprocess: + - `aymurai.models.flair.utils.FlairTextNormalize` +- Models: + - `aymurai.models.flair.core.FlairModel` (`aymurai/flair-ner-spanish-judicial`) + - `aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex` (`return_only_with_detalle=true` in production) +- Postprocess: + - `aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer` + - `aymurai.transforms.datetime_formatter.core.DatetimeFormatter` + - `aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer` (4 configured instances in production for `CONDUCTA`, `CONDUCTA_DESCRIPCION`, `DETALLE`, and `OBJETO_DE_LA_RESOLUCION`) + - `aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer` + +### Algorithms and processing notes +- NER extraction over judicial paragraphs. +- Decision filtering/classification for relevance, gated in production so `DECISION` is emitted only when a `DETALLE` entity is already present. +- Rule-based + embedding-based subcategorization. +- Date/time normalization and article-based subcategory mapping. + +### API contracts used by this flow +- `TextRequest` +- `DocumentInformation` +- `DataPublicDocumentAnnotations` (free-form document-level validation payload) + +### Core backend modules +- Router: `aymurai/api/endpoints/routers/datapublic/datapublic.py` +- Pipeline loading and inference: datapublic predict route in `aymurai/api/endpoints/routers/datapublic/datapublic.py` + +## Persistence (DB) +Tables touched by this flow: +- `datapublic_paragraph` +- `datapublic_document` +- `datapublic_document_paragraph` + +## Notes +- Current production pipeline directory name is `datapublic`. +- `document_id` is the document-level grouping key used to associate paragraph predictions and validation payloads. +- Validation persistence is document-level and intentionally accepts a free-form JSON object. +- `GET /datapublic/validation/document/{document_id}` returns `404` when the document does not exist; `POST` upserts the validation payload. +- Public router currently does not mount `/datapublic/dataset/*` routes. +- Paragraph-level validation route exists in code as commented legacy logic and is not part of the public flow. + +## Models used by this flow +- Flair NER: [../../models/flair-model-card.md](../../models/flair-model-card.md) +- Decision classifier: [../../models/decision-model-card.md](../../models/decision-model-card.md) + +## Related docs +- Pipelines index: [../README.md](../README.md) +- Datapublic entities: [../../entities/datapublic/README.md](../../entities/datapublic/README.md) +- API reference: [../../api/README.md](../../api/README.md) +- Internal database: [../../database/README.md](../../database/README.md) diff --git a/docs/pipeline/assets/pipeline.png b/docs/pipelines/datapublic/pipeline.png similarity index 100% rename from docs/pipeline/assets/pipeline.png rename to docs/pipelines/datapublic/pipeline.png diff --git a/notebooks/experiments/decision/01-training-2class-embeddingbag.ipynb b/notebooks/experiments/decision/01-training-2class-embeddingbag.ipynb new file mode 100644 index 00000000..7870bdb7 --- /dev/null +++ b/notebooks/experiments/decision/01-training-2class-embeddingbag.ipynb @@ -0,0 +1,561 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44b15c4f", + "metadata": {}, + "source": [ + "# Tiny decision classifier (EmbeddingBag, no torchtext)\n", + "\n", + "A fastText-style baseline for the decision detection task. It uses a hashed bag of words with `nn.EmbeddingBag` pooling and a single linear head to stay extremely small. No `torchtext` dependencies.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e40eb3f5", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n" + ] + }, + { + "cell_type": "markdown", + "id": "ce1f616a", + "metadata": {}, + "source": [ + "## Imports and config\n", + "\n", + "- Uses deterministic hashing for tokens to keep a fixed vocab size without building a vocab file.\n", + "- Model size is controlled by `vocab_size` and `embed_dim` (for example, 20k vocab and 64-dim embeddings is roughly 1.3M parameters).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "461aa6da", + "metadata": {}, + "outputs": [], + "source": [ + "import hashlib\n", + "import os\n", + "import random\n", + "import re\n", + "from dataclasses import dataclass\n", + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import torch\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.utils.class_weight import compute_class_weight\n", + "from torch import nn\n", + "from torch.cuda.amp import autocast, GradScaler\n", + "from torch.utils.data import DataLoader, Dataset\n", + "from unidecode import unidecode\n", + "\n", + "SEED = 42\n", + "random.seed(SEED)\n", + "np.random.seed(SEED)\n", + "torch.manual_seed(SEED)\n", + "torch.cuda.manual_seed_all(SEED)\n", + "torch.backends.cudnn.deterministic = True\n", + "\n", + "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "\n", + "@dataclass\n", + "class Config:\n", + " data_path: str = \"sentences-decision-manual.csv\"\n", + " vocab_size: int = 20000\n", + " embed_dim: int = 64\n", + " max_tokens: int = 128\n", + " batch_size: int = 512\n", + " lr: float = 5e-3\n", + " weight_decay: float = 1e-3\n", + " epochs: int = 50\n", + " dropout: float = 0.1\n", + " num_workers: int = 2\n", + " checkpoint_path: str = \"checkpoints/tiny-embeddingbag.pt\"\n", + "\n", + "\n", + "cfg = Config()\n", + "\n", + "print(f\"device: {device}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "69fbe089", + "metadata": {}, + "source": [ + "## Load data\n", + "\n", + "Input columns: `path`, `nro_registro`, `tomo`, `sentence`, `decision`, `hace_lugar`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5574c092", + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.read_csv(\n", + " cfg.data_path,\n", + " usecols=[\"path\", \"nro_registro\", \"tomo\", \"sentence\", \"decision\", \"hace_lugar\"],\n", + ")\n", + "print(data.head())\n", + "print(f\"Loaded {len(data)} rows\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "3bc71145", + "metadata": {}, + "source": [ + "## Preprocess labels\n", + "\n", + "- Drop null rows and duplicate sentences.\n", + "- Collapse labels to a binary target: `0 = not decision`, `1 = decision` (regardless of `hace_lugar`).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cafdac5c", + "metadata": {}, + "outputs": [], + "source": [ + "data.dropna(inplace=True)\n", + "\n", + "\n", + "def force_bool(value):\n", + " return True if value in [\"True\", True, 1, \"1\"] else False\n", + "\n", + "\n", + "def get_category(row):\n", + " decision, hace_lugar = row\n", + " if not decision:\n", + " return 0\n", + " if decision and not hace_lugar:\n", + " return 1\n", + " if decision and hace_lugar:\n", + " return 1\n", + " raise ValueError(\"unexpected label combo\")\n", + "\n", + "\n", + "data[\"decision\"] = data[\"decision\"].apply(force_bool).astype(bool)\n", + "data[\"hace_lugar\"] = data[\"hace_lugar\"].apply(force_bool).astype(bool)\n", + "data[\"category\"] = data[[\"decision\", \"hace_lugar\"]].apply(get_category, axis=1)\n", + "\n", + "data.dropna(subset=[\"category\"], inplace=True)\n", + "data.drop_duplicates(subset=\"sentence\", inplace=True)\n", + "\n", + "print(f\"After cleaning: {len(data)} rows\")\n", + "print(data[[\"category\"]].value_counts(normalize=True) * 100)\n" + ] + }, + { + "cell_type": "markdown", + "id": "cae1826b", + "metadata": {}, + "source": [ + "## Train/val/test split\n", + "\n", + "80/10/10 with stratification on the binary label.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52cb5244", + "metadata": {}, + "outputs": [], + "source": [ + "train_df, test_df = train_test_split(\n", + " data, test_size=0.2, random_state=SEED, stratify=data[\"category\"]\n", + ")\n", + "test_df, val_df = train_test_split(\n", + " test_df, test_size=0.5, random_state=SEED, stratify=test_df[\"category\"]\n", + ")\n", + "\n", + "for name, df_ in {\"train\": train_df, \"val\": val_df, \"test\": test_df}.items():\n", + " print(\n", + " f\"{name}: {len(df_)} rows | class balance: {df_['category'].value_counts(normalize=True).to_dict()}\"\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "id": "07ae169f", + "metadata": {}, + "source": [ + "## Tokenization and dataset\n", + "\n", + "- Simple normalize → whitespace split.\n", + "- Deterministic 32-bit Blake2 hash per token to map into `[0, vocab_size)`.\n", + "- Truncate to `max_tokens` to bound memory.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1332f587", + "metadata": {}, + "outputs": [], + "source": [ + "def normalize_text(text: str) -> str:\n", + " text = unidecode(str(text)).lower()\n", + " text = re.sub(r\"\\s+\", \" \", text).strip()\n", + " return text\n", + "\n", + "\n", + "def tokenize(text: str) -> list[str]:\n", + " text = normalize_text(text)\n", + " return [tok for tok in text.split(\" \") if tok]\n", + "\n", + "\n", + "def hash_token(token: str, vocab_size: int) -> int:\n", + " digest = hashlib.blake2b(token.encode(\"utf-8\"), digest_size=4).digest()\n", + " return int.from_bytes(digest, \"little\") % vocab_size\n", + "\n", + "\n", + "def encode_text(text: str, cfg: Config) -> torch.Tensor:\n", + " tokens = tokenize(text)\n", + " token_ids = [hash_token(tok, cfg.vocab_size) for tok in tokens[: cfg.max_tokens]]\n", + " if not token_ids:\n", + " token_ids = [0]\n", + " return torch.tensor(token_ids, dtype=torch.long)\n", + "\n", + "\n", + "class HashedTextDataset(Dataset):\n", + " def __init__(self, frame: pd.DataFrame, cfg: Config):\n", + " self.texts = frame[\"sentence\"].tolist()\n", + " self.labels = frame[\"category\"].astype(int).tolist()\n", + " self.cfg = cfg\n", + "\n", + " def __len__(self):\n", + " return len(self.labels)\n", + "\n", + " def __getitem__(self, idx: int):\n", + " token_ids = encode_text(self.texts[idx], self.cfg)\n", + " label = self.labels[idx]\n", + " return token_ids, label\n", + "\n", + "\n", + "def collate_batch(batch):\n", + " token_seqs, labels = zip(*batch)\n", + " offsets = torch.zeros(len(batch), dtype=torch.long)\n", + " total = 0\n", + " flat_tokens = []\n", + " for i, tokens in enumerate(token_seqs):\n", + " offsets[i] = total\n", + " flat_tokens.append(tokens)\n", + " total += len(tokens)\n", + " flat_tokens = torch.cat(flat_tokens)\n", + " labels = torch.tensor(labels, dtype=torch.long)\n", + " return flat_tokens, offsets, labels\n", + "\n", + "\n", + "train_ds = HashedTextDataset(train_df, cfg)\n", + "val_ds = HashedTextDataset(val_df, cfg)\n", + "test_ds = HashedTextDataset(test_df, cfg)\n", + "\n", + "train_loader = DataLoader(\n", + " train_ds,\n", + " batch_size=cfg.batch_size,\n", + " shuffle=True,\n", + " collate_fn=collate_batch,\n", + " num_workers=cfg.num_workers,\n", + ")\n", + "val_loader = DataLoader(\n", + " val_ds,\n", + " batch_size=cfg.batch_size,\n", + " shuffle=False,\n", + " collate_fn=collate_batch,\n", + " num_workers=cfg.num_workers,\n", + ")\n", + "test_loader = DataLoader(\n", + " test_ds,\n", + " batch_size=cfg.batch_size,\n", + " shuffle=False,\n", + " collate_fn=collate_batch,\n", + " num_workers=cfg.num_workers,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "bc80c8af", + "metadata": {}, + "source": [ + "## Model: EmbeddingBag + Linear\n", + "\n", + "Tiny architecture with mean pooling. Dropout is optional for slight regularization.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2605dcc2", + "metadata": {}, + "outputs": [], + "source": [ + "class TinyEmbeddingBagClassifier(nn.Module):\n", + " def __init__(self, cfg: Config, num_classes: int = 2):\n", + " super().__init__()\n", + " self.embedding = nn.EmbeddingBag(cfg.vocab_size, cfg.embed_dim, mode=\"mean\")\n", + " self.dropout = nn.Dropout(cfg.dropout)\n", + " self.head = nn.Linear(cfg.embed_dim, num_classes)\n", + "\n", + " def forward(self, tokens, offsets):\n", + " x = self.embedding(tokens, offsets)\n", + " x = self.dropout(x)\n", + " return self.head(x)\n", + "\n", + "\n", + "model = TinyEmbeddingBagClassifier(cfg).to(device)\n", + "param_count = sum(p.numel() for p in model.parameters())\n", + "print(model)\n", + "print(f\"Parameter count: {param_count:,}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "1a029057", + "metadata": {}, + "source": [ + "## Training utilities\n", + "\n", + "- Class-weighted cross entropy to compensate imbalance.\n", + "- Simple train/val loop with best-model tracking.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8d02447", + "metadata": {}, + "outputs": [], + "source": [ + "class_weights = compute_class_weight(\n", + " class_weight=\"balanced\",\n", + " classes=np.unique(train_df[\"category\"]),\n", + " y=train_df[\"category\"],\n", + ")\n", + "class_weights = torch.tensor(class_weights, dtype=torch.float, device=device)\n", + "\n", + "criterion = nn.CrossEntropyLoss(weight=class_weights)\n", + "optimizer = torch.optim.AdamW(\n", + " model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay\n", + ")\n", + "scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n", + " optimizer,\n", + " mode=\"min\",\n", + " factor=0.5,\n", + " patience=1,\n", + " threshold=1e-3,\n", + " min_lr=1e-5,\n", + ")\n", + "scaler = GradScaler(enabled=(device == \"cuda\"))\n", + "\n", + "\n", + "def run_epoch(model, loader, optimizer=None, scaler=None):\n", + " train_mode = optimizer is not None\n", + " model.train() if train_mode else model.eval()\n", + "\n", + " total_loss = 0.0\n", + " total_correct = 0\n", + " total_examples = 0\n", + "\n", + " for tokens, offsets, labels in loader:\n", + " tokens = tokens.to(device)\n", + " offsets = offsets.to(device)\n", + " labels = labels.to(device)\n", + "\n", + " if train_mode:\n", + " optimizer.zero_grad(set_to_none=True)\n", + " with autocast(enabled=device == \"cuda\"):\n", + " logits = model(tokens, offsets)\n", + " loss = criterion(logits, labels)\n", + " scaler.scale(loss).backward()\n", + " scaler.step(optimizer)\n", + " scaler.update()\n", + " else:\n", + " with torch.no_grad(), autocast(enabled=device == \"cuda\"):\n", + " logits = model(tokens, offsets)\n", + " loss = criterion(logits, labels)\n", + "\n", + " preds = logits.argmax(dim=1)\n", + " total_correct += (preds == labels).sum().item()\n", + " total_loss += loss.item() * labels.size(0)\n", + " total_examples += labels.size(0)\n", + "\n", + " avg_loss = total_loss / total_examples\n", + " acc = total_correct / total_examples\n", + " return avg_loss, acc\n", + "\n", + "\n", + "best_state = None\n", + "best_val_loss = float(\"inf\")\n", + "\n", + "for epoch in range(cfg.epochs):\n", + " train_loss, train_acc = run_epoch(model, train_loader, optimizer, scaler=scaler)\n", + " val_loss, val_acc = run_epoch(model, val_loader, optimizer=None, scaler=None)\n", + "\n", + " lr_before = optimizer.param_groups[0][\"lr\"]\n", + " scheduler.step(val_loss)\n", + " lr_after = optimizer.param_groups[0][\"lr\"]\n", + "\n", + " if val_loss < best_val_loss:\n", + " best_val_loss = val_loss\n", + " best_state = {k: v.cpu() for k, v in model.state_dict().items()}\n", + "\n", + " if lr_after < lr_before and best_state is not None:\n", + " # resume from best snapshot whenever LR is reduced\n", + " model.load_state_dict(best_state)\n", + "\n", + " cur_lr = optimizer.param_groups[0][\"lr\"]\n", + " print(\n", + " f\"Epoch {epoch + 1:02d} | lr {cur_lr:.2e} | train loss {train_loss:.4f} acc {train_acc:.3f} | \"\n", + " f\"val loss {val_loss:.4f} acc {val_acc:.3f}\"\n", + " )\n", + "\n", + "if best_state is not None:\n", + " model.load_state_dict(best_state)\n", + " print(f\"Loaded best state with val loss={best_val_loss:.4f}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "fb768bda", + "metadata": {}, + "source": [ + "## Evaluation\n", + "\n", + "Compute accuracy and a quick classification report on the held-out splits.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf65d689", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import classification_report, confusion_matrix\n", + "\n", + "\n", + "def collect_predictions(model, loader):\n", + " model.eval()\n", + " all_preds, all_labels = [], []\n", + " with torch.no_grad():\n", + " for tokens, offsets, labels in loader:\n", + " tokens = tokens.to(device)\n", + " offsets = offsets.to(device)\n", + " logits = model(tokens, offsets)\n", + " preds = logits.argmax(dim=1).cpu().tolist()\n", + " all_preds.extend(preds)\n", + " all_labels.extend(labels.tolist())\n", + " return all_labels, all_preds\n", + "\n", + "\n", + "for split_name, loader in [(\"val\", val_loader), (\"test\", test_loader)]:\n", + " y_true, y_pred = collect_predictions(model, loader)\n", + " print(f\"=== {split_name.upper()} ===\")\n", + " print(classification_report(y_true, y_pred, digits=3))\n", + " print(\"Confusion matrix:\")\n", + " print(confusion_matrix(y_true, y_pred))\n" + ] + }, + { + "cell_type": "markdown", + "id": "97811900", + "metadata": {}, + "source": [ + "## Save checkpoint\n", + "\n", + "Saves a lightweight `state_dict` for later reuse.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57af7004", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "ckpt_path = Path(cfg.checkpoint_path)\n", + "ckpt_path.parent.mkdir(parents=True, exist_ok=True)\n", + "torch.save({\"config\": cfg.__dict__, \"state_dict\": model.state_dict()}, ckpt_path)\n", + "print(f\"Saved to {ckpt_path}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "3c93782b", + "metadata": {}, + "source": [ + "## Save as safetensors\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7dc23b22", + "metadata": {}, + "outputs": [], + "source": [ + "from safetensors.torch import save_file\n", + "import json\n", + "\n", + "# Save model weights as safetensors\n", + "safetensor_path = ckpt_path.with_suffix(\".safetensors\")\n", + "save_file(model.state_dict(), safetensor_path)\n", + "\n", + "# Save config separately as JSON\n", + "config_path = ckpt_path.with_suffix(\".json\")\n", + "with open(config_path, \"w\") as f:\n", + " json.dump(cfg.__dict__, f, indent=2)\n", + "\n", + "print(f\"Saved model to {safetensor_path}\")\n", + "print(f\"Saved config to {config_path}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdbfa386", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/decision/README.md b/notebooks/experiments/decision/README.md deleted file mode 100644 index 83b4adeb..00000000 --- a/notebooks/experiments/decision/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Decision model experiments - -# Overview -The decision model is a two-class text classifier (multi-class approach) model that predicts if a paragraph is a decision or not. - -The model card can be found [here](../../../docs/pipeline/decision-model-card.md) - -The training data takes directly the paragraphs and split is 80% train and 10% validation and 10% test. - -# notebooks -- [00-data-generation.ipynb](00-data-generation.ipynb): generate the data for the model from the annotated data -- [01-training-2class-conv1d.ipynb](01-training-2class-conv1d.ipynb): train the model using a custom word embedding and a 1D convolutional neural network as feature extractor. then a simple dense layer is used to predict the class. -- [02-evaluation-2class-conv1d.ipynb](02-evaluation-2class-conv1d.ipynb): evaluate the model -- [03-pipeline-build.ipynb](03-pipeline-build.ipynb): build the pipeline \ No newline at end of file diff --git a/notebooks/experiments/entity-disambiguation/03-disambiguation-evaluation.ipynb b/notebooks/experiments/entity-disambiguation/03-disambiguation-evaluation.ipynb new file mode 100644 index 00000000..0797844a --- /dev/null +++ b/notebooks/experiments/entity-disambiguation/03-disambiguation-evaluation.ipynb @@ -0,0 +1,110 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "164a8904", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "\n", + "from aymurai.evaluation.metrics import evaluate_prediction_directories\n", + "\n", + "project_root = Path.cwd().parents[2]\n", + "if str(project_root) not in sys.path:\n", + " sys.path.insert(0, str(project_root))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72f996d1", + "metadata": {}, + "outputs": [], + "source": [ + "# Ajusta estas rutas antes de ejecutar\n", + "TEST_DIR = Path(\"/resources/data/restricted/disambiguation-eval/test\")\n", + "PREDS_DIR = Path(\n", + " \"/resources/data/restricted/disambiguation-eval/preds/preds-phi4:14b-sys-001-user-001-251228_0159\"\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7b456c9", + "metadata": {}, + "outputs": [], + "source": [ + "# Normalizando los textos\n", + "results, average = evaluate_prediction_directories(TEST_DIR, PREDS_DIR)\n", + "\n", + "if not results:\n", + " print(\"No se encontraron pares test/pred que evaluar.\")\n", + "else:\n", + " for doc_id, score, metrics in results:\n", + " print(f\"Documento: {doc_id}\")\n", + " print(f\" Score global: {score:.4f}\")\n", + " for metric_name, value in metrics.items():\n", + " print(f\" {metric_name}: {value:.4f}\")\n", + " print()\n", + "\n", + " print(f\"Promedio Score_global: {average:.4f}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80f08e1d", + "metadata": {}, + "outputs": [], + "source": [ + "# Sin Normalizar los textos\n", + "results, average = evaluate_prediction_directories(TEST_DIR, PREDS_DIR, normalize=False)\n", + "\n", + "if not results:\n", + " print(\"No se encontraron pares test/pred que evaluar.\")\n", + "else:\n", + " for doc_id, score, metrics in results:\n", + " print(f\"Documento: {doc_id}\")\n", + " print(f\" Score global: {score:.4f}\")\n", + " for metric_name, value in metrics.items():\n", + " print(f\" {metric_name}: {value:.4f}\")\n", + " print()\n", + "\n", + " print(f\"Promedio Score_global: {average:.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fd5e8b7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai (3.10.19)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/entity-disambiguation/06-pre-disambiguation-optimization.ipynb b/notebooks/experiments/entity-disambiguation/06-pre-disambiguation-optimization.ipynb new file mode 100644 index 00000000..fa875add --- /dev/null +++ b/notebooks/experiments/entity-disambiguation/06-pre-disambiguation-optimization.ipynb @@ -0,0 +1,1619 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c8f3d033", + "metadata": {}, + "source": [ + "# Entity Resolution & Hyperparameter Optimization\n", + "\n", + "This notebook outlines a systematic process for optimizing our **Entity Disambiguation** pipeline. The goal is to identify the most accurate configuration for grouping person mentions by comparing localized model outputs against their corresponding ground truth data.\n", + "\n", + "## Methodology\n", + "\n", + "### 1. Data Correspondence & Ground Truth\n", + "To ensure data integrity, we have moved away from a single global file approach. The pipeline now maintains a strict 1:1 correspondence between files to prevent entity overlaps across different documents. The evaluation process compares:\n", + "\n", + "* **`gold_json`**: The benchmark \"Gold Standard\" for a specific document.\n", + "* **`input_ner_preds_json`**: The raw NER model predictions for that specific input.\n", + "* **`cluster_json`**: The output generated by our clustering logic for those specific predictions.\n", + "\n", + "### 2. NER Extraction & Processing\n", + "We deploy our **Named Entity Recognition (NER)** model across the source documents. Due to performance constraints on macOS when processing large batches in a single loop, we process documents individually. \n", + "\n", + "This sequential processing allows us to:\n", + "1. Effectively utilize cache memory.\n", + "2. Identify and fix incomplete entities (e.g., missing `entity_id`) before the evaluation phase.\n", + "3. Ensure each `input_ner_preds_json` is correctly mapped to its `gold_json`.\n", + "\n", + "### 3. Clustering via `cluster_with_cdist`\n", + "The core of our pipeline is the `cluster_with_cdist` function, which transforms raw NER predictions into grouped clusters:\n", + "\n", + "* **Similarity Matrix**: Uses `process.cdist` from `rapidfuzz` to calculate an efficient similarity matrix based on a specific `scorer`, `score_cutoff threshold` and `processor`.\n", + "* **Union-Find Algorithm**: Implements **Disjoint Set Union (DSU)** logic to link entities that exceed the similarity threshold.\n", + "* **Canonical Selection**: Through `pick_canonical`, the system selects the most representative string for each group, prioritizing the longest original string.\n", + "\n", + "### 4. Systematic Grid Search & Evaluation\n", + "We execute a **Grid Search** by iterating through clustering hyperparameters (`Scorers`, `Thresholds`, and `Processors`). For each iteration, the resulting `cluster_json` is validated against its correspondent `gold_json`.\n", + "\n", + "#### Evaluation Metric: Weighted Disambiguation Score\n", + "The performance of each hyperparameter combination is measured by a composite score that balances four key dimensions:\n", + "\n", + "* **Entity-level F1**: Measures the overall quality of entity discovery.\n", + "* **Alias Macro F1**: Evaluates how well different name variants (aliases) are grouped.\n", + "* **Label Accuracy**: Ensures the primary name assigned to the cluster is correct.\n", + "* **Role Accuracy**: Validates the secondary attributes (roles) associated with the entity.\n", + "\n", + "#### Winner Selection\n", + "To ensure the robustness of our pipeline, the **winning combination** is determined by calculating the **best average score across all documents**. This cross-document averaging prevents overfitting to a single file and ensures that the selected `scorer`, `threshold` and `processor` perform consistently across the entire corpus.\n", + "\n", + "---\n", + "\n", + "> **Implementation Note:** By analyzing the weighted score across discrete file correspondences, we can determine the optimal configuration while avoiding the data pollution issues found in previous single-file versions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e14b29a", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext rich\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0386121", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import json\n", + "import mimetypes\n", + "import os\n", + "import re\n", + "import time\n", + "import unicodedata\n", + "from collections import Counter\n", + "from operator import itemgetter\n", + "from pathlib import Path\n", + "from typing import Iterable, Tuple\n", + "\n", + "import requests\n", + "from tqdm import tqdm\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import requests\n", + "from more_itertools import flatten, unique_everseen\n", + "\n", + "from aymurai.meta.entities import CanonicalEntities, CanonicalEntity\n", + "from aymurai.utils.json_data import get_pretty, save_json, load_json\n", + "import aymurai.evaluation.metrics as aymurai_metrics" + ] + }, + { + "cell_type": "markdown", + "id": "d4bd115d", + "metadata": {}, + "source": [ + "# #**1** First Step: Prepare the ***gold_json*** and ***ner_preds_json***" + ] + }, + { + "cell_type": "markdown", + "id": "34a4a0b7", + "metadata": {}, + "source": [ + "## /document-extract endpoint output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b76ddad4", + "metadata": {}, + "outputs": [], + "source": [ + "API_URL = os.getenv(\"DOCUMENT_API_BASE_URL\", \"http://127.0.0.1:8999\")\n", + "ENDPOINT = f\"{API_URL}/misc/document-extract\"\n", + "DATA_ROOT = Path(\n", + " os.getenv(\n", + " \"DOCUMENT_DATA_ROOT\", \"../../../resources/data/restricted/disambiguation-eval/files\"\n", + " )\n", + ")\n", + "GOLD_JSON_ROOT = Path(\n", + " os.getenv(\n", + " \"GOLD_JSON_ROOT\",\n", + " \"../../../resources/data/restricted/disambiguation-eval/canonical-entities/manual-predicted\",\n", + " )\n", + ")\n", + "DOC_EXTENSIONS = {\".pdf\", \".docx\"}\n", + "JSON_EXTENSION = {\".json\"}\n", + "REQUEST_TIMEOUT_S = float(os.getenv(\"DOCUMENT_REQUEST_TIMEOUT\", \"30\"))\n", + "\n", + "print(f\"Target endpoint: {ENDPOINT}\")\n", + "print(f\"Data root: {DATA_ROOT.resolve()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd44c127", + "metadata": {}, + "outputs": [], + "source": [ + "if not DATA_ROOT.exists():\n", + " raise FileNotFoundError(\n", + " f\"Directory '{DATA_ROOT}' not found. Update DATA_ROOT before continuing.\"\n", + " )\n", + "\n", + "\n", + "def discover_documents(root: Path, extensions: Iterable[str]) -> list[Path]:\n", + " extensions = {ext.lower() for ext in extensions}\n", + " return sorted(\n", + " path\n", + " for path in root.rglob(\"*\")\n", + " if path.is_file() and path.suffix.lower() in extensions\n", + " )\n", + "\n", + "\n", + "documents = discover_documents(DATA_ROOT, DOC_EXTENSIONS)\n", + "print(f\"Discovered {len(documents)} documents.\")\n", + "\n", + "gold_jsons = discover_documents(GOLD_JSON_ROOT, JSON_EXTENSION)\n", + "print(f\"Discovered {len(gold_jsons)} gold JSON files.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efd22ec4", + "metadata": {}, + "outputs": [], + "source": [ + "def call_extraction_api(\n", + " session: requests.Session, file_path: Path\n", + ") -> dict[str, object]:\n", + " payload: dict[str, object] = {\n", + " \"path\": str(file_path),\n", + " \"status\": \"failure\",\n", + " \"status_code\": None,\n", + " \"elapsed_s\": None,\n", + " \"detail\": None,\n", + " }\n", + "\n", + " if not file_path.exists():\n", + " payload[\"detail\"] = \"File does not exist\"\n", + " return payload\n", + "\n", + " mime_type = mimetypes.guess_type(file_path.name)[0] or \"application/octet-stream\"\n", + " files = {\n", + " \"file\": (file_path.name, file_path.open(\"rb\"), mime_type),\n", + " }\n", + "\n", + " try:\n", + " start = time.perf_counter()\n", + " response = session.post(\n", + " ENDPOINT,\n", + " files=files,\n", + " timeout=REQUEST_TIMEOUT_S,\n", + " )\n", + " elapsed = time.perf_counter() - start\n", + " except requests.RequestException as exc:\n", + " payload[\"detail\"] = f\"Request failed: {exc}\"\n", + " return payload\n", + " finally:\n", + " files[\"file\"][1].close()\n", + "\n", + " payload[\"status_code\"] = response.status_code\n", + " payload[\"elapsed_s\"] = elapsed\n", + "\n", + " try:\n", + " response_body = response.json()\n", + " except ValueError:\n", + " response_body = {\"raw\": response.text[:500]}\n", + "\n", + " if response.ok:\n", + " payload[\"status\"] = \"success\"\n", + " payload[\"detail\"] = {\n", + " \"document_id\": response_body.get(\"document_id\"),\n", + " \"document\": response_body.get(\"document\", []),\n", + " }\n", + " else:\n", + " payload[\"detail\"] = response_body\n", + "\n", + " return payload" + ] + }, + { + "cell_type": "markdown", + "id": "b0e88e1b", + "metadata": {}, + "source": [ + "## Inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11a316a1", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import chain\n", + "from operator import itemgetter\n", + "from typing import Any\n", + "\n", + "from more_itertools import unique_everseen\n", + "\n", + "\n", + "# Function to make inference using the API\n", + "def get_predictions(sample: str) -> dict:\n", + " response = requests.post(url=f\"{API_URL}/anonymizer/predict\", json={\"text\": sample})\n", + " response.raise_for_status()\n", + " return response.json()\n", + "\n", + "\n", + "def parse_prediction_labels(predictions: list[dict[str, Any]]) -> list[dict[str, str]]:\n", + " \"\"\"\n", + " Parse prediction labels to extract unique aymurai_label and aymurai_alt_text pairs.\n", + "\n", + " Args:\n", + " predictions (list[dict[str, Any]]): A list of prediction dictionaries.\n", + "\n", + " Returns:\n", + " list[dict[str, str]]: A list of dictionaries containing unique aymurai_label and aymurai_alt_text pairs.\n", + " \"\"\"\n", + " attrs_stream = (\n", + " label.get(\"attrs\") or {}\n", + " for label in chain.from_iterable(pred.get(\"labels\", ()) for pred in predictions)\n", + " )\n", + "\n", + " unique_pairs = unique_everseen(\n", + " (\n", + " attrs.get(\"aymurai_label\"),\n", + " attrs.get(\"aymurai_alt_text\"),\n", + " )\n", + " for attrs in attrs_stream\n", + " if attrs.get(\"aymurai_label\") and attrs.get(\"aymurai_alt_text\")\n", + " )\n", + "\n", + " return sorted(\n", + " ({\"aymurai_label\": label, \"text\": text} for label, text in unique_pairs),\n", + " key=itemgetter(\"aymurai_label\", \"text\"),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "4a3a97df", + "metadata": {}, + "source": [ + "## ***gold_json*** and ***ner_preds_json*** for input to clusterization developement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b3b2a74", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_and_save_entities(\n", + " document_paths: list[Path], output_path: Path, file_ending: str, target_label: str\n", + ") -> None:\n", + " \"\"\"\n", + " Processes a list of documents to extract specific entities and saves\n", + " each result as an individual JSON file.\n", + " It has been optimized to handle both document files and pre-existing JSON files.\n", + " \"\"\"\n", + "\n", + " with requests.Session() as session:\n", + " if \".json\" not in document_paths[0].suffix:\n", + " for doc_path in tqdm(\n", + " document_paths, desc=f\"Extracting {target_label} entities\"\n", + " ):\n", + " doc_path = Path(doc_path)\n", + "\n", + " # 1. API Extraction\n", + " response = call_extraction_api(session, doc_path)\n", + " document_data = response.get(\"detail\", {}).get(\"document\")\n", + "\n", + " if not document_data:\n", + " print(f\"Warning: No document content found for {doc_path.name}\")\n", + " continue\n", + "\n", + " # 2. Processing\n", + " raw_predictions = [\n", + " get_predictions(paragraph) for paragraph in document_data\n", + " ]\n", + " parsed_labels = parse_prediction_labels(raw_predictions)\n", + "\n", + " # 3. Filtering by dynamic label\n", + " filtered_entities = [\n", + " item\n", + " for item in parsed_labels\n", + " if item.get(\"aymurai_label\") == target_label\n", + " ]\n", + "\n", + " # 4. Saving individual file\n", + " clean_base_name = re.sub(\n", + " r\"\\s+|_\", \"-\", os.path.splitext(os.path.basename(doc_path))[0]\n", + " )\n", + " clean_base_name = re.sub(r\"-{2,}\", \"-\", clean_base_name).strip(\"-\")\n", + " clean_name = re.sub(r\"-{2,}\", \"-\", clean_base_name)\n", + " file_name = f\"{clean_name}{file_ending}\"\n", + " save_path = output_path / file_name\n", + "\n", + " save_json(file_path=save_path, json_data=filtered_entities)\n", + " else:\n", + " for doc_path in tqdm(\n", + " document_paths, desc=f\"Extracting {target_label} entities\"\n", + " ):\n", + " doc_path = Path(doc_path)\n", + "\n", + " # 1. Load existing JSON data\n", + " document_data = load_json(doc_path)\n", + "\n", + " if not document_data:\n", + " print(f\"Warning: No document content found for {doc_path.name}\")\n", + " continue\n", + "\n", + " # 2. Filtering by dynamic label\n", + " filtered_entities = [\n", + " item\n", + " for item in document_data\n", + " if item.get(\"aymurai_label\") == target_label\n", + " ]\n", + "\n", + " # 3. Saving individual file\n", + " clean_base_name = re.sub(\n", + " r\"\\s+|_\", \"-\", os.path.splitext(os.path.basename(doc_path))[0]\n", + " )\n", + " clean_base_name = re.sub(r\"-{2,}\", \"-\", clean_base_name).strip(\"-\")\n", + " clean_name = re.sub(r\"-{2,}\", \"-\", clean_base_name)\n", + " file_name = f\"{clean_name}{file_ending}\"\n", + " save_path = output_path / file_name\n", + "\n", + " save_json(file_path=save_path, json_data=filtered_entities)" + ] + }, + { + "cell_type": "markdown", + "id": "48e5096b", + "metadata": {}, + "source": [ + "## ***gold-jsons***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1e21607", + "metadata": {}, + "outputs": [], + "source": [ + "# We save the gold_json in a directory in /canonical-entities/pre-clusterization\n", + "\n", + "if not (\n", + " DATA_ROOT.parent / \"canonical-entities\" / \"pre-clusterization\" / \"gold-jsons\"\n", + ").exists():\n", + " os.makedirs(\n", + " DATA_ROOT.parent / \"canonical-entities\" / \"pre-clusterization\" / \"gold-jsons\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "9fe4d751", + "metadata": {}, + "source": [ + "#### We identified that some .json files were incomplete due to missing entity_id fields. Consequently, we implemented a function to resolve this issue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "466b7153", + "metadata": {}, + "outputs": [], + "source": [ + "def correct_entity_id(json_path: str) -> None:\n", + " \"\"\"Correct the entity_id field in the given JSON file to ensure it is a hex string.\"\"\"\n", + "\n", + " # Load existing canonical entities from JSON file\n", + " canonical_entities = load_json(json_path)\n", + "\n", + " # Convert reviewed entities back to CanonicalEntity objects\n", + " canonical_entities = [\n", + " CanonicalEntity.model_validate(entity) for entity in canonical_entities\n", + " ]\n", + "\n", + " # Update entity_id to be a hex string\n", + " canonical_entities = [\n", + " entity.model_dump() | {\"entity_id\": entity.entity_id.hex}\n", + " for entity in canonical_entities\n", + " ]\n", + "\n", + " save_json(file_path=json_path, json_data=canonical_entities)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49e0a3c1", + "metadata": {}, + "outputs": [], + "source": [ + "data = load_json(gold_jsons[0])\n", + "data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2896169", + "metadata": {}, + "outputs": [], + "source": [ + "correct_entity_id(gold_jsons[0])" + ] + }, + { + "cell_type": "markdown", + "id": "de9e145e", + "metadata": {}, + "source": [ + "#### Now we are ready to proceed with the ***gold_jsons***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53242f47", + "metadata": {}, + "outputs": [], + "source": [ + "extract_and_save_entities(\n", + " document_paths=gold_jsons,\n", + " output_path=DATA_ROOT.parent\n", + " / \"canonical-entities\"\n", + " / \"pre-clusterization\"\n", + " / \"gold-jsons\",\n", + " file_ending=\"-gold.json\",\n", + " target_label=\"PER\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "93cf26e0", + "metadata": {}, + "source": [ + "------------------------ START OF DISCARD SECTION ------------------------" + ] + }, + { + "cell_type": "markdown", + "id": "dd7c2dcf", + "metadata": {}, + "source": [ + "### This was the old version to make the ***gold_json***, all of the entities in a single file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a02881f", + "metadata": {}, + "outputs": [], + "source": [ + "# DISCARD\n", + "def gold_json_labels(label: str, json_paths: list[Path]) -> list[dict[str, str]]:\n", + " g_json = [\n", + " item\n", + " for json_path in json_paths\n", + " for item in load_json(json_path)\n", + " if item.get(\"aymurai_label\") == label\n", + " ]\n", + "\n", + " return g_json" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7ffdc3a", + "metadata": {}, + "outputs": [], + "source": [ + "# DISCARD\n", + "gold_json = gold_json_labels(label=\"PER\", json_paths=gold_jsons)\n", + "\n", + "save_json(\n", + " file_path=DATA_ROOT.parent\n", + " / \"canonical-entities\"\n", + " / \"pre-clusterization\"\n", + " / \"gold.json\",\n", + " json_data=gold_json,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "974128cb", + "metadata": {}, + "source": [ + "------------------------ END OF DISCARD SECTION ------------------------" + ] + }, + { + "cell_type": "markdown", + "id": "5e7933e4", + "metadata": {}, + "source": [ + "## ***ner-preds-jsons***" + ] + }, + { + "cell_type": "markdown", + "id": "58f10ce9", + "metadata": {}, + "source": [ + "#### Due to performance issues on macOS when processing all documents in a single loop, we processed them individually first to ensure they were correctly stored in the cache." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b986a569", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract document\n", + "session = requests.Session()\n", + "document = call_extraction_api(session, Path(documents[17]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21ab3461", + "metadata": {}, + "outputs": [], + "source": [ + "document = document.get(\"detail\", {}).get(\"document\")\n", + "\n", + "if not document:\n", + " raise ValueError(\"Document text is empty or not found.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4b2b2d5", + "metadata": {}, + "outputs": [], + "source": [ + "document" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef45ca9a", + "metadata": {}, + "outputs": [], + "source": [ + "# We save the gold_json in a directory in /canonical-entities/pre-clusterization\n", + "\n", + "if not (\n", + " DATA_ROOT.parent / \"canonical-entities\" / \"pre-clusterization\" / \"ner-preds-jsons\"\n", + ").exists():\n", + " os.makedirs(\n", + " DATA_ROOT.parent\n", + " / \"canonical-entities\"\n", + " / \"pre-clusterization\"\n", + " / \"ner-preds-jsons\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f4c70898", + "metadata": {}, + "source": [ + "#### Now we are ready to proceed with the ***ner_preds_json***" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3f58d78", + "metadata": {}, + "outputs": [], + "source": [ + "extract_and_save_entities(\n", + " document_paths=documents,\n", + " output_path=DATA_ROOT.parent\n", + " / \"canonical-entities\"\n", + " / \"pre-clusterization\"\n", + " / \"ner-preds-jsons\",\n", + " file_ending=\"-ner-preds.json\",\n", + " target_label=\"PER\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "e55481c4", + "metadata": {}, + "source": [ + "------------------------ START OF DISCARD SECTION ------------------------" + ] + }, + { + "cell_type": "markdown", + "id": "f0e34a8d", + "metadata": {}, + "source": [ + "#### The following cells belong to a previous version where all NER predictions were stored in a single input file. However, we discovered that certain entities appear in multiple files. Therefore, processing them in a single file would lead to incorrect comparisons against the ***gold_json***." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80045084", + "metadata": {}, + "outputs": [], + "source": [ + "# DISCARD\n", + "def get_entities_by_label(document_paths: list[Path], target_label: str) -> list[dict]:\n", + " \"\"\"\n", + " Iterates over a list of documents, extracts entities via API,\n", + " and returns a list of items matching the specified label.\n", + " \"\"\"\n", + " all_ner_preds = []\n", + "\n", + " # Using a session to reuse the connection for better performance\n", + " with requests.Session() as session:\n", + " for doc_path in tqdm(document_paths, desc=\"Processing documents\"):\n", + " # API Extraction\n", + " response = call_extraction_api(session, Path(doc_path))\n", + " document_data = response.get(\"detail\", {}).get(\"document\")\n", + "\n", + " if not document_data:\n", + " print(f\"Warning: No document content found for {doc_path}\")\n", + " continue\n", + "\n", + " # Get predictions for each paragraph\n", + " raw_predictions = [\n", + " get_predictions(paragraph) for paragraph in document_data\n", + " ]\n", + "\n", + " # Parse and flatten labels\n", + " parsed_labels = parse_prediction_labels(raw_predictions)\n", + "\n", + " # Filter by the selected label and extend the main list\n", + " filtered_entities = [\n", + " item\n", + " for item in parsed_labels\n", + " if item.get(\"aymurai_label\") == target_label\n", + " ]\n", + "\n", + " all_ner_preds.extend(filtered_entities)\n", + "\n", + " return all_ner_preds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fb69feb", + "metadata": {}, + "outputs": [], + "source": [ + "# DISCARD\n", + "ner_preds_json = get_entities_by_label(document_paths=documents, target_label=\"PER\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8370add6", + "metadata": {}, + "outputs": [], + "source": [ + "# DISCARD\n", + "save_json(\n", + " file_path=DATA_ROOT.parent\n", + " / \"canonical-entities\"\n", + " / \"pre-clusterization\"\n", + " / \"ner_preds.json\",\n", + " json_data=ner_preds_json,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "05e4d66e", + "metadata": {}, + "source": [ + "------------------------ END OF DISCARD SECTION ------------------------" + ] + }, + { + "cell_type": "markdown", + "id": "d86a63e2", + "metadata": {}, + "source": [ + "## ***Evaluation Pipeline: Ground Truth and NER Prediction Pairing***\n", + "\n", + "We define the following function to ensure a correct implementation of the metrics evaluation for the performance of each combination of hyperparameters. The function returns each pair of .json paths for the clusterization and following evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02d574a8", + "metadata": {}, + "outputs": [], + "source": [ + "def pair_gold_and_preds(\n", + " gold_paths: list[Path], pred_paths: list[Path]\n", + ") -> list[Tuple[Path, Path]]:\n", + " \"\"\"\n", + " Pairs gold standard JSONs with their corresponding NER predictions\n", + " based on the common document prefix.\n", + " \"\"\"\n", + "\n", + " # 1. Helper to extract the core ID of the file. It takes everything before \"-ner-preds\" or \"-canonical-entities-gold\"\n", + " def get_document_id(path: Path) -> str:\n", + " name = path.stem\n", + " # Remove known suffixes to get the base ID\n", + " name = name.replace(\"-ner-preds\", \"\")\n", + " name = name.replace(\"-canonical-entities-gold\", \"\")\n", + " return name\n", + "\n", + " # 2. Create a mapping of {doc_id: pred_path}\n", + " preds_map = {get_document_id(p): p for p in pred_paths}\n", + "\n", + " paired_paths = []\n", + " missing_preds = []\n", + "\n", + " # 3. Iterate through gold files and find their match\n", + " for gold_path in gold_paths:\n", + " doc_id = get_document_id(gold_path)\n", + "\n", + " if doc_id in preds_map:\n", + " paired_paths.append((gold_path, preds_map[doc_id]))\n", + " else:\n", + " missing_preds.append(gold_path.name)\n", + "\n", + " # 4. Validation / Logging\n", + " if missing_preds:\n", + " print(f\"Warning: No predictions found for {len(missing_preds)} gold files.\")\n", + " for missing in missing_preds:\n", + " print(f\" - Missing match for: {missing}\")\n", + "\n", + " print(f\"Successfully paired {len(paired_paths)} documents.\")\n", + "\n", + " return paired_paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11af85fa", + "metadata": {}, + "outputs": [], + "source": [ + "PRE_CLUSTERIZATION_ROOT = GOLD_JSON_ROOT.parent / \"pre-clusterization\"\n", + "\n", + "gold_docs = discover_documents(\n", + " root=PRE_CLUSTERIZATION_ROOT / \"gold-jsons\", extensions=JSON_EXTENSION\n", + ")\n", + "pred_docs = discover_documents(\n", + " root=PRE_CLUSTERIZATION_ROOT / \"ner-preds-jsons\", extensions=JSON_EXTENSION\n", + ")\n", + "\n", + "document_pairs = pair_gold_and_preds(gold_docs, pred_docs)" + ] + }, + { + "cell_type": "markdown", + "id": "843b13b7", + "metadata": {}, + "source": [ + "### **Hyperparameter Grid Search & Evaluation Pipeline**\n", + "\n", + "We define a specialized function to orchestrate a **Grid Search** across multiple hyperparameters. The goal is to systematically evaluate the performance of our clustering model by testing every possible combination of **scorers**, **thresholds**, and **text processors**.\n", + "\n", + "#### **Pipeline Architecture**\n", + "\n", + "1. **Directory-Based Organization**:\n", + "For each unique combination of hyperparameters, the function creates a dedicated directory. The folder is named using the parameters (e.g., `scorer-cosine_threshold-0.8_proc-normalized`) to ensure clear traceability.\n", + "2. **Pre-Clustering Execution**:\n", + "Within each iteration, the pipeline processes the previously paired `gold-json` and `ner-preds-json` files. It generates a new `pre-cluster.json` file, preserving the original document's identity in the filename.\n", + "3. **Performance Evaluation**:\n", + "Once the cluster is generated, the function evaluates the results against the corresponding **Gold Standard**. This ensures that the metric reflects the actual accuracy of the current hyperparameter configuration.\n", + "4. **Iterative Metrics Logging**:\n", + "The evaluation results are stored in a centralized JSON file within the specific combination's folder.\n", + "* **Keys:** Represent the evaluated document name.\n", + "* **Values:** Represent the calculated performance metric.\n", + "This file is updated incrementally as the loop progresses through each document pair.\n", + "\n", + "\n", + "5. **Grid Progression**:\n", + "After all document pairs have been processed for a specific set of parameters, the function moves to the next grid cell, repeating the directory creation and evaluation steps until all combinations are exhausted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f352cddd", + "metadata": {}, + "outputs": [], + "source": [ + "from rapidfuzz import fuzz, process, utils\n", + "from rapidfuzz.process import extractOne\n", + "from rapidfuzz.fuzz import (\n", + " ratio,\n", + " partial_ratio,\n", + " token_sort_ratio,\n", + " token_set_ratio,\n", + " partial_token_set_ratio,\n", + " partial_token_sort_ratio,\n", + " WRatio,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "559ffcac", + "metadata": {}, + "source": [ + "We take the functions defined by Juli in the `04-entity-disambiguation-from-pre-clustered-validations.ipynb` notebook and made one change:\n", + "- In `cluster_with_cdist` we added the text processor variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b7b481e", + "metadata": {}, + "outputs": [], + "source": [ + "def cluster_with_cdist(\n", + " items: list[dict],\n", + " threshold: int = 90,\n", + " scorer: callable = token_set_ratio,\n", + " processor: callable = None,\n", + "):\n", + " \"\"\"\n", + " Cluster entities and prepare them for CanonicalEntity conversion.\n", + " \"\"\"\n", + " if not items:\n", + " return []\n", + "\n", + " # 1. Extract texts and apply normalization\n", + " # We keep track of the original text, the processed text, and the label\n", + " entities = [item.get(\"text\", \"\") for item in items]\n", + " labels = [item.get(\"aymurai_label\", \"UNKNOWN\") for item in items]\n", + "\n", + " if processor:\n", + " normed = [processor(e) for e in entities]\n", + " else:\n", + " normed = [str(e) for e in entities]\n", + "\n", + " # 2. Similarity Matrix\n", + " sim = process.cdist(normed, normed, scorer=scorer, score_cutoff=threshold)\n", + " sim = np.array(sim)\n", + "\n", + " # 3. Union-Find Logic\n", + " parent = list(range(len(normed)))\n", + "\n", + " def find(i):\n", + " if parent[i] == i:\n", + " return i\n", + " parent[i] = find(parent[i])\n", + " return parent[i]\n", + "\n", + " def union(i, j):\n", + " root_i, root_j = find(i), find(j)\n", + " if root_i != root_j:\n", + " parent[root_j] = root_i\n", + "\n", + " n = len(normed)\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " if sim[i, j] >= threshold:\n", + " union(i, j)\n", + "\n", + " # 4. Group into the format parse_item expects: (orig, norm, label)\n", + " clusters_map = {}\n", + " for idx in range(n):\n", + " root = find(idx)\n", + " if root not in clusters_map:\n", + " clusters_map[root] = []\n", + " # This tuple matches your 'parse_item' len == 3 condition\n", + " clusters_map[root].append((entities[idx], normed[idx], labels[idx]))\n", + "\n", + " return list(clusters_map.values())\n", + "\n", + "\n", + "def parse_item(item: tuple[str, ...]) -> tuple[str, str, str]:\n", + " \"\"\"\n", + " Parse an item into (label, orig, norm).\n", + "\n", + " Accepts:\n", + " - (orig, norm, label)\n", + " - (labelled_orig, labelled_norm) with prefix 'LABEL:'\n", + " Args:\n", + " item (tuple[str, ...]): input item\n", + "\n", + " Returns:\n", + " tuple[str, str, str]: (label, orig, norm)\n", + " \"\"\"\n", + " if len(item) == 3:\n", + " orig, norm, label = item\n", + " return label, orig, norm\n", + "\n", + " # len == 2: assume \"LABEL:text\"\n", + " labelled_orig, labelled_norm = item\n", + " label, orig = labelled_orig.split(\":\", 1)\n", + " _, norm = labelled_norm.split(\":\", 1)\n", + " return label, orig, norm\n", + "\n", + "\n", + "def pick_cluster_label(parsed_items: list[tuple[str, str, str]]) -> str:\n", + " \"\"\"\n", + " Pick the most common label from parsed items.\n", + "\n", + " Args:\n", + " parsed_items (list[tuple[str, str, str]]): parsed items\n", + "\n", + " Returns:\n", + " str: chosen label\n", + " \"\"\"\n", + " labels = [lbl for lbl, _, _ in parsed_items]\n", + " # majority vote; fallback to first\n", + " return Counter(labels).most_common(1)[0][0]\n", + "\n", + "\n", + "def pick_canonical_text(parsed_items: list[tuple[str, str, str]]) -> str:\n", + " \"\"\"\n", + " Choose the longest original surface form; tweak as needed.\n", + "\n", + " Args:\n", + " parsed_items (list[tuple[str, str, str]]): parsed items\n", + "\n", + " Returns:\n", + " str: chosen canonical text\n", + " \"\"\"\n", + " return max(parsed_items, key=lambda x: len(x[1]))[1]\n", + "\n", + "\n", + "def clusters_to_canonical_entities(\n", + " clusters: list[list[tuple[str, str]]],\n", + ") -> list[CanonicalEntity]:\n", + " \"\"\"\n", + " Convert clusters to CanonicalEntity objects.\n", + "\n", + " Args:\n", + " clusters (list[list[tuple[str, str]]]): clusters of (original, normalized) entity tuples\n", + "\n", + " Returns:\n", + " list[CanonicalEntity]: list of CanonicalEntity objects\n", + " \"\"\"\n", + " canonical_entities = []\n", + "\n", + " for cluster in clusters:\n", + " parsed = [parse_item(item) for item in cluster] # [(label, orig, norm), ...]\n", + " label = pick_cluster_label(parsed)\n", + " canonical_text = pick_canonical_text(parsed)\n", + " aliases = sorted({orig for _, orig, _ in parsed})\n", + " ce = CanonicalEntity(\n", + " aymurai_label=label,\n", + " canonical_text=canonical_text,\n", + " aliases=aliases,\n", + " attributes={},\n", + " relations=[],\n", + " )\n", + " canonical_entities.append(ce)\n", + "\n", + " return canonical_entities" + ] + }, + { + "cell_type": "markdown", + "id": "afc1f54c", + "metadata": {}, + "source": [ + "We defined 2 new functions:\n", + "- `get_name` help us to get the cleaned name of the hyperparameters.\n", + "- `run_evaluation_grid_search` to run the grid search of the best hyperparameters combination.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3df974d", + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "\n", + "def get_name(obj):\n", + " \"\"\"Helper to extract a clean string name from a function or object.\"\"\"\n", + " if obj is None:\n", + " return \"None\"\n", + " if hasattr(obj, \"__name__\"):\n", + " return obj.__name__\n", + " # Fallback for complex objects or partials: remove memory addresses\n", + " clean_name = re.sub(r\" at 0x[0-9a-fA-F]+\", \"\", str(obj))\n", + " return clean_name.strip(\"<>\").replace(\"cyfunction \", \"\").replace(\"function \", \"\")\n", + "\n", + "\n", + "def run_evaluation_grid_search(\n", + " scorers: list,\n", + " thresholds: list,\n", + " processors: list,\n", + " document_pairs: list,\n", + " target_label: str,\n", + " base_output_path: Path,\n", + "):\n", + " \"\"\"\n", + " Runs a grid search over hyperparameters using nested loops.\n", + " \"\"\"\n", + "\n", + " # To keep track of all results and find the best\n", + " all_combinations_results = []\n", + "\n", + " for scorer in scorers:\n", + " for threshold in thresholds:\n", + " for processor in processors:\n", + " # 1. Create a descriptive folder name with the cleaned names\n", + " scorer_name = get_name(scorer)\n", + " processor_name = get_name(processor)\n", + " combo_name = (\n", + " f\"scorer-{scorer_name}-threshold-{threshold}-proc-{processor_name}\"\n", + " )\n", + "\n", + " combo_dir = base_output_path / combo_name\n", + " combo_dir.mkdir(parents=True, exist_ok=True)\n", + "\n", + " metrics_results = {}\n", + " metrics_file_path = (\n", + " combo_dir / f\"evaluation_metrics_{target_label}.json\"\n", + " )\n", + "\n", + " # List to collect scores for this specific combination\n", + " current_combo_scores = []\n", + "\n", + " # 2. Iterate over document pairs (Gold vs Prediction)\n", + " for gold_path, pred_path in tqdm(\n", + " document_pairs, desc=f\"Testing {combo_name}\"\n", + " ):\n", + " # Extract entities from the JSON, we filter by target label to ensure consistency if the file has multiple labels\n", + " entities = load_json(pred_path)\n", + " filtered_preds = [\n", + " p for p in entities if p[\"aymurai_label\"] == target_label\n", + " ]\n", + "\n", + " # --- Pre-clustering Logic ---\n", + " # Run the clustering using your hyperparameters\n", + " cluster_data = cluster_with_cdist(\n", + " items=filtered_preds,\n", + " threshold=threshold,\n", + " scorer=scorer,\n", + " processor=processor,\n", + " )\n", + "\n", + " # Convert to Canonical Entities\n", + " canonical_entities = clusters_to_canonical_entities(cluster_data)\n", + "\n", + " canonical_entities = [\n", + " CanonicalEntity.model_validate(entity)\n", + " for entity in canonical_entities\n", + " ]\n", + "\n", + " canonical_entities = [\n", + " entity.model_dump() | {\"entity_id\": entity.entity_id.hex}\n", + " for entity in canonical_entities\n", + " ]\n", + "\n", + " # Save the result as the pre-cluster.json\n", + " cluster_filename = f\"{pred_path.stem}-pre-cluster.json\"\n", + " cluster_output_path = combo_dir / cluster_filename\n", + "\n", + " save_json(\n", + " file_path=cluster_output_path, json_data=canonical_entities\n", + " )\n", + "\n", + " # --- Metric Evaluation ---\n", + " score, metrics = aymurai_metrics.evaluate_disambiguation(\n", + " gold_json=load_json(gold_path),\n", + " pred_json=load_json(cluster_output_path),\n", + " target_label=target_label,\n", + " )\n", + "\n", + " current_combo_scores.append(score)\n", + " doc_id = pred_path.stem\n", + " metrics_results[doc_id] = {\n", + " \"label\": target_label,\n", + " \"metric_value\": score,\n", + " \"detailed_metrics\": metrics,\n", + " }\n", + "\n", + " # --- Calculate Average for this combination ---\n", + " avg_score = (\n", + " np.mean(current_combo_scores) if current_combo_scores else 0.0\n", + " )\n", + "\n", + " # Add average to the results file for this folder\n", + " final_output = {\n", + " \"average_combination_score\": avg_score,\n", + " \"document_details\": metrics_results,\n", + " }\n", + "\n", + " with open(metrics_file_path, \"w\") as f:\n", + " json.dump(final_output, f, indent=4)\n", + "\n", + " # Store combination info for final ranking\n", + " all_combinations_results.append(\n", + " {\n", + " \"name\": combo_name,\n", + " \"score\": avg_score,\n", + " \"params\": {\n", + " \"scorer\": scorer_name,\n", + " \"threshold\": threshold,\n", + " \"processor\": processor_name,\n", + " },\n", + " }\n", + " )\n", + "\n", + " # Save all combinations results in a summary file\n", + " summary_file_path = (\n", + " base_output_path / f\"results_summary_{target_label}.json\"\n", + " )\n", + " with open(summary_file_path, \"w\") as f:\n", + " json.dump(all_combinations_results, f, indent=4)\n", + "\n", + " # --- Find the best combination ---\n", + " best_combo = max(all_combinations_results, key=lambda x: x[\"score\"])\n", + "\n", + " print(f\"\\nGrid Search for {target_label} completed.\")\n", + "\n", + " # We now copy the best combination folder to a new location with a standardized name\n", + " src = Path(base_output_path) / best_combo[\"name\"]\n", + " # Combine the parent destination path with the new folder name\n", + " new_name = f\"best-pre-clusterization-{target_label.lower()}\"\n", + " dest = Path(base_output_path.parent) / new_name\n", + " \n", + " try:\n", + " # copytree creates the destination directory with the 'new_name'\n", + " shutil.copytree(src, dest)\n", + " print(f\"Successfully copied '{src.name}' to '{dest}'\")\n", + " except FileExistsError:\n", + " print(f\"Error: A folder named '{new_name}' already exists in '{base_output_path.parent}'\")\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\") \n", + "\n", + " return best_combo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6369c582", + "metadata": {}, + "outputs": [], + "source": [ + "# We have to install jellyfish for the phonetic processor\n", + "!pip install jellyfish" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f48e9d4c", + "metadata": {}, + "outputs": [], + "source": [ + "def hard_normalizer(s: str) -> str:\n", + " \"\"\"\n", + " Normalize string for clustering: strips accents, removes punctuation (.,-),\n", + " lowercases, and collapses spaces.\n", + "\n", + " Args:\n", + " s (str): input string\n", + " Returns:\n", + " str: normalized string\n", + " \"\"\"\n", + " if not s:\n", + " return \"\"\n", + "\n", + " # Strip accents\n", + " s = \"\".join(\n", + " c for c in unicodedata.normalize(\"NFD\", s) if unicodedata.category(c) != \"Mn\"\n", + " )\n", + "\n", + " # Remove commas, periods, and hyphens\n", + " s = re.sub(r\"[.,\\-]\", \" \", s)\n", + "\n", + " # Lowercase and collapse whitespace\n", + " s = \" \".join(s.lower().split())\n", + "\n", + " return s\n", + "\n", + "def light_normalizer(s: str) -> str:\n", + " return s.lower().strip() if s else \"\"\n", + "\n", + "\n", + "def legal_text_normalizer(s: str) -> str:\n", + " if not s:\n", + " return \"\"\n", + "\n", + " # Standard cleaning (accents/lowercase)\n", + " s = \"\".join(\n", + " c for c in unicodedata.normalize(\"NFD\", s) if unicodedata.category(c) != \"Mn\"\n", + " )\n", + " s = s.lower()\n", + "\n", + " # Remove Legal Titles & Prefixes\n", + " legal_titles = r\"\\b(dr|dra|sr|sra|expte|nro|no|pcia)\\b\\.?\"\n", + " s = re.sub(legal_titles, \"\", s)\n", + "\n", + " # Remove common Spanish stopwords\n", + " stopwords = r\"\\b(de|del|la|las|el|los|y|en)\\b\"\n", + " s = re.sub(stopwords, \"\", s)\n", + "\n", + " # Remove all non-alphanumeric except spaces\n", + " s = re.sub(r\"[^\\w\\s]\", \"\", s)\n", + "\n", + " return \" \".join(s.split())\n", + "\n", + "\n", + "import jellyfish\n", + "\n", + "\n", + "def phonetic_normalizer(s: str) -> str:\n", + " if not s:\n", + " return \"\"\n", + " # Standardize first\n", + " clean = \"\".join(\n", + " c for c in unicodedata.normalize(\"NFD\", s) if unicodedata.category(c) != \"Mn\"\n", + " ).lower()\n", + "\n", + " return jellyfish.nysiis(clean)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f641f4a", + "metadata": {}, + "outputs": [], + "source": [ + "scorers = [\n", + " ratio,\n", + " partial_ratio,\n", + " token_sort_ratio,\n", + " token_set_ratio,\n", + " partial_token_set_ratio,\n", + " partial_token_sort_ratio,\n", + " WRatio,\n", + "]\n", + "\n", + "thresholds = list(range(50, 100, 5))\n", + "\n", + "processors = [\n", + " None,\n", + " hard_normalizer,\n", + " light_normalizer,\n", + " legal_text_normalizer,\n", + " phonetic_normalizer,\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6562159d", + "metadata": {}, + "outputs": [], + "source": [ + "top_result = run_evaluation_grid_search(\n", + " scorers=scorers,\n", + " thresholds=thresholds,\n", + " processors=processors,\n", + " document_pairs=document_pairs,\n", + " target_label=\"PER\",\n", + " base_output_path=PRE_CLUSTERIZATION_ROOT / \"grid-search-results\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a8eecab", + "metadata": {}, + "outputs": [], + "source": [ + "print(\n", + " f\"The best hyperparameter combination is '{top_result['name']}' \"\n", + " f\"with an average score of {top_result['score']:.4f}.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6aae6a7b", + "metadata": {}, + "outputs": [], + "source": [ + "gold_path = PRE_CLUSTERIZATION_ROOT / \"gold-jsons\"\n", + "\n", + "pred_path = PRE_CLUSTERIZATION_ROOT / \"grid-search-results\" / top_result[\"name\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cfbd11f", + "metadata": {}, + "outputs": [], + "source": [ + "result_validation_results, result_validation_avg_score = (\n", + " aymurai_metrics.evaluate_prediction_directories(\n", + " gold_dir=gold_path,\n", + " preds_dir=pred_path,\n", + " target_label=\"PER\",\n", + " pred_json_suffix=\"-ner-preds-pre-cluster\",\n", + " gold_json_suffix=\"-canonical-entities-gold\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bc466c9", + "metadata": {}, + "outputs": [], + "source": [ + "result_validation_avg_score" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f484a022", + "metadata": {}, + "outputs": [], + "source": [ + "result_validation_results" + ] + }, + { + "cell_type": "markdown", + "id": "86a21ede", + "metadata": {}, + "source": [ + "Check one of the pre-clusterization in one of the documents because there is only one cluster" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "163ecabc", + "metadata": {}, + "outputs": [], + "source": [ + "json_to_check = load_json(json_file_path=document_pairs[1][1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a00fc29", + "metadata": {}, + "outputs": [], + "source": [ + "pre_cluster_json = cluster_with_cdist(\n", + " items=json_to_check,\n", + " threshold=50,\n", + " scorer=partial_token_set_ratio,\n", + " processor=hard_normalizer,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "670abaf8", + "metadata": {}, + "outputs": [], + "source": [ + "# Convert to Canonical Entities\n", + "canonical_entities = clusters_to_canonical_entities(pre_cluster_json)\n", + "\n", + "canonical_entities = [\n", + " CanonicalEntity.model_validate(entity) for entity in canonical_entities\n", + "]\n", + "\n", + "canonical_entities = [\n", + " entity.model_dump() | {\"entity_id\": entity.entity_id.hex}\n", + " for entity in canonical_entities\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c15e0a2", + "metadata": {}, + "outputs": [], + "source": [ + "canonical_entities" + ] + }, + { + "cell_type": "markdown", + "id": "844f939a", + "metadata": {}, + "source": [ + "We are now analyzing the similarity matrix to identify items connected to multiple neighbors. By leveraging the Union-Find algorithm, we can determine if these local connections form a chain or tree structure that merges all entities into a single global cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e9a0b46", + "metadata": {}, + "outputs": [], + "source": [ + "processor = light_normalizer\n", + "\n", + "item = load_json(document_pairs[1][1])\n", + "\n", + "entities = [item.get(\"text\", \"\") for item in item]\n", + "labels = [item.get(\"aymurai_label\", \"UNKNOWN\") for item in item]\n", + "\n", + "if processor:\n", + " normed = [processor(e) for e in entities]\n", + "else:\n", + " normed = [str(e) for e in entities]\n", + "\n", + "sim = process.cdist(normed, normed, scorer=partial_token_set_ratio, score_cutoff=50)\n", + "sim = np.array(sim)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95270c37", + "metadata": {}, + "outputs": [], + "source": [ + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import ListedColormap\n", + "import numpy as np\n", + "\n", + "# Create a binary version for visualization: 0 if sim <= 0 else 1\n", + "viz_matrix = (sim > 0).astype(int)\n", + "\n", + "# Define the colormap: 0 -> white, 1 -> red\n", + "my_cmap = ListedColormap([\"white\", \"red\"])\n", + "\n", + "mask = np.triu(np.ones_like(sim, dtype=bool))\n", + "\n", + "# Plot the matrix using the binary color logic\n", + "plt.figure(figsize=(6, 6))\n", + "sns.heatmap(\n", + " viz_matrix,\n", + " cmap=my_cmap,\n", + " mask=mask,\n", + " cbar=False,\n", + " linewidths=0.5,\n", + " linecolor=\"lightgray\",\n", + " square=True, # Ensures cells are perfectly square\n", + ")\n", + "\n", + "plt.title(\"Connection Visualization (Red if > 0)\", fontsize=15)\n", + "plt.xlabel(\"Entity Index\")\n", + "plt.ylabel(\"Entity Index\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb8d30cc", + "metadata": {}, + "outputs": [], + "source": [ + "item[24]" + ] + }, + { + "cell_type": "markdown", + "id": "947379d0", + "metadata": {}, + "source": [ + "Based on the similarity matrix, there is a high probability that the algorithm will generate a single \"giant cluster\" due to a phenomenon known as **chaining**.\n", + "\n", + "Observation of the similarity matrix shows that **item [24]** (identified as an **\"L\"**) and **item [0]** are almost entirely highlighted in red. This indicates that these entities maintain a similarity score above the threshold with nearly every other entity in the set.\n", + "* **Union-Find Logic:** Because the pipeline utilizes a **Union-Find (Disjoint Set Union)** algorithm, a single \"bridge\" is sufficient to merge two distinct groups. In this case, item [24] acts as a universal bridge.\n", + "* **Cluster Collapse:** Since item [24] is connected to almost all other nodes, the algorithm will transitively link every entity into a single tree structure, eventually collapsing the entire dataset into one oversized cluster." + ] + }, + { + "cell_type": "markdown", + "id": "d3b5b315", + "metadata": {}, + "source": [ + "## ***CONCLUSION***: Hyperparameter Optimization Results\n", + "\n", + "After a systematic grid search across various scorers, **`token_set_ratio`** was identified as the top-performing configuration. The following analysis breaks down why this method provided the best balance for our entity disambiguation task:\n", + "\n", + "#### 1. Robustness to Context and Alias Matching (The \"Set\" Advantage)\n", + "\n", + "Unlike character-level scorers (like `ratio` or `partial_ratio`), `token_set_ratio` treats strings as unordered collections of words (tokens). \n", + "\n", + "* **Handling Name Aliases:** It is particularly effective at linking different versions of a person's name. For example, it recognizes that **\"Juan Pérez\"** and **\"Juan Alberto Pérez\"** refer to the same entity by identifying the shared tokens (\"Juan\", \"Pérez\") as a subset, yielding a high similarity score despite the additional middle name.\n", + "* **The \"Intersection\" Logic:** This is crucial for our dataset when a name appears as \"Juan Pérez\" in one document and \"Juan Pérez Asesor/a\" in another (where the NER might have accidentally included the role).\n", + "* **Subset Resilience:** It yields a score of 100 if one name is a perfect subset of the other, effectively ignoring \"noise\" words or titles that often appear in raw NER extractions.\n", + "\n", + "#### 2. Superior Performance over `token_sort_ratio`\n", + "\n", + "While `token_sort_ratio` is excellent at handling word reordering (e.g., \"Juan Pérez\" vs. \"Pérez, Juan\"), it is highly sensitive to the length of the string. \n", + "* In scenarios involving **aliases with extra names** (e.g., \"Juan Pérez\" vs. \"Juan Alberto Pérez\") or **titles** (e.g., \"Doctor Juan Pérez\"), `token_sort_ratio` would penalize the score significantly because the total word count differs. \n", + "* **`token_set_ratio`** maintains a high similarity score in these cases by prioritizing the common intersection of words over the total string length.\n", + "\n", + "#### 3. Preventing Over-Clustering (Avoidance of \"Partial\" Aggression)\n", + "\n", + "The grid search revealed that more aggressive scorers, such as `partial_token_set_ratio`, often led to **over-clustering** and data pollution.\n", + "\n", + "* **The Risk of \"Partial\" Logic:** These scorers are too \"patient\" with differences. If they find a small, similar fragment within a much longer, unrelated string, they force a high score. \n", + "* **Chaining Effect:** This was the primary cause of the \"Chaining Effect\" (as seen with **Item [24]**), where a single-letter entity or a common word acts as a universal bridge, transitively merging hundreds of unrelated individuals into a single \"giant cluster.\"\n", + "* **The `token_set_ratio` Balance:** It provides the necessary flexibility to catch legitimate aliases without being so aggressive that it collapses the distinct identities of the entire corpus.\n", + "\n", + "### Final Recommendation\n", + "\n", + "The **`token_set_ratio`** achieved the highest **Weighted Disambiguation Score** because it effectively handles variable-length mentions, middle names, and word-order changes while maintaining a strict enough threshold to avoid the transitive merging of unrelated person entities." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/entity-disambiguation/09-date-formatter-disambiguation.ipynb b/notebooks/experiments/entity-disambiguation/09-date-formatter-disambiguation.ipynb new file mode 100644 index 00000000..0cefa239 --- /dev/null +++ b/notebooks/experiments/entity-disambiguation/09-date-formatter-disambiguation.ipynb @@ -0,0 +1,360 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "185b896a", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext rich\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c675263", + "metadata": {}, + "outputs": [], + "source": [ + "# import locale\n", + "# import platform\n", + "\n", + "# locales_a_probar = ['es_AR.UTF-8', 'es_ES.UTF-8', 'es_MX.UTF-8', 'spanish']\n", + "\n", + "# for loc in locales_a_probar:\n", + "# try:\n", + "# locale.setlocale(locale.LC_ALL, loc)\n", + "# break\n", + "# except locale.Error:\n", + "# continue\n", + "\n", + "# print(f\"Sistema: {platform.system()}\")\n", + "# print(f\"Locale actual: {locale.getlocale()}\")\n", + "# print(f\"Default Locale: {locale.getdefaultlocale()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6640fde2", + "metadata": {}, + "outputs": [], + "source": [ + "# # 1. Forzamos a Python a que crea que \"es_AR.UTF-8\" es lo mismo que el locale básico\n", + "# # Esto intercepta la llamada que falla en patterns.py:4\n", + "# def mock_setlocale(category, value=None):\n", + "# return \"C\" # 'C' es el locale universal que siempre existe\n", + "\n", + "# locale.setlocale = mock_setlocale\n", + "\n", + "# # 2. Opcional: Si quieres ser más preciso, intenta usar el español genérico\n", + "# try:\n", + "# # En Mac a veces es 'es_ES.UTF-8' o solo 'es_AR' (sin .UTF-8)\n", + "# # Aquí intentamos forzar uno que funcione\n", + "# import _locale\n", + "# _locale._setlocale(locale.LC_ALL, 'en_US.UTF-8') \n", + "# except:\n", + "# pass\n", + "\n", + "# print(\"✅ Hack de locale aplicado. Ya puedes importar aymurai.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "692b5bfe", + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.transforms.datetime_formatter import DatetimeFormatter\n", + "from aymurai.meta.api_interfaces import DocLabel\n", + "from aymurai.utils.entity_disambiguation.date_formatter import get_canonical_dates" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54ad952d", + "metadata": {}, + "outputs": [], + "source": [ + "formatter = DatetimeFormatter()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73946d4a", + "metadata": {}, + "outputs": [], + "source": [ + "ent = DocLabel(\n", + " text= \"el dia 23 de enero de 2024\",\n", + " start_char= 0,\n", + " end_char= 500,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"enero de 2024\",\n", + " \"aymurai_alt_start_char\": 0,\n", + " \"aymurai_alt_end_char\": 400,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.7974909742673238,\n", + " \"aymurai_label_instance\": 2,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": True,\n", + " \"canonical_entity_id\": \"e8a5378d-bbc1-50f0-8da1-27871d5a80a2\"\n", + " }\n", + ")\n", + "\n", + "date_1 = DocLabel(\n", + " text=\"Buenos Aires, 14 de mayo de 2023\",\n", + " start_char=10,\n", + " end_char=42,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"14/05/2023\",\n", + " \"aymurai_alt_start_char\": 24,\n", + " \"aymurai_alt_end_char\": 42,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.9543,\n", + " \"aymurai_label_instance\": 1,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": \"fe54c1a2-b3d4-4e5f-a6b7-c8d9e0f1a2b3\"\n", + " }\n", + ")\n", + "\n", + "date_2 = DocLabel(\n", + " text=\"audiencia del 03 de febrero de 2024\",\n", + " start_char=150,\n", + " end_char=185,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"03/02/2024\",\n", + " \"aymurai_alt_start_char\": 164,\n", + " \"aymurai_alt_end_char\": 185,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.8876,\n", + " \"aymurai_label_instance\": 2,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": \"a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d\"\n", + " }\n", + ")\n", + "\n", + "date_3 = DocLabel(\n", + " text=\"22/10/2023\",\n", + " start_char=402,\n", + " end_char=412,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"22/10/2023\",\n", + " \"aymurai_alt_start_char\": 402,\n", + " \"aymurai_alt_end_char\": 412,\n", + " \"aymurai_method\": \"regex/patterns\",\n", + " \"aymurai_score\": 1.0,\n", + " \"aymurai_label_instance\": 3,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": \"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e\"\n", + " }\n", + ")\n", + "\n", + "date_4 = DocLabel(\n", + " text=\"notificado el pasado lunes 11\",\n", + " start_char=820,\n", + " end_char=849,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"11\",\n", + " \"aymurai_alt_start_char\": 847,\n", + " \"aymurai_alt_end_char\": 849,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.6124,\n", + " \"aymurai_label_instance\": 4,\n", + " \"aymurai_disambiguation\": \"llm\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": \"c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f\"\n", + " }\n", + ")\n", + "\n", + "date_5 = DocLabel(\n", + " text=\"primero de enero de dos mil veintiuno\",\n", + " start_char=1100,\n", + " end_char=1137,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"01/01/2021\",\n", + " \"aymurai_alt_start_char\": 1100,\n", + " \"aymurai_alt_end_char\": 1137,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.7942,\n", + " \"aymurai_label_instance\": 5,\n", + " \"aymurai_disambiguation\": \"llm\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": \"d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a\"\n", + " }\n", + ")\n", + "\n", + "\n", + "labels_baseline = [ent, date_1, date_2, date_3, date_4, date_5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "417e0719", + "metadata": {}, + "outputs": [], + "source": [ + "date_1 = DocLabel(\n", + " text=\"el dia 23 de enero de 2024\",\n", + " start_char=0,\n", + " end_char=26,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"23 de enero de 2024\",\n", + " \"aymurai_alt_start_char\": 7,\n", + " \"aymurai_alt_end_char\": 26,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.98,\n", + " \"aymurai_label_instance\": 1,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": None\n", + " }\n", + ")\n", + "\n", + "date_2 = DocLabel(\n", + " text=\"hechos del 23/01/24\",\n", + " start_char=150,\n", + " end_char=169,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"23/01/24\",\n", + " \"aymurai_alt_start_char\": 11,\n", + " \"aymurai_alt_end_char\": 19,\n", + " \"aymurai_method\": \"regex/patterns\",\n", + " \"aymurai_score\": 1.0,\n", + " \"aymurai_label_instance\": 2,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": None\n", + " }\n", + ")\n", + "\n", + "date_3 = DocLabel(\n", + " text=\"ocurrido el 23 enero 2024\",\n", + " start_char=300,\n", + " end_char=325,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"23 enero 2024\",\n", + " \"aymurai_alt_start_char\": 12,\n", + " \"aymurai_alt_end_char\": 25,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.91,\n", + " \"aymurai_label_instance\": 3,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": None\n", + " }\n", + ")\n", + "\n", + "date_4 = DocLabel(\n", + " text=\"23-01-2024\",\n", + " start_char=450,\n", + " end_char=460,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"23-01-2024\",\n", + " \"aymurai_alt_start_char\": 450,\n", + " \"aymurai_alt_end_char\": 460,\n", + " \"aymurai_method\": \"regex/patterns\",\n", + " \"aymurai_score\": 1.0,\n", + " \"aymurai_label_instance\": 4,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": None\n", + " }\n", + ")\n", + "\n", + "date_5 = DocLabel(\n", + " text=\"23 de enero\",\n", + " start_char=600,\n", + " end_char=618,\n", + " attrs={\n", + " \"aymurai_label\": \"FECHA\",\n", + " \"aymurai_label_subclass\": [],\n", + " \"aymurai_alt_text\": \"23 de enero\",\n", + " \"aymurai_alt_start_char\": 600,\n", + " \"aymurai_alt_end_char\": 618,\n", + " \"aymurai_method\": \"ner/flair\",\n", + " \"aymurai_score\": 0.99,\n", + " \"aymurai_label_instance\": 5,\n", + " \"aymurai_disambiguation\": \"fuzzy\",\n", + " \"aymurai_anonymize\": False,\n", + " \"canonical_entity_id\": None\n", + " }\n", + ")\n", + "\n", + "batch_dates = [date_1, date_2, date_3, date_4, date_5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b9581fd", + "metadata": {}, + "outputs": [], + "source": [ + "ce = get_canonical_dates(batch_dates)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7b7a58a", + "metadata": {}, + "outputs": [], + "source": [ + "ce" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/entity-disambiguation/10-anonymize-document-render-policy.ipynb b/notebooks/experiments/entity-disambiguation/10-anonymize-document-render-policy.ipynb new file mode 100644 index 00000000..92bc8185 --- /dev/null +++ b/notebooks/experiments/entity-disambiguation/10-anonymize-document-render-policy.ipynb @@ -0,0 +1,463 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "from pathlib import Path\n", + "\n", + "import requests\n", + "from tqdm import tqdm\n", + "\n", + "API_URL = \"http://localhost:8000\" # Url for debugger. change it to your own\n", + "DATA_ROOT = Path(\n", + " os.getenv(\n", + " \"DISAMBIGUATION_DATA_ROOT\",\n", + " \"../../../resources/data/restricted/disambiguation-eval/files\",\n", + " )\n", + ")\n", + "DOC_EXTENSIONS = {\".pdf\", \".docx\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sample document\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def discover_documents(root: Path, extensions: set[str]) -> list[Path]:\n", + " extensions = {ext.lower() for ext in extensions}\n", + " return sorted(\n", + " path\n", + " for path in root.rglob(\"*\")\n", + " if path.is_file() and path.suffix.lower() in extensions\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "documents = discover_documents(DATA_ROOT, DOC_EXTENSIONS)\n", + "\n", + "print(f\"Found {len(documents)} documents\")\n", + "\n", + "doc_path = documents[5]\n", + "print(f\"Processing document: {doc_path}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## /document-extract endpoint output\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import mimetypes\n", + "import time\n", + "from pathlib import Path\n", + "from typing import Any, Iterable\n", + "\n", + "from more_itertools import unique_everseen\n", + "\n", + "\n", + "def discover_documents(root: Path, extensions: Iterable[str]) -> list[Path]:\n", + " \"\"\"\n", + " Discover documents in a directory with given extensions.\n", + "\n", + " Args:\n", + " root (Path): Root directory to search for documents.\n", + " extensions (Iterable[str]): File extensions to include.\n", + "\n", + " Returns:\n", + " list[Path]: List of discovered document paths with the specified extensions.\n", + " \"\"\"\n", + " extensions = {ext.lower() for ext in extensions}\n", + " return sorted(\n", + " path\n", + " for path in root.rglob(\"*\")\n", + " if path.is_file() and path.suffix.lower() in extensions\n", + " )\n", + "\n", + "\n", + "def call_extraction_api(\n", + " session: requests.Session, endpoint: str, file_path: Path, timeout_s: float\n", + ") -> dict[str, object]:\n", + " \"\"\"\n", + " Call the extraction API with a document file.\n", + "\n", + " Args:\n", + " session (requests.Session): HTTP session for making requests.\n", + " endpoint (str): URL of the extraction API endpoint.\n", + " file_path (Path): Path to the document file to be processed.\n", + " timeout_s (float): Request timeout in seconds.\n", + "\n", + " Returns:\n", + " dict[str, object]: Payload containing the response details.\n", + " \"\"\"\n", + " payload: dict[str, object] = {\n", + " \"path\": str(file_path),\n", + " \"status\": \"failure\",\n", + " \"status_code\": None,\n", + " \"elapsed_s\": None,\n", + " \"detail\": None,\n", + " }\n", + "\n", + " if not file_path.exists():\n", + " payload[\"detail\"] = \"File does not exist\"\n", + " return payload\n", + "\n", + " mime_type = mimetypes.guess_type(file_path.name)[0] or \"application/octet-stream\"\n", + " files = {\n", + " \"file\": (file_path.name, file_path.open(\"rb\"), mime_type),\n", + " }\n", + "\n", + " try:\n", + " start = time.perf_counter()\n", + " response = session.post(\n", + " endpoint,\n", + " files=files,\n", + " timeout=timeout_s,\n", + " )\n", + " elapsed = time.perf_counter() - start\n", + " except requests.RequestException as exc:\n", + " payload[\"detail\"] = f\"Request failed: {exc}\"\n", + " return payload\n", + " finally:\n", + " files[\"file\"][1].close()\n", + "\n", + " payload[\"status_code\"] = response.status_code\n", + " payload[\"elapsed_s\"] = elapsed\n", + "\n", + " try:\n", + " response_body = response.json()\n", + " except ValueError:\n", + " response_body = {\"raw\": response.text[:500]}\n", + "\n", + " if response.ok:\n", + " payload[\"status\"] = \"success\"\n", + " payload[\"detail\"] = {\n", + " \"document_id\": response_body.get(\"document_id\"),\n", + " \"document\": response_body.get(\"document\", []),\n", + " }\n", + " else:\n", + " payload[\"detail\"] = response_body\n", + "\n", + " return payload\n", + "\n", + "\n", + "def parse_prediction_labels(predictions: list[dict[str, Any]]) -> list[dict[str, str]]:\n", + " \"\"\"\n", + " Parse prediction labels to extract unique aymurai labels and their alternative texts.\n", + "\n", + " Args:\n", + " predictions (list[dict[str, Any]]): List of prediction dictionaries.\n", + "\n", + " Returns:\n", + " list[dict[str, str]]: List of dictionaries containing unique aymurai labels and their alternative texts.\n", + " \"\"\"\n", + " attrs_stream = (\n", + " label.get(\"attrs\") or {}\n", + " for label in (label for pred in predictions for label in pred.get(\"labels\", ()))\n", + " )\n", + "\n", + " unique_pairs = unique_everseen(\n", + " (\n", + " attrs.get(\"aymurai_label\"),\n", + " attrs.get(\"aymurai_alt_text\"),\n", + " )\n", + " for attrs in attrs_stream\n", + " if attrs.get(\"aymurai_label\") and attrs.get(\"aymurai_alt_text\")\n", + " )\n", + "\n", + " return sorted(\n", + " ({\"aymurai_label\": label, \"text\": text} for label, text in unique_pairs),\n", + " key=lambda item: (item[\"aymurai_label\"], item[\"text\"]),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# /document-extract endpoint output\n", + "session = requests.Session()\n", + "document = call_extraction_api(\n", + " session,\n", + " endpoint=f\"{API_URL}/misc/document-extract\",\n", + " file_path=doc_path,\n", + " timeout_s=300,\n", + ")\n", + "paragraphs = document[\"detail\"][\"document\"]\n", + "document" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(paragraphs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Function to make inference using the API\n", + "def get_predictions(sample: str) -> dict:\n", + " response = requests.post(\n", + " url=f\"{API_URL}/anonymizer/predict\",\n", + " json={\"text\": sample},\n", + " params={\"use_cache\": False},\n", + " )\n", + " response.raise_for_status()\n", + " return response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "predictions = [get_predictions(paragraph) for paragraph in tqdm(paragraphs)]\n", + "predictions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export variants\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def disambiguate_and_export(\n", + " variant_name: str, label_policies: dict, render_policy: dict\n", + "):\n", + " response = requests.post(\n", + " url=f\"{API_URL}/anonymizer/disambiguate\",\n", + " json={\n", + " \"paragraphs\": predictions,\n", + " # \"custom_prompts\": {\"root\": []},\n", + " \"label_policies\": label_policies,\n", + " },\n", + " )\n", + " response.raise_for_status()\n", + " disambiguated = response.json()\n", + "\n", + " json_prediction = json.dumps(\n", + " {\n", + " \"data\": disambiguated[\"data\"],\n", + " \"label_policies\": label_policies,\n", + " \"render_policy\": render_policy,\n", + " }\n", + " )\n", + "\n", + " anonymize_labels = []\n", + " for item in disambiguated[\"data\"]:\n", + " for label in item.get(\"labels\", []):\n", + " if label:\n", + " anonymize_labels.append(label)\n", + "\n", + " with open(doc_path, \"rb\") as file:\n", + " files = {\"file\": file}\n", + "\n", + " response = requests.post(\n", + " url=f\"{API_URL}/anonymizer/anonymize-document\",\n", + " data={\"annotations\": json_prediction},\n", + " files=files,\n", + " )\n", + " response.raise_for_status()\n", + "\n", + " output_dir = \"output\"\n", + " os.makedirs(output_dir, exist_ok=True)\n", + "\n", + " filename = os.path.basename(doc_path)\n", + " filename, ext = os.path.splitext(filename)\n", + " out_path = f\"{output_dir}/{filename}-{variant_name}.odt\"\n", + "\n", + " with open(out_path, \"wb\") as file:\n", + " file.write(response.content)\n", + "\n", + " return disambiguated, out_path, anonymize_labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "render_policy = {\n", + " \"suffix_mode\": \"auto\",\n", + " \"suffix_threshold\": 1,\n", + "}\n", + "\n", + "# 1) everything fuzzy\n", + "label_policies_all_fuzzy = {\n", + " \"PER\": {\n", + " \"disambiguation\": \"fuzzy\",\n", + " \"anonymize\": True,\n", + " \"use_subclass_when_available\": True,\n", + " },\n", + " \"DNI\": {\n", + " \"disambiguation\": \"fuzzy\",\n", + " \"anonymize\": True,\n", + " \"use_subclass_when_available\": False,\n", + " },\n", + " \"LOC\": {\n", + " \"disambiguation\": \"fuzzy\",\n", + " \"anonymize\": True,\n", + " \"use_subclass_when_available\": False,\n", + " },\n", + " \"DIRECCION\": {\n", + " \"disambiguation\": \"fuzzy\",\n", + " \"anonymize\": True,\n", + " \"use_subclass_when_available\": False,\n", + " },\n", + " \"FECHA\": {\n", + " \"disambiguation\": \"fuzzy\",\n", + " \"anonymize\": True,\n", + " \"use_subclass_when_available\": False,\n", + " },\n", + "}\n", + "disambiguated_labels_fuzzy, out_all_fuzzy, anonymize_labels_fuzzy = (\n", + " disambiguate_and_export(\"all-fuzzy\", label_policies_all_fuzzy, render_policy)\n", + ")\n", + "\n", + "# # 2) FECHAs excluded\n", + "# label_policies_no_fecha = {\n", + "# \"PER\": {\"disambiguation\": \"fuzzy\", \"anonymize\": True, \"use_subclass_when_available\": True},\n", + "# \"DNI\": {\"disambiguation\": \"fuzzy\", \"anonymize\": True, \"use_subclass_when_available\": False},\n", + "# \"LOC\": {\"disambiguation\": \"fuzzy\", \"anonymize\": True, \"use_subclass_when_available\": False},\n", + "# \"DIRECCION\": {\"disambiguation\": \"fuzzy\", \"anonymize\": True, \"use_subclass_when_available\": False},\n", + "# \"FECHA\": {\"disambiguation\": \"none\", \"anonymize\": False, \"use_subclass_when_available\": False},\n", + "# }\n", + "# disambiguated_labels_no_fecha, out_no_fecha = disambiguate_and_export(\n", + "# \"no-fecha\", label_policies_no_fecha, render_policy\n", + "# )\n", + "\n", + "\n", + "# out_all_fuzzy, out_no_fecha" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "anonymize_labels_fuzzy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "from typing import Any\n", + "\n", + "\n", + "def group_alt_texts_by_entity(data: list[dict[str, Any]]) -> dict[str, list[str]]:\n", + " grouped_data = defaultdict(list)\n", + "\n", + " for entry in data:\n", + " attrs = entry.get(\"attrs\", {})\n", + " entity_id = attrs.get(\"canonical_entity_id\")\n", + " alt_text = attrs.get(\"aymurai_alt_text\")\n", + " subclass = attrs.get(\"aymurai_label_subclass\")\n", + " if entity_id and alt_text:\n", + " grouped_data[entity_id].append((alt_text, subclass))\n", + "\n", + " return dict(grouped_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result = group_alt_texts_by_entity(anonymize_labels_fuzzy)\n", + "print(json.dumps(result, indent=4, ensure_ascii=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/experiments/ner/flair/README.md b/notebooks/experiments/ner/flair/README.md deleted file mode 100644 index 83b27d78..00000000 --- a/notebooks/experiments/ner/flair/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# NER model experiments - -# Overview -Flair token classification model for Spanish judicial text. - -The model card can be found [here](../../../../docs/pipeline/flair-model-card.md) - -75% of the total 1200 documents (court rulings) is used for training and 12.5% for validation and 12.5% for testing. -Each document is split into paragrpahs. The data representation follows the CONLL-2003 format. - -# Notebooks -- [00-flair-direct-finetuning.ipynb](00-flair-direct-finetuning.ipynb): fine-tune a pretrained Flair model on the annotated data -- [01-flair-indirect-finetuning.ipynb](00-flair-indirect-finetuning.ipynb): fine-tune a pretrained Flair model on the annotated data -- [02-prediction-formatting.ipynb](02-prediction-formatting.ipynb): format the predictions to the AymurAI pipeline format -- [03-pipeline-integration.ipynb](03-pipeline-integration.ipynb): build the pipeline -- [04-flair-no-finetuning-no-decision.ipynb](04-flair-no-finetuning-no-decision.ipynb): train a Flair model excluding decisions from the data \ No newline at end of file diff --git a/notebooks/experiments/pdf-support/06-pymupdf-layout.ipynb b/notebooks/experiments/pdf-support/06-pymupdf-layout.ipynb new file mode 100644 index 00000000..803c8d22 --- /dev/null +++ b/notebooks/experiments/pdf-support/06-pymupdf-layout.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1098eca1", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext rich\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "id": "7e81fbe5", + "metadata": {}, + "source": [ + "# End-to-End PDF Anonymization (PyMuPDF Layout + AymurAI API)\n", + "This notebook builds layout-based paragraphs from the source PDF, runs `/anonymizer/predict` + `/anonymizer/disambiguate`, and compiles an anonymized PDF.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "258fbd18", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import pymupdf\n", + "import requests\n", + "from tqdm.auto import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcfd985e", + "metadata": {}, + "outputs": [], + "source": [ + "# Change these values to test different documents/environments.\n", + "API_URL = \"http://localhost:8999\"\n", + "SOURCE_PDF = Path(\"./document.pdf\")\n", + "\n", + "OUTPUT_DIR = Path(\"./output\")\n", + "USE_CACHE = False\n", + "\n", + "# Optional: keep as None to rely on backend default policies.\n", + "LABEL_POLICIES = None\n", + "\n", + "# Keep aligned with current anonymizer defaults.\n", + "RENDER_POLICY = {\"suffix_mode\": \"auto\", \"suffix_threshold\": 1}\n", + "\n", + "SOURCE_PDF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3860b71", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_document_via_api(pdf_path: Path) -> dict:\n", + " with pdf_path.open(\"rb\") as handle:\n", + " response = requests.post(\n", + " f\"{API_URL}/document-extract\",\n", + " files={\"file\": (pdf_path.name, handle, \"application/pdf\")},\n", + " timeout=600,\n", + " )\n", + "\n", + " response.raise_for_status()\n", + " return response.json()\n", + "\n", + "\n", + "def predict_paragraph(text: str, retries: int = 2) -> dict:\n", + " last_error = None\n", + " for attempt in range(retries + 1):\n", + " try:\n", + " response = requests.post(\n", + " f\"{API_URL}/anonymizer/predict\",\n", + " json={\"text\": text},\n", + " params={\"use_cache\": USE_CACHE},\n", + " timeout=600,\n", + " )\n", + " response.raise_for_status()\n", + " return response.json()\n", + " except Exception as exc:\n", + " last_error = exc\n", + " if attempt < retries:\n", + " time.sleep(2)\n", + " else:\n", + " raise last_error\n", + "\n", + " raise RuntimeError(\"Predict request exhausted retries\")\n", + "\n", + "\n", + "def disambiguate(predictions: list[dict]) -> dict:\n", + " payload = {\"paragraphs\": predictions}\n", + " if LABEL_POLICIES is not None:\n", + " payload[\"label_policies\"] = LABEL_POLICIES\n", + "\n", + " response = requests.post(\n", + " f\"{API_URL}/anonymizer/disambiguate\",\n", + " json=payload,\n", + " timeout=600,\n", + " )\n", + " response.raise_for_status()\n", + " return response.json()\n", + "\n", + "\n", + "def compile_pdf(pdf_path: Path, annotations: dict) -> Path:\n", + " payload = {\n", + " \"data\": annotations[\"data\"],\n", + " \"render_policy\": RENDER_POLICY,\n", + " }\n", + " if annotations.get(\"label_policies\") is not None:\n", + " payload[\"label_policies\"] = annotations[\"label_policies\"]\n", + "\n", + " with pdf_path.open(\"rb\") as handle:\n", + " response = requests.post(\n", + " f\"{API_URL}/anonymizer/anonymize-document\",\n", + " data={\"annotations\": json.dumps(payload, ensure_ascii=False)},\n", + " files={\"file\": (pdf_path.name, handle, \"application/pdf\")},\n", + " timeout=1200,\n", + " )\n", + "\n", + " response.raise_for_status()\n", + "\n", + " OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n", + " output_path = OUTPUT_DIR / f\"{pdf_path.stem}.anonymized.pdf\"\n", + " output_path.write_bytes(response.content)\n", + " return output_path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0a54485", + "metadata": {}, + "outputs": [], + "source": [ + "document_extract_payload = extract_document_via_api(SOURCE_PDF)\n", + "paragraphs = document_extract_payload[\"document\"]\n", + "\n", + "print(f\"Document ID: {document_extract_payload['document_id']}\")\n", + "print(f\"Paragraphs extracted: {len(paragraphs)}\")\n", + "\n", + "paragraphs[:5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3beaadee", + "metadata": {}, + "outputs": [], + "source": [ + "predictions = [\n", + " predict_paragraph(paragraph)\n", + " for paragraph in tqdm(paragraphs, desc=\"Predicting paragraphs\")\n", + "]\n", + "total_labels = sum(len(pred.get(\"labels\") or []) for pred in predictions)\n", + "print(f\"Predictions: {len(predictions)} paragraphs, {total_labels} labels\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "682760e0", + "metadata": {}, + "outputs": [], + "source": [ + "disambiguated = disambiguate(predictions)\n", + "total_labels = sum(len(pred.get(\"labels\") or []) for pred in disambiguated[\"data\"])\n", + "print(f\"Disambiguated labels: {total_labels}\")\n", + "disambiguated.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eae3f2c9", + "metadata": {}, + "outputs": [], + "source": [ + "[data for data in disambiguated[\"data\"] if data[\"labels\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "665dde4a", + "metadata": {}, + "outputs": [], + "source": [ + "output_pdf = compile_pdf(SOURCE_PDF, disambiguated)\n", + "print(output_pdf.resolve())\n", + "output_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "715a782a", + "metadata": {}, + "outputs": [], + "source": [ + "with pymupdf.open(str(output_pdf)) as doc:\n", + " watermark_hits = sum(\n", + " len(page.search_for(\"Documento anonimizado por AymurAI\")) for page in doc\n", + " )\n", + " print(f\"Pages: {doc.page_count}\")\n", + " print(f\"Watermark hits: {watermark_hits}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a274809", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.20" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/sentence-encoders/01-test-sentence-encoders-mac.ipynb b/notebooks/experiments/sentence-encoders/01-test-sentence-encoders-mac.ipynb new file mode 100644 index 00000000..378b1064 --- /dev/null +++ b/notebooks/experiments/sentence-encoders/01-test-sentence-encoders-mac.ipynb @@ -0,0 +1,346 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test Sentence Encoders on Apple Silicon\n", + "\n", + "This notebook tests the sentence-transformers based encoders that are compatible with Apple Silicon (M1/M2/M3)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import platform\n", + "import time\n", + "\n", + "import numpy as np\n", + "\n", + "# Check platform\n", + "print(f\"Platform: {platform.system()}\")\n", + "print(f\"Machine: {platform.machine()}\")\n", + "print(\n", + " f\"Apple Silicon: {platform.system() == 'Darwin' and platform.machine() == 'arm64'}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Test DistilUSE Encoder\n", + "\n", + "Knowledge-distilled version of Universal Sentence Encoder. 512-dimensional embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.models.usem import DistilUSEEncoder\n", + "\n", + "distiluse = DistilUSEEncoder()\n", + "print(f\"Model: {distiluse.MODEL_NAME}\")\n", + "print(f\"Device: {distiluse.model.device}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test encoding\n", + "test_sentences = [\n", + " \"El juez dictó sentencia en el caso de violencia de género.\",\n", + " \"La víctima presentó una denuncia por amenazas.\",\n", + " \"Se otorgó una medida de protección a la denunciante.\",\n", + " \"The judge issued a ruling in the gender violence case.\",\n", + " \"The victim filed a complaint for threats.\",\n", + "]\n", + "\n", + "start = time.time()\n", + "embeddings_distiluse = distiluse.encode(test_sentences, encoder_type=\"question_encoder\")\n", + "elapsed = time.time() - start\n", + "\n", + "print(f\"Embeddings shape: {embeddings_distiluse.shape}\")\n", + "print(f\"Encoding time: {elapsed:.3f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Test Multilingual MiniLM Encoder\n", + "\n", + "Higher quality embeddings, faster inference. 384-dimensional embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.models.usem import MultilingualMiniLMEncoder\n", + "\n", + "minilm = MultilingualMiniLMEncoder()\n", + "print(f\"Model: {minilm.MODEL_NAME}\")\n", + "print(f\"Device: {minilm.model.device}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start = time.time()\n", + "embeddings_minilm = minilm.encode(test_sentences, encoder_type=\"question_encoder\")\n", + "elapsed = time.time() - start\n", + "\n", + "print(f\"Embeddings shape: {embeddings_minilm.shape}\")\n", + "print(f\"Encoding time: {elapsed:.3f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Test Factory Auto-Detection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.models.usem import create_encoder, EncoderType\n", + "\n", + "# Auto-detect (should use DistilUSE on Apple Silicon)\n", + "encoder_auto = create_encoder()\n", + "print(f\"Auto-detected encoder: {type(encoder_auto).__name__}\")\n", + "\n", + "# Explicit selection\n", + "encoder_minilm = create_encoder(EncoderType.MINILM)\n", + "print(f\"Explicit MiniLM encoder: {type(encoder_minilm).__name__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Semantic Similarity Test\n", + "\n", + "Compare similarity between Spanish and English sentences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def cosine_similarity(a, b):\n", + " \"\"\"Compute cosine similarity between two vectors.\"\"\"\n", + " return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))\n", + "\n", + "\n", + "def similarity_matrix(embeddings, sentences):\n", + " \"\"\"Compute and display similarity matrix.\"\"\"\n", + " n = len(sentences)\n", + " sim_matrix = np.zeros((n, n))\n", + " for i in range(n):\n", + " for j in range(n):\n", + " sim_matrix[i, j] = cosine_similarity(embeddings[i], embeddings[j])\n", + " return sim_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute similarity matrix for DistilUSE\n", + "sim_distiluse = similarity_matrix(embeddings_distiluse, test_sentences)\n", + "\n", + "print(\"DistilUSE Similarity Matrix:\")\n", + "print(\"Sentences:\")\n", + "for i, s in enumerate(test_sentences):\n", + " print(f\" [{i}] {s[:60]}...\" if len(s) > 60 else f\" [{i}] {s}\")\n", + "print()\n", + "print(np.round(sim_distiluse, 3))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute similarity matrix for MiniLM\n", + "sim_minilm = similarity_matrix(embeddings_minilm, test_sentences)\n", + "\n", + "print(\"MiniLM Similarity Matrix:\")\n", + "print(np.round(sim_minilm, 3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Cross-lingual Similarity\n", + "\n", + "Test that Spanish-English translation pairs have high similarity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Spanish sentence [0] should be similar to English sentence [3]\n", + "# Spanish sentence [1] should be similar to English sentence [4]\n", + "\n", + "print(\"Cross-lingual similarity (Spanish ↔ English):\")\n", + "print(f\"\\nDistilUSE:\")\n", + "print(\n", + " f\" '{test_sentences[0][:40]}...' ↔ '{test_sentences[3][:40]}...': {sim_distiluse[0, 3]:.3f}\"\n", + ")\n", + "print(\n", + " f\" '{test_sentences[1][:40]}...' ↔ '{test_sentences[4][:40]}...': {sim_distiluse[1, 4]:.3f}\"\n", + ")\n", + "\n", + "print(f\"\\nMiniLM:\")\n", + "print(\n", + " f\" '{test_sentences[0][:40]}...' ↔ '{test_sentences[3][:40]}...': {sim_minilm[0, 3]:.3f}\"\n", + ")\n", + "print(\n", + " f\" '{test_sentences[1][:40]}...' ↔ '{test_sentences[4][:40]}...': {sim_minilm[1, 4]:.3f}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Batch Encoding Performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate more sentences for batch test\n", + "batch_sentences = test_sentences * 100 # 500 sentences\n", + "\n", + "print(f\"Batch size: {len(batch_sentences)} sentences\")\n", + "\n", + "# DistilUSE batch\n", + "start = time.time()\n", + "batch_embeddings_distiluse = distiluse.batch_encode(\n", + " batch_sentences, encoder_type=\"question_encoder\", batch_size=64\n", + ")\n", + "elapsed_distiluse = time.time() - start\n", + "print(\n", + " f\"\\nDistilUSE batch encoding: {elapsed_distiluse:.3f}s ({len(batch_sentences) / elapsed_distiluse:.1f} sent/s)\"\n", + ")\n", + "\n", + "# MiniLM batch\n", + "start = time.time()\n", + "batch_embeddings_minilm = minilm.batch_encode(\n", + " batch_sentences, encoder_type=\"question_encoder\", batch_size=64\n", + ")\n", + "elapsed_minilm = time.time() - start\n", + "print(\n", + " f\"MiniLM batch encoding: {elapsed_minilm:.3f}s ({len(batch_sentences) / elapsed_minilm:.1f} sent/s)\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Environment Variable Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test environment variable configuration\n", + "import importlib\n", + "import aymurai.models.usem.factory as factory_module\n", + "\n", + "# Set env var and reload\n", + "os.environ[\"SENTENCE_ENCODER_TYPE\"] = \"minilm\"\n", + "importlib.reload(factory_module)\n", + "from aymurai.models.usem.factory import create_encoder\n", + "\n", + "encoder_from_env = create_encoder()\n", + "print(f\"Encoder from SENTENCE_ENCODER_TYPE=minilm: {type(encoder_from_env).__name__}\")\n", + "\n", + "# Reset to auto\n", + "os.environ[\"SENTENCE_ENCODER_TYPE\"] = \"auto\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "Both `DistilUSEEncoder` and `MultilingualMiniLMEncoder` work on Apple Silicon:\n", + "\n", + "| Model | Embedding Dim | Speed | Quality |\n", + "|-------|---------------|-------|--------|\n", + "| DistilUSE | 512 | Good | Good (USE distillation) |\n", + "| MiniLM | 384 | Faster | Better |\n", + "\n", + "Use `SENTENCE_ENCODER_TYPE=auto` for automatic platform detection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai (3.10.19)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/experiments/sentence-encoders/02-tensorflow-deprecation.ipynb b/notebooks/experiments/sentence-encoders/02-tensorflow-deprecation.ipynb new file mode 100755 index 00000000..b0ce826c --- /dev/null +++ b/notebooks/experiments/sentence-encoders/02-tensorflow-deprecation.ipynb @@ -0,0 +1,2255 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "2a8e7308", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "%load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "730c04d9", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "import random\n", + "from collections import defaultdict\n", + "from copy import deepcopy\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "from IPython.display import Markdown\n", + "from rich import print\n", + "\n", + "from aymurai.utils.json_data import load_json\n", + "\n", + "sns.set_theme(style=\"whitegrid\")\n", + "plt.rcParams.update(\n", + " {\"figure.figsize\": (10, 6), \"axes.titlesize\": 14, \"axes.labelsize\": 12}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "211f50a2", + "metadata": {}, + "source": [ + "## Load annotations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1f76f76", + "metadata": {}, + "outputs": [], + "source": [ + "annotations_path = \"/resources/annotations/label-studio/resos-annotations/30-nov/project-3-at-2022-11-30-16-04-2b43bf39.json\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0e13682", + "metadata": {}, + "outputs": [], + "source": [ + "annotations = load_json(annotations_path)\n", + "print(f\"Loaded {len(annotations)} annotations\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5080fdc6", + "metadata": {}, + "outputs": [], + "source": [ + "def collect_label_annotations(annotations, target_labels):\n", + " \"\"\"Build minimal evaluation buckets keyed by label.\"\"\"\n", + " buckets_by_label = defaultdict(list)\n", + " target_labels = set(target_labels)\n", + "\n", + " for task in annotations:\n", + " for annotation in task.get(\"annotations\", []):\n", + " merged = {}\n", + " for result in annotation.get(\"result\", []):\n", + " result_id = result.get(\"id\")\n", + " value = result.get(\"value\", {})\n", + " slot = merged.setdefault(\n", + " result_id,\n", + " {\n", + " \"text\": value.get(\"text\"),\n", + " \"start\": value.get(\"start\"),\n", + " \"end\": value.get(\"end\"),\n", + " \"labels\": [],\n", + " \"choices\": [],\n", + " },\n", + " )\n", + "\n", + " # Update span info if present in the current result.\n", + " slot[\"text\"] = slot[\"text\"] or value.get(\"text\")\n", + " slot[\"start\"] = (\n", + " slot[\"start\"] if slot[\"start\"] is not None else value.get(\"start\")\n", + " )\n", + " slot[\"end\"] = (\n", + " slot[\"end\"] if slot[\"end\"] is not None else value.get(\"end\")\n", + " )\n", + " slot[\"labels\"].extend(value.get(\"labels\", []))\n", + " slot[\"choices\"].extend(value.get(\"choices\", []))\n", + "\n", + " for payload in merged.values():\n", + " labels = [\n", + " label for label in payload[\"labels\"] if label in target_labels\n", + " ]\n", + " if not labels or payload[\"text\"] is None:\n", + " continue\n", + "\n", + " item = {\n", + " \"text\": payload[\"text\"],\n", + " \"labels\": list(dict.fromkeys(payload[\"labels\"])),\n", + " \"choices\": list(dict.fromkeys(payload[\"choices\"])),\n", + " \"start\": payload[\"start\"],\n", + " \"end\": payload[\"end\"],\n", + " }\n", + "\n", + " for label in labels:\n", + " buckets_by_label[label].append(deepcopy(item))\n", + "\n", + " return {label: buckets_by_label.get(label, []) for label in target_labels}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4e9d0ab", + "metadata": {}, + "outputs": [], + "source": [ + "target_labels = [\n", + " \"CONDUCTA\",\n", + " \"CONDUCTA_DESCRIPCION\",\n", + " \"DETALLE\",\n", + " \"OBJETO_DE_LA_RESOLUCION\",\n", + "]\n", + "samples_by_label = collect_label_annotations(annotations, target_labels)" + ] + }, + { + "cell_type": "markdown", + "id": "ad8f304b", + "metadata": {}, + "source": [ + "## Exploratory data analysis\n", + "\n", + "We consolidate the annotated spans into a tabular view to inspect label and subcategory coverage before running any retrieval experiments.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d99c5871", + "metadata": {}, + "outputs": [], + "source": [ + "def samples_to_dataframe(samples):\n", + " rows = []\n", + " for label, items in samples.items():\n", + " for item in items:\n", + " rows.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"labels\": item[\"labels\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"n_choices\": len(item[\"choices\"]),\n", + " \"char_len\": len(item[\"text\"]),\n", + " }\n", + " )\n", + " return pd.DataFrame(rows)\n", + "\n", + "\n", + "samples_df = samples_to_dataframe(samples_by_label)\n", + "samples_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0eca77e", + "metadata": {}, + "outputs": [], + "source": [ + "samples_df[\"labels\"].map(len).describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbe01d41", + "metadata": {}, + "outputs": [], + "source": [ + "samples_df[\"choices\"].map(len).describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b898f23c", + "metadata": {}, + "outputs": [], + "source": [ + "samples_df.loc[samples_df[\"n_choices\"] == 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fefb83ad", + "metadata": {}, + "outputs": [], + "source": [ + "# Remove samples with zero choices\n", + "samples_df = samples_df[samples_df[\"n_choices\"] > 0].reset_index(drop=True)\n", + "samples_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2aa3550f", + "metadata": {}, + "outputs": [], + "source": [ + "for label, samples in samples_by_label.items():\n", + " print(f\"{label}: {len(samples)} samples before filtering\")\n", + " samples_by_label[label] = [\n", + " sample for sample in samples if len(sample[\"choices\"]) > 0\n", + " ]\n", + " print(f\"{label}: {len(samples_by_label[label])} samples after filtering\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12ff76ac", + "metadata": {}, + "outputs": [], + "source": [ + "samples_df.loc[samples_df[\"n_choices\"] > 1, [\"text\", \"choices\"]].explode(\n", + " \"choices\"\n", + ").drop_duplicates()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1bb2dfec", + "metadata": {}, + "outputs": [], + "source": [ + "label_counts = samples_df.groupby(\"label\").size().sort_values(ascending=False)\n", + "choice_counts = (\n", + " samples_df.explode(\"choices\").groupby([\"label\", \"choices\"]).size().rename(\"count\")\n", + ")\n", + "\n", + "summary_table = pd.DataFrame(\n", + " {\n", + " \"samples\": label_counts,\n", + " \"unique_choices\": choice_counts.groupby(\"label\").size(),\n", + " \"avg_text_len\": samples_df.groupby(\"label\")[\"char_len\"].mean().round(1),\n", + " }\n", + ")\n", + "summary_table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "729dc02e", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "order = label_counts.index\n", + "sns.barplot(x=label_counts.values, y=label_counts.index, ax=ax, palette=\"viridis\")\n", + "ax.set_title(\"Samples per label\")\n", + "ax.set_xlabel(\"Number of samples\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "plot_data = (\n", + " choice_counts.reset_index()\n", + " .sort_values(\"count\", ascending=False)\n", + " .groupby(\"label\")\n", + " .head(10)\n", + ")\n", + "\n", + "for label in order:\n", + " subset = plot_data.query(\"label == @label\").sort_values(\"count\", ascending=True)\n", + " fig, ax = plt.subplots(figsize=(8, 4))\n", + " sns.barplot(data=subset, x=\"count\", y=\"choices\", palette=\"viridis\", ax=ax)\n", + " ax.set_title(f\"Top subcategories for {label}\")\n", + " ax.set_xlabel(\"Frequency\")\n", + " ax.set_ylabel(\"Subcategory\")\n", + " ax.grid(axis=\"x\", alpha=0.2)\n", + " ax.invert_yaxis()\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61ef49f8", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "sns.boxplot(data=samples_df, x=\"label\", y=\"char_len\", palette=\"viridis\")\n", + "ax.set_title(\"Entity span length by label\")\n", + "ax.set_xlabel(\"Label\")\n", + "ax.set_ylabel(\"Character length\")\n", + "ax.set_ylim(0, 100)\n", + "plt.xticks(rotation=15)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a402956", + "metadata": {}, + "outputs": [], + "source": [ + "for label in label_counts.index:\n", + " print(f\"=== {label} ===\")\n", + " subset = samples_df.query(\"label == @label\")\n", + " for _, row in subset.sample(n=min(3, len(subset))).iterrows():\n", + " print(f\"- Text: {row['text']}\")\n", + " print(f\" Choices: {row['choices']}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "id": "535f6000", + "metadata": {}, + "source": [ + "## USEM pipeline audit\n", + "\n", + "We validate the existing TensorFlow-based USEM subcategorizer by reloading the cached assets, recomputing embeddings, and measuring retrieval accuracy before swapping in PyTorch alternatives." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51e8f65e", + "metadata": {}, + "outputs": [], + "source": [ + "pipeline_path = Path(\"/resources/pipelines/production/datapublic/pipeline.json\")\n", + "\n", + "with pipeline_path.open() as f:\n", + " pipeline_config = json.load(f)\n", + "\n", + "usem_configs = {\n", + " config[1][\"category\"]: {\n", + " \"subcategories_path\": config[1][\"subcategories_path\"],\n", + " \"response_embeddings_path\": config[1][\"response_embeddings_path\"],\n", + " }\n", + " for config in pipeline_config[\"postprocess\"]\n", + " if \"USEMSubcategorizer\" in config[0]\n", + "}\n", + "\n", + "usem_configs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ab1c51f", + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.transforms.entity_subcategories.usem import USEMSubcategorizer\n", + "\n", + "usem_models = {}\n", + "for label, cfg in usem_configs.items():\n", + " print(f\"Loading USEM assets for {label}…\")\n", + " usem_models[label] = USEMSubcategorizer(category=label, **cfg)\n", + "\n", + "list(usem_models.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d28103d8", + "metadata": {}, + "outputs": [], + "source": [ + "model = usem_models[\"CONDUCTA\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f07e07f9", + "metadata": {}, + "outputs": [], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0cd9987", + "metadata": {}, + "outputs": [], + "source": [ + "model.subcategories" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f51be1d", + "metadata": {}, + "outputs": [], + "source": [ + "# Sample encoding of first subcategory\n", + "encoded = model.usem.encode(\n", + " [model.subcategories[0].replace(\"_\", \" \")],\n", + " context_array=[model.subcategories[0].replace(\"_\", \" \")],\n", + " encoder_type=\"response_encoder\",\n", + ")\n", + "encoded" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "caa86d26", + "metadata": {}, + "outputs": [], + "source": [ + "def cosine_similarity(a, b):\n", + " a_norm = a / np.linalg.norm(a, axis=1, keepdims=True)\n", + " b_norm = b / np.linalg.norm(b, axis=1, keepdims=True)\n", + " return np.sum(a_norm * b_norm, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d5dc56a", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute cosine similarity matrix - first value should be 1\n", + "cosine_similarity(encoded, model.usem_vectors)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb470374", + "metadata": {}, + "outputs": [], + "source": [ + "import unicodedata\n", + "\n", + "\n", + "def normalize_text(text: str) -> str:\n", + " \"\"\"Normalize text by lowercasing and removing non-alphanumeric characters.\"\"\"\n", + " text = text.lower()\n", + " text = \"\".join(\n", + " char\n", + " for char in unicodedata.normalize(\"NFKD\", text)\n", + " if char.isalnum() or char.isspace()\n", + " )\n", + " return text\n", + "\n", + "\n", + "def normalize_subcategory(name: str) -> str:\n", + " \"\"\"Mirror the preprocessing applied when the cached embeddings were built.\"\"\"\n", + " # name = normalize_text(name)\n", + " return name.replace(\"_\", \" \")\n", + "\n", + "\n", + "def recompute_response_vectors(model):\n", + " \"\"\"Return normalized subcategory strings and freshly encoded vectors.\"\"\"\n", + " normalized = [\n", + " normalize_subcategory(subcategory) for subcategory in model.subcategories\n", + " ]\n", + " fresh_vectors = model.usem.encode(\n", + " normalized,\n", + " context_array=normalized,\n", + " encoder_type=\"response_encoder\",\n", + " )\n", + " return normalized, fresh_vectors.numpy()\n", + "\n", + "\n", + "cached = model.usem_vectors\n", + "normalized_subcategories, fresh = recompute_response_vectors(model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7362249", + "metadata": {}, + "outputs": [], + "source": [ + "def l2_normalize(matrix: np.ndarray) -> np.ndarray:\n", + " \"\"\"Row-wise L2 normalization to prepare cosine similarity checks.\"\"\"\n", + " return matrix / np.linalg.norm(matrix, axis=1, keepdims=True)\n", + "\n", + "\n", + "cached_norm = l2_normalize(cached)\n", + "fresh_norm = l2_normalize(fresh)\n", + "cosine_matrix = cached_norm @ fresh_norm.T\n", + "cosine_matrix.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a755a678", + "metadata": {}, + "outputs": [], + "source": [ + "row_max_indices = np.argmax(cosine_matrix, axis=1)\n", + "row_max_values = cosine_matrix[np.arange(len(row_max_indices)), row_max_indices]\n", + "mismatched_rows = np.where(row_max_indices != np.arange(len(row_max_indices)))[0]\n", + "diag_values = np.diag(cosine_matrix)\n", + "off_identity_matches = (\n", + " pd.DataFrame(\n", + " {\n", + " \"subcategory\": [model.subcategories[idx] for idx in mismatched_rows],\n", + " \"matched_index\": row_max_indices[mismatched_rows],\n", + " \"max_cosine\": row_max_values[mismatched_rows],\n", + " \"diagonal_cosine\": diag_values[mismatched_rows],\n", + " }\n", + " )\n", + " if mismatched_rows.size\n", + " else pd.DataFrame(\n", + " columns=[\n", + " \"subcategory\",\n", + " \"matched_index\",\n", + " \"max_cosine\",\n", + " \"diagonal_cosine\",\n", + " ]\n", + " )\n", + ")\n", + "off_identity_matches # .head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50f14d8c", + "metadata": {}, + "outputs": [], + "source": [ + "diag_values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a42e0a8", + "metadata": {}, + "outputs": [], + "source": [ + "tolerance = 0.99\n", + "near_identity_mask = diag_values >= tolerance\n", + "print(\n", + " f\"Diagonal entries at or above {tolerance:.2f}: {near_identity_mask.sum()} / {len(diag_values)}\"\n", + ")\n", + "diagonal_gaps = (\n", + " pd.DataFrame(\n", + " {\n", + " \"subcategory\": [\n", + " model.subcategories[idx]\n", + " for idx, ok in enumerate(near_identity_mask)\n", + " if not ok\n", + " ],\n", + " \"diagonal_cosine\": diag_values[~near_identity_mask],\n", + " \"shortfall\": 1 - diag_values[~near_identity_mask],\n", + " }\n", + " )\n", + " if (~near_identity_mask).any()\n", + " else pd.DataFrame(columns=[\"subcategory\", \"diagonal_cosine\", \"shortfall\"])\n", + ")\n", + "diagonal_gaps # .head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "129e8489", + "metadata": {}, + "outputs": [], + "source": [ + "embedding_checks = []\n", + "for label, model in usem_models.items():\n", + " cached = model.usem_vectors\n", + " normalized_subcategories, fresh = recompute_response_vectors(model)\n", + " cached_norm = l2_normalize(cached)\n", + " fresh_norm = l2_normalize(fresh)\n", + " delta = cached - fresh\n", + " row_l2 = np.linalg.norm(delta, axis=1)\n", + " cos = np.sum(cached_norm * fresh_norm, axis=1)\n", + " embedding_checks.append(\n", + " {\n", + " \"label\": label,\n", + " \"max_l2\": float(row_l2.max()),\n", + " \"mean_l2\": float(row_l2.mean()),\n", + " \"min_cos\": float(cos.min()),\n", + " \"mean_cos\": float(cos.mean()),\n", + " }\n", + " )\n", + "\n", + "embedding_checks_df = pd.DataFrame(embedding_checks).set_index(\"label\")\n", + "embedding_checks_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5c521a0", + "metadata": {}, + "outputs": [], + "source": [ + "# Using batch retrieval\n", + "def retrieve_samples(samples, models, top_k=5):\n", + " records = []\n", + " for label, items in samples.items():\n", + " model = models[label]\n", + " texts = [item[\"text\"] for item in items]\n", + " retrieved_lists = model.batch_retrieve(texts, top_k=top_k)\n", + " for item, retrieved in zip(items, retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + " return records\n", + "\n", + "\n", + "retrieval_records = retrieve_samples(samples_by_label, usem_models, top_k=5)\n", + "len(retrieval_records)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5278ead4", + "metadata": {}, + "outputs": [], + "source": [ + "# Filter out samples with no annotated choices\n", + "retrieval_records = [record for record in retrieval_records if record[\"choices\"]]\n", + "len(retrieval_records)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efbe083b", + "metadata": {}, + "outputs": [], + "source": [ + "def compute_topk_accuracy(records, ks=(1, 2, 3, 4, 5)):\n", + " metrics = []\n", + " labels = sorted({record[\"label\"] for record in records})\n", + " for label in labels:\n", + " label_records = [record for record in records if record[\"label\"] == label]\n", + " total = len(label_records)\n", + " for k in ks:\n", + " hits = sum(\n", + " any(choice in record[\"retrieved\"][:k] for choice in record[\"choices\"])\n", + " for record in label_records\n", + " )\n", + " metrics.append(\n", + " {\n", + " \"label\": label,\n", + " \"k\": k,\n", + " \"accuracy\": hits / total if total else np.nan,\n", + " }\n", + " )\n", + "\n", + " overall = []\n", + " for k in ks:\n", + " hits = sum(\n", + " any(choice in record[\"retrieved\"][:k] for choice in record[\"choices\"])\n", + " for record in records\n", + " )\n", + " overall.append({\"label\": \"OVERALL\", \"k\": k, \"accuracy\": hits / len(records)})\n", + "\n", + " metrics.extend(overall)\n", + " return pd.DataFrame(metrics)\n", + "\n", + "\n", + "baseline_accuracy = compute_topk_accuracy(retrieval_records)\n", + "baseline_accuracy_pivot = baseline_accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + ").sort_index()\n", + "\n", + "baseline_accuracy_pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa531a6c", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "\n", + "sns.heatmap(\n", + " baseline_accuracy_pivot.loc[\n", + " [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"USEM baseline top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "baseline_accuracy_pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f775ee51", + "metadata": {}, + "outputs": [], + "source": [ + "# For each label, show some random success cases where the correct choice was retrieved in top 5\n", + "for label in target_labels:\n", + " print(f\"=== {label} ===\")\n", + " label_records = [\n", + " record\n", + " for record in retrieval_records\n", + " if record[\"label\"] == label\n", + " and any(choice in record[\"retrieved\"][:5] for choice in record[\"choices\"])\n", + " ]\n", + " for record in random.sample(label_records, k=min(3, len(label_records))):\n", + " print(f\"- Text: {record['text']}\")\n", + " print(f\" Choices: {record['choices']}\")\n", + " print(f\" Retrieved: {record['retrieved'][:5]}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "441f8b7d", + "metadata": {}, + "outputs": [], + "source": [ + "# For each label, show some random failure cases where the correct choice was not retrieved in top 5\n", + "for label in target_labels:\n", + " print(f\"=== {label} ===\")\n", + " label_records = [\n", + " record\n", + " for record in retrieval_records\n", + " if record[\"label\"] == label\n", + " and not any(choice in record[\"retrieved\"][:5] for choice in record[\"choices\"])\n", + " ]\n", + " for record in random.sample(label_records, k=min(3, len(label_records))):\n", + " print(f\"- Text: {record['text']}\")\n", + " print(f\" Choices: {record['choices']}\")\n", + " print(f\" Retrieved: {record['retrieved'][:5]}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac517e27", + "metadata": {}, + "outputs": [], + "source": [ + "overall = baseline_accuracy_pivot.loc[\"OVERALL\"]\n", + "label_top1 = baseline_accuracy_pivot.loc[\n", + " [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"], 1\n", + "]\n", + "\n", + "summary_md = f\"\"\"\n", + "### Baseline findings\n", + "\n", + "- Overall top-1 accuracy: {overall[1]:.2%}\n", + "- Overall top-3 accuracy: {overall[3]:.2%}\n", + "- Overall top-5 accuracy: {overall[5]:.2%}\n", + "- Best-performing label (top-1): {label_top1.idxmax()} at {label_top1.max():.2%}\n", + "- Most challenging label (top-1): {label_top1.idxmin()} at {label_top1.min():.2%}\n", + "\n", + "#### Next steps\n", + "1. Replace the TensorFlow encoder with sentence-transformer checkpoints and repeat the evaluation.\n", + "2. Compare latency and memory footprint during retrieval for each contender.\n", + "3. Consolidate the winning model into the pipeline and update deployment assets.\n", + "\"\"\"\n", + "\n", + "Markdown(summary_md)" + ] + }, + { + "cell_type": "markdown", + "id": "4d794421", + "metadata": {}, + "source": [ + "## Random Retriever" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "250cabac", + "metadata": {}, + "outputs": [], + "source": [ + "class RandomRetriever:\n", + " def __init__(self, subcategories: list[str], seed: int = 42):\n", + " self.subcategories = subcategories\n", + " self._rng = random.Random(seed)\n", + "\n", + " def batch_retrieve(self, texts: list[str], top_k: int = 5) -> list[list[str]]:\n", + " k = min(top_k, len(self.subcategories))\n", + " if k == 0:\n", + " return [[] for _ in texts]\n", + " return [self._rng.sample(self.subcategories, k) for _ in texts]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f556454", + "metadata": {}, + "outputs": [], + "source": [ + "label_subcategories = {\n", + " label: model.subcategories for label, model in usem_models.items()\n", + "}\n", + "random_models = {\n", + " label: RandomRetriever(subcats, seed=idx * 17)\n", + " for idx, (label, subcats) in enumerate(label_subcategories.items())\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecc98fde", + "metadata": {}, + "outputs": [], + "source": [ + "baseline_variants = {\n", + " \"Random\": random_models,\n", + " # \"USEM\": usem_models,\n", + " # \"BM25\": bm25_models,\n", + " # \"USEM + BM25 hybrid\": hybrid_models,\n", + "}\n", + "\n", + "variant_records = {}\n", + "for variant, models in baseline_variants.items():\n", + " records = retrieve_samples(samples_by_label, models, top_k=5)\n", + " variant_records[variant] = records\n", + "\n", + "variant_accuracy = {\n", + " variant: compute_topk_accuracy(records)\n", + " for variant, records in variant_records.items()\n", + "}\n", + "variant_pivots = {\n", + " variant: df.pivot(index=\"label\", columns=\"k\", values=\"accuracy\").sort_index()\n", + " for variant, df in variant_accuracy.items()\n", + "}\n", + "\n", + "for variant, pivot in variant_pivots.items():\n", + " display(Markdown(f\"**{variant} baseline top-k accuracy**\"))\n", + " display(pivot)\n", + "\n", + "# combined_series = {\n", + "# \"TensorFlow USEM\": baseline_accuracy_pivot.loc[\"OVERALL\"],\n", + "# \"DistilUSE\": distiluse_accuracy_pivot.loc[\"OVERALL\"],\n", + "# \"MiniLM\": minilm_accuracy_pivot.loc[\"OVERALL\"],\n", + "# }\n", + "# combined_series.update(\n", + "# {variant: pivot.loc[\"OVERALL\"] for variant, pivot in variant_pivots.items()}\n", + "# )\n", + "# combined_accuracy = pd.DataFrame.from_dict(combined_series, orient=\"index\")\n", + "# combined_accuracy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "943672c8", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "\n", + "sns.heatmap(\n", + " variant_pivots[\"Random\"].loc[\n", + " [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"Random baseline top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "variant_pivots[\"Random\"].loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "f3ead018", + "metadata": {}, + "source": [ + "## BM25 retriever" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7b46e82", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import re\n", + "from collections import Counter\n", + "\n", + "token_pattern = re.compile(r\"\\w+\", re.UNICODE)\n", + "\n", + "\n", + "def tokenize(text: str) -> list[str]:\n", + " return token_pattern.findall(normalize_text(text))\n", + "\n", + "\n", + "class BM25Retriever:\n", + " def __init__(self, subcategories: list[str], k1: float = 1.2, b: float = 0.75):\n", + " self.subcategories = subcategories\n", + " normalized = [normalize_subcategory(sub) for sub in subcategories]\n", + " self.tokenized = [tokenize(text) for text in normalized]\n", + " self.doc_len = [len(tokens) for tokens in self.tokenized]\n", + " self.avgdl = sum(self.doc_len) / len(self.doc_len)\n", + " self.k1 = k1\n", + " self.b = b\n", + " self.N = len(self.tokenized)\n", + " self.doc_freqs = [Counter(tokens) for tokens in self.tokenized]\n", + " corpus_tokens = set().union(*self.doc_freqs)\n", + " self.df = Counter(\n", + " {\n", + " token: sum(1 for doc in self.doc_freqs if token in doc)\n", + " for token in corpus_tokens\n", + " }\n", + " )\n", + " self.idf = {\n", + " token: math.log(1 + (self.N - freq + 0.5) / (freq + 0.5))\n", + " for token, freq in self.df.items()\n", + " }\n", + "\n", + " def _score(self, tokens: list[str]) -> np.ndarray:\n", + " scores = np.zeros(self.N, dtype=np.float64)\n", + " for token in tokens:\n", + " idf = self.idf.get(token)\n", + " if idf is None:\n", + " continue\n", + " for idx, freq_map in enumerate(self.doc_freqs):\n", + " freq = freq_map.get(token, 0)\n", + " if freq == 0:\n", + " continue\n", + " denom = freq + self.k1 * (\n", + " 1 - self.b + self.b * self.doc_len[idx] / self.avgdl\n", + " )\n", + " scores[idx] += idf * freq * (self.k1 + 1) / denom\n", + " return scores\n", + "\n", + " def batch_retrieve(self, texts: list[str], top_k: int = 5) -> list[list[str]]:\n", + " return [self._topk(text, top_k) for text in texts]\n", + "\n", + " def _topk(self, text: str, top_k: int) -> list[str]:\n", + " scores = self._score(tokenize(text))\n", + " if top_k <= 0:\n", + " return []\n", + " top_indices = np.argsort(-scores)[:top_k]\n", + " return [self.subcategories[idx] for idx in top_indices]\n", + "\n", + " def score_vector(self, text: str) -> np.ndarray:\n", + " return self._score(tokenize(text))\n", + "\n", + "\n", + "class HybridRetriever:\n", + " def __init__(self, usem_model, bm25_model, bm25_weight: float = 0.5):\n", + " self.usem_model = usem_model\n", + " self.bm25_model = bm25_model\n", + " self.response_vectors = usem_model.usem_vectors\n", + " self.bm25_weight = bm25_weight\n", + "\n", + " def batch_retrieve(self, texts: list[str], top_k: int = 5) -> list[list[str]]:\n", + " query_vectors = self.usem_model.usem.batch_encode(\n", + " [normalize_text(text) for text in texts],\n", + " encoder_type=\"question_encoder\",\n", + " )\n", + " similarity = np.inner(query_vectors, self.response_vectors)\n", + " combined_results = []\n", + " for row_idx, text in enumerate(texts):\n", + " bm25_scores = self.bm25_model.score_vector(text)\n", + " bm25_norm = bm25_scores / (bm25_scores.max() + 1e-9)\n", + " sim_scores = similarity[row_idx]\n", + " sim_norm = sim_scores / (sim_scores.max() + 1e-9)\n", + " combined = self.bm25_weight * bm25_norm + (1 - self.bm25_weight) * sim_norm\n", + " k = min(top_k, combined.shape[0])\n", + " top_indices = np.argsort(-combined)[:k]\n", + " combined_results.append(\n", + " [self.bm25_model.subcategories[idx] for idx in top_indices]\n", + " )\n", + " return combined_results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b629aa3", + "metadata": {}, + "outputs": [], + "source": [ + "bm25_models = {\n", + " label: BM25Retriever(subcats) for label, subcats in label_subcategories.items()\n", + "}\n", + "hybrid_models = {\n", + " label: HybridRetriever(usem_models[label], bm25_models[label], bm25_weight=0.4)\n", + " for label in usem_models\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccfc969e", + "metadata": {}, + "outputs": [], + "source": [ + "baseline_variants = {\n", + " \"Random\": random_models,\n", + " \"BM25\": bm25_models,\n", + " \"USEM + BM25 hybrid\": hybrid_models,\n", + "}\n", + "\n", + "variant_records = {}\n", + "for variant, models in baseline_variants.items():\n", + " records = retrieve_samples(samples_by_label, models, top_k=5)\n", + " variant_records[variant] = records\n", + "\n", + "variant_accuracy = {\n", + " variant: compute_topk_accuracy(records)\n", + " for variant, records in variant_records.items()\n", + "}\n", + "variant_pivots = {\n", + " variant: df.pivot(index=\"label\", columns=\"k\", values=\"accuracy\").sort_index()\n", + " for variant, df in variant_accuracy.items()\n", + "}\n", + "\n", + "for variant, pivot in variant_pivots.items():\n", + " display(Markdown(f\"**{variant} baseline top-k accuracy**\"))\n", + " display(pivot)\n", + "\n", + " fig, ax = plt.subplots()\n", + "\n", + " sns.heatmap(\n", + " pivot.loc[\n", + " [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + " )\n", + " ax.set_title(f\"{variant} baseline top-k accuracy by label\")\n", + " ax.set_xlabel(\"k\")\n", + " ax.set_ylabel(\"Label\")\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ffbdfc8a", + "metadata": {}, + "source": [ + "## Challenger models: PyTorch sentence-transformers\n", + "\n", + "We now evaluate two PyTorch-based encoders as potential replacements for the TensorFlow USEM model:\n", + "- **DistilUSE**: Knowledge-distilled USE producing 512-dimensional embeddings\n", + "- **MultilingualMiniLM**: Higher-quality MiniLM producing 384-dimensional embeddings\n", + "\n", + "Both models support Apple Silicon (M1/M2/M3) via MPS backend and eliminate the TensorFlow dependency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f22169f", + "metadata": {}, + "outputs": [], + "source": [ + "from aymurai.models.usem.sentence_transformers_encoder import (\n", + " DistilUSEEncoder,\n", + " MultilingualMiniLMEncoder,\n", + ")\n", + "\n", + "# Initialize challenger encoders\n", + "distiluse_encoder = DistilUSEEncoder()\n", + "minilm_encoder = MultilingualMiniLMEncoder()\n", + "\n", + "challenger_encoders = {\n", + " \"DistilUSE\": distiluse_encoder,\n", + " \"MiniLM\": minilm_encoder,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "016fcb42", + "metadata": {}, + "outputs": [], + "source": [ + "from hashlib import md5\n", + "\n", + "from aymurai.utils.download import download\n", + "from aymurai.utils.misc import is_url\n", + "\n", + "\n", + "class SentenceTransformerRetriever:\n", + " def __init__(\n", + " self,\n", + " label,\n", + " subcategories_path,\n", + " encoder,\n", + " encoder_name=\"challenger\",\n", + " batch_size=256,\n", + " ):\n", + " self.label = label\n", + " self.encoder = encoder\n", + " self.batch_size = batch_size\n", + " self.encoder_name = encoder_name\n", + " self.subcategories = self._load_subcategories(subcategories_path)\n", + " normalized_subcategories = [\n", + " normalize_subcategory(sub) for sub in self.subcategories\n", + " ]\n", + " self.response_vectors = self.encoder.batch_encode(\n", + " normalized_subcategories,\n", + " encoder_type=\"response_encoder\",\n", + " batch_size=batch_size,\n", + " )\n", + "\n", + " def _load_subcategories(self, path):\n", + " resolved_path = Path(path)\n", + " if is_url(path):\n", + " cache_root = Path(\n", + " os.getenv(\"AYMURAI_CACHE_BASEPATH\", \"/resources/cache/aymurai\")\n", + " )\n", + " target_dir = cache_root / \"sentence_transformer_retriever\"\n", + " target_dir.mkdir(parents=True, exist_ok=True)\n", + " target_path = target_dir / md5(path.encode(\"utf-8\")).hexdigest()\n", + " resolved_path = Path(download(path, str(target_path)))\n", + " if not resolved_path.exists():\n", + " raise FileNotFoundError(\n", + " f\"Subcategory file not found for {self.label}: {resolved_path}\"\n", + " )\n", + " with resolved_path.open() as file:\n", + " return [line.strip() for line in file if line.strip()]\n", + "\n", + " def batch_retrieve(self, texts, top_k=5):\n", + " if not texts:\n", + " return []\n", + "\n", + " query_vectors = self.encoder.batch_encode(\n", + " texts,\n", + " encoder_type=\"question_encoder\",\n", + " batch_size=self.batch_size,\n", + " )\n", + " similarity = np.inner(query_vectors, self.response_vectors)\n", + " k = min(top_k, similarity.shape[1])\n", + " top_indices = np.argsort(-similarity, axis=1)[:, :k]\n", + " return [[self.subcategories[idx] for idx in row] for row in top_indices]\n", + "\n", + "\n", + "class HybridSentenceTransformerRetriever:\n", + " def __init__(self, base_retriever, bm25_model, bm25_weight: float = 0.4):\n", + " self.base_retriever = base_retriever\n", + " self.bm25_model = bm25_model\n", + " self.bm25_weight = bm25_weight\n", + " self.encoder = base_retriever.encoder\n", + " self.response_vectors = base_retriever.response_vectors\n", + " self.subcategories = base_retriever.subcategories\n", + " self.batch_size = base_retriever.batch_size\n", + "\n", + " def batch_retrieve(self, texts, top_k=5):\n", + " if not texts:\n", + " return []\n", + "\n", + " query_vectors = self.encoder.batch_encode(\n", + " texts,\n", + " encoder_type=\"question_encoder\",\n", + " batch_size=self.batch_size,\n", + " )\n", + " similarity = np.inner(query_vectors, self.response_vectors)\n", + " combined_results = []\n", + " for row_idx, text in enumerate(texts):\n", + " bm25_scores = self.bm25_model.score_vector(text)\n", + " bm25_norm = bm25_scores / (bm25_scores.max() + 1e-9)\n", + " sim_scores = similarity[row_idx]\n", + " sim_norm = sim_scores / (sim_scores.max() + 1e-9)\n", + " combined = self.bm25_weight * bm25_norm + (1 - self.bm25_weight) * sim_norm\n", + " k = min(top_k, combined.shape[0])\n", + " top_indices = np.argsort(-combined)[:k]\n", + " combined_results.append([self.subcategories[idx] for idx in top_indices])\n", + " return combined_results\n", + "\n", + "\n", + "def build_challenger_models(usem_configs, encoder, encoder_name, batch_size=256):\n", + " return {\n", + " label: SentenceTransformerRetriever(\n", + " label=label,\n", + " subcategories_path=cfg[\"subcategories_path\"],\n", + " encoder=encoder,\n", + " encoder_name=encoder_name,\n", + " batch_size=batch_size,\n", + " )\n", + " for label, cfg in usem_configs.items()\n", + " }\n", + "\n", + "\n", + "def build_hybrid_sentence_transformer_models(base_models, bm25_models, bm25_weight=0.4):\n", + " return {\n", + " label: HybridSentenceTransformerRetriever(\n", + " base_retriever=base_models[label],\n", + " bm25_model=bm25_models[label],\n", + " bm25_weight=bm25_weight,\n", + " )\n", + " for label in base_models\n", + " }\n", + "\n", + "\n", + "def evaluate_sentence_transformers(samples, models, top_k=5):\n", + " records = []\n", + " for label, items in samples.items():\n", + " retriever = models[label]\n", + " texts = [item[\"text\"] for item in items]\n", + " retrieved_lists = retriever.batch_retrieve(texts, top_k=top_k)\n", + " for item, retrieved in zip(items, retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + " return records\n", + "\n", + "\n", + "distiluse_models = build_challenger_models(usem_configs, distiluse_encoder, \"DistilUSE\")\n", + "minilm_models = build_challenger_models(usem_configs, minilm_encoder, \"MiniLM\")" + ] + }, + { + "cell_type": "markdown", + "id": "4847338b", + "metadata": {}, + "source": [ + "### DistilUSE evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3baaedfb", + "metadata": {}, + "outputs": [], + "source": [ + "distiluse_retrieval_records = evaluate_sentence_transformers(\n", + " samples_by_label,\n", + " distiluse_models,\n", + " top_k=5,\n", + ")\n", + "distiluse_accuracy = compute_topk_accuracy(distiluse_retrieval_records)\n", + "distiluse_accuracy_pivot = distiluse_accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + ").sort_index()\n", + "\n", + "distiluse_accuracy_pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30162b63", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " distiluse_accuracy_pivot.loc[\n", + " [label for label in distiluse_accuracy_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"DistilUSE top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "distiluse_accuracy_pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "a13c5d69", + "metadata": {}, + "source": [ + "### MultilingualMiniLM evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd0e59f1", + "metadata": {}, + "outputs": [], + "source": [ + "minilm_retrieval_records = evaluate_sentence_transformers(\n", + " samples_by_label,\n", + " minilm_models,\n", + " top_k=5,\n", + ")\n", + "minilm_accuracy = compute_topk_accuracy(minilm_retrieval_records)\n", + "minilm_accuracy_pivot = minilm_accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + ").sort_index()\n", + "\n", + "minilm_accuracy_pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e9fe5a7", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " minilm_accuracy_pivot.loc[\n", + " [label for label in minilm_accuracy_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"MultilingualMiniLM top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "minilm_accuracy_pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "4593cd14", + "metadata": {}, + "source": [ + "### Hybrid (BM25 + encoder) evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e58869d", + "metadata": {}, + "outputs": [], + "source": [ + "distiluse_hybrid_models = build_hybrid_sentence_transformer_models(\n", + " distiluse_models,\n", + " bm25_models,\n", + " bm25_weight=0.4,\n", + ")\n", + "minilm_hybrid_models = build_hybrid_sentence_transformer_models(\n", + " minilm_models,\n", + " bm25_models,\n", + " bm25_weight=0.4,\n", + ")\n", + "\n", + "distiluse_hybrid_records = evaluate_sentence_transformers(\n", + " samples_by_label,\n", + " distiluse_hybrid_models,\n", + " top_k=5,\n", + ")\n", + "distiluse_hybrid_accuracy = compute_topk_accuracy(distiluse_hybrid_records)\n", + "distiluse_hybrid_pivot = distiluse_hybrid_accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + ").sort_index()\n", + "\n", + "distiluse_hybrid_pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4c103f8", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " distiluse_hybrid_pivot.loc[\n", + " [label for label in distiluse_hybrid_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"DistilUSE + BM25 hybrid top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "distiluse_hybrid_pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c249abb", + "metadata": {}, + "outputs": [], + "source": [ + "minilm_hybrid_records = evaluate_sentence_transformers(\n", + " samples_by_label,\n", + " minilm_hybrid_models,\n", + " top_k=5,\n", + ")\n", + "minilm_hybrid_accuracy = compute_topk_accuracy(minilm_hybrid_records)\n", + "minilm_hybrid_pivot = minilm_hybrid_accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + ").sort_index()\n", + "\n", + "minilm_hybrid_pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5333bf04", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " minilm_hybrid_pivot.loc[\n", + " [label for label in minilm_hybrid_pivot.index if label != \"OVERALL\"]\n", + " ],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"MiniLM + BM25 hybrid top-k accuracy by label\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "minilm_hybrid_pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "6e4f967a", + "metadata": {}, + "source": [ + "## Model comparison and champion selection\n", + "\n", + "We compare the three models across all metrics to select the best replacement for the TensorFlow USEM implementation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ef3d532", + "metadata": {}, + "outputs": [], + "source": [ + "# Consolidate OVERALL accuracy across all models\n", + "comparison_df = pd.DataFrame(\n", + " {\n", + " \"TensorFlow USEM\": baseline_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"DistilUSE\": distiluse_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"MiniLM\": minilm_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"TensorFlow USEM + BM25\": variant_pivots[\"USEM + BM25 hybrid\"].loc[\"OVERALL\"],\n", + " \"DistilUSE + BM25\": distiluse_hybrid_pivot.loc[\"OVERALL\"],\n", + " \"MiniLM + BM25\": minilm_hybrid_pivot.loc[\"OVERALL\"],\n", + " }\n", + ").T\n", + "\n", + "comparison_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d90c448", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize comparison across k values as bar chart, sorted by performance at each k\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "melted = comparison_df.reset_index().melt(\n", + " id_vars=\"index\", var_name=\"k\", value_name=\"accuracy\"\n", + ")\n", + "\n", + "sns.barplot(\n", + " data=melted.sort_values(by=\"accuracy\", ascending=False),\n", + " x=\"k\",\n", + " y=\"accuracy\",\n", + " hue=\"index\",\n", + " ax=ax,\n", + ")\n", + "\n", + "ax.set_title(\"Top-k Accuracy Comparison Across Models\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_ylim(0, 1)\n", + "ax.legend(title=\"Model\", bbox_to_anchor=(1.05, 1), loc=\"upper left\")\n", + "ax.grid(axis=\"y\", alpha=0.3)\n", + "plt.xticks(rotation=0)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b42a256", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize comparison across k values as bar chart, sorted by performance at each k\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "melted = comparison_df.reset_index().melt(\n", + " id_vars=\"index\", var_name=\"k\", value_name=\"accuracy\"\n", + ")\n", + "# Drop TF USEM + BM25 variant for clarity\n", + "melted = melted[melted[\"index\"] != \"TensorFlow USEM + BM25\"]\n", + "\n", + "sns.barplot(\n", + " data=melted.sort_values(by=\"accuracy\", ascending=False),\n", + " x=\"k\",\n", + " y=\"accuracy\",\n", + " hue=\"index\",\n", + " ax=ax,\n", + ")\n", + "\n", + "ax.set_title(\"Top-k Accuracy Comparison Across Models\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_ylim(0, 1)\n", + "ax.legend(title=\"Model\", bbox_to_anchor=(1.05, 1), loc=\"upper left\")\n", + "ax.grid(axis=\"y\", alpha=0.3)\n", + "plt.xticks(rotation=0)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ff3390f", + "metadata": {}, + "outputs": [], + "source": [ + "# Per-label comparison for top-1 accuracy\n", + "label_index = [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"]\n", + "label_comparison = pd.DataFrame(\n", + " {\n", + " \"TensorFlow USEM\": baseline_accuracy_pivot.loc[label_index, 1],\n", + " \"DistilUSE\": distiluse_accuracy_pivot.loc[label_index, 1],\n", + " \"MiniLM\": minilm_accuracy_pivot.loc[label_index, 1],\n", + " \"TensorFlow USEM + BM25\": variant_pivots[\"USEM + BM25 hybrid\"].loc[\n", + " label_index, 1\n", + " ],\n", + " \"DistilUSE + BM25\": distiluse_hybrid_pivot.loc[label_index, 1],\n", + " \"MiniLM + BM25\": minilm_hybrid_pivot.loc[label_index, 1],\n", + " }\n", + ")\n", + "\n", + "label_comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34902f2c", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize comparison across labels as bar chart, sorted by performance at each label\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "melted = label_comparison.reset_index().melt(\n", + " id_vars=\"label\", var_name=\"Model\", value_name=\"accuracy\"\n", + ")\n", + "\n", + "sns.barplot(\n", + " data=melted.sort_values(by=[\"label\", \"accuracy\"], ascending=False),\n", + " x=\"label\",\n", + " y=\"accuracy\",\n", + " hue=\"Model\",\n", + " ax=ax,\n", + ")\n", + "\n", + "ax.set_title(\"Top-1 Accuracy Comparison by Label\")\n", + "ax.set_xlabel(\"Label\")\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_ylim(0, 1)\n", + "ax.legend(title=\"Model\", bbox_to_anchor=(1.05, 1), loc=\"upper left\")\n", + "ax.grid(axis=\"y\", alpha=0.3)\n", + "plt.xticks(rotation=45, ha=\"right\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "149f195b", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize comparison across labels as bar chart, sorted by performance at each label\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "melted = label_comparison.reset_index().melt(\n", + " id_vars=\"label\", var_name=\"Model\", value_name=\"accuracy\"\n", + ")\n", + "\n", + "for label in melted[\"label\"].unique():\n", + " subset = melted[melted[\"label\"] == label].sort_values(\n", + " by=\"accuracy\", ascending=False\n", + " )\n", + " sns.barplot(\n", + " data=subset,\n", + " x=\"label\",\n", + " y=\"accuracy\",\n", + " hue=\"Model\",\n", + " ax=ax,\n", + " )\n", + "\n", + "# Deduplicate legend entries and draw once after plotting\n", + "handles, labels = ax.get_legend_handles_labels()\n", + "by_label = dict(zip(labels, handles))\n", + "ax.legend(\n", + " by_label.values(),\n", + " by_label.keys(),\n", + " title=\"Model\",\n", + " bbox_to_anchor=(1.05, 1),\n", + " loc=\"upper left\",\n", + ")\n", + "\n", + "ax.set_title(\"Top-1 Accuracy Comparison by Label\")\n", + "ax.set_xlabel(\"Label\")\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_ylim(0, 1)\n", + "ax.grid(axis=\"y\", alpha=0.3)\n", + "plt.xticks(rotation=45, ha=\"right\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3af7d0a0", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize comparison across labels as bar chart, sorted by performance at each label\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "melted = label_comparison.reset_index().melt(\n", + " id_vars=\"label\", var_name=\"Model\", value_name=\"accuracy\"\n", + ")\n", + "\n", + "# Drop TF USEM + BM25 variant for clarity\n", + "melted = melted[melted[\"Model\"] != \"TensorFlow USEM + BM25\"]\n", + "\n", + "for label in melted[\"label\"].unique():\n", + " subset = melted[melted[\"label\"] == label].sort_values(\n", + " by=\"accuracy\", ascending=False\n", + " )\n", + " sns.barplot(\n", + " data=subset,\n", + " x=\"label\",\n", + " y=\"accuracy\",\n", + " hue=\"Model\",\n", + " ax=ax,\n", + " )\n", + "\n", + "# Deduplicate legend entries and draw once after plotting\n", + "handles, labels = ax.get_legend_handles_labels()\n", + "by_label = dict(zip(labels, handles))\n", + "ax.legend(\n", + " by_label.values(),\n", + " by_label.keys(),\n", + " title=\"Model\",\n", + " bbox_to_anchor=(1.05, 1),\n", + " loc=\"upper left\",\n", + ")\n", + "\n", + "ax.set_title(\"Top-1 Accuracy Comparison by Label\")\n", + "ax.set_xlabel(\"Label\")\n", + "ax.set_ylabel(\"Accuracy\")\n", + "ax.set_ylim(0, 1)\n", + "ax.grid(axis=\"y\", alpha=0.3)\n", + "plt.xticks(rotation=45, ha=\"right\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9aefccac", + "metadata": {}, + "outputs": [], + "source": [ + "# Determine champion model\n", + "top1_scores = comparison_df[1]\n", + "champion_model = top1_scores.idxmax()\n", + "champion_score = top1_scores.max()\n", + "\n", + "# Calculate performance delta vs baseline\n", + "baseline_score = top1_scores[\"TensorFlow USEM\"]\n", + "challengers = top1_scores.drop(\"TensorFlow USEM\")\n", + "\n", + "performance_summary = pd.DataFrame(\n", + " {\n", + " \"Model\": comparison_df.index,\n", + " \"Top-1\": comparison_df[1].values,\n", + " \"Top-3\": comparison_df[3].values,\n", + " \"Top-5\": comparison_df[5].values,\n", + " \"Δ vs Baseline (Top-1)\": [\n", + " 0.0 if model == \"TensorFlow USEM\" else (score - baseline_score)\n", + " for model, score in zip(comparison_df.index, comparison_df[1].values)\n", + " ],\n", + " }\n", + ").set_index(\"Model\")\n", + "\n", + "performance_summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4a72754", + "metadata": {}, + "outputs": [], + "source": [ + "final_summary_md = f\"\"\"\n", + "## Champion Model Selection\n", + "\n", + "### Performance Summary\n", + "\n", + "{performance_summary.to_markdown()}\n", + "\n", + "### Winner: **{champion_model}**\n", + "\n", + "- **Top-1 Accuracy**: {champion_score:.2%}\n", + "- **Performance vs Baseline**: {(champion_score - baseline_score):.2%} ({\"+\" if champion_score >= baseline_score else \"\"}{((champion_score - baseline_score) / baseline_score * 100):.1f}%)\n", + "\n", + "### Key Findings\n", + "\n", + "1. **{champion_model}** achieves the highest top-1 accuracy at {champion_score:.2%}\n", + "2. All PyTorch models eliminate TensorFlow dependency and support Apple Silicon (MPS)\n", + "3. Both challenger models maintain competitive accuracy while offering:\n", + " - Cross-platform compatibility (Linux, macOS, Windows)\n", + " - Apple Silicon acceleration via MPS backend\n", + " - Smaller model footprint and faster inference\n", + "\n", + "### Implementation Recommendations\n", + "\n", + "1. **Replace TensorFlow USEM** with **{champion_model}** in production pipeline\n", + "2. Update `USEMSubcategorizer` to use the selected PyTorch encoder by default\n", + "3. Regenerate cached embeddings using the champion encoder\n", + "4. Update deployment documentation to reflect new dependencies\n", + "5. Verify inference latency and memory footprint in production environment\n", + "\n", + "### Next Steps\n", + "\n", + "- [ ] Implement {champion_model} as default encoder in `aymurai.models.usem`\n", + "- [ ] Regenerate response embeddings for all label categories\n", + "- [ ] Update pipeline configuration files\n", + "- [ ] Run integration tests across all supported platforms\n", + "- [ ] Update deployment scripts and documentation\n", + "\"\"\"\n", + "\n", + "Markdown(final_summary_md)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64638e96", + "metadata": {}, + "outputs": [], + "source": [ + "# Best-performing model per label and k (excluding TensorFlow USEM + BM25)\n", + "label_index = [label for label in baseline_accuracy_pivot.index if label != \"OVERALL\"]\n", + "ks = [k for k in range(1, 6) if k in baseline_accuracy_pivot.columns]\n", + "\n", + "model_pivots = {\n", + " \"TensorFlow USEM\": baseline_accuracy_pivot,\n", + " \"DistilUSE\": distiluse_accuracy_pivot,\n", + " \"MiniLM\": minilm_accuracy_pivot,\n", + " \"DistilUSE + BM25\": distiluse_hybrid_pivot,\n", + " \"MiniLM + BM25\": minilm_hybrid_pivot,\n", + "}\n", + "\n", + "records = []\n", + "for model_name, pivot in model_pivots.items():\n", + " for label in label_index:\n", + " for k in ks:\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"k\": k,\n", + " \"model\": model_name,\n", + " \"accuracy\": pivot.loc[label, k],\n", + " }\n", + " )\n", + "\n", + "scores = pd.DataFrame(records)\n", + "best_models = scores.loc[scores.groupby([\"label\", \"k\"])[\"accuracy\"].idxmax()]\n", + "model_ranking = (\n", + " best_models[\"model\"].value_counts().rename_axis(\"model\").reset_index(name=\"wins\")\n", + ")\n", + "\n", + "best_models.reset_index(drop=True)\n", + "model_ranking" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fd27e32", + "metadata": {}, + "outputs": [], + "source": [ + "scores.query(\"k == 1\").sort_values([\"label\", \"accuracy\"], ascending=[True, False])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06b5a254", + "metadata": {}, + "outputs": [], + "source": [ + "scores.query(\"k == 3\").sort_values([\"label\", \"accuracy\"], ascending=[True, False])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b20b3ac", + "metadata": {}, + "outputs": [], + "source": [ + "scores.query(\"k == 5\").sort_values([\"label\", \"accuracy\"], ascending=[True, False])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "977f446a", + "metadata": {}, + "outputs": [], + "source": [ + "# Grid search BM25 weights for DistilUSE and MiniLM hybrids\n", + "weight_grid = [round(w, 2) for w in np.linspace(0.0, 0.9, 10)]\n", + "weight_grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11f9300a", + "metadata": {}, + "outputs": [], + "source": [ + "search_results = []\n", + "pivot_lookup = {}\n", + "\n", + "# Cache texts per label\n", + "label_texts = {\n", + " label: [item[\"text\"] for item in items] for label, items in samples_by_label.items()\n", + "}\n", + "\n", + "# Cache BM25 score vectors per label to avoid recomputing across weights\n", + "bm25_cache = {\n", + " label: [bm25_models[label].score_vector(text) for text in texts]\n", + " for label, texts in label_texts.items()\n", + "}\n", + "\n", + "# Cache question embeddings per model and label\n", + "query_cache = {}\n", + "for model_name, base_models in {\n", + " \"DistilUSE\": distiluse_models,\n", + " \"MiniLM\": minilm_models,\n", + "}.items():\n", + " any_model = next(iter(base_models.values()))\n", + " encoder = any_model.encoder\n", + " batch_size = any_model.batch_size\n", + " query_cache[model_name] = {\n", + " label: encoder.batch_encode(\n", + " texts, encoder_type=\"question_encoder\", batch_size=batch_size\n", + " )\n", + " for label, texts in label_texts.items()\n", + " }\n", + "\n", + "\n", + "def hybrid_retrieve_cached(\n", + " base_retriever,\n", + " bm25_model,\n", + " texts,\n", + " query_vectors,\n", + " bm25_scores,\n", + " bm25_weight,\n", + " top_k=5,\n", + "):\n", + " similarity = np.inner(query_vectors, base_retriever.response_vectors)\n", + " combined_results = []\n", + " for idx, text in enumerate(texts):\n", + " bm25_vec = bm25_scores[idx]\n", + " bm25_norm = bm25_vec / (bm25_vec.max() + 1e-9)\n", + " sim_scores = similarity[idx]\n", + " sim_norm = sim_scores / (sim_scores.max() + 1e-9)\n", + " combined = bm25_weight * bm25_norm + (1 - bm25_weight) * sim_norm\n", + " k = min(top_k, combined.shape[0])\n", + " top_indices = np.argsort(-combined)[:k]\n", + " combined_results.append([bm25_model.subcategories[i] for i in top_indices])\n", + " return combined_results\n", + "\n", + "\n", + "for model_name, base_models in {\n", + " \"DistilUSE\": distiluse_models,\n", + " \"MiniLM\": minilm_models,\n", + "}.items():\n", + " for weight in weight_grid:\n", + " records = []\n", + " for label, base_retriever in base_models.items():\n", + " texts = label_texts[label]\n", + " query_vectors = query_cache[model_name][label]\n", + " bm25_scores = bm25_cache[label]\n", + " retrieved_lists = hybrid_retrieve_cached(\n", + " base_retriever=base_retriever,\n", + " bm25_model=bm25_models[label],\n", + " texts=texts,\n", + " query_vectors=query_vectors,\n", + " bm25_scores=bm25_scores,\n", + " bm25_weight=weight,\n", + " top_k=5,\n", + " )\n", + " for item, retrieved in zip(samples_by_label[label], retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + " accuracy = compute_topk_accuracy(records)\n", + " pivot = accuracy.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + " ).sort_index()\n", + " pivot_lookup[(model_name, weight)] = pivot\n", + " overall = pivot.loc[\"OVERALL\"]\n", + " search_results.append(\n", + " {\n", + " \"model\": model_name,\n", + " \"bm25_weight\": weight,\n", + " \"top1\": overall.get(1, np.nan),\n", + " \"top3\": overall.get(3, np.nan),\n", + " \"top5\": overall.get(5, np.nan),\n", + " }\n", + " )\n", + "\n", + "search_df = pd.DataFrame(search_results)\n", + "\n", + "# Best weight per model by top-1 accuracy\n", + "best_rows = search_df.loc[search_df.groupby(\"model\")[\"top1\"].idxmax()].reset_index(\n", + " drop=True\n", + ")\n", + "best_pivots = {\n", + " row.model: pivot_lookup[(row.model, row.bm25_weight)]\n", + " for _, row in best_rows.iterrows()\n", + "}\n", + "\n", + "best_rows" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3ade77d", + "metadata": {}, + "outputs": [], + "source": [ + "search_df" + ] + }, + { + "cell_type": "markdown", + "id": "23d4f97e", + "metadata": {}, + "source": [ + "### Hybrid (tuned BM25 + encoder) evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7323a3c", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluate hybrids at tuned BM25 weights\n", + "\n", + "tuned_records = {}\n", + "tuned_pivots = {}\n", + "\n", + "for row in best_rows.itertuples():\n", + " model_name = row.model\n", + " weight = row.bm25_weight\n", + " base_models = distiluse_models if model_name == \"DistilUSE\" else minilm_models\n", + "\n", + " records = []\n", + " for label, base_retriever in base_models.items():\n", + " texts = label_texts[label]\n", + " query_vectors = query_cache[model_name][label]\n", + " bm25_scores = bm25_cache[label]\n", + " retrieved_lists = hybrid_retrieve_cached(\n", + " base_retriever=base_retriever,\n", + " bm25_model=bm25_models[label],\n", + " texts=texts,\n", + " query_vectors=query_vectors,\n", + " bm25_scores=bm25_scores,\n", + " bm25_weight=weight,\n", + " top_k=5,\n", + " )\n", + " for item, retrieved in zip(samples_by_label[label], retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + "\n", + " accuracy = compute_topk_accuracy(records)\n", + " pivot = accuracy.pivot(index=\"label\", columns=\"k\", values=\"accuracy\").sort_index()\n", + " tuned_records[model_name] = records\n", + " tuned_pivots[model_name] = pivot\n", + "\n", + "# Show tuned weights and overall performance\n", + "best_rows_display = best_rows.copy()\n", + "for model_name, pivot in tuned_pivots.items():\n", + " best_rows_display.loc[best_rows_display[\"model\"] == model_name, \"top1\"] = pivot.loc[\n", + " \"OVERALL\", 1\n", + " ]\n", + " best_rows_display.loc[best_rows_display[\"model\"] == model_name, \"top3\"] = pivot.loc[\n", + " \"OVERALL\", 3\n", + " ]\n", + " best_rows_display.loc[best_rows_display[\"model\"] == model_name, \"top5\"] = pivot.loc[\n", + " \"OVERALL\", 5\n", + " ]\n", + "\n", + "best_rows_display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86348779", + "metadata": {}, + "outputs": [], + "source": [ + "# Full pipeline re-evaluation with evaluate_sentence_transformers at tuned weights\n", + "\n", + "best_weights = best_rows.set_index(\"model\")[\"bm25_weight\"].to_dict()\n", + "\n", + "# Build tuned hybrid retrievers (standard pipeline, no caching shortcuts)\n", + "tuned_hybrid_models = {\n", + " \"DistilUSE + BM25 tuned\": build_hybrid_sentence_transformer_models(\n", + " distiluse_models,\n", + " bm25_models,\n", + " bm25_weight=float(best_weights[\"DistilUSE\"]),\n", + " ),\n", + " \"MiniLM + BM25 tuned\": build_hybrid_sentence_transformer_models(\n", + " minilm_models, bm25_models, bm25_weight=float(best_weights[\"MiniLM\"])\n", + " ),\n", + "}\n", + "\n", + "# Run evaluation pipeline\n", + "attribution = {}\n", + "tuned_hybrid_records = {}\n", + "tuned_hybrid_accuracy = {}\n", + "tuned_hybrid_pivots = {}\n", + "\n", + "for name, models in tuned_hybrid_models.items():\n", + " records = evaluate_sentence_transformers(samples_by_label, models, top_k=5)\n", + " tuned_hybrid_records[name] = records\n", + " df = compute_topk_accuracy(records)\n", + " tuned_hybrid_accuracy[name] = df\n", + " tuned_hybrid_pivots[name] = df.pivot(\n", + " index=\"label\", columns=\"k\", values=\"accuracy\"\n", + " ).sort_index()\n", + "\n", + "# Overall comparison table including tuned hybrids\n", + "comparison_df_tuned = pd.DataFrame(\n", + " {\n", + " \"TensorFlow USEM\": baseline_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"TensorFlow USEM + BM25\": variant_pivots[\"USEM + BM25 hybrid\"].loc[\"OVERALL\"],\n", + " \"DistilUSE\": distiluse_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"MiniLM\": minilm_accuracy_pivot.loc[\"OVERALL\"],\n", + " \"DistilUSE + BM25 tuned\": tuned_hybrid_pivots[\"DistilUSE + BM25 tuned\"].loc[\n", + " \"OVERALL\"\n", + " ],\n", + " \"MiniLM + BM25 tuned\": tuned_hybrid_pivots[\"MiniLM + BM25 tuned\"].loc[\n", + " \"OVERALL\"\n", + " ],\n", + " }\n", + ").T\n", + "\n", + "comparison_df_tuned.sort_values(by=1, ascending=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8bdb7199", + "metadata": {}, + "outputs": [], + "source": [ + "# Heatmaps per label for each model\n", + "pivot_map = {\n", + " \"TensorFlow USEM\": baseline_accuracy_pivot,\n", + " \"TensorFlow USEM + BM25\": variant_pivots[\"USEM + BM25 hybrid\"],\n", + " \"DistilUSE\": distiluse_accuracy_pivot,\n", + " \"MiniLM\": minilm_accuracy_pivot,\n", + " \"DistilUSE + BM25 tuned\": tuned_hybrid_pivots[\"DistilUSE + BM25 tuned\"],\n", + " \"MiniLM + BM25 tuned\": tuned_hybrid_pivots[\"MiniLM + BM25 tuned\"],\n", + "}\n", + "\n", + "labels_no_overall = [label for label in label_index]\n", + "\n", + "for name, pivot in pivot_map.items():\n", + " fig, ax = plt.subplots(figsize=(6, 4))\n", + " sns.heatmap(\n", + " pivot.loc[labels_no_overall],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " vmin=0,\n", + " vmax=1,\n", + " ax=ax,\n", + " )\n", + " ax.set_title(f\"{name} top-k accuracy by label\")\n", + " ax.set_xlabel(\"k\")\n", + " ax.set_ylabel(\"Label\")\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "961d15ad", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai (3.10.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/sentence-encoders/03-sentence-transformer-subcategorizer-smoke.ipynb b/notebooks/experiments/sentence-encoders/03-sentence-transformer-subcategorizer-smoke.ipynb new file mode 100644 index 00000000..8b84293e --- /dev/null +++ b/notebooks/experiments/sentence-encoders/03-sentence-transformer-subcategorizer-smoke.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0364d1dd", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ab7b227", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "\n", + "from aymurai.models.sentence_encoder.base import BaseSentenceEncoder\n", + "from aymurai.transforms.entity_subcategories.sentence_transformer import (\n", + " SentenceTransformerSubcategorizer,\n", + ")\n", + "\n", + "\n", + "class FakeEncoder(BaseSentenceEncoder):\n", + " def _vec(self, text: str) -> np.ndarray:\n", + " # Deterministic 8-dim vector from hash\n", + " h = abs(hash(text))\n", + " return np.array([(h >> i) & 0xFF for i in range(0, 64, 8)], dtype=np.float32)\n", + "\n", + " def encode(self, text_array, encoder_type, context_array=None):\n", + " return self.batch_encode(text_array, encoder_type)\n", + "\n", + " def batch_encode(self, text_array, encoder_type, batch_size: int = 256):\n", + " texts = list(text_array)\n", + " return np.vstack([self._vec(self.normalize_text(t)) for t in texts])\n", + "\n", + "\n", + "tmpdir = Path(tempfile.mkdtemp())\n", + "subcats_path = tmpdir / \"subcats.txt\"\n", + "subcats_path.write_text(\"\\n\".join([\"alpha\", \"beta\", \"gamma\"]))\n", + "\n", + "# BM25 + encoder path\n", + "hybrid_embeddings = tmpdir / \"embeddings_hybrid.npz\"\n", + "hybrid = SentenceTransformerSubcategorizer(\n", + " category=\"CONDUCTA\",\n", + " embeddings_path=str(hybrid_embeddings),\n", + " encoder_name=\"distiluse\",\n", + " bm25_weight=0.5,\n", + " encoder=FakeEncoder(),\n", + ")\n", + "print(hybrid.batch_retrieve([\"alpha case\", \"beta story\"], top_k=2))\n", + "\n", + "# Encoder-only path (bm25_weight = 0)\n", + "encoder_only_embeddings = tmpdir / \"embeddings_encoder_only.npz\"\n", + "encoder_only = SentenceTransformerSubcategorizer(\n", + " category=\"CONDUCTA\",\n", + " embeddings_path=str(encoder_only_embeddings),\n", + " encoder_name=\"distiluse\",\n", + " bm25_weight=0.0,\n", + " encoder=FakeEncoder(),\n", + " rebuild_embeddings=True,\n", + ")\n", + "print(encoder_only.batch_retrieve([\"gamma topic\"], top_k=2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c3bf286", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai (3.10.19)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/sentence-encoders/04-sentence-transformer-subcategorizer-eval.ipynb b/notebooks/experiments/sentence-encoders/04-sentence-transformer-subcategorizer-eval.ipynb new file mode 100644 index 00000000..1c271ecf --- /dev/null +++ b/notebooks/experiments/sentence-encoders/04-sentence-transformer-subcategorizer-eval.ipynb @@ -0,0 +1,420 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7071ca80", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aacdfd29", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from collections import defaultdict\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "\n", + "from aymurai.transforms.entity_subcategories.sentence_transformer import (\n", + " SentenceTransformerSubcategorizer,\n", + ")\n", + "from aymurai.utils.json_data import load_json\n", + "\n", + "sns.set_theme(style=\"whitegrid\")\n", + "plt.rcParams.update(\n", + " {\"figure.figsize\": (10, 6), \"axes.titlesize\": 14, \"axes.labelsize\": 12}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b102ccf", + "metadata": {}, + "outputs": [], + "source": [ + "# Config\n", + "pipeline_path = Path(\"/resources/pipelines/production/datapublic/pipeline.json\")\n", + "annotations_path = Path(\n", + " \"/resources/annotations/label-studio/resos-annotations/30-nov/project-3-at-2022-11-30-16-04-2b43bf39.json\"\n", + ")\n", + "target_labels = [\n", + " \"CONDUCTA\",\n", + " \"CONDUCTA_DESCRIPCION\",\n", + " \"DETALLE\",\n", + " \"OBJETO_DE_LA_RESOLUCION\",\n", + "]\n", + "encoder_name = \"distiluse\"\n", + "bm25_weight = 0.5 # use tuned default; set 0.0 for encoder-only\n", + "batch_size = 256\n", + "embeddings_dir = Path(\"/resources/cache/aymurai\")\n", + "embeddings_dir.mkdir(parents=True, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe9a836a", + "metadata": {}, + "outputs": [], + "source": [ + "def collect_label_annotations(annotations, target_labels):\n", + " buckets_by_label = defaultdict(list)\n", + " target_labels = set(target_labels)\n", + "\n", + " for task in annotations:\n", + " for annotation in task.get(\"annotations\", []):\n", + " merged = {}\n", + " for result in annotation.get(\"result\", []):\n", + " result_id = result.get(\"id\")\n", + " value = result.get(\"value\", {})\n", + " slot = merged.setdefault(\n", + " result_id,\n", + " {\n", + " \"text\": value.get(\"text\"),\n", + " \"start\": value.get(\"start\"),\n", + " \"end\": value.get(\"end\"),\n", + " \"labels\": [],\n", + " \"choices\": [],\n", + " },\n", + " )\n", + " slot[\"text\"] = slot[\"text\"] or value.get(\"text\")\n", + " slot[\"start\"] = (\n", + " slot[\"start\"] if slot[\"start\"] is not None else value.get(\"start\")\n", + " )\n", + " slot[\"end\"] = (\n", + " slot[\"end\"] if slot[\"end\"] is not None else value.get(\"end\")\n", + " )\n", + " slot[\"labels\"].extend(value.get(\"labels\", []))\n", + " slot[\"choices\"].extend(value.get(\"choices\", []))\n", + "\n", + " for payload in merged.values():\n", + " labels = [\n", + " label for label in payload[\"labels\"] if label in target_labels\n", + " ]\n", + " if not labels or payload[\"text\"] is None:\n", + " continue\n", + " item = {\n", + " \"text\": payload[\"text\"],\n", + " \"labels\": list(dict.fromkeys(payload[\"labels\"])),\n", + " \"choices\": list(dict.fromkeys(payload[\"choices\"])),\n", + " \"start\": payload[\"start\"],\n", + " \"end\": payload[\"end\"],\n", + " }\n", + " for label in labels:\n", + " buckets_by_label[label].append(item)\n", + " return {label: buckets_by_label.get(label, []) for label in target_labels}\n", + "\n", + "\n", + "def compute_topk_accuracy(records, ks=(1, 2, 3, 4, 5)):\n", + " metrics = []\n", + " labels = sorted({record[\"label\"] for record in records})\n", + " for label in labels:\n", + " label_records = [r for r in records if r[\"label\"] == label]\n", + " total = len(label_records)\n", + " for k in ks:\n", + " hits = sum(\n", + " any(choice in r[\"retrieved\"][:k] for choice in r[\"choices\"])\n", + " for r in label_records\n", + " )\n", + " metrics.append(\n", + " {\"label\": label, \"k\": k, \"accuracy\": hits / total if total else np.nan}\n", + " )\n", + " overall = []\n", + " for k in ks:\n", + " hits = sum(\n", + " any(choice in r[\"retrieved\"][:k] for choice in r[\"choices\"])\n", + " for r in records\n", + " )\n", + " overall.append(\n", + " {\n", + " \"label\": \"OVERALL\",\n", + " \"k\": k,\n", + " \"accuracy\": hits / len(records) if records else np.nan,\n", + " }\n", + " )\n", + " metrics.extend(overall)\n", + " return pd.DataFrame(metrics)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64556a79", + "metadata": {}, + "outputs": [], + "source": [ + "# Load annotations and collect samples\n", + "annotations = load_json(str(annotations_path))\n", + "samples_by_label = collect_label_annotations(annotations, target_labels)\n", + "# filter out empty-choice samples\n", + "for label in list(samples_by_label):\n", + " samples_by_label[label] = [s for s in samples_by_label[label] if s.get(\"choices\")]\n", + "\n", + "\n", + "def samples_to_dataframe(samples):\n", + " rows = []\n", + " for label, items in samples.items():\n", + " for item in items:\n", + " rows.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"n_choices\": len(item[\"choices\"]),\n", + " \"char_len\": len(item[\"text\"]),\n", + " }\n", + " )\n", + " return pd.DataFrame(rows)\n", + "\n", + "\n", + "samples_df = samples_to_dataframe(samples_by_label)\n", + "samples_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a93a2087", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract sentence-transformer config from pipeline (or fall back to targets)\n", + "with pipeline_path.open() as f:\n", + " pipeline_config = json.load(f)\n", + "\n", + "st_configs = {\n", + " config[1][\"category\"]: {\n", + " \"embeddings_path\": config[1].get(\"embeddings_path\"),\n", + " }\n", + " for config in pipeline_config.get(\"postprocess\", [])\n", + " if \"SentenceTransformerSubcategorizer\" in config[0]\n", + "}\n", + "\n", + "# Fallback: build configs from target_labels when pipeline does not define them\n", + "if not st_configs:\n", + " st_configs = {\n", + " label: {\"embeddings_path\": embeddings_dir / f\"{label.lower()}.npz\"}\n", + " for label in target_labels\n", + " }\n", + "\n", + "st_configs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fdf84cf", + "metadata": {}, + "outputs": [], + "source": [ + "# Build sentence-transformer subcategorizer models (hybrid by default)\n", + "models = {}\n", + "for label, cfg in st_configs.items():\n", + " emb_path = cfg.get(\"embeddings_path\") or (embeddings_dir / f\"{label.lower()}.npz\")\n", + " models[label] = SentenceTransformerSubcategorizer(\n", + " category=label,\n", + " embeddings_path=str(emb_path),\n", + " encoder_name=encoder_name,\n", + " bm25_weight=bm25_weight,\n", + " batch_size=batch_size,\n", + " rebuild_embeddings=True,\n", + " )\n", + "list(models.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e51e5b92", + "metadata": {}, + "outputs": [], + "source": [ + "# Re run to check loading existing embeddings\n", + "models = {}\n", + "for label, cfg in st_configs.items():\n", + " emb_path = cfg.get(\"embeddings_path\") or (embeddings_dir / f\"{label.lower()}.npz\")\n", + " models[label] = SentenceTransformerSubcategorizer(\n", + " category=label,\n", + " embeddings_path=str(emb_path),\n", + " encoder_name=encoder_name,\n", + " bm25_weight=bm25_weight,\n", + " batch_size=batch_size,\n", + " rebuild_embeddings=False,\n", + " )\n", + "list(models.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a384a261", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluate\n", + "records = []\n", + "for label, items in samples_by_label.items():\n", + " retriever = models[label]\n", + " texts = [item[\"text\"] for item in items]\n", + " retrieved_lists = retriever.batch_retrieve(texts, top_k=5)\n", + " for item, retrieved in zip(items, retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + "\n", + "accuracy_df = compute_topk_accuracy(records)\n", + "pivot = accuracy_df.pivot(index=\"label\", columns=\"k\", values=\"accuracy\").sort_index()\n", + "pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdc73175", + "metadata": {}, + "outputs": [], + "source": [ + "# Heatmap\n", + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " pivot.loc[[label for label in pivot.index if label != \"OVERALL\"]],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"SentenceTransformerSubcategorizer (distiluse) top-k accuracy\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a99ad20", + "metadata": {}, + "outputs": [], + "source": [ + "encoder_name = \"minilm\"\n", + "\n", + "# Build sentence-transformer subcategorizer models (hybrid by default)\n", + "models = {}\n", + "for label, cfg in st_configs.items():\n", + " emb_path = cfg.get(\"embeddings_path\") or (embeddings_dir / f\"{label.lower()}.npz\")\n", + " models[label] = SentenceTransformerSubcategorizer(\n", + " category=label,\n", + " embeddings_path=str(emb_path),\n", + " encoder_name=encoder_name,\n", + " bm25_weight=bm25_weight,\n", + " batch_size=batch_size,\n", + " rebuild_embeddings=True,\n", + " )\n", + "list(models.keys())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5c8bf7e", + "metadata": {}, + "outputs": [], + "source": [ + "# Evaluate\n", + "records = []\n", + "for label, items in samples_by_label.items():\n", + " retriever = models[label]\n", + " texts = [item[\"text\"] for item in items]\n", + " retrieved_lists = retriever.batch_retrieve(texts, top_k=5)\n", + " for item, retrieved in zip(items, retrieved_lists):\n", + " records.append(\n", + " {\n", + " \"label\": label,\n", + " \"text\": item[\"text\"],\n", + " \"choices\": item[\"choices\"],\n", + " \"retrieved\": retrieved,\n", + " }\n", + " )\n", + "\n", + "accuracy_df = compute_topk_accuracy(records)\n", + "pivot = accuracy_df.pivot(index=\"label\", columns=\"k\", values=\"accuracy\").sort_index()\n", + "pivot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3700072", + "metadata": {}, + "outputs": [], + "source": [ + "# Heatmap\n", + "fig, ax = plt.subplots()\n", + "sns.heatmap(\n", + " pivot.loc[[label for label in pivot.index if label != \"OVERALL\"]],\n", + " annot=True,\n", + " fmt=\".2f\",\n", + " cmap=\"YlGnBu\",\n", + " ax=ax,\n", + ")\n", + "ax.set_title(\"SentenceTransformerSubcategorizer (minilm) top-k accuracy\")\n", + "ax.set_xlabel(\"k\")\n", + "ax.set_ylabel(\"Label\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "pivot.loc[[\"OVERALL\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9c46485", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "aymurai (3.10.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiments/subcategories/00-exploration.ipynb b/notebooks/experiments/subcategories/00-exploration.ipynb index f29849cc..348323b0 100644 --- a/notebooks/experiments/subcategories/00-exploration.ipynb +++ b/notebooks/experiments/subcategories/00-exploration.ipynb @@ -31,6 +31,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "os.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\"" ] }, @@ -77,13 +78,13 @@ "metadata": {}, "outputs": [], "source": [ - "from aymurai.spacy.display import DocRender\n", - "from aymurai.datasets.ar_juz_pcyf_10.docs import ArgentinaJuzgadoPCyF10DocsDataset\n", + "from aymurai.datasets.ar_juz_pcyf_10.docs import (\n", + " ArgentinaJuzgadoPCyF10DocsDataset,\n", + ")\n", "from aymurai.datasets.ar_juz_pcyf_10.annotations import (\n", " ArgentinaJuzgadoPCyF10LabelStudioAnnotations,\n", ")\n", "\n", - "render = DocRender()\n", "docs = ArgentinaJuzgadoPCyF10LabelStudioAnnotations(\n", " \"/resources/data/restricted/annotations/20221130-bis/\"\n", ").data\n", @@ -772,13 +773,13 @@ "metadata": {}, "outputs": [], "source": [ - "with open('./conduct_options.txt', 'r') as file:\n", + "with open(\"./conduct_options.txt\", \"r\") as file:\n", " conduct_categories = file.read().splitlines()\n", - "with open('./conduct_desc_options.txt', 'r') as file:\n", + "with open(\"./conduct_desc_options.txt\", \"r\") as file:\n", " conduct_desc_categories = file.read().splitlines()\n", - "with open('./detail_options.txt', 'r') as file:\n", + "with open(\"./detail_options.txt\", \"r\") as file:\n", " detail_categories = file.read().splitlines()\n", - "with open('./object_options.txt', 'r') as file:\n", + "with open(\"./object_options.txt\", \"r\") as file:\n", " object_categories = file.read().splitlines()" ] }, @@ -801,7 +802,10 @@ "from aymurai.models.flair.core import FlairModel\n", "from aymurai.models.flair.utils import FlairTextNormalize\n", "from aymurai.transforms.entity_subcategories.regex import RegexSubcategorizer\n", - "from aymurai.transforms.entity_subcategories.usem import USEMSubcategorizePipeline, USEMSubcategorizer\n", + "from aymurai.transforms.entity_subcategories.usem import (\n", + " USEMSubcategorizePipeline,\n", + " USEMSubcategorizer,\n", + ")\n", "\n", "config = {\n", " \"preprocess\": [\n", @@ -821,39 +825,39 @@ " (RegexSubcategorizer, {}),\n", " (\n", " USEMSubcategorizer,\n", - " {\n", - " \"category\": \"CONDUCTA\",\n", - " # \"subcategories\": conduct_categories,\n", - " \"subcategories_path\": './conduct_options.txt',\n", - " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/conduct_embeddings.npy\",\n", - " }\n", + " {\n", + " \"category\": \"CONDUCTA\",\n", + " # \"subcategories\": conduct_categories,\n", + " \"subcategories_path\": \"./conduct_options.txt\",\n", + " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/conduct_embeddings.npy\",\n", + " },\n", " ),\n", " (\n", " USEMSubcategorizer,\n", - " {\n", - " \"category\": \"CONDUCTA_DESCRIPCION\",\n", - " # \"subcategories\": conduct_descr_categories,\n", - " \"subcategories_path\": './conduct_desc_options.txt',\n", - " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/conduct_descr_embeddings.npy\",\n", - " }\n", + " {\n", + " \"category\": \"CONDUCTA_DESCRIPCION\",\n", + " # \"subcategories\": conduct_descr_categories,\n", + " \"subcategories_path\": \"./conduct_desc_options.txt\",\n", + " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/conduct_descr_embeddings.npy\",\n", + " },\n", " ),\n", " (\n", " USEMSubcategorizer,\n", - " {\n", - " \"category\": \"DETALLE\",\n", - " # \"subcategories\": detail_categories,\n", - " \"subcategories_path\": './detail_options.txt',\n", - " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/detail_embeddings.npy\",\n", - " }\n", + " {\n", + " \"category\": \"DETALLE\",\n", + " # \"subcategories\": detail_categories,\n", + " \"subcategories_path\": \"./detail_options.txt\",\n", + " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/detail_embeddings.npy\",\n", + " },\n", " ),\n", " (\n", " USEMSubcategorizer,\n", - " {\n", - " \"category\": \"OBJETO_DE_LA_RESOLUCION\",\n", - " \"subcategories_path\": './object_options.txt',\n", - " # \"subcategories\": object_categories,\n", - " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/object_embeddings.npy\",\n", - " }\n", + " {\n", + " \"category\": \"OBJETO_DE_LA_RESOLUCION\",\n", + " \"subcategories_path\": \"./object_options.txt\",\n", + " # \"subcategories\": object_categories,\n", + " \"response_embeddings_path\": \"/resources/pipelines/examples/flair-simple/UsemEmbeddings/object_embeddings.npy\",\n", + " },\n", " ),\n", " ],\n", " # \"multiprocessing\": {},\n", @@ -871,7 +875,7 @@ "source": [ "pipeline = AymurAIPipeline(config)\n", "results = pipeline.preprocess(sample[:1])\n", - "results = pipeline.predict(results)\n" + "results = pipeline.predict(results)" ] }, { @@ -881,7 +885,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "pp = pipeline.postprocess(results)" ] }, @@ -948,7 +951,7 @@ "# # \"multiprocessing\": {},\n", "# \"use_cache\": False,\n", "# # 'log_level': 'debug'\n", - "# }\n" + "# }" ] }, { @@ -959,7 +962,7 @@ "outputs": [], "source": [ "# pipeline = AymurAIPipeline(config)\n", - "# " + "#" ] }, { @@ -969,9 +972,8 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# preprocess = pipeline.preprocess(sample)\n", - "# results = pipeline.predict(preprocess)\n" + "# results = pipeline.predict(preprocess)" ] }, { @@ -981,7 +983,7 @@ "metadata": {}, "outputs": [], "source": [ - "# \n", + "#\n", "postprocess = pipeline.postprocess(results)" ] }, @@ -1029,7 +1031,11 @@ "outputs": [], "source": [ "for label in [\"VIOLENCIA_DE_GENERO\"]:\n", - " __ = [_ for _ in results[idx][\"predictions\"][\"entities\"] if _[\"label\"] == label]\n", + " __ = [\n", + " _\n", + " for _ in results[idx][\"predictions\"][\"entities\"]\n", + " if _[\"label\"] == label\n", + " ]\n", " print(label)\n", " display(__)\n", " print(\"\\n\\n\")" @@ -1044,8 +1050,17 @@ }, "outputs": [], "source": [ - "for label in [\"CONDUCTA\", \"CONDUCTA_DESCRIPCION\", \"DETALLE\", \"OBJETO_DE_LA_RESOLUCION\"]:\n", - " __ = [_ for _ in results[idx][\"predictions\"][\"entities\"] if _[\"label\"] == label]\n", + "for label in [\n", + " \"CONDUCTA\",\n", + " \"CONDUCTA_DESCRIPCION\",\n", + " \"DETALLE\",\n", + " \"OBJETO_DE_LA_RESOLUCION\",\n", + "]:\n", + " __ = [\n", + " _\n", + " for _ in results[idx][\"predictions\"][\"entities\"]\n", + " if _[\"label\"] == label\n", + " ]\n", " print(label)\n", " display(__)\n", " print(\"\\n\\n\")" @@ -1068,8 +1083,9 @@ "source": [ "import re\n", "import pandas as pd\n", + "\n", "pd.set_option(\"display.max_rows\", 500)\n", - "pd.set_option('max_colwidth', 400)" + "pd.set_option(\"max_colwidth\", 400)" ] }, { @@ -1080,7 +1096,7 @@ "outputs": [], "source": [ "path = \"/resources/ner/flair/resos-20221130-no-decision/\"\n", - "df = pd.read_csv(path+\"train.txt\", sep=\"\\s\", header=None)\n", + "df = pd.read_csv(path + \"train.txt\", sep=\"\\s\", header=None)\n", "df.columns = [\"token\", \"label\"]\n", "df.head()" ] @@ -1127,7 +1143,7 @@ "\n", "def normalize_text(text: str) -> str:\n", " text = unidecode.unidecode(text.lower())\n", - " text = re.sub(r'[_\\-,.;:]+', '', text)\n", + " text = re.sub(r\"[_\\-,.;:]+\", \"\", text)\n", " return text" ] }, @@ -1167,7 +1183,7 @@ "\n", " if len(output_dict[\"choices\"]) == 1:\n", " output_dict[\"choices\"] = output_dict[\"choices\"][0]\n", - " \n", + "\n", " return output_dict" ] }, @@ -1190,8 +1206,10 @@ "outputs": [], "source": [ "viol_gen = [\n", - " pred_ent for pred_ent in pred_ents if pred_ent[\"label\"] == \"VIOLENCIA_DE_GENERO\"\n", - "]\n" + " pred_ent\n", + " for pred_ent in pred_ents\n", + " if pred_ent[\"label\"] == \"VIOLENCIA_DE_GENERO\"\n", + "]" ] }, { @@ -1240,8 +1258,8 @@ " for violence_type, pattern in violence_type_patterns.items():\n", " if re.search(pattern, normalized_pred):\n", " found_types.add(violence_type)\n", - " \n", - " return list(found_types)\n" + "\n", + " return list(found_types)" ] }, { @@ -1455,7 +1473,7 @@ " if re.search(pattern, normalized_pred):\n", " found_modalities.add(modality)\n", "\n", - " return list(found_modalities)\n" + " return list(found_modalities)" ] }, { @@ -1582,7 +1600,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"RELACION_Y_TIPO_ENTRE_ACUSADO/A_Y_DENUNCIANTE\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -1596,7 +1614,9 @@ "source": [ "relationship_choices = []\n", "for sample in annots:\n", - " relationship = list(filter(filter_relationship_choices, sample[\"annotations\"][0][\"result\"]))\n", + " relationship = list(\n", + " filter(filter_relationship_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " relationship_choices.extend(relationship)" ] }, @@ -1718,7 +1738,7 @@ "source": [ "def find_relationship_type(pred: str) -> list:\n", " normalized_pred = normalize_text(pred)\n", - " \n", + "\n", " relationship_type_patterns = {\n", " \"familiar\": r\"(?:familiar?|hij[ao]s?|[mp]adres?|herman[ao]s?)\",\n", " \"pareja\": r\"(? list:\n", " normalized_pred = normalize_text(pred)\n", - " \n", + "\n", " resolution_type_patterns = {\n", " \"interlocutoria\": r\"interlocut[oa]ria\",\n", " \"definitiva\": r\"definitiv[ao]\",\n", " }\n", - " \n", + "\n", " found_types = set()\n", - " \n", + "\n", " for res_type, pattern in resolution_type_patterns.items():\n", " if re.search(pattern, normalized_pred):\n", " found_types.add(res_type)\n", - " \n", + "\n", " return list(found_types)" ] }, @@ -2058,7 +2080,9 @@ }, "outputs": [], "source": [ - "resolution_texts.loc[(resolution_texts[\"types\"].map(len) == 0)].sort_values(\"text\")" + "resolution_texts.loc[(resolution_texts[\"types\"].map(len) == 0)].sort_values(\n", + " \"text\"\n", + ")" ] }, { @@ -2149,7 +2173,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"NIVEL_INSTRUCCION_CAT\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -2163,7 +2187,9 @@ "source": [ "education_choices = []\n", "for sample in annots:\n", - " education = list(filter(filter_education_choices, sample[\"annotations\"][0][\"result\"]))\n", + " education = list(\n", + " filter(filter_education_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " education_choices.extend(education)" ] }, @@ -2273,7 +2299,7 @@ "source": [ "def find_education_level(pred: str) -> list:\n", " normalized_pred = normalize_text(pred)\n", - " \n", + "\n", " education_level_patterns = {\n", " # \"sin_instruccion\": r\"\",\n", " \"sin_escolarizar\": r\"(?:no (?:posee|termino)|sin).+estudios?\",\n", @@ -2290,13 +2316,13 @@ " \"universitario_completo\": r\"(?:universitari[ao]s? (?!in)complet[ao]s?|(? 0)].drop_duplicates(\"text\")" + "education_texts.loc[(education_texts[\"types\"].map(len) > 0)].drop_duplicates(\n", + " \"text\"\n", + ")" ] }, { @@ -2348,7 +2376,9 @@ }, "outputs": [], "source": [ - "education_texts.loc[(education_texts[\"types\"].map(len) == 0)].sort_values(\"text\")" + "education_texts.loc[(education_texts[\"types\"].map(len) == 0)].sort_values(\n", + " \"text\"\n", + ")" ] }, { @@ -2360,7 +2390,9 @@ }, "outputs": [], "source": [ - "education_texts.loc[(education_texts[\"types\"].map(len) > 1)].sort_values(\"text\")" + "education_texts.loc[(education_texts[\"types\"].map(len) > 1)].sort_values(\n", + " \"text\"\n", + ")" ] }, { @@ -2450,7 +2482,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"GENERO_CAT\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -2464,7 +2496,9 @@ "source": [ "gender_choices = []\n", "for sample in annots:\n", - " gender = list(filter(filter_gender_choices, sample[\"annotations\"][0][\"result\"]))\n", + " gender = list(\n", + " filter(filter_gender_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " gender_choices.extend(gender)" ] }, @@ -2574,7 +2608,7 @@ "source": [ "def find_gender(pred: str) -> list:\n", " normalized_pred = normalize_text(pred)\n", - " \n", + "\n", " gender_patterns = {\n", " \"varon_cis\": r\"varon(?:es)? cis\",\n", " \"mujer_cis\": r\"mujer(?:es)? cis\",\n", @@ -2583,13 +2617,13 @@ " \"travesti\": r\"travesti\",\n", " \"no_binaria\": r\"no ?binari[aoex]\",\n", " }\n", - " \n", + "\n", " found_gender = set()\n", - " \n", + "\n", " for gender, pattern in gender_patterns.items():\n", " if re.search(pattern, normalized_pred):\n", " found_gender.add(gender)\n", - " \n", + "\n", " return list(found_gender)" ] }, @@ -2695,7 +2729,9 @@ "metadata": {}, "outputs": [], "source": [ - "not_determined[\"beginning\"] = not_determined[\"label\"].str.startswith(\"B\").astype(int)\n", + "not_determined[\"beginning\"] = (\n", + " not_determined[\"label\"].str.startswith(\"B\").astype(int)\n", + ")\n", "not_determined.head(10)" ] }, @@ -2743,7 +2779,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"PERSONA_ACUSADA_NO_DETERMINADA\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -2757,7 +2793,11 @@ "source": [ "not_determined_choices = []\n", "for sample in annots:\n", - " not_determined = list(filter(filter_not_determined_choices, sample[\"annotations\"][0][\"result\"]))\n", + " not_determined = list(\n", + " filter(\n", + " filter_not_determined_choices, sample[\"annotations\"][0][\"result\"]\n", + " )\n", + " )\n", " not_determined_choices.extend(not_determined)" ] }, @@ -2869,7 +2909,7 @@ "source": [ "def find_gender(pred: str) -> list:\n", " normalized_pred = normalize_text(pred)\n", - " \n", + "\n", " gender_patterns = {\n", " \"varon_cis\": r\"varon(?:es)? cis\",\n", " \"mujer_cis\": r\"mujer(?:es)? cis\",\n", @@ -2878,13 +2918,13 @@ " \"travesti\": r\"travesti\",\n", " \"no_binaria\": r\"no ?binari[aoex]\",\n", " }\n", - " \n", + "\n", " found_gender = set()\n", - " \n", + "\n", " for gender, pattern in gender_patterns.items():\n", " if re.search(pattern, normalized_pred):\n", " found_gender.add(gender)\n", - " \n", + "\n", " return list(found_gender)" ] }, @@ -3038,7 +3078,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"CONDUCTA\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -3052,7 +3092,9 @@ "source": [ "conduct_choices = []\n", "for sample in annots:\n", - " conduct = list(filter(filter_conduct_choices, sample[\"annotations\"][0][\"result\"]))\n", + " conduct = list(\n", + " filter(filter_conduct_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " conduct_choices.extend(conduct)" ] }, @@ -3091,7 +3133,7 @@ " group_key=\"choices\",\n", " sort_key=\"choices\",\n", " )\n", - ")\n" + ")" ] }, { @@ -3389,7 +3431,7 @@ " \"violar_inhabilitacion_para_conducir\",\n", " \"violar_reglamentacion_juego\",\n", " \"zanjas_y_pozos_en_via_publica\",\n", - "]\n" + "]" ] }, { @@ -3425,7 +3467,7 @@ "# categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))\n", "\n", "categories_embeddings = usem_qa.signatures[\"response_encoder\"](\n", - " input=tf.constant(categories), context=tf.constant(categories)\n", + " input=tf.constant(categories), context=tf.constant(categories)\n", ")\n", "\n", "# categories_embeddings = usem(categories)" @@ -3480,7 +3522,7 @@ "metadata": {}, "outputs": [], "source": [ - "#texts_embeddings = usem(texts)\n", + "# texts_embeddings = usem(texts)\n", "# texts_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(texts))" ] }, @@ -3543,7 +3585,7 @@ " top_3_similar = similarities.loc[text].sort_values(ascending=False)[:3]\n", " print(text)\n", " display(top_3_similar)\n", - " print(\"=\"*100)" + " print(\"=\" * 100)" ] }, { @@ -3553,13 +3595,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -3610,8 +3662,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3653,8 +3709,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3686,8 +3746,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3729,13 +3793,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -3786,8 +3860,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3829,8 +3907,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3862,8 +3944,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -3905,13 +3991,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -3962,8 +4058,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -4005,8 +4105,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -4038,8 +4142,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -4081,13 +4189,21 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", " text_embeddings = usem([normalized_text])\n", - " similarities = np.inner(text_embeddings.numpy(), conduct_embeddings.numpy())\n", + " similarities = np.inner(\n", + " text_embeddings.numpy(), conduct_embeddings.numpy()\n", + " )\n", " top_k = np.ravel(np.argsort(-similarities))[:k]\n", - " \n", - " return np.array([categories])[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array([categories])[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -4128,8 +4244,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -4415,7 +4535,7 @@ " if re.search(pattern, normalized_pred):\n", " found_types.add(modality)\n", "\n", - " return list(found_types)\n" + " return list(found_types)" ] }, { @@ -4490,7 +4610,9 @@ "metadata": {}, "outputs": [], "source": [ - "conduct_desc[\"beginning\"] = conduct_desc[\"label\"].str.startswith(\"B\").astype(int)\n", + "conduct_desc[\"beginning\"] = (\n", + " conduct_desc[\"label\"].str.startswith(\"B\").astype(int)\n", + ")\n", "conduct_desc.head(10)" ] }, @@ -4538,7 +4660,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"CONDUCTA_DESCRIPCION\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -4552,7 +4674,9 @@ "source": [ "conduct_desc_choices = []\n", "for sample in annots:\n", - " conduct_desc = list(filter(filter_conduct_desc_choices, sample[\"annotations\"][0][\"result\"]))\n", + " conduct_desc = list(\n", + " filter(filter_conduct_desc_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " conduct_desc_choices.extend(conduct_desc)" ] }, @@ -4591,7 +4715,7 @@ " group_key=\"choices\",\n", " sort_key=\"choices\",\n", " )\n", - ")\n" + ")" ] }, { @@ -4789,7 +4913,7 @@ "# categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))\n", "\n", "categories_embeddings = usem_qa.signatures[\"response_encoder\"](\n", - " input=tf.constant(categories) , context=tf.constant(categories)\n", + " input=tf.constant(categories), context=tf.constant(categories)\n", ")\n", "\n", "# categories_embeddings = usem(categories)" @@ -4802,7 +4926,9 @@ "metadata": {}, "outputs": [], "source": [ - "np.save(\"conduct_descr_embeddings.npy\", categories_embeddings[\"outputs\"].numpy())" + "np.save(\n", + " \"conduct_descr_embeddings.npy\", categories_embeddings[\"outputs\"].numpy()\n", + ")" ] }, { @@ -4854,8 +4980,10 @@ "metadata": {}, "outputs": [], "source": [ - "#texts_embeddings = usem(texts)\n", - "texts_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(texts))" + "# texts_embeddings = usem(texts)\n", + "texts_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(texts)\n", + ")" ] }, { @@ -4867,7 +4995,9 @@ }, "outputs": [], "source": [ - "similarities = np.inner(texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"])\n", + "similarities = np.inner(\n", + " texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"]\n", + ")\n", "similarities = pd.DataFrame(similarities, index=texts, columns=categories)\n", "similarities" ] @@ -4885,7 +5015,7 @@ " top_3_similar = similarities.loc[text].sort_values(ascending=False)[:3]\n", " print(text)\n", " display(top_3_similar)\n", - " print(\"=\"*100)" + " print(\"=\" * 100)" ] }, { @@ -4895,13 +5025,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -4952,8 +5092,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -4995,8 +5139,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5028,8 +5176,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5071,7 +5223,9 @@ "metadata": {}, "outputs": [], "source": [ - "categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))" + "categories_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(categories)\n", + ")" ] }, { @@ -5081,13 +5235,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -5138,8 +5302,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5181,8 +5349,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5214,8 +5386,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5257,13 +5433,21 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", " text_embeddings = usem([normalized_text])\n", - " similarities = np.inner(text_embeddings.numpy(), conduct_embeddings.numpy())\n", + " similarities = np.inner(\n", + " text_embeddings.numpy(), conduct_embeddings.numpy()\n", + " )\n", " top_k = np.ravel(np.argsort(-similarities))[:k]\n", - " \n", - " return np.array([categories])[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array([categories])[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -5304,8 +5488,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -5591,7 +5779,7 @@ " if re.search(pattern, normalized_pred):\n", " found_types.add(modality)\n", "\n", - " return list(found_types)\n" + " return list(found_types)" ] }, { @@ -5714,7 +5902,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"DETALLE\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -5728,7 +5916,9 @@ "source": [ "detail_choices = []\n", "for sample in annots:\n", - " detail = list(filter(filter_detail_choices, sample[\"annotations\"][0][\"result\"]))\n", + " detail = list(\n", + " filter(filter_detail_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " detail_choices.extend(detail)" ] }, @@ -6170,7 +6360,7 @@ "# categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))\n", "\n", "categories_embeddings = usem_qa.signatures[\"response_encoder\"](\n", - " input=tf.constant(categories) , context=tf.constant(categories)\n", + " input=tf.constant(categories), context=tf.constant(categories)\n", ")\n", "\n", "# categories_embeddings = usem(categories)" @@ -6235,8 +6425,10 @@ "metadata": {}, "outputs": [], "source": [ - "#texts_embeddings = usem(texts)\n", - "texts_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(texts))" + "# texts_embeddings = usem(texts)\n", + "texts_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(texts)\n", + ")" ] }, { @@ -6248,7 +6440,9 @@ }, "outputs": [], "source": [ - "similarities = np.inner(texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"])\n", + "similarities = np.inner(\n", + " texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"]\n", + ")\n", "similarities = pd.DataFrame(similarities, index=texts, columns=categories)\n", "similarities" ] @@ -6266,7 +6460,7 @@ " top_3_similar = similarities.loc[text].sort_values(ascending=False)[:3]\n", " print(text)\n", " display(top_3_similar)\n", - " print(\"=\"*100)" + " print(\"=\" * 100)" ] }, { @@ -6276,13 +6470,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -6333,8 +6537,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6376,8 +6584,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6409,8 +6621,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6452,7 +6668,9 @@ "metadata": {}, "outputs": [], "source": [ - "categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))" + "categories_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(categories)\n", + ")" ] }, { @@ -6462,13 +6680,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -6519,8 +6747,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6562,8 +6794,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6595,8 +6831,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6638,13 +6878,21 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", " text_embeddings = usem([normalized_text])\n", - " similarities = np.inner(text_embeddings.numpy(), conduct_embeddings.numpy())\n", + " similarities = np.inner(\n", + " text_embeddings.numpy(), conduct_embeddings.numpy()\n", + " )\n", " top_k = np.ravel(np.argsort(-similarities))[:k]\n", - " \n", - " return np.array([categories])[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array([categories])[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -6685,8 +6933,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -6808,7 +7060,7 @@ " from_name = annot[\"from_name\"]\n", " if from_name == \"OBJETO_DE_LA_RESOLUCION\":\n", " return True\n", - " \n", + "\n", " except:\n", " return False" ] @@ -6822,7 +7074,9 @@ "source": [ "res_object_choices = []\n", "for sample in annots:\n", - " res_object = list(filter(filter_res_object_choices, sample[\"annotations\"][0][\"result\"]))\n", + " res_object = list(\n", + " filter(filter_res_object_choices, sample[\"annotations\"][0][\"result\"])\n", + " )\n", " res_object_choices.extend(res_object)" ] }, @@ -7012,7 +7266,7 @@ "# categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))\n", "\n", "categories_embeddings = usem_qa.signatures[\"response_encoder\"](\n", - " input=tf.constant(categories) , context=tf.constant(categories)\n", + " input=tf.constant(categories), context=tf.constant(categories)\n", ")\n", "\n", "# categories_embeddings = usem(categories)" @@ -7077,8 +7331,10 @@ "metadata": {}, "outputs": [], "source": [ - "#texts_embeddings = usem(texts)\n", - "texts_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(texts))" + "# texts_embeddings = usem(texts)\n", + "texts_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(texts)\n", + ")" ] }, { @@ -7090,7 +7346,9 @@ }, "outputs": [], "source": [ - "similarities = np.inner(texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"])\n", + "similarities = np.inner(\n", + " texts_embeddings[\"outputs\"], categories_embeddings[\"outputs\"]\n", + ")\n", "similarities = pd.DataFrame(similarities, index=texts, columns=categories)\n", "similarities" ] @@ -7108,7 +7366,7 @@ " top_3_similar = similarities.loc[text].sort_values(ascending=False)[:3]\n", " print(text)\n", " display(top_3_similar)\n", - " print(\"=\"*100)" + " print(\"=\" * 100)" ] }, { @@ -7118,13 +7376,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -7165,8 +7433,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7208,8 +7480,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7241,8 +7517,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7284,7 +7564,9 @@ "metadata": {}, "outputs": [], "source": [ - "categories_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(categories))" + "categories_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(categories)\n", + ")" ] }, { @@ -7294,13 +7576,23 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", - " text_embeddings = usem_qa.signatures[\"question_encoder\"](input=tf.constant(normalized_text))\n", - " similarities = np.inner(text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"])\n", + " text_embeddings = usem_qa.signatures[\"question_encoder\"](\n", + " input=tf.constant(normalized_text)\n", + " )\n", + " similarities = np.inner(\n", + " text_embeddings[\"outputs\"], conduct_embeddings[\"outputs\"]\n", + " )\n", " top_k = np.argsort(np.ravel(-similarities))[:k]\n", - " \n", - " return np.array(categories)[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array(categories)[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -7351,8 +7643,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=2))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=2)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7394,8 +7690,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7427,8 +7727,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=5))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=5)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7470,13 +7774,21 @@ "metadata": {}, "outputs": [], "source": [ - "def find_top_k_similar(text, conduct_embeddings=categories_embeddings, categories=categories, k=1):\n", + "def find_top_k_similar(\n", + " text, conduct_embeddings=categories_embeddings, categories=categories, k=1\n", + "):\n", " normalized_text = normalize_text(text)\n", " text_embeddings = usem([normalized_text])\n", - " similarities = np.inner(text_embeddings.numpy(), conduct_embeddings.numpy())\n", + " similarities = np.inner(\n", + " text_embeddings.numpy(), conduct_embeddings.numpy()\n", + " )\n", " top_k = np.ravel(np.argsort(-similarities))[:k]\n", - " \n", - " return np.array([categories])[top_k][0] if k == 1 else np.array(categories)[top_k]" + "\n", + " return (\n", + " np.array([categories])[top_k][0]\n", + " if k == 1\n", + " else np.array(categories)[top_k]\n", + " )" ] }, { @@ -7517,8 +7829,12 @@ "metadata": {}, "outputs": [], "source": [ - "grouped[\"pred_choices\"] = grouped[\"text\"].map(lambda x: find_top_k_similar(x, k=3))\n", - "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(lambda x: [string.replace(\" \", \"_\") for string in x])" + "grouped[\"pred_choices\"] = grouped[\"text\"].map(\n", + " lambda x: find_top_k_similar(x, k=3)\n", + ")\n", + "grouped[\"pred_choices\"] = grouped[\"pred_choices\"].map(\n", + " lambda x: [string.replace(\" \", \"_\") for string in x]\n", + ")" ] }, { @@ -7580,7 +7896,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "aymurai (3.10.19)", "language": "python", "name": "python3" }, @@ -7594,12 +7910,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Oct 12 2022, 19:14:26) [GCC 9.4.0]" - }, - "vscode": { - "interpreter": { - "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" - } + "version": "3.10.19" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index f8b4b329..b5bb6d6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,12 +13,10 @@ authors = [ maintainers = [ { name = "jedzill4", email = "r@collectiveai.io" }, { name = "jansaldo", email = "juli@collectiveai.io" }, - ] description = "The backend API and machine learning models of AymurAI." readme = "README.md" license = { file = "LICENSE.md" } -# homepage = "https://github.com/AymurAI/backend" keywords = [ "api", "deep-learning", @@ -55,17 +53,13 @@ dependencies = [ "numpy<2.0.0", "more-itertools>=10.5.0", "odfpy>=1.4.1", - "gdown==4.6.0", "joblib>=1.4.2", - "textract==1.6.5", "datetime_matcher @ git+https://github.com/jedzill4/datetime_matcher", - # "scikit-learn==1.5.2", "jiwer==3.0.5", "datasets>=3.2.0", "unidecode==1.3.8", "sentencepiece==0.2.0", - # "flair @ git+https://github.com/flairNLP/flair@v0.13.1", - "flair==0.14.0", + "flair==0.15.1", "fastapi[standard]>=0.115.6", "uvicorn>=0.34.0", "python-multipart>=0.0.20", @@ -76,33 +70,25 @@ dependencies = [ "cachetools>=5.5.0", "diskcache>=5.6.3", "scipy<1.14.1", - "torch==1.12.1", # for decision model - "torchtext==0.13.1", # for decision model - "pytorch-lightning==1.8.3.post1", - "tensorflow_text==2.10.0", # last version compatible with Windows + "torch>=2.0", "psutil==6.1.0", "sqlmodel==0.0.22", "alembic>=1.13.3", "tenacity>=9.0.0", "python-dotenv>=1.0.1", - "tensorflow-hub>=0.16.1", + "sentence-transformers>=2.2.0", "pymupdf>=1.25.2", "pymupdf4llm>=0.0.17", "pypandoc>=1.15", "python-docx>=1.2.0", + "docx2txt>=0.9", ] [project.urls] Homepage = "https://www.aymurai.info/" -Repository = "https://github.com/aymurAI/backend" +Repository = "https://github.com/AymurAI/backend" Issues = "https://github.com/AymurAI/backend/issues" -# [project.optional-dependencies] -# gpu = [ -# "torch @ https://download.pytorch.org/whl/cu113/torch-1.12.1%2Bcu113-cp310-cp310-linux_x86_64.whl", -# "spacy[cuda113]==3.8.3", -# ] - [dependency-groups] dev = [ "matplotlib>=3.10.0", @@ -114,12 +100,14 @@ dev = [ "jupyter>=1.1.1", "pip>=24.3.1", ] +tests = ["pytest>=9.0.2"] [tool.setuptools.packages.find] include = ["aymurai"] [project.scripts] aymurai-api = "aymurai.api.main:main" +pipeline-download = "aymurai.scripts.pipelines_download:pipelines_download" [tool.setuptools.package-data] @@ -127,7 +115,24 @@ aymurai-api = "aymurai.api.main:main" [tool.setuptools_scm] version_file = "aymurai/version.py" +version_scheme = "only-version" +local_scheme = "no-local-version" + +[tool.uv] +required-environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64'", + "sys_platform == 'win32' and platform_machine == 'AMD64'", +] [tool.pylint.messages_control] disable = "C0330, C0326" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = ["-v", "--tb=short", "--strict-markers", "--disable-warnings"] +markers = [ + "integration: marks tests as integration tests", + "slow: marks tests as slow (model loading, >30s)", + "external: marks tests that depend on optional external binaries/services", +] diff --git a/docs/data/label-studio-config.xml b/resources/annotations/label-studio/datapublic/label-studio-config.xml similarity index 100% rename from docs/data/label-studio-config.xml rename to resources/annotations/label-studio/datapublic/label-studio-config.xml diff --git a/resources/api/static/logo256-text.ico b/resources/api/static/logo256-text.ico deleted file mode 100644 index 9ff327f6..00000000 Binary files a/resources/api/static/logo256-text.ico and /dev/null differ diff --git a/resources/pipelines/production/datapublic/pipeline.json b/resources/pipelines/production/datapublic/pipeline.json new file mode 100644 index 00000000..bfba7682 --- /dev/null +++ b/resources/pipelines/production/datapublic/pipeline.json @@ -0,0 +1,75 @@ +{ + "preprocess": [ + [ + "aymurai.models.flair.utils.FlairTextNormalize", + {} + ] + ], + "models": [ + [ + "aymurai.models.flair.core.FlairModel", + { + "basepath": "aymurai/flair-ner-spanish-judicial", + "split_doc": true, + "device": "cpu", + "use_tokenizer": false + } + ], + [ + "aymurai.models.decision.binregex.DecisionEmbeddingBagBinRegex", + { + "model_checkpoint": "https://github.com/AymurAI/backend/releases/download/v2.0.0-alpha.1/tiny-embeddingbag.safetensors", + "device": "cpu", + "threshold": 0.5, + "return_only_with_detalle": true + } + ] + ], + "postprocess": [ + [ + "aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer", + {} + ], + [ + "aymurai.transforms.datetime_formatter.core.DatetimeFormatter", + {} + ], + [ + "aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer", + { + "category": "CONDUCTA", + "embeddings_path": "conducta.npz", + "device": "cpu" + } + ], + [ + "aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer", + { + "category": "CONDUCTA_DESCRIPCION", + "embeddings_path": "conducta_descripcion.npz", + "device": "cpu" + } + ], + [ + "aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer", + { + "category": "DETALLE", + "embeddings_path": "detalle.npz", + "device": "cpu" + } + ], + [ + "aymurai.transforms.entity_subcategories.sentence_transformer.SentenceTransformerSubcategorizer", + { + "category": "OBJETO_DE_LA_RESOLUCION", + "embeddings_path": "objeto_de_la_resolucion.npz", + "device": "cpu" + } + ], + [ + "aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer", + {} + ] + ], + "use_cache": false +} \ No newline at end of file diff --git a/resources/pipelines/production/flair-anonymizer/pipeline.json b/resources/pipelines/production/flair-anonymizer/pipeline.json index 07e4638e..75302833 100644 --- a/resources/pipelines/production/flair-anonymizer/pipeline.json +++ b/resources/pipelines/production/flair-anonymizer/pipeline.json @@ -20,6 +20,10 @@ [ "aymurai.transforms.anonymization_postprocess.core.AnonymizationEntityCleaner", {} + ], + [ + "aymurai.transforms.datetime_formatter.core.DatetimeFormatter", + {} ] ], "use_cache": false diff --git a/resources/pipelines/production/full-paragraph/pipeline.json b/resources/pipelines/production/full-paragraph/pipeline.json deleted file mode 100644 index a60309b5..00000000 --- a/resources/pipelines/production/full-paragraph/pipeline.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "preprocess": [ - [ - "aymurai.models.flair.utils.FlairTextNormalize", - {} - ] - ], - "models": [ - [ - "aymurai.models.flair.core.FlairModel", - { - "basepath": "aymurai/flair-ner-spanish-judicial", - "split_doc": true, - "device": "cpu", - "use_tokenizer": false - } - ], - [ - "aymurai.models.decision.binregex.DecisionConv1dBinRegex", - { - "tokenizer_path": "https://drive.google.com/uc?id=1eljQOinpObdfBREIKxVnC5Y2g_sbhPHT&confirm=true", - "model_checkpoint": "https://drive.google.com/uc?id=19_YmBJnO06iS0qW8ak0zl0EIsJYin8kQ&confirm=true", - "device": "cpu", - "threshold": 0.5, - "return_only_with_detalle": true - } - ] - ], - "postprocess": [ - [ - "aymurai.transforms.entity_subcategories.regex.RegexSubcategorizer", - {} - ], - [ - "aymurai.transforms.datetime_formatter.core.DatetimeFormatter", - {} - ], - [ - "aymurai.transforms.entity_subcategories.usem.USEMSubcategorizer", - { - "category": "CONDUCTA", - "subcategories_path": "https://drive.google.com/uc?id=1Vj5BxyeHzDnR1T8jYjLuteM3YwzE7fTW&confirm=true", - "response_embeddings_path": "https://drive.google.com/uc?id=1zvBHGf1MeFyyG_I0TukJl1eaM-7TsbPF&confirm=true", - "device": "/cpu:0" - } - ], - [ - "aymurai.transforms.entity_subcategories.usem.USEMSubcategorizer", - { - "category": "CONDUCTA_DESCRIPCION", - "subcategories_path": "https://drive.google.com/uc?id=1A1I9xwzvynwxSv1I0SDHhN216Z3Yvoqj&confirm=true", - "response_embeddings_path": "https://drive.google.com/uc?id=1c3nYVDIq23kYqgMIIKGtDbIz6zDORpYK&confirm=true", - "device": "/cpu:0" - } - ], - [ - "aymurai.transforms.entity_subcategories.usem.USEMSubcategorizer", - { - "category": "DETALLE", - "subcategories_path": "https://drive.google.com/uc?id=1o1Z4fhGTtNzUIL2m3WOfDr_f0KXHu_Ms&confirm=true", - "response_embeddings_path": "https://drive.google.com/uc?id=1OumPgnnM9ffjHjObnb5NL96e3hnlt7Ik&confirm=true", - "device": "/cpu:0" - } - ], - [ - "aymurai.transforms.entity_subcategories.usem.USEMSubcategorizer", - { - "category": "OBJETO_DE_LA_RESOLUCION", - "subcategories_path": "https://drive.google.com/uc?id=1ksmfX_AJaE-OFEEGzj2N2mZgg5HZWB_4&confirm=true", - "response_embeddings_path": "https://drive.google.com/uc?id=18wOgqzNDsqF13nrvX2XscE0JS_xrgqBU&confirm=true", - "device": "/cpu:0" - } - ], - [ - "aymurai.transforms.entity_subcategories.article.ArticleSubcategorizer", - {} - ] - ], - "use_cache": false -} \ No newline at end of file diff --git a/test/api/mock-response/input/documento.docx b/test/api/mock-response/input/documento.docx deleted file mode 100644 index 51b2a7cd..00000000 Binary files a/test/api/mock-response/input/documento.docx and /dev/null differ diff --git a/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.conll b/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.conll deleted file mode 100644 index b384b3c6..00000000 --- a/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.conll +++ /dev/null @@ -1,174 +0,0 @@ --DOCSTART- -X- O -2020 -X- _ O -Año -X- _ O -del -X- _ O -General -X- _ O -Manuel -X- _ O -Belgrano -X- _ O -JUZGADO -X- _ O -DE -X- _ O -1RA -X- _ O -INSTANCIA -X- _ O -EN -X- _ O -LO -X- _ O -PENAL -X- _ O -CONTRAVENCIONAL -X- _ O -Y -X- _ O -DE -X- _ O -FALTAS -X- _ O -N°10 -X- _ O -SECRETARIA -X- _ O -N°19 -X- _ O -5 -X- _ O -MASCULINOS -X- _ O -Y -X- _ O -3 -X- _ O -FEMENINOS -X- _ O -DE -X- _ O -IDENTIDAD -X- _ O -DESCONOCIDA, -X- _ O -NN -X- _ O -Y -X- _ O -OTROS -X- _ O -SOBRE -X- _ O -5 -X- _ O -C -X- _ O -- -X- _ O -COMERCIO -X- _ O -DE -X- _ O -ESTUPEFACIENTES -X- _ O -O -X- _ O -CUALQUIER -X- _ O -MATERIA -X- _ O -PRIMA -X- _ O -PARA -X- _ O -SU -X- _ O -PRODUCCIÓN -X- _ O -/TENENCIA -X- _ O -CON -X- _ O -FINES -X- _ O -DE -X- _ O -COMERCIALIZACIÓN -X- _ O -Número: -X- _ O -IPP -X- _ O -11111/2013-0 -X- _ O -CUIJ: -X- _ O -IPP -X- _ O -J-01-06066606-6/2013-0 -X- _ O -Actuación -X- _ O -Nro: -X- _ O -66600666/2022 -X- _ O -AUDIENCIA -X- _ O -UNIPERSONAL -X- _ O -(art. -X- _ O -2 -X- _ O -bis -X- _ O -CPP) -X- _ O -Fecha: -X- _ O -7 -X- _ O -de -X- _ O -septiembre -X- _ O -de -X- _ O -2020 -X- _ O -Horario -X- _ O -de -X- _ O -inicio: -X- _ O -10:00 -X- _ O -horas -X- _ O -PARTICIPANTES -X- _ O -Juez: -X- _ O -Pablo -X- _ O -C. -X- _ O -Casas. -X- _ O -Secretario: -X- _ O -Pablo -X- _ O -Hilaire -X- _ O -Chaneton. -X- _ O -Fiscal: -X- _ O -Cristian -X- _ B-NOMBRE -Longobardi, -X- _ I-NOMBRE -UIT -X- _ O -Sur. -X- _ O -DESARROLLO -X- _ O -Juez: -X- _ O -le -X- _ O -hace -X- _ O -saber -X- _ O -al -X- _ O -Fiscal -X- _ O -que -X- _ O -tras -X- _ O -haber -X- _ O -analizado -X- _ O -en -X- _ O -forma -X- _ O -pormenorizada -X- _ O -el -X- _ O -caso, -X- _ O -lo -X- _ O -contactó -X- _ O -telefónicamente -X- _ O -para -X- _ O -realizar -X- _ O -esta -X- _ O -audiencia -X- _ O -frente -X- _ O -a -X- _ O -la -X- _ O -necesidad -X- _ O -de -X- _ O -solicitar -X- _ O -algunas -X- _ O -aclaraciones -X- _ O -y -X- _ O -precisiones -X- _ O -atinentes -X- _ O -a -X- _ O -la -X- _ O -prueba -X- _ O -presentada. -X- _ O -Fiscal: -X- _ O -refiere -X- _ O -que -X- _ O -en -X- _ O -función -X- _ O -de -X- _ O -lo -X- _ O -solicitado, -X- _ O -resolverá -X- _ O -e -X- _ O -informara -X- _ O -sobre -X- _ O -las -X- _ O -cuestiones -X- _ O -señaladas -X- _ O -a -X- _ O -la -X- _ O -mayor -X- _ O -brevedad -X- _ O -posible. -X- _ O -Juez: -X- _ O -manifiesta -X- _ O -que -X- _ O -lo -X- _ O -tiene -X- _ O -presente -X- _ O -y -X- _ O -que -X- _ O -quedará -X- _ O -a -X- _ O -la -X- _ O -espera -X- _ O -de -X- _ O -tales -X- _ O -aclaraciones -X- _ O -en -X- _ O -torno -X- _ O -a -X- _ O -la -X- _ O -prueba. -X- _ O -Horario -X- _ O -de -X- _ O -cierre: -X- _ O -10:15 -X- _ O -horas -X- _ O diff --git a/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.json b/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.json deleted file mode 100644 index 31684d62..00000000 --- a/test/api/mock-response/input/project-9-at-2022-11-10-22-41-09a5c372.json +++ /dev/null @@ -1,233 +0,0 @@ -[ - { - "id": 10252, - "data": { - "text": "2020 Año del General Manuel Belgrano\t\nJUZGADO DE 1RA INSTANCIA EN LO PENAL CONTRAVENCIONAL Y DE FALTAS N°10 SECRETARIA N°19 5 MASCULINOS Y 3 FEMENINOS DE IDENTIDAD DESCONOCIDA, NN Y OTROS SOBRE 5 C - COMERCIO DE ESTUPEFACIENTES O CUALQUIER MATERIA PRIMA PARA SU PRODUCCIÓN /TENENCIA CON FINES DE COMERCIALIZACIÓN \nNúmero: IPP 11111/2013-0 \nCUIJ: IPP J-01-06066606-6/2013-0 \nActuación Nro: 66600666/2022 \nAUDIENCIA UNIPERSONAL \n(art. 2 bis CPP)\n Fecha: 7 de septiembre de 2020 \n Horario de inicio: 10:00 horas \nPARTICIPANTES \nJuez: Pablo C. Casas. \t\nSecretario: Pablo Hilaire Chaneton.\nFiscal: Cristian Longobardi, UIT Sur.\nDESARROLLO \nJuez: le hace saber al Fiscal que tras haber analizado en forma pormenorizada el caso, lo contactó telefónicamente para realizar esta audiencia frente a la necesidad de solicitar algunas aclaraciones y precisiones atinentes a la prueba presentada.\nFiscal: refiere que en función de lo solicitado, resolverá e informara sobre las cuestiones señaladas a la mayor brevedad posible.\nJuez: manifiesta que lo tiene presente y que quedará a la espera de tales aclaraciones en torno a la prueba. \n \t\t\t\t\t\t Horario de cierre: 10:15 horas \n \n", - "meta_info": { - "id": "dump-20221021", - "path": "/test/api/mock-response/input/documento.docx" - } - }, - "annotations": [ - { - "id": 22, - "created_username": " rbarriga@collective.ai, 1", - "created_ago": "1 week, 6 days", - "completed_by": { - "id": 1, - "first_name": "", - "last_name": "", - "avatar": null, - "email": "rbarriga@collective.ai", - "initials": "rb" - }, - "result": [ - { - "value": { - "start": 594, - "end": 614, - "text": "Cristian Longobardi,", - "labels": [ - "NOMBRE" - ] - }, - "id": "GT6TTD8eMf", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 594, - "end": 614, - "text": "Cristian Longobardi,", - "choices": [ - "otro" - ] - }, - "id": "GT6TTD8eMf", - "from_name": "NOMBRE", - "to_name": "text", - "type": "choices", - "origin": "manual" - }, - { - "value": { - "start": 532, - "end": 547, - "text": "Pablo C. Casas.", - "labels": [ - "NOMBRE" - ] - }, - "id": "2Oq-A5l8yH", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 532, - "end": 547, - "text": "Pablo C. Casas.", - "choices": [ - "firma" - ] - }, - "id": "2Oq-A5l8yH", - "from_name": "NOMBRE", - "to_name": "text", - "type": "choices", - "origin": "manual" - }, - { - "value": { - "start": 1152, - "end": 1157, - "text": "10:15", - "labels": [ - "HORA_DE_CIERRE" - ] - }, - "id": "1bfxq1oxoG", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 498, - "end": 503, - "text": "10:00", - "labels": [ - "HORA_DE_INICIO" - ] - }, - "id": "3YLgdS7Hu7", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 453, - "end": 476, - "text": "7 de septiembre de 2020", - "labels": [ - "FECHA_RESOLUCION" - ] - }, - "id": "-HiUTvwFbK", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 327, - "end": 339, - "text": "11111/2013-0", - "labels": [ - "N_EXPTE_EJE" - ] - }, - "id": "2zZNLOfntI", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 195, - "end": 198, - "text": "5 C", - "labels": [ - "ART_INFRINGIDO" - ] - }, - "id": "eIl-etR2ye", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 201, - "end": 273, - "text": "COMERCIO DE ESTUPEFACIENTES O CUALQUIER MATERIA PRIMA PARA SU PRODUCCIÓN", - "labels": [ - "CONDUCTA" - ] - }, - "id": "sU9P19itW3", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 201, - "end": 273, - "text": "COMERCIO DE ESTUPEFACIENTES O CUALQUIER MATERIA PRIMA PARA SU PRODUCCIÓN", - "choices": [ - "estupefacientes" - ] - }, - "id": "sU9P19itW3", - "from_name": "CONDUCTA", - "to_name": "text", - "type": "choices", - "origin": "manual" - }, - { - "value": { - "start": 562, - "end": 585, - "text": "Pablo Hilaire Chaneton.", - "labels": [ - "NOMBRE" - ] - }, - "id": "7L82m_D2J3", - "from_name": "label", - "to_name": "text", - "type": "labels", - "origin": "manual" - }, - { - "value": { - "start": 562, - "end": 585, - "text": "Pablo Hilaire Chaneton.", - "choices": [ - "otro" - ] - }, - "id": "7L82m_D2J3", - "from_name": "NOMBRE", - "to_name": "text", - "type": "choices", - "origin": "manual" - } - ], - "was_cancelled": false, - "ground_truth": false, - "created_at": "2022-11-04T20:44:25.455645Z", - "updated_at": "2022-11-18T15:57:57.652671Z", - "lead_time": 228.062, - "task": 10252, - "parent_prediction": null, - "parent_annotation": null - } - ], - "predictions": [] - } -] \ No newline at end of file diff --git a/test/api/mock-response/output/mock-response.json b/test/api/mock-response/output/mock-response.json deleted file mode 100644 index 15a75e24..00000000 --- a/test/api/mock-response/output/mock-response.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "document": "2020 Año del General Manuel Belgrano\t\nJUZGADO DE 1RA INSTANCIA EN LO PENAL CONTRAVENCIONAL Y DE FALTAS N°10 SECRETARIA N°19 5 MASCULINOS Y 3 FEMENINOS DE IDENTIDAD DESCONOCIDA, NN Y OTROS SOBRE 5 C - COMERCIO DE ESTUPEFACIENTES O CUALQUIER MATERIA PRIMA PARA SU PRODUCCIÓN /TENENCIA CON FINES DE COMERCIALIZACIÓN \nNúmero: IPP 11111/2013-0 \nCUIJ: IPP J-01-06066606-6/2013-0 \nActuación Nro: 66600666/2022 \nAUDIENCIA UNIPERSONAL \n(art. 2 bis CPP)\n Fecha: 7 de septiembre de 2020 \n Horario de inicio: 10:00 horas \nPARTICIPANTES \nJuez: Pablo C. Casas. \t\nSecretario: Pablo Hilaire Chaneton.\nFiscal: Cristian Longobardi, UIT Sur.\nDESARROLLO \nJuez: le hace saber al Fiscal que tras haber analizado en forma pormenorizada el caso, lo contactó telefónicamente para realizar esta audiencia frente a la necesidad de solicitar algunas aclaraciones y precisiones atinentes a la prueba presentada.\nFiscal: refiere que en función de lo solicitado, resolverá e informara sobre las cuestiones señaladas a la mayor brevedad posible.\nJuez: manifiesta que lo tiene presente y que quedará a la espera de tales aclaraciones en torno a la prueba. \n \t\t\t\t\t\t Horario de cierre: 10:15 horas \nJuzgado PCyF No 10 - Tacuarí 138, 7o Piso - juzcyf10@jusbaires.gob.ar - 4014-6821/20 - @jpcyf10 \nJuzgado PCyF No 10 - Tacuarí 138, 7o Piso - juzcyf10@jusbaires.gob.ar - 4014-6821/20 - @jpcyf10", - "labels": [ - { - "text": "5 C", - "start_char": 195, - "end_char": 198, - "attrs": { - "aymurai_label": "ART_INFRINGIDO", - "aymurai_label_subclass": null - } - }, - { - "text": "COMERCIO DE ESTUPEFACIENTES O CUALQUIER MATERIA PRIMA PARA SU PRODUCCIÓN", - "start_char": 201, - "end_char": 273, - "attrs": { - "aymurai_label": "CONDUCTA", - "aymurai_label_subclass": [ - "estupefacientes" - ] - } - }, - { - "text": "11111/2013-0", - "start_char": 327, - "end_char": 339, - "attrs": { - "aymurai_label": "N_EXPTE_EJE", - "aymurai_label_subclass": null - } - }, - { - "text": "7 de septiembre de 2020", - "start_char": 453, - "end_char": 476, - "attrs": { - "aymurai_label": "FECHA_RESOLUCION", - "aymurai_label_subclass": null - } - }, - { - "text": "10:00", - "start_char": 498, - "end_char": 503, - "attrs": { - "aymurai_label": "HORA_DE_INICIO", - "aymurai_label_subclass": null - } - }, - { - "text": "Pablo C. Casas.", - "start_char": 532, - "end_char": 547, - "attrs": { - "aymurai_label": "NOMBRE", - "aymurai_label_subclass": [ - "firma" - ] - } - }, - { - "text": "Pablo Hilaire Chaneton.", - "start_char": 562, - "end_char": 585, - "attrs": { - "aymurai_label": "NOMBRE", - "aymurai_label_subclass": [ - "otro" - ] - } - }, - { - "text": "Cristian Longobardi,", - "start_char": 594, - "end_char": 614, - "attrs": { - "aymurai_label": "NOMBRE", - "aymurai_label_subclass": [ - "otro" - ] - } - }, - { - "text": "10:15", - "start_char": 1152, - "end_char": 1157, - "attrs": { - "aymurai_label": "HORA_DE_CIERRE", - "aymurai_label_subclass": null - } - } - ] -} \ No newline at end of file diff --git a/test/api/test_file.docx b/test/api/test_file.docx deleted file mode 100644 index 2a0e4c25..00000000 Binary files a/test/api/test_file.docx and /dev/null differ diff --git a/aymurai/models/usem/__init__.py b/tests/__init__.py similarity index 100% rename from aymurai/models/usem/__init__.py rename to tests/__init__.py diff --git a/aymurai/text/tokenizers/__init__.py b/tests/api/__init__.py similarity index 100% rename from aymurai/text/tokenizers/__init__.py rename to tests/api/__init__.py diff --git a/tests/api/conftest.py b/tests/api/conftest.py new file mode 100644 index 00000000..22dd7a69 --- /dev/null +++ b/tests/api/conftest.py @@ -0,0 +1,140 @@ +import os +from pathlib import Path + +import diskcache +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, StaticPool, create_engine + +os.environ.setdefault("DISKCACHE_ROOT", "/tmp/aymurai-test-diskcache") +os.environ.setdefault("AYMURAI_CACHE_BASEPATH", "/tmp/aymurai-test-cache") +os.environ.setdefault("RESOURCES_BASEPATH", "resources") + +from aymurai.api.endpoints.routers.anonymizer import anonymizer +from aymurai.api.endpoints.routers.datapublic import datapublic +from aymurai.api.endpoints.routers.misc import document_extract +from aymurai.database.meta.anonymization.paragraph import AnonymizationParagraphCreate +from aymurai.database.meta.datapublic.paragraph import DataPublicParagraphCreate +from aymurai.database.session import get_session +from aymurai.database.utils import text_to_uuid +from aymurai.meta.api_interfaces import DocLabel +from aymurai.meta.entities import EntityAttributes + + +@pytest.fixture(scope="function") +def app() -> FastAPI: + test_app = FastAPI() + test_app.include_router( + anonymizer.router, + prefix="/anonymizer", + tags=["anonymization/model"], + ) + test_app.include_router( + datapublic.router, + prefix="/datapublic", + tags=["datapublic/model"], + ) + test_app.include_router(document_extract.router, tags=["document"], deprecated=True) + test_app.include_router(document_extract.router, prefix="/misc", tags=["document"]) + return test_app + + +@pytest.fixture(scope="function", autouse=True) +def isolated_diskcache(tmp_path): + from aymurai.utils import cache as cache_module + + original_cache = cache_module.cache + test_cache = diskcache.Cache(str(tmp_path / "diskcache")) + cache_module.cache = test_cache + try: + yield test_cache + finally: + cache_module.cache = original_cache + test_cache.close() + + +@pytest.fixture(scope="function") +def db_engine(): + test_engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(test_engine) + + return test_engine + + +@pytest.fixture(scope="function") +def db_session(db_engine): + session = Session(db_engine) + yield session + session.close() + + +@pytest.fixture(scope="function") +def client(app, db_engine): + def override_get_session(): + with Session(db_engine) as session: + yield session + + app.dependency_overrides[get_session] = override_get_session + c = TestClient(app, raise_server_exceptions=False) + try: + yield c + finally: + c.close() + app.dependency_overrides.clear() + + +@pytest.fixture(scope="session") +def sample_docx(): + return Path("/resources/data/sample/document-01.docx") + + +def build_data_item(text: str = "sample text") -> dict: + return { + "path": "test", + "extension": "docx", + "dataset": "test", + "data": {"doc.text": text}, + "annotations": None, + "predictions": None, + } + + +def build_label(label: str = "PER", value: str = "John Doe") -> DocLabel: + attrs = EntityAttributes(aymurai_label=label) + return DocLabel( + text=value, + start_char=0, + end_char=len(value), + attrs=attrs, + ) + + +def build_anonymization_paragraph( + text: str = "sample text", + prediction: list[DocLabel] | None = None, + validation: list[DocLabel] | None = None, +) -> AnonymizationParagraphCreate: + return AnonymizationParagraphCreate( + id=text_to_uuid(text), + text=text, + prediction=prediction, + validation=validation, + ) + + +def build_datapublic_paragraph( + text: str = "sample text", + prediction: list[DocLabel] | None = None, + validation: list[DocLabel] | None = None, +) -> DataPublicParagraphCreate: + return DataPublicParagraphCreate( + id=text_to_uuid(text), + text=text, + prediction=prediction, + validation=validation, + ) diff --git a/tests/api/routers/__init__.py b/tests/api/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/routers/anonymizer/__init__.py b/tests/api/routers/anonymizer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/routers/anonymizer/test_anonymizer.py b/tests/api/routers/anonymizer/test_anonymizer.py new file mode 100644 index 00000000..4480e05f --- /dev/null +++ b/tests/api/routers/anonymizer/test_anonymizer.py @@ -0,0 +1,1304 @@ +import base64 +import json +import re +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pymupdf +import pytest +from docx import Document + +from aymurai.database.schema import AnonymizationParagraph +from aymurai.database.utils import text_to_uuid +from aymurai.meta.api_interfaces import LabelPolicy, RenderPolicy +from aymurai.text.anonymization import DocxAnonymizer, PdfAnonymizer, get_anonymizer +from aymurai.text.anonymization.alignment import index_paragraphs +from aymurai.text.anonymization.pdf.ops import _refine_signature_text_rect +from aymurai.text.anonymization.pdf.widgets import _signature_background_rect +from tests.api.conftest import build_label +from tests.api.routers.conftest import build_mock_pipeline + +PNG_1X1 = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a6R8AAAAASUVORK5CYII=" +) +PNG_BLACK_1X1 = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR42mNgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==" +) +WATERMARK_URL = "https://www.aymurai.info/" + +WINDOWS_PYMUPDF_LAYOUT_XFAIL = pytest.mark.xfail( + sys.platform == "win32", + reason="pymupdf4llm ONNX layout model receives int32 tensors on Windows (expects int64)", + strict=False, +) + + +def _write_pdf(path: Path, configure) -> Path: + doc = pymupdf.open() + page = doc.new_page() + configure(doc, page) + doc.save(path) + doc.close() + return path + + +def _label_dict(text: str, label: str = "PER", **attrs) -> dict: + payload = build_label(label, text).model_dump(mode="json") + payload["attrs"].update(attrs) + return payload + + +def _run_pdf_anonymizer( + tmp_path: Path, + source_path: Path, + document: str, + labels: list[dict], + render_context: dict | None = None, +) -> Path: + output_dir = tmp_path / "out" + output_dir.mkdir(exist_ok=True) + output_path = PdfAnonymizer().anonymize( + {"path": str(source_path)}, + [{"document": document, "labels": labels}], + str(output_dir), + render_context=render_context, + ) + return Path(output_path) + + +def _label_for_document_text(document: str, text: str, label: str = "PER") -> dict: + payload = _label_dict(text, label) + start = document.index(text) + payload["start_char"] = start + payload["end_char"] = start + len(text) + return payload + + +def _render_context_for_entities(labels: list[dict]) -> dict: + index_by_entity = {} + next_index_by_base = {} + for label in labels: + attrs = label.get("attrs") or {} + base = attrs.get("aymurai_label") or label.get("label") or "ENT" + entity_id = str(attrs.get("canonical_entity_id") or label.get("text")) + key = (base, entity_id) + if key not in index_by_entity: + next_index_by_base[base] = next_index_by_base.get(base, 0) + 1 + index_by_entity[key] = next_index_by_base[base] + + return { + "render_policy": RenderPolicy(suffix_mode="always", suffix_threshold=0), + "label_policies": {"PER": LabelPolicy()}, + "count_by_base": dict(next_index_by_base), + "index_by_entity": index_by_entity, + } + + +def _dark_pixel_ratio(page: pymupdf.Page, rect: pymupdf.Rect) -> float: + pixmap = page.get_pixmap( + matrix=pymupdf.Matrix(2, 2), + clip=rect, + alpha=False, + ) + samples = pixmap.samples + if not samples: + return 0.0 + + channels = pixmap.n + dark_pixels = 0 + total_pixels = pixmap.width * pixmap.height + for offset in range(0, len(samples), channels): + if all(channel < 96 for channel in samples[offset : offset + 3]): + dark_pixels += 1 + + return dark_pixels / max(total_pixels, 1) + + +def _assert_text_count(page_text: str, text: str, expected: int) -> None: + assert page_text.count(text) == expected, page_text + + +def _assert_rect_close(actual: pymupdf.Rect, expected: pymupdf.Rect) -> None: + assert (actual.x0, actual.y0, actual.x1, actual.y1) == pytest.approx( + (expected.x0, expected.y0, expected.x1, expected.y1) + ) + + +def _write_variable_signature_pdf( + path: Path, +) -> tuple[Path, list[dict], list[str], list[str], list[pymupdf.Rect]]: + blocks = [ + { + "origin": (58, 112), + "lines": [ + "Mesa de Control 42", + "Adriana Morales", + "Area de Validacion", + "Codigo A-17", + ], + "signer": "Adriana Morales", + "qr": "top", + }, + { + "origin": (326, 112), + "lines": [ + "Bernardo Diaz", + "Direccion Legal", + "Organismo Beta Sur", + "Tramite BX-900", + ], + "signer": "Bernardo Diaz", + "qr": "right", + }, + { + "origin": (58, 328), + "lines": [ + "Centro Operativo", + "Carolina Ruiz", + "Secretaria Tecnica", + "2026-05-26 10:15", + ], + "signer": "Carolina Ruiz", + "qr": "left", + }, + { + "origin": (326, 328), + "lines": [ + "Unidad Regional", + "Coordinacion de Revision", + "Daniel Silva", + "Expediente Digital Z-42", + ], + "signer": "Daniel Silva", + "qr": "top", + }, + { + "origin": (58, 544), + "lines": [ + "Responsable: Elena Torres - Acta Final", + "Delegacion Gamma", + "Registro Interno R-204", + ], + "signer": "Elena Torres", + "qr": "right", + }, + ] + + doc = pymupdf.open() + page = doc.new_page() + preds: list[dict] = [] + preserved_texts: list[str] = [] + signers: list[str] = [] + qr_rects: list[pymupdf.Rect] = [] + + for idx, block in enumerate(blocks): + x, y = block["origin"] + if block["qr"] == "right": + qr_rect = pymupdf.Rect(x + 150, y - 4, x + 182, y + 28) + elif block["qr"] == "left": + qr_rect = pymupdf.Rect(x - 2, y - 48, x + 30, y - 16) + else: + qr_rect = pymupdf.Rect(x, y - 52, x + 32, y - 20) + page.insert_image(qr_rect, stream=PNG_BLACK_1X1) + qr_rects.append(qr_rect) + + for line_idx, line in enumerate(block["lines"]): + page.insert_text((x, y + (line_idx * 16)), line, fontsize=11) + if line == block["signer"]: + continue + if block["signer"] in line: + preserved_texts.extend( + part.strip() for part in line.split(block["signer"]) if part.strip() + ) + else: + preserved_texts.append(line) + + widget = pymupdf.Widget() + widget.field_name = f"sig_{idx}" + widget.field_type = pymupdf.PDF_WIDGET_TYPE_SIGNATURE + widget.rect = pymupdf.Rect(x - 12, y - 62, x + 230, y + 64) + page.add_widget(widget) + + document = "\n".join(block["lines"]) + label = _label_for_document_text(document, block["signer"]) + preds.append({"document": document, "labels": [label]}) + signers.append(block["signer"]) + + doc.save(path) + doc.close() + return path, preds, signers, preserved_texts, qr_rects + + +@pytest.mark.integration +def test_anonymization_package_exports_and_registry_are_stable(): + assert PdfAnonymizer.__name__ == "PdfAnonymizer" + assert DocxAnonymizer.__name__ == "DocxAnonymizer" + assert isinstance(get_anonymizer("pdf"), PdfAnonymizer) + assert isinstance(get_anonymizer("docx"), DocxAnonymizer) + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_falls_back_from_invalid_alt_offsets(tmp_path): + document = "Ana Perez firmo el escrito" + source_path = _write_pdf( + tmp_path / "invalid-alt.pdf", + lambda _doc, page: page.insert_text((72, 72), document), + ) + labels = [ + _label_dict( + "Ana Perez", + aymurai_alt_start_char=999, + aymurai_alt_end_char=1000, + ) + ] + + output_path = _run_pdf_anonymizer(tmp_path, source_path, document, labels) + + with pymupdf.open(output_path) as output_doc: + page_text = output_doc[0].get_text() + + assert "Ana Perez" not in page_text + assert "" in page_text + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_scrubs_pdf_payloads_and_preserves_safe_links(tmp_path): + document = "Ana Perez presento el escrito" + + def configure(doc: pymupdf.Document, page: pymupdf.Page) -> None: + page.insert_text((72, 72), document) + sensitive_rect = page.search_for("Ana Perez")[0] + page.insert_link( + { + "kind": pymupdf.LINK_URI, + "from": sensitive_rect, + "uri": "https://secret.example", + } + ) + safe_rect = pymupdf.Rect(72, 140, 180, 155) + page.insert_text((72, 150), "Portal publico") + page.insert_link( + { + "kind": pymupdf.LINK_URI, + "from": safe_rect, + "uri": "https://safe.example", + } + ) + page.add_file_annot((220, 72), b"attached secret", "attached.txt") + doc.set_metadata( + { + "title": "Secret title", + "author": "Secret author", + "subject": "Secret subject", + "keywords": "alpha,beta", + "creator": "Secret creator", + "producer": "Secret producer", + } + ) + doc.set_xml_metadata("top-secret") + doc.embfile_add("secret.txt", b"secret bytes", filename="secret.txt") + + source_path = _write_pdf(tmp_path / "metadata.pdf", configure) + labels = [_label_dict("Ana Perez")] + + output_path = _run_pdf_anonymizer(tmp_path, source_path, document, labels) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + link_uris = {link.get("uri") for link in page.get_links()} + + assert output_doc.metadata.get("title") == "" + assert output_doc.metadata.get("subject") == "" + assert output_doc.metadata.get("keywords") == "" + assert output_doc.metadata.get("creationDate") == "" + assert re.fullmatch( + r"D:\d{14}\+00'00'", + output_doc.metadata.get("modDate") or "", + ) + assert output_doc.metadata.get("trapped") == "" + assert output_doc.metadata.get("author") == "" + assert output_doc.metadata.get("creator") == "AymurAI" + assert output_doc.metadata.get("producer") == "AymurAI" + assert not output_doc.get_xml_metadata() + assert output_doc.embfile_names() == [] + assert list(page.annots() or []) == [] + assert "https://secret.example" not in link_uris + assert "https://safe.example" in link_uris + assert WATERMARK_URL in link_uris + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_moves_watermark_away_from_footer_content(tmp_path): + document = "Ana Perez presento el escrito" + footer_rect = pymupdf.Rect(360, 760, 575, 815) + + def configure(_doc: pymupdf.Document, page: pymupdf.Page) -> None: + page.insert_text((72, 72), document) + page.draw_rect(footer_rect, color=(0, 0, 0), fill=(0, 0, 0), overlay=True) + + source_path = _write_pdf(tmp_path / "footer-watermark.pdf", configure) + output_path = _run_pdf_anonymizer( + tmp_path, + source_path, + document, + [_label_dict("Ana Perez")], + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + watermark_links = [ + link for link in page.get_links() if link.get("uri") == WATERMARK_URL + ] + + assert len(watermark_links) == 1 + watermark_rect = pymupdf.Rect(watermark_links[0]["from"]) + assert not watermark_rect.intersects(footer_rect) + assert watermark_rect.x1 < footer_rect.x0 + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_removes_image_backed_entities(tmp_path): + source_path = _write_pdf( + tmp_path / "image.pdf", + lambda _doc, page: ( + page.insert_image(pymupdf.Rect(60, 60, 220, 110), stream=PNG_1X1), + page.insert_text((80, 90), "Ana Perez"), + ), + ) + + output_path = _run_pdf_anonymizer( + tmp_path, + source_path, + "Ana Perez", + [_label_dict("Ana Perez")], + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + page_text = page.get_text() + + assert page.get_image_info() == [] + assert "Ana Perez" not in page_text + assert "" in page_text + + +def test_signature_background_rect_stays_on_signer_name_line(): + background = _signature_background_rect( + { + "line_rect": pymupdf.Rect(80, 70, 220, 118), + "canvas_rect": pymupdf.Rect(112, 70, 145, 82), + "redact_rect": pymupdf.Rect(112, 70, 145, 82), + }, + pymupdf.Rect(60, 60, 230, 130), + ) + + assert background.y0 >= 70 + assert background.y1 <= 82 + + +def test_signature_text_rect_refinement_does_not_include_role_text(tmp_path): + source_path = _write_pdf( + tmp_path / "signature-role.pdf", + lambda _doc, page: ( + page.insert_text((100, 80), "RUIZ"), + page.insert_text((100, 96), "JUEZ/A"), + ), + ) + + with pymupdf.open(source_path) as doc: + page = doc[0] + signer_rect = page.search_for("RUIZ")[0] + role_rect = page.search_for("JUEZ/A")[0] + loose_rect = pymupdf.Rect(signer_rect) + loose_rect.include_rect(role_rect) + + refined = _refine_signature_text_rect( + page, + "RUIZ", + pymupdf.Rect(80, 60, 200, 115), + loose_rect, + ) + + assert refined.intersects(signer_rect) + assert not refined.intersects(role_rect) + + +def test_signature_text_rect_refinement_returns_current_rect_when_no_hit_in_widget( + tmp_path, +): + source_path = _write_pdf( + tmp_path / "signature-no-hit.pdf", + lambda _doc, page: page.insert_text((260, 80), "RUIZ"), + ) + + with pymupdf.open(source_path) as doc: + page = doc[0] + current_rect = pymupdf.Rect(100, 72, 130, 84) + + refined = _refine_signature_text_rect( + page, + "RUIZ", + pymupdf.Rect(80, 60, 180, 115), + current_rect, + ) + + _assert_rect_close(refined, current_rect) + + +def test_signature_text_rect_refinement_selects_closest_matching_hit(tmp_path): + source_path = _write_pdf( + tmp_path / "signature-multiple-hits.pdf", + lambda _doc, page: ( + page.insert_text((100, 80), "RUIZ"), + page.insert_text((220, 80), "RUIZ"), + ), + ) + + with pymupdf.open(source_path) as doc: + page = doc[0] + left_rect, right_rect = page.search_for("RUIZ") + target = pymupdf.Rect(right_rect) + target.x0 += 2 + target.x1 += 2 + + refined = _refine_signature_text_rect( + page, + "RUIZ", + pymupdf.Rect(80, 60, 280, 115), + target, + ) + + assert refined.intersects(right_rect) + assert not refined.intersects(left_rect) + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_only_redacts_marked_signature_names_in_variable_layouts( + tmp_path, +): + source_path, preds, signers, preserved_texts, qr_rects = ( + _write_variable_signature_pdf(tmp_path / "variable-signatures.pdf") + ) + render_context = _render_context_for_entities([pred["labels"][0] for pred in preds]) + output_dir = tmp_path / "out-variable" + output_dir.mkdir(exist_ok=True) + + output_path = PdfAnonymizer().anonymize( + {"path": str(source_path)}, + preds, + str(output_dir), + render_context=render_context, + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + page_text = page.get_text() + + assert list(page.widgets() or []) == [] + assert len(page.get_image_info()) >= len(qr_rects) + + for signer in signers: + assert signer not in page_text + + for index in range(1, len(signers) + 1): + assert f"" in page_text + + for preserved_text in preserved_texts: + _assert_text_count(page_text, preserved_text, 1) + + for qr_rect in qr_rects: + assert _dark_pixel_ratio(page, qr_rect) > 0.25 + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_leaves_unlabeled_signature_names_visible(tmp_path): + source_path, preds, signers, preserved_texts, qr_rects = ( + _write_variable_signature_pdf(tmp_path / "partially-labeled-signatures.pdf") + ) + unlabeled_signer = signers[-1] + filtered_preds = [] + filtered_labels = [] + for pred, signer in zip(preds, signers, strict=True): + labels = [] if signer == unlabeled_signer else pred["labels"] + filtered_preds.append({**pred, "labels": labels}) + filtered_labels.extend(labels) + + render_context = _render_context_for_entities(filtered_labels) + output_dir = tmp_path / "out-partial" + output_dir.mkdir(exist_ok=True) + + output_path = PdfAnonymizer().anonymize( + {"path": str(source_path)}, + filtered_preds, + str(output_dir), + render_context=render_context, + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + page_text = page.get_text() + + assert list(page.widgets() or []) == [] + assert unlabeled_signer in page_text + assert "" not in page_text + + for index, signer in enumerate(signers[:-1], start=1): + assert signer not in page_text + assert f"" in page_text + + for preserved_text in preserved_texts: + _assert_text_count(page_text, preserved_text, 1) + + for qr_rect in qr_rects: + assert _dark_pixel_ratio(page, qr_rect) > 0.25 + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_preserves_non_signature_widget_appearance_when_baking( + tmp_path, +): + def configure(_doc: pymupdf.Document, page: pymupdf.Page) -> None: + page.insert_text((80, 88), "Ana Perez") + + text_widget = pymupdf.Widget() + text_widget.field_name = "public_field" + text_widget.field_type = pymupdf.PDF_WIDGET_TYPE_TEXT + text_widget.field_value = "Visible Field Value" + text_widget.text_font = "Helv" + text_widget.text_fontsize = 10 + text_widget.rect = pymupdf.Rect(260, 70, 410, 96) + page.add_widget(text_widget) + + signature_widget = pymupdf.Widget() + signature_widget.field_name = "sig_1" + signature_widget.field_type = pymupdf.PDF_WIDGET_TYPE_SIGNATURE + signature_widget.rect = pymupdf.Rect(60, 60, 180, 110) + page.add_widget(signature_widget) + + source_path = _write_pdf(tmp_path / "signature-and-text-widget.pdf", configure) + output_path = _run_pdf_anonymizer( + tmp_path, + source_path, + "Ana Perez", + [_label_dict("Ana Perez")], + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + page_text = page.get_text() + + assert list(page.widgets() or []) == [] + assert "Visible Field Value" in page_text + assert "Ana Perez" not in page_text + assert "" in page_text + + +@pytest.mark.integration +@WINDOWS_PYMUPDF_LAYOUT_XFAIL +def test_pdf_anonymizer_preserves_signature_appearance_when_redacting_signer_name( + tmp_path, +): + def configure(_doc: pymupdf.Document, page: pymupdf.Page) -> None: + page.insert_text((80, 76), "FIRMADO DIGITALMENTE") + page.insert_text((80, 92), "05/02/2025 14:17") + page.insert_text((80, 108), "Ana Perez") + page.insert_image(pymupdf.Rect(185, 68, 215, 98), stream=PNG_1X1) + widget = pymupdf.Widget() + widget.field_name = "sig_1" + widget.field_type = pymupdf.PDF_WIDGET_TYPE_SIGNATURE + widget.rect = pymupdf.Rect(60, 60, 230, 120) + page.add_widget(widget) + + source_path = _write_pdf(tmp_path / "signature.pdf", configure) + output_path = _run_pdf_anonymizer( + tmp_path, + source_path, + "Ana Perez", + [_label_dict("Ana Perez")], + ) + + with pymupdf.open(output_path) as output_doc: + page = output_doc[0] + page_text = page.get_text() + + assert list(page.widgets() or []) == [] + assert page.get_image_info() != [] + assert "FIRMADO DIGITALMENTE" in page_text + assert "05/02/2025 14:17" in page_text + assert "Ana Perez" not in page_text + assert "" in page_text + + +def test_index_paragraphs_reads_docx_xml_as_utf8(tmp_path): + xml_path = tmp_path / "document.xml" + xml_path.write_bytes( + """ + + + Señora — resolución + + +""".encode("utf-8") + ) + + paragraphs = index_paragraphs(str(xml_path)) + + assert len(paragraphs) == 1 + assert paragraphs[0]["plain_text"] == "Señora — resolución" + + +@pytest.mark.integration +def test_docx_anonymizer_sets_aymurai_core_properties(tmp_path): + source_path = tmp_path / "source.docx" + document = Document() + document.add_paragraph("Ana Perez firmo el escrito") + document.core_properties.author = "Sensitive Author" + document.core_properties.last_modified_by = "Sensitive Modifier" + document.save(source_path) + + started_at = datetime.now(timezone.utc).replace(microsecond=0) + + output_path = DocxAnonymizer().anonymize( + {"path": str(source_path)}, + [ + { + "document": "Ana Perez firmo el escrito", + "labels": [_label_dict("Ana Perez")], + } + ], + str(tmp_path / "out"), + ) + + output_document = Document(output_path) + core_properties = output_document.core_properties + assert core_properties.author == "" + assert core_properties.last_modified_by == "AymurAI" + assert core_properties.modified is not None + modified = core_properties.modified + if modified.tzinfo is None: + modified = modified.replace(tzinfo=timezone.utc) + assert started_at <= modified <= datetime.now(timezone.utc) + timedelta(seconds=5) + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_return_prediction_when_text_provided(mock_load_pipeline, client): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + response = client.post( + "/anonymizer/predict", + json={"text": "Sample anonymization text"}, + ) + + assert response.status_code == 200 + data = response.json() + assert "document" in data + assert "labels" in data + assert data["document"] == "Sample anonymization text" + assert isinstance(data["labels"], list) + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_return_cached_prediction_when_text_in_cache( + mock_load_pipeline, client, db_session +): + text = "Cached text with entities" + labels = [build_label("PER", "Juan Pérez").model_dump(mode="json")] + + paragraph_id = text_to_uuid(text) + cached_para = AnonymizationParagraph( + id=paragraph_id, + text=text, + prediction=labels, + ) + db_session.add(cached_para) + db_session.commit() + + response = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == text + assert data["labels"] == labels + mock_load_pipeline.assert_not_called() + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_store_prediction_in_db_when_use_cache_true( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + text = "New prediction to cache" + response = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + + paragraph_id = text_to_uuid(text) + stored = db_session.get(AnonymizationParagraph, paragraph_id) + assert stored is not None + assert stored.text == text + assert stored.prediction is not None + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_return_cached_result_when_calling_twice(mock_load_pipeline, client): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + text = "Repeated query text" + + response1 = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + data1 = response1.json() + + response2 = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + data2 = response2.json() + + assert response1.status_code == 200 + assert response2.status_code == 200 + assert data1["document"] == data2["document"] + assert data1["labels"] == data2["labels"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_return_prediction_without_storing_when_use_cache_false( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + text = "No cache storage text" + response = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": False}, + ) + + assert response.status_code == 200 + data = response.json() + assert "document" in data + assert "labels" in data + + paragraph_id = text_to_uuid(text) + stored = db_session.get(AnonymizationParagraph, paragraph_id) + assert stored is None + + +@pytest.mark.integration +def test_should_return_422_when_payload_is_invalid_json(client): + response = client.post( + "/anonymizer/predict", + content="not json", + headers={"Content-Type": "application/json"}, + ) + + assert response.status_code == 422 + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_use_cache_by_default_when_param_omitted( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + text = "Default cache behavior" + response = client.post( + "/anonymizer/predict", + json={"text": text}, + ) + + assert response.status_code == 200 + + paragraph_id = text_to_uuid(text) + stored = db_session.get(AnonymizationParagraph, paragraph_id) + assert stored is not None + assert stored.text == text + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_isolate_cache_when_different_texts( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + text1 = "First text for cache" + text2 = "Second text for cache" + + para1_id = text_to_uuid(text1) + para2_id = text_to_uuid(text2) + + labels1 = [build_label("PER", "Person1").model_dump(mode="json")] + labels2 = [build_label("LOC", "Location1").model_dump(mode="json")] + + para1 = AnonymizationParagraph(id=para1_id, text=text1, prediction=labels1) + para2 = AnonymizationParagraph(id=para2_id, text=text2, prediction=labels2) + + db_session.add_all([para1, para2]) + db_session.commit() + + response1 = client.post( + "/anonymizer/predict", + json={"text": text1}, + params={"use_cache": True}, + ) + + response2 = client.post( + "/anonymizer/predict", + json={"text": text2}, + params={"use_cache": True}, + ) + + assert response1.status_code == 200 + assert response2.status_code == 200 + + data1 = response1.json() + data2 = response2.json() + + assert data1["labels"] == labels1 + assert data2["labels"] == labels2 + assert data1["labels"] != data2["labels"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_dedupe_duplicate_labels_when_returning_cached_prediction( + mock_load_pipeline, client, db_session +): + text = "EL SEÑOR JUEZ, doctor Tarte, señaló :" + label = build_label("PER", "Tarte").model_dump(mode="json") + label.update({"start_char": 22, "end_char": 27}) + label["attrs"].update( + { + "aymurai_alt_text": "Tarte", + "aymurai_alt_start_char": 22, + "aymurai_alt_end_char": 27, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": True, + } + ) + + db_session.add( + AnonymizationParagraph( + id=text_to_uuid(text), + text=text, + prediction=[label, label], + ) + ) + db_session.commit() + + response = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == text + assert data["labels"] == [label] + mock_load_pipeline.assert_not_called() + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +def test_should_merge_cached_duplicate_labels_for_same_span_and_label( + mock_load_pipeline, client, db_session +): + text = ( + "Víctima: María Paula Trucha, DNI 23.456.789, quien se encuentra " + "conectada con su cámara apagada." + ) + dni_label = build_label("DNI", "23.456.789").model_dump(mode="json") + dni_label.update({"start_char": 33, "end_char": 43}) + dni_label["attrs"].update( + { + "aymurai_alt_text": "23.456.789", + "aymurai_alt_start_char": 33, + "aymurai_alt_end_char": 43, + "aymurai_label_instance": 2, + "aymurai_disambiguation": "fuzzy", + "aymurai_anonymize": True, + "canonical_entity_id": "0bba6d15-1b0c-51f0-b2ca-4fdc8a57cb73", + } + ) + enriched_dni_label = { + **dni_label, + "attrs": { + **dni_label["attrs"], + "aymurai_label_subclass": ["23456789"], + }, + } + + db_session.add( + AnonymizationParagraph( + id=text_to_uuid(text), + text=text, + prediction=[dni_label, enriched_dni_label], + ) + ) + db_session.commit() + + response = client.post( + "/anonymizer/predict", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == text + assert data["labels"] == [enriched_dni_label] + mock_load_pipeline.assert_not_called() + + +@pytest.mark.integration +@patch( + "aymurai.api.endpoints.routers.anonymizer.anonymizer.map_canonical_entities_ner_preds" +) +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_canonical_dates") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.build_canonical_entities") +def test_should_disambiguate_and_persist_paragraphs( + mock_build_canonical_entities, + mock_get_canonical_dates, + mock_map_canonical_entities, + client, + db_session, +): + mock_build_canonical_entities.return_value = [] + mock_get_canonical_dates.return_value = [] + mock_map_canonical_entities.side_effect = lambda predictions, canonical_entities: ( + predictions + ) + + text = "Ana Pérez denunció en el juzgado." + body = { + "paragraphs": [ + { + "document": text, + "labels": [build_label("PER", "Ana Pérez").model_dump(mode="json")], + } + ], + "label_policies": { + "PER": {"anonymize": True, "disambiguation": "none"}, + }, + } + + response = client.post("/anonymizer/disambiguate", json=body) + + assert response.status_code == 200 + payload = response.json() + assert payload["label_policies"]["PER"]["disambiguation"] == "none" + assert payload["data"][0]["labels"][0]["attrs"]["aymurai_disambiguation"] == "none" + assert payload["data"][0]["labels"][0]["attrs"]["aymurai_anonymize"] is True + + paragraph_id = text_to_uuid(text) + stored = db_session.get(AnonymizationParagraph, paragraph_id) + assert stored is not None + assert stored.prediction is not None + assert stored.prediction[0]["text"] == "Ana Pérez" + + +@pytest.mark.integration +@patch( + "aymurai.api.endpoints.routers.anonymizer.anonymizer.map_canonical_entities_ner_preds" +) +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_canonical_dates") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.build_canonical_entities") +def test_should_dedupe_duplicate_labels_when_disambiguating_and_persisting( + mock_build_canonical_entities, + mock_get_canonical_dates, + mock_map_canonical_entities, + client, + db_session, +): + mock_build_canonical_entities.return_value = [] + mock_get_canonical_dates.return_value = [] + mock_map_canonical_entities.side_effect = lambda predictions, canonical_entities: ( + predictions + ) + + text = "EL SEÑOR JUEZ, doctor Tarte, señaló :" + label = build_label("PER", "Tarte").model_dump(mode="json") + label.update({"start_char": 22, "end_char": 27}) + label["attrs"].update( + { + "aymurai_alt_text": "Tarte", + "aymurai_alt_start_char": 22, + "aymurai_alt_end_char": 27, + } + ) + body = { + "paragraphs": [{"document": text, "labels": [label, label]}], + "label_policies": { + "PER": {"anonymize": True, "disambiguation": "none"}, + }, + } + + response = client.post("/anonymizer/disambiguate", json=body) + + assert response.status_code == 200 + labels = response.json()["data"][0]["labels"] + assert labels == [ + { + **label, + "attrs": { + **label["attrs"], + "aymurai_disambiguation": "none", + "aymurai_anonymize": True, + }, + } + ] + + stored = db_session.get(AnonymizationParagraph, text_to_uuid(text)) + assert stored is not None + assert stored.prediction is not None + assert len(stored.prediction) == 1 + assert stored.prediction[0]["text"] == "Tarte" + assert stored.prediction[0]["attrs"]["aymurai_disambiguation"] == "none" + assert stored.prediction[0]["attrs"]["aymurai_anonymize"] is True + + +@pytest.mark.integration +def test_should_return_null_validation_when_paragraph_not_found(client): + response = client.post( + "/anonymizer/validation", + json={"text": "Paragraph without validation"}, + ) + + assert response.status_code == 200 + assert response.json() is None + + +@pytest.mark.integration +def test_should_return_validation_when_paragraph_exists(client, db_session): + text = "Validated paragraph" + labels = [build_label("PER", "María Soto").model_dump(mode="json")] + db_session.add( + AnonymizationParagraph( + id=text_to_uuid(text), + text=text, + validation=labels, + ) + ) + db_session.commit() + + response = client.post("/anonymizer/validation", json={"text": text}) + + assert response.status_code == 200 + assert response.json() == labels + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_anonymizer") +def test_should_return_application_pdf_when_pdf_document_is_anonymized( + mock_get_anonymizer, + client, + tmp_path, +): + anonymized_path = _write_pdf( + tmp_path / "output.pdf", + lambda _doc, page: page.insert_text((72, 72), "Anonymized PDF output"), + ) + mock_get_anonymizer.return_value = MagicMock(return_value=str(anonymized_path)) + + annotations = { + "data": [ + { + "document": "Ana Perez presento el escrito", + "labels": [build_label("PER", "Ana Perez").model_dump(mode="json")], + } + ], + "label_policies": {"PER": {"anonymize": True, "disambiguation": "none"}}, + "render_policy": {"suffix_mode": "auto", "suffix_threshold": 1}, + } + + response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={"file": ("sample.pdf", b"%PDF-1.4 fake", "application/pdf")}, + ) + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/pdf" + assert len(response.content) > 0 + + +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.subprocess.check_output") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_anonymizer") +def test_should_anonymize_document_when_annotations_are_valid( + mock_get_anonymizer, mock_check_output, client, tmp_path +): + # Fake anonymizer that writes a dummy docx output + anonymized_path = str(tmp_path / "output.docx") + with open(anonymized_path, "wb") as f: + f.write(b"fake-docx-content") + + mock_anonymizer = MagicMock(return_value=anonymized_path) + mock_get_anonymizer.return_value = mock_anonymizer + + def fake_convert(*args, **kwargs): + cmd = args[0] + source_path = cmd[-1] + output_path = source_path.rsplit(".", 1)[0] + ".odt" + with open(output_path, "wb") as output_file: + output_file.write(b"odt-content") + return "ok" + + mock_check_output.side_effect = fake_convert + annotations = { + "data": [ + { + "document": "Ana Pérez denunció en el juzgado.", + "labels": [build_label("PER", "Ana Pérez").model_dump(mode="json")], + } + ], + "label_policies": {"PER": {"anonymize": True, "disambiguation": "fuzzy"}}, + "render_policy": {"suffix_mode": "auto", "suffix_threshold": 1}, + } + + response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={ + "file": ( + "sample.docx", + b"input-document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/octet-stream" + assert len(response.content) > 0 + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.subprocess.check_output") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_anonymizer") +def test_should_exclude_null_alt_attrs_from_anonymize_document_preds( + mock_get_anonymizer, mock_check_output, client, tmp_path +): + anonymized_path = str(tmp_path / "output.docx") + with open(anonymized_path, "wb") as f: + f.write(b"fake-docx-content") + + mock_anonymizer = MagicMock(return_value=anonymized_path) + mock_get_anonymizer.return_value = mock_anonymizer + + def fake_convert(*args, **kwargs): + cmd = args[0] + source_path = cmd[-1] + output_path = source_path.rsplit(".", 1)[0] + ".odt" + with open(output_path, "wb") as output_file: + output_file.write(b"odt-content") + return "ok" + + mock_check_output.side_effect = fake_convert + annotations = { + "data": [ + { + "document": "Ana Perez denuncio en el juzgado.", + "labels": [build_label("PER", "Ana Perez").model_dump(mode="json")], + } + ], + "label_policies": {"PER": {"anonymize": True, "disambiguation": "fuzzy"}}, + "render_policy": {"suffix_mode": "auto", "suffix_threshold": 1}, + } + + response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={ + "file": ( + "sample.docx", + b"input-document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + + assert response.status_code == 200 + preds = mock_anonymizer.call_args[0][1] + assert preds[0]["labels"][0]["text"] == "Ana Perez" + + attrs = preds[0]["labels"][0]["attrs"] + assert "aymurai_alt_text" not in attrs + assert "aymurai_alt_start_char" not in attrs + assert "aymurai_alt_end_char" not in attrs + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.subprocess.check_output") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_anonymizer") +def test_should_return_500_when_anonymize_document_conversion_fails( + mock_get_anonymizer, mock_check_output, client, tmp_path +): + # Fake anonymizer that writes a dummy output + anonymized_path = str(tmp_path / "output.docx") + with open(anonymized_path, "wb") as f: + f.write(b"fake-docx-content") + + mock_anonymizer = MagicMock(return_value=anonymized_path) + mock_get_anonymizer.return_value = mock_anonymizer + + mock_check_output.side_effect = subprocess.CalledProcessError( + 1, + ["libreoffice"], + output=b"conversion failed", + ) + annotations = { + "data": [{"document": "text", "labels": []}], + "label_policies": None, + "render_policy": {"suffix_mode": "auto", "suffix_threshold": 1}, + } + + response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={ + "file": ( + "sample.docx", + b"input-document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + + assert response.status_code == 500 diff --git a/tests/api/routers/conftest.py b/tests/api/routers/conftest.py new file mode 100644 index 00000000..c60de7a0 --- /dev/null +++ b/tests/api/routers/conftest.py @@ -0,0 +1,27 @@ +from unittest.mock import MagicMock + +from tests.api.conftest import build_label + + +def build_mock_pipeline(): + mock = MagicMock() + + mock.preprocess.side_effect = lambda item: item + + def predict_single_impl(item): + item["predictions"] = {"entities": [build_label().model_dump(mode="json")]} + return item + + mock.predict_single.side_effect = predict_single_impl + + mock.postprocess.side_effect = lambda items: items + + return mock + + +def build_processed_data_item(text: str = "sample", labels: list | None = None): + return { + "path": "empty", + "data": {"doc.text": text}, + "predictions": {"entities": labels or []}, + } diff --git a/tests/api/routers/datapublic/__init__.py b/tests/api/routers/datapublic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/routers/datapublic/test_datapublic.py b/tests/api/routers/datapublic/test_datapublic.py new file mode 100644 index 00000000..69be0111 --- /dev/null +++ b/tests/api/routers/datapublic/test_datapublic.py @@ -0,0 +1,233 @@ +import uuid +from unittest.mock import patch + +import pytest + +from aymurai.database.schema import ( + DataPublicDocument, + DataPublicDocumentParagraph, + DataPublicParagraph, +) +from aymurai.database.utils import text_to_uuid +from tests.api.conftest import build_label +from tests.api.routers.conftest import build_mock_pipeline + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +def test_should_return_prediction_when_valid_document_id_and_text( + mock_load_pipeline, client +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-predict-valid") + response = client.post( + f"/datapublic/predict/{document_id}", + json={"text": "Sample datapublic text"}, + params={"use_cache": False}, + ) + + assert response.status_code == 200 + data = response.json() + assert "document" in data + assert "labels" in data + assert data["document"] == "Sample datapublic text" + assert isinstance(data["labels"], list) + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +def test_should_return_cached_prediction_when_text_in_cache( + mock_load_pipeline, client, db_session +): + text = "Cached datapublic text" + labels = [build_label("PER", "Juan González").model_dump(mode="json")] + + paragraph_id = text_to_uuid(text) + cached_para = DataPublicParagraph( + id=paragraph_id, + text=text, + prediction=labels, + ) + db_session.add(cached_para) + db_session.commit() + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-cached-text") + response = client.post( + f"/datapublic/predict/{document_id}", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == text + assert data["labels"] == labels + mock_load_pipeline.assert_not_called() + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +def test_should_store_paragraph_and_document_when_use_cache_true( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-store-cache-true") + text = "New datapublic paragraph" + + response = client.post( + f"/datapublic/predict/{document_id}", + json={"text": text}, + params={"use_cache": True}, + ) + + assert response.status_code == 200 + + paragraph_id = text_to_uuid(text) + stored_para = db_session.get(DataPublicParagraph, paragraph_id) + assert stored_para is not None + assert stored_para.text == text + assert stored_para.prediction is not None + + stored_doc = db_session.get(DataPublicDocument, document_id) + assert stored_doc is not None + + stored_link = ( + db_session.query(DataPublicDocumentParagraph) + .filter_by( + paragraph_id=paragraph_id, + document_id=document_id, + ) + .first() + ) + assert stored_link is not None + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +def test_should_return_prediction_without_storing_when_use_cache_false( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-cache-false") + text = "No datapublic storage text" + + response = client.post( + f"/datapublic/predict/{document_id}", + json={"text": text}, + params={"use_cache": False}, + ) + + assert response.status_code == 200 + data = response.json() + assert "document" in data + assert "labels" in data + + paragraph_id = text_to_uuid(text) + stored = db_session.get(DataPublicParagraph, paragraph_id) + assert stored is None + + +@pytest.mark.integration +def test_should_return_422_when_document_id_not_uuid(client): + response = client.post( + "/datapublic/predict/not-a-uuid", + json={"text": "Sample text"}, + ) + + assert response.status_code == 422 + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +def test_should_associate_multiple_paragraphs_with_same_document( + mock_load_pipeline, client, db_session +): + mock_pipeline = build_mock_pipeline() + mock_load_pipeline.return_value = mock_pipeline + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-associate-paragraphs") + text1 = "First paragraph for association" + text2 = "Second paragraph for association" + + response1 = client.post( + f"/datapublic/predict/{document_id}", + json={"text": text1}, + params={"use_cache": True}, + ) + + response2 = client.post( + f"/datapublic/predict/{document_id}", + json={"text": text2}, + params={"use_cache": True}, + ) + + assert response1.status_code == 200 + assert response2.status_code == 200 + + para1_id = text_to_uuid(text1) + para2_id = text_to_uuid(text2) + + stored_doc = db_session.get(DataPublicDocument, document_id) + assert stored_doc is not None + + links = ( + db_session.query(DataPublicDocumentParagraph) + .filter_by(document_id=document_id) + .all() + ) + assert len(links) == 2 + + link_para_ids = {link.paragraph_id for link in links} + assert para1_id in link_para_ids + assert para2_id in link_para_ids + + +@pytest.mark.integration +def test_should_return_404_when_validation_document_not_found(client): + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-validation-missing") + + response = client.get(f"/datapublic/validation/document/{document_id}") + + assert response.status_code == 404 + + +@pytest.mark.integration +def test_should_return_none_when_validation_not_set(client, db_session): + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-validation-empty") + db_session.add(DataPublicDocument(id=document_id)) + db_session.commit() + + response = client.get(f"/datapublic/validation/document/{document_id}") + + assert response.status_code == 200 + assert response.json() is None + + +@pytest.mark.integration +def test_should_upsert_and_read_document_validation(client, db_session): + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-validation-upsert") + payload = { + "materia": "penal", + "violencia_de_genero": "si", + "resolucion": {"tipo": "sentencia"}, + } + + post_response = client.post( + f"/datapublic/validation/document/{document_id}", + json=payload, + ) + assert post_response.status_code == 200 + + stored_doc = db_session.get(DataPublicDocument, document_id) + assert stored_doc is not None + assert stored_doc.validation == payload + + get_response = client.get(f"/datapublic/validation/document/{document_id}") + assert get_response.status_code == 200 + assert get_response.json() == payload diff --git a/tests/api/routers/misc/__init__.py b/tests/api/routers/misc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/routers/misc/test_document_extract.py b/tests/api/routers/misc/test_document_extract.py new file mode 100644 index 00000000..124c4b9c --- /dev/null +++ b/tests/api/routers/misc/test_document_extract.py @@ -0,0 +1,309 @@ +import concurrent.futures +import io +import sys +from unittest.mock import patch + +import pytest + +from aymurai.database.utils import data_to_uuid + + +def _build_docx_bytes(paragraphs: list[str]) -> bytes: + import docx + + document = docx.Document() + for paragraph in paragraphs: + document.add_paragraph(paragraph) + + stream = io.BytesIO() + document.save(stream) + return stream.getvalue() + + +def _build_pdf_bytes(paragraphs: list[str]) -> bytes: + import pymupdf + + pdf_document = pymupdf.open() + page = pdf_document.new_page() # type: ignore + for index, paragraph in enumerate(paragraphs): + page.insert_text((72, 72 + (index * 36)), paragraph) + + try: + to_bytes = getattr(pdf_document, "tobytes", None) + if callable(to_bytes): + serialized = to_bytes() + if isinstance(serialized, bytes): + return serialized + raise TypeError("Expected bytes from pymupdf tobytes()") + + serialized = pdf_document.write() + if isinstance(serialized, bytes): + return serialized + raise TypeError("Expected bytes from pymupdf write()") + finally: + pdf_document.close() + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_extract_real_text_from_sample_docx_without_mocking(client): + """Test that a generated DOCX is extracted without mocking.""" + expected_paragraphs = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", + ] + file_content = _build_docx_bytes(expected_paragraphs) + files = { + "file": ( + "sample.docx", + file_content, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document_id"] == str(data_to_uuid(file_content)) + assert data["document"] + extracted_text = " ".join(data["document"]) + for paragraph in expected_paragraphs: + assert paragraph in extracted_text + + +@pytest.mark.integration +@pytest.mark.slow +@pytest.mark.xfail( + sys.platform == "win32", + reason="pymupdf4llm ONNX layout model receives int32 tensors on Windows (expects int64)", + strict=False, +) +def test_should_extract_real_text_from_pdf_without_mocking(client): + """Test that a real PDF upload is extracted without mocking.""" + expected_paragraphs = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", + ] + file_content = _build_pdf_bytes(expected_paragraphs) + files = { + "file": ( + "sample.pdf", + file_content, + "application/pdf", + ) + } + + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document_id"] == str(data_to_uuid(file_content)) + assert data["document"] + extracted_text = " ".join(data["document"]) + for paragraph in expected_paragraphs: + assert paragraph in extracted_text + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_document_with_paragraphs_when_uploading_docx( + mock_extraction, client +): + """Test that document extraction returns paragraphs in response.""" + mock_extraction.return_value = "Para 1\nPara 2\nPara 3" + + files = { + "file": ( + "test.docx", + b"test content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert "document" in data + assert "document_id" in data + assert isinstance(data["document"], list) + assert len(data["document"]) == 3 + assert data["document"] == ["Para 1", "Para 2", "Para 3"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_document_via_misc_prefix_when_uploading(mock_extraction, client): + """Test that /misc/document-extract endpoint works.""" + mock_extraction.return_value = "Sample text" + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/misc/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == ["Sample text"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_document_via_deprecated_alias_when_uploading( + mock_extraction, client +): + """Test that deprecated /document-extract alias is still available.""" + mock_extraction.return_value = "Alias text" + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + assert response.json()["document"] == ["Alias text"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_deterministic_id_when_uploading_same_file_twice( + mock_extraction, client +): + """Test that same file bytes produce same document_id.""" + mock_extraction.return_value = "Same content" + + file_content = b"identical content" + files = { + "file": ( + "test.docx", + file_content, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + + response1 = client.post("/document-extract", files=files) + data1 = response1.json() + + # Reset mock and upload same file again + response2 = client.post("/document-extract", files=files) + data2 = response2.json() + + assert response1.status_code == 200 + assert response2.status_code == 200 + assert data1["document_id"] == data2["document_id"] + + +@pytest.mark.integration +def test_should_return_422_when_no_file_provided(client): + """Test that missing file returns 422 Unprocessable Entity.""" + response = client.post("/document-extract", files={}) + + assert response.status_code == 422 + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_empty_document_when_file_is_empty(mock_extraction, client): + """Test that empty extraction returns empty document list.""" + mock_extraction.return_value = "" + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == [] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_collapse_consecutive_duplicates_when_extracting( + mock_extraction, client +): + """Test that consecutive duplicate paragraphs are collapsed.""" + mock_extraction.return_value = "A\nA\nB\nB\nB\nC" + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == ["A", "B", "C"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_normalize_whitespace_when_extracting(mock_extraction, client): + """Test that multiple spaces are normalized to single space.""" + mock_extraction.return_value = "word1 word2 word3" + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 200 + data = response.json() + assert data["document"] == ["word1 word2 word3"] + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_504_when_extraction_times_out(mock_extraction, client): + """Test that TimeoutError returns 504 Gateway Timeout.""" + mock_extraction.side_effect = concurrent.futures.TimeoutError() + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 504 + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_return_500_when_extraction_fails(mock_extraction, client): + """Test that extraction errors return 500 Internal Server Error.""" + mock_extraction.side_effect = Exception("Extraction failed") + + files = { + "file": ( + "test.docx", + b"content", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + } + response = client.post("/document-extract", files=files) + + assert response.status_code == 500 diff --git a/tests/api/routers/test_pipeline_flows.py b/tests/api/routers/test_pipeline_flows.py new file mode 100644 index 00000000..8d53952d --- /dev/null +++ b/tests/api/routers/test_pipeline_flows.py @@ -0,0 +1,201 @@ +import io +import json +import shutil +import uuid +from unittest.mock import MagicMock, patch + +import pytest +from docx import Document as DocxDocument + +from aymurai.database.schema import DataPublicDocumentParagraph +from tests.api.routers.conftest import build_mock_pipeline + + +def _fake_libreoffice_convert(*args, **kwargs): + cmd = args[0] + source_path = cmd[-1] + output_path = source_path.rsplit(".", 1)[0] + ".odt" + with open(output_path, "wb") as output_file: + output_file.write(b"odt-content") + return "ok" + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.subprocess.check_output") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_anonymizer") +@patch( + "aymurai.api.endpoints.routers.anonymizer.anonymizer.map_canonical_entities_ner_preds" +) +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.get_canonical_dates") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.build_canonical_entities") +@patch("aymurai.api.endpoints.routers.anonymizer.anonymizer.load_pipeline") +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_run_anonymizer_flow_end_to_end( + mock_extract, + mock_load_pipeline, + mock_build_canonical_entities, + mock_get_canonical_dates, + mock_map_canonical_entities, + mock_get_anonymizer, + mock_check_output, + client, + tmp_path, +): + mock_extract.return_value = "Ana Pérez denunció.\nJuan Soto declaró." + mock_load_pipeline.return_value = build_mock_pipeline() + mock_build_canonical_entities.return_value = [] + mock_get_canonical_dates.return_value = [] + mock_map_canonical_entities.side_effect = lambda predictions, canonical_entities: ( + predictions + ) + + anonymized_path = str(tmp_path / "output.docx") + with open(anonymized_path, "wb") as f: + f.write(b"fake-docx-content") + mock_anonymizer = MagicMock(return_value=anonymized_path) + mock_get_anonymizer.return_value = mock_anonymizer + mock_check_output.side_effect = _fake_libreoffice_convert + + extract_response = client.post( + "/misc/document-extract", + files={ + "file": ( + "sample.docx", + b"doc-bytes", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + assert extract_response.status_code == 200 + paragraphs = extract_response.json()["document"] + assert len(paragraphs) == 2 + + predictions = [] + for paragraph in paragraphs: + predict_response = client.post("/anonymizer/predict", json={"text": paragraph}) + assert predict_response.status_code == 200 + predictions.append(predict_response.json()) + + disambiguate_response = client.post( + "/anonymizer/disambiguate", + json={ + "paragraphs": predictions, + "label_policies": { + "PER": {"anonymize": True, "disambiguation": "fuzzy"}, + }, + }, + ) + assert disambiguate_response.status_code == 200 + annotations = disambiguate_response.json() + assert len(annotations["data"]) == len(paragraphs) + + compile_response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={ + "file": ( + "sample.docx", + b"doc-bytes", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + assert compile_response.status_code == 200 + assert compile_response.headers["content-type"] == "application/octet-stream" + + validation_response = client.post( + "/anonymizer/validation", + json={"text": paragraphs[0]}, + ) + assert validation_response.status_code == 200 + assert isinstance(validation_response.json(), list) + + +@pytest.mark.integration +@patch("aymurai.api.endpoints.routers.datapublic.datapublic.load_pipeline") +@patch("aymurai.api.endpoints.routers.misc.document_extract.run_safe_text_extraction") +def test_should_run_datapublic_flow_end_to_end( + mock_extract, + mock_load_pipeline, + client, + db_session, +): + mock_extract.return_value = "Primera oración.\nSegunda oración." + mock_load_pipeline.return_value = build_mock_pipeline() + + extract_response = client.post( + "/misc/document-extract", + files={ + "file": ( + "sample.docx", + b"doc-bytes", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + assert extract_response.status_code == 200 + paragraphs = extract_response.json()["document"] + assert len(paragraphs) == 2 + + document_id = uuid.uuid5(uuid.NAMESPACE_URL, "datapublic-e2e-flow") + for paragraph in paragraphs: + predict_response = client.post( + f"/datapublic/predict/{document_id}", + json={"text": paragraph}, + ) + assert predict_response.status_code == 200 + + links = ( + db_session.query(DataPublicDocumentParagraph) + .filter_by(document_id=document_id) + .all() + ) + assert len(links) == len(paragraphs) + + validation_payload = {"materia": "penal", "violencia_de_genero": "si"} + save_response = client.post( + f"/datapublic/validation/document/{document_id}", + json=validation_payload, + ) + assert save_response.status_code == 200 + + read_response = client.get(f"/datapublic/validation/document/{document_id}") + assert read_response.status_code == 200 + assert read_response.json() == validation_payload + + +@pytest.mark.integration +@pytest.mark.slow +@pytest.mark.external +def test_should_compile_anonymized_document_with_real_libreoffice_when_available( + client, +): + if shutil.which("libreoffice") is None: + pytest.skip("LibreOffice binary is required for real compile integration test") + + annotations = { + "data": [{"document": "Texto base para anonimizar.", "labels": []}], + "label_policies": None, + "render_policy": {"suffix_mode": "auto", "suffix_threshold": 1}, + } + + doc = DocxDocument() + doc.add_paragraph("Texto base para anonimizar.") + buf = io.BytesIO() + doc.save(buf) + docx_bytes = buf.getvalue() + + response = client.post( + "/anonymizer/anonymize-document", + data={"annotations": json.dumps(annotations)}, + files={ + "file": ( + "sample.docx", + docx_bytes, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) + }, + ) + + assert response.status_code == 200 + assert response.headers["content-type"] == "application/octet-stream" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/pipelines/__init__.py b/tests/integration/pipelines/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/pipelines/conftest.py b/tests/integration/pipelines/conftest.py new file mode 100644 index 00000000..548e9110 --- /dev/null +++ b/tests/integration/pipelines/conftest.py @@ -0,0 +1,63 @@ +import os +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +import pytest + +if TYPE_CHECKING: + from aymurai.pipeline.pipeline import AymurAIPipeline + +os.environ.setdefault("DISKCACHE_ROOT", "resources/cache/diskcache") +os.environ.setdefault("AYMURAI_CACHE_BASEPATH", "resources/cache/aymurai") + + +PIPELINE_CONFIGS = { + "anonymizer": "resources/pipelines/production/flair-anonymizer", + "datapublic": "resources/pipelines/production/datapublic", +} + + +def load_test_pipeline(name: str) -> "AymurAIPipeline": + from aymurai.pipeline.pipeline import AymurAIPipeline + + if name not in PIPELINE_CONFIGS: + raise ValueError( + f"Unknown pipeline: {name}. Available: {list(PIPELINE_CONFIGS.keys())}" + ) + + path = PIPELINE_CONFIGS[name] + return AymurAIPipeline.load(path, print_config=False) + + +@pytest.fixture(scope="session") +def anonymizer_pipeline() -> "AymurAIPipeline": + return load_test_pipeline("anonymizer") + + +@pytest.fixture(scope="session") +def datapublic_pipeline() -> "AymurAIPipeline": + return load_test_pipeline("datapublic") + + +@pytest.fixture +def sample_text() -> str: + return ( + "El día 15 de marzo de 2023, el Sr. Juan Pérez fue imputado por el delito de " + "lesiones graves. La víctima, María González, declaró en el Juzgado Civil de Buenos Aires." + ) + + +def _build_pipeline_input(text: str) -> dict[str, Any]: + return { + "path": "test", + "extension": "", + "dataset": "", + "data": {"doc.text": text}, + "annotations": None, + "predictions": {}, + } + + +@pytest.fixture +def build_pipeline_input() -> Callable[[str], dict[str, Any]]: + return _build_pipeline_input diff --git a/tests/integration/pipelines/test_anonymizer.py b/tests/integration/pipelines/test_anonymizer.py new file mode 100644 index 00000000..faf461a2 --- /dev/null +++ b/tests/integration/pipelines/test_anonymizer.py @@ -0,0 +1,95 @@ +from typing import Any + +import pytest + +from aymurai.pipeline.pipeline import AymurAIPipeline + + +def _extract_entities(item: dict[str, Any]) -> list[dict[str, Any]]: + predictions = item.get("predictions") + if isinstance(predictions, dict): + entities = predictions.get("entities") or [] + if isinstance(entities, list): + return [entity for entity in entities if isinstance(entity, dict)] + return [] + + +@pytest.fixture +def input_item(sample_text: str, build_pipeline_input) -> dict[str, Any]: + return build_pipeline_input(sample_text) + + +@pytest.fixture +def preprocessed_item( + anonymizer_pipeline, + input_item: dict[str, Any], +) -> dict[str, Any]: + return anonymizer_pipeline.preprocess([input_item])[0] + + +@pytest.fixture +def predicted_item( + anonymizer_pipeline, + preprocessed_item: dict[str, Any], +) -> dict[str, Any]: + return anonymizer_pipeline.predict_single(preprocessed_item) + + +@pytest.fixture +def postprocessed_item( + anonymizer_pipeline, + predicted_item: dict[str, Any], +) -> dict[str, Any]: + return anonymizer_pipeline.postprocess([predicted_item])[0] + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_load_pipeline_when_given_production_config(anonymizer_pipeline): + assert isinstance(anonymizer_pipeline, AymurAIPipeline) + assert hasattr(anonymizer_pipeline, "pre_process") + assert hasattr(anonymizer_pipeline, "training_pipeline") + assert hasattr(anonymizer_pipeline, "post_process") + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_preprocess_when_given_text_input(preprocessed_item: dict[str, Any]): + assert isinstance(preprocessed_item, dict) + assert "data" in preprocessed_item + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_predict_single_when_given_preprocessed_item( + predicted_item: dict[str, Any], +): + assert isinstance(predicted_item, dict) + assert "predictions" in predicted_item + assert predicted_item["predictions"] is not None + entities = _extract_entities(predicted_item) + assert entities, "Expected at least one anonymizer entity in canonical sample text" + first_entity = entities[0] + assert first_entity.get("text") + assert isinstance(first_entity.get("start_char"), int) + assert isinstance(first_entity.get("end_char"), int) + assert isinstance(first_entity.get("attrs"), dict) + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_postprocess_when_given_predicted_items( + postprocessed_item: dict[str, Any], +): + assert isinstance(postprocessed_item, dict) + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_produce_anonymization_entities_when_running_full_chain( + postprocessed_item: dict[str, Any], +): + result = postprocessed_item + assert "predictions" in result + entities = _extract_entities(result) + assert entities, "Expected postprocess to preserve anonymizer entities" diff --git a/tests/integration/pipelines/test_datapublic.py b/tests/integration/pipelines/test_datapublic.py new file mode 100644 index 00000000..23a0c58b --- /dev/null +++ b/tests/integration/pipelines/test_datapublic.py @@ -0,0 +1,95 @@ +from typing import Any + +import pytest + +from aymurai.pipeline.pipeline import AymurAIPipeline + + +def _extract_entities(item: dict[str, Any]) -> list[dict[str, Any]]: + predictions = item.get("predictions") + if isinstance(predictions, dict): + entities = predictions.get("entities") or [] + if isinstance(entities, list): + return [entity for entity in entities if isinstance(entity, dict)] + return [] + + +@pytest.fixture +def input_item(sample_text: str, build_pipeline_input) -> dict[str, Any]: + return build_pipeline_input(sample_text) + + +@pytest.fixture +def preprocessed_item( + datapublic_pipeline, + input_item: dict[str, Any], +) -> dict[str, Any]: + return datapublic_pipeline.preprocess([input_item])[0] + + +@pytest.fixture +def predicted_item( + datapublic_pipeline, + preprocessed_item: dict[str, Any], +) -> dict[str, Any]: + return datapublic_pipeline.predict_single(preprocessed_item) + + +@pytest.fixture +def postprocessed_item( + datapublic_pipeline, + predicted_item: dict[str, Any], +) -> dict[str, Any]: + return datapublic_pipeline.postprocess([predicted_item])[0] + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_load_pipeline_when_given_production_config(datapublic_pipeline): + assert isinstance(datapublic_pipeline, AymurAIPipeline) + assert hasattr(datapublic_pipeline, "pre_process") + assert hasattr(datapublic_pipeline, "training_pipeline") + assert hasattr(datapublic_pipeline, "post_process") + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_preprocess_when_given_text_input(preprocessed_item: dict[str, Any]): + assert isinstance(preprocessed_item, dict) + assert "data" in preprocessed_item + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_predict_single_when_given_preprocessed_item( + predicted_item: dict[str, Any], +): + assert isinstance(predicted_item, dict) + assert "predictions" in predicted_item + assert predicted_item["predictions"] is not None + entities = _extract_entities(predicted_item) + assert entities, "Expected at least one datapublic entity in canonical sample text" + first_entity = entities[0] + assert first_entity.get("text") + assert isinstance(first_entity.get("start_char"), int) + assert isinstance(first_entity.get("end_char"), int) + assert isinstance(first_entity.get("attrs"), dict) + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_postprocess_when_given_predicted_items( + postprocessed_item: dict[str, Any], +): + assert isinstance(postprocessed_item, dict) + + +@pytest.mark.integration +@pytest.mark.slow +def test_should_produce_entities_when_running_full_chain( + postprocessed_item: dict[str, Any], +): + result = postprocessed_item + assert "predictions" in result + entities = _extract_entities(result) + assert entities, "Expected postprocess to preserve datapublic entities" diff --git a/tutorials/00-annotations/01-export-docs.ipynb b/tutorials/00-annotations/01-export-docs.ipynb deleted file mode 100644 index c6f94177..00000000 --- a/tutorials/00-annotations/01-export-docs.ipynb +++ /dev/null @@ -1,147 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Export documents\n", - "This notebook shows how to export documents from a path to a file that LabelStudio can read.\n", - "This is useful when you have a lot of documents and you want to use LabelStudio to label them.\n", - "\n", - "In this example we are going to export anonymized documents from the 10 criminal court from the Ciudad de Buenos Aires, Argentina. The anonymization consist on replacing the names or other sensible data of the parties with a generic name." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# load data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from glob import glob\n", - "\n", - "DOCS_PATH = '/resources/data/sample'\n", - "\n", - "paths = glob(f\"{DOCS_PATH}/**/*.doc\", recursive=True)\n", - "paths += glob(f\"{DOCS_PATH}/**/*.docx\", recursive=True)\n", - "\n", - "docs = [{\"path\": path} for path in paths]\n", - "print(\"doc files:\", len(docs))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.pipeline import AymurAIPipeline\n", - "from aymurai.text.extraction import FulltextExtract\n", - "from aymurai.text.normalize import TextNormalize\n", - "\n", - "config = {\n", - " \"preprocess\": [\n", - " (\n", - " FulltextExtract,\n", - " {\n", - " \"errors\": \"ignore\",\n", - " \"use_cache\": False,\n", - " },\n", - " ),\n", - " (TextNormalize, {}),\n", - " ],\n", - " \"models\": [],\n", - " \"postprocess\": [],\n", - " \"multiprocessing\": {},\n", - " \"use_cache\": False,\n", - " # 'log_level': 'debug'\n", - "}\n", - "\n", - "pipeline = AymurAIPipeline(config)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "preprocessed = pipeline.preprocess(docs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# export to labelstudio" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "def to_labelstudio_json(item):\n", - " obj = {\n", - " 'text': item['data']['doc.text'],\n", - " 'meta_info': {\n", - " 'path': item['path']\n", - " }\n", - " }\n", - " return obj\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "\n", - "export = map(to_labelstudio_json, preprocessed)\n", - "export = list(export)\n", - "\n", - "with open('dump-docs-labelstudio.json', 'w') as file:\n", - " json.dump(export, file)\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Oct 12 2022, 19:14:26) [GCC 9.4.0]" - }, - "vscode": { - "interpreter": { - "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/00-annotations/02-doc-visualization.ipynb b/tutorials/00-annotations/02-doc-visualization.ipynb deleted file mode 100644 index 6777549f..00000000 --- a/tutorials/00-annotations/02-doc-visualization.ipynb +++ /dev/null @@ -1,137 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Court ruling visualization\n", - "This notebook shows the annotated data.\n", - "\n", - "First, we need to load a sample of data. We are going to use annonimized data from the [data-preparation](../data-preparation) tutorial.\n", - "\n", - "Finaly, we display the annotations and their scores using nice displays by [spacy](https://spacy.io/)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Load annotatations dataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.datasets.ar_juz_pcyf_10.annotations import (\n", - " ArgentinaJuzgadoPCyF10LabelStudioAnnotations,\n", - ")\n", - "\n", - "docs = ArgentinaJuzgadoPCyF10LabelStudioAnnotations(\"/resources/data/sample\").data\n", - "\n", - "sample = docs[:10]\n", - "\n", - "print(len(docs))\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# process one document\n", - "one document paragraph by paragraph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "doc = sample[5:6]\n", - "pars = [\n", - " {\n", - " \"path\": \"empty\",\n", - " \"data\": {\n", - " \"doc.text\": par,\n", - " },\n", - " }\n", - " for par in doc[0][\"data\"][\"doc.text\"].splitlines()\n", - "]\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25daea97", - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.utils.display.render import DocRender, add_score_to_label, set_color\n", - "from aymurai.utils.display.colors import colors\n", - "from aymurai.utils.misc import get_element\n", - "from copy import deepcopy\n", - "\n", - "render = DocRender(ents_field=\"annotations\")\n", - "\n", - "\n", - "for item in deepcopy(doc):\n", - " ents = get_element(item, ['annotations', 'entities']) or []\n", - " ents = [set_color(ent, mapping=colors) for ent in ents]\n", - " # ents = [add_score_to_label(ent) for ent in ents]\n", - " item['annotations']['entities'] = ents\n", - " \n", - " options = {}\n", - " render(item, style='span', spans_key='sc', config=options)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9 (main, Dec 19 2022, 17:35:49) [GCC 12.2.0]" - }, - "vscode": { - "interpreter": { - "hash": "97cc609b13305c559618ec78a438abc56230b9381f827f22d070313b9a1f3777" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/00-annotations/README.md b/tutorials/00-annotations/README.md deleted file mode 100644 index 7b7fda50..00000000 --- a/tutorials/00-annotations/README.md +++ /dev/null @@ -1,25 +0,0 @@ -Annotation procedure -==================== - -In alliance with Criminal Court No. 10 of the Autonomous City of Buenos Aires, Argentina, we have developed a procedure to annotate court rulings. -We collect 1200 court rulings. -The annotations are made in a token classification task, where each token is annotated with the corresponding entity (27 in total, the complete list can be found [here](../data/en/entities-table.md)). -Also we subcategorize any entity that requires it. -These annotations were made using the [label studio](https://labelstud.io/) tool. -The configuration of the annotation task is available in the [label studio config file](../data/label-studio-config.xml). - -We are very concerned with data security (see [SECURITY.md](../../docs/SECURITY.md)), and since the legal ruling contains sensible data, we cannot share the data. But a sample of annonimized annotations can be found [here](../../resources/data/sample). - -The first step is to export the raw court rulings to a json that can reads label studio. Check out the [export](01-export-docs.ipynb) guide for details. - -Assuming you already have annotated documents, you can check the visualization of the annotations in the [visualization](02-visualization.ipynb) example. - -# Contributors -* Diego Scopetta -* Franny Rodriguez Gerzovich ([email](fraanyrodriguez@gmail.com)|[linkedin](https://www.linkedin.com/in/francescarg)) -* Laura Barreiro -* Matías Sosa -* Maximiliano Sosa -* Patricia Sandoval -* Santiago Bezchinsky ([email](santibezchinsky@gmail.com)|[linkedin](https://www.linkedin.com/in/santiago-bezchinsky)) -* Zoe Rodriguez Gerzovich diff --git a/tutorials/01-pipeline/01-evaluation.ipynb b/tutorials/01-pipeline/01-evaluation.ipynb deleted file mode 100644 index e1c5a11f..00000000 --- a/tutorials/01-pipeline/01-evaluation.ipynb +++ /dev/null @@ -1,169 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Pipeline evaluation\n", - "This notebook shows how to run the pipeline inference.\n", - "\n", - "First, we need to load a sample of data. We are going to use annonimized data from the [data-preparation](../data-preparation) tutorial.\n", - "\n", - "Then, we load the pipeline and run the inference. Take in mind that the documents needs to be splitted in paragrpahs before running the pipeline.\n", - "\n", - "Finaly, we display the annotations and their scores using nice displays by [spacy](https://spacy.io/)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Load annotatations dataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.datasets.ar_juz_pcyf_10.annotations import (\n", - " ArgentinaJuzgadoPCyF10LabelStudioAnnotations,\n", - ")\n", - "\n", - "docs = ArgentinaJuzgadoPCyF10LabelStudioAnnotations(\"/resources/data/sample\").data\n", - "\n", - "sample = docs[:10]\n", - "\n", - "print(len(docs))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Pipeline load\n", - "This pipeline is the same used by the api" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.pipeline import AymurAIPipeline\n", - "\n", - "pipeline = AymurAIPipeline.load(\"/resources/pipelines/production/full-paragraph\")\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# process one document\n", - "one document paragraph by paragraph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "doc = sample[5:6]\n", - "pars = [\n", - " {\n", - " \"path\": \"empty\",\n", - " \"data\": {\n", - " \"doc.text\": par,\n", - " },\n", - " }\n", - " for par in doc[0][\"data\"][\"doc.text\"].splitlines()\n", - "]\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "preprocessed = pipeline.preprocess(pars)\n", - "predicted = pipeline.predict(preprocessed)\n", - "postprocessed = pipeline.postprocess(predicted)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25daea97", - "metadata": {}, - "outputs": [], - "source": [ - "from aymurai.utils.display.render import DocRender, add_score_to_label, set_color\n", - "from aymurai.utils.display.colors import colors\n", - "from aymurai.utils.misc import get_element\n", - "from copy import deepcopy\n", - "\n", - "render = DocRender()\n", - "\n", - "\n", - "for item in deepcopy(postprocessed):\n", - " ents = get_element(item, ['predictions', 'entities']) or []\n", - " ents = [set_color(ent, mapping=colors) for ent in ents]\n", - " ents = [add_score_to_label(ent) for ent in ents]\n", - " item['predictions']['entities'] = ents\n", - " \n", - " options = {}\n", - " render(item, style='span', spans_key='sc', config=options)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9 (main, Dec 19 2022, 17:35:49) [GCC 12.2.0]" - }, - "vscode": { - "interpreter": { - "hash": "97cc609b13305c559618ec78a438abc56230b9381f827f22d070313b9a1f3777" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/01-pipeline/02-run-ner-from-huggingface.ipynb b/tutorials/01-pipeline/02-run-ner-from-huggingface.ipynb deleted file mode 100644 index c1643dd2..00000000 --- a/tutorials/01-pipeline/02-run-ner-from-huggingface.ipynb +++ /dev/null @@ -1,64 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from flair.data import Sentence\n", - "from flair.models import SequenceTagger\n", - "\n", - "tagger = SequenceTagger.load(\"aymurai/flair-ner-ar_juz_pcyf10\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# make example sentence\n", - "sentence = Sentence(\"1. DECLARAR EXTINGUIDA LA ACCIÓN PENAL en este caso por cumplimiento de la suspensión del proceso a prueba, y SOBRESEER a EZEQUIEL CAMILO MARCONNI, DNI 11.222.333, en orden a los delitos de lesiones leves agravadas, amenazas simples y agravadas por el uso de armas.\")\n", - "\n", - "# predict NER tags\n", - "tagger.predict(sentence)\n", - "\n", - "# print sentence\n", - "print(sentence)\n", - "\n", - "# print predicted NER spans\n", - "print('The following NER tags are found:')\n", - "# iterate over entities and print\n", - "for entity in sentence.get_spans('ner'):\n", - " print(entity)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Oct 12 2022, 19:14:26) [GCC 9.4.0]" - }, - "vscode": { - "interpreter": { - "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/GET_STARTED.md b/tutorials/GET_STARTED.md deleted file mode 100644 index 4465959c..00000000 --- a/tutorials/GET_STARTED.md +++ /dev/null @@ -1,4 +0,0 @@ -# Get started - -For developers looking to get started with the code. First please read our [contributing guidelines](docs/CONTRIBUTING.md) and [code of conduct](docs/CODE_OF_CONDUCT.md). We recommend use the devcontainer to run the code. You can check the [devcontainer documentation](https://code.visualstudio.com/docs/remote/containers) for more information. -Then you can check the [tutorials](tutorials/GET_STARTED.md) to get started with the code. \ No newline at end of file diff --git a/uv.lock b/uv.lock index 87d09d15..2d4cd1e6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,18 +1,14 @@ version = 1 +revision = 3 requires-python = "==3.10.*" - -[[package]] -name = "absl-py" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/8f/fc001b92ecc467cc32ab38398bd0bfb45df46e7523bf33c2ad22a505f06e/absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff", size = 118055 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308", size = 133706 }, +required-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'AMD64' and sys_platform == 'win32'", ] [[package]] name = "accelerate" -version = "1.2.1" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -23,23 +19,23 @@ dependencies = [ { name = "safetensors" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/09/7947691b7d44bfc739da4a44cc47d6a6d75e6fe9adf047c5234d7cb6be64/accelerate-1.2.1.tar.gz", hash = "sha256:03e161fc69d495daf2b9b5c8d5b43d06e2145520c04727b5bda56d49f1a43ab5", size = 341652 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/60/a585c806d6c0ec5f8149d44eb202714792802f484e6e2b1bf96b23bd2b00/accelerate-1.2.1-py3-none-any.whl", hash = "sha256:be1cbb958cf837e7cdfbde46b812964b1b8ae94c9c7d94d921540beafcee8ddf", size = 336355 }, + { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, ] [[package]] name = "aiohappyeyeballs" -version = "2.4.4" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -51,194 +47,188 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/7d/ff2e314b8f9e0b1df833e2d4778eaf23eae6b8cc8f922495d110ddcbf9e1/aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", size = 708550 }, - { url = "https://files.pythonhosted.org/packages/09/b8/aeb4975d5bba233d6f246941f5957a5ad4e3def8b0855a72742e391925f2/aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", size = 468430 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/5b620279b3df46e597008b09fa1e10027a39467387c2332657288e25811a/aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", size = 455593 }, - { url = "https://files.pythonhosted.org/packages/d8/75/0cdf014b816867d86c0bc26f3d3e3f194198dbf33037890beed629cd4f8f/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", size = 1584635 }, - { url = "https://files.pythonhosted.org/packages/df/2f/95b8f4e4dfeb57c1d9ad9fa911ede35a0249d75aa339edd2c2270dc539da/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", size = 1632363 }, - { url = "https://files.pythonhosted.org/packages/39/cb/70cf69ea7c50f5b0021a84f4c59c3622b2b3b81695f48a2f0e42ef7eba6e/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", size = 1668315 }, - { url = "https://files.pythonhosted.org/packages/2f/cc/3a3fc7a290eabc59839a7e15289cd48f33dd9337d06e301064e1e7fb26c5/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", size = 1589546 }, - { url = "https://files.pythonhosted.org/packages/15/b4/0f7b0ed41ac6000e283e7332f0f608d734b675a8509763ca78e93714cfb0/aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", size = 1544581 }, - { url = "https://files.pythonhosted.org/packages/58/b9/4d06470fd85c687b6b0e31935ef73dde6e31767c9576d617309a2206556f/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", size = 1529256 }, - { url = "https://files.pythonhosted.org/packages/61/a2/6958b1b880fc017fd35f5dfb2c26a9a50c755b75fd9ae001dc2236a4fb79/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", size = 1536592 }, - { url = "https://files.pythonhosted.org/packages/0f/dd/b974012a9551fd654f5bb95a6dd3f03d6e6472a17e1a8216dd42e9638d6c/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", size = 1607446 }, - { url = "https://files.pythonhosted.org/packages/e0/d3/6c98fd87e638e51f074a3f2061e81fcb92123bcaf1439ac1b4a896446e40/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", size = 1628809 }, - { url = "https://files.pythonhosted.org/packages/a8/2e/86e6f85cbca02be042c268c3d93e7f35977a0e127de56e319bdd1569eaa8/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", size = 1564291 }, - { url = "https://files.pythonhosted.org/packages/0b/8d/1f4ef3503b767717f65e1f5178b0173ab03cba1a19997ebf7b052161189f/aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", size = 416601 }, - { url = "https://files.pythonhosted.org/packages/ad/86/81cb83691b5ace3d9aa148dc42bacc3450d749fc88c5ec1973573c1c1779/aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", size = 442007 }, + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, ] [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "alembic" -version = "1.14.1" +version = "1.18.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, + { name = "tomli" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219 } +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 }, + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" -version = "4.8.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup" }, { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "appnope" version = "0.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, -] - -[[package]] -name = "argcomplete" -version = "1.10.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/28/07d2cfe0838f998ea2eafab59f52b0ceb1e70adb1831fa14b958a9fa6c5c/argcomplete-1.10.3.tar.gz", hash = "sha256:a37f522cf3b6a34abddfedb61c4546f60023b3799b22d1cd971eacdc0861530a", size = 50173 } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8e/6b293f883fdbd29b9c8170db44bddff9e7de224d8cf1eb4287f69f1766e5/argcomplete-1.10.3-py2.py3-none-any.whl", hash = "sha256:d8ea63ebaec7f59e56e7b2a386b1d1c7f1a7ae87902c9ee17d377eaa557f06fa", size = 36576 }, + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] [[package]] name = "argon2-cffi" -version = "23.1.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi-bindings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] [[package]] name = "argon2-cffi-bindings" -version = "21.2.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, - { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, - { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, - { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, - { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, - { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, - { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, - { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, - { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, - { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, ] [[package]] name = "arrow" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, - { name = "types-python-dateutil" }, + { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 }, + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] [[package]] name = "asttokens" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, -] - -[[package]] -name = "astunparse" -version = "1.6.3" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "async-lru" -version = "2.0.4" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/e2/2b4651eff771f6fd900d233e175ddc5e2be502c7eb62c0c42f975c6d36cd/async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627", size = 10019 } +sdist = { url = "https://files.pythonhosted.org/packages/05/8a/ca724066c32a53fa75f59e0f21aa822fdaa8a0dffa112d223634e3caabf9/async_lru-2.2.0.tar.gz", hash = "sha256:80abae2a237dbc6c60861d621619af39f0d920aea306de34cb992c879e01370c", size = 14654, upload-time = "2026-02-20T19:11:43.848Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9f/3c3503693386c4b0f245eaf5ca6198e3b28879ca0a40bde6b0e319793453/async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224", size = 6111 }, + { url = "https://files.pythonhosted.org/packages/13/5c/af990f019b8dd11c5492a6371fe74a5b0276357370030b67254a87329944/async_lru-2.2.0-py3-none-any.whl", hash = "sha256:e2c1cf731eba202b59c5feedaef14ffd9d02ad0037fcda64938699f2c380eafe", size = 7890, upload-time = "2026-02-20T19:11:42.273Z" }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] [[package]] name = "attrs" -version = "25.1.0" +version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "aymurai" -version = "1.1.12" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -246,10 +236,10 @@ dependencies = [ { name = "datasets" }, { name = "datetime-matcher" }, { name = "diskcache" }, + { name = "docx2txt" }, { name = "faker" }, { name = "fastapi", extra = ["standard"] }, { name = "flair" }, - { name = "gdown" }, { name = "jiwer" }, { name = "joblib" }, { name = "more-itertools" }, @@ -264,17 +254,13 @@ dependencies = [ { name = "python-docx" }, { name = "python-dotenv" }, { name = "python-multipart" }, - { name = "pytorch-lightning" }, { name = "requests" }, { name = "scipy" }, + { name = "sentence-transformers" }, { name = "sentencepiece" }, { name = "sqlmodel" }, { name = "tenacity" }, - { name = "tensorflow-hub" }, - { name = "tensorflow-text" }, - { name = "textract" }, { name = "torch" }, - { name = "torchtext" }, { name = "unidecode" }, { name = "uvicorn" }, { name = "xmltodict" }, @@ -291,6 +277,9 @@ dev = [ { name = "rich" }, { name = "seaborn" }, ] +tests = [ + { name = "pytest" }, +] [package.metadata] requires-dist = [ @@ -299,10 +288,10 @@ requires-dist = [ { name = "datasets", specifier = ">=3.2.0" }, { name = "datetime-matcher", git = "https://github.com/jedzill4/datetime_matcher" }, { name = "diskcache", specifier = ">=5.6.3" }, + { name = "docx2txt", specifier = ">=0.9" }, { name = "faker", specifier = "==18.11.2" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, - { name = "flair", specifier = "==0.14.0" }, - { name = "gdown", specifier = "==4.6.0" }, + { name = "flair", specifier = "==0.15.1" }, { name = "jiwer", specifier = "==3.0.5" }, { name = "joblib", specifier = ">=1.4.2" }, { name = "more-itertools", specifier = ">=10.5.0" }, @@ -317,17 +306,13 @@ requires-dist = [ { name = "python-docx", specifier = ">=1.2.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-multipart", specifier = ">=0.0.20" }, - { name = "pytorch-lightning", specifier = "==1.8.3.post1" }, { name = "requests", specifier = ">=2.32.3" }, { name = "scipy", specifier = "<1.14.1" }, + { name = "sentence-transformers", specifier = ">=2.2.0" }, { name = "sentencepiece", specifier = "==0.2.0" }, { name = "sqlmodel", specifier = "==0.0.22" }, { name = "tenacity", specifier = ">=9.0.0" }, - { name = "tensorflow-hub", specifier = ">=0.16.1" }, - { name = "tensorflow-text", specifier = "==2.10.0" }, - { name = "textract", specifier = "==1.6.5" }, - { name = "torch", specifier = "==1.12.1" }, - { name = "torchtext", specifier = "==0.13.1" }, + { name = "torch", specifier = ">=2.0" }, { name = "unidecode", specifier = "==1.3.8" }, { name = "uvicorn", specifier = ">=0.34.0" }, { name = "xmltodict", specifier = "==0.14.2" }, @@ -344,26 +329,28 @@ dev = [ { name = "rich", specifier = ">=13.9.4" }, { name = "seaborn", specifier = ">=0.13.2" }, ] +tests = [{ name = "pytest", specifier = ">=9.0.2" }] [[package]] name = "babel" -version = "2.16.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "beautifulsoup4" -version = "4.8.2" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/ba/0e121661f529e7f456e903bf5c4d255b8051d8ce2b5e629c5212efe4c3f1/beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a", size = 298650 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a1/c698cf319e9cfed6b17376281bd0efc6bfc8465698f54170ef60a485ab5d/beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887", size = 106874 }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] @@ -377,21 +364,21 @@ dependencies = [ { name = "lxml" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/9b/90086bbbffcf4aaa267a71e94f92eab1cfe4142198708129b8495e16c73f/bioc-2.1.tar.gz", hash = "sha256:7d9248198bdae291b0ebb218de2dc653c96d32a7eac2fa0ef4ed0c74ce45aaaa", size = 27924 } +sdist = { url = "https://files.pythonhosted.org/packages/44/9b/90086bbbffcf4aaa267a71e94f92eab1cfe4142198708129b8495e16c73f/bioc-2.1.tar.gz", hash = "sha256:7d9248198bdae291b0ebb218de2dc653c96d32a7eac2fa0ef4ed0c74ce45aaaa", size = 27924, upload-time = "2023-08-15T22:34:59.047Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/05/ae6dcab59673e2d498c645903ded6c5d76f7d44920386285feb96c517b3a/bioc-2.1-py3-none-any.whl", hash = "sha256:f1730d26821330f9f625058841612d6cfb616efb906fc682297fe04dd5d9398a", size = 33419 }, + { url = "https://files.pythonhosted.org/packages/4b/05/ae6dcab59673e2d498c645903ded6c5d76f7d44920386285feb96c517b3a/bioc-2.1-py3-none-any.whl", hash = "sha256:f1730d26821330f9f625058841612d6cfb616efb906fc682297fe04dd5d9398a", size = 33419, upload-time = "2023-08-15T22:34:56.022Z" }, ] [[package]] name = "bleach" -version = "6.2.0" +version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 } +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 }, + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, ] [package.optional-dependencies] @@ -401,203 +388,219 @@ css = [ [[package]] name = "boto3" -version = "1.36.6" +version = "1.42.66" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/31/f6189fcb81156cd2e7f7616e4c95958a47e53c12253c5e86a9dcc1a529c1/boto3-1.36.6.tar.gz", hash = "sha256:b36feae061dc0793cf311468956a0a9e99215ce38bc99a1a4e55a5b105f16297", size = 110998 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/2e/67206daa5acb6053157ae5241421713a84ed6015d33d0781985bd5558898/boto3-1.42.66.tar.gz", hash = "sha256:3bec5300fb2429c3be8e8961fdb1f11e85195922c8a980022332c20af05616d5", size = 112805, upload-time = "2026-03-11T19:58:19.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/44/4b4d7579297708b84d846a223b6e3be93767d12a3ee997e162e2a3a371a5/boto3-1.36.6-py3-none-any.whl", hash = "sha256:6d473f0f340d02b4e9ad5b8e68786a09728101a8b950231b89ebdaf72b6dca21", size = 139166 }, + { url = "https://files.pythonhosted.org/packages/4c/09/83224363c3f5e468e298e48beb577ffe8cb51f18c2116bc1ecf404796e60/boto3-1.42.66-py3-none-any.whl", hash = "sha256:7c6c60dc5500e8a2967a306372a5fdb4c7f9a5b8adc5eb9aa2ebb5081c51ff47", size = 140557, upload-time = "2026-03-11T19:58:17.61Z" }, ] [[package]] name = "botocore" -version = "1.36.6" +version = "1.42.66" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/bd1a28becf386a70ca41aa6b76b0d65d03aed81d39fac662d1f97754ffca/botocore-1.36.6.tar.gz", hash = "sha256:4864c53d638da191a34daf3ede3ff1371a3719d952cc0c6bd24ce2836a38dd77", size = 13479626 } +sdist = { url = "https://files.pythonhosted.org/packages/77/ef/1c8f89da69b0c3742120e19a6ea72ec46ac0596294466924fdd4cf0f36bb/botocore-1.42.66.tar.gz", hash = "sha256:39756a21142b646de552d798dde2105759b0b8fa0d881a34c26d15bd4c9448fa", size = 14977446, upload-time = "2026-03-11T19:58:07.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/ef/a0ac0d749c82fbaba28aa7b87a8ef5760de744e57dcbbfe3b99a0d65cf87/botocore-1.36.6-py3-none-any.whl", hash = "sha256:f77bbbb03fb420e260174650fb5c0cc142ec20a96967734eed2b0ef24334ef34", size = 13308005 }, + { url = "https://files.pythonhosted.org/packages/13/6f/7b45ed2ca300c1ad38ecfc82c1368546d4a90512d9dff589ebbd182a7317/botocore-1.42.66-py3-none-any.whl", hash = "sha256:ac48af1ab527dfa08c4617c387413ca56a7f87780d7bfc1da34ef847a59219a5", size = 14653886, upload-time = "2026-03-11T19:58:04.922Z" }, ] [[package]] name = "cachetools" -version = "5.5.1" +version = "7.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, ] [[package]] name = "certifi" -version = "2024.12.14" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, ] [[package]] name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, -] - -[[package]] -name = "chardet" -version = "3.0.4" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", size = 1868453 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", size = 133356 }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319, upload-time = "2026-03-06T06:00:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974, upload-time = "2026-03-06T06:00:28.222Z" }, + { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866, upload-time = "2026-03-06T06:00:29.769Z" }, + { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239, upload-time = "2026-03-06T06:00:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529, upload-time = "2026-03-06T06:00:32.608Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152, upload-time = "2026-03-06T06:00:33.93Z" }, + { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226, upload-time = "2026-03-06T06:00:35.469Z" }, + { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933, upload-time = "2026-03-06T06:00:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647, upload-time = "2026-03-06T06:00:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533, upload-time = "2026-03-06T06:00:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901, upload-time = "2026-03-06T06:00:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950, upload-time = "2026-03-06T06:00:45.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546, upload-time = "2026-03-06T06:00:46.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516, upload-time = "2026-03-06T06:00:47.924Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906, upload-time = "2026-03-06T06:00:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258, upload-time = "2026-03-06T06:00:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] -name = "comm" -version = "0.2.2" +name = "coloredlogs" +version = "15.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets" }, + { name = "humanfriendly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] [[package]] -name = "compressed-rtf" -version = "1.0.6" +name = "comm" +version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ac/abb196bb0b42a239d605fe97c314c3312374749013a07da4e6e0408f223c/compressed_rtf-1.0.6.tar.gz", hash = "sha256:c1c827f1d124d24608981a56e8b8691eb1f2a69a78ccad6440e7d92fde1781dd", size = 5800 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] [[package]] name = "conllu" version = "4.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/48/3e539bb27777d2204381e6bd352225d02965acce6e5a8d0717e9750dcc77/conllu-4.5.3.tar.gz", hash = "sha256:a016cf77e203b2e3ace82fcf0cba2874530d1458e874521640eba36e19546acc", size = 32768 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/48/3e539bb27777d2204381e6bd352225d02965acce6e5a8d0717e9750dcc77/conllu-4.5.3.tar.gz", hash = "sha256:a016cf77e203b2e3ace82fcf0cba2874530d1458e874521640eba36e19546acc", size = 32768, upload-time = "2023-06-19T12:37:49.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/3f/70a1dc5bc536755ec082b806594598a10cfffaf0de978f51d4e0e4fdfa47/conllu-4.5.3-py2.py3-none-any.whl", hash = "sha256:2b962b315c3c575e429105d045c888726df780b87a6dfe7609367e861990902d", size = 16098 }, + { url = "https://files.pythonhosted.org/packages/ce/3f/70a1dc5bc536755ec082b806594598a10cfffaf0de978f51d4e0e4fdfa47/conllu-4.5.3-py2.py3-none-any.whl", hash = "sha256:2b962b315c3c575e429105d045c888726df780b87a6dfe7609367e861990902d", size = 16098, upload-time = "2023-06-19T12:37:47.885Z" }, ] [[package]] name = "contourpy" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, - { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, - { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, - { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, - { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, - { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, - { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, - { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, - { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, - { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, - { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, - { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, + { url = "https://files.pythonhosted.org/packages/92/de/8ca2b613042550dcf9ef50c596c8b1f602afda92cf9032ac28a73f6ee410/cuda_pathfinder-1.4.2-py3-none-any.whl", hash = "sha256:eb354abc20278f8609dc5b666a24648655bef5613c6dfe78a238a6fd95566754", size = 44779, upload-time = "2026-03-10T21:57:30.974Z" }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "datasets" -version = "3.2.0" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, { name = "dill" }, { name = "filelock" }, { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, { name = "numpy" }, @@ -609,9 +612,9 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/48/744286c044e2b942d4fa67f92816126522ad1f0675def0ea3264e6242005/datasets-3.2.0.tar.gz", hash = "sha256:9a6e1a356052866b5dbdd9c9eedb000bf3fc43d986e3584d9b028f4976937229", size = 558366 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/9c/ba18de0b70858533e422ed6cfe0e46789473cef7fc7fc3653e23fa494730/datasets-4.7.0.tar.gz", hash = "sha256:4984cdfc65d04464da7f95205a55cb50515fd94ae3176caacb50a1b7273792e2", size = 602008, upload-time = "2026-03-09T19:01:49.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/84/0df6c5981f5fc722381662ff8cfbdf8aad64bec875f75d80b55bfef394ce/datasets-3.2.0-py3-none-any.whl", hash = "sha256:f3d2ba2698b7284a4518019658596a6a8bc79f31e51516524249d6c59cf0fe2a", size = 480647 }, + { url = "https://files.pythonhosted.org/packages/1e/03/c6d9c3119cf712f638fe763e887ecaac6acbb62bf1e2acc3cbde0df340fd/datasets-4.7.0-py3-none-any.whl", hash = "sha256:d5fe3025ec6acc3b5649f10d5576dff5e054134927604e6913c1467a04adc3c2", size = 527530, upload-time = "2026-03-09T19:01:47.443Z" }, ] [[package]] @@ -621,148 +624,130 @@ source = { git = "https://github.com/jedzill4/datetime_matcher#0e5793e8d1e3653f7 [[package]] name = "debugpy" -version = "1.8.12" +version = "1.8.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/25/c74e337134edf55c4dfc9af579eccb45af2393c40960e2795a94351e8140/debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce", size = 1641122 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/19/dd58334c0a1ec07babf80bf29fb8daf1a7ca4c1a3bbe61548e40616ac087/debugpy-1.8.12-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a", size = 2076091 }, - { url = "https://files.pythonhosted.org/packages/4c/37/bde1737da15f9617d11ab7b8d5267165f1b7dae116b2585a6643e89e1fa2/debugpy-1.8.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45", size = 3560717 }, - { url = "https://files.pythonhosted.org/packages/d9/ca/bc67f5a36a7de072908bc9e1156c0f0b272a9a2224cf21540ab1ffd71a1f/debugpy-1.8.12-cp310-cp310-win32.whl", hash = "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c", size = 5180672 }, - { url = "https://files.pythonhosted.org/packages/c1/b9/e899c0a80dfa674dbc992f36f2b1453cd1ee879143cdb455bc04fce999da/debugpy-1.8.12-cp310-cp310-win_amd64.whl", hash = "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9", size = 5212702 }, - { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] name = "deprecated" -version = "1.2.17" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/20/caa25c084ebad492360bf28ba5cb74f27b50fc6f3df965fd0add2b5b5993/deprecated-1.2.17.tar.gz", hash = "sha256:0114a10f0bbb750b90b2c2296c90cf7e9eaeb0abb5cf06c80de2c60138de0a82", size = 2928237 } +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/90/89be9a665bf63d3baaba6f34d9a514655abb42be2cc129f31c96df2cef51/Deprecated-1.2.17-py2.py3-none-any.whl", hash = "sha256:69cdc0a751671183f569495e2efb14baee4344b0236342eec29f1fde25d61818", size = 9140 }, + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] [[package]] name = "dill" -version = "0.3.8" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847 } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252 }, + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] [[package]] name = "diskcache" version = "5.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] name = "docopt" version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } [[package]] name = "docx2txt" -version = "0.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/7d/60ee3f2b16d9bfdfa72e8599470a2c1a5b759cb113c6fe1006be28359327/docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5", size = 2814 } - -[[package]] -name = "ebcdic" -version = "1.1.1" +version = "0.9" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/07/4486a038624e885e227fe79111914c01f55aa70a51920ff1a7f2bd216d10/docx2txt-0.9.tar.gz", hash = "sha256:18013f6229b14909028b19aa7bf4f8f3d6e4632d7b089ab29f7f0a4d1f660e28", size = 3613, upload-time = "2025-03-24T20:59:25.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/2f/633031205333bee5f9f93761af8268746aa75f38754823aabb8570eb245b/ebcdic-1.1.1-py2.py3-none-any.whl", hash = "sha256:33b4cb729bc2d0bf46cc1847b0e5946897cb8d3f53520c5b9aa5fa98d7e735f1", size = 128537 }, + { url = "https://files.pythonhosted.org/packages/d6/51/756e71bec48ece0ecc2a10e921ef2756e197dcb7e478f2b43673b6683902/docx2txt-0.9-py3-none-any.whl", hash = "sha256:e3718c0653fd6f2fcf4b51b02a61452ad1c38a4c163bcf0a6fd9486cd38f529a", size = 4025, upload-time = "2025-03-24T20:59:24.394Z" }, ] [[package]] name = "email-validator" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +dependencies = [ + { name = "typing-extensions" }, ] - -[[package]] -name = "executing" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] -name = "extract-msg" -version = "0.29.0" +name = "executing" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "compressed-rtf" }, - { name = "ebcdic" }, - { name = "imapclient" }, - { name = "olefile" }, - { name = "tzlocal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/70/60c17682d8b95077c526fe8374061f309fa647836f7783ee14b1277a9d9b/extract_msg-0.29.0.tar.gz", hash = "sha256:ae6ce5f78fddb582350cb49bbf2776eadecdbf3c74b7a305dced42bd187a5401", size = 72891 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/dc/511f62860fc076fc4e27bfbb1bc6b1f2b61e694d68007853d983d1877bdf/extract_msg-0.29.0-py2.py3-none-any.whl", hash = "sha256:a8885dc385d0c88c4b87fb2a573727c0115cd2ef5157956cf183878f940eef28", size = 72912 }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] @@ -772,23 +757,25 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/97/d7f0b25e181738b24e25092acc160bf30c682da6ab961e28f839a5f862b2/Faker-18.11.2.tar.gz", hash = "sha256:ec6e2824bb1d3546b36c156324b9df6bca5a3d6d03adf991e6a5586756dcab9d", size = 1670644 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/97/d7f0b25e181738b24e25092acc160bf30c682da6ab961e28f839a5f862b2/Faker-18.11.2.tar.gz", hash = "sha256:ec6e2824bb1d3546b36c156324b9df6bca5a3d6d03adf991e6a5586756dcab9d", size = 1670644, upload-time = "2023-06-27T15:24:28.075Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d5/cbb9d7083e1ba9ace3434f12c8bccb58e9ec5127323c09efea5d5fc59b1d/Faker-18.11.2-py3-none-any.whl", hash = "sha256:21c2c29638e98502f3bba9ad6a4f07a4b09c5e2150bb491ff02411a5888f6955", size = 1710039 }, + { url = "https://files.pythonhosted.org/packages/db/d5/cbb9d7083e1ba9ace3434f12c8bccb58e9ec5127323c09efea5d5fc59b1d/Faker-18.11.2-py3-none-any.whl", hash = "sha256:21c2c29638e98502f3bba9ad6a4f07a4b09c5e2150bb491ff02411a5888f6955", size = 1710039, upload-time = "2023-06-27T15:24:24.869Z" }, ] [[package]] name = "fastapi" -version = "0.115.7" +version = "0.135.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/f5/3f921e59f189e513adb9aef826e2841672d50a399fead4e69afdeb808ff4/fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015", size = 293177 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/7f/bbd4dcf0faf61bc68a01939256e2ed02d681e9334c1a3cef24d5f77aba9f/fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e", size = 94777 }, + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] [package.optional-dependencies] @@ -797,59 +784,107 @@ standard = [ { name = "fastapi-cli", extra = ["standard"] }, { name = "httpx" }, { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cli" -version = "0.0.7" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, + { name = "tomli" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705 }, + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, ] [package.optional-dependencies] standard = [ + { name = "fastapi-cloud-cli" }, { name = "uvicorn", extra = ["standard"] }, ] [[package]] -name = "fastjsonschema" -version = "2.21.1" +name = "fastapi-cloud-cli" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939 } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/e1/05c44e7bbc619e980fab0236cff9f5f323ac1aaa79434b4906febf98b1d3/fastapi_cloud_cli-0.15.0.tar.gz", hash = "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5", size = 45309, upload-time = "2026-03-11T22:31:32.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 }, + { url = "https://files.pythonhosted.org/packages/40/cc/1ccca747f5609be27186ea8c9219449142f40e3eded2c6089bba6a6ecc82/fastapi_cloud_cli-0.15.0-py3-none-any.whl", hash = "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", size = 32267, upload-time = "2026-03-11T22:31:33.499Z" }, ] [[package]] -name = "filelock" -version = "3.17.0" +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e2/51d9ee443aabcd5aa581d45b18b6198ced364b5cd97e5504c5d782ceb82c/fastar-0.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", size = 708536, upload-time = "2025-11-26T02:34:35.236Z" }, + { url = "https://files.pythonhosted.org/packages/07/2a/edfc6274768b8a3859a5ca4f8c29cb7f614d7f27d2378e2c88aa91cda54e/fastar-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", size = 632235, upload-time = "2025-11-26T02:34:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/3cfbaaec464caef196700ee2ffae1c03f94f7c5e2a85d0ec0ea9cdd1da81/fastar-0.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", size = 871386, upload-time = "2025-11-26T02:33:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/224a674ad541054179e4e6e0b54bb6e162f04f698a2512b42a8085fc6b6f/fastar-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", size = 764955, upload-time = "2025-11-26T02:32:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/4d/5e/4608184aa57cb6a54f62c1eb3e5133ba8d461fc7f13193c0255effbec12a/fastar-0.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", size = 765987, upload-time = "2025-11-26T02:32:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/6afd2b680dddfa10df9a16bbcf6cabfee0d92435d5c7e3f4cfe3b1712662/fastar-0.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", size = 930900, upload-time = "2025-11-26T02:33:16.059Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/b7a304bfcc1d06845cbfa4b464516f6fff9c8c6692f6ef80a3a86b04e199/fastar-0.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", size = 821523, upload-time = "2025-11-26T02:33:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/1d/da/9ef8605c6d233cd6ca3a95f7f518ac22aa064903afe6afa57733bfb7c31b/fastar-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", size = 821268, upload-time = "2025-11-26T02:34:04.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/ed37c78a6b4420de1677d82e79742787975c34847229c33dc376334c7283/fastar-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", size = 986286, upload-time = "2025-11-26T02:34:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a6/366b15f432d85d4089e6e4b52a09cc2a2bcf4d7a1f0771e3d3194deccb1e/fastar-0.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", size = 1041921, upload-time = "2025-11-26T02:35:07.292Z" }, + { url = "https://files.pythonhosted.org/packages/f4/45/45f8e6991e3ce9f8aeefdc8d4c200daada41097a36808643d1703464c3e2/fastar-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", size = 1047302, upload-time = "2025-11-26T02:35:24.995Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/a587796111a3cd4b78cd61ec3fc1252d8517d81f763f4164ed5680f84810/fastar-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", size = 995141, upload-time = "2025-11-26T02:35:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/89/c0/7a8ec86695b0b77168e220cf2af1aa30592f5ecdbd0ce6d641d29c4a8bae/fastar-0.8.0-cp310-cp310-win32.whl", hash = "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", size = 456544, upload-time = "2025-11-26T02:36:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/be/a9/8da4deb840121c59deabd939ce2dca3d6beec85576f3743d1144441938b5/fastar-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", size = 490701, upload-time = "2025-11-26T02:36:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/9f/6eaa810c240236eff2edf736cd50a17c97dbab1693cda4f7bcea09d13418/fastar-0.8.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", size = 710544, upload-time = "2025-11-26T02:34:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a5/58ff9e49a1cd5fbfc8f1238226cbf83b905376a391a6622cdd396b2cfa29/fastar-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812", size = 634020, upload-time = "2025-11-26T02:34:31.085Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/f839257c6600a83fbdb5a7fcc06319599086137b25ba38ca3d2c0fe14562/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", size = 871735, upload-time = "2025-11-26T02:34:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/4124c54260f7ee5cb7034bfe499eff2f8512b052d54be4671e59d4f25a4f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", size = 766779, upload-time = "2025-11-26T02:32:55.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/043b263c4126bf6557c942d099503989af9c5c7ee5cca9a04e00f754816f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", size = 766755, upload-time = "2025-11-26T02:33:11.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/ff/29a5dc06f2940439ebf98661ecc98d48d3f22fed8d6a2d5dc985d1e8da24/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", size = 932732, upload-time = "2025-11-26T02:33:27.122Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e8/2218830f422b37aad52c24b53cb84b5d88bd6fd6ad411bd6689b1a32500d/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", size = 822571, upload-time = "2025-11-26T02:33:42.986Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/ba6dfeff77cddfe58d85c490b1735c002b81c0d6f826916a8b6c4f8818bc/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", size = 822440, upload-time = "2025-11-26T02:34:15.439Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/54d5740c84b35de0eb12975397ecc16785b5ad8bed2dbac38b8c8a7c1edd/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", size = 987424, upload-time = "2025-11-26T02:35:02.742Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c7/18115927f16deb1ddffdbd4ae992e7e33064bc6defa2b92a147948f8bc0c/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", size = 1042675, upload-time = "2025-11-26T02:35:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/ca884fc7973ec6d765e87af23a4dd25784fb0a36ac2df825f18c3630bbab/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", size = 1047098, upload-time = "2025-11-26T02:35:37.643Z" }, + { url = "https://files.pythonhosted.org/packages/44/ee/25cd645db749b206bb95e1512e57e75d56ccbbb8ec3536f52a7979deab6b/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", size = 997397, upload-time = "2025-11-26T02:35:56.215Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] [[package]] -name = "fire" -version = "0.7.0" +name = "filelock" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "termcolor" }, +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf", size = 87189 } [[package]] name = "flair" -version = "0.14.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bioc" }, @@ -870,7 +905,6 @@ dependencies = [ { name = "regex" }, { name = "scikit-learn" }, { name = "segtok" }, - { name = "semver" }, { name = "sqlitedict" }, { name = "tabulate" }, { name = "torch" }, @@ -879,77 +913,77 @@ dependencies = [ { name = "transformers", extra = ["sentencepiece"] }, { name = "wikipedia-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/3d/81fedb2222c9b9f6b922b69166f53f6244f6e91037ed0a8e121acd16ceef/flair-0.14.0.tar.gz", hash = "sha256:dc14b58e93a52141f204d9995191aa3b7e0463a661a41faa8f8db30745a188a4", size = 371405 } +sdist = { url = "https://files.pythonhosted.org/packages/70/bf/f6cc2ee7fd15946fef5f7747327eeed49837a3d7fd34460129bc936f0de7/flair-0.15.1.tar.gz", hash = "sha256:780b4eeffba044c4a181f1810872fc01201c2eaed8d2ef4bcb126a6464e91933", size = 388607, upload-time = "2025-02-05T14:45:44.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f2/29a37585be8e824157d17c6196947b86a7312b0184a5d84f6792082130f5/flair-0.14.0-py3-none-any.whl", hash = "sha256:10735065ff462c05d0ea06ff01bc90e647f822a287858f9a6d0dabc7e402c754", size = 776453 }, + { url = "https://files.pythonhosted.org/packages/2b/b9/da0f10de728204eee8b582356a2dfab34bc02b1102fec061656d8db44630/flair-0.15.1-py3-none-any.whl", hash = "sha256:3b6b793f2380cd618e988e7b16fbadcec6502aaa8f11a0890390160303aed553", size = 1174604, upload-time = "2025-02-05T14:45:41.788Z" }, ] [[package]] name = "flatbuffers" -version = "25.1.24" +version = "25.12.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/20/c380c311843318b577650286b2c7eaaac3a011fb982df0050bdbd7e453c5/flatbuffers-25.1.24.tar.gz", hash = "sha256:e0f7b7d806c0abdf166275492663130af40c11f89445045fbef0aa3c9a8643ad", size = 22155 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/e2/b066e6e02d67bf5261a6d7539648c6da3365cc9eff3eb6d82009595d84d9/flatbuffers-25.1.24-py2.py3-none-any.whl", hash = "sha256:1abfebaf4083117225d0723087ea909896a34e3fec933beedb490d595ba24145", size = 30955 }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] name = "fonttools" -version = "4.55.6" +version = "4.62.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2e/0b11e907b90665253dbad425479e874e38a9e81ced397a4e3312b9116935/fonttools-4.55.6.tar.gz", hash = "sha256:1beb4647a0df5ceaea48015656525eb8081af226fe96554089fd3b274d239ef0", size = 3500677 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/96/686339e0fda8142b7ebed39af53f4a5694602a729662f42a6209e3be91d0/fonttools-4.62.0.tar.gz", hash = "sha256:0dc477c12b8076b4eb9af2e440421b0433ffa9e1dcb39e0640a6c94665ed1098", size = 3579521, upload-time = "2026-03-09T16:50:06.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/05/8c1a6ab8c443525c1cedee94d0371ec45cbcd11e4b01328ff10dcc483134/fonttools-4.55.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57d55fc965e5dd20c8a60d880e0f43bafb506be87af0b650bdc42591e41e0d0d", size = 2774906 }, - { url = "https://files.pythonhosted.org/packages/cd/53/de15cea829b49d50a0d0942d75bc71ca680536265abed03ce873ae71787b/fonttools-4.55.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:127999618afe3a2490fad54bab0650c5fbeab1f8109bdc0205f6ad34306deb8b", size = 2303345 }, - { url = "https://files.pythonhosted.org/packages/d2/23/90159149cc907ea2da0ca7b7baf5ad783c902de219ecb28bca3f789b82c3/fonttools-4.55.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3226d40cb92787e09dcc3730f54b3779dfe56bdfea624e263685ba17a6faac4", size = 4584790 }, - { url = "https://files.pythonhosted.org/packages/26/db/8d33a4575efe7ecd0487d4a53369d086ab7d879069e4c62d3687dec53941/fonttools-4.55.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e82772f70b84e17aa36e9f236feb2a4f73cb686ec1e162557a36cf759d1acd58", size = 4627464 }, - { url = "https://files.pythonhosted.org/packages/a3/c9/e90342b5eebce21ba2b04ce879c66e0316a5faaa7337498dfb5032953055/fonttools-4.55.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a632f85bd73e002b771bcbcdc512038fa5d2e09bb18c03a22fb8d400ea492ddf", size = 4581741 }, - { url = "https://files.pythonhosted.org/packages/e1/98/f4297b65849f15f6b49349a1ffc21e93d1859fa3579d0d5cb1590317060c/fonttools-4.55.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:791e0cf862cdd3a252df395f1bb5f65e3a760f1da3c7ce184d0f7998c266614d", size = 4751195 }, - { url = "https://files.pythonhosted.org/packages/63/73/9dcf4040edbdc5e33cc23d2b7d6a6d4bb50605efd5686d6b782815f4a818/fonttools-4.55.6-cp310-cp310-win32.whl", hash = "sha256:94f7f2c5c5f3a6422e954ecb6d37cc363e27d6f94050a7ed3f79f12157af6bb2", size = 2178422 }, - { url = "https://files.pythonhosted.org/packages/45/f0/ec0ce63f910db60a566201a550c06205595c10c980f6c74885f53cdf512b/fonttools-4.55.6-cp310-cp310-win_amd64.whl", hash = "sha256:2d15e02b93a46982a8513a208e8f89148bca8297640527365625be56151687d0", size = 2222886 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/6afc55d75036b8d3fe5ceaea2e8da2c04e8f3b298325de73a35f098cb9a8/fonttools-4.55.6-py3-none-any.whl", hash = "sha256:d20ab5a78d0536c26628eaadba661e7ae2427b1e5c748a0a510a44d914e1b155", size = 1112524 }, + { url = "https://files.pythonhosted.org/packages/82/e0/9db48ec7f6b95bae7b20667ded54f18dba8e759ef66232c8683822ae26fc/fonttools-4.62.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62b6a3d0028e458e9b59501cf7124a84cd69681c433570e4861aff4fb54a236c", size = 2873527, upload-time = "2026-03-09T16:48:12.416Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/86eccfdc922cb9fafc63189a9793fa9f6dd60e68a07be42e454ef2c0deae/fonttools-4.62.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:966557078b55e697f65300b18025c54e872d7908d1899b7314d7c16e64868cb2", size = 2417427, upload-time = "2026-03-09T16:48:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/d3/98/f547a1fceeae81a9a5c6461bde2badac8bf50bda7122a8012b32b1e65396/fonttools-4.62.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf34861145b516cddd19b07ae6f4a61ea1c6326031b960ec9ddce8ee815e888", size = 4934993, upload-time = "2026-03-09T16:48:18.186Z" }, + { url = "https://files.pythonhosted.org/packages/5c/57/a23a051fcff998fdfabdd33c6721b5bad499da08b586d3676993410071f0/fonttools-4.62.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e2ff573de2775508c8a366351fb901c4ced5dc6cf2d87dd15c973bedcdd5216", size = 4892154, upload-time = "2026-03-09T16:48:20.736Z" }, + { url = "https://files.pythonhosted.org/packages/e2/62/e27644b433dc6db1d47bc6028a27d772eec5cc8338e24a9a1fce5d7120aa/fonttools-4.62.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:55b189a1b3033860a38e4e5bd0626c5aa25c7ce9caee7bc784a8caec7a675401", size = 4911635, upload-time = "2026-03-09T16:48:23.174Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e2/1bf141911a5616bacfe9cf237c80ccd69d0d92482c38c0f7f6a55d063ad9/fonttools-4.62.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:825f98cd14907c74a4d0a3f7db8570886ffce9c6369fed1385020febf919abf6", size = 5031492, upload-time = "2026-03-09T16:48:25.095Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/790c292f4347ecfa77d9c7e0d1d91e04ab227f6e4a337ed4fe37ca388048/fonttools-4.62.0-cp310-cp310-win32.whl", hash = "sha256:c858030560f92a054444c6e46745227bfd3bb4e55383c80d79462cd47289e4b5", size = 1507656, upload-time = "2026-03-09T16:48:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ee/08c0b7f8bac6e44638de6fe9a3e710a623932f60eccd58912c4d4743516d/fonttools-4.62.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bf75eb69330e34ad2a096fac67887102c8537991eb6cac1507fc835bbb70e0a", size = 1556540, upload-time = "2026-03-09T16:48:30.359Z" }, + { url = "https://files.pythonhosted.org/packages/9c/57/c2487c281dde03abb2dec244fd67059b8d118bd30a653cbf69e94084cb23/fonttools-4.62.0-py3-none-any.whl", hash = "sha256:75064f19a10c50c74b336aa5ebe7b1f89fd0fb5255807bfd4b0c6317098f4af3", size = 1152427, upload-time = "2026-03-09T16:50:04.074Z" }, ] [[package]] name = "fqdn" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015 } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121 }, + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] [[package]] name = "frozenlist" -version = "1.5.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, - { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, - { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, - { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, - { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, - { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, - { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, - { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, - { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, - { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, - { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, - { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, - { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, - { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, - { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, - { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" -version = "2024.9.0" +version = "2026.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/7c/12b0943011daaaa9c35c2a2e22e5eb929ac90002f08f1259d69aedad84de/fsspec-2024.9.0.tar.gz", hash = "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8", size = 286206 } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/a0/6aaea0c2fbea2f89bfd5db25fb1e3481896a423002ebe4e55288907a97a3/fsspec-2024.9.0-py3-none-any.whl", hash = "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b", size = 179253 }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [package.optional-dependencies] @@ -964,160 +998,92 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821 }, -] - -[[package]] -name = "gast" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/4a/07c7e59cef23fb147454663c3271c21da68ba2ab141427c20548ae5a8a4d/gast-0.4.0.tar.gz", hash = "sha256:40feb7b8b8434785585ab224d1568b857edb18297e5a3047f1ba012bc83b42c1", size = 13804 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/48/583c032b79ae5b3daa02225a675aeb673e58d2cb698e78510feceb11958c/gast-0.4.0-py3-none-any.whl", hash = "sha256:b7adcdd5adbebf1adf17378da5ba3f543684dbec47b1cda1f3997e573cd542c4", size = 9824 }, + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, ] [[package]] name = "gdown" -version = "4.6.0" +version = "5.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "filelock" }, { name = "requests", extra = ["socks"] }, - { name = "six" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/a8/eeda0086e118c5cc725c549aadadffad4d8d508781add1bd3347f8ad7808/gdown-4.6.0.tar.gz", hash = "sha256:5ce3db0aeda54f46caacb2df86f31c3e3ecd17c355689e6456d85fb528ba9749", size = 14165 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/cf/919a9fa16faf8e4572a24d941353edaf4d54e3ddcd048e6c1aeb8c7a9903/gdown-5.2.1.tar.gz", hash = "sha256:247c2ad1f579db5b66b54c04e6a871995fc8fd7021708b950b8ba7b32cf90323", size = 284743, upload-time = "2026-01-11T09:34:01.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/54/0bbe240c6f59ac9209c2356dc65abd209963851569494376a077e55ef98d/gdown-4.6.0-py3-none-any.whl", hash = "sha256:e75c5aa8be8ea1cac642d4793f884339d887ab5e07aaa57fafa16c8a56a0cde5", size = 14400 }, -] - -[[package]] -name = "google-auth" -version = "2.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, -] - -[[package]] -name = "google-auth-oauthlib" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "requests-oauthlib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/21/b84fa7ef834d4b126faad13da6e582c8f888e196326b9d6aab1ae303df4f/google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a", size = 19516 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/0e/0636cc1448a7abc444fb1b3a63655e294e0d2d49092dc3de05241be6d43c/google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73", size = 18306 }, -] - -[[package]] -name = "google-pasta" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471 }, + { url = "https://files.pythonhosted.org/packages/87/21/35dd0a0b7428bd67b12b358d7b4277f693493a3839b071d540a4c8357b78/gdown-5.2.1-py3-none-any.whl", hash = "sha256:391f0480d495fb87644d1a1ee3ddfeb2144e1de31408fbc74f7e3b3ba927052b", size = 18241, upload-time = "2026-01-11T09:34:02.637Z" }, ] [[package]] name = "greenlet" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, -] - -[[package]] -name = "grpcio" -version = "1.70.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/e9/f72408bac1f7b05b25e4df569b02d6b200c8e7857193aa9f1df7a3744add/grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851", size = 5229736 }, - { url = "https://files.pythonhosted.org/packages/b3/17/e65139ea76dac7bcd8a3f17cbd37e3d1a070c44db3098d0be5e14c5bd6a1/grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf", size = 11432751 }, - { url = "https://files.pythonhosted.org/packages/a0/12/42de6082b4ab14a59d30b2fc7786882fdaa75813a4a4f3d4a8c4acd6ed59/grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5", size = 5711439 }, - { url = "https://files.pythonhosted.org/packages/34/f8/b5a19524d273cbd119274a387bb72d6fbb74578e13927a473bc34369f079/grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f", size = 6330777 }, - { url = "https://files.pythonhosted.org/packages/1a/67/3d6c0ad786238aac7fa93b79246fc452978fbfe9e5f86f70da8e8a2797d0/grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295", size = 5944639 }, - { url = "https://files.pythonhosted.org/packages/76/0d/d9f7cbc41c2743cf18236a29b6a582f41bd65572a7144d92b80bc1e68479/grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f", size = 6643543 }, - { url = "https://files.pythonhosted.org/packages/fc/24/bdd7e606b3400c14330e33a4698fa3a49e38a28c9e0a831441adbd3380d2/grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3", size = 6199897 }, - { url = "https://files.pythonhosted.org/packages/d1/33/8132eb370087960c82d01b89faeb28f3e58f5619ffe19889f57c58a19c18/grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199", size = 3617513 }, - { url = "https://files.pythonhosted.org/packages/99/bc/0fce5cfc0ca969df66f5dca6cf8d2258abb88146bf9ab89d8cf48e970137/grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1", size = 4303342 }, + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] -name = "h5py" -version = "3.12.1" +name = "hf-xet" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/0c/5c2b0a88158682aeafb10c1c2b735df5bc31f165bfe192f2ee9f2a23b5f1/h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf", size = 411457 } +sdist = { url = "https://files.pythonhosted.org/packages/68/01/928fd82663fb0ab455551a178303a2960e65029da66b21974594f3a20a94/hf_xet-1.4.0.tar.gz", hash = "sha256:48e6ba7422b0885c9bbd8ac8fdf5c4e1306c3499b82d489944609cc4eae8ecbd", size = 660350, upload-time = "2026-03-11T18:50:03.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/7d/b21045fbb004ad8bb6fb3be4e6ca903841722706f7130b9bba31ef2f88e3/h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda", size = 3402133 }, - { url = "https://files.pythonhosted.org/packages/29/a7/3c2a33fba1da64a0846744726fd067a92fb8abb887875a0dd8e3bac8b45d/h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3", size = 2866436 }, - { url = "https://files.pythonhosted.org/packages/1e/d0/4bf67c3937a2437c20844165766ddd1a1817ae6b9544c3743050d8e0f403/h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2", size = 5168596 }, - { url = "https://files.pythonhosted.org/packages/85/bc/e76f4b2096e0859225f5441d1b7f5e2041fffa19fc2c16756c67078417aa/h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307", size = 5341537 }, - { url = "https://files.pythonhosted.org/packages/99/bd/fb8ed45308bb97e04c02bd7aed324ba11e6a4bf9ed73967ca2a168e9cf92/h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e", size = 2990575 }, + { url = "https://files.pythonhosted.org/packages/9f/f9/a0b01945726aea81d2f213457cd5f5102a51e6fd1ca9f9769f561fb57501/hf_xet-1.4.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:981d2b5222c3baadf9567c135cf1d1073786f546b7745686978d46b5df179e16", size = 3799223, upload-time = "2026-03-11T18:49:49.884Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/ee62b0c00412f49a7e6f509f0104ee8808692278d247234336df48029349/hf_xet-1.4.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:cc8bd050349d0d7995ce7b3a3a18732a2a8062ce118a82431602088abb373428", size = 3560682, upload-time = "2026-03-11T18:49:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/93/d0/0fe5c44dbced465a651a03212e1135d0d7f95d19faada692920cb56f8e38/hf_xet-1.4.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5d0c38d2a280d814280b8c15eead4a43c9781e7bf6fc37843cffab06dcdc76b9", size = 4218323, upload-time = "2026-03-11T18:49:40.921Z" }, + { url = "https://files.pythonhosted.org/packages/73/df/7b3c99a4e50442039eae498e5c23db634538eb3e02214109880cf1165d4c/hf_xet-1.4.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a883f0250682ea888a1bd0af0631feda377e59ad7aae6fb75860ecee7ae0f93", size = 3997156, upload-time = "2026-03-11T18:49:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/47dfedf271c21d95346660ae1698e7ece5ab10791fa6c4f20c59f3713083/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:99e1d9255fe8ecdf57149bb0543d49e7b7bd8d491ddf431eb57e114253274df5", size = 4199052, upload-time = "2026-03-11T18:49:57.097Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c0/346b9aad1474e881e65f998d5c1981695f0af045bc7a99204d9d86759a89/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b25f06ce42bd2d5f2e79d4a2d72f783d3ac91827c80d34a38cf8e5290dd717b0", size = 4434346, upload-time = "2026-03-11T18:49:58.67Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d6/88ce9d6caa397c3b935263d5bcbe3ebf6c443f7c76098b8c523d206116b9/hf_xet-1.4.0-cp37-abi3-win_amd64.whl", hash = "sha256:8d6d7816d01e0fa33f315c8ca21b05eca0ce4cdc314f13b81d953e46cc6db11d", size = 3678921, upload-time = "2026-03-11T18:50:09.496Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/17d99ed253b28a9550ca479867c66a8af4c9bcd8cdc9a26b0c8007c2000a/hf_xet-1.4.0-cp37-abi3-win_arm64.whl", hash = "sha256:cb8d9549122b5b42f34b23b14c6b662a88a586a919d418c774d8dbbc4b3ce2aa", size = 3541054, upload-time = "2026-03-11T18:50:07.963Z" }, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httptools" -version = "0.6.4" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, ] [[package]] @@ -1130,74 +1096,87 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "huggingface-hub" -version = "0.27.1" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "requests" }, { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/d2/d6976de7542792fc077b498d64af64882b6d8bb40679284ec0bff77d5929/huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b", size = 379407 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/3f/50f6b25fafdcfb1c089187a328c95081abf882309afd86f4053951507cd1/huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec", size = 450658 }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] [[package]] name = "identify" -version = "2.6.6" +version = "2.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/bf/c68c46601bacd4c6fb4dd751a42b6e7087240eaabc6487f2ef7a48e0e8fc/identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251", size = 99217 } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/a1/68a395c17eeefb04917034bd0a1bfa765e7654fa150cca473d669aa3afb5/identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881", size = 99083 }, + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] -name = "imapclient" -version = "2.1.0" +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ea/31/883f78210ed7578f6dd41e4dbc3ad5e7c6127a51e56513b8b7bb7efdf9b3/IMAPClient-2.1.0.zip", hash = "sha256:60ba79758cc9f13ec910d7a3df9acaaf2bb6c458720d9a02ec33a41352fd1b99", size = 248423 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/39/e1c2c2c6e2356ab6ea81fcfc0a74b044b311d6a91a45300811d9a6077ef7/IMAPClient-2.1.0-py2.py3-none-any.whl", hash = "sha256:3eeb97b9aa8faab0caa5024d74bfde59408fbd542781246f6960873c7bf0dd01", size = 73972 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "intervaltree" -version = "3.1.0" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/fb/396d568039d21344639db96d940d40eb62befe704ef849b27949ded5c3bb/intervaltree-3.1.0.tar.gz", hash = "sha256:902b1b88936918f9b2a19e0e5eb7ccb430ae45cde4f39ea4b36932920d33952d", size = 32861 } +sdist = { url = "https://files.pythonhosted.org/packages/53/c3/b2afa612aa0373f3e6bb190e6de35f293b307d1537f109e3e25dbfcdf212/intervaltree-3.2.1.tar.gz", hash = "sha256:f3f7e8baeb7dd75b9f7a6d33cf3ec10025984a8e66e3016d537e52130c73cfe2", size = 1231531, upload-time = "2025-12-24T04:25:06.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7f/8a80a1c7c2ed05822b5a2b312d2995f30c533641f8198366ba2e26a7bb03/intervaltree-3.2.1-py2.py3-none-any.whl", hash = "sha256:a8a8381bbd35d48ceebee932c77ffc988492d22fb1d27d0ba1d74a7694eb8f0b", size = 25929, upload-time = "2025-12-24T04:25:05.298Z" }, +] [[package]] name = "ipykernel" -version = "6.29.5" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -1211,14 +1190,14 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, ] [[package]] name = "ipython" -version = "8.31.0" +version = "8.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1233,14 +1212,14 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/35/6f90fdddff7a08b7b715fccbd2427b5212c9525cd043d26fdc45bee0708d/ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b", size = 5501011 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/60/d0feb6b6d9fe4ab89fe8fe5b47cbf6cd936bfd9f1e7ffa9d0015425aeed6/ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6", size = 821583 }, + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, ] [[package]] name = "ipywidgets" -version = "8.1.5" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, @@ -1249,9 +1228,9 @@ dependencies = [ { name = "traitlets" }, { name = "widgetsnbextension" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/4c/dab2a281b07596a5fc220d49827fe6c794c66f1493d7a74f1df0640f2cc5/ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17", size = 116723 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/2d/9c0b76f2f9cc0ebede1b9371b6f317243028ed60b90705863d493bae622e/ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245", size = 139767 }, + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, ] [[package]] @@ -1261,9 +1240,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321 }, + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] [[package]] @@ -1273,21 +1252,21 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] @@ -1298,36 +1277,36 @@ dependencies = [ { name = "click" }, { name = "rapidfuzz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/37/99f800e78b9cff2b6c404812e33641b6b6b5b94088081e6a28548094e46f/jiwer-3.0.5.tar.gz", hash = "sha256:4a215a774b6a2ad92838bfc9f64f072709557af48e3eb9d6bdbcee6819535b2d", size = 17537 } +sdist = { url = "https://files.pythonhosted.org/packages/42/37/99f800e78b9cff2b6c404812e33641b6b6b5b94088081e6a28548094e46f/jiwer-3.0.5.tar.gz", hash = "sha256:4a215a774b6a2ad92838bfc9f64f072709557af48e3eb9d6bdbcee6819535b2d", size = 17537, upload-time = "2024-11-01T16:18:57.337Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/7f/00bec152973661ea89628490367fa3058d7f56d51d2c1ad02a44589d12cd/jiwer-3.0.5-py3-none-any.whl", hash = "sha256:5a55758fd1ed0b46a04e51eae6325ad77810511d1372dcbb2333ec8d5850f7b2", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/00bec152973661ea89628490367fa3058d7f56d51d2c1ad02a44589d12cd/jiwer-3.0.5-py3-none-any.whl", hash = "sha256:5a55758fd1ed0b46a04e51eae6325ad77810511d1372dcbb2333ec8d5850f7b2", size = 21990, upload-time = "2024-11-01T16:18:55.928Z" }, ] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "joblib" -version = "1.4.2" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "json5" -version = "0.10.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 }, + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, ] [[package]] @@ -1337,23 +1316,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359 } +sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701 }, + { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, ] [[package]] name = "jsonpointer" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, ] [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1361,9 +1340,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [package.optional-dependencies] @@ -1374,20 +1353,21 @@ format-nongpl = [ { name = "jsonpointer" }, { name = "rfc3339-validator" }, { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, { name = "uri-template" }, { name = "webcolors" }, ] [[package]] name = "jsonschema-specifications" -version = "2024.10.1" +version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -1402,14 +1382,14 @@ dependencies = [ { name = "nbconvert" }, { name = "notebook" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959 } +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657 }, + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, ] [[package]] name = "jupyter-client" -version = "8.6.3" +version = "8.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -1418,9 +1398,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, ] [[package]] @@ -1437,31 +1417,31 @@ dependencies = [ { name = "pyzmq" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510 }, + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, ] [[package]] name = "jupyter-core" -version = "5.7.2" +version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] [[package]] name = "jupyter-events" -version = "0.11.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, { name = "python-json-logger" }, { name = "pyyaml" }, { name = "referencing" }, @@ -1469,26 +1449,26 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/65/5791c8a979b5646ca29ea50e42b6708908b789f7ff389d1a03c1b93a1c54/jupyter_events-0.11.0.tar.gz", hash = "sha256:c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90", size = 62039 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8c/9b65cb2cd4ea32d885993d5542244641590530836802a2e8c7449a4c61c9/jupyter_events-0.11.0-py3-none-any.whl", hash = "sha256:36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf", size = 19423 }, + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, ] [[package]] name = "jupyter-lsp" -version = "2.2.5" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/b4/3200b0b09c12bc3b72d943d923323c398eff382d1dcc7c0dbc8b74630e40/jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001", size = 48741 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/e0/7bd7cff65594fd9936e2f9385701e44574fc7d721331ff676ce440b14100/jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da", size = 69146 }, + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, ] [[package]] name = "jupyter-server" -version = "2.15.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1511,27 +1491,27 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/8c/df09d4ab646141f130f9977b32b206ba8615d1969b2eba6a2e84b7f89137/jupyter_server-2.15.0.tar.gz", hash = "sha256:9d446b8697b4f7337a1b7cdcac40778babdd93ba614b6d68ab1c0c918f1c4084", size = 725227 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/a2/89eeaf0bb954a123a909859fa507fa86f96eb61b62dc30667b60dbd5fdaf/jupyter_server-2.15.0-py3-none-any.whl", hash = "sha256:872d989becf83517012ee669f09604aa4a28097c0bd90b2f424310156c2cdae3", size = 385826 }, + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, ] [[package]] name = "jupyter-server-terminals" -version = "0.5.3" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "terminado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656 }, + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, ] [[package]] name = "jupyterlab" -version = "4.3.4" +version = "4.5.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1549,23 +1529,23 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/45/1052f842e066902b1d78126df7e2269b1b9408991e1344e167b2e429f9e1/jupyterlab-4.3.4.tar.gz", hash = "sha256:f0bb9b09a04766e3423cccc2fc23169aa2ffedcdf8713e9e0fb33cac0b6859d0", size = 21797583 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/d5/730628e03fff2e8a8e8ccdaedde1489ab1309f9a4fa2536248884e30b7c7/jupyterlab-4.5.6.tar.gz", hash = "sha256:642fe2cfe7f0f5922a8a558ba7a0d246c7bc133b708dfe43f7b3a826d163cf42", size = 23970670, upload-time = "2026-03-11T14:17:04.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/48/af57263e53cfc220e522de047aa0993f53bab734fe812af1e03e33ac6d7c/jupyterlab-4.3.4-py3-none-any.whl", hash = "sha256:b754c2601c5be6adf87cb5a1d8495d653ffb945f021939f77776acaa94dae952", size = 11665373 }, + { url = "https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl", hash = "sha256:d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580", size = 12447124, upload-time = "2026-03-11T14:17:00.229Z" }, ] [[package]] name = "jupyterlab-pygments" version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900 } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884 }, + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, ] [[package]] name = "jupyterlab-server" -version = "2.27.3" +version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1576,68 +1556,44 @@ dependencies = [ { name = "packaging" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/c9/a883ce65eb27905ce77ace410d83587c82ea64dc85a48d1f7ed52bcfa68d/jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4", size = 76173 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700 }, + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, ] [[package]] name = "jupyterlab-widgets" -version = "3.0.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/73/fa26bbb747a9ea4fca6b01453aa22990d52ab62dd61384f1ac0dc9d4e7ba/jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed", size = 203556 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/93/858e87edc634d628e5d752ba944c2833133a28fa87bb093e6832ced36a3e/jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54", size = 214392 }, -] - -[[package]] -name = "keras" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/4d/dc255a437c9616b155e5bd55e325e092b7cdcb4652361d733ae051d40853/keras-2.10.0-py2.py3-none-any.whl", hash = "sha256:26a6e2c2522e7468ddea22710a99b3290493768fc08a39e75d1173a0e3452fdf", size = 1684294 }, -] - -[[package]] -name = "keras-preprocessing" -version = "1.1.2" +version = "3.0.16" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f1/b44337faca48874333769a29398fe4666686733c8880aa160b9fd5dfe600/Keras_Preprocessing-1.1.2.tar.gz", hash = "sha256:add82567c50c8bc648c14195bf544a5ce7c1f76761536956c3d2978970179ef3", size = 163598 } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/4c/7c3275a01e12ef9368a892926ab932b33bb13d55794881e3573482b378a7/Keras_Preprocessing-1.1.2-py2.py3-none-any.whl", hash = "sha256:7b82029b130ff61cc99b55f3bd27427df4838576838c5b2f65940e4fcec99a7b", size = 42581 }, + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] [[package]] name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, ] [[package]] @@ -1647,122 +1603,93 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474 } - -[[package]] -name = "libclang" -version = "18.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045 }, - { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641 }, - { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207 }, - { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943 }, - { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972 }, - { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494 }, - { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083 }, - { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112 }, -] +sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } [[package]] -name = "lightning-utilities" -version = "0.3.0" +name = "lark" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fire" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/ee/4b206e722c4fc138436aa91299692bba4fdad1b6ce8429bf291929456b04/lightning-utilities-0.3.0.tar.gz", hash = "sha256:d769ab9b76ebdee3243d1051d509aafee57d7947734ddc22977deef8a6427f2f", size = 15292 } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/fc/1f4ff2bcba4e6162276cabe831a431ef14681a7158e693a5cf828dd6fa1b/lightning_utilities-0.3.0-py3-none-any.whl", hash = "sha256:1ae9bdd8f1be3c81b1ac4820f6eeddcbafcc2505c57a5940054466f4763bc22d", size = 15594 }, + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] [[package]] name = "lxml" -version = "5.3.0" +version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570 }, - { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042 }, - { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213 }, - { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814 }, - { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084 }, - { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993 }, - { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462 }, - { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288 }, - { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435 }, - { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354 }, - { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973 }, - { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837 }, - { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555 }, - { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314 }, - { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303 }, - { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126 }, - { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065 }, - { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431 }, - { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683 }, - { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732 }, - { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377 }, - { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237 }, - { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557 }, +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, ] [[package]] name = "mako" -version = "1.3.8" +version = "1.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/d9/8518279534ed7dace1795d5a47e49d5299dd0994eed1053996402a8902f9/mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8", size = 392069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/bf/7a6a36ce2e4cafdfb202752be68850e22607fccd692847c45c1ae3c17ba6/Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627", size = 78569 }, -] - -[[package]] -name = "markdown" -version = "3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, ] [[package]] name = "matplotlib" -version = "3.10.0" +version = "3.10.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -1775,120 +1702,142 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/dd/fa2e1a45fce2d09f4aea3cee169760e672c8262325aa5796c49d543dc7e6/matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278", size = 36686418 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ec/3cdff7b5239adaaacefcc4f77c316dfbbdf853c4ed2beec467e0fec31b9f/matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6", size = 8160551 }, - { url = "https://files.pythonhosted.org/packages/41/f2/b518f2c7f29895c9b167bf79f8529c63383ae94eaf49a247a4528e9a148d/matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e", size = 8034853 }, - { url = "https://files.pythonhosted.org/packages/ed/8d/45754b4affdb8f0d1a44e4e2bcd932cdf35b256b60d5eda9f455bb293ed0/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5", size = 8446724 }, - { url = "https://files.pythonhosted.org/packages/09/5a/a113495110ae3e3395c72d82d7bc4802902e46dc797f6b041e572f195c56/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6", size = 8583905 }, - { url = "https://files.pythonhosted.org/packages/12/b1/8b1655b4c9ed4600c817c419f7eaaf70082630efd7556a5b2e77a8a3cdaf/matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1", size = 9395223 }, - { url = "https://files.pythonhosted.org/packages/5a/85/b9a54d64585a6b8737a78a61897450403c30f39e0bd3214270bb0b96f002/matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3", size = 8025355 }, - { url = "https://files.pythonhosted.org/packages/32/5f/29def7ce4e815ab939b56280976ee35afffb3bbdb43f332caee74cb8c951/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03", size = 8155500 }, - { url = "https://files.pythonhosted.org/packages/de/6d/d570383c9f7ca799d0a54161446f9ce7b17d6c50f2994b653514bcaa108f/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea", size = 8032398 }, - { url = "https://files.pythonhosted.org/packages/c9/b4/680aa700d99b48e8c4393fa08e9ab8c49c0555ee6f4c9c0a5e8ea8dfde5d/matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef", size = 8587361 }, + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.1.7" +version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mistune" -version = "3.1.0" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/6e/96fc7cb3288666c5de2c396eb0e338dc95f7a8e4920e43e38783a22d0084/mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667", size = 94401 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/b3/743ffc3f59da380da504d84ccd1faf9a857a1445991ff19bf2ec754163c2/mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1", size = 53694 }, + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] name = "mpld3" -version = "0.5.10" +version = "0.5.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "matplotlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/58/19378f4189a034eb3efc17b133426b8551af1d3b2c70d641a63124579629/mpld3-0.5.10.tar.gz", hash = "sha256:a478eb404fa5212505c59133cf272cd9a94105872e605597720e7f84de38fbc7", size = 1027709 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b2/9943f3e0bdf4ff9968b7ac4499152162d8ee8fc8eaa55487339872f86300/mpld3-0.5.12.tar.gz", hash = "sha256:1333e2bca012ea9af3c27801ba36f65bc26540b6fad4c56a903afb19477f2c37", size = 1028433, upload-time = "2025-11-05T18:12:24.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl", hash = "sha256:bea31799a4041029a906f53f2662bbf1c49903e0c0bc712b412354158ec7cf54", size = 203051, upload-time = "2025-11-05T18:12:22.527Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/6a/e3691bcc47485f38b09853207c928130571821d187cf174eed5418d45e82/mpld3-0.5.10-py3-none-any.whl", hash = "sha256:80877acce87ea447380fad7374668737505c8c0684aab05238e7c5dc1fab38c1", size = 202561 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "multidict" -version = "6.1.0" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, - { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, - { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, - { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, - { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, - { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, - { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, - { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, - { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, - { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, - { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, - { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, - { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, - { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, - { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, - { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "multiprocess" -version = "0.70.16" +version = "0.70.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603 } +sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503, upload-time = "2025-04-17T03:11:27.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f8/7f9a8f08bf98cea1dfaa181e05cc8bbcb59cecf044b5a9ac3cce39f9c449/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df", size = 135083, upload-time = "2025-04-17T03:11:04.223Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/b7b10dbfc17b2b3ce07d4d30b3ba8367d0ed32d6d46cd166e298f161dd46/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213", size = 135128, upload-time = "2025-04-17T03:11:06.045Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a3/5f8d3b9690ea5580bee5868ab7d7e2cfca74b7e826b28192b40aa3881cdc/multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2", size = 135132, upload-time = "2025-04-17T03:11:07.533Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948, upload-time = "2025-04-17T03:11:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636, upload-time = "2025-04-17T03:11:24.936Z" }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" }, +] + +[[package]] +name = "narwhals" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b4/02a8add181b8d2cd5da3b667cd102ae536e8c9572ab1a130816d70a89edb/narwhals-2.18.0.tar.gz", hash = "sha256:1de5cee338bc17c338c6278df2c38c0dd4290499fcf70d75e0a51d5f22a6e960", size = 620222, upload-time = "2026-03-10T15:51:27.14Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980 }, - { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982 }, - { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824 }, - { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628 }, - { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 }, + { url = "https://files.pythonhosted.org/packages/fe/75/0b4a10da17a44cf13567d08a9c7632a285297e46253263f1ae119129d10a/narwhals-2.18.0-py3-none-any.whl", hash = "sha256:68378155ee706ac9c5b25868ef62ecddd62947b6df7801a0a156bc0a615d2d0d", size = 444865, upload-time = "2026-03-10T15:51:24.085Z" }, ] [[package]] name = "nbclient" -version = "0.10.2" +version = "0.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, @@ -1896,14 +1845,14 @@ dependencies = [ { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424 } +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434 }, + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, ] [[package]] name = "nbconvert" -version = "7.16.5" +version = "7.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -1921,9 +1870,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/2c/d026c0367f2be2463d4c2f5b538e28add2bc67bc13730abb7f364ae4eb8b/nbconvert-7.16.5.tar.gz", hash = "sha256:c83467bb5777fdfaac5ebbb8e864f300b277f68692ecc04d6dab72f2d8442344", size = 856367 } +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/9e/2dcc9fe00cf55d95a8deae69384e9cea61816126e345754f6c75494d32ec/nbconvert-7.16.5-py3-none-any.whl", hash = "sha256:e12eac052d6fd03040af4166c563d76e7aeead2e9aadf5356db552a1784bd547", size = 258061 }, + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, ] [[package]] @@ -1936,44 +1885,53 @@ dependencies = [ { name = "jupyter-core" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, ] [[package]] name = "nbstripout" -version = "0.8.0" +version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nbformat" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/34/e0a9ae627f02d40861656be351da44647406d7f0ce5cbc09e85abba52935/nbstripout-0.8.0.tar.gz", hash = "sha256:4b9b563c3704f9b59067627bec7d0d5c7437527ab6c3a72dd3cf895d46bf5a44", size = 26018 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/6f/b52c4da26babeb521078c08c78c3187a59197098ffc7a70b0fe76851813a/nbstripout-0.9.1.tar.gz", hash = "sha256:313bbb4217c8e38998567e5d790b6bd6c3a17a8c39073b205b84dadfc5d756dc", size = 32356, upload-time = "2026-02-21T16:19:55.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f2/7aca174fb55ea04eb5918da199ffb6aa03258ee23e169d10a4d2a8cd4838/nbstripout-0.8.0-py2.py3-none-any.whl", hash = "sha256:b37f7b297fc6c02647d387d1049e4be8d0ecbf74640e502dce36ae93120ad420", size = 16237 }, + { url = "https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl", hash = "sha256:ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e", size = 19136, upload-time = "2026-02-21T16:19:54.868Z" }, ] [[package]] name = "nest-asyncio" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, ] [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "notebook" -version = "7.3.2" +version = "7.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -1982,9 +1940,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/04/ac488379d5afef43402b3fb4be2857db1a09804fecf98b9b714c741b225b/notebook-7.3.2.tar.gz", hash = "sha256:705e83a1785f45b383bf3ee13cb76680b92d24f56fb0c7d2136fe1d850cd3ca8", size = 12781804 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/6d/41052c48d6f6349ca0a7c4d1f6a78464de135e6d18f5829ba2510e62184c/notebook-7.5.5.tar.gz", hash = "sha256:dc0bfab0f2372c8278c457423d3256c34154ac2cc76bf20e9925260c461013c3", size = 14169167, upload-time = "2026-03-11T16:32:51.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/9b/76e50ee18f183ea5fe1784a9eeaa50f2c71802e4740d6e959592b0993298/notebook-7.3.2-py3-none-any.whl", hash = "sha256:e5f85fc59b69d3618d73cf27544418193ff8e8058d5bf61d315ce4f473556288", size = 13163630 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/cbd1deb9f07446241e88f8d5fecccd95b249bca0b4e5482214a4d1714c49/notebook-7.5.5-py3-none-any.whl", hash = "sha256:a7c14dbeefa6592e87f72290ca982e0c10f5bbf3786be2a600fda9da2764a2b8", size = 14578929, upload-time = "2026-03-11T16:32:48.021Z" }, ] [[package]] @@ -1994,133 +1952,245 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167 } +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307 }, + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] [[package]] name = "numpy" version = "1.26.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, ] [[package]] -name = "oauthlib" -version = "3.2.2" +name = "nvidia-cublas-cu12" +version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, ] [[package]] -name = "odfpy" -version = "1.4.1" +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "defusedxml" }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003f7ce3304f524774adda96e6aaab30ea79fd8fda7934/odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", size = 717045 } [[package]] -name = "olefile" -version = "0.47" +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565 }, + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, ] [[package]] -name = "opt-einsum" -version = "3.4.0" +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, ] [[package]] -name = "overrides" -version = "7.7.0" +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, ] [[package]] -name = "packaging" -version = "24.2" +name = "nvidia-cufft-cu12" +version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, ] [[package]] -name = "pandas" -version = "2.2.3" +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "odfpy" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003f7ce3304f524774adda96e6aaab30ea79fd8fda7934/odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", size = 717045, upload-time = "2020-01-18T16:55:48.852Z" } + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, - { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, - { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, - { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, - { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, - { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, - { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, ] [[package]] -name = "pandocfilters" -version = "1.5.1" +name = "overrides" +version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, ] [[package]] -name = "parso" -version = "0.8.4" +name = "packaging" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] -name = "pdfminer-six" -version = "20191110" +name = "pandas" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "chardet" }, - { name = "pycryptodome" }, - { name = "six" }, - { name = "sortedcontainers" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/31/7acc148333749d6a8ef7cbf25902bdf59a462811a69d040a9a259916b6bd/pdfminer.six-20191110.tar.gz", hash = "sha256:141a53ec491bee6d45bf9b2c7f82601426fb5d32636bcf6b9c8a8f3b6431fea6", size = 10280313 } + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/83/200b2723bcbf1d1248a8a7d16e6dd6cb970b5331397b11948428d7ebcf37/pdfminer.six-20191110-py2.py3-none-any.whl", hash = "sha256:ca2ca58f3ac66a486bce53a6ddba95dc2b27781612915fa41c444790ba9cd2a8", size = 5606096 }, + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] [[package]] @@ -2130,77 +2200,79 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "pillow" -version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, - { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, - { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, - { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, - { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, - { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, - { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, - { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, - { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, - { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, - { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, - { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, - { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, - { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, - { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, - { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, ] [[package]] name = "pip" -version = "24.3.1" +version = "26.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, ] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] name = "plotly" -version = "5.24.1" +version = "6.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "narwhals" }, { name = "packaging" }, - { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/4f/428f6d959818d7425a94c190a6b26fbc58035cbef40bf249be0b62a9aedd/plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae", size = 9479398 } +sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pptree" version = "3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/cd/f4f2b79d20a10563d071c38b6ad14bf9c5d75a0edef877d2bed60c024247/pptree-3.1.tar.gz", hash = "sha256:4dd0ba2f58000cbd29d68a5b64bac29bcb5a663642f79404877c0059668a69f6", size = 3043 } +sdist = { url = "https://files.pythonhosted.org/packages/01/cd/f4f2b79d20a10563d071c38b6ad14bf9c5d75a0edef877d2bed60c024247/pptree-3.1.tar.gz", hash = "sha256:4dd0ba2f58000cbd29d68a5b64bac29bcb5a663642f79404877c0059668a69f6", size = 3043, upload-time = "2020-04-15T18:28:53.362Z" } [[package]] name = "pre-commit" -version = "4.0.1" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2209,380 +2281,380 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] name = "prometheus-client" -version = "0.21.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "propcache" -version = "0.2.1" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/a5/0ea64c9426959ef145a938e38c832fc551843481d356713ececa9a8a64e8/propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6", size = 79296 }, - { url = "https://files.pythonhosted.org/packages/76/5a/916db1aba735f55e5eca4733eea4d1973845cf77dfe67c2381a2ca3ce52d/propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2", size = 45622 }, - { url = "https://files.pythonhosted.org/packages/2d/62/685d3cf268b8401ec12b250b925b21d152b9d193b7bffa5fdc4815c392c2/propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea", size = 45133 }, - { url = "https://files.pythonhosted.org/packages/4d/3d/31c9c29ee7192defc05aa4d01624fd85a41cf98e5922aaed206017329944/propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212", size = 204809 }, - { url = "https://files.pythonhosted.org/packages/10/a1/e4050776f4797fc86140ac9a480d5dc069fbfa9d499fe5c5d2fa1ae71f07/propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3", size = 219109 }, - { url = "https://files.pythonhosted.org/packages/c9/c0/e7ae0df76343d5e107d81e59acc085cea5fd36a48aa53ef09add7503e888/propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d", size = 217368 }, - { url = "https://files.pythonhosted.org/packages/fc/e1/e0a2ed6394b5772508868a977d3238f4afb2eebaf9976f0b44a8d347ad63/propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634", size = 205124 }, - { url = "https://files.pythonhosted.org/packages/50/c1/e388c232d15ca10f233c778bbdc1034ba53ede14c207a72008de45b2db2e/propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2", size = 195463 }, - { url = "https://files.pythonhosted.org/packages/0a/fd/71b349b9def426cc73813dbd0f33e266de77305e337c8c12bfb0a2a82bfb/propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958", size = 198358 }, - { url = "https://files.pythonhosted.org/packages/02/f2/d7c497cd148ebfc5b0ae32808e6c1af5922215fe38c7a06e4e722fe937c8/propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c", size = 195560 }, - { url = "https://files.pythonhosted.org/packages/bb/57/f37041bbe5e0dfed80a3f6be2612a3a75b9cfe2652abf2c99bef3455bbad/propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583", size = 196895 }, - { url = "https://files.pythonhosted.org/packages/83/36/ae3cc3e4f310bff2f064e3d2ed5558935cc7778d6f827dce74dcfa125304/propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf", size = 207124 }, - { url = "https://files.pythonhosted.org/packages/8c/c4/811b9f311f10ce9d31a32ff14ce58500458443627e4df4ae9c264defba7f/propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034", size = 210442 }, - { url = "https://files.pythonhosted.org/packages/18/dd/a1670d483a61ecac0d7fc4305d91caaac7a8fc1b200ea3965a01cf03bced/propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b", size = 203219 }, - { url = "https://files.pythonhosted.org/packages/f9/2d/30ced5afde41b099b2dc0c6573b66b45d16d73090e85655f1a30c5a24e07/propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", size = 40313 }, - { url = "https://files.pythonhosted.org/packages/23/84/bd9b207ac80da237af77aa6e153b08ffa83264b1c7882495984fcbfcf85c/propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba", size = 44428 }, - { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] [[package]] name = "protobuf" -version = "3.19.6" +version = "7.34.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/d1/79bfd1f481469b661a2eddab551255536401892722189433282bfb13cfb1/protobuf-3.19.6.tar.gz", hash = "sha256:5f5540d57a43042389e87661c6eaa50f47c19c6176e8cf1c4f287aeefeccb5c4", size = 218071 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/00/04a2ab36b70a52d0356852979e08b44edde0435f2115dc66e25f2100f3ab/protobuf-7.34.0.tar.gz", hash = "sha256:3871a3df67c710aaf7bb8d214cc997342e63ceebd940c8c7fc65c9b3d697591a", size = 454726, upload-time = "2026-02-27T00:30:25.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/3b/90f805b9e5ecacf8a216f2e5acabc2d3ad965b62803510be41804e6bfbfe/protobuf-3.19.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:010be24d5a44be7b0613750ab40bc8b8cedc796db468eae6c779b395f50d1fa1", size = 913631 }, - { url = "https://files.pythonhosted.org/packages/26/ef/bd6ba3b4ff9a35944bdd325e2c9ee56f71e855757f7d43938232499f0278/protobuf-3.19.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11478547958c2dfea921920617eb457bc26867b0d1aa065ab05f35080c5d9eb6", size = 1055327 }, - { url = "https://files.pythonhosted.org/packages/4a/25/85bcc155980b5d7754ebdf4cb32039105f6020b6b2b8f7536a866113fc1c/protobuf-3.19.6-cp310-cp310-win32.whl", hash = "sha256:559670e006e3173308c9254d63facb2c03865818f22204037ab76f7a0ff70b5f", size = 775745 }, - { url = "https://files.pythonhosted.org/packages/97/f9/a14bac5331f3e55bcbbed906a0c8b112f554152ddf09efeb6f5f95653ffd/protobuf-3.19.6-cp310-cp310-win_amd64.whl", hash = "sha256:347b393d4dd06fb93a77620781e11c058b3b0a5289262f094379ada2920a3730", size = 895657 }, - { url = "https://files.pythonhosted.org/packages/32/27/1141a8232723dcb10a595cc0ce4321dcbbd5215300bf4acfc142343205bf/protobuf-3.19.6-py2.py3-none-any.whl", hash = "sha256:14082457dc02be946f60b15aad35e9f5c69e738f80ebbc0900a19bc83734a5a4", size = 162648 }, + { url = "https://files.pythonhosted.org/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408", size = 429248, upload-time = "2026-02-27T00:30:14.924Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/b029bbbc61e8937545da5b79aa405ab2d9cf307a728f8c9459ad60d7a481/protobuf-7.34.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:9d7a5005fb96f3c1e64f397f91500b0eb371b28da81296ae73a6b08a5b76cdd6", size = 325753, upload-time = "2026-02-27T00:30:17.247Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/09f02671eb75b251c5550a1c48e7b3d4b0623efd7c95a15a50f6f9fc1e2e/protobuf-7.34.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4a72a8ec94e7a9f7ef7fe818ed26d073305f347f8b3b5ba31e22f81fd85fca02", size = 340200, upload-time = "2026-02-27T00:30:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01", size = 324268, upload-time = "2026-02-27T00:30:20.088Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3e/38ff2ddee5cc946f575c9d8cc822e34bde205cf61acf8099ad88ef19d7d2/protobuf-7.34.0-cp310-abi3-win32.whl", hash = "sha256:f791ec509707a1d91bd02e07df157e75e4fb9fbdad12a81b7396201ec244e2e3", size = 426628, upload-time = "2026-02-27T00:30:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/cb/71/7c32eaf34a61a1bae1b62a2ac4ffe09b8d1bb0cf93ad505f42040023db89/protobuf-7.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:9f9079f1dde4e32342ecbd1c118d76367090d4aaa19da78230c38101c5b3dd40", size = 437901, upload-time = "2026-02-27T00:30:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl", hash = "sha256:e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7", size = 170716, upload-time = "2026-02-27T00:30:23.994Z" }, ] [[package]] name = "psutil" version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565, upload-time = "2024-10-17T21:31:45.68Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, - { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, - { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, - { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, - { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, - { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, - { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762, upload-time = "2024-10-17T21:32:05.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777, upload-time = "2024-10-17T21:32:07.872Z" }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259, upload-time = "2024-10-17T21:32:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255, upload-time = "2024-10-17T21:32:11.964Z" }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804, upload-time = "2024-10-17T21:32:13.785Z" }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386, upload-time = "2024-10-17T21:32:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228, upload-time = "2024-10-17T21:32:23.88Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pyarrow" -version = "19.0.0" +version = "23.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/01/fe1fd04744c2aa038e5a11c7a4adb3d62bce09798695e54f7274b5977134/pyarrow-19.0.0.tar.gz", hash = "sha256:8d47c691765cf497aaeed4954d226568563f1b3b74ff61139f2d77876717084b", size = 1129096 } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/02/1ad80ffd3c558916858a49c83b6e494a9d93009bbebc603cf0cb8263bea7/pyarrow-19.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c318eda14f6627966997a7d8c374a87d084a94e4e38e9abbe97395c215830e0c", size = 30686262 }, - { url = "https://files.pythonhosted.org/packages/1b/f0/adab5f142eb8203db8bfbd3a816816e37a85423ae684567e7f3555658315/pyarrow-19.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:62ef8360ff256e960f57ce0299090fb86423afed5e46f18f1225f960e05aae3d", size = 32100005 }, - { url = "https://files.pythonhosted.org/packages/94/8b/e674083610e5efc48d2f205c568d842cdfdf683d12f9ff0d546e38757722/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2795064647add0f16563e57e3d294dbfc067b723f0fd82ecd80af56dad15f503", size = 41144815 }, - { url = "https://files.pythonhosted.org/packages/d5/fb/2726241a792b7f8a58789e5a63d1be9a5a4059206318fd0ff9485a578952/pyarrow-19.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a218670b26fb1bc74796458d97bcab072765f9b524f95b2fccad70158feb8b17", size = 42180380 }, - { url = "https://files.pythonhosted.org/packages/7d/09/7aef12446d8e7002dfc07bb7bc71f594c1d5844ca78b364a49f07efb65b1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66732e39eaa2247996a6b04c8aa33e3503d351831424cdf8d2e9a0582ac54b34", size = 40515021 }, - { url = "https://files.pythonhosted.org/packages/31/55/f05fc5608cc96060c2b24de505324d641888bd62d4eed2fa1dacd872a1e1/pyarrow-19.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e675a3ad4732b92d72e4d24009707e923cab76b0d088e5054914f11a797ebe44", size = 42067488 }, - { url = "https://files.pythonhosted.org/packages/f0/01/097653cec7a944c16313cb748a326771133c142034b252076bd84743b98d/pyarrow-19.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f094742275586cdd6b1a03655ccff3b24b2610c3af76f810356c4c71d24a2a6c", size = 25276726 }, + { url = "https://files.pythonhosted.org/packages/bc/a8/24e5dc6855f50a62936ceb004e6e9645e4219a8065f304145d7fb8a79d5d/pyarrow-23.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", size = 34307390, upload-time = "2026-02-16T10:08:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8e/4be5617b4aaae0287f621ad31c6036e5f63118cfca0dc57d42121ff49b51/pyarrow-23.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", size = 35853761, upload-time = "2026-02-16T10:08:17.811Z" }, + { url = "https://files.pythonhosted.org/packages/2e/08/3e56a18819462210432ae37d10f5c8eed3828be1d6c751b6e6a2e93c286a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", size = 44493116, upload-time = "2026-02-16T10:08:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/c40b68001dbec8a3faa4c08cd8c200798ac732d2854537c5449dc859f55a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", size = 47564532, upload-time = "2026-02-16T10:08:34.27Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/73f611989116b6f53347581b02177f9f620efdf3cd3f405d0e83cdf53a83/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", size = 48183685, upload-time = "2026-02-16T10:08:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/6c6b3ecdae2a8c3aced99956187e8302fc954cc2cca2a37cf2111dad16ce/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", size = 50605582, upload-time = "2026-02-16T10:08:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/8d/94/d359e708672878d7638a04a0448edf7c707f9e5606cee11e15aaa5c7535a/pyarrow-23.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", size = 27521148, upload-time = "2026-02-16T10:08:58.077Z" }, ] [[package]] -name = "pyasn1" -version = "0.6.1" +name = "pycparser" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] -name = "pyasn1-modules" -version = "0.4.1" +name = "pydantic" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] -[[package]] -name = "pycryptodome" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, - { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, - { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, - { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, - { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, - { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, - { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, - { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, - { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, - { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, - { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, - { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, - { url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 }, - { url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 }, - { url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 }, - { url = "https://files.pythonhosted.org/packages/96/02/90ae1ac9f28be4df0ed88c127bf4acc1b102b40053e172759d4d1c54d937/pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", size = 1788273 }, +[package.optional-dependencies] +email = [ + { name = "email-validator" }, ] [[package]] -name = "pydantic" -version = "2.10.6" +name = "pydantic-core" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, ] [[package]] -name = "pydantic-core" -version = "2.27.2" +name = "pydantic-extra-types" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, ] [[package]] name = "pydantic-settings" -version = "2.7.1" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pymupdf" -version = "1.25.2" +version = "1.27.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/fb/d80374ab091ab7ad5a5e7981a45c877ae094db668c1ab4d30f1109a4ec6a/pymupdf-1.27.2.tar.gz", hash = "sha256:37fc9cedeafb40839f86a074d4d9feab725144bdd4bbfd20308ff8957e2b10af", size = 85353104, upload-time = "2026-03-10T12:53:01.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/ee/2c10b6bde83ee42f5150b690ace952a802a7e632776dadd42bbfe5b68601/pymupdf-1.27.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a60ff9010d7025428e31d92ac2c9b4218c7c4844409d0b31a050565ea0a955fd", size = 23987468, upload-time = "2026-03-10T12:37:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/c8cc8c8ade83f5a75ac0f543edc2bc3c52d8c38c1d55d1e0713558258540/pymupdf-1.27.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5095efb242cfe1c46fec1c864a13f000098564829c98366582dde7ad9e61aa32", size = 23262964, upload-time = "2026-03-10T12:37:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8e/df2ab91a680a77c82bc4501cdca60767b3758d75552e4d2849647a16cbc0/pymupdf-1.27.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1081235fcfad268d801cd73a7b69c629939e2c46ed4d97035cb1bb7b5b90dc54", size = 24318675, upload-time = "2026-03-10T12:37:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/ab/56/c6c16fa2dcfe2476ec28a9aaaca773dc35c593699e81e573211c91442770/pymupdf-1.27.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:917f4dd52daea504d5c60e1430c17d637b5014a43e66d068b4b356effe087dba", size = 24947974, upload-time = "2026-03-10T12:38:00.779Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4f/1659f1d80b5d2f5aad134c2ca63894c63daf47a3ffb7e18987fe25e49097/pymupdf-1.27.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9617d5e71c334937c804544fa201946c5f73d0a97b5842b96857bdabfefbc343", size = 25169417, upload-time = "2026-03-10T12:38:18.912Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/e34d704f7242885dd1d67cfbe1040051a04b4b7e2cf1cbd27af9bd4500a3/pymupdf-1.27.2-cp310-abi3-win32.whl", hash = "sha256:6deef49e06c9a5d8670bf5835a911ab887dac4b3ed4bd60ab7d93da6aa8ff6f1", size = 18008725, upload-time = "2026-03-10T12:38:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fb/a3f1f8813f6e93c65d1f7ebca6530a889f1ae109229b537f7a617b2aab57/pymupdf-1.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:acdfdb7329882246545a0f6bc85f91739e2773ed81f9301c1687cffb826470f3", size = 19237944, upload-time = "2026-03-10T12:38:45.603Z" }, +] + +[[package]] +name = "pymupdf-layout" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/fc/dd8776dc5c2f8cf0e51cf81a5f1de3840996bed7ca03ec768b0733024fb9/pymupdf-1.25.2.tar.gz", hash = "sha256:9ea88ff1b3ccb359620f106a6fd5ba6877d959d21d78272052c3496ceede6eec", size = 63814915 } +dependencies = [ + { name = "networkx" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "pymupdf" }, + { name = "pyyaml" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/24/34/8c3d82719d118beb48fded78fcab7cbe9ac3bf1906dc87a9ca4fd950087d/pymupdf-1.25.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59dea22b633cc4fc13670b4c5db50d71f8cd4f420814420f33ce47ddcb61e1f6", size = 19336722 }, - { url = "https://files.pythonhosted.org/packages/4f/ec/c7f742f56ee42be27b3afdbf3364da12f03e309f6638e666a7816d9eef23/pymupdf-1.25.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e8b8a874497cd0deee89a6a4fb76a3a08173c8d39e88fc7cf715764ec5a243e9", size = 18570847 }, - { url = "https://files.pythonhosted.org/packages/9d/27/557ee235aded5185e4824459e1540142fbb9323e1b83f77cbefe2e2c4e1e/pymupdf-1.25.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f61e5cdb25b86eb28d34aa3557b49ecf9e361d5f5cd3b1660406f8f0bf813af7", size = 19430802 }, - { url = "https://files.pythonhosted.org/packages/0e/de/35fde3d49e0d187b95ab64cc61b4d275ebc7fd4f45e152b206b0e17e6b69/pymupdf-1.25.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8cfa7a97d78f813d286ecba32369059d88073edd1e5cf105f4cd0811f71925", size = 19994315 }, - { url = "https://files.pythonhosted.org/packages/9d/d3/a8a09b550c62306c76e1c2d892c0890287470164d7941aea35330cceee4d/pymupdf-1.25.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:295505fe1ecb7c7b57d4124d373e207ea311d8e40bc7ac3016d8ec2d60b091e9", size = 21117143 }, - { url = "https://files.pythonhosted.org/packages/ef/ac/fc4f37c7620a20d25443868ed665291e96f283eda068cda673e9edebf5f0/pymupdf-1.25.2-cp39-abi3-win32.whl", hash = "sha256:b9488c8b82bb9be36fb13ee0c8d43b0ddcc50af83b61da01e6040413d9e67da6", size = 15084555 }, - { url = "https://files.pythonhosted.org/packages/64/8e/1d0ff215b37343c7e0bec4d571f1413e4f76a416591276b97081f1814710/pymupdf-1.25.2-cp39-abi3-win_amd64.whl", hash = "sha256:1b4ca6f5780d319a08dff885a5a0e3585c5d7af04dcfa063c535b88371fd91c1", size = 16531823 }, + { url = "https://files.pythonhosted.org/packages/12/2e/c0d80ff460babfb91ae708ff2d75a9bbf074322ce4f4ef1afcd911c2f209/pymupdf_layout-1.27.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bee613779fd1053979b7bfbfb9d2a8937066002fe6981fe9290c5f806d27e44e", size = 15799804, upload-time = "2026-03-10T12:50:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/db/b7/436d0011e0d7e5a94ef23f3ac1f5198f0584f3dd1a29fb1deae2647bd01e/pymupdf_layout-1.27.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:447c2f20524a1cca26dbb30611521758fe464ced7fdedb441c884bf40564e92c", size = 15795181, upload-time = "2026-03-10T12:51:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f4/9a9a984f172979ee850ccade9d20ad8a9ce0194a517e8b07e69c57c73f7c/pymupdf_layout-1.27.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:96767de7f05695d771171ed39077363a280bae6e9d5de70e484a888ae44e3cab", size = 15805201, upload-time = "2026-03-10T12:51:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/be/2d/e0545dcbe65d0e762004c4266f3f272a508a414a9359f452099bf3283dd1/pymupdf_layout-1.27.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:962cd43d70b1816df1ac10405bfb3a85545449253f0813a30198829cae64e053", size = 15806229, upload-time = "2026-03-10T12:51:32.981Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b4/f9f7293162a7ce4ccd490241e1e1bd6f415b05809e8f75704cca47e6e32e/pymupdf_layout-1.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:57f63186ab7170dfe1b6918c2ad5c241fdab34a1ed27a75b020306fa29aaf0ec", size = 15809666, upload-time = "2026-03-10T12:51:43.679Z" }, ] [[package]] name = "pymupdf4llm" -version = "0.0.17" +version = "1.27.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymupdf" }, + { name = "pymupdf-layout" }, + { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/3c/1a530a410bdf76d83289bf30b3b86236d338b3f5f21842790c2cf7e9c1f6/pymupdf4llm-0.0.17.tar.gz", hash = "sha256:27287ef9fe0217cf37841a3ef2bcf70da2553c43d95ea39b664a6de6485678c3", size = 25180 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/af/1576ecfc8a62d31c0c8b34b856e52f6b05f1d76546dbac0e1d037f044a9e/pymupdf4llm-0.0.17-py3-none-any.whl", hash = "sha256:26de9996945f15e3ca507908f80dc18a959f5b5214bb2e302c7f7034089665a0", size = 26190 }, + { url = "https://files.pythonhosted.org/packages/e9/23/ac3edfdd7ede9d01895f444b7d4ffffc3f044809e00791808ccc8ea194eb/pymupdf4llm-1.27.2.1-py3-none-any.whl", hash = "sha256:3d1e40d34dbe806e6a8cc7f55999cbd4e5ff5dd8629cfc42d885014b4662ec67", size = 78177, upload-time = "2026-03-10T16:20:02.852Z" }, ] [[package]] name = "pypandoc" -version = "1.15" +version = "1.16.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/26e650d053df5f3874aa3c05901a14166ce3271f58bfe114fd776987efbd/pypandoc-1.15.tar.gz", hash = "sha256:ea25beebe712ae41d63f7410c08741a3cab0e420f6703f95bc9b3a749192ce13", size = 32940 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/18/9f5f70567b97758625335209b98d5cb857e19aa1a9306e9749567a240634/pypandoc-1.16.2.tar.gz", hash = "sha256:7a72a9fbf4a5dc700465e384c3bb333d22220efc4e972cb98cf6fc723cdca86b", size = 31477, upload-time = "2025-11-13T16:30:29.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/06/0763e0ccc81754d3eadb21b2cb86cf21bdedc9b52698c2ad6785db7f0a4e/pypandoc-1.15-py3-none-any.whl", hash = "sha256:4ededcc76c8770f27aaca6dff47724578428eca84212a31479403a9731fc2b16", size = 21321 }, + { url = "https://files.pythonhosted.org/packages/bb/e9/b145683854189bba84437ea569bfa786f408c8dc5bc16d8eb0753f5583bf/pypandoc-1.16.2-py3-none-any.whl", hash = "sha256:c200c1139c8e3247baf38d1e9279e85d9f162499d1999c6aa8418596558fe79b", size = 19451, upload-time = "2025-11-13T16:30:07.66Z" }, ] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] [[package]] name = "pysocks" version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 }, + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, ] [[package]] -name = "python-dateutil" -version = "2.9.0.post0" +name = "pytest" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] -name = "python-docx" -version = "1.2.0" +name = "python-dateutil" +version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lxml" }, - { name = "typing-extensions" }, + { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] -name = "python-dotenv" -version = "1.0.1" +name = "python-discovery" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, ] [[package]] -name = "python-json-logger" -version = "3.2.1" +name = "python-docx" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/c4/358cd13daa1d912ef795010897a483ab2f0b41c9ea1b35235a8b2f7d15a7/python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008", size = 16287 } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/72/2f30cf26664fcfa0bd8ec5ee62ec90c03bd485e4a294d92aabc76c5203a5/python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090", size = 14924 }, + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, ] [[package]] -name = "python-multipart" -version = "0.0.20" +name = "python-dotenv" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] -name = "python-pptx" -version = "0.6.23" +name = "python-json-logger" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lxml" }, - { name = "pillow" }, - { name = "xlsxwriter" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/e7/aeaf794b2d440da609684494075e64cfada248026ecb265807d0668cdd00/python-pptx-0.6.23.tar.gz", hash = "sha256:587497ff28e779ab18dbb074f6d4052893c85dedc95ed75df319364f331fedee", size = 10083771 } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/49/6eee83072983473e9905ffddd5c2032b9a0ca4616425560d6d582287b467/python_pptx-0.6.23-py3-none-any.whl", hash = "sha256:dd0527194627a2b7cc05f3ba23ecaa2d9a0d5ac9b6193a28ed1b7a716f4217d4", size = 471575 }, + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] [[package]] -name = "pytorch-lightning" -version = "1.8.3.post1" +name = "python-multipart" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fsspec", extra = ["http"] }, - { name = "lightning-utilities" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tensorboardx" }, - { name = "torch" }, - { name = "torchmetrics" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/a3/92533d6b7af907849eb491e867ae488689c4b6207b513ecd31d9febb7a5b/pytorch-lightning-1.8.3.post1.tar.gz", hash = "sha256:4a1804d55c3aa675a2dd21ee17282cad0bc703dfeace70e75dc956f1faf31411", size = 574889 } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/ea/7cb6830785a844eddf45f88b9b14cbf8c3ecce718d4de35503783522f894/pytorch_lightning-1.8.3.post1-py3-none-any.whl", hash = "sha256:2b04cdb876f4e8749161510712b22081dab8db3e6548530608680a415844b6e3", size = 798949 }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -2593,154 +2665,144 @@ dependencies = [ { name = "numpy" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/c3/aecc90deea743b93757e05e235f70eef479509eb3eb11b19f3aa0d528541/pytorch_revgrad-0.2.0.tar.gz", hash = "sha256:9cf097a7d18cbadddeaec9fef74b258d70b6cb8d0c77f524baab18bffc7d7be9", size = 7086 } +sdist = { url = "https://files.pythonhosted.org/packages/90/c3/aecc90deea743b93757e05e235f70eef479509eb3eb11b19f3aa0d528541/pytorch_revgrad-0.2.0.tar.gz", hash = "sha256:9cf097a7d18cbadddeaec9fef74b258d70b6cb8d0c77f524baab18bffc7d7be9", size = 7086, upload-time = "2021-01-09T17:35:49.131Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/e9/10b11b186b99c40213dca68cf6c38051b6704a74e1056d3f3ca4c12f14b9/pytorch_revgrad-0.2.0-py3-none-any.whl", hash = "sha256:2276fb189b2ce26f756a97effe2a6bcf8f7fdc60542c5dfb45c53f09ef123aa7", size = 4564 }, + { url = "https://files.pythonhosted.org/packages/ec/e9/10b11b186b99c40213dca68cf6c38051b6704a74e1056d3f3ca4c12f14b9/pytorch_revgrad-0.2.0-py3-none-any.whl", hash = "sha256:2276fb189b2ce26f756a97effe2a6bcf8f7fdc60542c5dfb45c53f09ef123aa7", size = 4564, upload-time = "2021-01-09T17:35:47.543Z" }, ] [[package]] name = "pytz" -version = "2024.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, -] - -[[package]] -name = "pywin32" -version = "308" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/a6/3e9f2c474895c1bb61b11fa9640be00067b5c5b363c501ee9c3fa53aec01/pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", size = 5927028 }, - { url = "https://files.pythonhosted.org/packages/d9/b4/84e2463422f869b4b718f79eb7530a4c1693e96b8a4e5e968de38be4d2ba/pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", size = 6558484 }, - { url = "https://files.pythonhosted.org/packages/9f/8f/fb84ab789713f7c6feacaa08dad3ec8105b88ade8d1c4f0f0dfcaaa017d6/pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", size = 7971454 }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] name = "pywinpty" -version = "2.0.14" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/82/90f8750423cba4b9b6c842df227609fb60704482d7abf6dd47e2babc055a/pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e", size = 27769 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/09/56376af256eab8cc5f8982a3b138d387136eca27fa1a8a68660e8ed59e4b/pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f", size = 1397115 }, + { url = "https://files.pythonhosted.org/packages/62/28/a652709bd76ca7533cd1c443b03add9f5051fdf71bc6bdb8801dddd4e7a3/pywinpty-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:ff05f12d775b142b11c6fe085129bdd759b61cf7d41da6c745e78e3a1ef5bf40", size = 2114320, upload-time = "2026-02-04T21:53:50.972Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/a0181cc5c2d5635d3dbc3802b97bc8e3ad4fa7502ccef576651a5e08e54c/pywinpty-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:340ccacb4d74278a631923794ccd758471cfc8eeeeee4610b280420a17ad1e82", size = 235670, upload-time = "2026-02-04T21:50:20.324Z" }, ] [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, ] [[package]] name = "pyzmq" -version = "26.2.0" +version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", size = 1340058 }, - { url = "https://files.pythonhosted.org/packages/a2/1f/a006f2e8e4f7d41d464272012695da17fb95f33b54342612a6890da96ff6/pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", size = 1008818 }, - { url = "https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", size = 673199 }, - { url = "https://files.pythonhosted.org/packages/c9/78/486f3e2e824f3a645238332bf5a4c4b4477c3063033a27c1e4052358dee2/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", size = 911762 }, - { url = "https://files.pythonhosted.org/packages/5e/3b/2eb1667c9b866f53e76ee8b0c301b0469745a23bd5a87b7ee3d5dd9eb6e5/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", size = 868773 }, - { url = "https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", size = 868834 }, - { url = "https://files.pythonhosted.org/packages/ad/e5/9efaeb1d2f4f8c50da04144f639b042bc52869d3a206d6bf672ab3522163/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", size = 1202861 }, - { url = "https://files.pythonhosted.org/packages/c3/62/c721b5608a8ac0a69bb83cbb7d07a56f3ff00b3991a138e44198a16f94c7/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", size = 1515304 }, - { url = "https://files.pythonhosted.org/packages/87/84/e8bd321aa99b72f48d4606fc5a0a920154125bd0a4608c67eab742dab087/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", size = 1414712 }, - { url = "https://files.pythonhosted.org/packages/cd/cd/420e3fd1ac6977b008b72e7ad2dae6350cc84d4c5027fc390b024e61738f/pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", size = 578113 }, - { url = "https://files.pythonhosted.org/packages/5c/57/73930d56ed45ae0cb4946f383f985c855c9b3d4063f26416998f07523c0e/pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", size = 641631 }, - { url = "https://files.pythonhosted.org/packages/61/d2/ae6ac5c397f1ccad59031c64beaafce7a0d6182e0452cc48f1c9c87d2dd0/pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", size = 543528 }, - { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, - { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, - { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, - { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, - { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, ] [[package]] name = "rapidfuzz" -version = "3.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/aa/25e5a20131369d82c7b8288c99c2c3011ec47a3f5953ccc9cb8145720be5/rapidfuzz-3.11.0.tar.gz", hash = "sha256:a53ca4d3f52f00b393fab9b5913c5bafb9afc27d030c8a1db1283da6917a860f", size = 57983000 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/70/820ebf9eb22ad97b9e0bb9fd1ad8c6be4c8db5a0974d12ce27b5c9a30db0/rapidfuzz-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb8a54543d16ab1b69e2c5ed96cabbff16db044a50eddfc028000138ca9ddf33", size = 1954240 }, - { url = "https://files.pythonhosted.org/packages/41/bc/e39abdc28160d8147ccab0aa922a29be50529dcf149615a68a324ff6f9b1/rapidfuzz-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231c8b2efbd7f8d2ecd1ae900363ba168b8870644bb8f2b5aa96e4a7573bde19", size = 1427139 }, - { url = "https://files.pythonhosted.org/packages/b6/2d/19b8e5d80257b13d73ba994552b78a69ac2ed70f1de716f1b02fcb84d09c/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54e7f442fb9cca81e9df32333fb075ef729052bcabe05b0afc0441f462299114", size = 1419602 }, - { url = "https://files.pythonhosted.org/packages/8c/82/1fc80cc531ec712872025c19118d78eb23aff09c7144b380c2c4b544b0d1/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:906f1f2a1b91c06599b3dd1be207449c5d4fc7bd1e1fa2f6aef161ea6223f165", size = 5635370 }, - { url = "https://files.pythonhosted.org/packages/3c/5c/007b90af25f98e301b5f7a095059b09f602701443d555724c9226a45514c/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed59044aea9eb6c663112170f2399b040d5d7b162828b141f2673e822093fa8", size = 1680848 }, - { url = "https://files.pythonhosted.org/packages/01/04/e481530eff5d1cf337b86a3095dd4de0b758c37291e51eb0d8c4f7d49719/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cb1965a28b0fa64abdee130c788a0bc0bb3cf9ef7e3a70bf055c086c14a3d7e", size = 1682131 }, - { url = "https://files.pythonhosted.org/packages/10/15/b0ec18edfe6146d8915679644ab7584cd0165724d6a53bcc43bd59f8edb5/rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b488b244931d0291412917e6e46ee9f6a14376625e150056fe7c4426ef28225", size = 3134097 }, - { url = "https://files.pythonhosted.org/packages/8b/0e/cf0a5d62977381bca981fc171fd6c85dc52ca1239eaacf9c1d38978c5866/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f0ba13557fec9d5ffc0a22826754a7457cc77f1b25145be10b7bb1d143ce84c6", size = 2332928 }, - { url = "https://files.pythonhosted.org/packages/dc/71/568d383eb36586c9e7e13f1327203e2be0938e5ff070c1fa2a99b418808e/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3871fa7dfcef00bad3c7e8ae8d8fd58089bad6fb21f608d2bf42832267ca9663", size = 6940409 }, - { url = "https://files.pythonhosted.org/packages/ba/23/02972657d69e6d3aae2cdbd67debad080410ff9ef8849d8eab5e580a48a5/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b2669eafee38c5884a6e7cc9769d25c19428549dcdf57de8541cf9e82822e7db", size = 2715928 }, - { url = "https://files.pythonhosted.org/packages/17/17/d964d770faa4e25e125618c00e31607cf8ce639d518fc29d200edf06cfda/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ffa1bb0e26297b0f22881b219ffc82a33a3c84ce6174a9d69406239b14575bd5", size = 3265078 }, - { url = "https://files.pythonhosted.org/packages/bc/13/a117412b1e4ed0bb23b9891a45a59812d96fde8c076b8b8b828aa7ca3710/rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:45b15b8a118856ac9caac6877f70f38b8a0d310475d50bc814698659eabc1cdb", size = 4169215 }, - { url = "https://files.pythonhosted.org/packages/9f/0d/89ef496aedf885db4bfe7f46ac6727666afe0d9d8ca5b4f9c7cc8eef0378/rapidfuzz-3.11.0-cp310-cp310-win32.whl", hash = "sha256:22033677982b9c4c49676f215b794b0404073f8974f98739cb7234e4a9ade9ad", size = 1841736 }, - { url = "https://files.pythonhosted.org/packages/47/9a/69019f4e9c8a42e4aca0169dbae71602aba4e0fa4c5e84515f3ed682e59a/rapidfuzz-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:be15496e7244361ff0efcd86e52559bacda9cd975eccf19426a0025f9547c792", size = 1614955 }, - { url = "https://files.pythonhosted.org/packages/37/65/6fb036e39d175299ce44e5186ee2d08b9ea02d732ed6dbd70280f63b4eba/rapidfuzz-3.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:714a7ba31ba46b64d30fccfe95f8013ea41a2e6237ba11a805a27cdd3bce2573", size = 863543 }, - { url = "https://files.pythonhosted.org/packages/30/5a/8ac67667663d24cc4d4b76f63783e58ef03e4d4843d02dab6b2f8470ea5e/rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f06e3c4c0a8badfc4910b9fd15beb1ad8f3b8fafa8ea82c023e5e607b66a78e4", size = 1853100 }, - { url = "https://files.pythonhosted.org/packages/dc/72/b043c26e93fb1bc5dfab1e5dd0f8d2f6135c2aa48e6db0660d4ecc5b157a/rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fe7aaf5a54821d340d21412f7f6e6272a9b17a0cbafc1d68f77f2fc11009dcd5", size = 1361961 }, - { url = "https://files.pythonhosted.org/packages/5c/4a/29916c0dd853d22ef7b988af43f4e34d327581e16f60b4c9b0f229fa306c/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25398d9ac7294e99876a3027ffc52c6bebeb2d702b1895af6ae9c541ee676702", size = 1354313 }, - { url = "https://files.pythonhosted.org/packages/41/39/f352af4ede7faeeea20bae2537f1fa60c3bbbf2696f0f2f3dda696745239/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a52eea839e4bdc72c5e60a444d26004da00bb5bc6301e99b3dde18212e41465", size = 5478019 }, - { url = "https://files.pythonhosted.org/packages/99/8e/86f8a11ac0edda63ff5314d992aa1576fff5d8233f4310d46a6bb0551122/rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c87319b0ab9d269ab84f6453601fd49b35d9e4a601bbaef43743f26fabf496c", size = 3056881 }, - { url = "https://files.pythonhosted.org/packages/98/53/222dceb24a83c7d7d76086b6d5bfd3d6aa9988ea73d356d287b5c437c0d5/rapidfuzz-3.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3048c6ed29d693fba7d2a7caf165f5e0bb2b9743a0989012a98a47b975355cca", size = 1543944 }, +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, + { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, - { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, - { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, - { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, - { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, - { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, - { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, - { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, - { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, - { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, - { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, - { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, - { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, - { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2748,9 +2810,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [package.optional-dependencies] @@ -2758,19 +2820,6 @@ socks = [ { name = "pysocks" }, ] -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, -] - [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -2778,130 +2827,156 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] [[package]] name = "rfc3986-validator" version = "0.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760 } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242 }, + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] [[package]] name = "rich" -version = "13.9.4" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "rich-toolkit" -version = "0.13.2" +version = "0.19.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 } +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 }, + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, ] [[package]] -name = "rpds-py" -version = "0.22.3" +name = "rignore" +version = "0.7.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/2a/ead1d09e57449b99dcc190d8d2323e3a167421d8f8fdf0f217c6f6befe47/rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", size = 359514 }, - { url = "https://files.pythonhosted.org/packages/8f/7e/1254f406b7793b586c68e217a6a24ec79040f85e030fff7e9049069284f4/rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", size = 349031 }, - { url = "https://files.pythonhosted.org/packages/aa/da/17c6a2c73730d426df53675ff9cc6653ac7a60b6438d03c18e1c822a576a/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", size = 381485 }, - { url = "https://files.pythonhosted.org/packages/aa/13/2dbacd820466aa2a3c4b747afb18d71209523d353cf865bf8f4796c969ea/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", size = 386794 }, - { url = "https://files.pythonhosted.org/packages/6d/62/96905d0a35ad4e4bc3c098b2f34b2e7266e211d08635baa690643d2227be/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", size = 423523 }, - { url = "https://files.pythonhosted.org/packages/eb/1b/d12770f2b6a9fc2c3ec0d810d7d440f6d465ccd8b7f16ae5385952c28b89/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", size = 446695 }, - { url = "https://files.pythonhosted.org/packages/4d/cf/96f1fd75512a017f8e07408b6d5dbeb492d9ed46bfe0555544294f3681b3/rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", size = 381959 }, - { url = "https://files.pythonhosted.org/packages/ab/f0/d1c5b501c8aea85aeb938b555bfdf7612110a2f8cdc21ae0482c93dd0c24/rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", size = 410420 }, - { url = "https://files.pythonhosted.org/packages/33/3b/45b6c58fb6aad5a569ae40fb890fc494c6b02203505a5008ee6dc68e65f7/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", size = 557620 }, - { url = "https://files.pythonhosted.org/packages/83/62/3fdd2d3d47bf0bb9b931c4c73036b4ab3ec77b25e016ae26fab0f02be2af/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", size = 584202 }, - { url = "https://files.pythonhosted.org/packages/04/f2/5dced98b64874b84ca824292f9cee2e3f30f3bcf231d15a903126684f74d/rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", size = 552787 }, - { url = "https://files.pythonhosted.org/packages/67/13/2273dea1204eda0aea0ef55145da96a9aa28b3f88bb5c70e994f69eda7c3/rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", size = 220088 }, - { url = "https://files.pythonhosted.org/packages/4e/80/8c8176b67ad7f4a894967a7a4014ba039626d96f1d4874d53e409b58d69f/rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", size = 231737 }, - { url = "https://files.pythonhosted.org/packages/8b/63/e29f8ee14fcf383574f73b6bbdcbec0fbc2e5fc36b4de44d1ac389b1de62/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", size = 360786 }, - { url = "https://files.pythonhosted.org/packages/d3/e0/771ee28b02a24e81c8c0e645796a371350a2bb6672753144f36ae2d2afc9/rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", size = 350589 }, - { url = "https://files.pythonhosted.org/packages/cf/49/abad4c4a1e6f3adf04785a99c247bfabe55ed868133e2d1881200aa5d381/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", size = 381848 }, - { url = "https://files.pythonhosted.org/packages/3a/7d/f4bc6d6fbe6af7a0d2b5f2ee77079efef7c8528712745659ec0026888998/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", size = 387879 }, - { url = "https://files.pythonhosted.org/packages/13/b0/575c797377fdcd26cedbb00a3324232e4cb2c5d121f6e4b0dbf8468b12ef/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", size = 423916 }, - { url = "https://files.pythonhosted.org/packages/54/78/87157fa39d58f32a68d3326f8a81ad8fb99f49fe2aa7ad9a1b7d544f9478/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", size = 448410 }, - { url = "https://files.pythonhosted.org/packages/59/69/860f89996065a88be1b6ff2d60e96a02b920a262d8aadab99e7903986597/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", size = 382841 }, - { url = "https://files.pythonhosted.org/packages/bd/d7/bc144e10d27e3cb350f98df2492a319edd3caaf52ddfe1293f37a9afbfd7/rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", size = 409662 }, - { url = "https://files.pythonhosted.org/packages/14/2a/6bed0b05233c291a94c7e89bc76ffa1c619d4e1979fbfe5d96024020c1fb/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", size = 558221 }, - { url = "https://files.pythonhosted.org/packages/11/23/cd8f566de444a137bc1ee5795e47069a947e60810ba4152886fe5308e1b7/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", size = 583780 }, - { url = "https://files.pythonhosted.org/packages/8d/63/79c3602afd14d501f751e615a74a59040328da5ef29ed5754ae80d236b84/rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", size = 553619 }, - { url = "https://files.pythonhosted.org/packages/9f/2e/c5c1689e80298d4e94c75b70faada4c25445739d91b94c211244a3ed7ed1/rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", size = 233338 }, + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, ] [[package]] -name = "rsa" -version = "4.9" +name = "rpds-py" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, ] [[package]] name = "s3transfer" -version = "0.11.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/45/2323b5928f86fd29f9afdcef4659f68fa73eaa5356912b774227f5cf46b5/s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f", size = 147885 } +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/ac/e7dc469e49048dc57f62e0c555d2ee3117fa30813d2a1a2962cce3a2a82a/s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc", size = 84151 }, + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] name = "safetensors" -version = "0.5.2" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/4f/2ef9ef1766f8c194b01b67a63a444d2e557c8fe1d82faf3ebd85f370a917/safetensors-0.5.2.tar.gz", hash = "sha256:cb4a8d98ba12fa016f4241932b1fc5e702e5143f5374bba0bbcf7ddc1c4cf2b8", size = 66957 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/d1/017e31e75e274492a11a456a9e7c171f8f7911fe50735b4ec6ff37221220/safetensors-0.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:45b6092997ceb8aa3801693781a71a99909ab9cc776fbc3fa9322d29b1d3bef2", size = 427067 }, - { url = "https://files.pythonhosted.org/packages/24/84/e9d3ff57ae50dd0028f301c9ee064e5087fe8b00e55696677a0413c377a7/safetensors-0.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6d0d6a8ee2215a440e1296b843edf44fd377b055ba350eaba74655a2fe2c4bae", size = 408856 }, - { url = "https://files.pythonhosted.org/packages/f1/1d/fe95f5dd73db16757b11915e8a5106337663182d0381811c81993e0014a9/safetensors-0.5.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86016d40bcaa3bcc9a56cd74d97e654b5f4f4abe42b038c71e4f00a089c4526c", size = 450088 }, - { url = "https://files.pythonhosted.org/packages/cf/21/e527961b12d5ab528c6e47b92d5f57f33563c28a972750b238b871924e49/safetensors-0.5.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:990833f70a5f9c7d3fc82c94507f03179930ff7d00941c287f73b6fcbf67f19e", size = 458966 }, - { url = "https://files.pythonhosted.org/packages/a5/8b/1a037d7a57f86837c0b41905040369aea7d8ca1ec4b2a77592372b2ec380/safetensors-0.5.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dfa7c2f3fe55db34eba90c29df94bcdac4821043fc391cb5d082d9922013869", size = 509915 }, - { url = "https://files.pythonhosted.org/packages/61/3d/03dd5cfd33839df0ee3f4581a20bd09c40246d169c0e4518f20b21d5f077/safetensors-0.5.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46ff2116150ae70a4e9c490d2ab6b6e1b1b93f25e520e540abe1b81b48560c3a", size = 527664 }, - { url = "https://files.pythonhosted.org/packages/c5/dc/8952caafa9a10a3c0f40fa86bacf3190ae7f55fa5eef87415b97b29cb97f/safetensors-0.5.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab696dfdc060caffb61dbe4066b86419107a24c804a4e373ba59be699ebd8d5", size = 461978 }, - { url = "https://files.pythonhosted.org/packages/60/da/82de1fcf1194e3dbefd4faa92dc98b33c06bed5d67890e0962dd98e18287/safetensors-0.5.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03c937100f38c9ff4c1507abea9928a6a9b02c9c1c9c3609ed4fb2bf413d4975", size = 491253 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d90e273c25f90c3ba1b0196a972003786f04c39e302fbd6649325b1272bb/safetensors-0.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a00e737948791b94dad83cf0eafc09a02c4d8c2171a239e8c8572fe04e25960e", size = 628644 }, - { url = "https://files.pythonhosted.org/packages/70/3c/acb23e05aa34b4f5edd2e7f393f8e6480fbccd10601ab42cd03a57d4ab5f/safetensors-0.5.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d3a06fae62418ec8e5c635b61a8086032c9e281f16c63c3af46a6efbab33156f", size = 721648 }, - { url = "https://files.pythonhosted.org/packages/71/45/eaa3dba5253a7c6931230dc961641455710ab231f8a89cb3c4c2af70f8c8/safetensors-0.5.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1506e4c2eda1431099cebe9abf6c76853e95d0b7a95addceaa74c6019c65d8cf", size = 659588 }, - { url = "https://files.pythonhosted.org/packages/b0/71/2f9851164f821064d43b481ddbea0149c2d676c4f4e077b178e7eeaa6660/safetensors-0.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c5b5d9da594f638a259fca766046f44c97244cc7ab8bef161b3e80d04becc76", size = 632533 }, - { url = "https://files.pythonhosted.org/packages/00/f1/5680e2ef61d9c61454fad82c344f0e40b8741a9dbd1e31484f0d31a9b1c3/safetensors-0.5.2-cp38-abi3-win32.whl", hash = "sha256:fe55c039d97090d1f85277d402954dd6ad27f63034fa81985a9cc59655ac3ee2", size = 291167 }, - { url = "https://files.pythonhosted.org/packages/86/ca/aa489392ec6fb59223ffce825461e1f811a3affd417121a2088be7a5758b/safetensors-0.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:78abdddd03a406646107f973c7843276e7b64e5e32623529dc17f3d94a20f589", size = 303756 }, +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, ] [[package]] name = "scikit-learn" -version = "1.5.2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, @@ -2909,13 +2984,13 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 } +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/89/be41419b4bec629a4691183a5eb1796f91252a13a5ffa243fd958cad7e91/scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", size = 12106070 }, - { url = "https://files.pythonhosted.org/packages/bf/e0/3b6d777d375f3b685f433c93384cdb724fb078e1dc8f8ff0950467e56c30/scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", size = 10971758 }, - { url = "https://files.pythonhosted.org/packages/7b/31/eb7dd56c371640753953277de11356c46a3149bfeebb3d7dcd90b993715a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", size = 12500080 }, - { url = "https://files.pythonhosted.org/packages/4c/1e/a7c7357e704459c7d56a18df4a0bf08669442d1f8878cc0864beccd6306a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", size = 13347241 }, - { url = "https://files.pythonhosted.org/packages/48/76/154ebda6794faf0b0f3ccb1b5cd9a19f0a63cb9e1f3d2c61b6114002677b/scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", size = 11000477 }, + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, ] [[package]] @@ -2925,16 +3000,16 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/e5/0230da034a2e1b1feb32621d7cd57c59484091d6dccc9e6b855b0d309fc9/scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b", size = 58618870 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/e5/0230da034a2e1b1feb32621d7cd57c59484091d6dccc9e6b855b0d309fc9/scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b", size = 58618870, upload-time = "2024-06-24T20:35:18.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/face72921ce52d74880b380e6f86b3caa6c65766c5808fbe179e208b9c6d/scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484", size = 39120226 }, - { url = "https://files.pythonhosted.org/packages/6e/a1/0093566d31ae662e942d4079e2a4dea4256723bf3d072ae67f5ba41aee0d/scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6", size = 29866893 }, - { url = "https://files.pythonhosted.org/packages/52/21/05a182fb405a53dfbdf6415308bf185677e89188bc2206de011a3653f48e/scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7", size = 23076258 }, - { url = "https://files.pythonhosted.org/packages/5c/63/9954d14012a2f4aff4570f1aaf076d7f65f3fc246ae4483b765488d57d51/scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1", size = 25454715 }, - { url = "https://files.pythonhosted.org/packages/57/b8/ca969a99d34956c6546cbb9ea3f863a387009f68cdbad13cdb07db0cc23d/scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0", size = 35569038 }, - { url = "https://files.pythonhosted.org/packages/e2/20/15c8fe0dfebb6facd81b3d08bf45dfa080e305deb17172b0a40eba59e927/scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0", size = 41135959 }, - { url = "https://files.pythonhosted.org/packages/df/a2/8721f93fbf98a69067d20bdfded36a7de2a3d811f192edba9eeefbde61b8/scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d", size = 41118514 }, - { url = "https://files.pythonhosted.org/packages/a3/0c/82c1330c08f31d61142d38cb9a185e01c2403c990d10dab208032e62d0fa/scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359", size = 44763252 }, + { url = "https://files.pythonhosted.org/packages/c6/90/face72921ce52d74880b380e6f86b3caa6c65766c5808fbe179e208b9c6d/scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484", size = 39120226, upload-time = "2024-06-24T20:31:50.451Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a1/0093566d31ae662e942d4079e2a4dea4256723bf3d072ae67f5ba41aee0d/scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6", size = 29866893, upload-time = "2024-06-24T20:31:57.337Z" }, + { url = "https://files.pythonhosted.org/packages/52/21/05a182fb405a53dfbdf6415308bf185677e89188bc2206de011a3653f48e/scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7", size = 23076258, upload-time = "2024-06-24T20:32:02.711Z" }, + { url = "https://files.pythonhosted.org/packages/5c/63/9954d14012a2f4aff4570f1aaf076d7f65f3fc246ae4483b765488d57d51/scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1", size = 25454715, upload-time = "2024-06-24T20:32:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/57/b8/ca969a99d34956c6546cbb9ea3f863a387009f68cdbad13cdb07db0cc23d/scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0", size = 35569038, upload-time = "2024-06-24T20:32:17.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/15c8fe0dfebb6facd81b3d08bf45dfa080e305deb17172b0a40eba59e927/scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0", size = 41135959, upload-time = "2024-06-24T20:32:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/8721f93fbf98a69067d20bdfded36a7de2a3d811f192edba9eeefbde61b8/scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d", size = 41118514, upload-time = "2024-06-24T20:32:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0c/82c1330c08f31d61142d38cb9a185e01c2403c990d10dab208032e62d0fa/scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359", size = 44763252, upload-time = "2024-06-24T20:32:45.06Z" }, ] [[package]] @@ -2946,9 +3021,9 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696 } +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 }, + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] [[package]] @@ -2958,133 +3033,138 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/8a/1ae8e3deb805831933b63d58851ab41ff2472099e15511fc62039421ad70/segtok-1.5.11.tar.gz", hash = "sha256:8ab2dd44245bcbfec25b575dc4618473bbdf2af8c2649698cd5a370f42f3db23", size = 25244 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/8a/1ae8e3deb805831933b63d58851ab41ff2472099e15511fc62039421ad70/segtok-1.5.11.tar.gz", hash = "sha256:8ab2dd44245bcbfec25b575dc4618473bbdf2af8c2649698cd5a370f42f3db23", size = 25244, upload-time = "2021-12-15T21:56:14.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/60/d384dbae5d4756e33f1750fa3472303de2c827011907a64e213e114d0556/segtok-1.5.11-py3-none-any.whl", hash = "sha256:910616b76198c3141b2772df530270d3b706e42ae69a5b30ef115c7bd5d1501a", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/dd/60/d384dbae5d4756e33f1750fa3472303de2c827011907a64e213e114d0556/segtok-1.5.11-py3-none-any.whl", hash = "sha256:910616b76198c3141b2772df530270d3b706e42ae69a5b30ef115c7bd5d1501a", size = 24332, upload-time = "2021-12-15T21:56:12.508Z" }, ] [[package]] -name = "semver" -version = "3.0.4" +name = "send2trash" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730 } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912 }, + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] [[package]] -name = "send2trash" -version = "1.8.3" +name = "sentence-transformers" +version = "5.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394 } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/26/448453925b6ce0c29d8b54327caa71ee4835511aef02070467402273079c/sentence_transformers-5.3.0.tar.gz", hash = "sha256:414a0a881f53a4df0e6cbace75f823bfcb6b94d674c42a384b498959b7c065e2", size = 403330, upload-time = "2026-03-12T14:53:40.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072 }, + { url = "https://files.pythonhosted.org/packages/e2/9c/2fa7224058cad8df68d84bafee21716f30892cecc7ad1ad73bde61d23754/sentence_transformers-5.3.0-py3-none-any.whl", hash = "sha256:dca6b98db790274a68185d27a65801b58b4caf653a4e556b5f62827509347c7d", size = 512390, upload-time = "2026-03-12T14:53:39.035Z" }, ] [[package]] name = "sentencepiece" version = "0.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/d2/b9c7ca067c26d8ff085d252c89b5f69609ca93fb85a00ede95f4857865d4/sentencepiece-0.2.0.tar.gz", hash = "sha256:a52c19171daaf2e697dc6cbe67684e0fa341b1248966f6aebb541de654d15843", size = 2632106 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/d2/b9c7ca067c26d8ff085d252c89b5f69609ca93fb85a00ede95f4857865d4/sentencepiece-0.2.0.tar.gz", hash = "sha256:a52c19171daaf2e697dc6cbe67684e0fa341b1248966f6aebb541de654d15843", size = 2632106, upload-time = "2024-02-19T17:06:47.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/71/98648c3b64b23edb5403f74bcc906ad21766872a6e1ada26ea3f1eb941ab/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:188779e1298a1c8b8253c7d3ad729cb0a9891e5cef5e5d07ce4592c54869e227", size = 2408979 }, - { url = "https://files.pythonhosted.org/packages/77/9f/7efbaa6d4c0c718a9affbecc536b03ca62f99f421bdffb531c16030e2d2b/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bed9cf85b296fa2b76fc2547b9cbb691a523864cebaee86304c43a7b4cb1b452", size = 1238845 }, - { url = "https://files.pythonhosted.org/packages/1c/e4/c2541027a43ec6962ba9b601805d17ba3f86b38bdeae0e8ac65a2981e248/sentencepiece-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7b67e724bead13f18db6e1d10b6bbdc454af574d70efbb36f27d90387be1ca3", size = 1181472 }, - { url = "https://files.pythonhosted.org/packages/fd/46/316c1ba6c52b97de76aff7b9da678f7afbb52136afb2987c474d95630e65/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fde4b08cfe237be4484c6c7c2e2c75fb862cfeab6bd5449ce4caeafd97b767a", size = 1259151 }, - { url = "https://files.pythonhosted.org/packages/aa/5a/3c48738a0835d76dd06c62b6ac48d39c923cde78dd0f587353bdcbb99851/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c378492056202d1c48a4979650981635fd97875a00eabb1f00c6a236b013b5e", size = 1355931 }, - { url = "https://files.pythonhosted.org/packages/a6/27/33019685023221ca8ed98e8ceb7ae5e166032686fa3662c68f1f1edf334e/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1380ce6540a368de2ef6d7e6ba14ba8f3258df650d39ba7d833b79ee68a52040", size = 1301537 }, - { url = "https://files.pythonhosted.org/packages/ca/e4/55f97cef14293171fef5f96e96999919ab5b4d1ce95b53547ad653d7e3bf/sentencepiece-0.2.0-cp310-cp310-win32.whl", hash = "sha256:a1151d6a6dd4b43e552394aed0edfe9292820272f0194bd56c7c1660a0c06c3d", size = 936747 }, - { url = "https://files.pythonhosted.org/packages/85/f4/4ef1a6e0e9dbd8a60780a91df8b7452ada14cfaa0e17b3b8dfa42cecae18/sentencepiece-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:d490142b0521ef22bc1085f061d922a2a6666175bb6b42e588ff95c0db6819b2", size = 991525 }, + { url = "https://files.pythonhosted.org/packages/f6/71/98648c3b64b23edb5403f74bcc906ad21766872a6e1ada26ea3f1eb941ab/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:188779e1298a1c8b8253c7d3ad729cb0a9891e5cef5e5d07ce4592c54869e227", size = 2408979, upload-time = "2024-02-19T17:05:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/77/9f/7efbaa6d4c0c718a9affbecc536b03ca62f99f421bdffb531c16030e2d2b/sentencepiece-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bed9cf85b296fa2b76fc2547b9cbb691a523864cebaee86304c43a7b4cb1b452", size = 1238845, upload-time = "2024-02-19T17:05:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e4/c2541027a43ec6962ba9b601805d17ba3f86b38bdeae0e8ac65a2981e248/sentencepiece-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7b67e724bead13f18db6e1d10b6bbdc454af574d70efbb36f27d90387be1ca3", size = 1181472, upload-time = "2024-02-19T17:05:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/fd/46/316c1ba6c52b97de76aff7b9da678f7afbb52136afb2987c474d95630e65/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fde4b08cfe237be4484c6c7c2e2c75fb862cfeab6bd5449ce4caeafd97b767a", size = 1259151, upload-time = "2024-02-19T17:05:42.594Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5a/3c48738a0835d76dd06c62b6ac48d39c923cde78dd0f587353bdcbb99851/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c378492056202d1c48a4979650981635fd97875a00eabb1f00c6a236b013b5e", size = 1355931, upload-time = "2024-02-19T17:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/a6/27/33019685023221ca8ed98e8ceb7ae5e166032686fa3662c68f1f1edf334e/sentencepiece-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1380ce6540a368de2ef6d7e6ba14ba8f3258df650d39ba7d833b79ee68a52040", size = 1301537, upload-time = "2024-02-19T17:05:46.713Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/55f97cef14293171fef5f96e96999919ab5b4d1ce95b53547ad653d7e3bf/sentencepiece-0.2.0-cp310-cp310-win32.whl", hash = "sha256:a1151d6a6dd4b43e552394aed0edfe9292820272f0194bd56c7c1660a0c06c3d", size = 936747, upload-time = "2024-02-19T17:05:48.705Z" }, + { url = "https://files.pythonhosted.org/packages/85/f4/4ef1a6e0e9dbd8a60780a91df8b7452ada14cfaa0e17b3b8dfa42cecae18/sentencepiece-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:d490142b0521ef22bc1085f061d922a2a6666175bb6b42e588ff95c0db6819b2", size = 991525, upload-time = "2024-02-19T17:05:55.145Z" }, ] [[package]] -name = "setuptools" -version = "75.8.0" +name = "sentry-sdk" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, + { url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" }, ] [[package]] -name = "shellingham" -version = "1.5.4" +name = "setuptools" +version = "82.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] -name = "six" -version = "1.12.0" +name = "shellingham" +version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/bf/4138e7bfb757de47d1f4b6994648ec67a51efe58fa907c1e11e350cddfca/six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73", size = 32725 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", size = 10586 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] -name = "sniffio" -version = "1.3.1" +name = "six" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" -version = "2.6" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, -] - -[[package]] -name = "speechrecognition" -version = "3.8.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/e1/7f5678cd94ec1234269d23756dbdaa4c8cfaed973412f88ae8adf7893a50/SpeechRecognition-3.8.1-py2.py3-none-any.whl", hash = "sha256:4d8f73a0c05ec70331c3bacaa89ecc06dfa8d9aba0899276664cda06ab597e8e", size = 32833456 }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sqlalchemy" -version = "2.0.37" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/20/93ea2518df4d7a14ebe9ace9ab8bb92aaf7df0072b9007644de74172b06c/sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb", size = 9626249 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/21/aaf0cd2e7ee56e464af7cba38a54f9c1203570181ec5d847711f33c9f520/SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e", size = 2102915 }, - { url = "https://files.pythonhosted.org/packages/fd/01/6615256759515f13bb7d7b49981326f1f4e80ff1bd92dccd53f99dab79ea/SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069", size = 2094095 }, - { url = "https://files.pythonhosted.org/packages/6a/f2/400252bda1bd67da7a35bb2ab84d10a8ad43975d42f15b207a9efb765446/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1", size = 3076482 }, - { url = "https://files.pythonhosted.org/packages/40/c6/e7e8e894c8f065f96ca202cdb00454d60d4962279b3eb5a81b8766dfa836/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84", size = 3084750 }, - { url = "https://files.pythonhosted.org/packages/d6/ee/1cdab04b7760e48273f2592037df156afae044e2e6589157673bd2a830c0/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f", size = 3040575 }, - { url = "https://files.pythonhosted.org/packages/4d/af/2dd456bfd8d4b9750792ceedd828bddf83860f2420545e5effbaf722dae5/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4", size = 3066113 }, - { url = "https://files.pythonhosted.org/packages/dd/d7/ad997559574f94d7bd895a8a63996afef518d07e9eaf5a2a9cbbcb877c16/SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72", size = 2075239 }, - { url = "https://files.pythonhosted.org/packages/d0/82/141fbed705a21af2d825068831da1d80d720945df60c2b97ddc5133b3714/SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636", size = 2099307 }, - { url = "https://files.pythonhosted.org/packages/3b/36/59cc97c365f2f79ac9f3f51446cae56dfd82c4f2dd98497e6be6de20fb91/SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1", size = 1894113 }, + { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] name = "sqlitedict" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/9a/7620d1e9dcb02839ed6d4b14064e609cdd7a8ae1e47289aa0456796dd9ca/sqlitedict-2.1.0.tar.gz", hash = "sha256:03d9cfb96d602996f1d4c2db2856f1224b96a9c431bdd16e78032a72940f9e8c", size = 21846 } +sdist = { url = "https://files.pythonhosted.org/packages/12/9a/7620d1e9dcb02839ed6d4b14064e609cdd7a8ae1e47289aa0456796dd9ca/sqlitedict-2.1.0.tar.gz", hash = "sha256:03d9cfb96d602996f1d4c2db2856f1224b96a9c431bdd16e78032a72940f9e8c", size = 21846, upload-time = "2022-12-03T13:39:13.102Z" } [[package]] name = "sqlmodel" @@ -3094,9 +3174,9 @@ dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392, upload-time = "2024-08-31T09:43:24.088Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276 }, + { url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276, upload-time = "2024-08-31T09:43:22.358Z" }, ] [[package]] @@ -3108,217 +3188,52 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "starlette" -version = "0.45.3" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, -] - -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, -] - -[[package]] -name = "tenacity" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, -] - -[[package]] -name = "tensorboard" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "google-auth" }, - { name = "google-auth-oauthlib" }, - { name = "grpcio" }, - { name = "markdown" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "tensorboard-data-server" }, - { name = "tensorboard-plugin-wit" }, - { name = "werkzeug" }, - { name = "wheel" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/49/a5ec29886ef823718c8ae54ed0b3ad7e19066b5bf21cec5038427e6a04c4/tensorboard-2.10.1-py3-none-any.whl", hash = "sha256:fb9222c1750e2fa35ef170d998a1e229f626eeced3004494a8849c88c15d8c1c", size = 5873392 }, -] - -[[package]] -name = "tensorboard-data-server" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/69/5747a957f95e2e1d252ca41476ae40ce79d70d38151d2e494feb7722860c/tensorboard_data_server-0.6.1-py3-none-any.whl", hash = "sha256:809fe9887682d35c1f7d1f54f0f40f98bb1f771b14265b453ca051e2ce58fca7", size = 2350 }, - { url = "https://files.pythonhosted.org/packages/3e/48/dd135dbb3cf16bfb923720163493cab70e7336db4b5f3103d49efa730404/tensorboard_data_server-0.6.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fa8cef9be4fcae2f2363c88176638baf2da19c5ec90addb49b1cde05c95c88ee", size = 3546350 }, - { url = "https://files.pythonhosted.org/packages/60/f9/802efd84988bffd9f644c03b6e66fde8e76c3aa33db4279ddd11c5d61f4b/tensorboard_data_server-0.6.1-py3-none-manylinux2010_x86_64.whl", hash = "sha256:d8237580755e58eff68d1f3abefb5b1e39ae5c8b127cc40920f9c4fb33f4b98a", size = 4910134 }, -] - -[[package]] -name = "tensorboard-plugin-wit" -version = "1.8.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/68/e8ecfac5dd594b676c23a7f07ea34c197d7d69b3313afdf8ac1b0a9905a2/tensorboard_plugin_wit-1.8.1-py3-none-any.whl", hash = "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe", size = 781327 }, -] - -[[package]] -name = "tensorboardx" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/e3/c68897ee471518b1f8d3e53c602aec6f2b09092aa774da88764262f03f56/tensorboardX-2.6.tar.gz", hash = "sha256:d4c036964dd2deb075a1909832b276daa383eab3f9db519ad90b99f5aea06b0c", size = 91196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/9f/d532d37f10ac7af136d4c2ba71e1fe7af0f3cc0cc076dfc05826171e9737/tensorboardX-2.6-py2.py3-none-any.whl", hash = "sha256:24a7cd076488de1e9d15ef25371b8ebf90c4f8f622af2477c611198f03f4a606", size = 114480 }, -] - -[[package]] -name = "tensorflow" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "astunparse" }, - { name = "flatbuffers" }, - { name = "gast" }, - { name = "google-pasta" }, - { name = "grpcio" }, - { name = "h5py" }, - { name = "keras" }, - { name = "keras-preprocessing" }, - { name = "libclang" }, - { name = "numpy" }, - { name = "opt-einsum" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "setuptools" }, - { name = "six" }, - { name = "tensorboard" }, - { name = "tensorflow-estimator" }, - { name = "tensorflow-io-gcs-filesystem" }, - { name = "termcolor" }, { name = "typing-extensions" }, - { name = "wrapt" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/84/92f4f4c017ef2071412745034a98108106347478c56475c65d275bd2a792/tensorflow-2.10.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:dc3587dfa714be711d2681d5e2fb59037b18e83e692f084db49bce31b6268d15", size = 241225350 }, - { url = "https://files.pythonhosted.org/packages/27/35/50f68ad5c082836045b2b068d095b0ed5bb6fdee4dfcb9af76058df4ed66/tensorflow-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3cab933757eb0c204dc4cf34d031939e33cae8f97a7aaef00a12678129b17f", size = 1937 }, - { url = "https://files.pythonhosted.org/packages/b2/c3/668c91cc7074eed672691f130562c0f02d89aebf01f6e14f1741f7fb900b/tensorflow-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f1d579b849afaea7b10f7693dc43b1d07321d279a016f01e2ddfe971d0d8af", size = 578143480 }, - { url = "https://files.pythonhosted.org/packages/ad/87/f484e0b86687c97d2dfb081e03e948b796561fc8608b409a9366e3b4a663/tensorflow-2.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6049664f9a0d14b0a4a7e6f058be87b2d8c27be826d7dd9a870ff03683fbc0b", size = 455948187 }, -] - -[[package]] -name = "tensorflow-estimator" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/05/9d7f3a6c783669cba36a6eb4555d0c73a516eee935dde6176dfb8512f94e/tensorflow_estimator-2.10.0-py2.py3-none-any.whl", hash = "sha256:f324ea17cd57f16e33bf188711d5077e6b2e5f5a12c328d6e01a07b23888edcd", size = 438698 }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] -name = "tensorflow-hub" -version = "0.16.1" +name = "sympy" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "protobuf" }, - { name = "tf-keras" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/50/00dba77925bf2a0a1e45d7bcf8a69a1d2534fb4bb277d9010bd148d2235e/tensorflow_hub-0.16.1-py2.py3-none-any.whl", hash = "sha256:e10c184b3d08daeafada11ffea2dd46781725b6bef01fad1f74d6634ad05311f", size = 30771 }, + { name = "mpmath" }, ] - -[[package]] -name = "tensorflow-io-gcs-filesystem" -version = "0.37.1" -source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/a3/12d7e7326a707919b321e2d6e4c88eb61596457940fd2b8ff3e9b7fac8a7/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:249c12b830165841411ba71e08215d0e94277a49c551e6dd5d72aab54fe5491b", size = 2470224 }, - { url = "https://files.pythonhosted.org/packages/1c/55/3849a188cc15e58fefde20e9524d124a629a67a06b4dc0f6c881cb3c6e39/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:257aab23470a0796978efc9c2bcf8b0bc80f22e6298612a4c0a50d3f4e88060c", size = 3479613 }, - { url = "https://files.pythonhosted.org/packages/e2/19/9095c69e22c879cb3896321e676c69273a549a3148c4f62aa4bc5ebdb20f/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8febbfcc67c61e542a5ac1a98c7c20a91a5e1afc2e14b1ef0cb7c28bc3b6aa70", size = 4842078 }, - { url = "https://files.pythonhosted.org/packages/f3/48/47b7d25572961a48b1de3729b7a11e835b888e41e0203cca82df95d23b91/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9679b36e3a80921876f31685ab6f7270f3411a4cc51bc2847e80d0e4b5291e27", size = 5085736 }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] -name = "tensorflow-macos" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "astunparse" }, - { name = "flatbuffers" }, - { name = "gast" }, - { name = "google-pasta" }, - { name = "grpcio" }, - { name = "h5py" }, - { name = "keras" }, - { name = "keras-preprocessing" }, - { name = "libclang" }, - { name = "numpy" }, - { name = "opt-einsum" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "setuptools" }, - { name = "six" }, - { name = "tensorboard" }, - { name = "tensorflow-estimator" }, - { name = "termcolor" }, - { name = "typing-extensions" }, - { name = "wrapt" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/18/d7d06e1c8bf4cafd091d62854b8e6a9e8db176c1b1a5171a586ec17d4b54/tensorflow_macos-2.10.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:dfd1dd478b3ae01e8d578c38083bef68bc838ceaa05a813b6788fe9e6ec19140", size = 211520008 }, - { url = "https://files.pythonhosted.org/packages/6f/a1/3bd220bacb1dcd4eea526c4b5376eddfb0fbb156c9814684dc9be24b7bc8/tensorflow_macos-2.10.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:aa074b5442d3411e5416c5112531d8b78a8c469ca92fa41c0e0cf14428608bf3", size = 245993233 }, -] - -[[package]] -name = "tensorflow-text" -version = "2.10.0" +name = "tabulate" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tensorflow", marker = "platform_machine != 'arm64' or platform_system != 'Darwin'" }, - { name = "tensorflow-hub" }, - { name = "tensorflow-macos", marker = "platform_machine == 'arm64' and platform_system == 'Darwin'" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/64/489c459f46f678ec4822b815ba9dfb1182c351e2208e98b6e83543254b55/tensorflow_text-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84b76847189d0d7bc8d4a130f0617e13bc9b810cf1feeb9c49da2906a2f39785", size = 5621257 }, - { url = "https://files.pythonhosted.org/packages/37/a5/bf11fb427226283b2abefcad355030effdb1a0c45978b80c63a38556bdd2/tensorflow_text-2.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:554f05956922afe0269dfb1e2fd73f5e049fa5746c47fd34d92ff7e447d746d4", size = 5889598 }, - { url = "https://files.pythonhosted.org/packages/88/a2/8d2ee50c8e5e355bf23975a0b7fa49ddd9c9b0ecb48b8c77d78ff4b3bcc0/tensorflow_text-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:269a3d7c3bd34e069e5d4ccaece902fc46aad65220301ee3bdb70d4ab031a60d", size = 5004273 }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] [[package]] -name = "termcolor" -version = "2.5.0" +name = "tenacity" +version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] @@ -3330,48 +3245,18 @@ dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154 }, -] - -[[package]] -name = "textract" -version = "1.6.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argcomplete" }, - { name = "beautifulsoup4" }, - { name = "chardet" }, - { name = "docx2txt" }, - { name = "extract-msg" }, - { name = "pdfminer-six" }, - { name = "python-pptx" }, - { name = "six" }, - { name = "speechrecognition" }, - { name = "xlrd" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/9f/dd29fcec368f007d44e51f0273489d5172a6d32ed9c796df5054fbb31c9f/textract-1.6.5.tar.gz", hash = "sha256:68f0f09056885821e6c43d8538987518daa94057c306679f2857cc5ee66ad850", size = 17871 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/3e/ac16b6bf28edf78296aea7d0cb416b49ed30282ac8c711662541015ee6f3/textract-1.6.5-py3-none-any.whl", hash = "sha256:0accd78ec42864e3e3827f9ef798ced9aac4727b664303b724a198fed73fa438", size = 23140 }, -] - -[[package]] -name = "tf-keras" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/0c/36054828137226dc3a559b525640f296a99ee8eb38beca32b36d29bb303b/tf_keras-2.15.0.tar.gz", hash = "sha256:d7559c2ba40667679fcb2105d3e4b68bbc07ecafbf1037462ce7b3974c3c6798", size = 1250420 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/26/ca8a6cca61f2a44f1e7ee71ebdb9c8dfbc4371f418db811cdca4641f6daa/tf_keras-2.15.0-py3-none-any.whl", hash = "sha256:48607ee60a4d1fa7c09d6a44293a803faf3136e7a43f92df089ac094117547d2", size = 1714973 }, + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, ] [[package]] name = "threadpoolctl" -version = "3.5.0" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] [[package]] @@ -3381,148 +3266,142 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 } +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 }, + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] [[package]] name = "tokenizers" -version = "0.21.0" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/41/c2be10975ca37f6ec40d7abd7e98a5213bb04f284b869c1a24e6504fd94d/tokenizers-0.21.0.tar.gz", hash = "sha256:ee0894bf311b75b0c03079f33859ae4b2334d675d4e93f5a4132e1eae2834fe4", size = 343021 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/5c/8b09607b37e996dc47e70d6a7b6f4bdd4e4d5ab22fe49d7374565c7fefaf/tokenizers-0.21.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3c4c93eae637e7d2aaae3d376f06085164e1660f89304c0ab2b1d08a406636b2", size = 2647461 }, - { url = "https://files.pythonhosted.org/packages/22/7a/88e58bb297c22633ed1c9d16029316e5b5ac5ee44012164c2edede599a5e/tokenizers-0.21.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f53ea537c925422a2e0e92a24cce96f6bc5046bbef24a1652a5edc8ba975f62e", size = 2563639 }, - { url = "https://files.pythonhosted.org/packages/f7/14/83429177c19364df27d22bc096d4c2e431e0ba43e56c525434f1f9b0fd00/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b177fb54c4702ef611de0c069d9169f0004233890e0c4c5bd5508ae05abf193", size = 2903304 }, - { url = "https://files.pythonhosted.org/packages/7e/db/3433eab42347e0dc5452d8fcc8da03f638c9accffefe5a7c78146666964a/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b43779a269f4629bebb114e19c3fca0223296ae9fea8bb9a7a6c6fb0657ff8e", size = 2804378 }, - { url = "https://files.pythonhosted.org/packages/57/8b/7da5e6f89736c2ade02816b4733983fca1c226b0c42980b1ae9dc8fcf5cc/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aeb255802be90acfd363626753fda0064a8df06031012fe7d52fd9a905eb00e", size = 3095488 }, - { url = "https://files.pythonhosted.org/packages/4d/f6/5ed6711093dc2c04a4e03f6461798b12669bc5a17c8be7cce1240e0b5ce8/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b09dbeb7a8d73ee204a70f94fc06ea0f17dcf0844f16102b9f414f0b7463ba", size = 3121410 }, - { url = "https://files.pythonhosted.org/packages/81/42/07600892d48950c5e80505b81411044a2d969368cdc0d929b1c847bf6697/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:400832c0904f77ce87c40f1a8a27493071282f785724ae62144324f171377273", size = 3388821 }, - { url = "https://files.pythonhosted.org/packages/22/06/69d7ce374747edaf1695a4f61b83570d91cc8bbfc51ccfecf76f56ab4aac/tokenizers-0.21.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84ca973b3a96894d1707e189c14a774b701596d579ffc7e69debfc036a61a04", size = 3008868 }, - { url = "https://files.pythonhosted.org/packages/c8/69/54a0aee4d576045b49a0eb8bffdc495634309c823bf886042e6f46b80058/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb7202d231b273c34ec67767378cd04c767e967fda12d4a9e36208a34e2f137e", size = 8975831 }, - { url = "https://files.pythonhosted.org/packages/f7/f3/b776061e4f3ebf2905ba1a25d90380aafd10c02d406437a8ba22d1724d76/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:089d56db6782a73a27fd8abf3ba21779f5b85d4a9f35e3b493c7bbcbbf0d539b", size = 8920746 }, - { url = "https://files.pythonhosted.org/packages/d8/ee/ce83d5ec8b6844ad4c3ecfe3333d58ecc1adc61f0878b323a15355bcab24/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c87ca3dc48b9b1222d984b6b7490355a6fdb411a2d810f6f05977258400ddb74", size = 9161814 }, - { url = "https://files.pythonhosted.org/packages/18/07/3e88e65c0ed28fa93aa0c4d264988428eef3df2764c3126dc83e243cb36f/tokenizers-0.21.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4145505a973116f91bc3ac45988a92e618a6f83eb458f49ea0790df94ee243ff", size = 9357138 }, - { url = "https://files.pythonhosted.org/packages/15/b0/dc4572ca61555fc482ebc933f26cb407c6aceb3dc19c301c68184f8cad03/tokenizers-0.21.0-cp39-abi3-win32.whl", hash = "sha256:eb1702c2f27d25d9dd5b389cc1f2f51813e99f8ca30d9e25348db6585a97e24a", size = 2202266 }, - { url = "https://files.pythonhosted.org/packages/44/69/d21eb253fa91622da25585d362a874fa4710be600f0ea9446d8d0217cec1/tokenizers-0.21.0-cp39-abi3-win_amd64.whl", hash = "sha256:87841da5a25a3a5f70c102de371db120f41873b854ba65e52bccd57df5a3780c", size = 2389192 }, +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "torch" -version = "1.12.1" +version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/74/7342c7f21449557a8263c925071a55081edd7e9b641404cfe31d6fb71d3b/torch-1.12.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:9c038662db894a23e49e385df13d47b2a777ffd56d9bcd5b832593fab0a7e286", size = 776338835 }, - { url = "https://files.pythonhosted.org/packages/36/b0/4857929aa28dfe26f7de909ebf002b60499edcd7566441a7433df865f9ba/torch-1.12.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4e1b9c14cf13fd2ab8d769529050629a0e68a6fc5cb8e84b4a3cc1dd8c4fe541", size = 55712361 }, - { url = "https://files.pythonhosted.org/packages/b9/25/fc2111599a038aa6c1c618a7dc9246aabc95f899008949ade31213255a0c/torch-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:e9c8f4a311ac29fc7e8e955cfb7733deb5dbe1bdaabf5d4af2765695824b7e0d", size = 162235663 }, - { url = "https://files.pythonhosted.org/packages/37/72/ef80d39a371a9b4a8aadfb22b141972cc67a7075dae69a9d5b8116505ec0/torch-1.12.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:976c3f997cea38ee91a0dd3c3a42322785414748d1761ef926b789dfa97c6134", size = 133811424 }, - { url = "https://files.pythonhosted.org/packages/2f/17/8b557dde1cdb5fbe82f90a3f192046c8e508f106456e12f17c87543e6a42/torch-1.12.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:68104e4715a55c4bb29a85c6a8d57d820e0757da363be1ba680fa8cc5be17b52", size = 49099890 }, -] - -[[package]] -name = "torchmetrics" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "torch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/ed/9f76e2d65d2e6d67a0ba097f34f4b618fb7466d731476d3d3440dfe2cb8e/torchmetrics-0.11.4.tar.gz", hash = "sha256:1fe45a14b44dd65d90199017dd5a4b5a128d56a8a311da7916c402c18c671494", size = 307144 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/47/6e9f9b41c48750a45ad07cc6d43a2979bfc09e6989656aece97cc59cbef1/torchmetrics-0.11.4-py3-none-any.whl", hash = "sha256:45f892f3534e91f3ad9e2488d1b05a93b7cb76b7d037969435a41a1f24750d9a", size = 519162 }, -] - -[[package]] -name = "torchtext" -version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "requests" }, - { name = "torch" }, - { name = "tqdm" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/6f/673e3533a296f135fecfccdf2519ecfba7e16971dca5c3bc7a8cc9cfd064/torchtext-0.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aa59df8f9542674311fc23aca147a34256c936c25840c4b6ee3fee47d511ad17", size = 1775853 }, - { url = "https://files.pythonhosted.org/packages/2f/bd/5f27d604d3dc6421b3c0bd24c28f73eecb091fce9dbdcbe7af314f252cee/torchtext-0.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:492a22727181edf5a33fa18587f3430ffbb366c2ea835e0f4f8a08be8d9e859c", size = 1966034 }, - { url = "https://files.pythonhosted.org/packages/48/4e/56352383c30b75becd5faaff8d404eb86f3a2282d72ae52aaad705bf22bf/torchtext-0.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f56359165eb00ea2ff998b67727f87e7fa32665c1a46610c9b5a2d5d581095e5", size = 1910382 }, - { url = "https://files.pythonhosted.org/packages/3f/fd/e3eef8b5d691cdeb42506fc9fec2a64ab1549039cdf9e7545a171e4a694e/torchtext-0.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:b5bf4f9da5326b6e74318b4a5035abfaf166b3abd822565be685f44947adb8d3", size = 1832850 }, - { url = "https://files.pythonhosted.org/packages/b8/9d/7f9b786637d664579b66ae7c6742d662a91b8e6a9f55a34e22c20883d801/torchtext-0.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5f8c1b426bda2f22e5bfd118c779c12f8612ed1077e5e3a491242bb4ab7ac4d3", size = 2239348 }, + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, ] [[package]] name = "tornado" -version = "6.4.2" +version = "6.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, - { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, - { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, - { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, - { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, - { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, - { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, - { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, - { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, - { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "transformer-smaller-training-vocab" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, { name = "torch" }, { name = "transformers", extra = ["sentencepiece", "torch"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/28/5214ff2a93d9cccc55d0679cb8cbc3b9d52f1f07860c92841b16cfeb5026/transformer_smaller_training_vocab-0.4.0.tar.gz", hash = "sha256:d7360ac084786f66f99ef16d621f34acbb0dce6d9a624525d1f7dc8b6c3a49f7", size = 12141 } +sdist = { url = "https://files.pythonhosted.org/packages/45/6f/85142d145fd2c453053e7dcd5500c31cd26ce51f9010cf0fe698001853f2/transformer_smaller_training_vocab-0.4.2.tar.gz", hash = "sha256:aed4390331e63d9a0998d76f277b7d44ad5d38b8427ad7792b1e28c807801ed8", size = 11754, upload-time = "2025-06-15T13:34:38.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/c8/6a02e88256dc48faf3eae5732a94035f4ea0edd91d8224333693111921ba/transformer_smaller_training_vocab-0.4.0-py3-none-any.whl", hash = "sha256:01cb3d8f4818121172e1591a06c3149bf49bc18d6f6f269eb42d2c4ed155cfcc", size = 14120 }, + { url = "https://files.pythonhosted.org/packages/6c/88/94fa030995bc9a54e911172fd6ca26a81c2a5ddafd896ff62ad9cc99088b/transformer_smaller_training_vocab-0.4.2-py3-none-any.whl", hash = "sha256:49fcdb3134ede5faca41d3bed2588bd21a4098b64f261e7b198f163d394c3ef0", size = 14073, upload-time = "2025-06-15T13:34:37.468Z" }, ] [[package]] name = "transformers" -version = "4.47.1" +version = "4.57.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -3536,9 +3415,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/1a/936aeb4f88112f670b604f5748034568dbc2b9bbb457a8d4518b1a15510a/transformers-4.47.1.tar.gz", hash = "sha256:6c29c05a5f595e278481166539202bf8641281536df1c42357ee58a45d0a564a", size = 8707421 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3a/8bdab26e09c5a242182b7ba9152e216d5ab4ae2d78c4298eb4872549cd35/transformers-4.47.1-py3-none-any.whl", hash = "sha256:d2f5d19bb6283cd66c893ec7e6d931d6370bbf1cc93633326ff1f41a40046c9c", size = 10133598 }, + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] [package.optional-dependencies] @@ -3551,99 +3430,98 @@ torch = [ { name = "torch" }, ] +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, +] + [[package]] name = "typer" -version = "0.15.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] -name = "tzdata" -version = "2025.1" +name = "typing-inspection" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] -name = "tzlocal" -version = "5.2" +name = "tzdata" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859 }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "unidecode" version = "1.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/89/19151076a006b9ac0dd37b1354e031f5297891ee507eb624755e58e10d3e/Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4", size = 192701 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/89/19151076a006b9ac0dd37b1354e031f5297891ee507eb624755e58e10d3e/Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4", size = 192701, upload-time = "2024-01-11T11:58:35.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39", size = 235494 }, + { url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39", size = 235494, upload-time = "2024-01-11T11:58:33.012Z" }, ] [[package]] name = "uri-template" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678 } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140 }, + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, ] [[package]] name = "urllib3" -version = "2.3.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [package.optional-dependencies] @@ -3659,261 +3537,217 @@ standard = [ [[package]] name = "uvloop" -version = "0.21.0" +version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, - { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, - { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, - { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, - { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, - { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, ] [[package]] name = "virtualenv" -version = "20.29.1" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] name = "watchfiles" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 }, - { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 }, - { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 }, - { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 }, - { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 }, - { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 }, - { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 }, - { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 }, - { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 }, - { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 }, - { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 }, - { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 }, - { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 }, - { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 }, - { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 }, - { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 }, + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, ] [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] [[package]] name = "webcolors" -version = "24.11.1" +version = "25.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, ] [[package]] name = "webencodings" version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] [[package]] name = "websocket-client" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] [[package]] name = "websockets" -version = "14.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/fa/76607eb7dcec27b2d18d63f60a32e60e2b8629780f343bb83a4dbb9f4350/websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885", size = 163089 }, - { url = "https://files.pythonhosted.org/packages/9e/00/ad2246b5030575b79e7af0721810fdaecaf94c4b2625842ef7a756fa06dd/websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397", size = 160741 }, - { url = "https://files.pythonhosted.org/packages/72/f7/60f10924d333a28a1ff3fcdec85acf226281331bdabe9ad74947e1b7fc0a/websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610", size = 160996 }, - { url = "https://files.pythonhosted.org/packages/63/7c/c655789cf78648c01ac6ecbe2d6c18f91b75bdc263ffee4d08ce628d12f0/websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3", size = 169974 }, - { url = "https://files.pythonhosted.org/packages/fb/5b/013ed8b4611857ac92ac631079c08d9715b388bd1d88ec62e245f87a39df/websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980", size = 168985 }, - { url = "https://files.pythonhosted.org/packages/cd/33/aa3e32fd0df213a5a442310754fe3f89dd87a0b8e5b4e11e0991dd3bcc50/websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8", size = 169297 }, - { url = "https://files.pythonhosted.org/packages/93/17/dae0174883d6399f57853ac44abf5f228eaba86d98d160f390ffabc19b6e/websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7", size = 169677 }, - { url = "https://files.pythonhosted.org/packages/42/e2/0375af7ac00169b98647c804651c515054b34977b6c1354f1458e4116c1e/websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f", size = 169089 }, - { url = "https://files.pythonhosted.org/packages/73/8d/80f71d2a351a44b602859af65261d3dde3a0ce4e76cf9383738a949e0cc3/websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d", size = 169026 }, - { url = "https://files.pythonhosted.org/packages/48/97/173b1fa6052223e52bb4054a141433ad74931d94c575e04b654200b98ca4/websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d", size = 163967 }, - { url = "https://files.pythonhosted.org/packages/c0/5b/2fcf60f38252a4562b28b66077e0d2b48f91fef645d5f78874cd1dec807b/websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2", size = 164413 }, - { url = "https://files.pythonhosted.org/packages/10/3d/91d3d2bb1325cd83e8e2c02d0262c7d4426dc8fa0831ef1aa4d6bf2041af/websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29", size = 160773 }, - { url = "https://files.pythonhosted.org/packages/33/7c/cdedadfef7381939577858b1b5718a4ab073adbb584e429dd9d9dc9bfe16/websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c", size = 161007 }, - { url = "https://files.pythonhosted.org/packages/ca/35/7a20a3c450b27c04e50fbbfc3dfb161ed8e827b2a26ae31c4b59b018b8c6/websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2", size = 162264 }, - { url = "https://files.pythonhosted.org/packages/e8/9c/e3f9600564b0c813f2448375cf28b47dc42c514344faed3a05d71fb527f9/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c", size = 161873 }, - { url = "https://files.pythonhosted.org/packages/3f/37/260f189b16b2b8290d6ae80c9f96d8b34692cf1bb3475df54c38d3deb57d/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a", size = 161818 }, - { url = "https://files.pythonhosted.org/packages/ff/1e/e47dedac8bf7140e59aa6a679e850c4df9610ae844d71b6015263ddea37b/websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3", size = 164465 }, - { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, -] - -[[package]] -name = "werkzeug" -version = "3.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, -] - -[[package]] -name = "wheel" -version = "0.45.1" +version = "16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "widgetsnbextension" -version = "4.0.13" +version = "4.0.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/fc/238c424fd7f4ebb25f8b1da9a934a3ad7c848286732ae04263661eb0fc03/widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6", size = 1164730 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/02/88b65cc394961a60c43c70517066b6b679738caf78506a5da7b88ffcb643/widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71", size = 2335872 }, + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, ] [[package]] name = "wikipedia-api" -version = "0.8.1" +version = "0.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/aa/2e35be124dfc7e581480705f912040172f6570cc12e68a245ba9258c32ef/wikipedia_api-0.8.1.tar.gz", hash = "sha256:b31e93b3f5407c1a1ba413ed7326a05379a3c270df6cf6a211aca67a14c5658b", size = 19934 } - -[[package]] -name = "wrapt" -version = "1.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, -] - -[[package]] -name = "xlrd" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/05/ec9d4fcbbb74bbf4da9f622b3b61aec541e4eccf31d3c60c5422ec027ce2/xlrd-1.2.0.tar.gz", hash = "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", size = 554079 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/59/a6f5790043cff046658d44bc5b594a7d0cc0d9a1a6911a0df6e7aba2179c/wikipedia_api-0.10.2.tar.gz", hash = "sha256:93fc84d2d88b043c626a03bc013a741c206ab60c0517bfce51fa60a0edc5087d", size = 29841, upload-time = "2026-03-06T22:01:16.615Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/16/63576a1a001752e34bf8ea62e367997530dc553b689356b9879339cf45a4/xlrd-1.2.0-py2.py3-none-any.whl", hash = "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde", size = 103251 }, + { url = "https://files.pythonhosted.org/packages/34/41/cf1ceb3071b58175d6960d5f45bfd5c007fc8ab5bec8ca189efd02bb05e4/wikipedia_api-0.10.2-py3-none-any.whl", hash = "sha256:0aa6d09e46909d396d81af97de5dee06004c1f823725b4f66c352b1096d48163", size = 22690, upload-time = "2026-03-06T22:01:15.024Z" }, ] [[package]] -name = "xlsxwriter" -version = "3.2.1" +name = "wrapt" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/4f/108b0bada5cfcc47c24ea6181f4c563fbafef50bcc0054089c256b2ae578/XlsxWriter-3.2.1.tar.gz", hash = "sha256:97618759cb264fb6a93397f660cca156ffa9561743b1823dafb60dc4474e1902", size = 202868 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/30/040af902cb8a9909d320779d8467aa7590bb91477767fd2b7551f4d91bb5/XlsxWriter-3.2.1-py3-none-any.whl", hash = "sha256:7e8f7c60b7a1660ef791d46ab5de78469cb978b991ca841af61f5832d2f9f4fe", size = 162778 }, + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] [[package]] name = "xmltodict" version = "0.14.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942 } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981 }, + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, ] [[package]] name = "xxhash" -version = "3.5.0" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212", size = 31970 }, - { url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520", size = 30801 }, - { url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680", size = 220927 }, - { url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da", size = 200360 }, - { url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23", size = 428528 }, - { url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196", size = 194149 }, - { url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c", size = 207703 }, - { url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482", size = 216255 }, - { url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296", size = 202744 }, - { url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415", size = 210115 }, - { url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198", size = 414247 }, - { url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442", size = 191419 }, - { url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da", size = 30114 }, - { url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9", size = 30003 }, - { url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6", size = 26773 }, - { url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c", size = 29732 }, - { url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986", size = 36214 }, - { url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6", size = 32020 }, - { url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b", size = 40515 }, - { url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064 }, +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, ] [[package]] name = "yarl" -version = "1.18.3" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, - { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, - { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, - { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, - { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, - { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, - { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, - { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, - { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, - { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, - { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, - { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, - { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, - { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, - { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ]