hdx-python-country 4.1.2__tar.gz → 4.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/.github/workflows/run-python-tests.yaml +2 -2
  2. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/PKG-INFO +28 -1
  3. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/README.md +25 -0
  4. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/pyproject.toml +13 -0
  5. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/_version.py +2 -2
  6. hdx_python_country-4.1.3/src/hdx/location/wfp_api.py +323 -0
  7. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/wfp_exchangerates.py +15 -14
  8. hdx_python_country-4.1.3/tests/fixtures/wfp/Markets_List_AFG_1.json +28 -0
  9. hdx_python_country-4.1.3/tests/fixtures/wfp/Markets_List_AFG_2.json +5 -0
  10. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/test_currency.py +1 -1
  11. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/test_wfp_exchangerates.py +6 -1
  12. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/uv.lock +205 -103
  13. hdx_python_country-4.1.2/src/hdx/location/wfp_api.py +0 -171
  14. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/.github/workflows/publish.yaml +0 -0
  15. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/.gitignore +0 -0
  16. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/.pre-commit-config.yaml +0 -0
  17. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/LICENSE +0 -0
  18. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/documentation/.readthedocs.yaml +0 -0
  19. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/documentation/index.md +0 -0
  20. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/documentation/mkdocs.yaml +0 -0
  21. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy.csv +0 -0
  22. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/__init__.py +0 -0
  23. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/adminlevel.py +0 -0
  24. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/country.py +0 -0
  25. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/currency.py +0 -0
  26. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/src/hdx/location/int_timestamp.py +0 -0
  27. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/adminlevel.yaml +0 -0
  28. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/adminlevelparent.yaml +0 -0
  29. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
  30. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
  31. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/secondary_historic_rates.csv +0 -0
  32. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/secondary_rates.json +0 -0
  33. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_List_1.json +0 -0
  34. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_List_2.json +0 -0
  35. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_1.json +0 -0
  36. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_10.json +0 -0
  37. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_2.json +0 -0
  38. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_3.json +0 -0
  39. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_4.json +0 -0
  40. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_5.json +0 -0
  41. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_6.json +0 -0
  42. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_7.json +0 -0
  43. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_8.json +0 -0
  44. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_9.json +0 -0
  45. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
  46. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/__init__.py +0 -0
  47. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/conftest.py +0 -0
  48. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/test_adminlevel.py +0 -0
  49. {hdx_python_country-4.1.2 → hdx_python_country-4.1.3}/tests/hdx/location/test_country.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  python-version: "3.13"
26
26
 
27
27
  - name: Install dependencies
28
- run: uv sync --frozen
28
+ run: uv sync --frozen --extra wfp
29
29
 
30
30
  - name: Check styling
31
31
  run: |
@@ -37,7 +37,7 @@ jobs:
37
37
 
38
38
  - name: Publish Unit Test Results
39
39
  uses: EnricoMi/publish-unit-test-result-action@v2
40
- if: always()
40
+ if: success()
41
41
  with:
42
42
  files: test-results.xml
43
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hdx-python-country
3
- Version: 4.1.2
3
+ Version: 4.1.3
4
4
  Summary: HDX Python country code and exchange rate (fx) utilities
5
5
  Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-country
6
6
  Project-URL: Repository, https://github.com/OCHA-DAP/hdx-python-country
@@ -31,6 +31,8 @@ Requires-Dist: hdx-python-utilities<5,>=4.1.2
31
31
  Requires-Dist: tenacity
32
32
  Provides-Extra: docs
33
33
  Requires-Dist: mkapi; extra == 'docs'
34
+ Provides-Extra: wfp
35
+ Requires-Dist: data-bridges-client<10,>=9.0.0; extra == 'wfp'
34
36
  Description-Content-Type: text/markdown
35
37
 
36
38
  [![Build Status](https://github.com/OCHA-DAP/hdx-python-country/actions/workflows/run-python-tests.yaml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-country/actions/workflows/run-python-tests.yaml)
@@ -140,3 +142,28 @@ To run the tests and view coverage, execute:
140
142
  The documentation, including API documentation, is generated using ReadtheDocs and
141
143
  MkDocs with Material. As you change the source code, remember to update the
142
144
  documentation at `documentation/index.md`.
145
+
146
+ ## WFP DataBridges Client
147
+
148
+ The `WFPAPI` class (`hdx.location.wfp_api`) uses WFP's `data-bridges-client` library, which is
149
+ published on WFP's own package index rather than PyPI. To install it, use the `wfp` extra with
150
+ that index added:
151
+
152
+ ```shell
153
+ pip install "hdx-python-country[wfp]" \
154
+ --extra-index-url https://d2i4vvypvg40rv.cloudfront.net/pypi/ \
155
+ --index-strategy unsafe-best-match
156
+ ```
157
+
158
+ If using `uv`, add the following to your project's `pyproject.toml`:
159
+
160
+ ```toml
161
+ [tool.uv]
162
+ index-strategy = "unsafe-best-match"
163
+
164
+ [[tool.uv.index]]
165
+ name = "wfp"
166
+ url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/"
167
+ ```
168
+
169
+ then run `uv sync --extra wfp`.
@@ -105,3 +105,28 @@ To run the tests and view coverage, execute:
105
105
  The documentation, including API documentation, is generated using ReadtheDocs and
106
106
  MkDocs with Material. As you change the source code, remember to update the
107
107
  documentation at `documentation/index.md`.
108
+
109
+ ## WFP DataBridges Client
110
+
111
+ The `WFPAPI` class (`hdx.location.wfp_api`) uses WFP's `data-bridges-client` library, which is
112
+ published on WFP's own package index rather than PyPI. To install it, use the `wfp` extra with
113
+ that index added:
114
+
115
+ ```shell
116
+ pip install "hdx-python-country[wfp]" \
117
+ --extra-index-url https://d2i4vvypvg40rv.cloudfront.net/pypi/ \
118
+ --index-strategy unsafe-best-match
119
+ ```
120
+
121
+ If using `uv`, add the following to your project's `pyproject.toml`:
122
+
123
+ ```toml
124
+ [tool.uv]
125
+ index-strategy = "unsafe-best-match"
126
+
127
+ [[tool.uv.index]]
128
+ name = "wfp"
129
+ url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/"
130
+ ```
131
+
132
+ then run `uv sync --extra wfp`.
@@ -42,6 +42,7 @@ dependencies = [
42
42
 
43
43
  [project.optional-dependencies]
44
44
  docs = ["mkapi"]
45
+ wfp = ["data-bridges-client>=9.0.0,<10"]
45
46
 
46
47
  [dependency-groups]
47
48
  dev = [
@@ -49,6 +50,7 @@ dev = [
49
50
  "pytest-cov",
50
51
  "pre-commit",
51
52
  "ruff==0.14.13",
53
+ "data-bridges-client>=9.0.0,<10",
52
54
  ]
53
55
 
54
56
  [project.urls]
@@ -76,6 +78,17 @@ packages = ["src/hdx"]
76
78
  [tool.hatch.metadata]
77
79
  allow-direct-references = true
78
80
 
81
+ # ----------------------------------------------------------------------------
82
+ # uv (Package Management)
83
+ # ----------------------------------------------------------------------------
84
+
85
+ [tool.uv]
86
+ index-strategy = "unsafe-best-match"
87
+
88
+ [[tool.uv.index]]
89
+ name = "wfp"
90
+ url = "https://d2i4vvypvg40rv.cloudfront.net/pypi/"
91
+
79
92
  # ----------------------------------------------------------------------------
80
93
  # Ruff (Linting & Formatting)
81
94
  # ----------------------------------------------------------------------------
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '4.1.2'
22
- __version_tuple__ = version_tuple = (4, 1, 2)
21
+ __version__ = version = '4.1.3'
22
+ __version_tuple__ = version_tuple = (4, 1, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,323 @@
1
+ import json
2
+ import logging
3
+ from collections.abc import Callable
4
+ from datetime import date, datetime
5
+ from os import getenv
6
+ from typing import Any
7
+
8
+ from data_bridges_client import (
9
+ ApiClient,
10
+ ApiException,
11
+ CommoditiesApi,
12
+ Configuration,
13
+ CurrencyApi,
14
+ MarketPricesApi,
15
+ MarketsApi,
16
+ PagedCommodityListDTO,
17
+ PagedCurrencyListDTO,
18
+ PagedMarketListDTO,
19
+ UsdIndirectQuotationPagedResult,
20
+ ViewExtendedMonthlyAggregatedPricePagedResult,
21
+ )
22
+ from data_bridges_client.models.commodity_dto import CommodityDTO
23
+ from data_bridges_client.models.currency_dto import CurrencyDTO
24
+ from data_bridges_client.models.market_dto import MarketDTO
25
+ from data_bridges_client.models.usd_indirect_quotation import UsdIndirectQuotation
26
+ from data_bridges_client.models.view_extended_monthly_aggregated_price import (
27
+ ViewExtendedMonthlyAggregatedPrice,
28
+ )
29
+ from data_bridges_client.token import WfpApiToken
30
+ from hdx.utilities.loader import load_json
31
+ from hdx.utilities.retriever import Retrieve
32
+ from tenacity import (
33
+ Retrying,
34
+ after_log,
35
+ retry_if_exception_type,
36
+ stop_after_attempt,
37
+ wait_fixed,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ def _json_default(value: Any) -> str:
44
+ """JSON serializer fallback for values data_bridges_client's generated
45
+ to_dict() leaves as native Python objects (e.g. datetime fields, since
46
+ it calls pydantic's model_dump() rather than model_dump(mode="json")).
47
+ """
48
+ if isinstance(value, (datetime, date)):
49
+ return value.isoformat()
50
+ raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
51
+
52
+
53
+ class WFPAPI:
54
+ """Light wrapper around WFP's data-bridges-client library. It needs a
55
+ retriever used only to decide whether to load from disk or hit the
56
+ network and whether to persist the network result. Token refreshing is
57
+ delegated to data_bridges_client's own WfpApiToken.
58
+
59
+ Args:
60
+ retriever: Retrieve object for interacting with WFP API
61
+ """
62
+
63
+ default_retry_params = {
64
+ "retry": retry_if_exception_type(ApiException),
65
+ "after": after_log(logger, logging.INFO),
66
+ }
67
+
68
+ def __init__(
69
+ self,
70
+ retriever: Retrieve,
71
+ wfp_key: str | None = None,
72
+ wfp_secret: str | None = None,
73
+ ):
74
+ self.retriever = retriever
75
+ self.retry_params = {"attempts": 1, "wait": 1}
76
+ if wfp_key:
77
+ self._wfp_key = wfp_key
78
+ else:
79
+ self._wfp_key = getenv("WFP_KEY")
80
+ if wfp_secret:
81
+ self._wfp_secret = wfp_secret
82
+ else:
83
+ self._wfp_secret = getenv("WFP_SECRET")
84
+ self.token = WfpApiToken(api_key=self._wfp_key, api_secret=self._wfp_secret)
85
+ self.configuration = Configuration()
86
+ self.api_client = ApiClient(self.configuration)
87
+ self.currency_api = CurrencyApi(self.api_client)
88
+ self.markets_api = MarketsApi(self.api_client)
89
+ self.commodities_api = CommoditiesApi(self.api_client)
90
+ self.market_prices_api = MarketPricesApi(self.api_client)
91
+
92
+ def get_retry_params(self) -> dict:
93
+ return self.retry_params
94
+
95
+ def update_retry_params(self, attempts: int, wait: int) -> dict:
96
+ self.retry_params["attempts"] = attempts
97
+ self.retry_params["wait"] = wait
98
+ return self.retry_params
99
+
100
+ def refresh_token(self) -> None:
101
+ self.configuration.access_token = self.token.refresh()
102
+
103
+ def _with_retry(self, api_method: Callable, **kwargs: Any) -> Any:
104
+ retryer = Retrying(
105
+ retry=self.default_retry_params["retry"],
106
+ after=self.default_retry_params["after"],
107
+ stop=stop_after_attempt(self.retry_params["attempts"]),
108
+ wait=wait_fixed(self.retry_params["wait"]),
109
+ )
110
+ for attempt in retryer:
111
+ with attempt:
112
+ try:
113
+ return api_method(**kwargs)
114
+ except ApiException as err:
115
+ if err.status not in (104, 401, 403):
116
+ raise
117
+ self.refresh_token()
118
+ return api_method(**kwargs)
119
+
120
+ def _call(
121
+ self,
122
+ api_method: Callable,
123
+ model_cls: type,
124
+ filename: str,
125
+ log: str,
126
+ **kwargs: Any,
127
+ ) -> Any:
128
+ """Call a data_bridges_client API method, loading from or saving to
129
+ disk exactly as Retrieve does, so existing fixtures keep working
130
+ unchanged.
131
+
132
+ Args:
133
+ api_method: Bound method on one of the data_bridges_client APIs
134
+ model_cls: Paged result model class to (de)serialize with
135
+ filename: Filename of saved file
136
+ log: Text to use in log string to describe the call
137
+ **kwargs: Parameters to pass to api_method
138
+
139
+ Returns:
140
+ An instance of model_cls, or None if there is no saved data
141
+ """
142
+ filename, _ = self.retriever.get_filename("", filename)
143
+ saved_path = self.retriever.saved_dir / filename
144
+ if self.retriever.use_saved:
145
+ logger.info(f"Using saved {log} in {saved_path}")
146
+ try:
147
+ return model_cls.from_dict(load_json(saved_path))
148
+ except FileNotFoundError:
149
+ return None
150
+ result = self._with_retry(api_method, **kwargs)
151
+ if self.retriever.save:
152
+ logger.info(f"Saving {log} in {saved_path}")
153
+ with open(saved_path, "w", encoding="utf-8") as f:
154
+ json.dump(result.to_dict(), f, default=_json_default)
155
+ return result
156
+
157
+ @staticmethod
158
+ def _countryiso3s(countryiso3: str | None) -> list:
159
+ if countryiso3 == "PSE": # hack as PSE is treated by WFP as 2 areas
160
+ return ["PSW", "PSG"]
161
+ return [countryiso3]
162
+
163
+ @staticmethod
164
+ def _filename_and_log(
165
+ base_filename: str, countryiso3: str | None, page: int
166
+ ) -> tuple[str, str]:
167
+ if countryiso3 is None:
168
+ return f"{base_filename}_{page}.json", f"{base_filename} page {page}"
169
+ return (
170
+ f"{base_filename}_{countryiso3}_{page}.json",
171
+ f"{base_filename} for {countryiso3} page {page}",
172
+ )
173
+
174
+ def _get_all_pages(
175
+ self,
176
+ api_method: Callable,
177
+ model_cls: type,
178
+ base_filename: str,
179
+ countryiso3: str | None,
180
+ country_param: str,
181
+ extra_params: dict,
182
+ ) -> list:
183
+ all_items = []
184
+ for country in self._countryiso3s(countryiso3):
185
+ page = 1
186
+ while True:
187
+ kwargs = dict(extra_params)
188
+ kwargs["page"] = page
189
+ if country is not None:
190
+ kwargs[country_param] = country
191
+ filename, log = self._filename_and_log(base_filename, country, page)
192
+ result = self._call(api_method, model_cls, filename, log, **kwargs)
193
+ items = result.items if result else None
194
+ if not items:
195
+ break
196
+ all_items.extend(items)
197
+ page += 1
198
+ return all_items
199
+
200
+ def get_currencies(
201
+ self, countryiso3: str | None = None, currency: str | None = None
202
+ ) -> list[CurrencyDTO]:
203
+ """Get list of currencies from the WFP API.
204
+
205
+ Args:
206
+ countryiso3: Country for which to obtain data. Defaults to all countries.
207
+ currency: Currency 3-letter code to filter by. Defaults to all currencies.
208
+
209
+ Returns:
210
+ List of currencies from the WFP API
211
+ """
212
+ extra_params = {"currency_name": currency} if currency else {}
213
+ return self._get_all_pages(
214
+ self.currency_api.currency_list_get,
215
+ PagedCurrencyListDTO,
216
+ "Currency_List",
217
+ countryiso3,
218
+ "country_code",
219
+ extra_params,
220
+ )
221
+
222
+ def get_currency_usd_indirect_quotations(
223
+ self, currency: str, countryiso3: str | None = None
224
+ ) -> list[UsdIndirectQuotation]:
225
+ """Get USD indirect quotations for a currency from the WFP API.
226
+
227
+ Args:
228
+ currency: Currency 3-letter code
229
+ countryiso3: Country for which to obtain data. Defaults to all countries.
230
+
231
+ Returns:
232
+ List of USD indirect quotations from the WFP API
233
+ """
234
+ return self._get_all_pages(
235
+ self.currency_api.currency_usd_indirect_quotation_get,
236
+ UsdIndirectQuotationPagedResult,
237
+ "Currency_UsdIndirectQuotation",
238
+ countryiso3,
239
+ # this endpoint uses country_iso3, unlike every other one here which
240
+ # uses country_code - don't "fix" this to country_code
241
+ "country_iso3",
242
+ {"currency_name": currency},
243
+ )
244
+
245
+ def get_markets(self, countryiso3: str | None = None) -> list[MarketDTO]:
246
+ """Get list of markets from the WFP API.
247
+
248
+ Args:
249
+ countryiso3: Country for which to obtain data. Defaults to all countries.
250
+
251
+ Returns:
252
+ List of markets from the WFP API
253
+ """
254
+ return self._get_all_pages(
255
+ self.markets_api.markets_list_get,
256
+ PagedMarketListDTO,
257
+ "Markets_List",
258
+ countryiso3,
259
+ "country_code",
260
+ {},
261
+ )
262
+
263
+ def get_commodities(self, countryiso3: str | None = None) -> list[CommodityDTO]:
264
+ """Get list of commodities from the WFP API.
265
+
266
+ Args:
267
+ countryiso3: Country for which to obtain data. Defaults to all countries.
268
+
269
+ Returns:
270
+ List of commodities from the WFP API
271
+ """
272
+ return self._get_all_pages(
273
+ self.commodities_api.commodities_list_get,
274
+ PagedCommodityListDTO,
275
+ "Commodities_List",
276
+ countryiso3,
277
+ "country_code",
278
+ {},
279
+ )
280
+
281
+ def get_commodity_categories(
282
+ self, countryiso3: str | None = None
283
+ ) -> list[CommodityDTO]:
284
+ """Get list of commodity categories from the WFP API.
285
+
286
+ Args:
287
+ countryiso3: Country for which to obtain data. Defaults to all countries.
288
+
289
+ Returns:
290
+ List of commodity categories from the WFP API
291
+ """
292
+ return self._get_all_pages(
293
+ self.commodities_api.commodities_categories_list_get,
294
+ PagedCommodityListDTO,
295
+ "Commodities_Categories_List",
296
+ countryiso3,
297
+ "country_code",
298
+ {},
299
+ )
300
+
301
+ def get_market_prices_monthly(
302
+ self, countryiso3: str | None = None, **kwargs: Any
303
+ ) -> list[ViewExtendedMonthlyAggregatedPrice]:
304
+ """Get monthly aggregated market prices from the WFP API.
305
+
306
+ Args:
307
+ countryiso3: Country for which to obtain data. Defaults to all countries.
308
+ **kwargs: Additional filters accepted by
309
+ data_bridges_client's MarketPricesApi.market_prices_price_monthly_get,
310
+ e.g. market_id, commodity_id, price_type_name, currency_id,
311
+ price_flag, start_date, end_date, latest_value_only
312
+
313
+ Returns:
314
+ List of monthly aggregated market prices from the WFP API
315
+ """
316
+ return self._get_all_pages(
317
+ self.market_prices_api.market_prices_price_monthly_get,
318
+ ViewExtendedMonthlyAggregatedPricePagedResult,
319
+ "MarketPrices_PriceMonthly",
320
+ countryiso3,
321
+ "country_code",
322
+ kwargs,
323
+ )
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  from collections.abc import Sequence
3
-
4
- from hdx.utilities.dateparse import parse_date
3
+ from datetime import timezone
5
4
 
6
5
  from .int_timestamp import get_int_timestamp
7
6
  from .wfp_api import WFPAPI
@@ -26,11 +25,11 @@ class WFPExchangeRates:
26
25
  List of currency codes and names in WFP API
27
26
  """
28
27
  currencies = []
29
- for currency in self.wfp_api.get_items("Currency/List"):
30
- currency_name = currency["extendedName"]
28
+ for currency in self.wfp_api.get_currencies():
29
+ currency_name = currency.extended_name
31
30
  if currency_name:
32
31
  currency_name = currency_name.strip()
33
- currencies.append({"code": currency["name"], "name": currency_name})
32
+ currencies.append({"code": currency.name, "name": currency_name})
34
33
  return currencies
35
34
 
36
35
  def get_currencies(self) -> list[str]:
@@ -40,8 +39,8 @@ class WFPExchangeRates:
40
39
  List of currency codes in WFP API
41
40
  """
42
41
  currencies = []
43
- for currency in self.wfp_api.get_items("Currency/List"):
44
- currencies.append(currency["name"])
42
+ for currency in self.wfp_api.get_currencies():
43
+ currencies.append(currency.name)
45
44
  return currencies
46
45
 
47
46
  def get_currency_historic_rates(self, currency: str) -> dict[int, float]:
@@ -53,17 +52,19 @@ class WFPExchangeRates:
53
52
  Returns:
54
53
  Mapping from timestamp to rate
55
54
  """
56
- quotes = self.wfp_api.get_items(
57
- "Currency/UsdIndirectQuotation",
58
- parameters={"currencyName": currency},
59
- )
55
+ quotes = self.wfp_api.get_currency_usd_indirect_quotations(currency)
60
56
  historic_rates = {}
61
57
  for quote in reversed(quotes):
62
- if not quote["isOfficial"]:
58
+ if not quote.is_official:
63
59
  continue
64
- date = parse_date(quote["date"])
60
+ date = quote.var_date
61
+ if date.tzinfo is None:
62
+ # data_bridges_client parses WFP's timezone-less date strings
63
+ # into naive datetimes; assume UTC to match previous
64
+ # parse_date-based behaviour
65
+ date = date.replace(tzinfo=timezone.utc)
65
66
  timestamp = get_int_timestamp(date)
66
- historic_rates[timestamp] = quote["value"]
67
+ historic_rates[timestamp] = quote.value
67
68
  return historic_rates
68
69
 
69
70
  def get_historic_rates(self, currencies: Sequence[str]) -> dict[str, dict]:
@@ -0,0 +1,28 @@
1
+ {
2
+ "items": [
3
+ {
4
+ "marketId": 179,
5
+ "marketName": "Kabul",
6
+ "marketLocalName": "Kabul",
7
+ "marketLatitude": 34.526011,
8
+ "marketLongitude": 69.176033,
9
+ "admin1Name": "Kabul",
10
+ "admin1Code": 3417,
11
+ "admin2Name": "Kabul",
12
+ "admin2Code": 33475
13
+ },
14
+ {
15
+ "marketId": 180,
16
+ "marketName": "Herat",
17
+ "marketLocalName": "Herat",
18
+ "marketLatitude": 34.348418,
19
+ "marketLongitude": 62.199747,
20
+ "admin1Name": "Herat",
21
+ "admin1Code": 3424,
22
+ "admin2Name": "Herat",
23
+ "admin2Code": 33520
24
+ }
25
+ ],
26
+ "page": 1,
27
+ "totalItems": 2
28
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "items": [],
3
+ "page": 2,
4
+ "totalItems": 2
5
+ }
@@ -126,7 +126,7 @@ class TestCurrency:
126
126
  )
127
127
  rate2gbp = Currency.get_current_rate("gbp")
128
128
  assert rate2gbp != 1
129
- assert abs(rate1gbp - rate2gbp) / rate1gbp < 0.008
129
+ assert abs(rate1gbp - rate2gbp) / rate1gbp < 0.01
130
130
  Currency.setup(
131
131
  retriever=retrievers[1],
132
132
  primary_rates_url="fail",
@@ -27,7 +27,7 @@ class TestWFPExchangeRates:
27
27
  )
28
28
  currency = "afn"
29
29
  date = parse_date("2020-02-20")
30
- wfp_api = WFPAPI(downloader, retriever)
30
+ wfp_api = WFPAPI(retriever)
31
31
  wfp_api.update_retry_params(attempts=5, wait=5)
32
32
  wfp_fx = WFPExchangeRates(wfp_api)
33
33
  retry_params = wfp_fx.wfp_api.get_retry_params()
@@ -62,3 +62,8 @@ class TestWFPExchangeRates:
62
62
  Currency.get_historic_rate(currency, date, ignore_timeinfo=False)
63
63
  == 77.01
64
64
  )
65
+
66
+ markets = wfp_api.get_markets(countryiso3="AFG")
67
+ assert len(markets) == 2
68
+ assert markets[0].market_name == "Kabul"
69
+ assert markets[1].market_name == "Herat"