From b1864a0c282a3d7ec141cbd4fed3e3355541aacc Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Wed, 20 May 2026 17:35:15 +0200 Subject: [PATCH 01/38] feat(client.py): Rename auth params and procedure to align with new WFP API Porta (portal.api.wfp.org) BREAKING CHANGE: --- CHANGELOG.md | 6 ++++++ data_bridges_knots/client.py | 33 ++++++++------------------------- pyproject.toml | 12 +++++++----- uv.lock | 28 ++++++++++++++++++---------- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abaae0a..7b2258c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.2.0 (2026-05-20) + +### Feat + +- **auth**: change auth params for new WFP API Portal (portal.api.wfp.org) + ## 2.1.4 (2026-05-19) ### Fix diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index c9abf09..2f16a72 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -31,9 +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 + - WFP_API_CLIENT_ID: WFP API Gateway key for authentication + - WFP_API_CLIENT_SECRET: WFP API Gateway secret for authentication - DATABRIDGES_VERSION: API version (e.g., 'v1') - DATABRIDGES_API_KEY: (Optional) DataBridges-specific API key for certain endpoints @@ -45,18 +44,16 @@ 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['WFP_API_CLIENT_ID'] = 'your_key' + >>> os.environ['WFP_API_CLIENT_SECRET'] = 'your_secret' >>> os.environ['DATABRIDGES_VERSION'] = 'v1' >>> config = config_from_env() >>> client = DataBridgesShapes(config) """ required_vars = { - "KEY": "WFP_API_KEY", - "SECRET": "WFP_API_SECRET", - "SCOPES": "DATABRIDGES_SCOPES", + "KEY": "WFP_API_CLIENT_ID", + "SECRET": "WFP_API_CLIENT_SECRET", "VERSION": "DATABRIDGES_VERSION", } @@ -69,11 +66,7 @@ def config_from_env() -> Dict: 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[config_key] = value if missing: raise ValueError( @@ -113,7 +106,6 @@ class DataBridgesShapes: ... 'KEY': 'your-api-key', ... 'SECRET': 'your-api-secret', ... 'VERSION': '7.0.0', - ... 'SCOPES': ['vamdatabridges_household-fulldata_get'], ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' ... } >>> client = DataBridgesShapes(config) @@ -127,15 +119,6 @@ class DataBridgesShapes: def __init__(self, yaml_config_path, env="prod"): - 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" - ), - category=DeprecationWarning, - stacklevel=2, - ) - # Load and validate config once self.config = self._load_config(yaml_config_path) self._validate_config(self.config) @@ -215,7 +198,7 @@ def _setup_configuration_and_authentication(self, config: Dict): secret = config["SECRET"] scopes = config["SCOPES"] version = config["VERSION"] - uri = "https://api.wfp.org/vam-data-bridges/" + uri = "https://gateway.api.wfp.org/" host = str(uri + version) logger.info("DataBridges API: %s", host) diff --git a/pyproject.toml b/pyproject.toml index 0be2c9b..311b4ff 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 = "2.2.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" @@ -16,7 +16,7 @@ requires-python = ">=3.10" dependencies = [ 'PyYAML', 'pandas>=2', - 'data-bridges-client==7.0.0', + "data-bridges-client>=8.0.0", "httpx>=0.28.1", ] @@ -49,7 +49,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 @@ -110,14 +110,16 @@ dev = [ "mkdocs-material", "mkdocstrings[python]", "bandit", + "python-dotenv>=1.2.2", ] [tool.coverage.report] fail_under = 50 show_missing = true +# TEMP [tool.uv.sources] -data-bridges-client = { index = "wfp-hip-pypi" } +data-bridges-client = { git = "https://github.com/WFP-VAM/DataBridgesAPI.git", rev = "release/v8.0.0" } [[tool.uv.index]] name = "wfp-hip-pypi" @@ -126,7 +128,7 @@ explicit = true [tool.commitizen] name = "cz_conventional_commits" -version = "2.1.4" +version = "2.2.0" tag_format = "$version" version_files = [ "pyproject.toml:version", diff --git a/uv.lock b/uv.lock index 2420502..599b135 100644 --- a/uv.lock +++ b/uv.lock @@ -499,18 +499,15 @@ wheels = [ [[package]] name = "data-bridges-client" -version = "7.0.0" -source = { registry = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" } +version = "8.0.0" +source = { git = "https://github.com/WFP-VAM/DataBridgesAPI.git?rev=release%2Fv8.0.0#9ba821140020eb6ef3326ffbeb2787f8bb7047a1" } 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" } -wheels = [ - { url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/data-bridges-client/data_bridges_client-7.0.0-py3-none-any.whl" }, -] [[package]] name = "data-bridges-knots" @@ -550,6 +547,7 @@ dev = [ { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "python-dotenv" }, { name = "ruff" }, ] @@ -557,7 +555,7 @@ dev = [ 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 = "data-bridges-client", git = "https://github.com/WFP-VAM/DataBridgesAPI.git?rev=release%2Fv8.0.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "isort", marker = "extra == 'dev'" }, { name = "pandas", specifier = ">=2" }, @@ -582,6 +580,7 @@ dev = [ { name = "mypy" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "ruff" }, ] @@ -1906,6 +1905,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 +2280,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]] From a0b21f0dd6e1d30e903298376ef0babebcce6202 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Wed, 20 May 2026 17:38:13 +0200 Subject: [PATCH 02/38] docs: add knots 3.0.0 to pyproject.toml --- pyproject.toml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 311b4ff..bc6e539 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "data_bridges_knots" -version = "2.2.0" +version = "2.1.4" 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" @@ -90,6 +90,13 @@ testpaths = ["tests"] [tool.coverage.run] source = ["tests"] + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "v$version" +version_scheme = "semver" +version_provider = "uv" +update_changelog_on_bump = true [coverage.paths] source = "wfp-survey-toolbox" @@ -126,11 +133,3 @@ name = "wfp-hip-pypi" url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" explicit = true -[tool.commitizen] -name = "cz_conventional_commits" -version = "2.2.0" -tag_format = "$version" -version_files = [ - "pyproject.toml:version", -] -update_changelog_on_bump = true From 35cd158bc28e43780ae6ff4f3026af0b4f479219 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Wed, 20 May 2026 18:04:39 +0200 Subject: [PATCH 03/38] chore: remove installation from Github for data-bridges-client --- pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bc6e539..57bedfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,9 @@ requires-python = ">=3.10" dependencies = [ 'PyYAML', 'pandas>=2', - "data-bridges-client>=8.0.0", + "data-bridges-client==8.0.0", "httpx>=0.28.1", + ] [project.optional-dependencies] @@ -124,9 +125,6 @@ dev = [ fail_under = 50 show_missing = true -# TEMP -[tool.uv.sources] -data-bridges-client = { git = "https://github.com/WFP-VAM/DataBridgesAPI.git", rev = "release/v8.0.0" } [[tool.uv.index]] name = "wfp-hip-pypi" From cd8836ca3178c6beaca0af65172ebc66bc409aa7 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Wed, 20 May 2026 18:10:00 +0200 Subject: [PATCH 04/38] Update version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bc6e539..09976a9 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" From 53bb99fa8c3e615db8f6f99308bdc3bf54a7f336 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Wed, 20 May 2026 18:10:39 +0200 Subject: [PATCH 05/38] Point data-bridges-client to hip pypi repository --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09976a9..e89fdce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ show_missing = true # TEMP [tool.uv.sources] -data-bridges-client = { git = "https://github.com/WFP-VAM/DataBridgesAPI.git", rev = "release/v8.0.0" } +data-bridges-client = { index = "wfp-hip-pypi" } [[tool.uv.index]] name = "wfp-hip-pypi" From e4746f0f94e20b6aeb9d701786e061031503046a Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Wed, 20 May 2026 18:17:23 +0200 Subject: [PATCH 06/38] Reintroduce hip pypi uv source --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a21262b..a137bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,8 @@ dev = [ fail_under = 50 show_missing = true +[tool.uv.sources] +data-bridges-client = { index = "wfp-hip-pypi" } [[tool.uv.index]] name = "wfp-hip-pypi" From 4c528a37cf03b4ee64bdb6da19bb1a5a6f4a2301 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 10:33:55 +0200 Subject: [PATCH 07/38] chore: update pyproject.toml to fix installation of data-bridges-client; consolidate dependency-groups, setup-tools.package.find and remove reference to wfp-survey-toolbox (other project --- pyproject.toml | 72 ++++++++--------- uv.lock | 216 ++++++++++++++++++++++++------------------------- 2 files changed, 141 insertions(+), 147 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57bedfd..e36d4b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,15 +14,30 @@ keywords = ["VAM", "WFP", "data"] requires-python = ">=3.10" dependencies = [ - 'PyYAML', + 'PyYAML>=6', 'pandas>=2', - "data-bridges-client==8.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" +] STATA = ["stata-setup", "pystata"] R = [] @@ -31,8 +46,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'] @@ -88,9 +103,14 @@ addopts = [ # Only collect tests from the tests directory testpaths = ["tests"] + [tool.coverage.run] -source = ["tests"] +source = ["data_bridges_knots"] +branch = true +[tool.coverage.report] +fail_under = 50 +show_missing = true [tool.commitizen] name = "cz_conventional_commits" @@ -98,36 +118,16 @@ tag_format = "v$version" version_scheme = "semver" version_provider = "uv" update_changelog_on_bump = true -[coverage.paths] -source = "wfp-survey-toolbox" - -[coverage.run] -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", - "python-dotenv>=1.2.2", -] - -[tool.coverage.report] -fail_under = 50 -show_missing = true +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple" [[tool.uv.index]] name = "wfp-hip-pypi" url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" -explicit = true +# explicit = true + +[tool.uv.sources] +data-bridges-client = { index = "wfp-hip-pypi" } diff --git a/uv.lock b/uv.lock index 599b135..a3c2f0d 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]] @@ -500,7 +500,7 @@ wheels = [ [[package]] name = "data-bridges-client" version = "8.0.0" -source = { git = "https://github.com/WFP-VAM/DataBridgesAPI.git?rev=release%2Fv8.0.0#9ba821140020eb6ef3326ffbeb2787f8bb7047a1" } +source = { registry = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, @@ -508,6 +508,10 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] +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-8.0.0-py3-none-any.whl" }, +] [[package]] name = "data-bridges-knots" @@ -515,29 +519,16 @@ version = "2.1.4" 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" }, @@ -545,32 +536,29 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "pip-tools" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "python-dotenv" }, { name = "ruff" }, ] +stata = [ + { name = "pystata" }, + { name = "stata-setup" }, +] [package.metadata] requires-dist = [ - { name = "black", marker = "extra == 'dev'" }, - { name = "bumpver", marker = "extra == 'dev'" }, - { name = "data-bridges-client", git = "https://github.com/WFP-VAM/DataBridgesAPI.git?rev=release%2Fv8.0.0" }, - { 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" }, @@ -578,11 +566,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" }, ] +r = [] +stata = [ + { name = "pystata" }, + { name = "stata-setup" }, +] [[package]] name = "decli" @@ -2342,88 +2336,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]] From b9e7a4d7a9f5551b24c197c4d6bfe21f989429fe Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 10:37:12 +0200 Subject: [PATCH 08/38] Bump 2.1.4 -> 3.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e36d4b6..5f90d3f 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" From ad8f69b402ad7d4b36473198dd7e32a7eba4e726 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 10:38:03 +0200 Subject: [PATCH 09/38] fix: remove unused deprecation warning --- data_bridges_knots/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 2f16a72..a737118 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -3,7 +3,6 @@ import logging import os import time -import warnings from datetime import date import data_bridges_client From 7116e98a4e94627ac6d854800a624b1da2b90a52 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 11:25:00 +0200 Subject: [PATCH 10/38] fix: remove obsolete SCOPES from client --- data_bridges_knots/client.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index a737118..72ccaad 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -4,7 +4,7 @@ import os import time from datetime import date - +import warnings import data_bridges_client import numpy as np import pandas as pd @@ -92,7 +92,7 @@ class DataBridgesShapes: 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 + and optionally DATABRIDGES_API_KEY env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" Examples: @@ -118,6 +118,8 @@ class DataBridgesShapes: def __init__(self, yaml_config_path, env="prod"): + warnings.warn("DataBridgesShape will be renamed DataBridgesKnots in the next major release of this package (4.0.0).", FutureWarning) + # Load and validate config once self.config = self._load_config(yaml_config_path) self._validate_config(self.config) @@ -176,7 +178,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 = ["KEY", "SECRET", "VERSION"] missing = [field for field in required_fields if field not in config] if missing: raise ValueError( @@ -195,7 +197,6 @@ def _setup_configuration_and_authentication(self, config: Dict): """ key = config["KEY"] secret = config["SECRET"] - scopes = config["SCOPES"] version = config["VERSION"] uri = "https://gateway.api.wfp.org/" host = str(uri + version) @@ -204,7 +205,7 @@ def _setup_configuration_and_authentication(self, config: Dict): 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__()) From 531a93e8f6ec33f1d8837dfc7a209cef1c6a7ff2 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 11:25:33 +0200 Subject: [PATCH 11/38] test: add boilerplate code for testing client --- tests/test_client.py | 105 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 7641b8e..ccd8e57 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,2 +1,103 @@ -def test_client(): - pass +import os +import pytest +import pandas as pd + +from data_bridges_knots.client import DataBridgesShapes, config_from_env +from data_bridges_client.rest import ApiException + + +# ------------------------- +# ✅ Fixtures +# ------------------------- + +@pytest.fixture +def config_dict(): + return { + "KEY": os.getenv("WFP_API_CLIENT_ID", "dummy"), + "SECRET": os.getenv("WFP_API_CLIENT_SECRET", "dummy"), + "VERSION": os.getenv("DATABRIDGES_VERSION", "v1"), + } + + +@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_config_from_env(monkeypatch): + monkeypatch.setenv("WFP_API_CLIENT_ID", "key") + monkeypatch.setenv("WFP_API_CLIENT_SECRET", "secret") + monkeypatch.setenv("DATABRIDGES_VERSION", "v1") + + cfg = config_from_env() + + assert cfg["KEY"] == "key" + assert cfg["SECRET"] == "secret" + assert cfg["VERSION"] == "v1" + + +def test_client_init(config_dict): + client = DataBridgesShapes(config_dict) + assert isinstance(client, DataBridgesShapes) + + +# ========================================================= +# ✅ 3. SUCCESS TESTS (expect 200) +# ========================================================= + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args", + [ + ("get_prices", ("KEN",)), + ("get_exchange_rates", ("KEN",)), + ("get_commodities_list", ()), + ("get_currency_list", ()), + ("get_markets_list", ("KEN",)), + ("get_gorp", ("country_latest",)), + ("get_mfi_surveys", ()), + ], +) +def test_endpoints_success(client, func, args): + method = getattr(client, func) + + try: + result = method(*args) + assert isinstance(result, (pd.DataFrame, str, bytes)) + except ApiException as e: + pytest.fail(f"{func} returned 403 but expected success: {e}") + + +# ========================================================= +# ✅ 4. FORBIDDEN TESTS (expect 403) +# ========================================================= + +@pytest.mark.integration +@pytest.mark.parametrize( + "func,args", + [ + ("get_household_survey", (1234, "full")), + ("get_household_survey", (1234, "draft")), + # Add more endpoints known to require permissions + ], +) +def test_endpoints_forbidden(client, func, args): + method = getattr(client, func) + + with pytest.raises(ApiException) as exc: + method(*args) + + assert exc.value.status == 403 \ No newline at end of file From 157ed5b10a747842e6fa5c447fb6b0d3814a09b8 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 11:26:06 +0200 Subject: [PATCH 12/38] docs: update CHANGELOG and ROADMAP --- CHANGELOG.md | 5 ----- ROADMAP.md | 23 ++++++++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2258c..4183b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,3 @@ -## 2.2.0 (2026-05-20) - -### Feat - -- **auth**: change auth params for new WFP API Portal (portal.api.wfp.org) ## 2.1.4 (2026-05-19) diff --git a/ROADMAP.md b/ROADMAP.md index 53b4f12..71095bd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,7 +20,7 @@ This document outlines the planned features and improvements for the `DataBridge ## Future Release ### v1.1.0 -- [ ] STATA support + - [X] Fix optional dependencies for STATA - [X] Update setup.py and pyproject.toml to include DataBridges API v6.0 @@ -28,17 +28,26 @@ This document outlines the planned features and improvements for the `DataBridge - [X] R example files ### v1.2.1 -- [ ] Documentation: Enhance documentation and provide more usage examples -- [ ] Automation: GitHub Actions linting and testing +- [X] Documentation: Enhance documentation and provide more usage examples +- [X] Automation: GitHub Actions linting and testing + + + +Please note that this roadmap is subject to change, and the priorities may be adjusted based on the project's needs and available resources. + +# v3.0.0 "Delicate Dragon" + +## Future Releases (3.1.0+) +- [ ] Full endpoint list +- [ ] STATA support +- [ ] Tooling to search + - [ ] Bug fixing: Test AIMS and RPME endpoints - [ ] Bug fixing: DPO change for XLSForm - [ ] Bug fixing: Markets GeoJSON response - [ ] Bug fixing: JSON to DataFrame response - [ ] Bug fixing: Market list JSON and CSV -## Future Release (2.0.0 "Charismatic Chimera") - Testing: Unit testing - Testing: Improve error handling and logging -- Refactoring: Optimize performance and improve code efficiency - -Please note that this roadmap is subject to change, and the priorities may be adjusted based on the project's needs and available resources. +- Refactoring: Optimize performance and improve code efficiency \ No newline at end of file From 0382897251272c2b2ee8f0393aa33bd7ad793e13 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 11:58:11 +0200 Subject: [PATCH 13/38] fix: host in client --- Makefile | 2 +- data_bridges_knots/client.py | 84 +++++++++++++++++++++--------------- pyproject.toml | 4 +- tests/test_client.py | 14 ++++-- uv.lock | 2 +- 5 files changed, 62 insertions(+), 44 deletions(-) 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/data_bridges_knots/client.py b/data_bridges_knots/client.py index 72ccaad..78827ff 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -3,8 +3,9 @@ import logging import os import time -from datetime import date import warnings +from datetime import date + import data_bridges_client import numpy as np import pandas as pd @@ -83,42 +84,53 @@ def config_from_env() -> Dict: class DataBridgesShapes: """DataBridgesShapes is a class that provides an interface to interact with 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. - - Args: - yaml_config_path (str | dict): Either: - - Path to YAML configuration file (str), or - - Configuration dictionary (dict) with required keys: KEY, SECRET, VERSION, - and optionally DATABRIDGES_API_KEY - env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" - - Examples: - >>> # Initialize with YAML file (traditional method) - >>> client = DataBridgesShapes("data_bridges_api_config.yaml") - >>> df_prices = client.get_prices("KEN", "2025-09-01") - - >>> # Initialize with dictionary (new method) - >>> config = { - ... 'KEY': 'your-api-key', - ... 'SECRET': 'your-api-secret', - ... 'VERSION': '7.0.0', - ... '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) + 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. + + Args: + yaml_config_path (str | dict): Either: + - Path to YAML configuration file (str), or + - Configuration dictionary (dict) with required keys: KEY, SECRET, VERSION, + and optionally DATABRIDGES_API_KEY + env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" + + Examples: + >>> # Initialize with YAML file (traditional method) + >>> client = DataBridgesShapes("data_bridges_api_config.yaml") + >>> df_prices = client.get_prices("KEN", "2025-09-01") + + >>> # Initialize with dictionary (new method) + >>> config = { + ... 'KEY': 'your-api-key', + ... 'SECRET': 'your-api-secret', + ... 'VERSION': '7.0.0', + ... '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) """ def __init__(self, yaml_config_path, env="prod"): - warnings.warn("DataBridgesShape will be renamed DataBridgesKnots in the next major release of this package (4.0.0).", FutureWarning) + + warnings.warn( + ( + "\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" + ), + FutureWarning, + stacklevel=2, + ) + # Load and validate config once self.config = self._load_config(yaml_config_path) @@ -198,8 +210,10 @@ def _setup_configuration_and_authentication(self, config: Dict): key = config["KEY"] secret = config["SECRET"] version = config["VERSION"] - uri = "https://gateway.api.wfp.org/" - host = str(uri + version) + BASE_URI = "https://gateway.api.wfp.org/vam-data-bridges" + host = f"{BASE_URI}/{version.strip('/')}" + + logger.info("DataBridges API: %s", host) diff --git a/pyproject.toml b/pyproject.toml index 82d7d0c..aeff774 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,7 @@ 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" } @@ -130,6 +131,3 @@ name = "wfp-hip-pypi" url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/" # explicit = true - -[tool.uv.sources] -data-bridges-client = { index = "wfp-hip-pypi" } diff --git a/tests/test_client.py b/tests/test_client.py index ccd8e57..2938f6e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,15 +1,16 @@ import os -import pytest -import pandas as pd -from data_bridges_knots.client import DataBridgesShapes, config_from_env +import pandas as pd +import pytest from data_bridges_client.rest import ApiException +from data_bridges_knots.client import DataBridgesShapes, config_from_env # ------------------------- # ✅ Fixtures # ------------------------- + @pytest.fixture def config_dict(): return { @@ -28,8 +29,10 @@ def client(config_dict): # ✅ 1. Import # ------------------------- + def test_import(): from data_bridges_knots.client import DataBridgesShapes + assert DataBridgesShapes is not None @@ -37,6 +40,7 @@ def test_import(): # ✅ 2. Config # ------------------------- + def test_config_from_env(monkeypatch): monkeypatch.setenv("WFP_API_CLIENT_ID", "key") monkeypatch.setenv("WFP_API_CLIENT_SECRET", "secret") @@ -58,6 +62,7 @@ def test_client_init(config_dict): # ✅ 3. SUCCESS TESTS (expect 200) # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args", @@ -85,6 +90,7 @@ def test_endpoints_success(client, func, args): # ✅ 4. FORBIDDEN TESTS (expect 403) # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args", @@ -100,4 +106,4 @@ def test_endpoints_forbidden(client, func, args): with pytest.raises(ApiException) as exc: method(*args) - assert exc.value.status == 403 \ No newline at end of file + assert exc.value.status == 403 diff --git a/uv.lock b/uv.lock index a3c2f0d..db0a09f 100644 --- a/uv.lock +++ b/uv.lock @@ -515,7 +515,7 @@ wheels = [ [[package]] name = "data-bridges-knots" -version = "2.1.4" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "data-bridges-client" }, From 92e43d1f601d56fdfcced59e88eee4012c14c4e8 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 12:16:59 +0200 Subject: [PATCH 14/38] test: fix some tests, add workflow status badge to README --- README.md | 9 +++++++++ tests/test_client.py | 25 +++++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 68ee6b1..e70c099 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # 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 diff --git a/tests/test_client.py b/tests/test_client.py index 2938f6e..49aec56 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,7 +5,7 @@ from data_bridges_client.rest import ApiException from data_bridges_knots.client import DataBridgesShapes, config_from_env - +from dotenv import load_dotenv # ------------------------- # ✅ Fixtures # ------------------------- @@ -13,12 +13,10 @@ @pytest.fixture def config_dict(): - return { - "KEY": os.getenv("WFP_API_CLIENT_ID", "dummy"), - "SECRET": os.getenv("WFP_API_CLIENT_SECRET", "dummy"), - "VERSION": os.getenv("DATABRIDGES_VERSION", "v1"), - } - + load_dotenv() + config = config_from_env() + + return config @pytest.fixture def client(config_dict): @@ -29,7 +27,6 @@ def client(config_dict): # ✅ 1. Import # ------------------------- - def test_import(): from data_bridges_knots.client import DataBridgesShapes @@ -41,18 +38,6 @@ def test_import(): # ------------------------- -def test_config_from_env(monkeypatch): - monkeypatch.setenv("WFP_API_CLIENT_ID", "key") - monkeypatch.setenv("WFP_API_CLIENT_SECRET", "secret") - monkeypatch.setenv("DATABRIDGES_VERSION", "v1") - - cfg = config_from_env() - - assert cfg["KEY"] == "key" - assert cfg["SECRET"] == "secret" - assert cfg["VERSION"] == "v1" - - def test_client_init(config_dict): client = DataBridgesShapes(config_dict) assert isinstance(client, DataBridgesShapes) From e1dbc4587092d203c38c9c5317a8d8c60ada5cff Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 14:12:25 +0200 Subject: [PATCH 15/38] test: adding get_commodities_list test with kwargs --- ROADMAP.md | 55 +++++++++++++++++++++++++++- data_bridges_knots/client.py | 18 +--------- tests/test_client.py | 69 ++++++++++++++++++------------------ 3 files changed, 90 insertions(+), 52 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 71095bd..9f04711 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -50,4 +50,57 @@ Please note that this roadmap is subject to change, and the priorities may be ad - Testing: Unit testing - Testing: Improve error handling and logging -- Refactoring: Optimize performance and improve code efficiency \ No newline at end of file +- Refactoring: Optimize performance and improve code efficiency + + +# Endpoints + +| 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_knots/client.py b/data_bridges_knots/client.py index 78827ff..3096cdb 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -105,7 +105,7 @@ class DataBridgesShapes: >>> config = { ... 'KEY': 'your-api-key', ... 'SECRET': 'your-api-secret', - ... 'VERSION': '7.0.0', + ... 'VERSION': 'v1', ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' ... } >>> client = DataBridgesShapes(config) @@ -131,7 +131,6 @@ def __init__(self, yaml_config_path, env="prod"): stacklevel=2, ) - # Load and validate config once self.config = self._load_config(yaml_config_path) self._validate_config(self.config) @@ -213,8 +212,6 @@ def _setup_configuration_and_authentication(self, config: Dict): BASE_URI = "https://gateway.api.wfp.org/vam-data-bridges" host = f"{BASE_URI}/{version.strip('/')}" - - logger.info("DataBridges API: %s", host) token = WfpApiToken(api_key=key, api_secret=secret) @@ -231,7 +228,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, @@ -247,7 +243,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. @@ -270,17 +265,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( diff --git a/tests/test_client.py b/tests/test_client.py index 49aec56..3cad833 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,15 +29,12 @@ def client(config_dict): 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) @@ -48,47 +45,51 @@ def test_client_init(config_dict): # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( - "func,args", + "func,args,kwargs", [ - ("get_prices", ("KEN",)), - ("get_exchange_rates", ("KEN",)), - ("get_commodities_list", ()), - ("get_currency_list", ()), - ("get_markets_list", ("KEN",)), - ("get_gorp", ("country_latest",)), - ("get_mfi_surveys", ()), + ("get_prices", ("KEN",), {}), + ("get_prices", ("KEN",), {"start_date": "2025-01-01"}), + + ("get_exchange_rates", ("ETH",), {}), + + ("get_food_security_list", ("ETH", 2025), {}), + + ("get_commodities_list", (), {}), + ("get_commodities_list", (), {"country_iso3": "TZA"}), + ("get_commodities_list", (), {"commodity_name": "Maize"}), ], ) -def test_endpoints_success(client, func, args): + +def test_endpoints_success(client, func, args, kwargs): method = getattr(client, func) - try: - result = method(*args) - assert isinstance(result, (pd.DataFrame, str, bytes)) - except ApiException as e: - pytest.fail(f"{func} returned 403 but expected success: {e}") + result = method(*args, **kwargs) + import pandas as pd + assert isinstance(result, (pd.DataFrame, str, bytes)) -# ========================================================= -# ✅ 4. FORBIDDEN TESTS (expect 403) -# ========================================================= +# # ========================================================= +# # ✅ 4. FORBIDDEN ENDPOINT TESTS (expect 403) +# # ========================================================= -@pytest.mark.integration -@pytest.mark.parametrize( - "func,args", - [ - ("get_household_survey", (1234, "full")), - ("get_household_survey", (1234, "draft")), - # Add more endpoints known to require permissions - ], -) -def test_endpoints_forbidden(client, func, args): - method = getattr(client, func) - with pytest.raises(ApiException) as exc: - method(*args) +# @pytest.mark.integration +# @pytest.mark.parametrize( +# "func,args", +# [ +# ("get_household_survey", (1234, "full")), +# # ("get_household_survey", (1234, "draft")), +# # Add more endpoints known to require permissions +# ], +# ) +# def test_endpoints_forbidden(client, func, args): +# method = getattr(client, func) + +# with pytest.raises(ApiException) as exc: +# method(*args) - assert exc.value.status == 403 +# assert exc.value.status == 403 From d23c79838e488a69711c40f4632637602655b491 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 15:09:04 +0200 Subject: [PATCH 16/38] test: add additional tests for client endpoints --- pyproject.toml | 4 ++ tests/test_client.py | 103 +++++++++++++++++++++++++++++++++---------- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aeff774..c44fb57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,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", diff --git a/tests/test_client.py b/tests/test_client.py index 3cad833..f769c42 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,3 @@ -import os - import pandas as pd import pytest from data_bridges_client.rest import ApiException @@ -39,27 +37,85 @@ def test_client_init(config_dict): client = DataBridgesShapes(config_dict) assert isinstance(client, DataBridgesShapes) - # ========================================================= # ✅ 3. SUCCESS TESTS (expect 200) # ========================================================= - - -@pytest.mark.integration +pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", [ + # Prices ("get_prices", ("KEN",), {}), - ("get_prices", ("KEN",), {"start_date": "2025-01-01"}), + ("get_prices", ("KEN",), {"start_date": "2025-01-01", "end_date": "2025-12-31"}), + # Exchange rates ("get_exchange_rates", ("ETH",), {}), + # Food security ("get_food_security_list", ("ETH", 2025), {}), + # 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 unit conversions + ("get_commodity_units_conversion_list", (), {}), + ("get_commodity_units_conversion_list", (), {"country_iso3": "TZA"}), + + # 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"}), + + # Economic indicators + ("get_economic_indicator_list", (), {}), + ("get_economic_indicator_list", (), {"indicator_name": "", "country_iso3": ""}), + + # Markets + ("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), {}), + ("get_market_geojson_list", ("AFG",), {}), + + # GORP + ("get_gorp", ("country_latest",), {}), + ("get_gorp", ("global_latest",), {}), + ("get_gorp", ("regional_latest",), {}), + + # Household surveys + ("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"}), + + # XLS forms + ("get_household_xslform_definition", (2067,), {}), + ("get_household_questionnaire", (2075,), {}), + ("get_choice_list", (123,), {}), + + # MFI surveys + ("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}), + + # MFI XLS forms + ("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"}), ], ) @@ -68,28 +124,27 @@ def test_endpoints_success(client, func, args, kwargs): result = method(*args, **kwargs) - import pandas as pd assert isinstance(result, (pd.DataFrame, str, bytes)) - # # ========================================================= # # ✅ 4. FORBIDDEN ENDPOINT TESTS (expect 403) # # ========================================================= +pytest.mark.integration +@pytest.mark.parametrize( + "func,args,kwargs", + [ + # AIMS + ("get_aims_analysis_rounds", (231,), {}), + ("get_aims_polygon_files", (231,), {}), + ("get_household_survey", (3094, "draft"), {}), + # Add more endpoints known to require permissions + ], +) +def test_endpoints_forbidden(client, func, args, kwargs): + method = getattr(client, func) -# @pytest.mark.integration -# @pytest.mark.parametrize( -# "func,args", -# [ -# ("get_household_survey", (1234, "full")), -# # ("get_household_survey", (1234, "draft")), -# # Add more endpoints known to require permissions -# ], -# ) -# def test_endpoints_forbidden(client, func, args): -# method = getattr(client, func) - -# with pytest.raises(ApiException) as exc: -# method(*args) + with pytest.raises(ApiException) as exc: + method(*args, **kwargs) -# assert exc.value.status == 403 + assert exc.value.status == 403 From dc5053108ef1393d8aaddf68a37eded36a608695 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 16:05:54 +0200 Subject: [PATCH 17/38] fix: remove obsolete endpoints --- data_bridges_knots/client.py | 161 +++++++++++++++++------------------ tests/test_client.py | 16 ++-- 2 files changed, 84 insertions(+), 93 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 3096cdb..19d2610 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -383,52 +383,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, @@ -932,71 +886,99 @@ 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: + print("Exception when calling GlobalOutlookApi->global_outlook_country_latest_get: %s\n" % e) + + # 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 + + # try: + # if data_type == "country_latest": + # gorp_data = gorp_api_instance.gorp_country_latest_get(env=env) + # elif data_type == "global_latest": + # gorp_data = gorp_api_instance.gorp_global_latest_get(env=env) + # elif data_type == "regional_latest": + # gorp_data = gorp_api_instance.gorp_regional_latest_get(env=env) + # else: + # raise ValueError(f"Invalid data_type: {data_type}") + + # 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 def get_household_survey( self, survey_id: int, access_type: str, page_size: Optional[int] = 600 @@ -1698,11 +1680,24 @@ def get_mfi_surveys_base_data( ) raise + def get_ipc_and_equivalent_data(): + pass + + def get_hotpost_data(): + pass + + def get_aims_data(): + pass + + def get_rpme_data(): + pass + + def get_cari_data(): + pass if __name__ == "__main__": import yaml # FOR TESTING CONFIG_PATH = r"data_bridges_api_config.yaml" - - client = DataBridgesShapes(CONFIG_PATH) + client = DataBridgesShapes(CONFIG_PATH) \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index f769c42..86f8345 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -52,9 +52,6 @@ def test_client_init(config_dict): # Exchange rates ("get_exchange_rates", ("ETH",), {}), - # Food security - ("get_food_security_list", ("ETH", 2025), {}), - # Commodities ("get_commodities_list", (), {}), ("get_commodities_list", (), {"country_iso3": "TZA"}), @@ -91,9 +88,9 @@ def test_client_init(config_dict): ("get_market_geojson_list", ("AFG",), {}), # GORP - ("get_gorp", ("country_latest",), {}), - ("get_gorp", ("global_latest",), {}), - ("get_gorp", ("regional_latest",), {}), + ("get_global_outlook", ("country_latest"), {}), + ("get_global_outlook", ("global_latest"), {}), + ("get_global_outlook", ("regional_latest"), {}), # Household surveys ("get_household_survey", (3094, "official"), {}), @@ -121,9 +118,7 @@ def test_client_init(config_dict): def test_endpoints_success(client, func, args, kwargs): method = getattr(client, func) - result = method(*args, **kwargs) - assert isinstance(result, (pd.DataFrame, str, bytes)) # # ========================================================= @@ -134,11 +129,12 @@ def test_endpoints_success(client, func, args, kwargs): @pytest.mark.parametrize( "func,args,kwargs", [ + ("get_household_survey", (3094, "draft"), {}), + # Add more endpoints known to require permissions + # AIMS ("get_aims_analysis_rounds", (231,), {}), ("get_aims_polygon_files", (231,), {}), - ("get_household_survey", (3094, "draft"), {}), - # Add more endpoints known to require permissions ], ) def test_endpoints_forbidden(client, func, args, kwargs): From 9624f5af2437a677f474e36601919b3e40d707fe Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 16:33:24 +0200 Subject: [PATCH 18/38] test: add tests for all implemented & working endpoints; update ROADMAP --- ROADMAP.md | 36 +++----- data_bridges_knots/client.py | 103 +-------------------- tests/test_client.py | 171 +++++++++++++++++++++++++---------- 3 files changed, 143 insertions(+), 167 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9f04711..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,43 +17,33 @@ This document outlines the planned features and improvements for the `DataBridge - [X] Endpoints: XlsFormsApi - [X] Endpoints: SurveysApi -## Future Release - -### v1.1.0 +## 2.0.0 "Cheerful Chimera" (DataBridges API v6.0.0) - [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 - -### v1.2.1 - [X] Documentation: Enhance documentation and provide more usage examples - [X] Automation: GitHub Actions linting and testing - -Please note that this roadmap is subject to change, and the priorities may be adjusted based on the project's needs and available resources. - -# v3.0.0 "Delicate Dragon" +# v3.0.0 "Delicate Dragon" (DataBridgesAPI v1.0.0 - new API) +- [ ] Testing: Unit testing for implemented endpotns ## Future Releases (3.1.0+) -- [ ] Full endpoint list +- [ ] Add missing endpoints + - [ ] RpmeApi + - [ ] IpcchApi + - [ ] GlobalOutlookApi +- [ ] Refactor client.py into modules (e.g. `endpoints/household.py`) - [ ] STATA support -- [ ] Tooling to search - -- [ ] Bug fixing: Test AIMS and RPME endpoints -- [ ] Bug fixing: DPO change for XLSForm +- [ ] 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: Unit testing -- Testing: Improve error handling and logging -- Refactoring: Optimize performance and improve code efficiency +- [ ] Testing: Add tests for helper functions (labels) +- [ ] Refactoring: Optimize performance and improve code efficiency (e.g. polars) -# Endpoints +# Endpoints v1 | Function | Method (data-bridges-client) | Endpoint | Description | Developed | Tested | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------ | diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 19d2610..bc581a5 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -948,38 +948,6 @@ def get_global_outlook( except Exception as e: print("Exception when calling GlobalOutlookApi->global_outlook_country_latest_get: %s\n" % e) - # 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 - - # try: - # if data_type == "country_latest": - # gorp_data = gorp_api_instance.gorp_country_latest_get(env=env) - # elif data_type == "global_latest": - # gorp_data = gorp_api_instance.gorp_global_latest_get(env=env) - # elif data_type == "regional_latest": - # gorp_data = gorp_api_instance.gorp_regional_latest_get(env=env) - # else: - # raise ValueError(f"Invalid data_type: {data_type}") - - # 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 - def get_household_survey( self, survey_id: int, access_type: str, page_size: Optional[int] = 600 ) -> pd.DataFrame: @@ -1224,68 +1192,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 ): @@ -1696,8 +1602,9 @@ def get_cari_data(): pass if __name__ == "__main__": - import yaml + pass + # import yaml - # FOR TESTING - CONFIG_PATH = r"data_bridges_api_config.yaml" - client = DataBridgesShapes(CONFIG_PATH) \ No newline at end of file + # # FOR TESTING + # CONFIG_PATH = r"data_bridges_api_config.yaml" + # client = DataBridgesShapes(CONFIG_PATH) \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index 86f8345..35e3ee2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -41,7 +41,16 @@ def test_client_init(config_dict): # ✅ 3. SUCCESS TESTS (expect 200) # ========================================================= -pytest.mark.integration +import pandas as pd +import pytest +from data_bridges_client.rest import ApiException + + +# ========================================================= +# ✅ PRICES & CURRENCY +# ========================================================= + +@pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", [ @@ -52,6 +61,30 @@ def test_client_init(config_dict): # 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"}), @@ -63,84 +96,130 @@ def test_client_init(config_dict): ("get_commodity_units_list", (), {"commodity_unit_name": "Kg"}), ("get_commodity_units_list", (), {"commodity_unit_id": 5}), - # Commodity unit conversions + # 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)) - # 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"}), - # Economic indicators - ("get_economic_indicator_list", (), {}), - ("get_economic_indicator_list", (), {"indicator_name": "", "country_iso3": ""}), +# ========================================================= +# ✅ MARKETS +# ========================================================= - # 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), {}), ("get_market_geojson_list", ("AFG",), {}), + ], +) +def test_markets_endpoints(client, func, args, kwargs): + method = getattr(client, func) + result = method(*args, **kwargs) + assert isinstance(result, (pd.DataFrame, str, bytes)) + + +# ========================================================= +# ✅ 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)) + - # GORP - ("get_global_outlook", ("country_latest"), {}), - ("get_global_outlook", ("global_latest"), {}), - ("get_global_outlook", ("regional_latest"), {}), +# ========================================================= +# ✅ HOUSEHOLD & SURVEYS +# ========================================================= - # 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_surveys_list", (), { + "country_iso3": "COG", + "start_date": "2024-01-01", + "end_date": "2024-12-31", + }), - # XLS forms ("get_household_xslform_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 +# ========================================================= - # MFI surveys +@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}), - # MFI XLS forms ("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"}), + ("get_mfi_xls_forms_detailed", (), { + "adm0_code": 231, + "start_date": "2023-01-01", + "end_date": "2023-12-31", + }), ], ) - -def test_endpoints_success(client, func, args, kwargs): +def test_mfi_endpoints(client, func, args, kwargs): method = getattr(client, func) result = method(*args, **kwargs) assert isinstance(result, (pd.DataFrame, str, bytes)) + # # ========================================================= -# # ✅ 4. FORBIDDEN ENDPOINT TESTS (expect 403) +# # ✅ FORBIDDEN ENDPOINTS # # ========================================================= -pytest.mark.integration -@pytest.mark.parametrize( - "func,args,kwargs", - [ - ("get_household_survey", (3094, "draft"), {}), - # Add more endpoints known to require permissions - - # AIMS - ("get_aims_analysis_rounds", (231,), {}), - ("get_aims_polygon_files", (231,), {}), - ], -) -def test_endpoints_forbidden(client, func, args, kwargs): - method = getattr(client, func) - - with pytest.raises(ApiException) as exc: - method(*args, **kwargs) - - assert exc.value.status == 403 +# @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) From c1f66225363195bf7492030a0e921704dfec2fe3 Mon Sep 17 00:00:00 2001 From: Alessandra Gherardelli Date: Thu, 21 May 2026 16:35:02 +0200 Subject: [PATCH 19/38] Potential fix for pull request finding 'First parameter of a method is not named 'self'' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- data_bridges_knots/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index bc581a5..4f4c6e3 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -1586,19 +1586,19 @@ def get_mfi_surveys_base_data( ) raise - def get_ipc_and_equivalent_data(): + def get_ipc_and_equivalent_data(self): pass - def get_hotpost_data(): + def get_hotpost_data(self): pass - def get_aims_data(): + def get_aims_data(self): pass - def get_rpme_data(): + def get_rpme_data(self): pass - def get_cari_data(): + def get_cari_data(self): pass if __name__ == "__main__": From f8447a56c858f4ec5a8fe5ac777bd586d141506c Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 16:44:34 +0200 Subject: [PATCH 20/38] docs: update README and remove obsolete examples --- README.md | 39 ++++++------ data_bridges_api_config_sample.yaml | 12 ++-- examples/example.py | 94 ----------------------------- examples/example_R.R | 75 ----------------------- examples/example_STATA.do | 54 ----------------- 5 files changed, 24 insertions(+), 250 deletions(-) delete mode 100644 examples/example.py delete mode 100644 examples/example_R.R delete mode 100644 examples/example_STATA.do diff --git a/README.md b/README.md index e70c099..16d5a89 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,16 @@ 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/). -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 \ @@ -36,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`): @@ -64,14 +74,10 @@ 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. @@ -83,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' } @@ -101,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 WFP_API_CLIENT_ID="your-api-key" +export WFP_API_CLIENT_SECRET="your-api-secret" export DATABRIDGES_VERSION="7.0.0" -export DATABRIDGES_SCOPES="scope1,scope2,scope3" export DATABRIDGES_API_KEY="optional-databridges-key" ``` 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/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 From cedb14d345466976a64e332b898ad57ca130a1d9 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 16:46:22 +0200 Subject: [PATCH 21/38] chore: apply codestyle --- data_bridges_knots/client.py | 31 +++++++++++------ tests/test_client.py | 65 +++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index bc581a5..26351a0 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -119,7 +119,6 @@ class DataBridgesShapes: def __init__(self, yaml_config_path, env="prod"): - warnings.warn( ( "\n[FUTURE WARNING]\n" @@ -891,7 +890,6 @@ def get_global_outlook( data_type: Literal["country_latest", "global_latest", "regional_latest"], page: Optional[int] = None, ) -> pd.DataFrame: - """Retrieves data from the Global Outlook API. The Global Outlook API provides access to WFP’s forward-looking analysis and @@ -923,30 +921,42 @@ def get_global_outlook( 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: # 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) + env = ( + self.env + ) # str | Environment. * `prod` - api.vam.wfp.org * `dev` - dev.api.vam.wfp.org (optional) try: if data_type == "country_latest": - api_response = api_instance.global_outlook_country_latest_get(env=env) + api_response = api_instance.global_outlook_country_latest_get( + env=env + ) elif data_type == "global_latest": - api_response = api_instance.global_outlook_global_latest_get(env=env) + api_response = api_instance.global_outlook_global_latest_get( + env=env + ) elif data_type == "regional_latest": - api_response = api_instance.global_outlook_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}") + logger.info( + f"Successfully retrieved Global Outlook data for type: {data_type}" + ) return pd.DataFrame([item.to_dict() for item in api_response.items]) except Exception as e: - print("Exception when calling GlobalOutlookApi->global_outlook_country_latest_get: %s\n" % e) + print( + "Exception when calling GlobalOutlookApi->global_outlook_country_latest_get: %s\n" + % e + ) def get_household_survey( self, survey_id: int, access_type: str, page_size: Optional[int] = 600 @@ -1601,10 +1611,11 @@ def get_rpme_data(): def get_cari_data(): pass + if __name__ == "__main__": pass # import yaml # # FOR TESTING # CONFIG_PATH = r"data_bridges_api_config.yaml" - # client = DataBridgesShapes(CONFIG_PATH) \ No newline at end of file + # client = DataBridgesShapes(CONFIG_PATH) diff --git a/tests/test_client.py b/tests/test_client.py index 35e3ee2..3be46e1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,9 +1,9 @@ import pandas as pd import pytest -from data_bridges_client.rest import ApiException +from dotenv import load_dotenv from data_bridges_knots.client import DataBridgesShapes, config_from_env -from dotenv import load_dotenv + # ------------------------- # ✅ Fixtures # ------------------------- @@ -13,9 +13,10 @@ def config_dict(): load_dotenv() config = config_from_env() - + return config + @pytest.fixture def client(config_dict): return DataBridgesShapes(config_dict) @@ -25,47 +26,49 @@ def client(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) # ========================================================= -import pandas as pd -import pytest -from data_bridges_client.rest import ApiException - - # ========================================================= # ✅ 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"}), - + ( + "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"}), @@ -81,6 +84,7 @@ def test_prices_and_currency_endpoints(client, func, args, kwargs): # ✅ COMMODITIES # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", @@ -90,12 +94,10 @@ def test_prices_and_currency_endpoints(client, func, args, kwargs): ("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"}), @@ -111,6 +113,7 @@ def test_commodities_endpoints(client, func, args, kwargs): # ✅ MARKETS # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", @@ -132,6 +135,7 @@ def test_markets_endpoints(client, func, args, kwargs): # ✅ ECONOMIC + OUTLOOK # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", @@ -149,20 +153,23 @@ def test_economic_indicator_endpoints(client, func, args, kwargs): # ✅ 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_surveys_list", + (), + { + "country_iso3": "COG", + "start_date": "2024-01-01", + "end_date": "2024-12-31", + }, + ), ("get_household_xslform_definition", (2067,), {}), ("get_household_questionnaire", (2075,), {}), ("get_choice_list", (123,), {}), @@ -178,6 +185,7 @@ def test_household_endpoints(client, func, args, kwargs): # ✅ MFI # ========================================================= + @pytest.mark.integration @pytest.mark.parametrize( "func,args,kwargs", @@ -186,14 +194,17 @@ def test_household_endpoints(client, func, args, kwargs): ("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", - }), + ( + "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): From caae2ddd486134e51dfc532d27f1fcdaead42531 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 16:53:10 +0200 Subject: [PATCH 22/38] chore: update github actions to exclude test_client (as it requires credentials) --- .github/workflows/python-package.yml | 2 +- tests/test_client.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 39a4147..2b6553d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -35,7 +35,7 @@ jobs: uv run ruff check . --fix - name: Run tests run: | - uv run pytest + uv run pytest -m "not integration" deploy-docs: runs-on: ubuntu-latest diff --git a/tests/test_client.py b/tests/test_client.py index 3be46e1..5b0dc69 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,6 +4,10 @@ from data_bridges_knots.client import DataBridgesShapes, config_from_env + +pytestmark = pytest.mark.integration + + # ------------------------- # ✅ Fixtures # ------------------------- From 705ff010532beb2091c4c3b8368fd2072b69a025 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 17:03:37 +0200 Subject: [PATCH 23/38] docs: update README --- README.md | 2 +- tests/test_client.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 16d5a89..dbd044c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This Python module allows you to get data from the WFP Data Bridges API, includi ## 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 latest stable `data_bridges_knots` package in your environment using uv: diff --git a/tests/test_client.py b/tests/test_client.py index 5b0dc69..14afe93 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,10 +4,8 @@ from data_bridges_knots.client import DataBridgesShapes, config_from_env - pytestmark = pytest.mark.integration - # ------------------------- # ✅ Fixtures # ------------------------- From 412d0ef92a077a295da221c0f4d64e43378bc64d Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Thu, 21 May 2026 17:06:00 +0200 Subject: [PATCH 24/38] revert github action --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2b6553d..39a4147 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -35,7 +35,7 @@ jobs: uv run ruff check . --fix - name: Run tests run: | - uv run pytest -m "not integration" + uv run pytest deploy-docs: runs-on: ubuntu-latest From 152b3c803fb3047ae03408c143dbf937421320d4 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 09:44:13 +0200 Subject: [PATCH 25/38] Pass repo secrets to GitHub action workflow step --- .github/workflows/python-package.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 39a4147..caa2e0a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -36,6 +36,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 }} + deploy-docs: runs-on: ubuntu-latest From 68a5566b3beec128537e17822c60e27a8a5bcf09 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:12:17 +0200 Subject: [PATCH 26/38] Use [""WFP_API_CLIENT_ID", "WFP_API_CLIENT_SECRET", "DATABRIDGES_VERSION" throughout --- README.md | 4 ++-- data_bridges_knots/client.py | 30 ++++++++++++------------------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index dbd044c..558f9dd 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ There are three ways to configure DataBridgesShapes: WFP_API_CLIENT_SECRET: '' DATABRIDGES_API_KEY: '' ``` -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) @@ -105,7 +105,7 @@ Set the following environment variables and use the `config_from_env()` helper: ```bash export WFP_API_CLIENT_ID="your-api-key" export WFP_API_CLIENT_SECRET="your-api-secret" -export DATABRIDGES_VERSION="7.0.0" +export DATABRIDGES_VERSION="v1" export DATABRIDGES_API_KEY="optional-databridges-key" ``` diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 2c3703c..a363333 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -51,29 +51,23 @@ def config_from_env() -> Dict: >>> client = DataBridgesShapes(config) """ - required_vars = { - "KEY": "WFP_API_CLIENT_ID", - "SECRET": "WFP_API_CLIENT_SECRET", - "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: - 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 @@ -92,8 +86,8 @@ class DataBridgesShapes: Args: yaml_config_path (str | dict): Either: - Path to YAML configuration file (str), or - - Configuration dictionary (dict) with required keys: KEY, SECRET, VERSION, - and optionally DATABRIDGES_API_KEY + - Configuration dictionary (dict) with required keys: WFP_API_CLIENT_ID, + WFP_API_CLIENT_SECRET, DATABRIDGES_VERSION, and optionally DATABRIDGES_API_KEY env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" Examples: @@ -103,9 +97,9 @@ class DataBridgesShapes: >>> # Initialize with dictionary (new method) >>> config = { - ... 'KEY': 'your-api-key', - ... 'SECRET': 'your-api-secret', - ... 'VERSION': 'v1', + ... 'WFP_API_CLIENT_ID': 'your-client-id', + ... 'WFP_API_CLIENT_SECRET': 'your-client-secret', + ... 'DATABRIDGES_VERSION': 'v1', ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' ... } >>> client = DataBridgesShapes(config) @@ -188,7 +182,7 @@ def _validate_config(self, config: Dict) -> None: Raises: ValueError: If required fields are missing from configuration """ - required_fields = ["KEY", "SECRET", "VERSION"] + required_fields = ["WFP_API_CLIENT_ID", "WFP_API_CLIENT_SECRET", "DATABRIDGES_VERSION"] missing = [field for field in required_fields if field not in config] if missing: raise ValueError( @@ -205,9 +199,9 @@ def _setup_configuration_and_authentication(self, config: Dict): Configuration: DataBridges configuration object """ - key = config["KEY"] - secret = config["SECRET"] - version = config["VERSION"] + key = config["WFP_API_CLIENT_ID"] + secret = config["WFP_API_CLIENT_SECRET"] + version = config["DATABRIDGES_VERSION"] BASE_URI = "https://gateway.api.wfp.org/vam-data-bridges" host = f"{BASE_URI}/{version.strip('/')}" From 82193c50737ce80c81340e14351100a354a57ca8 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:23:52 +0200 Subject: [PATCH 27/38] Clean up Exception handling --- data_bridges_knots/client.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index a363333..cec807a 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -438,7 +438,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, @@ -699,15 +699,12 @@ def get_economic_indicator_list( format=format, env=self.env, ) - print( - "The response of EconomicDataApi->economic_data_indicator_list_get:\n" - ) 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): @@ -726,13 +723,12 @@ 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") 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 @@ -780,7 +776,8 @@ 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( @@ -824,9 +821,9 @@ 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 @@ -947,10 +944,10 @@ def get_global_outlook( return pd.DataFrame([item.to_dict() for item in api_response.items]) except Exception as e: - print( - "Exception when calling GlobalOutlookApi->global_outlook_country_latest_get: %s\n" - % 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 @@ -1493,7 +1490,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 From 38322dfe96d15182c443565965a69234e4365add Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:25:35 +0200 Subject: [PATCH 28/38] Remove duplicate GHA run after PR merge --- .github/workflows/python-package.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index caa2e0a..4edf77d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,9 +1,6 @@ --- name: Python package on: - push: - branches: - - main pull_request: branches: - main From deb8618012f4ae787021d4dd853f7e5f53046959 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:26:56 +0200 Subject: [PATCH 29/38] Add DATABRIDGES_API_KEY to GHA secrets --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4edf77d..685ac8e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,7 +37,7 @@ jobs: 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 From e7c4f088e661b3ad291a207f5ba61bb679047967 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:29:40 +0200 Subject: [PATCH 30/38] Fix docstring indentation --- data_bridges_knots/client.py | 63 +++++++++++++++++------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index cec807a..344889c 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -76,39 +76,36 @@ def config_from_env() -> Dict: class DataBridgesShapes: - """DataBridgesShapes is a class that provides an interface to interact with 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. - - Args: - yaml_config_path (str | dict): Either: - - Path to YAML configuration file (str), or - - Configuration dictionary (dict) with required keys: WFP_API_CLIENT_ID, - WFP_API_CLIENT_SECRET, DATABRIDGES_VERSION, and optionally DATABRIDGES_API_KEY - env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod" - - Examples: - >>> # Initialize with YAML file (traditional method) - >>> client = DataBridgesShapes("data_bridges_api_config.yaml") - >>> df_prices = client.get_prices("KEN", "2025-09-01") - - >>> # Initialize with dictionary (new method) - >>> config = { - ... 'WFP_API_CLIENT_ID': 'your-client-id', - ... 'WFP_API_CLIENT_SECRET': 'your-client-secret', - ... 'DATABRIDGES_VERSION': 'v1', - ... '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) + """Interface to the Data Bridges API. + + 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 a YAML configuration file (str), or + - Configuration dictionary with required keys: WFP_API_CLIENT_ID, + WFP_API_CLIENT_SECRET, DATABRIDGES_VERSION, and optionally DATABRIDGES_API_KEY + env (str, optional): Environment to use ('prod' or 'dev'). Defaults to "prod". + + Examples: + >>> # Initialize with YAML file + >>> client = DataBridgesShapes("data_bridges_api_config.yaml") + >>> df_prices = client.get_prices("KEN", "2025-09-01") + + >>> # Initialize with dictionary + >>> config = { + ... 'WFP_API_CLIENT_ID': 'your-client-id', + ... 'WFP_API_CLIENT_SECRET': 'your-client-secret', + ... 'DATABRIDGES_VERSION': 'v1', + ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' + ... } + >>> client = DataBridgesShapes(config) + + >>> # Initialize from environment variables + >>> from data_bridges_knots.client import config_from_env + >>> client = DataBridgesShapes(config_from_env()) """ def __init__(self, yaml_config_path, env="prod"): From cc414d82273e5856818173e6f0e3f17a6932f5e5 Mon Sep 17 00:00:00 2001 From: Paolo Lucchino Date: Fri, 22 May 2026 10:42:23 +0200 Subject: [PATCH 31/38] Fix country_iso3 in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 558f9dd..2587995 100644 --- a/README.md +++ b/README.md @@ -135,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/) From c0e445889c2a4551a5a159f25f52c55e60bc87fc Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 11:20:15 +0200 Subject: [PATCH 32/38] fix: get_economic_indicator_list response as data_frame --- data_bridges_knots/__init__.py | 3 ++- data_bridges_knots/client.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) 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 2c3703c..24b1e71 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -670,7 +670,6 @@ def get_usd_indirect_quotation( ) raise - # FIXME: JSON response def get_economic_indicator_list( self, page: Optional[int] = 1, @@ -705,10 +704,12 @@ def get_economic_indicator_list( format=format, env=self.env, ) - print( + logger.info( "The response of EconomicDataApi->economic_data_indicator_list_get:\n" ) - return api_response + df = pd.DataFrame([item.to_dict() for item in api_response.items]) + df = df.replace({np.nan: None}) + return df except Exception as e: print( "Exception when calling EconomicDataApi->economic_data_indicator_list_get: %s\n" From bda09bd9950494dd5f8cfe7e3163531b0a4faaff Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 11:48:36 +0200 Subject: [PATCH 33/38] fix: add mandatory country_iso3 param to get_market_geojson_list, fix response as geoJSON and add tests for it --- data_bridges_knots/client.py | 17 +++++++++++++---- tests/test_client.py | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 24b1e71..8b79d96 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -3,6 +3,8 @@ import logging import os import time +from urllib import response +import json import warnings from datetime import date @@ -717,9 +719,12 @@ def get_economic_indicator_list( ) # BUG: Unsupported content type: 'application/geo+json - def get_market_geojson_list(self, country_iso3: Optional[str] = None): + def get_market_geojson_list(self, country_iso3: str = None): - adm0code = get_adm0_code(country_iso3) + 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( @@ -733,8 +738,12 @@ 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") - return api_response + logger.info("The response of MarketsApi->markets_geo_json_list_get:\n") + + geojson_dict = api_response.model_dump() + + geojson_json = json.dumps(geojson_dict) + return geojson_dict except Exception as e: print( "Exception when calling MarketsApi->markets_geo_json_list_get: %s\n" diff --git a/tests/test_client.py b/tests/test_client.py index 14afe93..e8a64a1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -124,14 +124,32 @@ def test_commodities_endpoints(client, func, args, kwargs): ("get_markets_as_csv", ("AFG",), {}), ("get_markets_as_csv", ("AFG",), {"local_names": True}), ("get_nearby_markets", ("AFG", 34.515, 69.208), {}), - ("get_market_geojson_list", ("AFG",), {}), ], ) + 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 From 5fcb941f11df16d5c1f85993749de08594c2b9be Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 12:03:09 +0200 Subject: [PATCH 34/38] fix: tests for market_geojson --- data_bridges_knots/client.py | 8 ++------ tests/test_client.py | 9 +++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 8b79d96..dbefc90 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -3,8 +3,6 @@ import logging import os import time -from urllib import response -import json import warnings from datetime import date @@ -718,9 +716,8 @@ def get_economic_indicator_list( % e ) - # BUG: Unsupported content type: 'application/geo+json 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: @@ -739,10 +736,9 @@ def get_market_geojson_list(self, country_iso3: str = None): adm0code=adm0code, env=self.env ) logger.info("The response of MarketsApi->markets_geo_json_list_get:\n") - + geojson_dict = api_response.model_dump() - geojson_json = json.dumps(geojson_dict) return geojson_dict except Exception as e: print( diff --git a/tests/test_client.py b/tests/test_client.py index e8a64a1..f42b9d6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -126,31 +126,32 @@ def test_commodities_endpoints(client, func, args, kwargs): ("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"), {}), + ("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 # ========================================================= From 6e4569e4f8af0c43165e3e23732cbd3c454495e7 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 13:00:51 +0200 Subject: [PATCH 35/38] chore(python-package.yml): amend GitHub Action to test Python packaging, linting and test suits only on main branch (no longer when PR to main) --- .github/workflows/actions-updater.yml | 26 -------------------------- .github/workflows/publish-s3.yml | 3 +++ .github/workflows/python-package.yml | 8 ++++---- 3 files changed, 7 insertions(+), 30 deletions(-) delete mode 100644 .github/workflows/actions-updater.yml 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/publish-s3.yml b/.github/workflows/publish-s3.yml index 446157a..78b15d0 100644 --- a/.github/workflows/publish-s3.yml +++ b/.github/workflows/publish-s3.yml @@ -2,6 +2,9 @@ name: Publish to S3 PyPI on: push: + branches: + - main + tags: - '*' diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 685ac8e..5c92567 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,9 +1,9 @@ --- name: Python package on: - pull_request: - branches: - - main + push: + tags: + - '*' permissions: contents: write @@ -53,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 From c91cdeb151d4b6c5bcf384d4d5af68dc10a1f26b Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 14:29:35 +0200 Subject: [PATCH 36/38] chore: amend python-package.yml action to run only on main --- .github/workflows/publish-s3.yml | 3 --- .github/workflows/python-package.yml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-s3.yml b/.github/workflows/publish-s3.yml index 78b15d0..446157a 100644 --- a/.github/workflows/publish-s3.yml +++ b/.github/workflows/publish-s3.yml @@ -2,9 +2,6 @@ name: Publish to S3 PyPI on: push: - branches: - - main - tags: - '*' diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5c92567..4d49719 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -2,8 +2,8 @@ name: Python package on: push: - tags: - - '*' + branches: + - main permissions: contents: write From bf3209138138ca092c69b1273050909331cd3388 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 14:32:30 +0200 Subject: [PATCH 37/38] feat!(client): add default API version that can be used in client as DataBridgesShape initialization variable; rename get_household_xslform_definition to get_household_xlsform_definition (typo in XLSForm) --- data_bridges_knots/client.py | 53 ++++++++++++++++++++++-------------- tests/conftest.py | 6 ++-- tests/test_client.py | 5 ++-- tests/test_labels.py | 28 +++++++++---------- 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index d4db23a..10054d4 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -33,7 +33,6 @@ def config_from_env() -> Dict: Reads configuration from the following environment variables: - WFP_API_CLIENT_ID: WFP API Gateway key for authentication - WFP_API_CLIENT_SECRET: WFP API Gateway secret for authentication - - DATABRIDGES_VERSION: API version (e.g., 'v1') - DATABRIDGES_API_KEY: (Optional) DataBridges-specific API key for certain endpoints Returns: @@ -46,12 +45,15 @@ def config_from_env() -> Dict: >>> import os >>> os.environ['WFP_API_CLIENT_ID'] = 'your_key' >>> os.environ['WFP_API_CLIENT_SECRET'] = 'your_secret' - >>> os.environ['DATABRIDGES_VERSION'] = 'v1' >>> config = config_from_env() >>> client = DataBridgesShapes(config) """ - required_vars = ["WFP_API_CLIENT_ID", "WFP_API_CLIENT_SECRET", "DATABRIDGES_VERSION"] + required_vars = [ + "WFP_API_CLIENT_ID", + "WFP_API_CLIENT_SECRET", + "DATABRIDGES_VERSION", + ] config = {} missing = [] @@ -85,9 +87,11 @@ class DataBridgesShapes: Args: yaml_config_path (str | dict): Either: - Path to a YAML configuration file (str), or - - Configuration dictionary with required keys: WFP_API_CLIENT_ID, - WFP_API_CLIENT_SECRET, DATABRIDGES_VERSION, and optionally DATABRIDGES_API_KEY + - 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 @@ -98,7 +102,6 @@ class DataBridgesShapes: >>> config = { ... 'WFP_API_CLIENT_ID': 'your-client-id', ... 'WFP_API_CLIENT_SECRET': 'your-client-secret', - ... 'DATABRIDGES_VERSION': 'v1', ... 'DATABRIDGES_API_KEY': 'optional-databridges-key' ... } >>> client = DataBridgesShapes(config) @@ -108,7 +111,7 @@ class DataBridgesShapes: >>> 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( ( @@ -121,6 +124,11 @@ def __init__(self, yaml_config_path, env="prod"): 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) @@ -128,11 +136,10 @@ 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 ( @@ -179,7 +186,7 @@ def _validate_config(self, config: Dict) -> None: Raises: ValueError: If required fields are missing from configuration """ - required_fields = ["WFP_API_CLIENT_ID", "WFP_API_CLIENT_SECRET", "DATABRIDGES_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( @@ -198,9 +205,9 @@ def _setup_configuration_and_authentication(self, config: Dict): """ key = config["WFP_API_CLIENT_ID"] secret = config["WFP_API_CLIENT_SECRET"] - version = config["DATABRIDGES_VERSION"] BASE_URI = "https://gateway.api.wfp.org/vam-data-bridges" - host = f"{BASE_URI}/{version.strip('/')}" + host = f"{BASE_URI}/{self.api_version.strip('/')}" + print("host: ", host) logger.info("DataBridges API: %s", host) @@ -704,7 +711,8 @@ def get_economic_indicator_list( return api_response except Exception as e: logger.error( - "Exception when calling EconomicDataApi->economic_data_indicator_list_get: %s", e + "Exception when calling EconomicDataApi->economic_data_indicator_list_get: %s", + e, ) raise @@ -735,7 +743,8 @@ def get_market_geojson_list(self, country_iso3: str = None): return api_response except Exception as e: logger.error( - "Exception when calling MarketsApi->markets_geo_json_list_get: %s", e + "Exception when calling MarketsApi->markets_geo_json_list_get: %s", + e, ) raise @@ -785,10 +794,11 @@ def get_markets_list( df = df.replace({np.nan: None}) return df except Exception as e: - logger.error("Exception when calling MarketsApi->markets_list_get: %s", 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: @@ -830,7 +840,8 @@ def get_markets_as_csv( return api_response except Exception as e: logger.error( - "Exception when calling MarketsApi->markets_markets_as_csv_get: %s", e + "Exception when calling MarketsApi->markets_markets_as_csv_get: %s", + e, ) raise @@ -1115,7 +1126,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: @@ -1131,7 +1142,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] @@ -1175,7 +1186,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: 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 f96406c..621e4e1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -141,7 +141,7 @@ def test_markets_endpoints(client, func, args, kwargs): @pytest.mark.parametrize( "func,args,kwargs", [ - ("get_market_geojson_list", ("AFG", ), {}), + ("get_market_geojson_list", ("AFG",), {}), ], ) def test_geojson_endpoints(client, func, args, kwargs): @@ -150,7 +150,6 @@ def test_geojson_endpoints(client, func, args, kwargs): assert isinstance(result, dict) - # ========================================================= # ✅ ECONOMIC + OUTLOOK # ========================================================= @@ -190,7 +189,7 @@ def test_economic_indicator_endpoints(client, func, args, kwargs): "end_date": "2024-12-31", }, ), - ("get_household_xslform_definition", (2067,), {}), + ("get_household_xlsform_definition", (2067,), {}), ("get_household_questionnaire", (2075,), {}), ("get_choice_list", (123,), {}), ], 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) From d406a3192fefa121ed7fc8a0183a09ee8bb15214 Mon Sep 17 00:00:00 2001 From: AlexGherardelli Date: Fri, 22 May 2026 14:45:06 +0200 Subject: [PATCH 38/38] chore: linting + add dev dependency for tests --- data_bridges_knots/client.py | 1 - pyproject.toml | 3 ++- uv.lock | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data_bridges_knots/client.py b/data_bridges_knots/client.py index 10054d4..c98e046 100644 --- a/data_bridges_knots/client.py +++ b/data_bridges_knots/client.py @@ -137,7 +137,6 @@ def __init__(self, yaml_config_path, env="prod", api_version="v1"): self.configuration = self._setup_configuration_and_authentication(self.config) self.data_bridges_api_key = self.config.get("DATABRIDGES_API_KEY", "") - def __repr__(self): return f"DataBridgesShapes(host='{self.configuration.host}', env='{self.env}'), api_version='{self.api_version}'" diff --git a/pyproject.toml b/pyproject.toml index c44fb57..00edad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ dev = [ "bandit", "python-dotenv>=1.2.2", "bumpver", - "pip-tools" + "pip-tools", + "setuptools>=82.0.1", ] STATA = ["stata-setup", "pystata"] R = [] diff --git a/uv.lock b/uv.lock index db0a09f..28e8134 100644 --- a/uv.lock +++ b/uv.lock @@ -541,6 +541,7 @@ dev = [ { name = "pytest-cov" }, { name = "python-dotenv" }, { name = "ruff" }, + { name = "setuptools" }, ] stata = [ { name = "pystata" }, @@ -571,6 +572,7 @@ dev = [ { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "ruff" }, + { name = "setuptools", specifier = ">=82.0.1" }, ] r = [] stata = [