diff --git a/.github/workflows/actions-updater.yml b/.github/workflows/actions-updater.yml deleted file mode 100644 index 4488094..0000000 --- a/.github/workflows/actions-updater.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: GitHub Actions Version Updater - -# Controls when the action will run. -on: - schedule: - # Automatically run on every Sunday - - cron: '0 0 * * 0' - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - with: - # [Required] Access token with `workflow` scope. - token: ${{ secrets.WORKFLOW_SECRET }} - - - name: Run GitHub Actions Version Updater - uses: saadmk11/github-actions-version-updater@v0.9.0 - with: - # [Required] Access token with `workflow` scope. - token: ${{ secrets.WORKFLOW_SECRET }} \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 39a4147..4d49719 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -4,9 +4,6 @@ on: push: branches: - main - pull_request: - branches: - - main permissions: contents: write @@ -36,6 +33,11 @@ jobs: - name: Run tests run: | uv run pytest + env: + WFP_API_CLIENT_ID: ${{ secrets.WFP_API_CLIENT_ID }} + WFP_API_CLIENT_SECRET: ${{ secrets.WFP_API_CLIENT_SECRET }} + DATABRIDGES_VERSION: ${{ secrets.DATABRIDGES_VERSION }} + DATABRIDGES_API_KEY: ${{ secrets.DATABRIDGES_API_KEY }} deploy-docs: runs-on: ubuntu-latest @@ -51,7 +53,7 @@ jobs: - name: Install uv and set Python version uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b with: - python-version: "3.12" + python-version: "3.13" - name: Install dependencies run: uv sync --dev - name: Deploy documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index abaae0a..4183b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + ## 2.1.4 (2026-05-19) ### Fix diff --git a/Makefile b/Makefile index 5c4427b..fd82ddb 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ check-codestyle: .PHONY: codestyle codestyle: uv run isort --settings-path pyproject.toml ./ - uv run black --config pyproject.toml ./ + uv run black --fast --config pyproject.toml ./ uv run ruff check . --fix #* Tests diff --git a/README.md b/README.md index 68ee6b1..2587995 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ # Data Bridges Knots +[![Python package](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/python-package.yml/badge.svg)](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/python-package.yml) + +[![CodeQL](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/github-code-scanning/codeql) + +[![pages-build-deployment](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/pages/pages-build-deployment) + +[![Publish to S3 PyPI](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/publish-s3.yml/badge.svg)](https://github.com/WFP-VAM/DataBridgesKnots/actions/workflows/publish-s3.yml) + + This Python module allows you to get data from the WFP Data Bridges API, including household survey data, market prices, exchange rates, GORP (Global Operational Response Plan) data, and food security data (IPC equivalent). It is a wrapper for the [Data Bridges API Client](https://github.com/WFP-VAM/DataBridgesAPI), providing an easier way to data analysts to get VAM and monitoring data using their language of choice (Python, R and STATA). ## Getting started -User guide on the package can be found [here](https://wfp-vam.github.io/DataBridgesKnots/reference/) +> [!IMPORTANT] +> User guide on the package can be found [here](https://wfp-vam.github.io/DataBridgesKnots/reference/) + ## Installation ### Using uv -> :point_right: We recommend using `uv` as package manager. You can install it using the [instructions here](https://docs.astral.sh/uv/getting-started/installation/). +> [!TIP] :point_right: We recommend using `uv` as package manager. You can install it using the [instructions here](https://docs.astral.sh/uv/getting-started/installation/). -Install the `data_bridges_knots` package in your environment using uv: +Install the latest stable `data_bridges_knots` package in your environment using uv: ``` uv venv .venv && source .venv/bin/activate && uv pip install data-bridges-knots \ @@ -27,6 +38,14 @@ pip install data-bridges-knots \ STATA and R users will also need the appropriate optional dependencies to use this package in their respective software. To install the package with these dependencies, use the following command: + +#### Development version +If you're looking for a specific release/development version, you can install it by running this command, and adding the release number: + +``` + uv pip install git+https://github.com/WFP-VAM/DataBridgesKnots/@release/vX.x.x +``` + ### STATA users STATA users need to install the `data_bridges_knots` with additional STATA dependencies (`pystata`, and `stata-setup`): @@ -55,16 +74,12 @@ There are three ways to configure DataBridgesShapes: 2. The structure of the file is: ```yaml - NAME: '' - VERSION : '' - KEY: '' - SECRET: '' + DATABRIDGES_VERSION : '' + WFP_API_CLIENT_ID: '' + WFP_API_CLIENT_SECRET: '' DATABRIDGES_API_KEY: '' - SCOPES: - - '' - - '' ``` -3. Replace the placeholders with your actual API key and secret from the Data Bridges API. Update the SCOPES list with the required scopes for your use case. +3. Replace the placeholders with your actual credentials from the Data Bridges API portal. ### Option 2: Dictionary Configuration (Recommended for Testing/Programmatic Use) @@ -74,13 +89,9 @@ You can also initialize the client directly with a Python dictionary: from data_bridges_knots import DataBridgesShapes config = { - 'KEY': 'your-api-key', - 'SECRET': 'your-api-secret', - 'VERSION': '7.0.0', - 'SCOPES': [ - 'vamdatabridges_household-fulldata_get', - 'vamdatabridges_marketprices-pricemonthly_get' - ], + 'WFP_API_CLIENT_ID': 'your-api-key', + 'WFP_API_CLIENT_SECRET': 'your-api-secret', + 'DATABRIDGES_VERSION': 'v1', 'DATABRIDGES_API_KEY': 'optional-databridges-key' } @@ -92,10 +103,9 @@ client = DataBridgesShapes(config) Set the following environment variables and use the `config_from_env()` helper: ```bash -export DATABRIDGES_KEY="your-api-key" -export DATABRIDGES_SECRET="your-api-secret" -export DATABRIDGES_VERSION="7.0.0" -export DATABRIDGES_SCOPES="scope1,scope2,scope3" +export WFP_API_CLIENT_ID="your-api-key" +export WFP_API_CLIENT_SECRET="your-api-secret" +export DATABRIDGES_VERSION="v1" export DATABRIDGES_API_KEY="optional-databridges-key" ``` @@ -125,7 +135,7 @@ CONFIG_PATH = r"data_bridges_api_config.yaml" client = DataBridgesShapes(CONFIG_PATH) # COMMODITY DATA -commodity_units_list = client.get_commodity_units_list(country_code="TZA", commodity_unit_name="Kg", page=1, format='json') +commodity_units_list = client.get_commodity_units_list(country_iso3="TZA", commodity_unit_name="Kg", page=1, format='json') ``` Additional examples are in the [examples](https://github.com/WFP-VAM/DataBridgesKnots/tree/main/examples) folder and in the [API Reference document](https://wfp-vam.github.io/DataBridgesKnots/reference/) diff --git a/ROADMAP.md b/ROADMAP.md index 53b4f12..a94172b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ This document outlines the planned features and improvements for the `DataBridgesKnots` package, which provides a wrapper for the WFP Data Bridges API. -## Upcoming Release: 1.0.0 "Brilliant Bigfoot" (DataBridges API v5.0) +## 1.0.0 "Brilliant Bigfoot" (DataBridges API v5.0.0) ### New Features - [X] Endpoints: CommoditiesApis @@ -17,28 +17,80 @@ This document outlines the planned features and improvements for the `DataBridge - [X] Endpoints: XlsFormsApi - [X] Endpoints: SurveysApi -## Future Release +## 2.0.0 "Cheerful Chimera" (DataBridges API v6.0.0) -### v1.1.0 -- [ ] STATA support - [X] Fix optional dependencies for STATA - [X] Update setup.py and pyproject.toml to include DataBridges API v6.0 - -### v1.2.0 - [X] R example files +- [X] Documentation: Enhance documentation and provide more usage examples +- [X] Automation: GitHub Actions linting and testing + -### v1.2.1 -- [ ] Documentation: Enhance documentation and provide more usage examples -- [ ] Automation: GitHub Actions linting and testing -- [ ] Bug fixing: Test AIMS and RPME endpoints -- [ ] Bug fixing: DPO change for XLSForm +# v3.0.0 "Delicate Dragon" (DataBridgesAPI v1.0.0 - new API) +- [ ] Testing: Unit testing for implemented endpotns + +## Future Releases (3.1.0+) +- [ ] Add missing endpoints + - [ ] RpmeApi + - [ ] IpcchApi + - [ ] GlobalOutlookApi +- [ ] Refactor client.py into modules (e.g. `endpoints/household.py`) +- [ ] STATA support +- [ ] Add helper functions to search surveys - [ ] Bug fixing: Markets GeoJSON response -- [ ] Bug fixing: JSON to DataFrame response - [ ] Bug fixing: Market list JSON and CSV +- [ ] Testing: Add tests for helper functions (labels) +- [ ] Refactoring: Optimize performance and improve code efficiency (e.g. polars) + -## Future Release (2.0.0 "Charismatic Chimera") -- Testing: Unit testing -- Testing: Improve error handling and logging -- Refactoring: Optimize performance and improve code efficiency +# Endpoints v1 -Please note that this roadmap is subject to change, and the priorities may be adjusted based on the project's needs and available resources. +| Function | Method (data-bridges-client) | Endpoint | Description | Developed | Tested | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------ | +| | [**commodities_categories_list_get**](docs/CommoditiesApi.md#commodities_categories_list_get) | **GET** /Commodities/Categories/List | Provides the list of categories. | | | +| | [**commodities_list_get**](docs/CommoditiesApi.md#commodities_list_get) | **GET** /Commodities/List | Provide the detailed list of the commodities available in DataBridges platform | | | +| | [**commodity_units_conversion_list_get**](docs/CommodityUnitsApi.md#commodity_units_conversion_list_get) | **GET** /CommodityUnits/Conversion/List | Provides conversion factors to Kilogram or Litres for each convertible unit of measure. | | | +| | [**commodity_units_list_get**](docs/CommodityUnitsApi.md#commodity_units_list_get) | **GET** /CommodityUnits/List | Provides the detailed list of the unit of measure available in DataBridges platform | | | +| | [**currency_list_get**](docs/CurrencyApi.md#currency_list_get) | **GET** /Currency/List | Returns the list of currencies available in the internal VAM database, with Currency 3-letter code, matching with ISO 4217. | | | +| | [**currency_usd_indirect_quotation_get**](docs/CurrencyApi.md#currency_usd_indirect_quotation_get) | **GET** /Currency/UsdIndirectQuotation | Returns the value of the Exchange rates from Trading Economics, for official rates, and DataViz for unofficial rates. | | | +| | [**economic_data_indicator_list_get**](docs/EconomicDataApi.md#economic_data_indicator_list_get) | **GET** /EconomicData/IndicatorList | Returns the lists of indicators. | | | +| | [**economic_data_indicator_name_get**](docs/EconomicDataApi.md#economic_data_indicator_name_get) | **GET** /EconomicData/{indicatorName} | Returns the time series of values for different indicators. | | | +| | [**global_outlook_country_latest_get**](docs/GlobalOutlookApi.md#global_outlook_country_latest_get) | **GET** /GlobalOutlook/CountryLatest | Return the latest country dataset of number of acutely food insecure (in thousands) based on WFP's Global Outlook. | | | +| | [**global_outlook_global_latest_get**](docs/GlobalOutlookApi.md#global_outlook_global_latest_get) | **GET** /GlobalOutlook/GlobalLatest | Return the latest global dataset of number of acutely food insecure (in millions) based on WFP's Global Outlook. | | | +| | [**global_outlook_regional_latest_get**](docs/GlobalOutlookApi.md#global_outlook_regional_latest_get) | **GET** /GlobalOutlook/RegionalLatest | Return the latest regional dataset of number of acutely food insecure (in millions) based on WFP's Global Outlook. | | | +| | [**hunger_hotspot_categories_and_indicators_get**](docs/HungerHotspotApi.md#hunger_hotspot_categories_and_indicators_get) | **GET** /HungerHotspot/CategoriesAndIndicators | Retrieves Hunger Hotspot categories and indicators. | | | +| | [**hunger_hotspot_data_get**](docs/HungerHotspotApi.md#hunger_hotspot_data_get) | **GET** /HungerHotspot/Data | Retrieves a paginated list of Hunger Hotspot data. | | | +| | [**cari_adm0_values_get**](docs/IncubationApi.md#cari_adm0_values_get) | **GET** /Cari/Adm0Values | Retrieves a paginated list of Adm0 CARI results based on the specified indicator, administrative code, and survey. | | | +| | [**cari_adm1_values_get**](docs/IncubationApi.md#cari_adm1_values_get) | **GET** /Cari/Adm1Values | Retrieves a paginated list of Adm1 CARI results based on the specified indicator, administrative code, and survey. | | | +| | [**household_draft_internal_base_data_get**](docs/IncubationApi.md#household_draft_internal_base_data_get) | **GET** /Household/DraftInternalBaseData | Get data that includes the core household fields only by Survey ID. To access this data, please contact Wael ATTIA for authorization. This endpoint will send you only data you have access to, based on permissions assigned to your application profile. The \"apiKey\" can be found in the profile section of the DataBridges application. | | | +| | [**household_full_data_get**](docs/IncubationApi.md#household_full_data_get) | **GET** /Household/FullData | Get a full dataset that includes all the fields included in the survey in addition to the core household fields by Survey ID. To access this data, please contact Wael ATTIA for authorization. This endpoint will send you only data you have access to, based on permissions assigned to your application profile. The \"apiKey\" can be found in the profile section of the DataBridges application. | | | +| | [**household_official_use_base_data_get**](docs/IncubationApi.md#household_official_use_base_data_get) | **GET** /Household/OfficialUseBaseData | Get data that includes the core household fields only by Survey ID | | | +| | [**household_public_base_data_get**](docs/IncubationApi.md#household_public_base_data_get) | **GET** /Household/PublicBaseData | Get data that includes the core household fields only by Survey ID | | | +| | [**household_surveys_get**](docs/IncubationApi.md#household_surveys_get) | **GET** /Household/Surveys | Retrieve 1) Survey IDs, 2) their corresponding XLS Form IDs, and 3) Base XLS Form of all household surveys conducted in a country. A date of reference, SurveyDate, for the data collection is set by the officer responsible for the upload for each survey. | | | +| | [**m_fi_surveys_processed_data_with_keyset_pagination_get**](docs/IncubationApi.md#m_fi_surveys_processed_data_with_keyset_pagination_get) | **GET** /MFI/Surveys/ProcessedDataWithKeysetPagination | Please use this endpoint only for large data retrieval - Response will include only JSON format - Get a MFI processed data in long format; levels indicate the data aggregation level 1) Normalized Score, 2) Trader Aggregate Score, 3) Market Aggregate Score, 4) Trader Median, 5) Trader Mean, 6) Market Mean; each line corresponds to one of the nine dimensions of scores plus the final MFI aggregate score; 1) Assortment, 2) Availability, 3) Price, 4) Resilience, 5) Competition, 6) Infrastructure, 7) Service, 8) Quality, 9) Access and Protection, and 10) MFI final score; the variable label describes each variable and its value range | | | +| | [**xls_forms_definition_get**](docs/IncubationApi.md#xls_forms_definition_get) | **GET** /XlsForms/definition | Get a complete set of XLS Form definitions of a given XLS Form ID. This is the digital version of the questionnaire used during the data collection exercise. | | | +| | [**ipcch_ipcch_and_equivalent_historical_peaks_get**](docs/IpcchApi.md#ipcch_ipcch_and_equivalent_historical_peaks_get) | **GET** /Ipcch/IPCCHAndEquivalent-HistoricalPeaks | Retrieves a paginated list of historical IPCCH and Equivalent peaks data, optionally filtered by ISO3 country code. | | | +| | [**ipcch_ipcch_and_equivalent_latest_peaks_get**](docs/IpcchApi.md#ipcch_ipcch_and_equivalent_latest_peaks_get) | **GET** /Ipcch/IPCCHAndEquivalent-LatestPeaks | Retrieves a paginated list of the latest IPCCH and Equivalent peaks data, optionally filtered by ISO3 country code. | | | +| | [**ipcch_ipcch_and_equivalent_most_recent_get**](docs/IpcchApi.md#ipcch_ipcch_and_equivalent_most_recent_get) | **GET** /Ipcch/IPCCHAndEquivalent-MostRecent | Retrieves a paginated list of the most recent IPCCH and Equivalent data, optionally filtered by ISO3 country code. | | | +| | [**ipcch_ipcch_and_equivalent_peaks_wfp_dashboard_get**](docs/IpcchApi.md#ipcch_ipcch_and_equivalent_peaks_wfp_dashboard_get) | **GET** /Ipcch/IPCCHAndEquivalentPeaks-WFPDashboard | Retrieves a paginated list of IPCCH and Equivalent Peaks data for the WFP Dashboard. | | | +| | [**ipcch_ipcch_historical_data_get**](docs/IpcchApi.md#ipcch_ipcch_historical_data_get) | **GET** /Ipcch/IPCCH-HistoricalData | Retrieves a paginated list of IPCCH and Equivalent Historical Data. | | | +| | [**market_prices_alps_get**](docs/MarketPricesApi.md#market_prices_alps_get) | **GET** /MarketPrices/Alps | Returns time series values of ALPS and PEWI. | | | +| | [**market_prices_price_daily_get**](docs/MarketPricesApi.md#market_prices_price_daily_get) | **GET** /MarketPrices/PriceDaily | Returns a daily time series of commodity market prices. | | | +| | [**market_prices_price_monthly_get**](docs/MarketPricesApi.md#market_prices_price_monthly_get) | **GET** /MarketPrices/PriceMonthly | Returns a monthly time series of commodity market prices. | | | +| | [**market_prices_price_raw_get**](docs/MarketPricesApi.md#market_prices_price_raw_get) | **GET** /MarketPrices/PriceRaw | Returns original commodity market prices | | | +| | [**market_prices_price_weekly_get**](docs/MarketPricesApi.md#market_prices_price_weekly_get) | **GET** /MarketPrices/PriceWeekly | Returns a weekly time series of commodity market prices. | | | +| | [**markets_geo_json_list_get**](docs/MarketsApi.md#markets_geo_json_list_get) | **GET** /Markets/GeoJSONList | Provide a list of geo referenced markets in a specific country | | | +| | [**markets_list_get**](docs/MarketsApi.md#markets_list_get) | **GET** /Markets/List | Get a complete list of markets in a country | | | +| | [**markets_markets_as_csv_get**](docs/MarketsApi.md#markets_markets_as_csv_get) | **GET** /Markets/MarketsAsCSV | Get a complete list of markets in a country | | | +| | [**markets_nearby_markets_get**](docs/MarketsApi.md#markets_nearby_markets_get) | **GET** /Markets/NearbyMarkets | Find markets near a given location by longitude and latitude within a 15Km distance | | | +| | [**rpme_base_data_get**](docs/RpmeApi.md#rpme_base_data_get) | **GET** /Rpme/BaseData | Get data that includes the core RPME fields only by Survey ID | | | +| | [**rpme_full_data_get**](docs/RpmeApi.md#rpme_full_data_get) | **GET** /Rpme/FullData | Get a full dataset that includes all the fields included in the survey in addition to the core RPME fields by Survey ID. | | | +| | [**rpme_output_values_get**](docs/RpmeApi.md#rpme_output_values_get) | **GET** /Rpme/OutputValues | Processed values for each variable used in the assessments | | | +| | [**rpme_surveys_get**](docs/RpmeApi.md#rpme_surveys_get) | **GET** /Rpme/Surveys | Retrieve 1) Survey IDs, 2) their corresponding XLS Form IDs, and 3) Base XLS Form of all RPME surveys conducted in a country. The date of reference, SurveyDate, for the data collection is set by the officer responsible for the upload of each survey. | | | +| | [**rpme_variables_get**](docs/RpmeApi.md#rpme_variables_get) | **GET** /Rpme/Variables | List of variables | | | +| | [**rpme_xls_forms_get**](docs/RpmeApi.md#rpme_xls_forms_get) | **GET** /Rpme/XLSForms | Get a complete list of XLS Forms uploaded on the RPME in a given period of data collection. This is the digital version of the questionnaire used during the data collection exercise. | | | +| | [**m_fi_surveys_base_data_get**](docs/SurveysApi.md#m_fi_surveys_base_data_get) | **GET** /MFI/Surveys/BaseData | Get data that includes the core Market Functionality Index (MFI) fields only by Survey ID | | | +| | [**m_fi_surveys_full_data_get**](docs/SurveysApi.md#m_fi_surveys_full_data_get) | **GET** /MFI/Surveys/FullData | Get a full dataset that includes all the fields included in the survey in addition to the core Market Functionality Index (MFI) fields by Survey ID. To access this data, please contact global.mfi@wfp.org for authorization. | | | +| | [**m_fi_surveys_get**](docs/SurveysApi.md#m_fi_surveys_get) | **GET** /MFI/Surveys | Retrieve 1) Survey IDs, 2) their corresponding XLS Form IDs, and 3) Base XLS Form of all MFI surveys conducted in a country. A date of reference, SurveyDate, for the data collection is set by the officer responsible for the upload for each survey. | | | +| | [**m_fi_surveys_processed_data_get**](docs/SurveysApi.md#m_fi_surveys_processed_data_get) | **GET** /MFI/Surveys/ProcessedData | Get a MFI processed data in long format; levels indicate the data aggregation level 1) Normalized Score, 2) Trader Aggregate Score, 3) Market Aggregate Score, 4) Trader Median, 5) Trader Mean, 6) Market Mean; each line corresponds to one of the nine dimensions of scores plus the final MFI aggregate score; 1) Assortment, 2) Availability, 3) Price, 4) Resilience, 5) Competition, 6) Infrastructure, 7) Service, 8) Quality, 9) Access and Protection, and 10) MFI final score; the variable label describes each variable and its value range | | | +| | [**m_fi_xls_forms_get**](docs/XlsFormsApi.md#m_fi_xls_forms_get) | **GET** /MFI/XlsForms | Get a complete list of XLS Forms uploaded on the MFI Data Bridge in a given period of data collection. This is the digital version of the questionnaire used during the data collection exercise. | | | diff --git a/data_bridges_api_config_sample.yaml b/data_bridges_api_config_sample.yaml index 06a36f9..45173a6 100644 --- a/data_bridges_api_config_sample.yaml +++ b/data_bridges_api_config_sample.yaml @@ -1,8 +1,4 @@ -NAME: 'name-of-the-application' -VERSION : '7.0.0' -KEY: '' # WFP API Gateway API Key -SECRET: '' # WFP API Gateway API Secret -DATABRIDGES_API_KEY = '' # WFP DataBridges API Key (required for household data) -SCOPES: # WFP DataBridges API Scopes (required for restricted endpoints) - - '' - - '' \ No newline at end of file +DATABRIDGES_VERSION : '' +WFP_API_CLIENT_ID: '' +WFP_API_CLIENT_SECRET: '' +DATABRIDGES_API_KEY: '' \ No newline at end of file diff --git a/data_bridges_knots/__init__.py b/data_bridges_knots/__init__.py index 38afa22..1bd84f2 100644 --- a/data_bridges_knots/__init__.py +++ b/data_bridges_knots/__init__.py @@ -4,7 +4,7 @@ Wrapper for DataBridges client. """ -from .client import DataBridgesShapes +from .client import DataBridgesShapes, config_from_env from .labels import get_choice_labels, get_variable_labels, map_value_labels __all__ = [ @@ -13,4 +13,5 @@ "get_variable_labels", "get_choice_labels", "map_value_labels", + "config_from_env", ] diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index c9abf09..c98e046 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -31,10 +31,8 @@ def config_from_env() -> Dict: """Construct DataBridges configuration dictionary from environment variables. Reads configuration from the following environment variables: - - WFP_API_KEY: WFP API Gateway key for authentication - - WFP_API_SECRET: WFP API Gateway secret for authentication - - DATABRIDGES_SCOPES: Comma-separated list of API scopes - - DATABRIDGES_VERSION: API version (e.g., 'v1') + - WFP_API_CLIENT_ID: WFP API Gateway key for authentication + - WFP_API_CLIENT_SECRET: WFP API Gateway secret for authentication - DATABRIDGES_API_KEY: (Optional) DataBridges-specific API key for certain endpoints Returns: @@ -45,42 +43,33 @@ def config_from_env() -> Dict: Examples: >>> import os - >>> os.environ['WFP_API_KEY'] = 'your_key' - >>> os.environ['WFP_API_SECRET'] = 'your_secret' - >>> os.environ['DATABRIDGES_SCOPES'] = 'scope1,scope2' - >>> os.environ['DATABRIDGES_VERSION'] = 'v1' + >>> os.environ['WFP_API_CLIENT_ID'] = 'your_key' + >>> os.environ['WFP_API_CLIENT_SECRET'] = 'your_secret' >>> config = config_from_env() >>> client = DataBridgesShapes(config) """ - required_vars = { - "KEY": "WFP_API_KEY", - "SECRET": "WFP_API_SECRET", - "SCOPES": "DATABRIDGES_SCOPES", - "VERSION": "DATABRIDGES_VERSION", - } + required_vars = [ + "WFP_API_CLIENT_ID", + "WFP_API_CLIENT_SECRET", + "DATABRIDGES_VERSION", + ] config = {} missing = [] - # Load required variables - for config_key, env_var in required_vars.items(): + for env_var in required_vars: value = os.getenv(env_var) if value is None: missing.append(env_var) else: - # Special handling for SCOPES - split comma-separated string into list - if config_key == "SCOPES": - config[config_key] = [scope.strip() for scope in value.split(",")] - else: - config[config_key] = value + config[env_var] = value if missing: raise ValueError( f"Missing required environment variables: {', '.join(missing)}" ) - # Load optional DATABRIDGES_API_KEY databridges_api_key = os.getenv("DATABRIDGES_API_KEY") if databridges_api_key: config["DATABRIDGES_API_KEY"] = databridges_api_key @@ -89,53 +78,57 @@ def config_from_env() -> Dict: class DataBridgesShapes: - """DataBridgesShapes is a class that provides an interface to interact with the Data Bridges API. + """Interface to the Data Bridges API. - This class includes methods for fetching various types of data such as market prices, - exchange rates, food security data, commodities, and more. The class can be initialized - with either a YAML configuration file or a configuration dictionary, and supports - multiple environments. + Provides methods for fetching market prices, exchange rates, food security data, + commodities, and more. Can be initialized from a YAML file, a dictionary, or + environment variables. Args: yaml_config_path (str | dict): Either: - - Path to YAML configuration file (str), or - - Configuration dictionary (dict) with required keys: KEY, SECRET, VERSION, - SCOPES, and optionally DATABRIDGES_API_KEY - env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" + - Path to a YAML configuration file (str), or + - Configuration dictionary (e.g. .env) with required keys: WFP_API_CLIENT_ID, + WFP_API_CLIENT_SECRET, and optionally DATABRIDGES_API_KEY + env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod". + api_version (str, optional): Data Bridges API version to use. Defaults to "v1" (current version) + Examples: - >>> # Initialize with YAML file (traditional method) + >>> # Initialize with YAML file >>> client = DataBridgesShapes("data_bridges_api_config.yaml") >>> df_prices = client.get_prices("KEN", "2025-09-01") - >>> # Initialize with dictionary (new method) + >>> # Initialize with dictionary >>> config = { - ... 'KEY': 'your-api-key', - ... 'SECRET': 'your-api-secret', - ... 'VERSION': '7.0.0', - ... 'SCOPES': ['vamdatabridges_household-fulldata_get'], + ... 'WFP_API_CLIENT_ID': 'your-client-id', + ... 'WFP_API_CLIENT_SECRET': 'your-client-secret', ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' ... } >>> client = DataBridgesShapes(config) - >>> exchange_rates = client.get_exchange_rates("ETH") >>> # Initialize from environment variables >>> from data_bridges_knots.client import config_from_env - >>> config = config_from_env() - >>> client = DataBridgesShapes(config) + >>> client = DataBridgesShapes(config_from_env()) """ - def __init__(self, yaml_config_path, env="prod"): + def __init__(self, yaml_config_path, env="prod", api_version="v1"): warnings.warn( ( - "Authentication handling will change in the next version, which is a breaking change. " - "Please upgrade to DataBridgesKnots v3.0.0 by 31 May 2026" + "\n[FUTURE WARNING]\n" + "DataBridgesShapes will be renamed to 'DataBridgesKnots' " + "in version 4.0.0 (next major release, scheduled for 1 July 2026).\n" + "Please update your imports accordingly.\n" ), - category=DeprecationWarning, + FutureWarning, stacklevel=2, ) + # Initialize instance variables + self.api_version = api_version + self.env = env + self.xlsform = None + # Load and validate config once self.config = self._load_config(yaml_config_path) self._validate_config(self.config) @@ -143,11 +136,9 @@ def __init__(self, yaml_config_path, env="prod"): # Setup authentication and extract API key self.configuration = self._setup_configuration_and_authentication(self.config) self.data_bridges_api_key = self.config.get("DATABRIDGES_API_KEY", "") - self.env = env - self.xlsform = None def __repr__(self): - return f"DataBridgesShapes(host='{self.configuration.host}', env='{self.env}')" + return f"DataBridgesShapes(host='{self.configuration.host}', env='{self.env}'), api_version='{self.api_version}'" def __str__(self): return ( @@ -194,7 +185,7 @@ def _validate_config(self, config: Dict) -> None: Raises: ValueError: If required fields are missing from configuration """ - required_fields = ["KEY", "SECRET", "SCOPES", "VERSION"] + required_fields = ["WFP_API_CLIENT_ID", "WFP_API_CLIENT_SECRET"] missing = [field for field in required_fields if field not in config] if missing: raise ValueError( @@ -211,18 +202,17 @@ def _setup_configuration_and_authentication(self, config: Dict): Configuration: DataBridges configuration object """ - key = config["KEY"] - secret = config["SECRET"] - scopes = config["SCOPES"] - version = config["VERSION"] - uri = "https://api.wfp.org/vam-data-bridges/" - host = str(uri + version) + key = config["WFP_API_CLIENT_ID"] + secret = config["WFP_API_CLIENT_SECRET"] + BASE_URI = "https://gateway.api.wfp.org/vam-data-bridges" + host = f"{BASE_URI}/{self.api_version.strip('/')}" + print("host: ", host) logger.info("DataBridges API: %s", host) token = WfpApiToken(api_key=key, api_secret=secret) configuration = data_bridges_client.Configuration( - host=host, access_token=token.refresh(scopes=scopes) + host=host, access_token=token.refresh() ) logger.debug("Token used: %s", token.__repr__()) @@ -234,7 +224,6 @@ def get_prices( start_date: Optional[str] = None, end_date: Optional[str] = None, page_size: int = 1000, - survey_date: Optional[str] = None, # Deprecated, use start_date instead market_id: int = 0, commodity_id: int = 0, currency_id: int = 0, @@ -250,7 +239,6 @@ def get_prices( end_date (str, optional): End date in ISO format (e.g., '2022-01-01'). If None, defaults to today's date. page_size (int, optional): Number of items per page. Defaults to 1000. - survey_date (str, optional): Deprecated. Use start_date instead. market_id (int, optional): Unique ID of a Market. Defaults to 0. commodity_id (int, optional): The exact ID of a Commodity. Defaults to 0. currency_id (int, optional): The exact ID of a currency. Defaults to 0. @@ -273,17 +261,6 @@ def get_prices( ... price_flag="actual" ... ) """ - # Handle deprecated survey_date parameter - if start_date is None and survey_date is not None: - import warnings - - warnings.warn( - "The survey_date parameter is deprecated. Use start_date instead.", - DeprecationWarning, - stacklevel=2, - ) - start_date = survey_date - if start_date: # Format the date according to RFC 3339 standard start_date = date.fromisoformat(start_date).strftime( @@ -402,52 +379,6 @@ def get_exchange_rates( df = df.replace({np.nan: None}) return df - def get_food_security_list( - self, iso3: Optional[str] = None, year: Optional[int] = None, page: int = 1 - ) -> pd.DataFrame: - """Retrieves food security data from IPC and equivalent data sources - - Args: - iso3 (str, optional): The country ISO3 code - year (int, optional): The year to retrieve data for - page (int, optional): Page number for paged results. Defaults to 1 - - Returns: - pd.DataFrame: DataFrame containing food security data with relevant indicators - and metrics for the specified country and year - - Examples: - >>> client = DataBridgesShapes("data_bridges_api_config.yaml") - >>> # Get food security data for Ethiopia in 2023 - >>> eth_food_security = client.get_food_security_list("ETH", 2025) - >>> # Get all food security data - >>> all_food_security = client.get_food_security_list() - - Raises: - ApiException: If there's an error calling the Food Security API - """ - with data_bridges_client.ApiClient( - self._setup_configuration_and_authentication(self.config) - ) as api_client: - food_security_api_instance = data_bridges_client.FoodSecurityApi(api_client) - env = self.env - - try: - api_response = food_security_api_instance.food_security_list_get( - iso3=iso3, year=year, page=page, env=env - ) - logger.info("Successfully retrieved food security data") - - df = pd.DataFrame([item.to_dict() for item in api_response.items]) - df = df.replace({np.nan: None}) - return df - - except ApiException as e: - logger.error( - f"Exception when calling FoodSecurityApi->food_security_list_get: {e}" - ) - raise - def get_commodities_list( self, country_iso3: Optional[str] = None, @@ -510,7 +441,7 @@ def get_commodities_list( logger.error( f"Exception when calling CommoditiesApi->commodities_list_get: {e}" ) - raise + raise def get_commodity_units_conversion_list( self, @@ -736,7 +667,6 @@ def get_usd_indirect_quotation( ) raise - # FIXME: JSON response def get_economic_indicator_list( self, page: Optional[int] = 1, @@ -771,20 +701,26 @@ def get_economic_indicator_list( format=format, env=self.env, ) - print( + logger.info( "The response of EconomicDataApi->economic_data_indicator_list_get:\n" ) + df = pd.DataFrame([item.to_dict() for item in api_response.items]) + df = df.replace({np.nan: None}) + return df return api_response except Exception as e: - print( - "Exception when calling EconomicDataApi->economic_data_indicator_list_get: %s\n" - % e + logger.error( + "Exception when calling EconomicDataApi->economic_data_indicator_list_get: %s", + e, ) + raise - # BUG: Unsupported content type: 'application/geo+json - def get_market_geojson_list(self, country_iso3: Optional[str] = None): - - adm0code = get_adm0_code(country_iso3) + def get_market_geojson_list(self, country_iso3: str = None): + """Returns a list of geo-referenced markets in a specific country.""" + if country_iso3 is None: + raise ValueError("country_iso3 parameter is required") + else: + adm0code = get_adm0_code(country_iso3) # Enter a context with an instance of the API client with data_bridges_client.ApiClient( @@ -798,13 +734,18 @@ def get_market_geojson_list(self, country_iso3: Optional[str] = None): api_response = api_instance.markets_geo_json_list_get( adm0code=adm0code, env=self.env ) - print("The response of MarketsApi->markets_geo_json_list_get:\n") + logger.info("The response of MarketsApi->markets_geo_json_list_get:\n") + + geojson_dict = api_response.model_dump() + + return geojson_dict return api_response except Exception as e: - print( - "Exception when calling MarketsApi->markets_geo_json_list_get: %s\n" - % e + logger.error( + "Exception when calling MarketsApi->markets_geo_json_list_get: %s", + e, ) + raise def get_markets_list( self, country_iso3: Optional[str] = None, page: Optional[int] = 1 @@ -852,9 +793,11 @@ def get_markets_list( df = df.replace({np.nan: None}) return df except Exception as e: - print("Exception when calling MarketsApi->markets_list_get: %s\n" % e) + logger.error( + "Exception when calling MarketsApi->markets_list_get: %s", e + ) + raise - # BUG: JSON resonse + fix no response def get_markets_as_csv( self, country_iso3: Optional[str] = None, local_names: bool = False ) -> str: @@ -896,9 +839,10 @@ def get_markets_as_csv( return api_response except Exception as e: logger.error( - "Exception when calling MarketsApi->markets_markets_as_csv_get: %s\n" - % e + "Exception when calling MarketsApi->markets_markets_as_csv_get: %s", + e, ) + raise def get_nearby_markets( self, country_iso3: str = None, lat: float = None, lng: float = None @@ -951,71 +895,78 @@ def get_nearby_markets( ) raise - def get_gorp( + def get_global_outlook( self, data_type: Literal["country_latest", "global_latest", "regional_latest"], page: Optional[int] = None, ) -> pd.DataFrame: - """Retrieves data from the Global Operational Response Plan (GORP) API. + """Retrieves data from the Global Outlook API. - The GORP API provides access to WFP's operational response planning data at - different geographical levels. + The Global Outlook API provides access to WFP’s forward-looking analysis and + aggregated insights at different geographical levels, including country, + regional, and global summaries. Args: - data_type (str): The type of GORP data to retrieve. Must be one of: + data_type (str): The type of Global Outlook data to retrieve. Must be one of: - 'country_latest': Latest data at country level - 'global_latest': Latest global aggregated data - 'regional_latest': Latest data aggregated by region - page (int, optional): Page number for paginated results. Required for - 'latest' and 'list' data types. Defaults to None. + page (int, optional): Page number for paginated results. Currently not used + for latest endpoints. Defaults to None. Returns: - pd.DataFrame: DataFrame containing data from the Global Operational Response Plan (GORP) + pd.DataFrame: DataFrame containing Global Outlook data for the selected scope. Examples: >>> client = DataBridgesShapes("data_bridges_api_config.yaml") - >>> # Get latest country-level data - >>> country_data = client.get_gorp("country_latest") - >>> # Get global summary - >>> global_data = client.get_gorp("global_latest") - >>> # Get regional breakdown - >>> regional_data = client.get_gorp("regional_latest") + >>> # Get latest country-level outlook + >>> country_data = client.get_global_outlook("country_latest") + >>> # Get global outlook summary + >>> global_data = client.get_global_outlook("global_latest") + >>> # Get regional outlook data + >>> regional_data = client.get_global_outlook("regional_latest") Raises: ValueError: If data_type is not one of the allowed values - ApiException: If there's an error accessing the GORP API + ApiException: If there is an error accessing the Global Outlook API """ + + # Enter a context with an instance of the API client with data_bridges_client.ApiClient( self._setup_configuration_and_authentication(self.config) ) as api_client: - gorp_api_instance = data_bridges_client.GorpApi(api_client) - env = self.env + # Create an instance of the API class + api_instance = data_bridges_client.GlobalOutlookApi(api_client) + env = ( + self.env + ) # str | Environment. * `prod` - api.vam.wfp.org * `dev` - dev.api.vam.wfp.org (optional) try: if data_type == "country_latest": - gorp_data = gorp_api_instance.gorp_country_latest_get(env=env) + api_response = api_instance.global_outlook_country_latest_get( + env=env + ) elif data_type == "global_latest": - gorp_data = gorp_api_instance.gorp_global_latest_get(env=env) + api_response = api_instance.global_outlook_global_latest_get( + env=env + ) + elif data_type == "regional_latest": - gorp_data = gorp_api_instance.gorp_regional_latest_get(env=env) + api_response = api_instance.global_outlook_regional_latest_get( + env=env + ) else: raise ValueError(f"Invalid data_type: {data_type}") + logger.info( + f"Successfully retrieved Global Outlook data for type: {data_type}" + ) + return pd.DataFrame([item.to_dict() for item in api_response.items]) - logger.info(f"Successfully retrieved GORP data for type: {data_type}") - - if isinstance(gorp_data, list): - df = pd.DataFrame([item.to_dict() for item in gorp_data]) - elif hasattr(gorp_data, "items"): - df = pd.DataFrame([item.to_dict() for item in gorp_data.items]) - else: - df = pd.DataFrame([gorp_data.to_dict()]) - - df = df.replace({np.nan: None}) - return df - - except ApiException as e: - logger.error(f"Exception when calling GorpApi->{data_type}: {e}") - raise + except Exception as e: + logger.error( + "Exception when calling GlobalOutlookApi->%s: %s", data_type, e + ) + raise def get_household_survey( self, survey_id: int, access_type: str, page_size: Optional[int] = 600 @@ -1174,7 +1125,7 @@ def get_household_surveys_list( ) raise - def get_household_xslform_definition(self, xls_form_id: int) -> pd.DataFrame: + def get_household_xlsform_definition(self, xls_form_id: int) -> pd.DataFrame: """Retrieves the complete XLS Form definition for a questionnaire. Args: @@ -1190,7 +1141,7 @@ def get_household_xslform_definition(self, xls_form_id: int) -> pd.DataFrame: Examples: >>> client = DataBridgesShapes("data_bridges_api_config.yaml") >>> # Get form definition - >>> form_def = client.get_household_xslform_definition(2067) + >>> form_def = client.get_household_xlsform_definition(2067) >>> # Access form fields >>> fields = form_def['fields'].iloc[0] @@ -1234,7 +1185,7 @@ def get_household_questionnaire(self, xls_form_id: int) -> pd.DataFrame: >>> questionnaire = client.get_household_questionnaire(2075) """ if self.xlsform is None: - self.xlsform = self.get_household_xslform_definition(xls_form_id) + self.xlsform = self.get_household_xlsform_definition(xls_form_id) return pd.DataFrame(list(self.xlsform.fields)[0]) def get_choice_list(self, xls_form_id: int) -> pd.DataFrame: @@ -1261,68 +1212,6 @@ def get_choice_list(self, xls_form_id: int) -> pd.DataFrame: choices["label"] = choices["choices"].apply(lambda x: x["label"]) return choices[["name", "value", "label"]] - # FIXME: Get scopes for AIMS then test the following function - def get_aims_analysis_rounds(self, adm0_code): - """ - Download all analysis rounds for AIMS (Asset Impact Monitoring System) data. - - Args: - adm0_code (int): The country adm0Code. - - Returns: - bytes: The downloaded data as bytes. - """ - with data_bridges_client.ApiClient( - self._setup_configuration_and_authentication(self.config) - ) as api_client: - api_instance = data_bridges_client.IncubationApi(api_client) - env = self.env - - try: - api_response = api_instance.aims_download_all_analysis_rounds_get( - adm0_code=adm0_code, env=env - ) - logger.info( - f"Successfully downloaded AIMS analysis rounds for adm0Code: {adm0_code}" - ) - return api_response - except ApiException as e: - logger.error( - f"Exception when calling IncubationApi->aims_download_all_analysis_rounds_get: {e}" - ) - raise - - # FIXME: Get scopes for AIMS then test the following function - def get_aims_polygon_files(self, adm0_code): - """ - Download polygon files for Landscape Impact Assessment (LIA) assets. - - Args: - adm0_code (int): The country adm0Code. - - Returns: - bytes: The downloaded polygon files as bytes. - """ - with data_bridges_client.ApiClient( - self._setup_configuration_and_authentication(self.config) - ) as api_client: - api_instance = data_bridges_client.IncubationApi(api_client) - env = self.env - - try: - api_response = api_instance.aims_download_polygon_files_get( - adm0_code=adm0_code, env=env - ) - logger.info( - f"Successfully downloaded AIMS polygon files for adm0Code: {adm0_code}" - ) - return api_response - except ApiException as e: - logger.error( - f"Exception when calling IncubationApi->aims_download_polygon_files_get: {e}" - ) - raise - def get_mfi_surveys_full_data( self, survey_id=None, page: Optional[int] = 1, page_size=20 ): @@ -1620,7 +1509,7 @@ def get_mfi_xls_forms( logger.error( f"Exception when calling XlsFormsApi->m_fi_xls_forms_get: {e}" ) - raise + raise def get_mfi_xls_forms_detailed( self, adm0_code=0, page: Optional[int] = 1, start_date=None, end_date=None @@ -1717,11 +1606,26 @@ def get_mfi_surveys_base_data( ) raise + def get_ipc_and_equivalent_data(self): + pass -if __name__ == "__main__": - import yaml + def get_hotpost_data(self): + pass + + def get_aims_data(self): + pass - # FOR TESTING - CONFIG_PATH = r"data_bridges_api_config.yaml" + def get_rpme_data(self): + pass + + def get_cari_data(self): + pass + + +if __name__ == "__main__": + pass + # import yaml - client = DataBridgesShapes(CONFIG_PATH) + # # FOR TESTING + # CONFIG_PATH = r"data_bridges_api_config.yaml" + # client = DataBridgesShapes(CONFIG_PATH) diff --git a/examples/example.py b/examples/example.py deleted file mode 100644 index bf5db1e..0000000 --- a/examples/example.py +++ /dev/null @@ -1,94 +0,0 @@ -from data_bridges_knots import DataBridgesShapes - -CONFIG_PATH = r"data_bridges_api_config.yaml" - -client = DataBridgesShapes(CONFIG_PATH) - -# %% COMMODITY DATA -# Get commodity unit list for a country -commodity_units_list = client.get_commodity_units_list( - country_code="TZA", commodity_unit_name="Kg", page=1, format="json" -) - -# Get commodity unit conversion list for a country -comodity_unit_conversion_list = client.get_commodity_units_conversion_list( - country_code="TZA", - commodity_id=1, - from_unit_id=1, - to_unit_id=2, - page=1, - format="json", -) - -# %% CURRENCTY DATA -# Get currency list -currency_list = client.get_currency_list( - country_code="TZA", currency_name="TZS", currency_id=0, page=1, format="json" -) - -# Get USD indirect quotation for a country -usd_indirect_quotation = client.get_usd_indirect_quotation( - country_iso3="TZA", currency_name="TZS", page=1, format="json" -) - -# %% MARKETS DATA - -# Get a complete list of markets in a country -markets_list = client.get_markets_list(country_code="TZA") - -# Get a complete list of markets in a country -markets_csv = client.get_markets_as_csv(adm0code=4, local_names=False) - -# Get markets near a given location by longitude and latitude within a 15Km distance -nearby_markets = client.get_nearby_markets(adm0code=56) - -# %% MARKET FUNCTIONALITY INDEX -# Get the MFI surveys for a given country -get_mfi_surveys = client.get_mfi_surveys(adm0_code=1) - -# Get the MFI functionality index for a given country (full data) -get_mfi_surveys_full_data = client.get_mfi_surveys_full_data(survey_id=3673) - -# Get the MFI functionality index for a given country (processed data) - -get_mfi_surveys_processed_data = client.get_mfi_surveys_processed_data(survey_id=3673) - -# Get MFI XLSForm information -mfi_xls_forms = client.get_mfi_xls_forms( - page=1, start_date="2023-01-01", end_date="2023-12-31" -) - -xls_forms = client.get_mfi_xls_forms_detailed( - adm0_code=0, page=1, start_date="2023-01-01", end_date="2023-12-31" -) - - -# %% FOOD SECURITY DATA -# Get IPC and equivalent food insecurity numbers for all countries -get_food_security_list = client.get_food_security_list() - -# %% GLOBAL OPERATION RESPONSE PLAN (GOPR) -# Get country-level latest data from the Global Operation Response Plan (GOPR) -country_latest_df = client.get_gorp("country_latest") # no data currently uploaded - -# Get global latest data from the Global Operation Response Plan (GOPR) -global_latest_df = client.get_gorp("global_latest") - -# Get regional latest data -regional_latest_df = client.get_gorp("regional_latest") - -# %% HOUSEHOLD ASSESSMENT & MONITORING DATA -# Get list of household surveys available -surveys_list = client.get_household_surveys() - -# Get survey data for a specific survey -survey_data = client.get_household_survey(survey_id=3094, access_type="official") - -# Get XLSForm definition for a specific survey -xlsform = client.get_household_xslform_definition(xls_form_id=1509) - -# Get survey questionnaire for a specific survey -questionnaire = client.get_household_questionnaire(xls_form_id=1509) - -# Get choice list for a specific survey -choices = client.get_choice_list(xls_form_id=1509) diff --git a/examples/example_R.R b/examples/example_R.R deleted file mode 100644 index 8c252aa..0000000 --- a/examples/example_R.R +++ /dev/null @@ -1,75 +0,0 @@ -# First, install reticulate if not already installed -#install.packages("reticulate") -library(reticulate) - -# Use the correct conda environment -use_condaenv("knots-3.11", required = TRUE) - -# Import the Python module through reticulate -data_bridges_knots <- import("data_bridges_knots") - -# Create client instance -config_path <- "data_bridges_api_config.yaml" -client <- data_bridges_knots$DataBridgesShapes(config_path) - -# COMMODITY DATA -# Get commodity unit list for Tanzania -commodity_units <- client$get_commodity_units_list( - country_code = "TZA", - commodity_unit_name = "Kg", - page = 1L, - format = "json" -) - -# CURRENCY DATA -# Get Tanzania Shilling exchange rates -exchange_rates <- client$get_usd_indirect_quotation( - country_iso3 = "TZA", - currency_name = "TZS", - page = 1L, - format = "json" -) - - - - -# ── FOOD SECURITY DATA ── -# Get IPC and equivalent food insecurity numbers for all countries -food_security_list <- client$get_food_security_list() - -# ── GLOBAL OPERATION RESPONSE PLAN (GOPR) ── -# Get country-level latest data -country_latest_df <- client$get_gorp("country_latest") # no data currently uploaded - -# Get global latest data -global_latest_df <- client$get_gorp("global_latest") - -# Get regional latest data -regional_latest_df <- client$get_gorp("regional_latest") - -# ── HOUSEHOLD ASSESSMENT & MONITORING DATA ── -# Get list of household surveys available -surveys_list <- client$get_household_surveys() - -# Get survey data for a specific survey -survey_data <- client$get_household_survey( - survey_id = 3094L, - access_type = "official" -) - -# Get XLSForm definition for a specific survey -xlsform <- client$get_household_xslform_definition( - xls_form_id = 1509L -) - -# Get survey questionnaire for a specific survey -questionnaire <- client$get_household_questionnaire( - xls_form_id = 1509L -) - -# Get choice list for a specific survey -choices <- client$get_choice_list( - xls_form_id = 1509L -) - - diff --git a/examples/example_STATA.do b/examples/example_STATA.do deleted file mode 100644 index 9eda3e4..0000000 --- a/examples/example_STATA.do +++ /dev/null @@ -1,54 +0,0 @@ -python set exect "path/to/python/env" - -python: - -""" -Read a 'full' Household dataset from Data Bridges and load it into STATA. -Only works if user has STATA 18+ installed and added to PATH. -""" - -from data_bridges_knots import DataBridgesShapes -from data_bridges_knots.labels import get_variable_labels, get_choice_labels, map_value_labels -from data_bridges_knots.load_stata import load_stata -import numpy as np -import stata_setup -from sfi import Data, Macro, SFIToolkit, Frame, Datetime as dt - -stata_path = r"E:\Program Files\Stata18" -stata_version = "mp" - -# Path to YAML file containing Data Bridges API credentials -CONFIG_PATH = r"data_bridges_api_config.yaml" - -# Example dataset and questionnaire from 2023 Congo CFSVA -CONGO_CFSVA = { - 'questionnaire': 1509, - 'dataset': 3094 -} - -# Initialize DataBridges client with credentials from YAML file -client = DataBridgesShapes(CONFIG_PATH) - -survey_data = client.get_household_survey(survey_id=CONGO_CFSVA['dataset'], access_type='full', page_size=800) -questionnaire = client.get_household_questionnaire(CONGO_CFSVA['questionnaire']) -choice_list = client.get_choice_list(CONGO_CFSVA['questionnaire']) - - -variable_labels = get_variable_labels(questionnaire) -# get value labels -value_labels = get_choice_labels(questionnaire) - -survey_data_value_labels = map_value_labels(survey_data, questionnaire) -# mapped.replace({np.nan: None}) - -# # Export -survey_data.to_csv(f"congo_cfsva_survey_data.csv", index=False) -questionnaire.to_csv(f"congo_cfsva_questionnaire.csv", index=False) -choice_list.to_csv(f"congo_csfsva_choice_list .csv", index=False) -survey_data_value_labels.to_csv(f"congo_cfsva_mapped.csv", index=False) - -# Load into STATA dataframe -ds = load_stata(survey_data_value_labels, stata_path=stata_path, stata_version=stata_version, variable_labels=variable_labels, value_labels=value_labels) - - -end \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0be2c9b..00edad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "data_bridges_knots" -version = "2.1.4" +version = "3.0.0" authors = [{ name = "Alessandra Gherardelli", email = "alessandra.gherardelli@wfp.org" }, {name = "Valerio Giuffrida", email = "valerio.giuffrida@wfp.org"}] description = "Multi programming language wrapper for WFP Data Bridges API client" readme = "README.md" @@ -14,14 +14,31 @@ keywords = ["VAM", "WFP", "data"] requires-python = ">=3.10" dependencies = [ - 'PyYAML', + 'PyYAML>=6', 'pandas>=2', - 'data-bridges-client==7.0.0', - "httpx>=0.28.1", + "data-bridges-client>=8.0.0", ] -[project.optional-dependencies] -dev = ["black", "bumpver", "isort", "pip-tools", "pytest"] + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.3.4", + "coverage-badge>=1.1.2", + "commitizen>=4.6.0", + "black", + "isort", + "ruff", + "mypy", + "mkdocs", + "mkdocs-material", + "mkdocstrings[python]", + "bandit", + "python-dotenv>=1.2.2", + "bumpver", + "pip-tools", + "setuptools>=82.0.1", +] STATA = ["stata-setup", "pystata"] R = [] @@ -30,8 +47,8 @@ R = [] repository = "https://github.com/WFP-VAM/DataBridgesKnots/" homepage = "https://github.com/WFP-VAM/DataBridgesKnots/" -[tool.setuptools] -packages = ["data_bridges_knots"] +[tool.setuptools.packages.find] +include = ["data_bridges_knots*"] [tool.setuptools.package-data] data_bridges_knots = ['country_list.json'] @@ -49,7 +66,7 @@ indent = 4 [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file -python_version = 3.9 +python_version = 3.10 pretty = true show_traceback = true color_output = true @@ -78,6 +95,10 @@ warn_unused_ignores = true # Directories that are not visited by pytest collector: norecursedirs =["hooks", "*.egg", ".eggs", "dist", "build", "docs", ".tox", ".git", "__pycache__"] +markers = [ + "integration: tests hitting real API" +] + # Extra options: addopts = [ "--strict-markers", @@ -87,48 +108,31 @@ addopts = [ # Only collect tests from the tests directory testpaths = ["tests"] -[tool.coverage.run] -source = ["tests"] - -[coverage.paths] -source = "wfp-survey-toolbox" -[coverage.run] +[tool.coverage.run] +source = ["data_bridges_knots"] branch = true -[dependency-groups] -dev = [ - "pytest-cov>=6.0.0", - "pytest>=8.3.4", - "coverage-badge>=1.1.2", - "commitizen>=4.6.0", - "black", - "isort", - "ruff", - "mypy", - "mkdocs", - "mkdocs-material", - "mkdocstrings[python]", - "bandit", -] - [tool.coverage.report] fail_under = 50 show_missing = true +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "v$version" +version_scheme = "semver" +version_provider = "uv" +update_changelog_on_bump = true + +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple" + [tool.uv.sources] data-bridges-client = { index = "wfp-hip-pypi" } [[tool.uv.index]] name = "wfp-hip-pypi" url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" -explicit = true +# explicit = true -[tool.commitizen] -name = "cz_conventional_commits" -version = "2.1.4" -tag_format = "$version" -version_files = [ - "pyproject.toml:version", -] -update_changelog_on_bump = true diff --git a/tests/conftest.py b/tests/conftest.py index 70146c7..7029273 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,8 +17,8 @@ def sample_survey_df(): @pytest.fixture -def sample_xslform_df(): - """Fixture providing a sample questionnaire in XSLForm""" +def sample_xlsform_df(): + """Fixture providing a sample questionnaire in xlsForm""" client = DataBridgesShapes("data_bridges_api_config.yaml") df = client.get_household_questionnaire( 1883 @@ -27,7 +27,7 @@ def sample_xslform_df(): @pytest.fixture -def sample_xslform_pkl(): +def sample_xlsform_pkl(): """Fixture providing a sample survey dataset""" return pd.read_pickle("tests/static/test_xlsform.pkl") diff --git a/tests/test_client.py b/tests/test_client.py index 7641b8e..621e4e1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,2 +1,255 @@ -def test_client(): - pass +import pandas as pd +import pytest +from dotenv import load_dotenv + +from data_bridges_knots.client import DataBridgesShapes, config_from_env + +pytestmark = pytest.mark.integration + +# ------------------------- +# ✅ Fixtures +# ------------------------- + + +@pytest.fixture +def config_dict(): + load_dotenv() + config = config_from_env() + + return config + + +@pytest.fixture +def client(config_dict): + return DataBridgesShapes(config_dict) + + +# ------------------------- +# ✅ 1. Import +# ------------------------- + + +def test_import(): + from data_bridges_knots.client import DataBridgesShapes + + assert DataBridgesShapes is not None + + +# ------------------------- +# ✅ 2. Config +# ------------------------- + + +def test_client_init(config_dict): + client = DataBridgesShapes(config_dict) + assert isinstance(client, DataBridgesShapes) + + +# ========================================================= +# ✅ 3. SUCCESS TESTS (expect 200) +# ========================================================= + +# ========================================================= +# ✅ PRICES & CURRENCY +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + # Prices + ("get_prices", ("KEN",), {}), + ( + "get_prices", + ("KEN",), + {"start_date": "2025-01-01", "end_date": "2025-12-31"}, + ), + # Exchange rates + ("get_exchange_rates", ("ETH",), {}), + # Currency + ("get_currency_list", (), {"country_iso3": "TZA"}), + ("get_currency_list", (), {"currency_name": "ETB"}), + ("get_currency_list", (), {"currency_id": 1}), + # USD indirect quotation + ("get_usd_indirect_quotation", (), {"country_iso3": "ETH"}), + ("get_usd_indirect_quotation", (), {"currency_name": "ETB"}), + ], +) +def test_prices_and_currency_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ COMMODITIES +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + # Commodities + ("get_commodities_list", (), {}), + ("get_commodities_list", (), {"country_iso3": "TZA"}), + ("get_commodities_list", (), {"commodity_name": "Maize"}), + ("get_commodities_list", (), {"commodity_id": 123}), + # Commodity units + ("get_commodity_units_list", (), {"country_iso3": "TZA"}), + ("get_commodity_units_list", (), {"commodity_unit_name": "Kg"}), + ("get_commodity_units_list", (), {"commodity_unit_id": 5}), + # Commodity conversions + ("get_commodity_units_conversion_list", (), {}), + ("get_commodity_units_conversion_list", (), {"country_iso3": "TZA"}), + ], +) +def test_commodities_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ MARKETS +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + ("get_markets_list", ("AFG",), {}), + ("get_markets_as_csv", ("AFG",), {}), + ("get_markets_as_csv", ("AFG",), {"local_names": True}), + ("get_nearby_markets", ("AFG", 34.515, 69.208), {}), + ], +) +def test_markets_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ GEOJSON +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + ("get_market_geojson_list", ("AFG",), {}), + ], +) +def test_geojson_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, dict) + + +# ========================================================= +# ✅ ECONOMIC + OUTLOOK +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + ("get_economic_indicator_list", (), {}), + ], +) +def test_economic_indicator_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ HOUSEHOLD & SURVEYS +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + ("get_household_survey", (3094, "official"), {}), + ("get_household_survey", (3094, "public"), {}), + ("get_household_surveys_list", (), {"country_iso3": "COG"}), + ( + "get_household_surveys_list", + (), + { + "country_iso3": "COG", + "start_date": "2024-01-01", + "end_date": "2024-12-31", + }, + ), + ("get_household_xlsform_definition", (2067,), {}), + ("get_household_questionnaire", (2075,), {}), + ("get_choice_list", (123,), {}), + ], +) +def test_household_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ MFI +# ========================================================= + + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + ("get_mfi_surveys", (), {}), + ("get_mfi_surveys_full_data", (), {"survey_id": 123}), + ("get_mfi_surveys_processed_data", (), {"survey_id": 123}), + ("get_mfi_surveys_base_data", (), {"survey_id": 123}), + ("get_mfi_xls_forms", (), {}), + ("get_mfi_xls_forms_detailed", (), {"adm0_code": 231}), + ( + "get_mfi_xls_forms_detailed", + (), + { + "adm0_code": 231, + "start_date": "2023-01-01", + "end_date": "2023-12-31", + }, + ), + ], +) +def test_mfi_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# # ========================================================= +# # ✅ FORBIDDEN ENDPOINTS +# # ========================================================= + +# @pytest.mark.integration +# @pytest.mark.parametrize( +# "func,args,kwargs", +# [ +# ("get_household_survey", (3094, "draft"), {}), +# ("get_global_outlook", ("country_latest",), {}), +# ("get_global_outlook", ("global_latest",), {}), +# ("get_global_outlook", ("regional_latest",), {}), +# ], +# ) +# def test_forbidden_endpoints(client, func, args, kwargs): +# result = method(*args, **kwargs) + +# if isinstance(result, pd.DataFrame): +# assert result.empty or "403" in str(result) +# else: +# assert "403" in str(result) diff --git a/tests/test_labels.py b/tests/test_labels.py index b4befa3..75e359f 100644 --- a/tests/test_labels.py +++ b/tests/test_labels.py @@ -5,8 +5,8 @@ # from data_bridges_knots.labels import get_choice_labels, get_variable_labels -# def test_sample_questionnaire_df(sample_xslform_df): -# assert isinstance(sample_xslform_df, pd.DataFrame) +# def test_sample_questionnaire_df(sample_xlsform_df): +# assert isinstance(sample_xlsform_df, pd.DataFrame) # # % TESTS FOR get_variable_labels @@ -52,32 +52,32 @@ # assert result == expected -# def test_return_column_labels_as_df(sample_xslform_df): -# result = get_variable_labels(sample_xslform_df, format="df") +# def test_return_column_labels_as_df(sample_xlsform_df): +# result = get_variable_labels(sample_xlsform_df, format="df") # assert isinstance(result, pd.DataFrame) -# def test_return_column_labels_as_dict(sample_xslform_df): -# result = get_variable_labels(sample_xslform_df, format="dict") +# def test_return_column_labels_as_dict(sample_xlsform_df): +# result = get_variable_labels(sample_xlsform_df, format="dict") # assert isinstance(result, Dict) -# def test_return_column_labels_as_json(sample_xslform_df): -# result = get_variable_labels(sample_xslform_df, format="json") +# def test_return_column_labels_as_json(sample_xlsform_df): +# result = get_variable_labels(sample_xlsform_df, format="json") # assert isinstance(result, str) # # % TESTS FOR get_choice_labels -# def test_return_value_labels_as_df(sample_xslform_df): -# result = get_choice_labels(sample_xslform_df, "df") +# def test_return_value_labels_as_df(sample_xlsform_df): +# result = get_choice_labels(sample_xlsform_df, "df") # assert isinstance(result, pd.DataFrame) -# def test_return_value_labels_as_dict(sample_xslform_df): -# result = get_choice_labels(sample_xslform_df) +# def test_return_value_labels_as_dict(sample_xlsform_df): +# result = get_choice_labels(sample_xlsform_df) # assert isinstance(result, Dict) -# def test_return_value_labels_as_json(sample_xslform_df): -# result = get_choice_labels(sample_xslform_df, "json") +# def test_return_value_labels_as_json(sample_xlsform_df): +# result = get_choice_labels(sample_xlsform_df, "json") # assert isinstance(result, str) diff --git a/uv.lock b/uv.lock index 2420502..28e8134 100644 --- a/uv.lock +++ b/uv.lock @@ -209,11 +209,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -499,48 +499,36 @@ wheels = [ [[package]] name = "data-bridges-client" -version = "7.0.0" +version = "8.0.0" source = { registry = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" } dependencies = [ + { name = "httpx" }, { name = "pydantic" }, { name = "python-dateutil" }, { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/data-bridges-client/data_bridges_client-7.0.0.tar.gz" } +sdist = { url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/data-bridges-client/data_bridges_client-8.0.0.tar.gz" } wheels = [ - { url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/data-bridges-client/data_bridges_client-7.0.0-py3-none-any.whl" }, + { url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/data-bridges-client/data_bridges_client-8.0.0-py3-none-any.whl" }, ] [[package]] name = "data-bridges-knots" -version = "2.1.4" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "data-bridges-client" }, - { name = "httpx" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, ] -[package.optional-dependencies] -dev = [ - { name = "black" }, - { name = "bumpver" }, - { name = "isort" }, - { name = "pip-tools" }, - { name = "pytest" }, -] -stata = [ - { name = "pystata" }, - { name = "stata-setup" }, -] - [package.dev-dependencies] dev = [ { name = "bandit" }, { name = "black" }, + { name = "bumpver" }, { name = "commitizen" }, { name = "coverage-badge" }, { name = "isort" }, @@ -548,31 +536,30 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "pip-tools" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "python-dotenv" }, { name = "ruff" }, + { name = "setuptools" }, +] +stata = [ + { name = "pystata" }, + { name = "stata-setup" }, ] [package.metadata] requires-dist = [ - { name = "black", marker = "extra == 'dev'" }, - { name = "bumpver", marker = "extra == 'dev'" }, - { name = "data-bridges-client", specifier = "<=7.0.0", index = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "isort", marker = "extra == 'dev'" }, + { name = "data-bridges-client", specifier = ">=8.0.0", index = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" }, { name = "pandas", specifier = ">=2" }, - { name = "pip-tools", marker = "extra == 'dev'" }, - { name = "pystata", marker = "extra == 'stata'" }, - { name = "pytest", marker = "extra == 'dev'" }, - { name = "pyyaml" }, - { name = "stata-setup", marker = "extra == 'stata'" }, + { name = "pyyaml", specifier = ">=6" }, ] -provides-extras = ["dev", "stata", "r"] [package.metadata.requires-dev] dev = [ { name = "bandit" }, { name = "black" }, + { name = "bumpver" }, { name = "commitizen", specifier = ">=4.6.0" }, { name = "coverage-badge", specifier = ">=1.1.2" }, { name = "isort" }, @@ -580,9 +567,17 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings", extras = ["python"] }, { name = "mypy" }, + { name = "pip-tools" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "ruff" }, + { name = "setuptools", specifier = ">=82.0.1" }, +] +r = [] +stata = [ + { name = "pystata" }, + { name = "stata-setup" }, ] [[package]] @@ -1906,6 +1901,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -2272,11 +2276,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.0.7" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/47/b215df9f71b4fdba1025fc05a77db2ad243fa0926755a52c5e71659f4e3c/urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", size = 282546, upload-time = "2023-10-17T17:46:50.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/b2/b157855192a68541a91ba7b2bbcb91f1b4faa51f8bae38d8005c034be524/urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e", size = 124213, upload-time = "2023-10-17T17:46:48.538Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -2334,88 +2338,88 @@ wheels = [ [[package]] name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, - { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, - { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, - { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, - { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, - { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, - { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, - { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f0/5e969d268d59e6035f2f1960da9e82fe6db24a7b8abe8e36a78c27cb3e2b/wrapt-2.2.0.tar.gz", hash = "sha256:b70a0b75b0a5a58d04aad06b3f167d49e729381d3417413656220c0cd7617847", size = 125173, upload-time = "2026-05-21T04:51:39.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/c6/17263421accbbc27bc4c8535eb9215a18a914d15eab4829a59e93f5ad29d/wrapt-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b3946f0ff079623dc4f117363040433be390bfebce3719de50dfecbf31efdf0", size = 80088, upload-time = "2026-05-21T04:49:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/40/0d/81230469d6a7c6878e0763b7d84ebab6da3625ce62e8fd83086c982b8726/wrapt-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a50822bbbefb90b132a780c17356062a2452cd5525bfa4b5b596fd6474cceaa6", size = 81177, upload-time = "2026-05-21T04:49:12.589Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/a09c8346f270ab1328ba9e6594d73d86450de22bc4d29a23167ff82d7ec1/wrapt-2.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:29c0b2c075f8854b3345be584ab3d84f8968c45605d1914be1c94939cef5d702", size = 152069, upload-time = "2026-05-21T04:49:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/5e/79b6d6295733b9fa1bee096120a556366951e3c0140234310080ede40e42/wrapt-2.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0d4a79d9af893d80caa5b709e024dd2d387f3f047008286036143f118d7010", size = 154319, upload-time = "2026-05-21T04:49:16.097Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4d/a72b95e9389a4f350150d9a3ce9b263bad16f476551004a12de167ae7d0b/wrapt-2.2.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10e8f78948d13369b770fc17bf72272aac98b4b92d49a38f479abf718f6b615b", size = 148874, upload-time = "2026-05-21T04:49:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/0a/56/ffec9a08beb6fcfc30b259c6b8b36741675c58de69f1c035746f06fa4a07/wrapt-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a4482d1d4108052827b354850bd6e3d1ed56262cbe4b0e8051876c298fb99280", size = 153250, upload-time = "2026-05-21T04:49:19.413Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c5/7ab2e23d594f28b2fc00bd19e82163bce2f77e2bc916e9dc247e0f886a41/wrapt-2.2.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:43c36019a690b2cb089665eab01a50c92d814553c6e57ff03d2c68e63ce8f00b", size = 147902, upload-time = "2026-05-21T04:49:20.749Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/565965b9613dccf20286880e314cc41b20a85b2f4a7fe275786bb08b330e/wrapt-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb9336f2dc99de00c9e58487cae5541ee4d79e859377b6312d98973d4661c584", size = 151334, upload-time = "2026-05-21T04:49:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/0e/1890765d97cc3016ba444f8158856a35f8944785660eb88ff73b2d1e2b9b/wrapt-2.2.0-cp310-cp310-win32.whl", hash = "sha256:63a09b40bba3b2482983e2aeba6e45e20e1f567821ac89c8922229ecc1de7f65", size = 77405, upload-time = "2026-05-21T04:49:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/02/02/a943f4d0f9084a354a722468ff2899e9177449f03f4bff8ef234792f27ad/wrapt-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2ff803b3607cd76cb9b853b03d15279c7ffc8ba69e69f76304cd23d2722f2b65", size = 80353, upload-time = "2026-05-21T04:49:25.87Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c2/2c7838cf368c04aebaef93f756f5b76e0eb12bb710c2926111dc96e5aaf9/wrapt-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:af17d3ce1e2cc5d22ae8fe8921d7801c980ea3f5d6da4ecbd0f85c4f9e030181", size = 79121, upload-time = "2026-05-21T04:49:27.778Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/a3eb4a1ef48fc743c4107e82d5b1144287ef8353b0f6844fee1add28d663/wrapt-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b93e1ccddbdf59cec4f7683dc84bc56eb61628eb01b22bdefc15f04cd09f8fae", size = 80324, upload-time = "2026-05-21T04:49:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/03248de44165f9c06dc23da981f3d58889ee2600004289c7afd12ef316b1/wrapt-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97fbe7a0df35afe37e7e2f053dee6300a3eed00055cfd907fa51161e22c40236", size = 81201, upload-time = "2026-05-21T04:49:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/39/99/ed8c0f9f0d3c9631259bf5c5d776ec7a70d6d888ce060ad4758f00a29683/wrapt-2.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d8f6cf451ec4aab0cdbad128d9be1219e95ceaa9940566d71570b2d820ee50b3", size = 158770, upload-time = "2026-05-21T04:49:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/25/fc/6eed4204b30562f113e40151b94ec1ee565c040d90623a4223742cf5aa68/wrapt-2.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f1dc1d1a2f0b081d8c1eef2203e61717b537a1bcb0d8e4d1405aeb15aa85c34", size = 160322, upload-time = "2026-05-21T04:49:33.959Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3d/cb9d33c140cce69e025d946deac44c636ce16a079cd4410722b552aecb5e/wrapt-2.2.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952ec99e71d584a0e451795dbd468909c8794727ecddd9ebb4fe9803e2803f1e", size = 153088, upload-time = "2026-05-21T04:49:35.715Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/e452de05a75c008acef9055dd9a58fc6a4d08a5e42747394a91030f83169/wrapt-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33ff34dc349320dc16ebe0cdf70dddf5ae9328f4a448823a00f37976d0cc2234", size = 159258, upload-time = "2026-05-21T04:49:38.249Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/c06ee1605a5b11da535b64e26c9f2330de7a8e3a2253afc533f37a5a682f/wrapt-2.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d23ea5a8e4ae99640d027d2fd05c9d03f8d24d561fc26c0462e96affa31bf408", size = 152155, upload-time = "2026-05-21T04:49:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/d7cb1d184afe5a1db15515f86758fd08fa795a650f2af18ff221758921d7/wrapt-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c95f72d212e1f178f9619b77fd7ee3533e82ded6a5ad119dd88134e185ee3b0", size = 157920, upload-time = "2026-05-21T04:49:41.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/80/5bbdade010313edbb14afbdd916a054c74c99c2f04b0f8358086c728815a/wrapt-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db93eebcf951f9ee41d75dc0423378fa918fc6706db59bc20c02f6563b6b210d", size = 77572, upload-time = "2026-05-21T04:49:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/5f/32/9df5dd381c2d4d9f14d8d442de4efd8ef8fda3df8b25a384e7060a6d91a8/wrapt-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c7ee3a3737d9656ddf2c9cc1f1548ec963d966251e899561da142697d33a9d", size = 80624, upload-time = "2026-05-21T04:49:44.411Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5c/3c441a01c9e1f072f0a9c062a3aa709b3fe488af649ecb0b74206e5a9754/wrapt-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:7e291fa9129d9998ed5035390d4bb9cf429c489f40e5ddaa06a1e83ed52048a7", size = 79003, upload-time = "2026-05-21T04:49:45.687Z" }, + { url = "https://files.pythonhosted.org/packages/83/ac/0d40f7f625b78d698dd8fcaf2df31585d2185dd0c261b82f7cc334c53168/wrapt-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8a76b27fe0d600f8a34313e1a528309aa807a16aa3a72000619bc56339020125", size = 80992, upload-time = "2026-05-21T04:49:47.024Z" }, + { url = "https://files.pythonhosted.org/packages/a0/56/bec7ac3b1c40bee400aecf0db3abee9d3461fd8f02eb42fb02693092b3d9/wrapt-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:778aa2f59615973f2637d9025a708b69196c4814f38d905647fa1a56d7ff6b79", size = 81648, upload-time = "2026-05-21T04:49:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a9/6ecf97645bde3fc5faa980516f7007ece0b38d3219e5add54042d3ae8b4e/wrapt-2.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5b7f10aa09d1f5abfe3ccd022dec566a5010465b98b3755cc0705a762547101f", size = 168683, upload-time = "2026-05-21T04:49:49.703Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/de03c995ade9b215f2c019be6442fc206b05ddcbec9d2f81bf94157aef47/wrapt-2.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d98bf0078736df226e36875aa58a78f9d3b0888bcf585144fb30edbbf7145238", size = 170982, upload-time = "2026-05-21T04:49:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/19/f8/6255eb9827dbd137569de68554b1e9535c3ac79cdbc377af3da415891807/wrapt-2.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b62f40eb24ccf05246d203461c8920889fd38dce76978df16fe28e6f0128447d", size = 160002, upload-time = "2026-05-21T04:49:53.598Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dd/962a9281d9c35e21c5a662c7d05c2af0108a3c833d2d6ab2eb546e520f7e/wrapt-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8ce59cad2ee5a4d58ee647c4ed4d9adc4282ffdc31e98cba7f831536776a0f9", size = 168827, upload-time = "2026-05-21T04:49:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ef/6a10e1200b2238be6da767d1814ab298f20e533a6c210f9ae6423ee3139c/wrapt-2.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:bb7c060c3faa78fe066b6b1c65de285d8d61fb6e01ee8195625b9636c3cd9775", size = 158164, upload-time = "2026-05-21T04:49:56.587Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b2/4f5f4c722aa730eb2c0723ee8f32d0d7315d07173cdac0d08b7b92bbab39/wrapt-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4297b7338cfa48b5cfefc7416d2ae52b0aad89e9b24da479ec010717b987c07f", size = 167111, upload-time = "2026-05-21T04:49:57.996Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/93fabbf2b505b610d019ec537c9dfa785a96920dcfc2ff8f57727aa54625/wrapt-2.2.0-cp312-cp312-win32.whl", hash = "sha256:9b58e2cdbcfe2278a031a12a7d73836d66bc1e9e65f97c63ea0a022f2f9f351b", size = 77867, upload-time = "2026-05-21T04:49:59.339Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/1564bcd9863dbf2cca3a687f53a6eeaaa08850e331948f1c4c7818401e88/wrapt-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:199abadf7dcceab4bdc5bfe356275a56b1cb429296e283da2fe90c20b09f8d07", size = 80827, upload-time = "2026-05-21T04:50:01.212Z" }, + { url = "https://files.pythonhosted.org/packages/67/04/354d2fd146936dccf55aced66a606f6e1665435e3119765acb00a8753eb3/wrapt-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8d40f1fb34d600b3eaf812941d6bcf313075728868cad1dafb7021e6a4e77983", size = 79094, upload-time = "2026-05-21T04:50:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bc/00d23a39b5f002dfa20f7441721bb44198e7c7b4a6b3f3d7b4ff88fe2dc6/wrapt-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:49c7ad697d6b13f322a1c3bb22a1c66827d5c0d303a4479e327210ee4d4ad179", size = 80816, upload-time = "2026-05-21T04:50:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/d0c5ecb47818be6d1717ea51eec1285f8d53777994fe44deaf9d7299f65c/wrapt-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07dd562ebb774cad070eeedb93c7a29647979e30f0cfd1f5c9b9f803f687b6f4", size = 81346, upload-time = "2026-05-21T04:50:05.882Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/a68afc8f7b085bc34fa6e17a120a10b2a9e27579369c79fb40f31ba95d69/wrapt-2.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5b865e611c186d15366964e3d9500af504920ce7b92a211d61a83d2d3c42a508", size = 166769, upload-time = "2026-05-21T04:50:07.604Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/b3dadb67dd612223615438ce080be6bd1fee6de12ee16b2ff9725b3169b1/wrapt-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12331011cbf76b782d0beec7c7ed880f51454c127ab12012cfaecf56de01a80c", size = 166825, upload-time = "2026-05-21T04:50:09.129Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b3/9cb0277fb0f5c853aa6a91f384784e73db4c3db8ff0f405bc3f71d93daae/wrapt-2.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8ae3f4b50a3befa56da0f09d2b71a192454ce48e8887823dbc9228cdbb610f3", size = 157882, upload-time = "2026-05-21T04:50:10.954Z" }, + { url = "https://files.pythonhosted.org/packages/83/f6/e4295b9dadfd73d1db30fced3cdf1d083787d77857257998c5b9dda8b3d9/wrapt-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:370b2c36e8fee503c275e39b4588d74412cd0a7792f7f3a7b54c44c4d33d4884", size = 165791, upload-time = "2026-05-21T04:50:12.698Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/e66764a3aefb45a3a60ac76ea6878417a13f98e67f046f8e78b0a9ca6063/wrapt-2.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9040b15216e07ed68762e44ff231a460036e4bf3543f83988f669e7078847b2c", size = 156574, upload-time = "2026-05-21T04:50:14.28Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/e3355e82cc765a411283ff4335ab41034d4eab9f5226b3e5840bebcaaf96/wrapt-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8062689c0e6faf0c2532f566a492fb48ba60923c2cd6effda7cac9639dbdc1f3", size = 165943, upload-time = "2026-05-21T04:50:16.373Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/eda5a79813cd9ee86cd7275b9eac5338166886a5ffc9dcf881a3068d03a3/wrapt-2.2.0-cp313-cp313-win32.whl", hash = "sha256:a3848854af260eb4cc33602c685524fff7c8816f033325f750c7fc75c6deccf9", size = 77824, upload-time = "2026-05-21T04:50:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/ba/de/1eadc4caa3797a33d231572435eed9116d24f56dc6c909c43b59092fbb37/wrapt-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b8111f8f5b8553c066caa26193921dea4185efecf1f9b38473054205137800", size = 80737, upload-time = "2026-05-21T04:50:20.038Z" }, + { url = "https://files.pythonhosted.org/packages/11/34/aeda6d757664a569a19d3e88e89f1c52134bbaa59b053bb316c69c71c459/wrapt-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:195db5b92deba6feb818732694ad478abb8a529d97a113cc256e5e49ee2dd80d", size = 79094, upload-time = "2026-05-21T04:50:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/92/c7/3bfdcddd4c0281d104305e473953f1402bcae1898089656b6a9567a1e5cd/wrapt-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cf93c441b11c1f3ae2ccf1e8d876939b301b3234ec19f311ab0e7543a9d4427e", size = 82751, upload-time = "2026-05-21T04:50:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/dc/96/37cc2bf299cfbf21f6bb7dfd0ba590e2d29f9e1fe6aa334a97395f4406dc/wrapt-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b208a5dd6f9da3d4b17aa2e4f8ca9c5dc6b9a2ed571fdef9ed465102487b445c", size = 83315, upload-time = "2026-05-21T04:50:25.085Z" }, + { url = "https://files.pythonhosted.org/packages/18/b0/bd4b4c51243a38009cc1c96f0503a535a7d8044636626bc7c545e766e73d/wrapt-2.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5248171d3cd33f12c144e7aa1222983cb6ab42651e985ce51fec400a876afbfd", size = 203752, upload-time = "2026-05-21T04:50:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b6/aee7c4fd7f19026d464ca7fd8a83efa5f3168ed33897ca0d1ec83bd15de4/wrapt-2.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f663528d6ea1804d279462671b2bf98a4c0d8a4a8dd319bb3ee0629b743387f", size = 209665, upload-time = "2026-05-21T04:50:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/91/be1181e580cd20a2584260285aa25fa9eb64a27a5921a431008910ea5d70/wrapt-2.2.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb240700f3b597c1d40d0932bfed2f4130fec2f02b8c2cb0bcdae45d321cb691", size = 194678, upload-time = "2026-05-21T04:50:30.307Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/cae7b5f26bf1385f562b7904db23b686e66a4f4f4b3496675531b1d0d968/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1bf3ea62734b24c0241442d8b7684ef53a8de6cad0c2eba1e99fd2297b4a92e4", size = 205364, upload-time = "2026-05-21T04:50:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/647312c3fcef95e6c65fd4c11efe4575cd021ac0074f3000cb066fc67c9e/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ec257eedd8c3988cf76e351e949e3a56a61d90f4bb4e060de2ebfa6603df2a42", size = 192139, upload-time = "2026-05-21T04:50:33.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/c4/b40d8d176979b9397a4cfcc9eaafdd20697fc6e62293d70b1951d422b988/wrapt-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f58e1aa46c204171a2faa49b1ef2953edebb3913d270bb3bae7e970f254c9293", size = 199221, upload-time = "2026-05-21T04:50:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cf/71f00a6a0e9f5244c0bcc4e445d1087467d1c80e788637929bea0a1ea637/wrapt-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:615be1d2b21450748e759bed7bf9ba8bc28307e91cb96b6e968f54f39e938ee5", size = 79438, upload-time = "2026-05-21T04:50:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/b7/01/8219ee5e1491fdd880564af04a809eb8866481faff5cce6105174202667d/wrapt-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0680304db389599691bac06a2f9fb3f0ed06af59f132d35801a38cf6c321ab59", size = 83024, upload-time = "2026-05-21T04:50:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/ec61c19ea596299b0f0fca9f5ff82418a5152d933772bac90c61a4b06c30/wrapt-2.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:60bef9dc4348a76e9c2981ec4b06b779bac02556af4479030e6f62b18545b3cc", size = 80282, upload-time = "2026-05-21T04:50:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/11/b7/dd4278d51621fd5054f840744be1c830b37e9d7b9b22b5590eb69c5039a3/wrapt-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5c17982ccfece323bb297a195c9602ef407819199d8dbf99b8041770513fd68f", size = 80861, upload-time = "2026-05-21T04:50:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/06efd37278eaee793521aee41091cb29fe20603dc5bd2f5cdc4e73fe9ce8/wrapt-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d2aab40474b6adae53d14d1f6a7785f4346a93c072adf1e69ca11a1b6afc789e", size = 81436, upload-time = "2026-05-21T04:50:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/21/95/46922f9415f109506f8bdfd903138dbde8a507a70ca02904b8dcffaac171/wrapt-2.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db48e2623a8aca63dfcfa7e574a5f3a9f760be1c464ee23f6387f70cc9112aa2", size = 166655, upload-time = "2026-05-21T04:50:45.951Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/601af72054c2166e11781a30b0fd6f7d500e9186351e73f8ff5d923afcee/wrapt-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f990f1b5c8ee4ff980bdef3f73f50728fd911b9ab8de8c43144e8019dcd845ff", size = 166257, upload-time = "2026-05-21T04:50:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/b2e96a62cd572f186eb94be906d4854dd301b20a3b30b648c8ddab11a2fb/wrapt-2.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c990d58100f9ebb8e7a20bd2e7bd3c60838be38c5bbccdd35041bc9f36dc0cea", size = 157694, upload-time = "2026-05-21T04:50:49.215Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/77fa31bda9344ca76d6a8eb6f5bd274aea1a7e24d6279b21fc2349d41fbe/wrapt-2.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:686f1798727bf4a708df015ca782b20abe99b3664e1ee9786b7712b0e2310586", size = 166036, upload-time = "2026-05-21T04:50:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/118d00ad41f270128aa94a80b8150c5b720c18e06dc1a2291795c33839ec/wrapt-2.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b9733ef187cf05e774484ed2f703992a44429050f1cfea2e94dac543da78292", size = 156437, upload-time = "2026-05-21T04:50:52.415Z" }, + { url = "https://files.pythonhosted.org/packages/24/43/16017c26a1eeccbbf8f79f5172095bf9b0cb7183ac9bfc4a3c2c9fc37675/wrapt-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:231e2728ba04536821d2327ad2b3cb2c20cc79197fe5c30ddf71b12d95febe10", size = 165492, upload-time = "2026-05-21T04:50:54.16Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d5d59f0a074e192f1cdafdeafca3d1aca25c3dd9172e0418fd04a912b864/wrapt-2.2.0-cp314-cp314-win32.whl", hash = "sha256:319720847afa6c58c32f84f9743bdcf34448ae56908c00f409764c627ff2c1fe", size = 78343, upload-time = "2026-05-21T04:50:56.028Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f5/c7fbbcbd8285f1999666115a793890a38e8b88744b8c3630059a0efa88bb/wrapt-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:628fbd908649611c8b9293e2e050231f1e230be152e7d38140e3b818ec6aade0", size = 81144, upload-time = "2026-05-21T04:50:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/93/aa/152902a4b85cb55daad6e383a91ca5e23fd8d56132a4aa44987b7154f5e3/wrapt-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b4ce4240a3f095e77cfcc5aed6001bd63af13ea53c35ef496af1a5a972e7eaa9", size = 79573, upload-time = "2026-05-21T04:50:59.307Z" }, + { url = "https://files.pythonhosted.org/packages/af/fe/a25c3eee98417de1caf541c1b234bbc3a8b0ce4817b0c8934ca57bfe3e89/wrapt-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f0318a47d23c9407f4f94c06824662499e889ab8c192c1162e4f542a118fd700", size = 82844, upload-time = "2026-05-21T04:51:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/31060eb2f475b7798926f46c1779ec93329a48730cbeb8f9c0855162f97b/wrapt-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a094508b7cd6e583378f3cf50f125814961660225bad88f4ecaa691e30b09e1", size = 83321, upload-time = "2026-05-21T04:51:02.253Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/62702f8bdaf509e444ec38bf142122db8c5ebbdfe6e2ca8e1dd7d43fb574/wrapt-2.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:115ff1501c11ac0e267c4afd6f6b3dd24b48afcc77b029e6062f71b12bce1d79", size = 203740, upload-time = "2026-05-21T04:51:03.8Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c6/c9ea3537ea759edcc856a32fc2d16abee41d7474f853bf00089058c0a33e/wrapt-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d4156fd35d0bdab58eac4a6854fbd053a59544fc57eb66e977b3c13c087a1c", size = 209671, upload-time = "2026-05-21T04:51:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/5925232cf614c23969b2267d954976e288993ef9e94a74eba4f26ad41232/wrapt-2.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b0aa81f4a3d0203ae8450eae5e794540afbf00a97dd0b81accbe5b4a5362cbb", size = 194717, upload-time = "2026-05-21T04:51:07.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/95/b824ac1e5900f39f80d0d4e97cf59389b078d0fed3551f471911f9b46281/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74b7949da2ffcd79869ac1e90946c14ce61a714269403a879ea9ed85a993c81f", size = 205335, upload-time = "2026-05-21T04:51:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/623708a153bb1a519260bf61086c5f381196a7d505ac729f7979b0d1a957/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c7af243871699358ebf34a770205bf2b61ccb17a0b003e8726d2028cc36ce364", size = 192170, upload-time = "2026-05-21T04:51:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/d771a75386676fe08086affe57b0f7cffafe528642ae5ebf95200811248e/wrapt-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb9d0c3f416e2c7c37498d1716fe323379da8b4e860da3d3818a6ec8fff7b7e5", size = 199200, upload-time = "2026-05-21T04:51:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/afd8991950e38f32c73008a3f2cd834cab32e338cc1997b7a39272a22cbc/wrapt-2.2.0-cp314-cp314t-win32.whl", hash = "sha256:4d5b485a6f617825fa7449f5025ebcdad9355acb328cb6d198ba225762219bc0", size = 80206, upload-time = "2026-05-21T04:51:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/d10901fb7686ec642e22d75d260f07ba6e05d28d5c83cb1efdc8d5c03e07/wrapt-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cccce5c70a209eb385c82d063f332ed97fc02d1cf7bffb95b2e6995b5a9b8388", size = 83830, upload-time = "2026-05-21T04:51:15.341Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/d41fd5f17432703783f996fddc475d40baf20fe76f2c6dc217c2dd219b4c/wrapt-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9ad894d5dc5960ebd546a87a78160a8c645b99899e7e45a538436919bc9be5a6", size = 80711, upload-time = "2026-05-21T04:51:16.859Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/713f33fcd8f7b0aa87c9d068b590dc1e86c51d5e329bf83dd91ee47fe872/wrapt-2.2.0-py3-none-any.whl", hash = "sha256:03b77d3ecab6c38e5da7a5709cee6899083d08fc1bcd648b4fa78b346fc66282", size = 60994, upload-time = "2026-05-21T04:51:37.606Z" }, ] [[package]]