From ab800acf2e0551c2c3b44f6bf802d66c982c0a31 Mon Sep 17 00:00:00 2001 From: Florian Maas Date: Sun, 23 Jun 2024 13:27:35 +0200 Subject: [PATCH] Use simple in-memory vector database instead of Pinecone (#4) * Add simple in-memory vector database * switch to blue color style * better UI --------- Co-authored-by: florian --- .DS_Store | Bin 6148 -> 0 bytes .env.template | 5 +- .github/workflows/main.yml | 2 +- .github/workflows/publish.yml | 1 + README.md | 47 +- SETUP.md | 4 +- frontend/Dockerfile | 6 +- frontend/app/components/GitHubButton.tsx | 2 +- frontend/app/components/GoogleAnalytics.tsx | 29 + frontend/app/components/Header.tsx | 37 ++ frontend/app/components/InfoBox.tsx | 20 +- .../app/components/SearchResultsTable.tsx | 10 +- frontend/app/components/SupportButton.tsx | 2 +- frontend/app/globals.css | 4 +- frontend/app/layout.tsx | 6 +- frontend/app/page.tsx | 137 ++-- frontend/tailwind.config.ts | 26 + poetry.lock | 594 ++++++++---------- pypi_bigquery.sql | 4 +- pypi_scout/api/data_loader.py | 65 ++ pypi_scout/api/main.py | 22 +- pypi_scout/api/utils.py | 38 -- pypi_scout/config.py | 33 +- pypi_scout/data/description_cleaner.py | 2 +- pypi_scout/data/raw_data_reader.py | 8 +- pypi_scout/embeddings/embeddings_creator.py | 60 ++ .../embeddings/simple_vector_database.py | 55 ++ .../scripts/create_vector_embeddings.py | 45 ++ pypi_scout/scripts/setup.py | 22 +- pypi_scout/scripts/setup_pinecone.py | 43 -- .../scripts/upload_processed_datasets.py | 56 +- pypi_scout/scripts/upsert_data.py | 43 -- pypi_scout/utils/blob_io.py | 36 +- pypi_scout/vector_database/__init__.py | 5 - pypi_scout/vector_database/interface.py | 80 --- pyproject.toml | 5 +- requirements-cpu.txt | 2 +- static/architecture.png | Bin 58699 -> 0 bytes tests/test_vdb.py | 64 +- 39 files changed, 795 insertions(+), 825 deletions(-) delete mode 100644 .DS_Store create mode 100644 frontend/app/components/GoogleAnalytics.tsx create mode 100644 frontend/app/components/Header.tsx create mode 100644 pypi_scout/api/data_loader.py delete mode 100644 pypi_scout/api/utils.py create mode 100644 pypi_scout/embeddings/embeddings_creator.py create mode 100644 pypi_scout/embeddings/simple_vector_database.py create mode 100644 pypi_scout/scripts/create_vector_embeddings.py delete mode 100644 pypi_scout/scripts/setup_pinecone.py delete mode 100644 pypi_scout/scripts/upsert_data.py delete mode 100644 pypi_scout/vector_database/__init__.py delete mode 100644 pypi_scout/vector_database/interface.py delete mode 100644 static/architecture.png diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 692e3682b622e099b02943a17317dec49f01965a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}N6?5Kii9g+1#F`2?Ok z`v5+PGfAo~RgY3+1}0xJKS}o6CD~z&@oG0LGge@X2~fmb9-40i{iqX?vz9qPj&lUP zPAiC20ORM0XfjMA1NiP77BZiu%pZM!K`V|rX{q$ab2HBD+`O0<3*ySVQ#~*B({88g zw=ZyXqEs9V`+jg1HG1{J@}WvnKS~;Hoe+f$2)R6ul2G-ks+)uzo$DEg$cucvu-fle zN|mDADQ^#ovcI=gD$2_C?qHA?E9;y4$MyTTnW#rYB8Try%c8|0JVEF4dE(o$ccYpu zbe^BCuZ{Yes#de#s>t4*jn}1@gK>ZOtG{(OGIx3nsbA|pcK@1QBQ^NqB4-yp7wU`;yM+Y>z1ORlvtOfd5OJI()=vvGSf(L|~ zR6vu;ZHd86I@qO+b1h~DO*-SY_~3SBZYvb7SBLqf4rkmoNG&ly42&}{V}=F1|BrsI z|HqSPL<|rE|B3S8conaLY5}{%4xno> SGYA$C`Vr7HP(uv-DFYw=vt|1L diff --git a/.env.template b/.env.template index c7b1fc6..9b41733 100644 --- a/.env.template +++ b/.env.template @@ -1 +1,4 @@ -PINECONE_TOKEN=your-api-token +STORAGE_BACKEND=BLOB +STORAGE_BACKEND_BLOB_ACCOUNT_NAME= +STORAGE_BACKEND_BLOB_CONTAINER_NAME= +STORAGE_BACKEND_BLOB_KEY= diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fde63c2..1c880c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] fail-fast: false steps: - name: Check out diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 737c04c..cddcca7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,3 +55,4 @@ jobs: tags: pypiscoutacr.azurecr.io/pypi-scout-frontend:latest build-args: | NEXT_PUBLIC_API_URL=https://pypiscout.com/api + NEXT_PUBLIC_GA_TRACKING_ID=${{ secrets.NEXT_PUBLIC_GA_TRACKING_ID }} diff --git a/README.md b/README.md index 3f088bc..a09814f 100644 --- a/README.md +++ b/README.md @@ -12,48 +12,38 @@ Inspired by [this blog post](https://koaning.io/posts/search-boxes/) about findi ## How does this work? -The project works by collecting project summaries and descriptions for all packages on PyPI with more than 50 weekly downloads. These are then converted into vector representations using [Sentence Transformers](https://www.sbert.net/). When the user enters a query, it is converted into a vector representation, and the most similar package descriptions are fetched from the vector database. Additional weight is given to the amount of weekly downloads before presenting the results to the user in a dashboard. +The project works by collecting project summaries and descriptions for all packages on PyPI with more than 100 weekly downloads. These are then converted into vector representations using [Sentence Transformers](https://www.sbert.net/). When the user enters a query, it is converted into a vector representation, and the most similar package descriptions are fetched from the vector database. Additional weight is given to the amount of weekly downloads before presenting the results to the user in a dashboard. -## Architecture +## Stack The project uses the following technologies: -1. **[Pinecone](https://www.pinecone.io/)** as vector database -2. **[FastAPI](https://fastapi.tiangolo.com/)** for the API backend -3. **[NextJS](https://nextjs.org/) and [TailwindCSS](https://tailwindcss.com/)** for the frontend -4. **[Sentence Transformers](https://www.sbert.net/)** for vector embeddings - -
- -![Architecture](./static/architecture.png) +1. **[FastAPI](https://fastapi.tiangolo.com/)** for the API backend +2. **[NextJS](https://nextjs.org/) and [TailwindCSS](https://tailwindcss.com/)** for the frontend +3. **[Sentence Transformers](https://www.sbert.net/)** for vector embeddings ## Getting Started -### Prerequisites - -1. **Set Up Pinecone** - - Since PyPI Scout uses [Pinecone](https://www.pinecone.io/) as the vector database, register for a free account on their website. Obtain your API key using the instructions [here](https://docs.pinecone.io/guides/get-started/quickstart). - -2. **Create a `.env` File** +### Build and Setup - Copy the `.env.template` to create a new `.env` file: +#### 1. (Optional) **Create a `.env` file** - ```sh - cp .env.template .env - ``` +By default, all data will be stored on your local machine. It is also possible to store the data for the API on Azure Blob storage, and +have the API read from there. To do so, create a `.env` file: - Then add your Pinecone API key from step 1 to this file. +```sh +cp .env.template .env +``` -### Build and Setup +and fill in the required fields. -#### 1. **Run the Setup Script** +#### 2. **Run the Setup Script** The setup script will: - Download and process the PyPI dataset and store the results in the `data` directory. -- Set up your Pinecone index. -- Create vector embeddings for the PyPI dataset and upsert them to the Pinecone index. +- Create vector embeddings for the PyPI dataset. +- If the `STORAGE_BACKEND` environment variable is set to `BLOB`: Upload the datasets to blob storage. There are three methods to run the setup script, dependent on if you have a NVIDIA GPU and [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed. Please run the setup script using the method that is applicable for you: @@ -62,9 +52,10 @@ There are three methods to run the setup script, dependent on if you have a NVID - [Option 3: Using Docker without NVIDIA GPU and NVIDIA Container Toolkit](SETUP.md#option-3-using-docker-without-nvidia-gpu-and-nvidia-container-toolkit) > [!NOTE] -> Although the dataset contains all packages on PyPI with more than 50 weekly downloads, by default only the top 25% of packages with the highest weekly downloads (those with more than approximately 650 downloads per week) are added to the vector database. To include packages with less weekly downloads in the database, you can increase the value of `FRAC_DATA_TO_INCLUDE` in `pypi_scout/config.py`. +> The dataset contains approximately 100.000 packages on PyPI with more than 100 weekly downloads. To speed up local development, +> you can lower the amount of packages that is processed locally by lowering the value of `FRAC_DATA_TO_INCLUDE` in `pypi_scout/config.py`. -#### 2. **Run the Application** +#### 3. **Run the Application** Start the application using Docker Compose: diff --git a/SETUP.md b/SETUP.md index 56a1ba6..1a0d3f9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -3,8 +3,8 @@ The setup script will: - Download and process the PyPI dataset and store the results in the `data` directory. -- Set up your Pinecone index. -- Create vector embeddings for the PyPI dataset and upsert them to the Pinecone index. +- Create vector embeddings for the PyPI dataset. +- If the `STORAGE_BACKEND` environment variable is set to `BLOB`: Upload the datasets to blob storage. There are three ways to run the setup script: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index ac7d507..0f346a5 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,11 +13,11 @@ RUN npm install # Copy the rest of the application code to the container COPY . . -# Build argument to accept the API URL during build time +# Add build arguments to environment ARG NEXT_PUBLIC_API_URL - -# Set environment variable within the container +ARG NEXT_PUBLIC_GA_TRACKING_ID ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +ENV NEXT_PUBLIC_GA_TRACKING_ID=${NEXT_PUBLIC_GA_TRACKING_ID} # Build the Next.js application RUN npm run build diff --git a/frontend/app/components/GitHubButton.tsx b/frontend/app/components/GitHubButton.tsx index 8104cc5..1047f4b 100644 --- a/frontend/app/components/GitHubButton.tsx +++ b/frontend/app/components/GitHubButton.tsx @@ -6,7 +6,7 @@ const GitHubButton: React.FC = () => { href="https://github.com/fpgmaas/pypi-scout" target="_blank" rel="noopener noreferrer" - className="flex items-center p-2 border border-gray-700 rounded bg-gray-900 text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-700" + className="flex items-center p-2 border border-sky-700 rounded bg-sky-900 text-white hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-700" > { + useEffect(() => { + const trackingId = process.env.NEXT_PUBLIC_GA_TRACKING_ID; + if (trackingId) { + const script1 = document.createElement("script"); + script1.async = true; + script1.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`; + document.head.appendChild(script1); + + const script2 = document.createElement("script"); + script2.innerHTML = ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '${trackingId}'); + `; + document.head.appendChild(script2); + } + }, []); + + return null; +}; + +export default GoogleAnalytics; diff --git a/frontend/app/components/Header.tsx b/frontend/app/components/Header.tsx new file mode 100644 index 0000000..99f29da --- /dev/null +++ b/frontend/app/components/Header.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import GitHubButton from "./GitHubButton"; +import SupportButton from "./SupportButton"; +import { FaBars, FaTimes } from "react-icons/fa"; + +const Header: React.FC = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + return ( +
+
+ + +
+
+ +
+ {isMenuOpen && ( +
+ + +
+ )} + + ); +}; + +export default Header; diff --git a/frontend/app/components/InfoBox.tsx b/frontend/app/components/InfoBox.tsx index c13857a..88c5cd4 100644 --- a/frontend/app/components/InfoBox.tsx +++ b/frontend/app/components/InfoBox.tsx @@ -8,21 +8,23 @@ const InfoBox: React.FC = ({ infoBoxVisible }) => { if (!infoBoxVisible) return null; return ( -
-

How does this work?

-

+

+

+ How does this work? +

+

This application allows you to search for Python packages on PyPI using natural language queries. For example, a query could be "a package that creates plots and beautiful visualizations".


-

+

Once you click search, your query will be matched against the summary - and the first part of the description of the ~30.000 most popular - packages on PyPI, which are all packages with at least ~600 downloads - per week. The results are then scored based on their similarity to the - query and their number of weekly downloads, and the best results are - displayed in the table below. + and the first part of the description of the ~100.000 most popular + packages on PyPI, which includes all packages with at least ~100 + downloads per week. The results are then scored based on their + similarity to the query and their number of weekly downloads, and the + best results are displayed in the table below.

); diff --git a/frontend/app/components/SearchResultsTable.tsx b/frontend/app/components/SearchResultsTable.tsx index befefa5..4f64d90 100644 --- a/frontend/app/components/SearchResultsTable.tsx +++ b/frontend/app/components/SearchResultsTable.tsx @@ -33,8 +33,8 @@ const SearchResultsTable: React.FC = ({ return (
- - +
+ - + {results.map((result, index) => ( - + @@ -92,7 +92,7 @@ const SearchResultsTable: React.FC = ({ href={`https://pypi.org/project/${result.name}/`} target="_blank" rel="noopener noreferrer" - className="text-blue-400 hover:underline flex items-center" + className="text-sky-500 hover:underline flex items-center hover:text-orange-800" > PyPI diff --git a/frontend/app/components/SupportButton.tsx b/frontend/app/components/SupportButton.tsx index a55a2fb..242ab1b 100644 --- a/frontend/app/components/SupportButton.tsx +++ b/frontend/app/components/SupportButton.tsx @@ -6,7 +6,7 @@ const SupportButton: React.FC = () => { href="https://ko-fi.com/fpgmaas" target="_blank" rel="noopener noreferrer" - className="flex items-center p-2 border border-gray-700 rounded bg-gray-900 text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-700" + className="flex items-center p-2 border border-sky-700 rounded bg-sky-900 text-white hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-700" > ) { return ( - {children} + + + {children} + ); } diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index b539365..6a26d08 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,8 +5,7 @@ import { handleSearch, sortResults } from "./utils/search"; import SearchResultsTable from "./components/SearchResultsTable"; import InfoBox from "./components/InfoBox"; import { ClipLoader } from "react-spinners"; -import GitHubButton from "./components/GitHubButton"; -import SupportButton from "./components/SupportButton"; +import Header from "./components/Header"; interface Match { name: string; @@ -33,80 +32,76 @@ export default function Home() { }; return ( -
-
-
- - +
+
+
+
+ + pypi-scout logo + +

+ Find packages on PyPI with natural language queries +

-
-
- - pypi-scout logo - -

- Enter your query to search for Python packages -

-
- -
- - - {loading && ( - - )} - {error &&

{error}

} -
+
+ + + {loading && ( + + )} + {error &&

{error}

} +
-
- -
+
+ +
- + - {results.length > 0 && ( -
-
- + {results.length > 0 && ( +
+
+ +
-
- )} -
+ )} + + ); } diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 7e4bd91..8576951 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -8,6 +8,31 @@ const config: Config = { ], theme: { extend: { + colors: { + sky: { + 50: "#d9f0ff", // Darkened from #f0f9ff + 100: "#c3e4fe", // Darkened from #e0f2fe + 200: "#a3d4fd", // Darkened from #bae6fd + 300: "#5cbdfc", // Darkened from #7dd3fc + 400: "#2aa3f8", // Darkened from #38bdf8 + 500: "#0b8edc", // Darkened from #0ea5e9 + 600: "#026baa", // Darkened from #0284c7 + 700: "#015a89", // Darkened from #0369a1 + 800: "#054b6e", // Darkened from #075985 + 900: "#083857", // Darkened from #0c4a6e + 950: "#062338", // Darkened from #082f49 + }, + orange: { + 100: "#f8d5c7", + 200: "#f1ac9a", + 300: "#ea836d", + 400: "#e35a40", + 500: "#d77a61", + 600: "#c45b3f", + 700: "#b23a1b", + 800: "#D18829", // Orange from logo + }, + }, backgroundImage: { "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", "gradient-conic": @@ -17,4 +42,5 @@ const config: Config = { }, plugins: [], }; + export default config; diff --git a/poetry.lock b/poetry.lock index b00709e..4fc1922 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,9 +11,6 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "anyio" version = "4.4.0" @@ -141,21 +138,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] -[[package]] -name = "astunparse" -version = "1.6.3" -description = "An AST unparser for Python" -optional = false -python-versions = "*" -files = [ - {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, - {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, -] - -[package.dependencies] -six = ">=1.6.1,<2.0" -wheel = ">=0.23.0,<1.0" - [[package]] name = "async-lru" version = "2.0.4" @@ -239,23 +221,9 @@ files = [ {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -optional = false -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -546,63 +514,63 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.5.3" +version = "7.5.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] [package.dependencies] @@ -786,13 +754,13 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "2.1.2" +version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, - {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, ] [package.dependencies] @@ -887,18 +855,18 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.15.1" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, - {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -990,17 +958,16 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "0.46.0" +version = "0.47.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.46.0-py3-none-any.whl", hash = "sha256:7804a22aa981e442bcb3ff1365268cee590e40c6b68388075d3e1287b7d817ca"}, - {file = "griffe-0.46.0.tar.gz", hash = "sha256:8b3b913f70cad7dfe410094b180181e00fec282f60e47cf8bcce187139f099e5"}, + {file = "griffe-0.47.0-py3-none-any.whl", hash = "sha256:07a2fd6a8c3d21d0bbb0decf701d62042ccc8a576645c7f8799fe1f10de2b2de"}, + {file = "griffe-0.47.0.tar.gz", hash = "sha256:95119a440a3c932b13293538bdbc405bee4c36428547553dc6b327e7e7d35e5a"}, ] [package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" [[package]] @@ -1168,22 +1135,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "7.2.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, + {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" @@ -1263,42 +1230,40 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.12.3" +version = "8.18.1" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, - {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] [package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] -all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] [[package]] name = "isodate" @@ -1412,11 +1377,9 @@ files = [ attrs = ">=22.2.0" fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} @@ -1440,7 +1403,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] @@ -1596,7 +1558,6 @@ files = [ async-lru = ">=1.0.0" httpx = ">=0.25.0" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -importlib-resources = {version = ">=1.4", markers = "python_version < \"3.9\""} ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" @@ -1656,13 +1617,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "limits" -version = "3.12.0" +version = "3.13.0" description = "Rate limiting utilities" optional = false python-versions = ">=3.8" files = [ - {file = "limits-3.12.0-py3-none-any.whl", hash = "sha256:48d91e94a0888fb1251aa31423d716ae72ceff997231363f7968a5eaa51dc56d"}, - {file = "limits-3.12.0.tar.gz", hash = "sha256:95764065715a11b9fdcc82558cac2fb59a1febbb7aa2acd045f72ab0c16ec04f"}, + {file = "limits-3.13.0-py3-none-any.whl", hash = "sha256:9767f7233da4255e9904b79908a728e8ec0984c0b086058b4cbbd309aea553f6"}, + {file = "limits-3.13.0.tar.gz", hash = "sha256:6571b0c567bfa175a35fed9f8a954c0c92f1c3200804282f1b8f1de4ad98a953"}, ] [package.dependencies] @@ -2274,21 +2235,21 @@ files = [ [[package]] name = "networkx" -version = "3.1" +version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, - {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, ] [package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "nodeenv" @@ -2320,39 +2281,56 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "1.24.4" +version = "2.0.0" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, + {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, + {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, + {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, + {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, + {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, + {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, + {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, + {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, + {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, ] [[package]] @@ -2635,17 +2613,6 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -optional = false -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - [[package]] name = "pillow" version = "10.3.0" @@ -2732,40 +2699,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa typing = ["typing-extensions"] xmp = ["defusedxml"] -[[package]] -name = "pinecone" -version = "4.0.0" -description = "Pinecone client and SDK" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "pinecone-4.0.0-py3-none-any.whl", hash = "sha256:63c441c4ddab70c2acc7436d7f7e78e33d7d47b894093c3a9016b46b4173c47f"}, - {file = "pinecone-4.0.0.tar.gz", hash = "sha256:47e3e7b8f4a0da9a8339fa63f3d4f9a260b03d2cc53f2ad83d17f3fee947861e"}, -] - -[package.dependencies] -certifi = ">=2019.11.17" -tqdm = ">=4.64.1" -typing-extensions = ">=3.7.4" -urllib3 = [ - {version = ">=1.26.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, - {version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, -] - -[package.extras] -grpc = ["googleapis-common-protos (>=1.53.0)", "grpcio (>=1.44.0)", "grpcio (>=1.59.0)", "lz4 (>=3.1.3)", "protobuf (>=4.25,<5.0)", "protoc-gen-openapiv2 (>=0.0.1,<0.0.2)"] - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "platformdirs" version = "4.2.2" @@ -2838,13 +2771,13 @@ xlsxwriter = ["xlsxwriter"] [[package]] name = "pre-commit" -version = "3.5.0" +version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] @@ -2884,27 +2817,28 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.9.8" +version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, - {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, - {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, - {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, - {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, - {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, - {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, - {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, - {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, - {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, ] [package.extras] @@ -3090,22 +3024,22 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pyproject-api" -version = "1.6.1" +version = "1.7.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" files = [ - {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, - {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, ] [package.dependencies] -packaging = ">=23.1" +packaging = ">=24.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] [[package]] name = "pysocks" @@ -3229,17 +3163,6 @@ files = [ [package.extras] dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - [[package]] name = "pywin32" version = "306" @@ -3616,7 +3539,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -3853,88 +3775,90 @@ torch = ["safetensors[numpy]", "torch (>=1.10)"] [[package]] name = "scikit-learn" -version = "1.3.2" +version = "1.5.0" description = "A set of python modules for machine learning and data mining" optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05"}, - {file = "scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1"}, - {file = "scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a"}, - {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c"}, - {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161"}, - {file = "scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c"}, - {file = "scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66"}, - {file = "scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157"}, - {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb"}, - {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"}, - {file = "scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b"}, - {file = "scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028"}, - {file = "scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5"}, - {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525"}, - {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c"}, - {file = "scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107"}, - {file = "scikit_learn-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93"}, - {file = "scikit_learn-1.3.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073"}, - {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d"}, - {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf"}, - {file = "scikit_learn-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0"}, - {file = "scikit_learn-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03"}, - {file = "scikit_learn-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e"}, - {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a"}, - {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9"}, - {file = "scikit_learn-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0"}, +python-versions = ">=3.9" +files = [ + {file = "scikit_learn-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12e40ac48555e6b551f0a0a5743cc94cc5a765c9513fe708e01f0aa001da2801"}, + {file = "scikit_learn-1.5.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f405c4dae288f5f6553b10c4ac9ea7754d5180ec11e296464adb5d6ac68b6ef5"}, + {file = "scikit_learn-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df8ccabbf583315f13160a4bb06037bde99ea7d8211a69787a6b7c5d4ebb6fc3"}, + {file = "scikit_learn-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c75ea812cd83b1385bbfa94ae971f0d80adb338a9523f6bbcb5e0b0381151d4"}, + {file = "scikit_learn-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:a90c5da84829a0b9b4bf00daf62754b2be741e66b5946911f5bdfaa869fcedd6"}, + {file = "scikit_learn-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a65af2d8a6cce4e163a7951a4cfbfa7fceb2d5c013a4b593686c7f16445cf9d"}, + {file = "scikit_learn-1.5.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:4c0c56c3005f2ec1db3787aeaabefa96256580678cec783986836fc64f8ff622"}, + {file = "scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f77547165c00625551e5c250cefa3f03f2fc92c5e18668abd90bfc4be2e0bff"}, + {file = "scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415"}, + {file = "scikit_learn-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:a03b09f9f7f09ffe8c5efffe2e9de1196c696d811be6798ad5eddf323c6f4d40"}, + {file = "scikit_learn-1.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:460806030c666addee1f074788b3978329a5bfdc9b7d63e7aad3f6d45c67a210"}, + {file = "scikit_learn-1.5.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:1b94d6440603752b27842eda97f6395f570941857456c606eb1d638efdb38184"}, + {file = "scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d82c2e573f0f2f2f0be897e7a31fcf4e73869247738ab8c3ce7245549af58ab8"}, + {file = "scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06"}, + {file = "scikit_learn-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:855fc5fa8ed9e4f08291203af3d3e5fbdc4737bd617a371559aaa2088166046e"}, + {file = "scikit_learn-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:40fb7d4a9a2db07e6e0cae4dc7bdbb8fada17043bac24104d8165e10e4cff1a2"}, + {file = "scikit_learn-1.5.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:47132440050b1c5beb95f8ba0b2402bbd9057ce96ec0ba86f2f445dd4f34df67"}, + {file = "scikit_learn-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174beb56e3e881c90424e21f576fa69c4ffcf5174632a79ab4461c4c960315ac"}, + {file = "scikit_learn-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261fe334ca48f09ed64b8fae13f9b46cc43ac5f580c4a605cbb0a517456c8f71"}, + {file = "scikit_learn-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:057b991ac64b3e75c9c04b5f9395eaf19a6179244c089afdebaad98264bff37c"}, + {file = "scikit_learn-1.5.0.tar.gz", hash = "sha256:789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7"}, ] [package.dependencies] -joblib = ">=1.1.1" -numpy = ">=1.17.3,<2.0" -scipy = ">=1.5.0" -threadpoolctl = ">=2.0.0" +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" [package.extras] -benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.15.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.15.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] [[package]] name = "scipy" -version = "1.9.3" +version = "1.13.1" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, +python-versions = ">=3.9" +files = [ + {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, + {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, + {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, + {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, + {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, + {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, + {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, + {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, + {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, + {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, ] [package.dependencies] -numpy = ">=1.18.5,<1.26.0" +numpy = ">=1.22.4,<2.3" [package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "send2trash" @@ -3979,18 +3903,18 @@ train = ["accelerate (>=0.20.3)", "datasets"] [[package]] name = "setuptools" -version = "70.0.0" +version = "70.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, + {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" @@ -4106,15 +4030,15 @@ mpmath = ">=1.1.0,<1.4.0" [[package]] name = "tbb" -version = "2021.12.0" +version = "2021.13.0" description = "Intel® oneAPI Threading Building Blocks (oneTBB)" optional = false python-versions = "*" files = [ - {file = "tbb-2021.12.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:f2cc9a7f8ababaa506cbff796ce97c3bf91062ba521e15054394f773375d81d8"}, - {file = "tbb-2021.12.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:a925e9a7c77d3a46ae31c34b0bb7f801c4118e857d137b68f68a8e458fcf2bd7"}, - {file = "tbb-2021.12.0-py3-none-win32.whl", hash = "sha256:b1725b30c174048edc8be70bd43bb95473f396ce895d91151a474d0fa9f450a8"}, - {file = "tbb-2021.12.0-py3-none-win_amd64.whl", hash = "sha256:fc2772d850229f2f3df85f1109c4844c495a2db7433d38200959ee9265b34789"}, + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:a2567725329639519d46d92a2634cf61e76601dac2f777a05686fea546c4fe4f"}, + {file = "tbb-2021.13.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aaf667e92849adb012b8874d6393282afc318aca4407fc62f912ee30a22da46a"}, + {file = "tbb-2021.13.0-py3-none-win32.whl", hash = "sha256:6669d26703e9943f6164c6407bd4a237a45007e79b8d3832fe6999576eaaa9ef"}, + {file = "tbb-2021.13.0-py3-none-win_amd64.whl", hash = "sha256:3528a53e4bbe64b07a6112b4c5a00ff3c61924ee46c9c68e004a1ac7ad1f09c3"}, ] [[package]] @@ -4662,13 +4586,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -4749,13 +4673,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -5032,20 +4956,6 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] -[[package]] -name = "wheel" -version = "0.43.0" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, - {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=65)"] - [[package]] name = "wrapt" version = "1.16.0" @@ -5142,5 +5052,5 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" -python-versions = ">=3.8,<4.0" -content-hash = "37d93dd135cc2322805eca294629bfc4414851c85c813caf127736c17be30067" +python-versions = ">=3.9,<4.0" +content-hash = "de83ca89b79716cfc21e06d916d81963401155a517238f9ff066b31ef349a160" diff --git a/pypi_bigquery.sql b/pypi_bigquery.sql index 0dec4b7..a72b31f 100644 --- a/pypi_bigquery.sql +++ b/pypi_bigquery.sql @@ -5,11 +5,11 @@ WITH recent_downloads AS ( FROM `bigquery-public-data.pypi.file_downloads` WHERE - DATE(timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND CURRENT_DATE() + DATE(timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY) AND CURRENT_DATE() GROUP BY project HAVING - download_count >= 250 + download_count >= 100 ) SELECT rd.project AS name, diff --git a/pypi_scout/api/data_loader.py b/pypi_scout/api/data_loader.py new file mode 100644 index 0000000..c4ea031 --- /dev/null +++ b/pypi_scout/api/data_loader.py @@ -0,0 +1,65 @@ +import logging +from typing import Tuple + +import polars as pl + +from pypi_scout.config import Config, StorageBackend +from pypi_scout.utils.blob_io import BlobIO + + +class ApiDataLoader: + def __init__(self, config: Config): + self.config = config + + def load_dataset(self) -> Tuple[pl.DataFrame, pl.DataFrame]: + if self.config.STORAGE_BACKEND == StorageBackend.LOCAL: + df_packages, df_embeddings = self._load_local_dataset() + elif self.config.STORAGE_BACKEND == StorageBackend.BLOB: + df_packages, df_embeddings = self._load_blob_dataset() + else: + raise ValueError(f"Unexpected value found for STORAGE_BACKEND: {self.config.STORAGE_BACKEND}") # noqa: TRY003 + + return df_packages, df_embeddings + + def _load_local_dataset(self) -> Tuple[pl.DataFrame, pl.DataFrame]: + packages_dataset_path = self.config.DATA_DIR / self.config.DATASET_FOR_API_CSV_NAME + embeddings_dataset_path = self.config.DATA_DIR / self.config.EMBEDDINGS_PARQUET_NAME + + logging.info(f"Reading packages dataset from `{packages_dataset_path}`...") + df_packages = pl.read_csv(packages_dataset_path) + self._log_packages_dataset_info(df_packages) + + logging.info(f"Reading embeddings from `{embeddings_dataset_path}`...") + df_embeddings = pl.read_parquet(embeddings_dataset_path) + self._log_embeddings_dataset_info(df_embeddings) + + return df_packages, df_embeddings + + def _load_blob_dataset(self) -> Tuple[pl.DataFrame, pl.DataFrame]: + blob_io = BlobIO( + self.config.STORAGE_BACKEND_BLOB_ACCOUNT_NAME, + self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME, + self.config.STORAGE_BACKEND_BLOB_KEY, + ) + + logging.info( + f"Downloading `{self.config.DATASET_FOR_API_CSV_NAME}` from container `{self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}`..." + ) + df_packages = blob_io.download_csv_to_df(self.config.DATASET_FOR_API_CSV_NAME) + self._log_packages_dataset_info(df_packages) + + logging.info( + f"Downloading `{self.config.EMBEDDINGS_PARQUET_NAME}` from container `{self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}`..." + ) + df_embeddings = blob_io.download_parquet_to_df(self.config.EMBEDDINGS_PARQUET_NAME) + self._log_embeddings_dataset_info(df_embeddings) + + return df_packages, df_embeddings + + def _log_packages_dataset_info(self, df_packages: pl.DataFrame) -> None: + logging.info(f"Finished loading the `packages` dataset. Number of rows in dataset: {len(df_packages):,}") + logging.info(df_packages.describe()) + + def _log_embeddings_dataset_info(self, df_embeddings: pl.DataFrame) -> None: + logging.info(f"Finished loading the `embeddings` dataset. Number of rows in dataset: {len(df_embeddings):,}") + logging.info(df_embeddings.describe()) diff --git a/pypi_scout/api/main.py b/pypi_scout/api/main.py index 5e2ad64..5e70afb 100644 --- a/pypi_scout/api/main.py +++ b/pypi_scout/api/main.py @@ -11,11 +11,11 @@ from slowapi.util import get_remote_address from starlette.requests import Request -from pypi_scout.api.utils import load_dataset +from pypi_scout.api.data_loader import ApiDataLoader from pypi_scout.config import Config +from pypi_scout.embeddings.simple_vector_database import SimpleVectorDatabase from pypi_scout.utils.logging import setup_logging from pypi_scout.utils.score_calculator import calculate_score -from pypi_scout.vector_database import VectorDatabaseInterface # Setup logging setup_logging() @@ -40,16 +40,12 @@ allow_headers=["*"], ) -# Load dataset and initialize model and vector database interface -df = load_dataset(config) +data_loader = ApiDataLoader(config) +df_packages, df_embeddings = data_loader.load_dataset() + model = SentenceTransformer(config.EMBEDDINGS_MODEL_NAME) -vector_database_interface = VectorDatabaseInterface( - pinecone_token=config.PINECONE_TOKEN, - pinecone_index_name=config.PINECONE_INDEX_NAME, - embeddings_model=model, - pinecone_namespace=config.PINECONE_NAMESPACE, -) +vector_database = SimpleVectorDatabase(embeddings_model=model, df_embeddings=df_embeddings) class QueryModel(BaseModel): @@ -83,8 +79,8 @@ async def search(query: QueryModel, request: Request): raise HTTPException(status_code=400, detail="top_k cannot be larger than 100.") logging.info(f"Searching for similar projects. Query: '{query.query}'") - df_matches = vector_database_interface.find_similar(query.query, top_k=query.top_k * 2) - df_matches = df_matches.join(df, how="left", on="name") + df_matches = vector_database.find_similar(query.query, top_k=query.top_k * 2) + df_matches = df_matches.join(df_packages, how="left", on="name") logging.info( f"Fetched the {len(df_matches)} most similar projects. Calculating the weighted scores and filtering..." ) @@ -110,5 +106,5 @@ async def search(query: QueryModel, request: Request): df_matches = df_matches.head(query.top_k) logging.info(f"Returning the {len(df_matches)} best matches.") - + df_matches = df_matches.select(["name", "similarity", "summary", "weekly_downloads"]) return SearchResponse(matches=df_matches.to_dicts(), warning=warning, warning_message=warning_message) diff --git a/pypi_scout/api/utils.py b/pypi_scout/api/utils.py deleted file mode 100644 index efa70a4..0000000 --- a/pypi_scout/api/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -import logging -import sys - -import polars as pl - -from pypi_scout.config import Config, StorageBackend -from pypi_scout.utils.blob_io import BlobIO - - -def load_dataset(config: Config) -> pl.DataFrame: - dataset_path = config.DATA_DIR / config.DATASET_FOR_API_CSV_NAME - - if dataset_path.exists(): - logging.info(f"Found local dataset. Reading dataset from `{dataset_path}`...") - df = pl.read_csv(dataset_path) - - elif config.STORAGE_BACKEND == StorageBackend.BLOB: - logging.info( - f"Downloading `{config.DATASET_FOR_API_CSV_NAME}` from container `{config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}`..." - ) - blob_io = BlobIO( - config.STORAGE_BACKEND_BLOB_ACCOUNT_NAME, - config.STORAGE_BACKEND_BLOB_CONTAINER_NAME, - config.STORAGE_BACKEND_BLOB_KEY, - ) - df = blob_io.download_csv(config.DATASET_FOR_API_CSV_NAME) - logging.info("Finished downloading.") - - else: - logging.error( - f"Dataset {dataset_path} not found, and config.StorageBackend is not `BLOB` so can't download the dataset from Azure. Terminating." - ) - sys.exit(1) - - logging.info(f"Finished loading the processed dataset. Number of rows: {len(df):,}") - logging.info(f"The highest weekly downloads in the dataset: {df['weekly_downloads'].max():,}") - logging.info(f"The lowest weekly downloads in the dataset: {df['weekly_downloads'].min():,}") - return df diff --git a/pypi_scout/config.py b/pypi_scout/config.py index 671c054..57cb849 100644 --- a/pypi_scout/config.py +++ b/pypi_scout/config.py @@ -1,5 +1,5 @@ import os -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -11,23 +11,11 @@ class StorageBackend(Enum): @dataclass class Config: - # Name of the Pinecone index used for storing vector representations of the package descriptions. - PINECONE_INDEX_NAME = "pypi" - - # Namespace within the Pinecone index to logically separate data. - PINECONE_NAMESPACE = "ns1" - - # API token for authenticating with Pinecone. Should be set as an environment variable. - PINECONE_TOKEN: str = field(default_factory=lambda: os.getenv("PINECONE_TOKEN")) - # Name of the model used for generating vector embeddings from text. # See https://sbert.net/docs/sentence_transformer/pretrained_models.html for available models. EMBEDDINGS_MODEL_NAME = "all-mpnet-base-v2" - # Dimension of the vector embeddings produced by the model. Should match the output of the model above. - EMBEDDINGS_DIMENSION = 768 - - # Boolean to overwrite existing files. e.g. re-download the raw dataset, upload processed dataset to blob, etc. + # Boolean to overwrite raw data file if it already exists OVERWRITE: bool = True # Directory where dataset files are stored. @@ -43,22 +31,26 @@ class Config: # For example; it needs the name, weekly downloads, and the summary, but not the (cleaned) description. DATASET_FOR_API_CSV_NAME = "dataset_for_api.csv" + # Filename for the dataset that contains the minimal data that the API needs. + # For example; it needs the name, weekly downloads, and the summary, but not the (cleaned) description. + EMBEDDINGS_PARQUET_NAME = "embeddings.parquet" + # Google Drive file ID for downloading the raw dataset. - GOOGLE_FILE_ID = "1huR7-VD3AieBRCcQyRX9MWbPLMb_czjq" + GOOGLE_FILE_ID = "1IDJvCsq1gz0yUSXgff13pMl3nUk7zJzb" # Number of top results to return for a query. - N_RESULTS_TO_RETURN = 30 + N_RESULTS_TO_RETURN = 40 # Fraction of the dataset to include in the vector database. This value determines the portion of top packages # (sorted by weekly downloads) to include. Increase this value to include a larger portion of the dataset, up to 1.0 (100%). # For reference, a value of 0.25 corresponds to including all PyPI packages with at least approximately 650 weekly downloads - FRAC_DATA_TO_INCLUDE = 0.25 + FRAC_DATA_TO_INCLUDE = 1 # Weights for the combined score calculation. Higher WEIGHT_SIMILARITY prioritizes # relevance based on text similarity, while higher WEIGHT_WEEKLY_DOWNLOADS prioritizes # packages with more weekly downloads. - WEIGHT_SIMILARITY = 0.6 - WEIGHT_WEEKLY_DOWNLOADS = 0.4 + WEIGHT_SIMILARITY = 0.5 + WEIGHT_WEEKLY_DOWNLOADS = 0.5 # Storage backend configuration. Can be either StorageBackend.LOCAL or StorageBackend.BLOB. # If StorageBackend.BLOB, the processed dataset will be uploaded to Blob, and the backend API @@ -70,9 +62,6 @@ class Config: STORAGE_BACKEND_BLOB_KEY: str | None = None def __post_init__(self) -> None: - if not self.PINECONE_TOKEN: - raise OSError("PINECONE_TOKEN not found in environment variables") # noqa: TRY003 - if os.getenv("STORAGE_BACKEND") == "BLOB": self.STORAGE_BACKEND = StorageBackend.BLOB self.STORAGE_BACKEND_BLOB_ACCOUNT_NAME = os.getenv("STORAGE_BACKEND_BLOB_ACCOUNT_NAME") diff --git a/pypi_scout/data/description_cleaner.py b/pypi_scout/data/description_cleaner.py index 5bfa167..5f34c8c 100644 --- a/pypi_scout/data/description_cleaner.py +++ b/pypi_scout/data/description_cleaner.py @@ -25,7 +25,7 @@ def clean(self, df: pl.DataFrame, input_col: str, output_col: str) -> pl.DataFra Returns: pl.DataFrame: The modified DataFrame with the cleaned text. """ - df = df.with_columns(pl.col(input_col).apply(self._clean_text).alias(output_col)) + df = df.with_columns(pl.col(input_col).map_elements(self._clean_text, return_dtype=pl.String).alias(output_col)) return df def _clean_text(self, text: str) -> str: diff --git a/pypi_scout/data/raw_data_reader.py b/pypi_scout/data/raw_data_reader.py index f0de2ba..a3a6eef 100644 --- a/pypi_scout/data/raw_data_reader.py +++ b/pypi_scout/data/raw_data_reader.py @@ -21,9 +21,13 @@ def read(self): DataFrame: The processed dataframe. """ df = pl.read_csv(self.raw_dataset) - df = df.with_columns(weekly_downloads=(pl.col("number_of_downloads") / 4).round().cast(pl.Int32)) + df = df.with_columns(weekly_downloads=pl.col("number_of_downloads").cast(pl.Int32)) df = df.drop("number_of_downloads") df = df.unique(subset="name") - df = df.filter(~pl.col("description").is_null()) + df = df.filter(~(pl.col("description").is_null() & pl.col("summary").is_null())) df = df.sort("weekly_downloads", descending=True) + df = df.with_columns( + summary=pl.col("summary").fill_null(""), + description=pl.col("description").fill_null(""), + ) return df diff --git a/pypi_scout/embeddings/embeddings_creator.py b/pypi_scout/embeddings/embeddings_creator.py new file mode 100644 index 0000000..a61d7cc --- /dev/null +++ b/pypi_scout/embeddings/embeddings_creator.py @@ -0,0 +1,60 @@ +import logging + +import polars as pl +from sentence_transformers import SentenceTransformer +from tqdm import tqdm + + +class VectorEmbeddingCreator: + def __init__( + self, + embeddings_model: SentenceTransformer, + embedding_column_name: str = "embeddings", + batch_size: int = 128, + ): + """ + Initializes the VectorEmbeddingCreator with a SentenceTransformer model, embedding column name, and batch size. + + Args: + embeddings_model (SentenceTransformer): The SentenceTransformer model to generate embeddings. + embedding_column_name (str, optional): The name of the column to store embeddings. Defaults to 'embeddings'. + batch_size (int, optional): The size of batches to process at a time. Defaults to 128. + """ + self.model = embeddings_model + self.embedding_column_name = embedding_column_name + self.batch_size = batch_size + + def add_embeddings(self, df: pl.DataFrame, text_column: str) -> pl.DataFrame: + """ + Adds embeddings to the DataFrame based on the specified text column. + + Args: + df (pl.DataFrame): The Polars DataFrame to which embeddings will be added. + text_column (str): The column name containing text to generate embeddings for. + + Returns: + pl.DataFrame: The DataFrame with an additional column containing embeddings. + """ + logging.info("Splitting DataFrame into batches...") + df_chunks = self._split_dataframe_in_batches(df, batch_size=self.batch_size) + all_embeddings = [] + + logging.info("Generating embeddings...") + for chunk in tqdm(df_chunks, desc="Generating embeddings", unit="batch"): + embeddings = self._generate_embeddings(chunk, text_column) + all_embeddings.extend(embeddings) + + df = df.with_columns(pl.Series(self.embedding_column_name, all_embeddings)) + return df + + def _generate_embeddings(self, chunk: pl.DataFrame, text_column: str) -> list: + embeddings = self.model.encode(list(chunk[text_column]), show_progress_bar=False) + return embeddings + + @staticmethod + def _split_dataframe_in_batches(df: pl.DataFrame, batch_size: int) -> list: + """ + Splits a Polars DataFrame into batches. + """ + n_chunks = (df.height + batch_size - 1) // batch_size + return [df.slice(i * batch_size, batch_size) for i in range(n_chunks)] diff --git a/pypi_scout/embeddings/simple_vector_database.py b/pypi_scout/embeddings/simple_vector_database.py new file mode 100644 index 0000000..7812b76 --- /dev/null +++ b/pypi_scout/embeddings/simple_vector_database.py @@ -0,0 +1,55 @@ +import numpy as np +import polars as pl +from sentence_transformers import SentenceTransformer +from sklearn.metrics.pairwise import cosine_similarity + + +class SimpleVectorDatabase: + def __init__( + self, + embeddings_model: SentenceTransformer, + df_embeddings: pl.DataFrame, + embedding_column: str = "embeddings", + processed_column: str = "embeddings_array", + ): + """ + Initializes the SimpleVectorDatabase with a SentenceTransformer model and a DataFrame containing embeddings. + + Args: + embeddings_model (SentenceTransformer): The SentenceTransformer model to generate embeddings. + df_embeddings (pl.DataFrame): The Polars DataFrame containing the initial embeddings. + embedding_column (str, optional): The name of the column containing the original embeddings. Defaults to 'embeddings'. + """ + self.embeddings_model = embeddings_model + self.df_embeddings = df_embeddings + self.embedding_column = embedding_column + self.embeddings_matrix = self._create_embeddings_matrix() + + def find_similar(self, query: str, top_k: int = 25) -> pl.DataFrame: + """ + Finds the top_k most similar vectors in the database for a given query. + + Args: + query (str): The query string to find similar vectors for. + top_k (int, optional): The number of similar vectors to retrieve. Defaults to 25. + + Returns: + pl.DataFrame: A Polars DataFrame containing the most similar vectors and their similarity scores. + """ + query_embedding = self.embeddings_model.encode(query, show_progress_bar=False) + + similarities = cosine_similarity([query_embedding], self.embeddings_matrix)[0] + + top_k_indices = np.argsort(similarities)[::-1][:top_k] + top_k_scores = similarities[top_k_indices] + df_best_matches = self.df_embeddings[top_k_indices] + + df_best_matches = df_best_matches.with_columns(pl.Series("similarity", top_k_scores)) + df_best_matches = df_best_matches.drop(self.embedding_column) + + return df_best_matches + + def _create_embeddings_matrix(self) -> np.ndarray: + return np.stack( + self.df_embeddings[self.embedding_column].apply(lambda x: np.array(x, dtype=np.float32)).to_numpy() + ) diff --git a/pypi_scout/scripts/create_vector_embeddings.py b/pypi_scout/scripts/create_vector_embeddings.py new file mode 100644 index 0000000..870b019 --- /dev/null +++ b/pypi_scout/scripts/create_vector_embeddings.py @@ -0,0 +1,45 @@ +import logging +from pathlib import Path + +import polars as pl +from dotenv import load_dotenv +from sentence_transformers import SentenceTransformer + +from pypi_scout.config import Config +from pypi_scout.embeddings.embeddings_creator import VectorEmbeddingCreator +from pypi_scout.utils.logging import setup_logging + + +def read_processed_dataset(path_to_processed_dataset: Path): + logging.info("📂 Reading the processed dataset...") + df = pl.read_csv(path_to_processed_dataset) + logging.info(f"📊 Number of rows in the processed dataset: {len(df):,}") + return df + + +def write_parquet(df: pl.DataFrame, processed_dataset_path: Path): + logging.info(f"Storing dataset in {processed_dataset_path}...") + df.write_parquet(processed_dataset_path) + logging.info("✅ Done!") + + +def create_vector_embeddings(): + setup_logging() + load_dotenv() + + config = Config() + df = read_processed_dataset(config.DATA_DIR / config.PROCESSED_DATASET_CSV_NAME) + df = df.with_columns( + summary_and_description_cleaned=pl.concat_str(pl.col("summary"), pl.lit(" - "), pl.col("description_cleaned")) + ) + df = VectorEmbeddingCreator(embeddings_model=SentenceTransformer(config.EMBEDDINGS_MODEL_NAME)).add_embeddings( + df, text_column="summary_and_description_cleaned" + ) + + df = df.select("name", "embeddings").unique(subset="name") + write_parquet(df, config.DATA_DIR / config.EMBEDDINGS_PARQUET_NAME) + + +if __name__ == "__main__": + setup_logging() + create_vector_embeddings() diff --git a/pypi_scout/scripts/setup.py b/pypi_scout/scripts/setup.py index 795e201..7145162 100644 --- a/pypi_scout/scripts/setup.py +++ b/pypi_scout/scripts/setup.py @@ -1,37 +1,27 @@ -import argparse import logging +from pypi_scout.scripts.create_vector_embeddings import create_vector_embeddings from pypi_scout.scripts.download_raw_dataset import download_raw_dataset from pypi_scout.scripts.process_raw_dataset import process_raw_dataset -from pypi_scout.scripts.setup_pinecone import setup_pinecone from pypi_scout.scripts.upload_processed_datasets import upload_processed_datasets -from pypi_scout.scripts.upsert_data import upsert_data from pypi_scout.utils.logging import setup_logging -def main(no_upsert): +def main(): setup_logging() - logging.info("\n\nSETTING UP PINECONE -------------\n") - setup_pinecone() - logging.info("\n\nDOWNLOADING RAW DATASET -------------\n") download_raw_dataset() logging.info("\n\nPROCESSING RAW DATASET -------------\n") process_raw_dataset() + logging.info("\n\nCREATING VECTOR EMBEDDINGS -------------\n") + create_vector_embeddings() + logging.info("\n\nUPLOADING PROCESSED DATASETS -------------\n") upload_processed_datasets() - if not no_upsert: - logging.info("\n\nUPSERTING DATA TO PINECONE -------------\n") - upsert_data() if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run the setup script with optional flags.") - parser.add_argument("--no-upsert", action="store_true", help="If set, do not upsert data to the Pinecone database.") - - args = parser.parse_args() - - main(no_upsert=args.no_upsert) + main() diff --git a/pypi_scout/scripts/setup_pinecone.py b/pypi_scout/scripts/setup_pinecone.py deleted file mode 100644 index 4f3b1f2..0000000 --- a/pypi_scout/scripts/setup_pinecone.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging - -from dotenv import load_dotenv -from pinecone import Pinecone, ServerlessSpec -from pinecone.core.client.exceptions import PineconeApiException - -from pypi_scout.config import Config -from pypi_scout.utils.logging import setup_logging - - -def setup_pinecone(): - """ - This script sets up a Pinecone index for storing embeddings. - - It loads the environment variables from a .env file, creates a Pinecone client, - and creates an index with the specified name, dimension, metric, and serverless specification. - """ - - load_dotenv() - config = Config() - - logging.info("🔗 Connecting to Pinecone..") - pc = Pinecone(api_key=config.PINECONE_TOKEN) - - try: - logging.info("Creating Pinecone index..") - pc.create_index( - name=config.PINECONE_INDEX_NAME, - dimension=config.EMBEDDINGS_DIMENSION, - metric="dotproduct", - spec=ServerlessSpec(cloud="aws", region="us-east-1"), - ) - logging.info("✅ Pinecone index created successfully.") - except PineconeApiException as e: - if e.status == 409: - logging.warning(f"🔹 Pinecone index '{config.PINECONE_INDEX_NAME}' already exists.") - else: - logging.exception("❌ An error occurred while creating the Pinecone index.") - - -if __name__ == "__main__": - setup_logging() - setup_pinecone() diff --git a/pypi_scout/scripts/upload_processed_datasets.py b/pypi_scout/scripts/upload_processed_datasets.py index 4d94043..e55b51f 100644 --- a/pypi_scout/scripts/upload_processed_datasets.py +++ b/pypi_scout/scripts/upload_processed_datasets.py @@ -1,7 +1,5 @@ import logging -from pathlib import Path -import polars as pl from dotenv import load_dotenv from pypi_scout.config import Config, StorageBackend @@ -9,45 +7,6 @@ from pypi_scout.utils.logging import setup_logging -class CsvToBlobUploader: - def __init__(self, config: Config): - self.config = config - self.blob_io = BlobIO( - config.STORAGE_BACKEND_BLOB_ACCOUNT_NAME, - config.STORAGE_BACKEND_BLOB_CONTAINER_NAME, - config.STORAGE_BACKEND_BLOB_KEY, - ) - self.overwrite = config.OVERWRITE - - def read_csv(self, path_to_csv: Path) -> pl.DataFrame: - logging.info(f"📂 Reading the dataset from {path_to_csv}...") - df = pl.read_csv(path_to_csv) - logging.info(f"📊 Number of rows in the dataset {path_to_csv.name}: {len(df):,}") - return df - - def upload_csv_to_blob(self, df: pl.DataFrame, csv_name: str): - if self.blob_io.exists(csv_name): - if not self.overwrite: - logging.info( - f"🔹 Dataset {csv_name} already exists in container '{self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}'! Skipping upload." - ) - return - else: - logging.info( - f"⤵️ Dataset {csv_name} already exists in container '{self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}', but overwrite is enabled. Overwriting..." - ) - - logging.info(f"Uploading {csv_name} to blob container {self.config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}...") - self.blob_io.upload_csv(df, csv_name) - logging.info("✅ Done!") - - def process_and_upload_datasets(self, dataset_names: list[str]): - for dataset_name in dataset_names: - csv_path = self.config.DATA_DIR / dataset_name - df = self.read_csv(csv_path) - self.upload_csv_to_blob(df, dataset_name) - - def upload_processed_datasets(): load_dotenv() config = Config() @@ -58,10 +17,19 @@ def upload_processed_datasets(): ) return - dataset_names = [config.PROCESSED_DATASET_CSV_NAME, config.DATASET_FOR_API_CSV_NAME] + file_names = [config.PROCESSED_DATASET_CSV_NAME, config.DATASET_FOR_API_CSV_NAME, config.EMBEDDINGS_PARQUET_NAME] + + blob_io = BlobIO( + config.STORAGE_BACKEND_BLOB_ACCOUNT_NAME, + config.STORAGE_BACKEND_BLOB_CONTAINER_NAME, + config.STORAGE_BACKEND_BLOB_KEY, + ) + + for file_name in file_names: + logging.info(f"💫 Uploading {file_name} to blob container `{config.STORAGE_BACKEND_BLOB_CONTAINER_NAME}`...") + blob_io.upload_local_file(config.DATA_DIR / file_name, file_name) - uploader = CsvToBlobUploader(config) - uploader.process_and_upload_datasets(dataset_names) + logging.info("✅ Done!") if __name__ == "__main__": diff --git a/pypi_scout/scripts/upsert_data.py b/pypi_scout/scripts/upsert_data.py deleted file mode 100644 index fef0a42..0000000 --- a/pypi_scout/scripts/upsert_data.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging - -import polars as pl -from dotenv import load_dotenv -from sentence_transformers import SentenceTransformer - -from pypi_scout.config import Config -from pypi_scout.utils.logging import setup_logging -from pypi_scout.vector_database import VectorDatabaseInterface - - -def upsert_data(): - """ - Upserts data from a processed dataset CSV into a vector database. - """ - load_dotenv() - config = Config() - - logging.info("Reading the processed dataset...") - df = pl.read_csv(config.DATA_DIR / config.PROCESSED_DATASET_CSV_NAME) - logging.info("Number of rows in the dataset: %s", len(df)) - - logging.info("🔗 Connecting to the vector database..") - vector_database_interface = VectorDatabaseInterface( - pinecone_token=config.PINECONE_TOKEN, - pinecone_index_name=config.PINECONE_INDEX_NAME, - embeddings_model=SentenceTransformer(config.EMBEDDINGS_MODEL_NAME), - pinecone_namespace=config.PINECONE_NAMESPACE, - ) - - logging.info("⬆️ Upserting data into the vector database..") - logging.info("This can take a while...") - logging.info("If this really takes too long, consider lowering the value of `FRAC_DATA_TO_INCLUDE` in config.py.") - df = df.with_columns( - summary_and_description_cleaned=pl.concat_str(pl.col("summary"), pl.lit(" - "), pl.col("description_cleaned")) - ) - vector_database_interface.upsert_polars(df, key_column="name", text_column="summary_and_description_cleaned") - logging.info("✅ Done!") - - -if __name__ == "__main__": - setup_logging() - upsert_data() diff --git a/pypi_scout/utils/blob_io.py b/pypi_scout/utils/blob_io.py index 74eaa5e..a5fd4af 100644 --- a/pypi_scout/utils/blob_io.py +++ b/pypi_scout/utils/blob_io.py @@ -1,10 +1,15 @@ import tempfile -from io import BytesIO +from enum import Enum import polars as pl from azure.storage.blob import BlobServiceClient +class Format(Enum): + CSV = "csv" + PARQUET = "parquet" + + class BlobIO: def __init__(self, account_name: str, container_name: str, account_key: str): self.account_name = account_name @@ -15,32 +20,33 @@ def __init__(self, account_name: str, container_name: str, account_key: str): ) self.container_client = self.service_client.get_container_client(container_name) - def upload_csv(self, data_frame: pl.DataFrame, blob_name: str) -> None: - csv_buffer = BytesIO() - data_frame.write_csv(csv_buffer) - csv_buffer.seek(0) # Reset buffer position to the beginning - blob_client = self.container_client.get_blob_client(blob_name) - blob_client.upload_blob(csv_buffer, overwrite=True) - - def upload_local_csv(self, local_file_path: str, blob_name: str) -> None: + def upload_local_file(self, local_file_path: str, blob_name: str) -> None: with open(local_file_path, "rb") as data: blob_client = self.container_client.get_blob_client(blob_name) blob_client.upload_blob(data, overwrite=True) - def download_csv(self, blob_name: str) -> pl.DataFrame: + def download_csv_to_df(self, blob_name: str): + return self._download_as_df(blob_name, Format.CSV) + + def download_parquet_to_df(self, blob_name: str): + return self._download_as_df(blob_name, Format.PARQUET) + + def _download_as_df(self, blob_name: str, format: Format) -> pl.DataFrame: # noqa: A002 + """ + //TODO: Improve by not reading into a file first. + """ blob_client = self.container_client.get_blob_client(blob_name) download_stream = blob_client.download_blob() - # Create a temporary file with tempfile.NamedTemporaryFile(delete=True) as temp_file: - # Download the blob content into the temporary file temp_file.write(download_stream.readall()) temp_file.flush() - # Read the CSV using Polars - df = pl.read_csv(temp_file.name) + if format == Format.CSV: + return pl.read_csv(temp_file.name) - return df + if format == Format.PARQUET: + return pl.read_parquet(temp_file.name) def exists(self, blob_name): blob_client = self.container_client.get_blob_client(blob_name) diff --git a/pypi_scout/vector_database/__init__.py b/pypi_scout/vector_database/__init__.py deleted file mode 100644 index 7bdf73d..0000000 --- a/pypi_scout/vector_database/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from pypi_scout.vector_database.interface import VectorDatabaseInterface - -__all__ = ("VectorDatabaseInterface",) diff --git a/pypi_scout/vector_database/interface.py b/pypi_scout/vector_database/interface.py deleted file mode 100644 index 3293231..0000000 --- a/pypi_scout/vector_database/interface.py +++ /dev/null @@ -1,80 +0,0 @@ -import logging - -import polars as pl -from pinecone import Pinecone -from sentence_transformers import SentenceTransformer -from tqdm import tqdm - - -class VectorDatabaseInterface: - """ - A class that provides an interface for interacting with a vector database. - - Args: - pinecone_token (str): The Pinecone API token. - pinecone_index_name (str): The name of the Pinecone index. - pinecone_namespace (str): The namespace for the Pinecone index. - embeddings_model (SentenceTransformer): The sentence transformer model for encoding text into embeddings. - batch_size (int, optional): The batch size for upserting data. Defaults to 250. - """ - - def __init__( - self, - pinecone_token: str, - pinecone_index_name: str, - pinecone_namespace: str, - embeddings_model: SentenceTransformer, - batch_size: int = 128, - ): - self.batch_size = batch_size - self.model = embeddings_model - logging.info("Connecting to Pinecone...") - pc = Pinecone(api_key=pinecone_token) - self.index = pc.Index(pinecone_index_name) - self.pinecone_namespace = pinecone_namespace - logging.info("Connection successful.") - - def upsert_polars(self, df: pl.DataFrame, key_column: str, text_column: str): - """ - Upserts the data from a Polars DataFrame into the vector database. - - Args: - df (pl.DataFrame): The Polars DataFrame containing the data to be upserted. - key_column (str): The name of the column in the DataFrame containing the unique keys. - text_column (str): The name of the column in the DataFrame containing the text data. - """ - df_chunks = self._split_dataframe_in_batches(df, batch_size=self.batch_size) - for chunk in tqdm(df_chunks, desc="Upserting batches", unit="batch"): - self._upsert_chunk(chunk, key_column, text_column) - - def find_similar(self, query: str, top_k: int = 25) -> pl.DataFrame: - """ - Finds similar vectors in the database for a given query. - - Args: - query (str): The query string. - top_k (int, optional): The number of similar vectors to retrieve. Defaults to 25. - - Returns: - pl.DataFrame: A Polars DataFrame containing the similar vectors and their similarity scores. - """ - embeddings = self.model.encode(query, show_progress_bar=False) - matches = self.index.query( - namespace=self.pinecone_namespace, vector=embeddings.tolist(), top_k=top_k, include_values=False - ) - return pl.from_dicts([{"name": x["id"], "similarity": x["score"]} for x in matches["matches"]]) - - def _upsert_chunk(self, chunk: pl.DataFrame, key_column: str, text_column: str) -> None: - embeddings = self.model.encode(list(chunk[text_column]), show_progress_bar=False) - vectors = [ - {"id": project_name, "values": embedding} for project_name, embedding in zip(chunk[key_column], embeddings) - ] - self.index.upsert(vectors=vectors, namespace=self.pinecone_namespace, show_progress=False) - - @staticmethod - def _split_dataframe_in_batches(df: pl.DataFrame, batch_size: int) -> pl.DataFrame: - """ - Splits a Polars DataFrame into batches. - """ - n_chunks = (df.height + batch_size - 1) // batch_size - return [df.slice(i * batch_size, batch_size) for i in range(n_chunks)] diff --git a/pyproject.toml b/pyproject.toml index aecb24e..7d51b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,13 +11,12 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<4.0" +python = ">=3.9,<4.0" beautifulsoup4 = "^4.12.3" polars = "^0.20.31" sentence-transformers = "^3.0.1" lxml = "^5.2.2" python-dotenv = "^1.0.1" -pinecone = "^4.0.0" tqdm = "^4.66.4" fastapi = "^0.111.0" pydantic = "^2.7.4" @@ -26,6 +25,8 @@ gdown = "^5.2.0" azure-storage-blob = "^12.20.0" slowapi = "^0.1.9" starlette = "^0.37.2" +numpy = "^2.0.0" +scikit-learn = "^1.5.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" diff --git a/requirements-cpu.txt b/requirements-cpu.txt index 2c10e0e..bf9744c 100644 --- a/requirements-cpu.txt +++ b/requirements-cpu.txt @@ -5,7 +5,6 @@ polars==0.20.31 sentence-transformers==3.0.1 lxml==5.2.2 python-dotenv==1.0.1 -pinecone-client==4.0.0 tqdm==4.66.4 fastapi==0.111.0 pydantic==2.7.4 @@ -16,5 +15,6 @@ numpy==1.24.4 azure-storage-blob==12.20.0 slowapi==0.1.9 starlette==0.37.2 +scikit-learn==1.5.0 --index-url=https://download.pytorch.org/whl/cpu --extra-index-url=https://pypi.org/simple diff --git a/static/architecture.png b/static/architecture.png deleted file mode 100644 index 6b46e8b5a34ec70f5ca7931f14e8595e4b3e91f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58699 zcmeFZbySqw8$T)~NFzNo905g2X=Xr5j!8&LiwYtsAVVXK3L^%gNE%3oQqm}(paTkm z($Yu_9Yfsb1wB8%>;85Bx@+Ba$93??%8MfE`}XalJN2)o z{=R+W*nRs*WvTYVZ*oV@wZLB_?)uv5`|>_>Pr^UQ?bObx?b}y~quI2ffPWuw{nyle z-#+>V?i0<-gl03j+;HHu8x{?es<>Jz1f5J zW=}rc&-zHWj#RIXTrGCixovznKyhvpPj7b<{#+HA9q`1kPbYqlI&OCr?~CW!5z+kp z8tLL7Ff#4E))n`o|NK56hJ@kwuVYheYc>9SeEk3a`2ROo=H7*5w6`FR7jvJIfw#Hu zt=H(vOwWy9-^9z^hQrOPd<^r<%LU^lEbh~=OEiBkwi|6tJbq)U z;il21c-5Vw+A-X*4G|12IMr=Y)lEW3)?CewWoKOo zr6G@k>xpXOtg_)i05LbqrFy;52)^|uE5CZgD%s5m!5ZMRqZVb*;dtvX<8bcV7$Iv{*D%w4F>&JK^LoceIzx0uzZiwtF1$D!3yaj!94mB%Fj#lYb@0Sw-`mnknB13F z7euc0l;pm)Z&ydjJ0GP($=vfL;diFq%K#G04iIQ2P6@sRJz^nGzNxet$JvoC*6 zGBs?x%W0~npLpzw{*`Ujm|I6pi@LY9hSu7`hAEopj=(mR)X#?wQ$JGnayC@>=GvOJ zFxv7mrVbV=yIwlVwpce!UBArL#xsXa{GtI*>H9nE#!apt?jL$?@Je6*#aT|nD=l%4 z;5*s2RWR$UBgT0-{bZe4h8GO0d^}&77QgTbtlXH&)_;4`>6KY&UPr!_Rh1V?*5O&d z_PNJ0u`kX&$yzncHp+eG`Rm)4JPYM(1JzX?)7^SXUb9Cn-rjuV+*ff5jP#jtzU7yf zI((BorTMfRQYWgk2mBV#dH>2Sy*$yDcFb?hl~dN?;a$$-CmW-=M3{xo-uv)Cq^0B4 zh0&*8{d#kYitc00mEMb&w>Q^|?AryGISVemr#3CV8l7WSdcHq!$NzhQO^D<7BCQgq z9$~(KwKTtl42Sz6YbN zRj*f0GfRvJxjR)4Bef96^=~VqmrQOHoe_C@ zHMFgY-y@&0?<+|aS6em-2G#a}1jEthOR^f#$mVk-HJ;RGEY+CQ=buy&QlHsWN>U%b z*N)%ymz7Sqgh*IaWcI6Id{-U@Y;An^VNbQIi)0cqz>_X9K9;$5KUcf%?Ob)X5A#v; zGTk@{iz8yjc?OsR-}0^UeV817y#Da*!9EO5WqZx`d%62dzm3V4M+iUOdgc3Vua6hG z45-LXpJI6Or6IA+{S<@Emxj2ddxtOvXBdoc$QT@0UL6QDqc<($D+wLCG*OsJgL$*9 zE)}uDaFcgtoVFIdRH^Mm0y~rOXciDRQT||rUvkA zo%6Sb$(vdz`TSC2_?AxA6^7b6tS^jaz5Mv|{hiLj@P0A(@z0N7x3ew971&;$d-B4k zFs+Fu*kCD0O^0L2zF}V3v0IAs*yYHnJ~{g~eow#ExdAJlUlWEQx}-y5)EMVB;(pA4 zlE*}<&&ca*Ur$yU6Oym>SC&&BBk=mnyy@y79>DmvHM~6kRB6gTyk8ZI7g#zmgo)H; zuL>h1YvKheRokCXRgDn>-V@m{F}f9$JpErM5<^$zeU408o}n@%^lRq}Kb$pv>8moy zd$YppVfo9a+g-garvtxD;Cb9RCPXLiX~uqo$<5lIy!l zmECW#U^oaIaYs$l!_5=8#6@Y-5>)(^##8tOkJU-3EZullAim`tl}gE6YkR|c&(Y?k zGGL|h4;X!vj!+V%t*SQ{FjsjTDe9NNcL2k^JT5XuQqO&6XKOXZjc)G?uT$xBm<GjK~^KgLVus{|HbX%TM1@yVAQ+@0cEa;@6pz?}fI zvXFW_Toj{?<9!64KNCM_Y~}w*hzMQGVln_vu|r&|@W#=z#Y%EW7a9UeA^W#)%Ky*)qN{(wwFePf5?+Y`K?< zIv;E!!H3gvIaYar|8a0UB$z(&%vct(4-qon-7S|tFcI_;qIwontcVWp?og%0cK9E{ zX4@kVbnfQvSsp7155XYEL{F9<0<5E>fS{n2ZiUCS<#kwXmzAIJZ$ZHuAwl%mqN|^> zP>gVo(j|;cv62(ti(hW|Ntl%!{c}00?x69B_Kd9gppDUZD`fl;8kLO+A=$HHXX1|1 zV)s^)-kiv1R}l!ojckH~3x}w@2#QrYyK>>p2`)+kj z&FW{DKir>kuL#`Po_@b;5G-0GT!e>53{T5)GcLh=<|co<$=*oHGkA4yZ$9aw)ZIZZ zU5DzkhNL{F43OJjzrVgXp6!iQ*U;eHOYXJwYWJhqcCb5)~2i~jipa{&oMHPgx35SK_W4*4sPPxu?{ zEO%KUw+FA=cZ2A#OABvRx7RXuFLEx1Td~7#8S~G%k>mX3*?~Vx4kjRCR^ph|Uf?xnxi<=Ac(3qN z`{__pe~p4zGm5UK)J1-8An^YiIe8ae(*HdWWSTG#B#V0k8RcGtfga0V|9d*Hm4+EM z^FQD3T9Rz8S*cMts?ic*^ zB}1Embvohtr^m90UrpC0uFQ1WdA`zr{<|>LQUg=F^6{Zwba-jMPfPbfKj+>u0gZ6l zSU9q(?6VU;3T-3YoCF2uezm2AY~)B7<#7D98kCEQsqJm~R;spV2qy|c`V|u*nvZl8 zN-hxu1aH&xDBm0GP4^!v8G;kFI3jn2uUD!k^L9S_itdEZPDUn0OE3%qN_FSw8l z;q?{XA;+&dXxHhBF2I?4Wtbi3AT!uAWPM4ti=LD_duB{s6i%_zQ)Mq$R!E=!Ue=CW zGht*3EH>ZFTq+_eIDmo61<>hT1UL8)K~>eqV_iIo;g_ zX$~?nF8#gT2fG=k?WtTCza@-+t1FrqlcG#xkdSxLa9j5fToC3VT>kX<(O)L~HOKUL zr@ehgW^CbmRe{jD(+z>!(Qx9Hy>v_0ZvVZ?!%Y$TkvZ$%9d>6-gozHopVbqd?(gQJDe#%t$3Zj|CnPW>Q4PWc-Y!mYf zAMN@jb5<}EFn)s&k_X~Mv1|c@p+^}(`|EJ)K2#plVeF7gc%O!CcOt<V{D53z0-|xom-kO2nmJxjF#o7lLECxfuEcwI<=hCKB6 z?=YGT8_FqV9poU@ts^?E)5*tz>-wiv*ImuZ_!j+5Sk;-l3M!nuSdv4I z6W_NpDi~b$`Ijm7S1Sn~!s_p6w7Xj1E1scfM?XF~Ub4nc&N5a@((q>Atw-L<>ijyi z*H5@45R3@wkp~TpN`HN>cc;SnTVIt}q7oFuAE!jeOIpeOH7zwsW*Lst@e)ps^O6&4 zm*}~rv}yTU3mLoxsgDWLtd5G3*R5n0Rakh%(>LaXvRkja<0S9) zsnc52wCGvk`x^e&(vJG`JyV(zZTC4z`p<%L5Y|$gS7OvAezvw93gs|5lOxn! zP{ZqUKmc86WkJ)E{?X63LXeAa0xQ6fdl`uoIIfx1 z+S2QrA5dbP4wX;2YiLSGB<-cIXIvsZ#&G@E>a1GiPx`T@46!354E)auD=Z#%N{I{J zwuIXR$MW}WLc*m-7hWGm!)4~jRnJ2d(5Es@vKe^0KkQzk!`t$ldX`XrqblE4Ob9`t zuWr-CLrZp$M7t`XSK5VV;O*nEqPASc3nmjVCoV|6s zh`NZNWNnquqBj_}|IL=C-by|@s&`1-5H}v#(zmQMe3O87eH$~&99E0(6NoQfjc$?F zWF!g~5#E;3ahJx-()1L%2>Gh!RW^3dPPkcH4ZCeZO5Nz98GRhj^L({fl|p4KC?eZ_ zH?h~UtffR(D^cRM3XW#03o%9iE0bzLN7hz*e1Hx0WW~JdoH<`9^LO5pE3G&kc2uU` zt$htsH}#C7EwhKrjfm83%_TJwrw8)-RgJpb46sf@X9ldWbOG9<6N*Y=M^>Z#XHW0D zwG}g5KUGpM7Alap|6*A_WStlh-F_Q$InS%E#YKYCHNG3Q88Lo1^aKh+5Z z=EU$yP9L%az6Z+T8woBt#+ONX$w{#2p)mRwAD(id!Mf_`vkc7cS}Zpc%7vEN!u-cR z7TVBn^y*QS5;nd!$Ypm47ljes#=MoNvGjZ@+>@@_T}g`WcYP1=B@^4yG{jwmibh)m zaEgP9Q1JbA+;Nt|Rgj7|vJKb{1ihDEQM#v=nywL&x#Zx+E~E4b#~Qtvd2AqAqHmBP zpV2(6FvdTuKq$SoPA_sFoGyB>s^yA3F_4dTguXqt&G)2DlfP( z`!{*>*kcK^X6&ULOu~e_N~EA~TS1n6_IPEQehewE z`;fHbKy}}ZKwCbj(%KhRb-VFHeX1)DI=6;Z+R2&|yEsTAZ@h9;iPc6KTuZ}ojP%pALgH3NseKW^%2@jYv#?i|M&13 zu!)86YFSxU_+CD((B3&RU7Gz-8+qHmT4bnF*6+2=n+b$Ka)B~&A}@b8ujyEio0ov2 z+f~&HF|gU2a(?@?s|?yg8g6F{HncAbU6LK4!q?r-AU3)UolU*MK#!?i@P8dvp+Nr|r6Je-a;c;gJ|hF_Xk(fB z_J6(N-jebfhsbCXyN^gU{JK3p*6R5*tmkzN=Apl6c+)k{fz0M`i5Lay`-I0Q*^+UF z_CF^uY?igdQ`~YfKJ6wvV%Q|3AvRvS%VeC|_`WV-b2%u|mZuE=-ebLtld#7CH^UyE zTg`O6nSkR&4N)E9mVKv}7s@+C!D+<$J+*GCguq=OnRK{U5><w|Ful9BD^Bn2xIArF1s;(v5*?0XHIhmtEU#*+wOl3P}Xy~pO2MhTTaZMrnRZ*cseG!JDL$qG~Mwf@Sr};v%Npvpz znhUfR*NA8LA2Gr)<=YvMv1JoW2X>5Bqi?)1Hqmr-yx50Rj-k|6yVL3J%0kdADZO7@ zb>m{G^H%RN0i`WV$OK4zbY?@p_G{6VGjK$JzWICkfRZtj-OCdX90d%T{f_^zc;#&e zQR~hJbF3wK?kSeGQ#KO^AVY7QZ_(SV-OFim8m#JDg2ylD;%1+Fi`|k<~E9 z=dhNXN#S^=$Q_I}GWRbgpF(U|2__ho%0?<|j-|3)OZ)AM)hy2jJpsXi(&~@TMk<9i ztX4=G;OpQi!|!P_$ltiC!fxJG-5%ap-g(nqfaNxAw$#2-)<@yhTQg~!(t}acx`(qB z?Q%$YiNCNFjI{O&FQz56Cx)G4VtO*^HNNIJ^AUfj*t@Y16dpfR6g0xv6HKP(x~5bx zrT})y&MIys{ss-AkQ%4i5E2(!l+3@f9DtZXW#qKt{#27CDV53D*)O9oE5b0*g%ZO* z%{6Hx5G+*2m)$6hx-!Pke{9<2!a$xp5<*s*U4;kht~&(s=){+6$90b8ajSkV^5M~q zr8Al~ow-hK_!*FYo-ZS3YeQCJOUaToBIr3+dAFp{=P1#@U>y6CZ9>X3IZ#vMAZ3tc z@S*!>&vZm)w?@NPl_fRN(8iK-HRnL(HJ4)ounqi$YwvlO)^@3O)AthCZB!RG|`r$6e^vDUH=IMrCHAB75!mk4m9B>2#x0-p z*0i(?;!XK!cOp?u%cD}cWy-U$dgW12lfxTv^i^8DVxMU9!2s?&U?*Ir^?Q^$s@RW9lhSG<@>&m-%OANGY#=E;Vh!d8{d`g?UH~ zn}gb}W+NnMyd~s~AAqxWyVZ?_m#CQOVZ&D*(s=~xF&+yNT<3BQY37M%WXWK~(tkld zP}63k=o%2#!qiZx*P_1mo9Sq#X?wWX9TNOv9UU)jYnf50 zO!;!*CC>p-uVr8q&2z#JmuDD9Y`q( zPp61hb8HGCo9B>8Re|?I{nf7W?g2vZ)h8Scxa+pht}aP z#`5I$8<&Y|x8!Tv)rM?2cHW`%TqD>FU;O+;-X(cvg^=X$Sr$+KH?E3+UG^qv?Lp<( z&087C`&ULk?<0@)S#-(}xZ^a&%au%P-nu#yO)S2|D!O*5mTcu#QaLx~sxOsFPV#X} zBEg4j$nE=1OkQLFdk!gkwxvv2a_4RRiX`u3s^FYtA?j6JS9|OPEw*-Qqk_=;)SJ6I zg6dq3v(S;MTWBpxGzB$_^M(5>=77PV5`dE07?dQP^{DPnaKbOY1w0=Mb0gvvh~ zW(muKkiXI405@})ZQ`7{pZIntIE4PPrlK2&L=~>&sMA}-(n%)Scm`sR_FrD^EyU+4 zQ1e!@5>99iMfitxb;}!LhhoCVC#Po=&B`#@TjNuU9xu47@~)N*z{2dSGr?9ROt*aR zddJ?t)5iRhUCik2UpU$Y06;tKC%W=e0MVH2#~4N1$e7P?xyJ+_bkq)EOpB!t z4ny-$i8R?YFjrCn&Mj(TisHGuabNExNK?dy+%Li~Xes-6RIv8QaPZ z23hkBtMtzQm?BV=Uh}kCc|NCnEbU+bW&WMxtIu&6&AqsM3UtucUEBBVHLFZVjTz>9b_5x@b{@!d*&KP72Rju|P--G#oynY2RzhO4p zU!GFLIL{nl{%c=_cVT`M4*{r)Cx2P$10iktPWv@N1H;~BfPPO`$^O-u-Sqc-+*g(iQKn{EF0&{Da z(mB&pYWMfs{Z+nuZ@;dSze${{*?U`Q@D&iR|1E!W5M?wu4*~77)Qn_6 z)h|HC0{aZ9;eY$g4|emTodgfm3X-UfT)#2?Iaehc@ZXmI*8kWba05}|gXJER_mQgK z@7!2xVyE45dxkc$XYgX-B;h2W12((a2uVtwA9pV{k`lPBC&ruJ?Yl=oIFKG2tgBQ8 ztnlV^!tO91fPyk8djMSYAFE;^kl9I_z{w7DCm(Xq`a#v66PlU=NWjE9{$-8K-({XoT$?kp|y9d zPRzN#@>RIqkoyKOD8CVAy~_wHU+!+#>6)pV7QD-;y}y6xj6v-KjPK2r{y)?y9o8t% z{1s_(j%hIp;pU8Gu6@z;KiQS37qWN3ST5k^7Mu72GaJdKesjvPlFjjwDI0#tQZ}S} z+a)Lu`3&VTyU#f&#+Ep7a<2=%Wpe_B^joC z@?IRfGRL!v4I{kpF4D<#D;W(W8-nlZc_@0#CgrYv29e8e!m9k#2}*Pj>&3QvSdb4P zL?w$wI4$QT8mxp>wc{Q*o}Nj)7KD)YSpuGFf?GnxMGktHn*k(Jj z4DVEV?eZ|YggmI*K76}_(Ax+Uyb%7YEB{R4rT3004SfDfv2F+Uv;b#x$>p4 zmk?qO`Q&SexIHR?Fv-vT%DcA7%P2#_V32QVAuCk`0s+wSAvH+s%hxUg{cN)axfdS| z?aD-ABghSF$smATpo@lW-rY;)HZyJf+mMafw{v$vXn>SZeZdlg>tJt=(a|3tiM3>C z>Y_S za)Y-ZOFIq9Hb=Tx(9V6B{Q5Hb42TZ|fozTiAz;bDt70 zs9Z;3PVuK79XT~n?LV4K+koEbA`eDr>X?K$Y6v~w%+iSxN%Zc(=TR9N9V1u8;<9JnV zI$pHNxq`ittUZJTN5+wE2(qJblyn6(izq)LGm&%p@h^C~=TVl4IY`)lXp1;fC`LH(}>7&siUMr z{X<4cZ%_)eb(w6>h}cy)K0m`g*0vTXKMWA9Ba+bLvO(A9kZB6W z`yBR8Klh^So1=d3Z?ofK?NG{USCEGNAjMQm78T{V?3E{@j(ymtZQ{J}bFaHI|(aaV7cu2)7=W z`G0+JuE=}wD&&fH9Q!IdSLQ%Fc^P=(teex_0*Ho^`aB{OG@I!yzo4={!Yupxz%nA2 z&9iuWd3Clg3vM>UXj8)MrTJ$tc=-2?QKnSpMt!^=u;KSSp~oZ3C3?O`N3hY8R45OSS|7+ibp5MT@3Zg@tG=? zR{oe)oS2d&Gf|xof>$TMga|`a4($O+>_8%gA~NS?)Coz-rQj2i2bR)L>`73hg)SRz%!G4^SYS826f*NkAPZX2{MG-)?pqdhUSc!Dx< zZMtqe)R5u0yUy{x9@-vPu8DI-i2N5Mg9bWBmr~X7qDy}2`Oj^eVmMQR_8ih3U@R3| zSu@7V>d?m%LdL-LXN|XmfsKIr^~TH(+438sw*lofm9VVLG^#;J3pm9mK^t)hIG=|# z+Z$)-ifczQ5nUJ}o~AIZ*<84@xw(kH0rl_td=uYj1Yf{CM6(SEhvQlePPx4vS z8&hK6K}LcopG(q&IOKu_ggK4{r-{H1o5Mcckz4neXx-{u*)dBLivnpjQM?8mf+05l za2yLsJI}62NBDw@aa^kP)ZGNsb`dQCM=}N zw5xRme6%~9^3F*p>SqxYHfz-3)Y06EU*sOrH$+MuWq2OJmL}Cfr4ggc;SeG4FURfB z`htIG-+p}_A!ajiF#P9ay{oVVM}KI&u80N4#xoj?v?W*3tsTP0&9^ZDP1{Nv0n4`S zxO1wO=zv9n*u#K;wJtO_P1x2Y-ni$h0^70-`1+ZbtJ*x6VUjL18cJIa1eBLl_3chf3^nU|8DNcU64v;Z^a$HAk2yJ`NN|} zrjwEJQaeB?P)dOYE!VM<`^O<%THG5h%x^8r-ptyvKnj%*L@Dbrp*yU9 zyBR7DN1{_-h3Ef_CTdZMl|EaLk^OJ1V56+4AmoT|W=h4)N;p73iM&LWqz2PcL$6dZ zogMl3sD({`Ldj<-(@mTj*h54!{hK}9yUEi4=;+pIf`f4s&`MJe>!<-ymTaEZ5ORQ> z2K?vWv;WW4C#I34@hox}OLiEz6?C_rfN+2KF$=GIt@~gt$#3wVNsDqnn#BXi+cfp4 zwl-aF!r(1f^I!QjjypR@!Fm0G5a<3CL|TQ$CU4yszZKg-~W6 zk`)Kn!LR*~O}5#0W<8=7g52i_0QLGk0Xth>Z$5DoPC(+_u>$d!)2z84B$ncaDY{>^Z8`q+rH*lr9!H}VUfFx$pFhWu4@mUjdN4ny zThYA96o8x=^h}fSV%Cp2)Q28uG&-gXHZoc*v|Gmr&O!f`TAEEm_>sG{?>_7*wG2nH z^Q&)g=*!r*ov<*f_Vc!-N#s$y5xQ4sF~#EG^pVU#e@)4dDJ0C&B`TnZ<$Z&Jw6!sA z=X;6MlL}hToo#tq97$hzdVw9%*Fz zvNAWoH~Zh*Kng;lLy4wgJ1SMO$uKrtcg4iCYc;Y^I3)v_f0RfrcKWwsl-j0kvP~+qnX~w4WH1ZwnBa zIDa3iW@f~7fGt>*VX(nG!p|GGITc(}3r7Pz6vpfas+PZ*O)Wsr%5PXga6t;L)M1ok zlBOOBcs=Z2j>|x`!%k>;ILd5WB;7R!j_=fhWt9)-o;EkK`NHlM%3RZ^1*AxhW9FbO zz+op<>Yo-zNQ+@bszQ~fMm{|FW6}#O$Q7>QlXKOn_M>A!nt5s_14NuCv*bibs~Un{ zg0q?`f>C~}eFw~-e* z3he()6thSczaY4|Mz*2|*4G94asJlFy%#RG1lfVW)j@d*0Z{g|QG&bc+kv15!I5xy z;jT|Fj(zG3IL1)^h4?ujaY@N@Dsu)@X>w3wBd^$((%`G)z~Ql;gnMg(19fw*KX-+B z3iQqnPx`%p>+J2Jd?2+eeP@dGLR{NQBbSTRpxZ$oMH?$R&SRQeXlQI&hRi#$6Q3GU zY4c?YEEfN}YS&C|1k1k&UcrDrR^A1)vx;BRG$`NX-)0lf3YcxrXmTgf0e-i$tduWc6o&k7FGXda?C2&YxPL>EEsJGbizN0+|T?RYXIL*|*4a);$`{gCP2S1gg)J*Gblb&<7W z8ezi56clW65J(~pV5M+O0ladK2^)!f`%#Jbqc_6m9(R+{-0lX9=hX^(7Bp*34V+Cp z_5t!G`<{Tge$4QX-?I@%W+MydHS8dVk>=;R>Ux!i0y&;3qy=irto+86;fudNc@Ftx z6?91*Nf0&4Nhk~xykyC1L5T)%(r`!O;_K$Jz1|`wZ)PMgA^>|GdEBb-c#^W$XSV4C zF;JW*neRBRJ?n$G|2M!?e6 zPwCWF91V^c$BE;C(rFyJF%9tNRCUmH9pF%NSIAZnX~9^CCIq(9JO@{4|!!Qth6|3`-}oP4x()10p+o2Jw_$3%MB1(Bt*0z7G0 z7fkW|#4XY+IH-rLs{QUmLx&+`2oI;f7iTx!m%VNT@I&$C)`Zy6moSX*GHNQY`0|Hz zOXJFF>{EA8+gmPzr~AJB-t8VFLdz}))HHvqzVNRGPltiUC(#>jH} zuCX`qWobqooLY6O0gHVEn%sXOP(4F{5I|87*X`I}X-dzdbh*+ja+9IH8-h|Zz_lY= zs{`??{k}Ti@+`!lF22tyX4DKR?fAO7wbN5|@V?1v^^rk>E`G;JWdJ!usv_21Anz3wcG z!h8FvsR5?#!a*RHUg|y(dHm8ls?G$hhHqIUv-x4QwbhLL6YBS@w`(%gyHi^np7XyYmm(ep=nCfQ^-zY*a1M zwg`)AQE_wT2>7Wu*rtCW3L0jRVL~3uIURwVA`a>9AbKx%8b5~)4Fp(vWnNyi+X(bx zUZL$5DMq?YnG7S}_LeN#$t9Gx+K4KE(A#eRwd!Kq9SV$z2aa`Ga_MZ{sUs+CH?BM7 z(GkP@M+~zv{mnB@(!HrE9DuFPwj`@$%dc7i#^#GA#C&1#i?4N?=q!&XO#}3yR~3gn z=Vm@AJKLEZ@AGN?2a>1r&>+yDl8S56AeK|{#xtexHleG%Wp2l8>d8CJ_Q^u?R3d^J zztBmMIDWPD9;5_6ruw`F48u-}ecTLuSj`*2P=$BpT6@73){_D-+>u_s^W6{v^Qm!H zrn?KL+=kp%CfYSXTYK1ZszZ-RO=Kjp62DKGG{{X~pQfZ$6}m0KVH7niRVhW;ls!xG z(zk9h*_CQz{(EqW8xQy5TGIQONvR)JDlYR1dP@BA5$jk9QM6T`Sz9%s(lc!}6?Dt> z)$Lc_@Ok3Y`?7W8%e~Sh+$A+0Zh!B6z*>{-!cdw!hd`8+yzv1^8?qWh>yusdqj{1v zj1x~0AMIEq_=&qpObzH`J`$Xml)Dn< zP3B|XS9KFoD8qWbE`FXUxAU-}M`0ME%cVr3ljHY?DFy-aFWO4;jK-pVxl-0=7KI7? z1UP1=J0j1GwmaKSp>{Y}ywa{MQmO_=I36rjIi}ekoF)lHP(WTCqxFPP@Ws#q zDzkYCQuQJEeyj#avNNi=Cpum|piW?t6Y@JrxIG?9cmwX!A@UF@Qy&)R;L79}wiT92 zX2Yn?Fs;okPxYw4N;-#?L@wNDxP^3jk(%{LrX&sF(B19yefcz&e16hFLEIYV^IH2< z#OALsRBx(H;1+4gx3E=Al|uJYFuo;7S1%8|3CLHQ9isMo^ z?ikt4t;WI#TgJtip#%?Bb=0O)-o>cm`;F5W+`l@PIYa z#y@3fqrzDi^<>?S_LDlEGU+a}-bgCb9o{o5bCNQQZQX|!SXu>wPnNEiY3R@|J=3_y zlhZaRr&ECoGKU|!r{Tvvo@ih4BB~LlZ$eT9DINm>_5O>Oi3tUkI@b9j9okfBBCXnO zVb(vPJGuCi;j{7EfP7I=N!ih=)3#=W?AV@1k)z5aw&VEFSlv%FAyLb2M2;h&_sCKt zIh>=LLJ!Q(KmH{tLO?IY(_)i1&X7%0ZiSJAL=lE)SS!P!O4755$sjORSxl8gb{c9^}22|B~S}ZAT^{a-`vm2Mm-17j5w2p8OW%TAn{oweL zIuIyXvsr|y*Jd%~xcviYJ!soZlQlUUSw|o1l}gzGWQ!r5r;_25hLCh6V^tGmqX3X3 z?+c!-tH@TVw)j$Yv+c>gy7QMCUoh)+Sl3_k|A{-LC*2z?S|ml@jQ)qlJ=MC5kx7f8 zl`HJjllg#-wCeyGcM$KVK{KZ}7mQSRrG>5MKp^CmP6NU=mrkFhKC9VIIj}Uff1d+H zYG_>j++Z#_k(+@NYAtNs|Bv2M!@((&=QML?a(;vbN*|CN!$4}eAiN(OsI! z_gtOr!qXH?>-*2%yKQ)FiO8%`7HNLVuxM#M#4v(mmT{f_?9FJ_cWrmHb;@ubJ}a8v ziHxj_BEh9J6o}m=X^J9`yW1&(gw^ADYMRxgV*S?ZHd`uM6B;A5Zc{xjd680)t_5{tj6pOSVIh5{H$RnG1+x97q=plW-f$j6{< zbRhi7&1GLUi#6E%7;Dq3GK|afHvoDWJ}9_g2<+V}pxocDR6BhyIuzjmB^Df#`3ok( z>|zUp@6>dr<$*>S))!pxeB;nr^mH@dq!0*B7PdI^Z&hBdNm8``EHFT6tt)^_w%&oK zYK#%%p%RRNg{ddcRj_GW^tP5|6F=al8xv4eRU*$Asd-mO_%PTL{dffVDN)E=VB3+a-?g6m=(^1f##S1z@288)|Dw>*Q@ahuO2Z07q(DxA6 zdt7Q{OY{pMl0V7SQ-CxqJWw;hp(%!^#1f6fUkGztKnF6F1MH$2GAi$OKUHT_F(;(C zCfBsMaV3CP%6cEy!m~lMKyK#D2O>Jq%iPR|)KevRZ`z)p<7n7RYWQlg=P#q4Q-MJ9 z0AMtdFEx1;8j_vnuAj|0Q#S+kXUT_0g_Xy+LNvFX+EUd@R+m75r*IZ56CDm4z^NKg znJduz{81FC24r|nbH4!LM425nfqV|X;1PXe^C z#_9JYn=5#)>nK*$?6rMtu9|A#wx6vSg;-9)b6f->7lnDJJd(7stjfQ`CjQYII*Xfa z;k$_d;dy?+F;fUq)7roLwcj5Y#P7C3&2OChF7hw{!tzs>K7F4wmSEi@mAg2O=R#1E zUg+27Q)9PE;z&i(U;$=o9I+(Dp#lgGSF)6D3r~ zp*(=%*VBXQWERKvLzlMCkfTTx2gUmo6B4crldRsh4rOm5YH-m-U#!1L9+9uBfjqphsTHh5K!s5#KVuoy@uwbCS< zIHXOO^gWm_C*j^*Ud%-vN`QEV8R_}le!e>}*=Cx$c{({0Yt1zaP zQVkDTH)g>H1_z9hc9XOgnD36$7dcjTnE~`ISdu#VFopTKFVF8FQ;)L5On%i-l`YXN zcm>^o3TXq1jgqq|Q+(t1~_03tQrlSvYiL z3P3YxJc3!Rhhmew8N!zZ2J7rF2n@PFz8@X%%x`^R>K)$$ab~^24*)Ux-~tJCl#OiE za=Q+9q(gnF9S;pHrp?)OOF6F?Ff;{c8R5PzZO&o!(U!CD($9 z0~VO|Ab3~Q7Abl!wMV6Y81evVxcj=1j|%(aB&|LNH8o8HQPxHI^9Q>g0^tVoWE}(p zsL123er`Nazj<0Ip}oqVadvREfR0)ISGoVvy`wyYiJ6TRaC;fyQP;+L)aE3Y+Uop= zpV?;$Fl zs-}556)v;7Q-Pmt*XY*ScnJJ5g;Vpv1>|YefWU5|%Opry7ah#T1S}@}Lo9u%u6PWF zw0zPi@~F*d;BhRqdclJUW27IJW7-_NSs)G%n3>v^w5+s$%M+?Poy%OsNct1A4F&F^ zdonlqQ-%||f#vjktt!+<8LQn@SUWPbRWj~G5CDsSCGzsyxgwM_bTcRQxp$CRAJDIm? zgKf}Fi2sX5aoby}2a*?-K0Z}0h2soQ1oFc%|K3;-=M+6%yRh_C09+%tU{B73aIth}uL1n%UP}ch6-}b-^(3-9Yxu{3q%Zb&EU->=fQ|AL+A~3Y^ zq&RriOGj|<1A1&Xzld>~S4?>+Q#dxQiykL5kj~y(>zmXt3O=(BHarT{K@f`oGT<;f ztTc2PCDNW?tVW4ht0@((Yux&jqB^h^MM^@(`-KD#vH!c22=xi2!8*U&2%Wk+!hyRD zcD2KA;h|eEgc~&luyr-bvo;|_hDM$D1OqS6682b7-HdIbB{erK_FqagP?6w~Sb{uiL%e|> zfE9QPCxRJrh?nAnbzdHjn6;8HSg*w;XCTV^Mx)iD8l5&~)wdP;ACdZ!SA|wJ_GS34PcSyC7_NNyKc+?i(7(ES# zh(r|dx+zRGJV$HyNZo`Z^$r9gRW{JM3c6BZ#~RDUs2V6J+srTY^3P1?JT z<7aGn?>~3Z*pZ-Cqo36{K0qH}OnF|Er+=d7TiQMP(1NG#n^RA4^~T2S$aJ7qV?EhK4EN?EjOyF?xpT;4Y&^VwVxT2S}>TSn$XcfhRi zp~|MCz?WHbZrYchd=doyx|>1ON6#>|QbcHAVT@|18Z(~7Jw%Y|KEE9@YxEFJQ}mZa8`~2cUAgcx6dhpN(+ZMpE^N3 znOj8szeAYy%0kF=Y(I*`);ijiyLbDBJs1`AVX^O`*CX5){BFPgyBCk#BO#f|bu}zL z0Y6xR5M7(I85W9C$ROAI)a@}i-HQD5dot{Z^DM*v`)MFW=~j1XH?q~Y{cZ1)w-yH8 zZ8~kEX)IV#q>UuU{vqJoA)Tz=eUzXu3GAC7itK8>zd=;TAi33o8Fr0Gjo-Ix=yQr7!CJl@{thQOY_m{n{`q$|2;0L z+M{`4&j=5Q7yDXz!Q~9|F7xQW5AzmaiG=8TZ}F7IU{3=E4;J!&8!;7aA#gr7KF?FX zyTof?WtH?eWpg%`IRh;fq*)W7yR`Ta8W64%vU$^U}D6 z>eXu+-cKWwAc-UVf8XZ%R4ekEv1C|i7@i>)CSv{(iTj;!U?UUjI+Nq_jgedMIy8?d4!IpokECaN=|GUbFb2H&?ZhW3@374vD z21|eiTpOY}bR-4RBmuetej;=yX@Xq2X=z?Yuxh_yrDl(V^QGUZj9=7Mcn=?{@V-vm z7rT<&XW1ma2j#dDr)v6J)3$dQz|K37)&?op5@>-p<{m?&Sh*!P%KHx~FH*w{~ugt=Wl;&Hu`S!|8wS9HbW|Ugn_tXwo2gFdr{SDf!jc>kW3W0#Nne7n^7K+BSe1xc&8oc;%Q%U@i5p> z+P^w3{hp|G+O(6`NV75?uuHC^agrtE&nC0d|pFGkf zZfUm{w;vLFg)Q=xB4OWnM)>H{no}529rK z+2)7Me;ocAIx7tLQhw5;W0}mpe^9T?ur-LhdhawmjgW#;e54rhzT-!RtvcR+2B5=7 z{{#kAwzW?he>$%~a*|FhHg+LG+jVGv?(8|#)HCadlLu3sK|k$= zBBKb}0$IRsQbGH*aPoBQSEjNQ@FS@{jEC-b@h{Di2nE1}v9YVBzXS9{E?GJV<#D)6 z$)-pjM9mr1@alpT(xy6(`_oLF9Iu?jb6UxaiNXncE zy+7~mM_6lU+LYCSa?~~I6W-_wAqjT7=)qL#-%}b~{&(|e95(k=EeBe!=D_BlN2mTT zWlAu9DxN0ZP*(Pp%O`9nlwImwO!+k1GC)eBvMxlLC&7t<9Ci!QN0h{{zR(-`_(A z7li7bPgH$2{2XdaYE=C`l*xT*?03_mS3f~Mm4T@H!^g+`XGC{N?|1vw5UqqlBO?!; zekxF9z&KKEjoj1n>-r6GKC&QL`ssRhC%A@-7HT{yS<$V)b3Ga)iX~ndi-ef1&gKjK zubwaL&$UWbk+V_5iS-fohCHw;jW&1%CZDdn*&TC_B_5CbJs^^D1Qc_X#bdWDpxL}6$xFIPUx&5> zT&!fPre)eLSG?gVsp!J{;KTg^<|=+z=Ou4NuW|#8EHsDFN9LB7@@7vfzWnk z&HBv-%)a^zr^mP+cnH*$E^f-!iq}@%TlI49D=?9v7Jqy)evV=DPouc|WbmDz#|&uf z=Rf`M>1$RR^3g29(P|3#h^3j?<{wGk_pk+MRw@FB7LA1Y^`EalT)Uj?^c`f+`7T31 z&fMRs+nY-XRqKj{vNLN$B&MHQ4BP#27XAlKF*k&ucR8jPb5YwmCDnCxlTtD?36ATW zcxj-qaIQPctw47kPUsOFoV^EH&2@GyMJl!<2;8e}zqDFWRlANFt)f_m*T@8|2-6c1 zMfk@dbP8^8YP}{Au)MJP#d;X(dVg=PGnI1a3s9-v@-v(hZeZ4#QVqj_IBwFCT?me4 zt2xRJFSfFAU1@s!-6x>{<4n+_FH(a{Ng0nk;DW9AMSiI+vW`k999k;7i-@JZ(5{9l z-P;jkSWUdbK)^;0Qfx&=>rY-*`_f>Q!|$QjaX~_L(-osswLeKf5*18HmR<4o)F|NH$bWOWUf4@ z)dT9Fa+(9q-%*IWG85A^hn_r+3a`B#h$2C7T;G1 z9=T*R?}h54V9U2T!G5N{BJo0ZOy`ZxpjSMM_p~hyMLuJM z`&$Je;n63-ggb z$ssR7YyW2`TiOO&#CqB}D3T5*)<+phJ+pRn+_RK30xFKS2Q8;rkB_by76Qm#tR{*q zZg4mh5}e18?#4t@A2sS(j#4wUdd92AS7T+1l1MBBh@1w3@Tek{CP$-G!f7 zDdLL-ZUXQO!3r6DIs5O8e;UG6h4(5t=HEG-t_Xz5oWJrX+hlvGr^-S#Lo#0TnMU~X zRzyo>aKY4%sFS|8oi|YeP7li67o#8s#fbgy(B6jZf+QVEDkTkfJI2oiBo#K!JWUoo~@KU4*lpWaM7cauzsuB{G_Raj@2+hMa9mEDr?hORhQbM^mPU8;{Dqb zUY^Y@G)OcvJkXuT;)D#VhXAiMX{_J2oY#vrHMvi9D3M&}sH_pK9a-hUW%1>sf^)?|^KpprCoh9R7kmcYzTXsP zikWDK1o`06&)dIrZ7`kI{t1hfDLE~NmN<6JCxKo5Egzw3g-*$he8vQ@!6)r(qf^CR z8MD0)d>Ua(u^b|EbcZUN+|vPY7C=^@P`g5i{=xDaKi=4$7fmyP)@(BK9~C>O1v()T{K{vLkiNoq5z3@+F6aF_6IH&9==|vxX3m6o3BNl#-+{qSGs!c3 z(Qkdsps&zYeS=|8Fy!;>`P1?A`V2&9=fLQwFYkL>kBG*J?n{rXf|*^`=L|9I8aB0V zq;L|E?o53IWu_@;;^DaBGOQZ4|{5a!^q&wIT+H1loOvVcApPf@mF zy713jo>JZ;?afLf}cz zLctLMseOT0dMb{7>Ef6%lytG2(TD0q>4(Q{oR;c&Wd+7rX^AW(Pfm?L1i6o?Yy@qg zljOraSG$MWBXH%dcoJac9} zmS0o1Vd05TteODqAZmAQ5+vEkw`mOek*t3Q$+`BPs-Vx%a_|X_5uy+`lFjJc|y?L)s8+dJWcj1R^(c0SXF{o`Fa*i}g1+ z+&H1Djljbv%p{Hj^JD7FQEm5U#Fxh?ZBuE zJ^UbmNq_kM8tB6#p}A-kU8!KXFHaNEZA_4=F|4z&57!%x5CBMnJYMI&Q1Jx#uk65e zF!BD>^F;#r-^hU>_5q?q1cgKd^qo1>U&VLF@TpB9#1tcFGm`7V2HHRc=u$sLxtu_~ z`mb>X>|L7M`v~#i!LDf56)$Glh~lC-fhU#S>IixEyoiGTL*O)Y$m;cuVExLBvXki| z%T;{q`D0R+rsTb+I54+)6zRhIbSM21izm8Axux@e_fNiVg2+IUHn90k^AV`lvLLp9 zqfgT?3rh1nP(o(TzMTy@`V8X~ij_Wlf)FEXn|gZ?K*yir4aY;^AN*7LyKAn`-#z3g z)Rrutzi-eSBDz-M^XDU>+s|qPxCQuP83-=m21rH)a`otiQ6M5+IEE*a%>f-U4ooTP`$K08 z`As5uFJAXmjOK~^aOrc{ zxp%VE8p3sam-XlDya9KMZwS7kNqw469~{~m2l6zRS2GdlDGQLh9~!(Bt3G+q(;u&t z^BY?8Y&ANzitdv6lG8g6hSd*=tNUZc zJ^5D*q>fxBiW{H3?Z4vRFAn2D7PM;YynC8SLaxwD2!+_9$A8z|446FM%n%|$A1K6V zr#G?NVE1~Gx|t`$h`wDY-R^CQZN+3+v+Jo9f(o|Pks4R&YEKCK;suHNqlDrz`hx|8 zW)eFN+^w!9*035ibBM-Hy?qv zg%HhXyYs`-GUkUp9wFyLGu4xYqs{7|XeN8C#=E2p0`Wx%UV^0Ry1Tp#0$+aUo)*(_ z$iHNtXe2s2JC##QvaVh7{MD#0(Np9~Q(QmQ@8|Z)IXcV{T|*(n9k` z2ys&&UQoq&G&|Iqp^#u9r0JfT z-g1p(^LH5lbcjxz`zU1GCT5pzMU0dg%$&({Ueza9D{L&bf>u1RIds;0)*kY@whmX( zYq4uXH-9o6;2oSfm;sp2QUaOfVT6d#s}Ty?lF7u-q}t?U(y`{5Pr1PwvN9^)&~IIr z%)c@G@oe*bY-NzG)i`baTEHY(^VJX;`slAR7eaK0Q_eQu4zZ)(!Sc>A-k;l>&Xl>} z@@8CK1^$CrYj?`oN^c4Wqv|vD<Ver?%v8agEf z5j>$b(V!=x3%HjAqAB}TituL?#!01=K9F!Wb&KzkCJ|azzg3hYWOq}0^@n*xD&v8X zUz%%s>hj{$*0%*)GvDdyuD-i0whQZ<;!uj@2vi0$o9M@VbLT2}vuM6j+=wi)-6xO& zMOzR0WFj<7Oya<2B%gnyp2BsS)TwX&iV@qTquG6`*6}*@osk@KslLLP5~JIBk}C#b zI_||3FpTN0(rQ2>Fk?uFWzscNNVfK7f^q^+89h2C%Edjc@eYL^BRt zN5vm~|7$KEu6wAs`OCTW_{)d9Z`UN^+;6waXw;0#i5gcU3kamU7V*f2hZ0V>d|^Q9ZnwkO7-Zp)~}o@*0rpY zcW&^nHp>v9LDDHz++~&RO`PCNzinWKd33%JrL#S*50dUNFc{@@JtPfjQFv- z!(yW5VhuJz0hura9A(T65!%i?;FN$m?H}cq-QBjJj4!7+WD99`GLs zyY^p22+<#Lc%ejp_cy98dh{Q)rzs8YSiSn?ud6iGvM2N6WUa`CgoeTG(yrE#qJzfV z+YSEdc3r*wp*Mu2w27w~H-CW*@d@H*l-GnJ?gcMo|Co&yOaKJ4PB-I4)IJjKbjB5? zM})gRkgCzT?o`OW^xo?5nU18>FU``vmGDJA9^@;!6iT&32!PT^@3t=|<9n;Up5f2= zgIQOqL|WIi*#&3Ot}EDq#CyJKHLCwIwJs96tTer$0j44RBdCYY%xJYi^w=ILI9%r` zT`^dBXx(UCwsM&%?06YF?j*5YWbFs|!3N}NlW-uPVPjaWSa0Kmfn7_b@}aCbkScPlVjH_Oaur!#GL5W-tW=$tvs4K1f2pbI0);?3-Jb6 z63!Q&AFkGNwc7E-&Ti$O1ze8$qUqXXe4$JP6kQ&+*C-*o3HalSTW8RqUyB4k1JdRi zkzo;emldlLr+{tXg&Mc3S>LQ*eg5fqAydfpD5UFCLVCm&Y!tnN#lwMp(=VqNCNaa) z16xwS*(in5)fZVw70|!Cx%qfZ=FZ#72jO;8Ef-*?OH)Cf47>zKIFD4OhBq#FcKWLh zBxv+HYMOS8d|W>ccYQIoDOG;5dAPPgwd4L`B#uid;aU9r>~x@g*LA<^D@eu(K4YN( z2x5a6L>sf#NPFQs5XcSf1$IO@W1jF{Lp{xU*Hpu(vVYnY$C_K#Lqfo{$`PUeHB*!K zpOKG7cE6uB8`yE0epAiVlAy`hUhI0(#=_RjH*Q(Vc*u4#giUr?9rD;+f;01|KjXSq z=bgz|(vu~^`nab8z=^>faO*%;lmAVd5p)pUt@NR4PssMFnt{{y#r~#6e99i(88kbf z51{qoy&>)dyg4y^6$gTv9P55{mW%fa`EvIX#baY?8rOj&^BYUqD~T0uLXRUySD zcdg}wDD6|c8gkxDh%Mif_7FOFRC4%IiJNz1**C{B%kH47f#VJDfXj~Ci;g=1meU_R z4D#rkY}s7KHyH4g!mr&&dfv0xZr@qxADS*96K;?`37VBiP}k{!u#M?ePDy82+#{c!2!x98=T@}!DiC`s_% zj?$kgS9qo4RtkWAO}ND;^fZP%gz-q1p{WrhtAnAT0`^ zcZQ=*6cjeiGcFG@6k_a~io~#d7ucdY|DKFHwzs>d#~zl@osa6KqmQ2^Lt-$v$L`U1 z#NYV0$sDe?!$T#W`O_*LmF>Q}h;Lymufsk5=CDr*0GB?Hp4E9wZ71iLz-2^MwK((jDNx%Wc88pmV zQCYc(9LnH!ho%y86BHLiht!Sy`-lrgEk{RNkVR3S(y+WRB6GC~F=~xRK{GIVE-0NE8f85xtlI1SZ zs*;-9fC-Z=q~Yc+{yKrM~?8XF+VF%+91WjM;Z# zxCrEHi7~(XqnXF&otA)$~@%x6f~RE)%8A7`*e%V9IWmi#LN4@Sl$1-fO(jmEDUzdy0Be+JnOw*zFF~R!Q3s!(o^4%J-4KAnXPkYirdhAE0TMR4)){GS>!ft%54vV z1YB3LFMgmdPMi&u@4CD`S3dtENhhL9&ig%0>O}IJ^`rMPbxIdTeaEky#gbg&`qy>T z_$G${hy9G$mfcVDl*=w)l7HL4 z&Kh!IyMbHnzOnyQlos6!%#cqc>%GWdxJ-ktjA~J7+*T}StIPKY>GKEe=hh5y@AeW# zG!`~-Xa$kh@LSPHf&tR$`ou$%zCqvB{<#Z+gg9YjBh+nGP;EXvg`2H7*$``|R!-JA zSLIr=KHvoJHsMOH$M)Zjh>X8?4i@+2k}ka1;<*wtf(#wh{U;VVUL%K1&aJmuW<|cy zpN7>Cbs~P%q1MK#+VYs%|F8IY?$ZkCb&u*9NMGzDMghmhzA^0Rf3NnQC9RAP+rR8AXYUCLuK`1UJK0s)YEf2aV0;^ z`0!P*Fb`CCUEO#}ASHjxJw*(M8=G)KJW9rKgy6uk;P|y2`Lz>0PcCvsiG%v1=IQ3B z{vChIifzx(_vn>3yj7nG(6DjNwCaa}GG&l&irwM5BxApCJl%oKfKPN%LYYP8uin9y zjZSAr-ISf7ccSEb<>QOUaLLp0pi27eEn{?b3=_kb6f5Q$HG7KD%dAs(>+%iEM&vux z-zLo4cg&P5+B(!TsLUQY+?Rx_S!^rnl}`ND-*|~R1l+$quXz46ijz7Y%Z<5dTW&h! zqN$=ZZ2nE*jCj!H%}82ye%Yr3ZG|7|ODay|DFHsyN0Uj;M0os@3Uq?ffS>hYJV7HJ z%ss`vQT>f!-o~OnXJ8b)*qFi~ej3SsNI8|3gw(rkhDS~jy%Hhf)_Z;*$7KEX7H_gU!(9MJIl?83{`Kk0KKGU#i6=B zlxc6??CB3`DNeE-YILl_b%yGECZ5jHkuR5(C`Ii(hzGFs>+5bz5@C`l{qn!id%_Z- zG|)q&_WzR%!HV7)YmFEC2HOiJ&)PpI;ICqCiq4jXeiXDKVg03gMHNFd96P;)^)32( z$-RRh88KO`^_94bEea{Bem$n;xZ4R%dpKg?-mcFT_d2dV#l#5blQ{DIY(sZ& z{2gNA#;4yLseQyvT2xrwF~GkwQSg;M(qoUfRii2@RX*V!s66WSoY7N=gM3y4^d1o0 z`R{f>8wIyBKbf+Vn)vtQz!*ozJ%{Mw!%Az&34?-Ttj7Bgp9y(G8Q(&WBerCg<%)Jb z$%Z@eW*Q_Cm>XbAt|bkdFEw=83`B7=eVYX*SFx?A)&l*BppNVPCvNV#og-5)Cf5oC zWUxC@o{o<;aKUg_ez*6^Jb*%_B~IrOrPDY zU1VmS7_E4Cvv&P&;7OLccJ8OebN)ehquoxd8hO`0LCN~_(kjfK^_Uq^YPv5l zgq|;?1P@;*9app&p}o%pV>>cnOJ17yngvJ?P4w#Y=AZXM?g6Q-;jsBQ%k$@onbTzp zWuxLUviF>E=<^Yvc9bsk{&DtFvZhM53x&(_8K789zh-qGs>Ew0!go8-hxKobG%+Vrr@AR)23(rfZO$jz4{;uSSs4;C^p}hj<9}Qyb0H`71A&FprLO#2?YceI_3Et> zRF$6bCtrlv7?r%R6IzYvFvo`m-RS&RR@2WjtMJaS<`1Qfq4 zijcNvkIvl=qP}DB&Sm5=R%2->4FzYj@&`{DngEN&PL;bjatG^}OQ)00w@26c&AW&gfBv@om;LmqRDktuO8zQ?-XVKw{j9=e{2RK7owqd`t46 z+fIfcrG?b$$F)AN^acIUS|oQWgq>sa6sm_*OM@gvgZuRgDnc^OwI@XbhC`rv@dy^# zrN4OOr~SbkgaHF=-{jr#6ZDTw&{F*G!zN`LAJw0|I#k=1;-7s77glV}+Ly^d$)+?f zOP()j^RbM8Q-&Q?$lH-ab;}G=OD&j(?v=eOo2MWiSg($k5}BGG$f1$oO+Hn>1I0h{ ze^}l+gUA?y_!sym<7?D$Txg9K-S)@a9yb=q%~pT>i@;*zn7Vu?hK-`i7y_3##)yfD zmD`0}Hm1nrzf9gDr0Sl3Oca^|%tE2-j`gI@6ljXU$ps)`@~JQXboxlOQ)t?%W`hsW z=Tc?8;1MO}HdNrxg%p_kxG(g~@RTD2ZdKb`WL}Dr3DwLEXdr3jp4Ut$sZcbP%TJHA ztTR_)>e@(BUdTE?DUR^tQip|18uLGg3S-*CaeN97_Kv%m<7uREWLU&ga@HJkOUOMLl*#9_68XyITEtt;N5YtkZ#k zqht%ws(x%%TK3qRra_z+mZo!$i2NnIAPM?W1RA#k>xaY&PKgU3T)GJ=Y-1!UGxa;g zHSOyXOtGo76^h2Q?A$>gNuxI9Lqw=I$DAfEIp3eSlzol)W3290+raC%;zZQqv2~1* z;aLyLQu?HsLrm6n|CeYPrMk;;DBW=!eKs`)@!MWr*j{hvMUIXi(7k{mbDVPOO4D*V zyKshf;Em2p0qz#h#mg+68JwsCOsQj>4Ms!{t+2FvQ6TFaIL+wBG9480?Us3G3lMuh z&s2dQN*#i{irsjXI8s0n4emThBf4=4;_ARzs0~FOn$(fhdB2q3b+LB5GcqE#OL8!^ zHH+t8Ks%n zOhs}`_4ZQlqBQ;8Mot6y0EGm$P)7TahnmdDc!yFL3JN3A_Cop%TovLrX?P8W0CX4w z>@2PCKr{8KM#l#$hFod26I%I<6!WmYX=V0JzP4LO6{fEHE4y7bZBmMLD@DMnLOeC( zp)Hj}h#oT6OSdBP6#a!~EMed+y@*3gnc)3WP=@C86AZsX92W7QXU+0rgTxjDc>C|| zH5UMXs=+N1!7tC#IyUu)cqQRJqJfRn*%xGccj&AiCz-6GtB1whH@Si zo_*21u|6ShS?;*I*>w=rAEXx-In>*Qnf=~B_3py8@RdI>L>@LiPz1bFT85JKA5RQa z?Vmb&eJ#vmKw@63PhlqiOmY5~DIZ^RfglkwVt1thp^JX@Gc#$%02m?K`s++yUTk2C zBgNKvJ3H|yTd#kaXNA6#C*Q%}`6X`4Ah9l&BAeZb?$?7o>Kw0`oJt(fouc1>%*5DPD3=o3X*qd&LOMd()AD;Ieb(|T6 zvz)H7b-nbS6V7iOkEdOEbCM3vy&7MGh}~R(j>S^|)`X20W*x)EdweYp9_mk{$rWO%S8;&|48)f5$ zj7A_ZFg>$1+%d+4=dD=$E%T6nNpt7dZ~wC!I1r){+f)T)!BJkme0jzOU)+Y#{*la!b~e?xx*Z0>`g zn*xC+5v?cb)Io@W2bb#3Fz(ul!6Qx$Gxhcy7C+&GYQCmVihAzb_uP-FHl&Ijy!_%t zVoWkR+u93v#~8}@yv|E``WmUh-+^lW6gxByI=QD5Gk(K*?)#hJh>N+bWP^eaMgmF4 zNhfyGI{z0leV01GbDVq{$*_W0wQPvne$IIYvs-fB5mDcRvOXJ$vjXjoI*jtZ-v^44d7>+CL1uo=a+%=#_a)s4t-1}gy`ehjcUf0 zn$S=GK!ZUwE@cmcLh+-Wfk!-N#JeODaQ3_{RFm}Qs!hmVSk+gpx+; z?+3jG6_Q+S+7+*VexPpa$J|{f_k#zg+U<9L}QlSgB9=-LPBRHk{a%kvaS zNu}1A2Pd3!gy-~AgVIw^Gh?PJNMd__YvxXL!=9?#uTKQep*=7E&ibQ4T(PS*+7JdXFw!J1%ih`^7qX+bnMcz;t|Lr)@X6ve{)iI!zI95qva4xPh! z&JeVUZ&i>y?>-=mM<0EQ_XG}4bkb+4#;mtfzBWUG1yx7!X(wbTa+K#qh1*p`0A^Jy z2kBd}BIQy~O~mb-oW|F#t`_hLC^|6H@<)$6aeSXr_?3L9v^Qb{I)Ln5n7Mu`9odWUfEUx@FN2MN>PlLtcpe%WfmYJ3*EYR1+nEcWBzPd}o; zhec-MbGjFL$hP?_N>lrl<|oNAo&JMLQ?(`@wnX^$HN7B>&p3HJ`E{4UT#7;_=|0vd zfS0TYz)egIyZv}4~UFq26&PBinj+$NqG8peT(EAr+H+qj?}~y2Xof-23O<=NM}dcYTmryzFYMEKLi7~7tF zsmojz%S){c$Dbi(KoeeW`swommp5B0%e!w=k1kv*qXjWP2`_^9(t!>I33DKz1za&2 zj-D(EL>9ykK1b)BVJ9;H6AB;Ko<=-&T>4+t{>7VX=4*t~Nwt4I{Tp0krjS39orH9` z1I5pxbz0BapCsC?fe5k%2`5Cf(va?gahD2;l;jJbkB~CCpAW@crm#I94cSa(E!K52 zq*hNN2LA@_X>u^=zndTyS01y2F@#2RU;&5*8*z40$xN5aAp-#VulFmOVlAeGcdu-L z`l=0Fr+v|QbM9~Y1q$)L!~B`wzRlj-abIO+^GYc5ZPkef=*u6WDqibuQ9IwnND?y` zcBXk{>~5#y9zW#7h&)@i2OQ)O757tPYoyk(Xorg{XoEGHB%$j1`%8k+epFw-q0#ct z02)corq1)f;9+E=7kAS-pQRMd1ODK3hTSGQ&oWN+?0<%T$^oxtlH=b>9|aZM4PulI zmFMt$4Y=o;l&Xj-!&XK5K<~(AC5_k82kQ1p{5txMlmKSJ?S#b}fF|*A`PJ_p-c8Iu zCq@f8&U_N8^^`j%uzgJ-%3AyKBy$#$O0QM_Yn{SBHZq_D%r0d)zMj;*c!8O4X0_J% z)EwSGmMYu)Pu2gI3G3&eM(g2fz@0s*trRRJW2W6B%^@v3E=B(UhcEdd^4Lu_y}> zE$(}a6HTp=j*UF(TE?g#UjHX*QF1(I(3#ykALJ{-xmgDei%Y`ThMVjuZ5%||%ho)p z1ZO>R{7@}82)Dqn6^Em4nd@vH+NC2vv9UQ>WcDy&dn&tXS+d3%3WNt#Q%QdaQkvQ>vDLl6QIcKS=d+@owTRW3*WA(An zCrsQ+$<9kM1JmyJIpP*XC0K`Pb^0|j^x{}yKzp$IhLK6vEHkLzQ3_EA;f zU1Jn2uRj_a)IYHCzL}u_I>bKy@r&*N4%iGi`zT$+>Yc|f1rA>mBL1>^#`EB_(y9}& zeXfzC45?VX^fvZ*j@x%cUTcEZpSk%)rJO0`W~PM12e5#|Z&2{!9wcCE_%pYL)2VcLHUF957G<^b??=7-M2YkGPS^gKC7YVX z-X$mw9ZzD|Q5(z|`J6pU%!Zv;;R?&z1E;VvCOUq^SQ5p&%+v7|?F*Pg z0Zri_Q?c`fu~x#Tfgr74ON%xD33(O1zS$ZP;be9YycR`J>5d&O2TS~n(IiSL*h5_k z#iKej7FJmo2jj;L0;ONCPKV}z-CyXPd$%fBRBvD00SXrhm!jr?IZuXQ2(YIX8M{Sq zpPUqku)@8Q43`NyAhT$;DJ%u#ehdf>&|X? zUP+peOu?)}`_-S9>E@Sn^;O-A6rQ3muJKc+`h|dZ5IyvrGa!I^ruy!xF_|vK+~eP$ z_ne(mY!l9gQjm<_+JVd(wU2V-=kW>dlVK|)OzhFV?t7s7b7G`LD;77DU|r_h1{_fs_P@wNR+Fn zlOhu38nA?j<^B=g_f8;JmD}@ZD|(;C|2d4YxT!P}>h4FJ?M{6<9>Tz2C}G4=WV+>6 zZi51$tLbsv&z!0Vnh4f#)O}SM;W6U;*PS`n*Ltpp#i|-8i@D=?|6H9PIruwYt&3XQ z41$_XDdY|l{<{x3h`_oMtqyDY>QsM zJMED%S7_2q#^}lD6E6^c9{Z(B{NUl1$W4gMkhre(AV2Q0mK`4j0My`Inmy>U-6)Vv zULrtQ*S6sn{wgrO_hg%{U|2UUY``V!xxgg5`!to;vdoQzG&zAoqV3heDO#}6(R>Lz z%_LB&(3Z=2G&F;I+LJh|G1T_?xUd1BNga9K6|%QOBgpG~ zdo1fq|6b%*mG4iPgEA_)nFr&U;xCClyJu0c=G6MJI#I6z8{Qm@PuupoZ^gLrHB?h> zfy=eNHde20tb)O8#lC}}Mxv({BE0Ws0P2pU>b^~Ap&FuFjWIgyPC9vKXW<@Lq#tr` z9f9Y0<5nZY6U;*5%h)rs-^Uy57+M_-W0VpbAa5Q#W41DBF;(}r#xSFNfoen3fGcF3;VY36Ox!_oqh$X@ z8U=!WITY_T8CZuW&}594-eaQly`7&SnlG2NxiA?`06fh$=YGUaHp&>8eF7728{pRv zY4c=x@BT!3YpTntpVikry_)e5Inf=N+eAu#n)a?StX|wrd~a&-Td$Dk`SIq7Lc*0N z{;#cSA|5-)WBzr%`-iJvP~0+dzdY<&ovOgvKfVi~@N+n*c1aw7=mKl9<*!_rG?9GQ zhZ=z5`jIQOpnCfuOio3RfmnZotXhjc<07FSy2UjxoUU-Cc(aVm*@1Zprs+!5GcqOP z$Agq$?e~g%!k8^lRI21<635RKR>X%;v z7)Uv88Ryc&7-U#g08rrdhpgHC8x~uI>Jv|-o}YX;C;xiU^LylHMRI3mBVz;{^ZUYk zqnz+kw4RM$UIk0r@8drIgkDE(=7JoW$cDcU2Cd0=!sm+PDKm8L2dS52HgLtssQYyP zTsIzy!GYQ06h^bh*h0i(ppN7QwZxBks>{)3EhE;^PNjM4Eej^_BJ1c4K>zzzSc`$=f>{)^QQCNxYq}jwludhv$dP^O?}It zI0@AQeo*h1@PC=cchv&?Ej~>%g@-}r*S%ZIvBgu!}zRXu{kV}oS zk0dgNdPBa#a;tGjirD*I`HsrawLX+}06mW1jHB=1W6o6_7#s>t>S+j|Yx-3KagR!% zdqM8z6pZx%lgcbe6*!QXNJ5H*4cezSwsl&<>n?eDC|G24`dwl>ykc{)q!{Nn=_=R8I>@Ej$^Q@gU01oys=rVA0uuX8 z^1~}3#!wM(xWo~|T0Xklh(i36zNlkQ7bYs`bc8cmJOux)!7`lpB1+gp6Sk<@TEV6& zXW+i2{&IeYAHb1UWLTsc@HoCgc6gZ6pfnlm#U?!=@QDLn!8`s0oj_p?|HUSzzMgSZ z%Zy8wvS*NCNs^zHpuW>01LoVdIi_;KD^=TGwkP*!ZGL-Jv;BoyR>t25Q-U_xHA>bg z5KlE3u($#uM9~7W1%!l1#Ak#|4w8qY*s~5by^y@kh(C2xs6f0XLZA>S zoHsx@)s{Y&zKHE;PldD>17xOJ^IgdDbS4dxNo*$5pfobM>XNHd3}ZsIKLCdNfn^F} z@edF0JjN{g&GX9xq@7Iv7WMI)3gz1sZabMT;bj2tC)+(o{q6XLbJq|HiV*RhqSKM% zShg^xoAA?Q&nU(<^fqByN>=eR&rpAmn@IN^{zJvyIOd4E=9>A%VlZlB#SgYztw&>k z%mY_+XKWDB_x^s~U3G@&K1q|lio(-=2sc;Yj zPF_-Q_zOI}?4Qp(!muVHn7V@JC{u%T>}5BX=tZZ+t!}LKu)d2udL!33{D}xX5Oq8H zjEU%dA#$Zyw`r92juUIj71wKU4#YD@K(XnUz%~xzA22-oCI>58~8l(D7XAWAluQ*M$}F|Y#nW-cjTJ##+721rztY2IqhA{6>U2+#uz3X2ElLb18kAe zM;v1q(4syf^@bDxHUtAKfl>;tm0jmcrU)Stj05kJP^ySx8OeJ=e@dncn7lRy=``Td zN0bPlUj*d`XVazA9rY1H?lb1~AN7`waK^1NxW`uCY+Y9panXuw+TSjLe4&YeyN=5} z-t?ChTUzA(aFDWGZlxn_> zIVwJXFj)b-40}-0P*Q`BGcPCu8MvE3Dv&h{JtEIh5JXi6YgT;9L*ySOsUdYBOBMi| z9)`5-^hpoeZz@CfJqL@92$oHh;0QF9FSs`2o8zMeXQQNilwP#@oHOB-$#3g^ZQDdYvKU ztjecjAS*C9Yw(QN|LDx0xX&PV5iNvq$~!osAR)#3eBBVTTaeQN^b|GVaC|WNUE$%U z#^GtZsmcsc4eo9**oR2a9(WeILEqsHHGNQ2R!g^WSv0cI*MFJ&B>}KVKLF3J_(HM! z{MA~Mxw!}wJ)oEY;lQ8~EJ8DDUiGUa^6}TmyEV6%__d5|v+w>uaVQT?H&%b)O;wLm z%eu*cc+78=X}PpsgqYAEcC0qj=eam}Mj51M-v?V+j^|&;vXiM?nO)y)osUns2HTgw zKbcJ}S?fPjTv8LvGXRdDquy?U8}^6?2r6M5m>iWpG-70Ufvoo)#@xPVp0ZUpB`-o@_&hX98V>a)~}=I$*|fe>QM^|7%; zPY_?o+Djxs(mqxBFINSd*XDOhA@4)N&v{?Q9xBHvJ6=CrR2FDT$Tw=^>0j!Da??nT zXStfxiNuFH5QX^NYaaXgQ~Jx`B8Yu<2~8HsU(!KTYYlX9#n&9}D;$}vQjQ5|# zgn54y;JyYJH0rI#FmR_o)Xo*Y5%&ZsnB@QY>9m(BpcyoDDu3{o7&2CGTDvPhvEf3F zqpH4)b*schOt$*CRoLDB(_KpUDMv^e^iq7SH!qAy4-lvYFiZIe8f1tzgIB~fItUpb6f*nXC6A6! zS@k38h|1vZG_*g^w@U$QJL|GaB!0-~M1>wT{e|sBj^+d(4}6SpNDt&N`q|%uyYIXp zOSo;UvH0?LqQ~1ncjFj(y)CtzIJ;3NWf8aLd>{8ZwoMVuIFDpD7$Mi`$B|L7yUuD; ze)1zy9xILgef^P%TdTIo-)PRf(w?nSN`$+o@*Us!d~-iFskWCQ;>xFjq(AE)_)W-- zvCAtxoN4yvU%lr}iNlm`!-6CVZwI%PQ;gQoxN^|J#spY=Q!8PRvil$JPlshnim%CG z{BQZ}ccK#*{*%}n&IWzNiBfj(VKn40r_V)J#Bwy@Fe$~dgfuySkZ98$m#?PSfo{jh z^i|KxK0isMC@4_BhqAITG!@4m!Xk=`+1+mt$x^_!^T#Q#sGECb-pbC zJudxu;S*rl7>sf0U{C)6PF7j`Ihgf+{jZ>esu47?0?$s~+d1UEX1+!EP4siJWCOT& z>0s=#E?Y4%lrv4_Dv;C}=>d{epEIQQ#p}xqxhvTie`N8_FQWYzNaNlh($(^b{JtK> z&GFK1SSHf&BeqpUc^Vb1&L#)2ftPw71z2UpGu{?7@9TB*p%8({c7xLb+Kj zUXQh2<>UMwyY`thoGs-DK$*^l+E`kwR`G`x?r&ZIM~1n?z&25Q<-=~p-{j^jjeUaejB!6Fqq zqX(RWvxDaxMukE8GvZ<=GaeMw{eOKT#3rBL1R@Y~D9+ixsQHxRcNmM!yn;XP+RYz* zOTpIsg5gv%2-&=!2C_tI%7VC*9?#>fS9a3rPo|t1;4n3TH~qzUvT~6H)oZeMAWXR?omnHUA8=Nx+l9b zQ_y;BHNnsdK>hRe$x_&kHB@4tZcMivOfc+U2;(n|JF7ln0L6Xe1)(8OaU)F`nd_HqAR2T{KX35q~Jse3B;tlHjyVxZHw6N8lV)N)TIQP!3HVGs0A0t{;+Jw)9HAM}KbB$nG&eEa<#`eAvnU4Uiv0Vd9jjNQ%OG~s?!Wx(TYpAJbBumD^c&>&efQmQ zTD8sn)`6#eyTiu&`(;3Vb$4Dc|{O{rF^K>y!JW9_dCV~=D5lypRaVY$=t_ zzBFo?iX4w%?bpk2?o3=zW8}z00}X)}Bh(4lhKHC**gr6ku!f)h@%h-TgC7>Q^nTJa z1PaFtax^QV_cc{7Z+BH!LQdPWQQ4-07mQ*XDw^E7BT6aoLxf7ntK_^tU31NbyE75`5><7LNqlVg-Wc^MU+j*T>)QYn9>`&qAsv~T%jtnUrZ1VE4GuA;QYbN;+)^c$uS z%0lM3hW`trIB$>`9wj9f#tGv>0z%@8`w$(P`B!LBI8z9WXo9uK6nyS za@-S?ctl1~?Y&R=e&Gj6dL8l9Qnu;Iwqg!*@tAz==lcqP3*Id?)I9x5%Kp$z4X?Fx zc~(`crX;nUL;1if?xyJ*3@Z#jMpE?MbqFPFAxCh3|78G{kZzC~YCw+VdyUgC_nhcF z6@**mAL9ohOHd?%x&Z62KS5da%+X}$^@oov`{Ng~p+WcgUZ@l@1a?|k-xWchT<>Il zu#@;@;3G*`*JUiuh`vw2zZSC6+YR!jt>Bdk($JcYVEHFfyM1vt8->rC*wGH2wXTj+ zzuYOZKtHjM-W(rZr}s6@n(${FHy8=g#SaIzzysaGb$VrzjH z_b|(${<3R&iHh+Q1A@}`tMsGW7oy&|efn!>*5CM!qpjPS-@j^Syc-yKbu*EN zgfD+`piFM6>+kG8#?c}_8^3y;(eK?-WjT+SZE$79&9r@a!{$-y^}VBH%^!ENuB^Q7 z{7EvOjhopUpT)E)=W_W0HOk`J%CF7?$r07BQ#eeQ2S)e)WM7xE@et}>_Bo{=Tv5j4 zd#%D06c$hX0VhW!AO9yMgr=dqi>aUFb>>kr-gLqc%_*tM`GkZE!-Ltl3}zjH*^ulTYzc7S7*^W}Q7%h+sH7+G?mdKxWtx9kIK z^6WdWHhpvHVyfVpzsxCldw;PebrmJ8i@E>K z@ScAUZL3n{i<*Yuvjs}(6$AH`)<`h}xNbo{JM!=pRmRqvsA;B_et)#zC6LII+F-P+ zMeO~x!h`nvnk?m+2```^aw6cd``T-4Kv|%{zb-FWdgXe$H^&A%a|;g|~V> zp*KVy88Ge9-oSf^70NGJgX)2+q*5C66P+q<)j5wBzrSkq*4I^lIuBHlxIH?r+vVis z*pV?RV!sppo)T;Wp7eHvM7Wv7&Hwf*^gVi>L_{b@V{r3@`XyaFt#JmS-`9MXetzjW ze8oPn^qVUWhsn?n$FeQ^UWz=s@`lI8>2ENn+&KHZ*IE2|l~kGv^pck9+A~Hleq|jF z=f}7q1)*oA4sU~RmD1RZ2ro^VS}~)JS@)J=>^eT}AF*0G4VhF55J>(ydn_JwH0a(M z;FTf$+~haF{f}TS3}#)=W~VqWC&Cx6qo?p_|&qiGgO{i{zxs9{K z#a!)2jP;&jg;9au&0It;z{{RO;VTGEWFmOfs%};d^i1S~cQCj?X!w1^$4G}IfXs6- zQ#Acukk)pj$+W0scro*)JEmZMZ@G_d`&R_dtAH2*8I0DHYhGs(m|oD}Sia%Sq`IGj z>ALyGQGCfky|_Z3gOLJ<=0N?~+b*vcflG(myX5U?5Fk@%5NB%ZhmH}5rHab+==x4v zQT|z7_2qlAVw{AS36EDI4zN#uD3Oo-H!I0#&L8{yNq~*VweJcgzb04jDQD#}{cNZC;2zQc%!l)C;_MPH4&d>t1A zJIEscTML1ycVtq-@*RvXM3C6Re<>=nImoX@pVZ{GFgz55+cSMAXCy|JMX-w1ubk!O zFg8x#5jPv#??y++f?}$VZuoIPlQI)K=GqNl-|fE7zxq+*$wiRAH;&Up!?EjWW0GpP zYEQrUQi-0=lpDb zvbiGQM~3UR`{9pGv~T8-SlwaEVe_QfCSv~A2jW7d<&DB=dE8Hz{iug+uV=P+jOoyo z;YJz?5#*UR+0_>#nho^mn4 z1YGh1ItREpxS;}j1cHwfRKJm{(r+}xt=-{nqJz~elE)N4iW zM910|nnhH++Gd(&YSZt0O&5Ju%}BmFQMHn$-oEWmp?-lL(gbDCDv6~`)o5R-HVJ)= z<-5p~gQe7lsov9MPCQAw!W|qY5z*CdsZ>~6*2NiE!qIO5#B(pD(k4Na@eO#KL z9(b#T7aL**y(+x^KMf-0=Zu9CrG;%^by9o&7?4*Ovn>+6P2 z!KB<(Z;e?CXh=Gi-FWrLPHw#Rx$bPLlCDLWwRV|7g~JhVlxnWP13okAtQe647spNGp?TFi^_aikd!!ff6+zU zsVmpt&_oh_eZT#zoLxRiZ>@C&JuSfR21iG$vDkMPB-&EHfE};{?dBSos@)s(hz5@J|Q&p@jzhBb*1_>y!W_a=?Kii%b1<~A_TpW*?naq`IHCj z+QoT-R@&T#vX!<&59s0>YC^+WCSN2s#rqd0}c~@y4b&# zjYSt^2*D2nKDkShz1;s8uj^(_;q!8*S{7v!ykSEUC595=ONx*5*sJnJ_2q7s%t`#7+O|=)n+9LsE+Q@KU>sm4*G8|P*t*lzgHqoXUOdxCL8XcP zZJS>)FVd@)i}^OobvVr8K8o_J!Fm|Rw(C7kv{FpqCav^0qAJL~*}EiRIJ0EtC@?)V zL*DML$c1=gi`f1em0HTXkB3KQv&`KAy?McktHZ=;@ltzFxuK^=WSTNe{-*NsOZ{Bg zTiRm9LvM*bXM~-!eL$V3s5;?+B6y03$d1@YZnT@l?kd&1jJq=1$QfnA*4Ad*`SO&; z)g`_Gqd?ruisr>CKgAo!kqJ3E6yRxIGM~J0gB(gg5h??jz!%XnRXar?;S;7j?Kd<( z;%1)K*N@nE&W)OItug2m`h*rtauEBBe=J~W{&B-ucqQqZAQ09j%_=IF`cGuRZOK@|{Y_MR*G;)^Au6*bb`mWXe}LJUis^ z$`w03xk!#*_$eg&>D!ccv2N{S8{Pa3n)*57_93uOFzpf9BSvk1swwoWgFFnVzZMOL}=UYuFpbf|FT=a1!K`PEU7vm&%Iv zeCvNc`Kizs@nvtJsPngIYeA54ljmEp+#tibC9rSxPddirq>` zW9nb&#iB_)<#gv*kX-atRi~eYQ(OC}vl@!52i{lsr-YSFh@u%ib4mTrU6D6}62b;T ziZY!e=MQVe>i5tqU(Wj|$y&-8s)f-8?^Wnh^0MfiBhPG#|9NMiw=DY5bP}D{^|pN6 zbxJ-KdSx_u#k$vB%F{RLgkboP@+xm8=5VXuKgi2%8?fB}`i+$6bMk|)%sc6K{4BcA zPxYH#+!^wp*rNHu(pT{MVDPU4_Q^Y+E1qrZtO=@*TA;+JAB#3=tPoX;I@k^gnS zhDw)qll^J`G*FEihw1E?6RExDfRnifyY1WSH}@HP>^oSAnW#~}Iee6sgB1^CZzLL_ zikshQ+?zw|Bho_eL!>sXn^JCRBBns)(?+%D)dUN-#ge$2Xm6?vblmga`I*1Xv(vcB zB=MOJTeH*XbT6xLb98Q{l%d(-h!6kn)z_=lo3qVHjF#;(yD ze_EWVFiyhkJ3yEn52Rxh)(1>?%l;26jOkg=(!FnEccqoM z#EL&9ZkapTzRLOOrPhP!lvaxz#=c0ahVfiX%(~hmMLY`&qGW4;bvA7q9x5OnsCD$x zQf+0?xi!9VJVWyKFDt<$DuN@FnTUT01?mG+swYX-)-iKhQgHW$59yOu{&Tc72M4z1 z?pKnzMI>TFS=O6oSvm%wTQ-7)CL>@M8L>C~{ zgR%_Hl zg<`Sx*ck8Kc_gghK%cvfiLO#B#53F+t>4wE{mY6lf8Ht?JYu{FsW4fs-N zI1xI0NNt{wFf#F;HjXoOkJ0R-_4{<5vl@pH_}H4&siMwKM+d1-(tGM^1)GLra!=+VOtDs6Z+P%VOI zdmd{|_xHC~$Fd%Y5A{}wLR^!@bED0GMr0_-RseBs87(~8PQwyeb5+&jPq(ni7uOK& zP~YN7%63qr^h))5HHKw7!an>NRpoyL_JMzrwuwH|8!}}{xL3IpDP_@7&5JgFbTb|# zzaK{E1Lse9Ch2RM=&hFa?>PbHe|PFcFV5Vqp9f9DAl#P1{~Hl0F>YU>Cii$&D}BM7&k_ zz%V#|Vs+TDvLHr9ko*x5T$;BVeoWc_aKvFc<~r5w(d@v*Q^k%M%$caW{Xa*g1w_OS zv(0f`!u4|nzR?6#e>$A{ss1*z@%O_(T&4>v{lm?PnHP=E!)EF?z4!WdOzS14gtCGt zy|cDX_csR{(CEHhBC_K%#Of{NNXfg4nnu3YhrX^uRP3>7a*6IYA+LgR?+JFy$uBC@ zgLo1j5T;h{h*wR0Xu@6&HHf!jh>8Z8EX_uPtWu1+!K2uB!HSzkR#C?b?ogC&ALVow z?2XRd<`us5^6=p5G{nF_;tP-T|9y}y-KZp>E`I=kFuDJ5RoF$f3iY?*bfITjyg$Wm zzYe=&qwmJr6U@OQ)@uIDZhJAvA-5at+^625`@70>Imp56dmBs>=bRqUO*nyMSt3)A4_r`lJ7zS#AFY-LyiP3)N4+>&Se zWAEuH;M}nJ9ql-hpG=5~mbabxs&>gf>>R8LV*Jh4#D}o%om9tUwt+aAUZ32hZ>ekS zCiR?Tw11Hb+;eXCLu~zO-=18LZz-7BlX2>pA{hu~7lV^dYFa<-U+;q_*@yt$iAN+% z>U~C!IMO&x-~b3cQ)Al*CV3>RWlfQF1ddKMa-BJ^nNsuOSM>;eps&o}&rIy#))ys{ zpIfpNWswnNxAb|A>69YA|Fib+7CPqW=zM4t-^kdAfen zd7a57nu)r(;sKcr#pb8uzG*8xH~G2YkN&*anmYmimXm6Ni-5R;CwZlS!rapW=Hc%l z8&tUSW~j{7!NfipG_rkEk1DEhGog9#wdpa}?;h2+G_B54k%~hF9h^$|G^k?RfHeuV z)usi|-i2drSj7a#y4W-68xb_%LxIi)8k~gy-+4d1ONn~<1=$%kb`$yjL8lsp^^rNj ztHy+^;MGs0o9?I6>GRI!hkN+og@WjUB&~aM(FXOM=;ACb02Pf8wn=oHyMA>10Ctcw zVAby~gf&la4*GWl+c^18B<|qnhHf#7xoVj(h=JrdBg8Ty`{pt@Pp>R-El0 zdhpKMgpS0y?LrQ~1#@P0G^+5?WLq1|j`1YlxC7MVM@k^(rE?)iCcgaWf;;zejSA*7uRsh@(F>~7QK{Tt<)O-)r|FP!; zcRAeP+iP|DOhKov*X-~*JtuUC>^An{*BMn23y$CH#=k4xVX(=5W{!~g^M%TFQx>u5 zId%uBJ;l4d=_v?tN#<2xQC6ThHaP`Ma=V%Q%fW<}7@J(@HKiiseiEm4moa7F!fA_` zrA2(2pwqJG{HJAg2(@C?N<$5+J(W)r*Q6u9ceWl$@iiXHCj`*^xoF>8b>I333!=F~ z#fp>p(->4cc#+S6F3P5{QzJ@;!nohOhwRKko235f@ZQB<*2Pa7EiHQNn9$3ul}59+ z!M|C^@6ym86PS|Sl!f#+Z_4XN)Okg<`VuRb&!~9-|IXR^1)%Ecj`T-tde&uUEZw*3 zrhWvp7OKW{MO<>zz}5tTc-U~kTu8B#wY&jfKwY24Ptb!qm9i!p6*(z7?;V-DSIZl(LRjP?W;uA}4oYj+KOkABWhwg>hPF-ttH>ORj~bzd1I zpFh}1Xsx+y9^@B}a>?LcG`&Y^a*}`$ zZY?)y6r@E-qG0n#URlP;1jg+Z6rL={Ffr|+$Zd6?Bl?lz)A^|fbzET^U@Npx7+L@w zhZoHKD1-U;$&K^u^HWXbw8$`V;roBO!u2AH{YKJjcY4{QiPdjH@?tg zPwl>lP%@vEJhfC@9n9F?J2PYGnVv+G^Sg0Xr`pYsZg?=WJ z=qLN#7a>Bd3h<@vwl*O5kIR3tO!Z(96TVh=>mJNRRZ=9po#2(IEaT-qFjX??7y~e< z#tv(#$NFeJ8ECA0HV$QZ-mD{95p;oGNH@vTGI8r=_cE`Vp)3hQv0@gJ>bh;LHQbXQ zV5Tfdi*lH)A223nF|{?JphMY~ll@N;0*OQ`J7H><-oe*ZHsV*ma^K$mh(3AETq`RoWUGrlb;dKt(87x!~q?7Hg7#wm!?j@~C=4VebrtRFR0q7Ks*lU3%< z@aHxheOe>3Z8E2*Fg~CSwq~&FFN2kZ5P%EottU$LYZeo&Z#M<4s;!-+dZ3!6nal!C=nHs_YB_09wyQWS zXAQxt;Jm>Gy)>N6&^v*rKC^MJX=^xb>CAj`=BDiHA3#oO4Ue1cF;__CnNhpRX2x+S zJ~GvN0ZjFN(S;XQ0AGGW3mry&vY|B5p%;Mh^5WH0pU(X=H}AhICG>oh4dXbAQ9>5- z+qa$VvIK*TXTZoV47xbST;zo|Lz#u(;8Y0HoWq6(bXntFkLG$LJk~5YUTR5!SYU?Z z`$=f1xq}cys{c-7cdhM6m$3b0|Ia{>mQ6Hvx&t$&JHWFL1QV84=##dAnqhR~N~(Ro zzM-i#hAl?GYj>7J91SKbK?R&OrXj%f&i@q+-UmWVPiFdQCz z1bB;=zc_@0e#~VTEwOXYz5h;kGSojQaFLE?j+ZTm8#&t1vcE!KeXITn*N#@fb4Gup z9p+R)%_aysE}bv)hB4YxOj2e;y0y02==4+~?JC#(5yjaliNk3t$B|8dJp8F&J1b82 zS&TS5D@ljfR^Q;>)cla%lC3o5evu=xGmy5NJ-L zo25Y9cMq6&vpK4I2>l`bRhkc6TcdY#77qHCCJr|y`T}VkCA4KB5Mca)9hM9QNm$MR zM)T8Y>BLoD7|4hOOG-@m#p1*2haSta&*)5CLZp=a0C`{H|0YjMo@4HO5r;72O>?{} zjdwH2Et7jW@g6imN;y>iG(nGYsC+U`%hn#4g~;X(cj42XXWy3#+o+CyOkKV95+fbt zby+OqIElw_FkFBbi@JvsPwO5=Ci>bXy$Yj=@VmtLgVJb+YnRg@etOMnLBAu>3Cu`p z8uv$49Z~Kx)=4uIYmq#a)gfA-7t#-&G>(9}c9ee!uV^s*Au$y-i-_%TN4PTa05$KN z&r;M~iw3Z$bwuW8%l#T-^!{o2_C7%ZP_5Izre}=yl^*d;J>kua$Sxkz*vipTeI8?K zy<%GvKEs{nU1?)#zY?H4-8G=LYsj_ePk^8%U@+b_M1ybJ$amh2etRKeGJ+mPkD@31 zr))-7$5$uChnt3*Fn+I%&<}hPVu~rcP9~Zv!6DA{2n4w*&NNJ1in0|Kvf5N%$&@aw zrn_VQ;3TIh*Dp^z|KxY{YyDNdi_;v_+>XIl20xg7dM(w{9R-~g;FIe_rv8TIc`g;a zpNGrlxMbSo@h6eR4zl18MP^^sSYBdZ4~ybko3e5vz2)_PU{Xzqvf$4OI81T$-GkVX z45D=3>yhS79wm?K9a37Sn_v8MLp#&dnG8E(jDj5N@-PzGXnH9F1-|QTr+-z^FF@#~ zdZtlk=(_Ge1X*2ig$?7qJ(UTj?w`R$FaJ&XFK>|9!! z)NrGUFaYz}^?l&dbx^%g*Wd%19oxJQx$VUA8A!pX#MYRiq}ckKv2&|6L6^RESuP|K zsep^0&ZT{5*ZJq$HRowTx8F*$i?+w8U(Ya>#>EHkjr@wiaBTYWRpB1mIwe0qCpl`{ zQ`BjI(Nq>6w2|-&>=o;^MEbBaQBq0Q2>8%gIJreX(c6cb=-2Cw+-+VtQa0Uu7t*7Yt=y2+*N(osFiO)DS zmF$Tt$I;0Z1&jnbZ&Xb-Pf$vlJ8o}o+0>736BDn339u-HA#E9Ed5mc$8exsbq~5i^ zR)nwV0b9hdF!qJr*SxYLDn|8z5eehK-(V@WPD-`o%@8M)p zfGpWNDb8f!=Wz%;S@9dlkhEJF!9_0H(=}ni1j^bu_wk#fVi+Y7|L?!=?dho|z=xy0 zmx!gr0V1+6@w!Kc!dN!WQ&Q79zQ*X79m%lJI|-bD$^qnrSU8H92PU=_*@V{#DG#7* zor!Y4d5%E;FB5_HO5xWaHooo2i@Siv1vP%qy!ZwP_a^nXuLIAl#* zv33)w%x7nr6Ot5yq2LZWEnU@o|NmYP^g^XLec*~{VaFPcX#S{5_umJkfULy!Th8KV z%}V9Ck{og;`%=LuS(gn$^?Id#n1Ad4v#76ZF%7%P+U9TEodj3p? z9EF2!2lDrbpdi@)-l|0@70+F{nmNa8wG zVZ6$RkHvBiHY3I{*AN0wp2nkG}K#{1Y_% z5Y$+~ND9#OKBd+Z36EO9g!TLxiC;B4^Z6G1r4 zEdIZ*3c@zG&>g9p1^Yjddo?ivqQ;9bZSE~sm%aKJKp+m6_|E1)7|ELi3#;=6l88YR z%LeStTH)C`8En!m5!;H)0Z`m`L4i^;O>+*f$43?W zyVUJXWHzvBVGmfYX35#lxA@NMJQx9>jg_b6@1FNB17uS%dCZg9Ll8K7eh$uoyR`rF zW(C2RrIvs|TnV{B9?Jx?d27V;XTx2fs`-Q?gj^DZb%3peD{jVP-qtc#sShTQigapN zvO2Jy{otfdBOwc7K;Z~#*<=yNALCAAkPzSC*uPRlB`4Y!y@AUSB73Z!AwJ+uw(_{v$2KRli2su?mEJ1cJ-@A&4Gd!*~yh_wvRD+JG?&20#_)G;!wMm#6vG0|Bt%BVfC6=I0mRzn4- zU%hL`#ZtNoA0NeTm(Uu1$Ai_iiOFDQ|H5rL7$I36BYgr%{&=bWTPGvgH=ZzvsGk=* z7}wptE9JHIsQx*Y>?nNS4{17&!3SOh^Q-?tcJfW*xBtP$G9Ugs$f7kIm}MY;?~-A8 z83^vT(+v{b#QW=+_*hD};F8#Ye(qLq4)nD!AJzeuT{Gz>o&_{K=5;!zxkgQor>ZUG zahO7RqJEDPF{xnb(Z9$xB=; zYC?Dw{;L_@PY5o!L0;|12|G3tGzw^D%|zrWIktMf1IXU0!bcBk9gPfZo#gr%kPQ7& z|9Bwt0J5j*IUFovJZ#OYDDsB#+!SA|e5N3TO4L9#^l^7-Ef$0y#*_F8-;Oal#8Q$1 zN_R|}h^jRp%U;5l#{u7(TTCxdoej8=S6CnVAh4A1!A$wdHHA#bR#^EKT3SVkOjE@0xM3IJLl6A|LvcHDSaw8Cp1;CZH!O0A*|K~v4-tvGEdzP$ z4)k)PCT_fvy!x6{_LnYC#+Zf#9|UfIRL|a-!ouQIf*wretz}>mhE{Xd>?A~K}x1F zrvV*9VyoAMz}RHh0IT;9>XKxRtQUei!RBF3c5orf2!HElPcor2L;y77VGk}fj;Y?> zYl$Cs!M~>SkPwSg7=Dn-z`v^2!ygUt!N6HcLCpL0H4_A9^O?0wYxwUh?C4dt{8iZl zJvF~0oWHB!KYACc0vi+i%vW|au3j^rI`%E;i zBEU+XV+}hl8&mGWoXB-HJ-P;ytVNT@1ie z?aWchnyIDT9ry{sq!73?uCvpy;T#|mDpW)4;DUzSC)<&mx>aUU5=?I{WEB+p#)C{g zeZr)yhhHp#f%6PD`+VM>Cc%A-urcGYFUUtd13Z&g3+2Ei4A)#@px8{;)D%1s*HRzCYS-0Ke*|LuD`m6Fi!Erg%!70zaeX%FvBTgA-$UAQ=(lKk7ldRT=bAJ=pRaV=CnK_PM&?G&Mz4*MjEao9ji!uNjV=J7TvI{3KWBUW z>MPb#bBx=oFGPrbs9O4g0XP6XqXF2~Po&TGb^V{5K)E?{lAThFK^_I3{!#oCc;<(( zuug$qT?$A7{bz(MC|CN`!>xyX1p?l zMg5Lk`nW^b@dubhN^uik7l}_hbS!|{o*>K!*hUP4~A@1J=eDp2N z_UE4g`!Af#3!U8j0=CEY&Gs|*=hnRK;YsT2vgWJi5r^(4mrtU`oHLqS?Xrr7j^Xgk z4wVOFg+8eu!S72s3OEUv6n|P@h+JzPcFxDSe&G{dY2rQSS>HCILWUI^N$7J^ji41S zC?T74*KcM70x561m^0YcN~=>@%O<+uu5)@@c&Xx1O>PEXF#vnLxEH`IH5nvwX2V!% z>S$Fw7Oy-N`}aGZ>UPnCPsM{J#XE`I`rjuNCoCt1Fyq>jQopj4H+nJpMV_w=X2YB` zsY9RL_8S!U+?cN3g!r>x&zp_nJRW|;@_5@>A6g$YYnx9GFl{GTnR?4AoLQh0{=hQH zDZGCJzmeE;f?W+8(Pe@Eh4>xKg9Yz}fR#-DojcIw6GhBbyE$r4`Fzx>@te2;(BKDTQ3HnkYW$DP2xz{i=mVoI$Q zFrt7>Gr??nPy0+KV-!CGa-4}vi>3BWYq@@_?E={(!cF(GcQl)1vq?nH-{Rf&(N!v1 zVldru#ZEKgE3-|xe{aq_W@2wWH0E&T?&r-Z7T5HI62yN zZaZEiao)t%i7`^#z%%5S5`R8*H>uc(&dYVSVK$~IpoGM_F2)6|c@e0KX4rPFl^JbQ zo-I6dzTKg!}T0jJ^RHLlaB+BDm^jES5IPs_udVXg&7pNF08A#*M?m z$x0*d+xxZvJLmgBV5?>Sishs%*Lvp=LjI53?uPEED)R;DtW0Bts=TQ&`?9sxwE!-c z5%f5E0zI{=b+2%!aHu$-{Sfh^+is!MGw&e(Akp^hl4)p5Y~0kU%Hh}R6N(i!_{PMs zYoYj9gj6_8PnFf%CwNUNsK&Qf3e%iE8m^41D;j!Eh$tHRdZuakfpk@6ZHakPYE9XC z(`IPivkhpCKHJ!^D|oiSy%D#RS_b`F{>i7NhPe}ZMjWGdal5INWxS6+ez|LD7$0D& zt*h(hS)lFiC9-N5?RCY@=$%=G>KUx-~!J2 zHddn%`NnzPGn7dykEGGj3DbCi{KEStkK(edHqC&ljjn=S6HmE=2M0=?Y6pg%S_j&O zZl3Iq3}1VGeRN=Gn4n)Y!LH6>Pb{I&anK^?jm9BZ)(;++0HS4iI&lM?Fg!o>YGSxC}(w9 zDqT!08$;dk*NG?3EX#3x@js%knk!mOK4miRmf1;d+5`&fX!ysbj_gXQjg`FHGVt`3 zvcxVE?1~jB!imXE6eSYxoAMj~jEv7I%|(xsD$Y#^2cLj~fn)Qn683H|*mAyIYIGOkSr9>Cy5u!vF>d|QfB}~23`V&$i zIvlKAAah`0vEYqIYMX|Se;lTHWy?)0GJf(vp+b|Uyrj4;&pu(ps1Pd2szsx+Q5_nr zVAOvv5eo~8Pz;9&{)2@T91D|{|Nig)|Iz<{6aTv>w1ieJuulbF2S2N=3B+s;Wd)4~ JmG><|{|^zo^&= 0 + assert result["similarity"].max() <= 1 - # Check that the result matches the expected DataFrame - assert result_df.equals(expected_df) + # Check that the result contains the expected columns + expected_columns = ["id", "text", "similarity"] + assert set(result.columns) == set(expected_columns)
= ({
{truncateText(result.name, 20)}