Skip to content

Commit

Permalink
No wfp python lib (#59)
Browse files Browse the repository at this point in the history
* Use 5.0.0 of databridges REST API directly
Remove WFP databridges Python library

* Fix test

* Add retrying

* Pass WFP API object to WFPExchangeRates
  • Loading branch information
mcarans authored Oct 30, 2024
1 parent ff4738c commit 9c1759d
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 98 deletions.
6 changes: 3 additions & 3 deletions .config/pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ default_language_version:
python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-ast
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
rev: v0.7.0
hooks:
# Run the linter.
- id: ruff
Expand All @@ -17,7 +17,7 @@ repos:
- id: ruff-format
args: [--config, .config/ruff.toml]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.4.5
rev: 0.4.24
hooks:
# Run the pip compile
- id: pip-compile
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ requires-python = ">=3.8"

dependencies = [
"hdx-python-utilities>=3.7.4",
"libhxl>=5.2.1",
"libhxl>=5.2.2",
"pyphonetics",
"tenacity",
]
dynamic = ["version"]

Expand All @@ -50,16 +51,12 @@ Homepage = "https://github.com/OCHA-DAP/hdx-python-country"
[project.optional-dependencies]
test = ["pytest", "pytest-cov"]
dev = ["pre-commit"]
#wfp = ["data-bridges-client@git+https://github.com/WFP-VAM/DataBridgesAPI@dev#egg=data-bridges-client"]


#########
# Hatch #
#########

#[tool.hatch.metadata]
#allow-direct-references = true

# Build

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -92,6 +89,9 @@ run = """
--cov-report=lcov --cov-report=term-missing
"""

[tool.hatch.envs.hatch-static-analysis]
dependencies = ["ruff==0.7.0"]

[tool.hatch.envs.hatch-static-analysis.scripts]
format-check = ["ruff format --config .config/ruff.toml --check --diff {args:.}",]
format-fix = ["ruff format --config .config/ruff.toml {args:.}",]
Expand Down
22 changes: 13 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ charset-normalizer==3.4.0
# via requests
click==8.1.7
# via typer
coverage==7.6.3
coverage==7.6.4
# via pytest-cov
distlib==0.3.9
# via virtualenv
et-xmlfile==1.1.0
et-xmlfile==2.0.0
# via openpyxl
filelock==3.16.1
# via virtualenv
Expand Down Expand Up @@ -54,15 +54,15 @@ jsonschema==4.23.0
# tableschema-to-template
jsonschema-specifications==2024.10.1
# via jsonschema
libhxl==5.2.1
libhxl==5.2.2
# via hdx-python-country (pyproject.toml)
loguru==0.7.2
# via hdx-python-utilities
markdown-it-py==3.0.0
# via rich
marko==2.1.2
# via frictionless
markupsafe==3.0.1
markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
Expand All @@ -74,6 +74,8 @@ packaging==24.1
# via pytest
petl==1.7.15
# via frictionless
pip==24.3.1
# via simpleeval
platformdirs==4.3.6
# via virtualenv
pluggy==1.5.0
Expand All @@ -96,7 +98,7 @@ pytest==8.3.3
# via
# hdx-python-country (pyproject.toml)
# pytest-cov
pytest-cov==5.0.0
pytest-cov==6.0.0
# via hdx-python-country (pyproject.toml)
python-dateutil==2.9.0.post0
# via
Expand Down Expand Up @@ -127,19 +129,19 @@ requests-file==2.1.0
# via hdx-python-utilities
rfc3986==2.0.0
# via frictionless
rich==13.9.2
rich==13.9.3
# via typer
rpds-py==0.20.0
# via
# jsonschema
# referencing
ruamel-yaml==0.18.6
# via hdx-python-utilities
ruamel-yaml-clib==0.2.8
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
shellingham==1.5.4
# via typer
simpleeval==1.0.0
simpleeval==1.0.1
# via frictionless
six==1.16.0
# via python-dateutil
Expand All @@ -151,6 +153,8 @@ tableschema-to-template==0.0.13
# via hdx-python-utilities
tabulate==0.9.0
# via frictionless
tenacity==9.0.0
# via hdx-python-country (pyproject.toml)
text-unidecode==1.3
# via python-slugify
typer==0.12.5
Expand All @@ -171,7 +175,7 @@ urllib3==2.2.3
# requests
validators==0.34.0
# via frictionless
virtualenv==20.26.6
virtualenv==20.27.1
# via pre-commit
wheel==0.44.0
# via libhxl
Expand Down
157 changes: 157 additions & 0 deletions src/hdx/location/wfp_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import logging
from typing import Any, Dict, List, Optional

from tenacity import (
Retrying,
after_log,
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
)

from hdx.utilities.base_downloader import DownloadError
from hdx.utilities.downloader import Download
from hdx.utilities.retriever import Retrieve

logger = logging.getLogger(__name__)


class WFPAPI:
"""Light wrapper around WFP REST API. It needs a token_downloader that has
been configured with WFP basic authentication credentials and a retriever
that will configured by this class with the bearer token obtained from the
token_downloader.
Args:
token_downloader (Download): Download object with WFP basic authentication
retriever (Retrieve): Retrieve object for interacting with WFP API
"""

token_url = "https://api.wfp.org/token"
base_url = "https://api.wfp.org/vam-data-bridges/5.0.0/"
scope = "vamdatabridges_commodities-list_get vamdatabridges_commodityunits-list_get vamdatabridges_marketprices-alps_get vamdatabridges_commodities-categories-list_get vamdatabridges_commodityunits-conversion-list_get vamdatabridges_marketprices-priceweekly_get vamdatabridges_markets-geojsonlist_get vamdatabridges_marketprices-pricemonthly_get vamdatabridges_markets-list_get vamdatabridges_currency-list_get vamdatabridges_currency-usdindirectquotation_get"
default_retry_params = {
"retry": retry_if_exception_type(DownloadError),
"after": after_log(logger, logging.INFO),
}

def __init__(
self,
token_downloader: Download,
retriever: Retrieve,
):
self.token_downloader = token_downloader
self.retriever = retriever
self.retry_params = {"attempts": 1, "wait": 1}

def get_retry_params(self) -> Dict:
return self.retry_params

def update_retry_params(self, attempts: int, wait: int) -> Dict:
self.retry_params["attempts"] = attempts
self.retry_params["wait"] = wait
return self.retry_params

def refresh_token(self) -> None:
self.token_downloader.download(
self.token_url,
post=True,
parameters={
"grant_type": "client_credentials",
"scope": self.scope,
},
)
bearer_token = self.token_downloader.get_json()["access_token"]
self.retriever.downloader.set_bearer_token(bearer_token)

def retrieve(
self,
url: str,
filename: str,
log: str,
parameters: Optional[Dict] = None,
) -> Any:
"""Retrieve JSON from WFP API.
Args:
url (str): URL to download
filename (Optional[str]): Filename of saved file. Defaults to getting from url.
log (Optional[str]): Text to use in log string to describe download. Defaults to filename.
parameters (Dict): Parameters to pass to download_json call
Returns:
Any: The data from the JSON file
"""
retryer = Retrying(
retry=self.default_retry_params["retry"],
after=self.default_retry_params["after"],
stop=stop_after_attempt(self.retry_params["attempts"]),
wait=wait_fixed(self.retry_params["wait"]),
)
for attempt in retryer:
with attempt:
try:
results = self.retriever.download_json(
url, filename, log, False, parameters=parameters
)
except DownloadError:
response = self.retriever.downloader.response
if response and response.status_code not in (
104,
401,
403,
):
raise
self.refresh_token()
results = self.retriever.download_json(
url, filename, log, False, parameters=parameters
)
return results

def get_items(
self,
endpoint: str,
countryiso3: Optional[str] = None,
parameters: Optional[Dict] = None,
) -> List:
"""Retrieve a list of items from the WFP API.
Args:
endpoint (str): End point to call
countryiso3 (Optional[str]): Country for which to obtain data. Defaults to all countries.
parameters (Optional[Dict]): Paramaters to pass to call. Defaults to None.
Returns:
List: List of items from the WFP endpoint
"""
if not parameters:
parameters = {}
all_data = []
url = f"{self.base_url}{endpoint}"
url_parts = url.split("/")
base_filename = f"{url_parts[-2]}_{url_parts[-1]}"
if countryiso3 == "PSE": # hack as PSE is treated by WFP as 2 areas
countryiso3s = ["PSW", "PSG"]
else:
countryiso3s = [countryiso3]
for countryiso3 in countryiso3s:
page = 1
data = None
while data is None or len(data) > 0:
page_parameters = {"page": page}
page_parameters.update(parameters)
if countryiso3 is None:
filename = f"{base_filename}_{page}.json"
log = f"{base_filename} page {page}"
else:
filename = f"{base_filename}_{countryiso3}_{page}.json"
log = f"{base_filename} for {countryiso3} page {page}"
page_parameters["CountryCode"] = countryiso3
try:
json = self.retrieve(url, filename, log, page_parameters)
except FileNotFoundError:
json = {"items": []}
data = json["items"]
all_data.extend(data)
page = page + 1
return all_data
Loading

0 comments on commit 9c1759d

Please sign in to comment.