hdx-python-country 4.2.8__tar.gz → 4.2.9__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 (55) hide show
  1. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/PKG-INFO +3 -3
  2. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/pyproject.toml +3 -3
  3. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/_version.py +2 -2
  4. hdx_python_country-4.2.9/src/hdx/location/wfp_api.py +481 -0
  5. hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_List.csv +129 -0
  6. hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_UsdIndirectQuotation_AFN.csv +8695 -0
  7. hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_UsdIndirectQuotation_SYP.csv +4 -0
  8. hdx_python_country-4.2.9/tests/fixtures/wfp/MarketPrices_PriceMonthly_BTN.csv +5 -0
  9. hdx_python_country-4.2.9/tests/fixtures/wfp/Markets_List_AFG.csv +3 -0
  10. hdx_python_country-4.2.9/tests/hdx/location/test_wfp_api.py +230 -0
  11. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/uv.lock +197 -177
  12. hdx_python_country-4.2.8/src/hdx/location/wfp_api.py +0 -323
  13. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_List_1.json +0 -1
  14. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_List_2.json +0 -1
  15. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_1.json +0 -1
  16. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_10.json +0 -1
  17. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_2.json +0 -1
  18. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_3.json +0 -1
  19. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_4.json +0 -1
  20. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_5.json +0 -1
  21. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_6.json +0 -1
  22. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_7.json +0 -1
  23. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_8.json +0 -1
  24. hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_9.json +0 -1
  25. hdx_python_country-4.2.8/tests/fixtures/wfp/Markets_List_AFG_1.json +0 -28
  26. hdx_python_country-4.2.8/tests/fixtures/wfp/Markets_List_AFG_2.json +0 -5
  27. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.github/workflows/publish.yaml +0 -0
  28. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.github/workflows/run-python-tests.yaml +0 -0
  29. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.gitignore +0 -0
  30. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.pre-commit-config.yaml +0 -0
  31. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/LICENSE +0 -0
  32. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/README.md +0 -0
  33. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/.readthedocs.yaml +0 -0
  34. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/index.md +0 -0
  35. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/mkdocs.yaml +0 -0
  36. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy.csv +0 -0
  37. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/__init__.py +0 -0
  38. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/adminlevel.py +0 -0
  39. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/country.py +0 -0
  40. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/currency.py +0 -0
  41. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/int_timestamp.py +0 -0
  42. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/wfp_exchangerates.py +0 -0
  43. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/adminlevel.yaml +0 -0
  44. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/adminlevelparent.yaml +0 -0
  45. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
  46. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
  47. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/secondary_historic_rates.csv +0 -0
  48. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/secondary_rates.json +0 -0
  49. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
  50. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/__init__.py +0 -0
  51. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/conftest.py +0 -0
  52. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_adminlevel.py +0 -0
  53. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_country.py +0 -0
  54. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_currency.py +0 -0
  55. {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_wfp_exchangerates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hdx-python-country
3
- Version: 4.2.8
3
+ Version: 4.2.9
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
@@ -26,12 +26,12 @@ Classifier: Programming Language :: Python :: 3.13
26
26
  Classifier: Programming Language :: Python :: 3.14
27
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
28
  Requires-Python: >=3.11
29
- Requires-Dist: hdx-python-utilities<5,>=4.2.6
29
+ Requires-Dist: hdx-python-utilities<5,>=4.2.7
30
30
  Requires-Dist: tenacity
31
31
  Provides-Extra: docs
32
32
  Requires-Dist: mkapi; extra == 'docs'
33
33
  Provides-Extra: wfp
34
- Requires-Dist: data-bridges-client<10,>=9.0.0; extra == 'wfp'
34
+ Requires-Dist: data-bridges-client<10,>=9.0.3; extra == 'wfp'
35
35
  Description-Content-Type: text/markdown
36
36
 
37
37
  [![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)
@@ -35,13 +35,13 @@ dynamic = ["version"]
35
35
  requires-python = ">=3.11"
36
36
 
37
37
  dependencies = [
38
- "hdx-python-utilities>=4.2.6,<5",
38
+ "hdx-python-utilities>=4.2.7,<5",
39
39
  "tenacity",
40
40
  ]
41
41
 
42
42
  [project.optional-dependencies]
43
43
  docs = ["mkapi"]
44
- wfp = ["data-bridges-client>=9.0.0,<10"]
44
+ wfp = ["data-bridges-client>=9.0.3,<10"]
45
45
 
46
46
  [dependency-groups]
47
47
  dev = [
@@ -49,7 +49,7 @@ dev = [
49
49
  "pytest-cov",
50
50
  "pre-commit",
51
51
  "ruff==0.17.3",
52
- "data-bridges-client>=9.0.0,<10",
52
+ "data-bridges-client>=9.0.3,<10",
53
53
  ]
54
54
 
55
55
  [project.urls]
@@ -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.2.8'
22
- __version_tuple__ = version_tuple = (4, 2, 8)
21
+ __version__ = version = '4.2.9'
22
+ __version_tuple__ = version_tuple = (4, 2, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,481 @@
1
+ import csv
2
+ import io
3
+ import logging
4
+ import types
5
+ from collections.abc import Callable
6
+ from datetime import date, datetime, timedelta
7
+ from functools import cache, partial
8
+ from os import getenv
9
+ from typing import Annotated, Any, ClassVar, Union, get_args, get_origin
10
+
11
+ from data_bridges_client import (
12
+ ApiClient,
13
+ ApiException,
14
+ CommoditiesApi,
15
+ Configuration,
16
+ CurrencyApi,
17
+ MarketPricesApi,
18
+ MarketsApi,
19
+ )
20
+ from data_bridges_client.exceptions import ServiceException
21
+ from data_bridges_client.models.commodity_dto import CommodityDTO
22
+ from data_bridges_client.models.currency_dto import CurrencyDTO
23
+ from data_bridges_client.models.market_dto import MarketDTO
24
+ from data_bridges_client.models.usd_indirect_quotation import UsdIndirectQuotation
25
+ from data_bridges_client.models.view_extended_monthly_aggregated_price import (
26
+ ViewExtendedMonthlyAggregatedPrice,
27
+ )
28
+ from data_bridges_client.token import WfpApiToken
29
+ from hdx.utilities.retriever import Retrieve
30
+ from tenacity import (
31
+ Retrying,
32
+ after_log,
33
+ retry_if_exception_type,
34
+ stop_after_attempt,
35
+ wait_fixed,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ _CSV_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
41
+ _PRICES_START_DATE = date(1990, 1, 1)
42
+
43
+
44
+ def _default_end_date() -> date:
45
+ # WFP has forecast prices a year or more ahead
46
+ return date(date.today().year + 5, 12, 31)
47
+
48
+
49
+ def _to_date(value: date | None, default: date) -> date:
50
+ if value is None:
51
+ return default
52
+ if isinstance(value, datetime):
53
+ return value.date()
54
+ return value
55
+
56
+
57
+ def _csv_datetime(value: str) -> datetime:
58
+ return datetime.strptime(value, _CSV_DATE_FORMAT)
59
+
60
+
61
+ def _csv_bool(value: str) -> bool:
62
+ return value == "True"
63
+
64
+
65
+ def _csv_none(value: str) -> None:
66
+ return None
67
+
68
+
69
+ def _field_types(annotation: Any) -> set:
70
+ origin = get_origin(annotation)
71
+ if origin is Annotated:
72
+ return _field_types(get_args(annotation)[0])
73
+ if origin in (Union, types.UnionType):
74
+ return set().union(
75
+ *(_field_types(x) for x in get_args(annotation) if x is not type(None))
76
+ )
77
+ return {origin or annotation}
78
+
79
+
80
+ @cache
81
+ def _csv_converters(model_cls: type) -> dict[str, Callable]:
82
+ """Map the JSON key of each non-string field of a data_bridges_client
83
+ model to a function that converts its CSV string value, based on the
84
+ field's type. List fields (CommodityDTO's qualities and processing) are
85
+ not in WFP's CSV output, so are always None.
86
+
87
+ Args:
88
+ model_cls: data_bridges_client model class
89
+
90
+ Returns:
91
+ Map from JSON key to converter function
92
+ """
93
+ converters = {}
94
+ for name, field in model_cls.model_fields.items():
95
+ if name == "additional_properties":
96
+ continue
97
+ field_types = _field_types(field.annotation)
98
+ if list in field_types:
99
+ converter = _csv_none
100
+ elif bool in field_types:
101
+ converter = _csv_bool
102
+ elif float in field_types:
103
+ converter = float
104
+ elif int in field_types:
105
+ converter = int
106
+ elif datetime in field_types:
107
+ converter = _csv_datetime
108
+ else:
109
+ continue
110
+ converters[field.alias or name] = converter
111
+ return converters
112
+
113
+
114
+ def _parse_csv(text: str, model_cls: type) -> list:
115
+ """Parse WFP's CSV output into data_bridges_client model objects. CSV
116
+ headers are the JSON keys with the first letter capitalised and every
117
+ value is a string. Blank strings are kept as "" (as JSON returns them)
118
+ but blank values of non-string fields become None.
119
+
120
+ Args:
121
+ text: CSV text
122
+ model_cls: data_bridges_client model class of each row
123
+
124
+ Returns:
125
+ List of model_cls objects
126
+ """
127
+ converters = _csv_converters(model_cls)
128
+ reader = csv.reader(io.StringIO(text))
129
+ keys = [header[0].lower() + header[1:] for header in next(reader, [])]
130
+ columns = [(key, converters.get(key)) for key in keys]
131
+ items = []
132
+ for row in reader:
133
+ if not row:
134
+ continue
135
+ obj = {}
136
+ for (key, converter), value in zip(columns, row):
137
+ if converter:
138
+ value = converter(value) if value else None
139
+ obj[key] = value
140
+ items.append(model_cls.from_dict(obj))
141
+ return items
142
+
143
+
144
+ class WFPAPI:
145
+ """Light wrapper around WFP's data-bridges-client library. All data is
146
+ requested as CSV, which returns every row in one response, rather than
147
+ JSON, whose paging returns some rows twice and leaves others out. It
148
+ needs a retriever used only to decide whether to load from disk or hit
149
+ the network and whether to persist the network result (as the CSV
150
+ returned by WFP). Token refreshing is delegated to data_bridges_client's
151
+ own WfpApiToken.
152
+
153
+ Args:
154
+ retriever: Retrieve object for interacting with WFP API
155
+ """
156
+
157
+ default_retry_params: ClassVar[dict[str, Any]] = {
158
+ "retry": retry_if_exception_type(ApiException),
159
+ "after": after_log(logger, logging.INFO),
160
+ }
161
+
162
+ def __init__(
163
+ self,
164
+ retriever: Retrieve,
165
+ wfp_key: str | None = None,
166
+ wfp_secret: str | None = None,
167
+ ):
168
+ self.retriever = retriever
169
+ self.retry_params = {"attempts": 1, "wait": 1}
170
+ if wfp_key:
171
+ self._wfp_key = wfp_key
172
+ else:
173
+ self._wfp_key = getenv("WFP_KEY")
174
+ if wfp_secret:
175
+ self._wfp_secret = wfp_secret
176
+ else:
177
+ self._wfp_secret = getenv("WFP_SECRET")
178
+ self.token = WfpApiToken(api_key=self._wfp_key, api_secret=self._wfp_secret)
179
+ self.configuration = Configuration()
180
+ self.api_client = ApiClient(self.configuration)
181
+ self.currency_api = CurrencyApi(self.api_client)
182
+ self.markets_api = MarketsApi(self.api_client)
183
+ self.commodities_api = CommoditiesApi(self.api_client)
184
+ self.market_prices_api = MarketPricesApi(self.api_client)
185
+
186
+ def get_retry_params(self) -> dict:
187
+ return self.retry_params
188
+
189
+ def update_retry_params(self, attempts: int, wait: int) -> dict:
190
+ self.retry_params["attempts"] = attempts
191
+ self.retry_params["wait"] = wait
192
+ return self.retry_params
193
+
194
+ def refresh_token(self) -> None:
195
+ self.configuration.access_token = self.token.refresh()
196
+
197
+ def _with_retry(self, api_method: Callable, **kwargs: Any) -> Any:
198
+ retryer = Retrying(
199
+ retry=self.default_retry_params["retry"],
200
+ after=self.default_retry_params["after"],
201
+ stop=stop_after_attempt(self.retry_params["attempts"]),
202
+ wait=wait_fixed(self.retry_params["wait"]),
203
+ )
204
+ for attempt in retryer:
205
+ with attempt:
206
+ try:
207
+ return api_method(**kwargs)
208
+ except ApiException as err:
209
+ if err.status not in (104, 401, 403):
210
+ raise
211
+ self.refresh_token()
212
+ return api_method(**kwargs)
213
+
214
+ @staticmethod
215
+ def _get_csv(api_method: Callable, **kwargs: Any) -> str:
216
+ """Call a data_bridges_client API method's _without_preload_content
217
+ variant with format CSV, raising ApiException (as the methods that
218
+ parse JSON do) if the response is not OK.
219
+
220
+ Args:
221
+ api_method: Bound _without_preload_content method
222
+ **kwargs: Parameters to pass to api_method
223
+
224
+ Returns:
225
+ CSV text
226
+ """
227
+ response = api_method(format="CSV", **kwargs)
228
+ text = response.data.decode("utf-8-sig")
229
+ if response.status != 200:
230
+ ApiException.from_response(http_resp=response, body=text, data=None)
231
+ return text
232
+
233
+ def _call(
234
+ self, csv_method: Callable, filename: str, log: str, **kwargs: Any
235
+ ) -> str | None:
236
+ """Get CSV text from csv_method, loading from or saving to disk as
237
+ Retrieve does.
238
+
239
+ Args:
240
+ csv_method: Function returning CSV text
241
+ filename: Filename of saved file
242
+ log: Text to use in log string to describe the call
243
+ **kwargs: Parameters to pass to csv_method
244
+
245
+ Returns:
246
+ CSV text, or None if there is no saved data
247
+ """
248
+ filename, _ = self.retriever.get_filename("", filename)
249
+ saved_path = self.retriever.saved_dir / filename
250
+ if self.retriever.use_saved:
251
+ logger.info(f"Using saved {log} in {saved_path}")
252
+ try:
253
+ with open(saved_path, encoding="utf-8", newline="") as f:
254
+ return f.read()
255
+ except FileNotFoundError:
256
+ return None
257
+ text = self._with_retry(csv_method, **kwargs)
258
+ if self.retriever.save:
259
+ logger.info(f"Saving {log} in {saved_path}")
260
+ with open(saved_path, "w", encoding="utf-8", newline="") as f:
261
+ f.write(text)
262
+ return text
263
+
264
+ @staticmethod
265
+ def _countryiso3s(countryiso3: str | None) -> list:
266
+ if countryiso3 == "PSE": # hack as PSE is treated by WFP as 2 areas
267
+ return ["PSW", "PSG"]
268
+ return [countryiso3]
269
+
270
+ def _get_all(
271
+ self,
272
+ api_method: Callable,
273
+ model_cls: type,
274
+ base_filename: str,
275
+ countryiso3: str | None,
276
+ country_param: str,
277
+ extra_params: dict,
278
+ get_csv: Callable | None = None,
279
+ ) -> list:
280
+ csv_method = partial(get_csv or self._get_csv, api_method)
281
+ all_items = []
282
+ for country in self._countryiso3s(countryiso3):
283
+ kwargs = dict(extra_params)
284
+ if country is None:
285
+ filename = f"{base_filename}.csv"
286
+ log = base_filename
287
+ else:
288
+ kwargs[country_param] = country
289
+ filename = f"{base_filename}_{country}.csv"
290
+ log = f"{base_filename} for {country}"
291
+ text = self._call(csv_method, filename, log, **kwargs)
292
+ if text:
293
+ all_items.extend(_parse_csv(text, model_cls))
294
+ return all_items
295
+
296
+ def get_currencies(
297
+ self, countryiso3: str | None = None, currency: str | None = None
298
+ ) -> list[CurrencyDTO]:
299
+ """Get list of currencies from the WFP API.
300
+
301
+ Args:
302
+ countryiso3: Country for which to obtain data. Defaults to all countries.
303
+ currency: Currency 3-letter code to filter by. Defaults to all currencies.
304
+
305
+ Returns:
306
+ List of currencies from the WFP API
307
+ """
308
+ if currency:
309
+ base_filename = f"Currency_List_{currency.upper()}"
310
+ extra_params = {"currency_name": currency}
311
+ else:
312
+ base_filename = "Currency_List"
313
+ extra_params = {}
314
+ return self._get_all(
315
+ self.currency_api.currency_list_get_without_preload_content,
316
+ CurrencyDTO,
317
+ base_filename,
318
+ countryiso3,
319
+ "country_code",
320
+ extra_params,
321
+ )
322
+
323
+ def get_currency_usd_indirect_quotations(
324
+ self, currency: str, countryiso3: str | None = None
325
+ ) -> list[UsdIndirectQuotation]:
326
+ """Get USD indirect quotations for a currency from the WFP API.
327
+
328
+ Args:
329
+ currency: Currency 3-letter code
330
+ countryiso3: Country for which to obtain data. Defaults to all countries.
331
+
332
+ Returns:
333
+ List of USD indirect quotations from the WFP API
334
+ """
335
+ return self._get_all(
336
+ self.currency_api.currency_usd_indirect_quotation_get_without_preload_content,
337
+ UsdIndirectQuotation,
338
+ f"Currency_UsdIndirectQuotation_{currency.upper()}",
339
+ countryiso3,
340
+ # this endpoint uses country_iso3, unlike every other one here which
341
+ # uses country_code - don't "fix" this to country_code
342
+ "country_iso3",
343
+ {"currency_name": currency},
344
+ )
345
+
346
+ def get_markets(self, countryiso3: str | None = None) -> list[MarketDTO]:
347
+ """Get list of markets from the WFP API.
348
+
349
+ Args:
350
+ countryiso3: Country for which to obtain data. Defaults to all countries.
351
+
352
+ Returns:
353
+ List of markets from the WFP API
354
+ """
355
+ return self._get_all(
356
+ self.markets_api.markets_list_get_without_preload_content,
357
+ MarketDTO,
358
+ "Markets_List",
359
+ countryiso3,
360
+ "country_code",
361
+ {},
362
+ )
363
+
364
+ def get_commodities(self, countryiso3: str | None = None) -> list[CommodityDTO]:
365
+ """Get list of commodities from the WFP API. WFP's CSV output does not
366
+ include qualities and processing, so they are None.
367
+
368
+ Args:
369
+ countryiso3: Country for which to obtain data. Defaults to all countries.
370
+
371
+ Returns:
372
+ List of commodities from the WFP API
373
+ """
374
+ return self._get_all(
375
+ self.commodities_api.commodities_list_get_without_preload_content,
376
+ CommodityDTO,
377
+ "Commodities_List",
378
+ countryiso3,
379
+ "country_code",
380
+ {},
381
+ )
382
+
383
+ def get_commodity_categories(
384
+ self, countryiso3: str | None = None
385
+ ) -> list[CommodityDTO]:
386
+ """Get list of commodity categories from the WFP API.
387
+
388
+ Args:
389
+ countryiso3: Country for which to obtain data. Defaults to all countries.
390
+
391
+ Returns:
392
+ List of commodity categories from the WFP API
393
+ """
394
+ return self._get_all(
395
+ self.commodities_api.commodities_categories_list_get_without_preload_content,
396
+ CommodityDTO,
397
+ "Commodities_Categories_List",
398
+ countryiso3,
399
+ "country_code",
400
+ {},
401
+ )
402
+
403
+ def _get_prices_csv_range(
404
+ self, api_method: Callable, start_date: date, end_date: date, **kwargs: Any
405
+ ) -> list[str]:
406
+ """Get monthly aggregated market prices between two dates (inclusive)
407
+ as CSV. If the API gateway times out (504), the date range is split in
408
+ two and each half is requested.
409
+
410
+ Args:
411
+ api_method: Bound market_prices_price_monthly_get_without_preload_content
412
+ start_date: Start date
413
+ end_date: End date
414
+ **kwargs: Parameters to pass to api_method
415
+
416
+ Returns:
417
+ List of CSV texts, one per date range, in date order
418
+ """
419
+ try:
420
+ return [
421
+ self._get_csv(
422
+ api_method, start_date=start_date, end_date=end_date, **kwargs
423
+ )
424
+ ]
425
+ except ServiceException as err:
426
+ if err.status != 504 or start_date >= end_date:
427
+ raise
428
+ middle = start_date + (end_date - start_date) // 2
429
+ logger.info(
430
+ f"Timeout getting prices from {start_date} to {end_date}, splitting at {middle}"
431
+ )
432
+ return self._get_prices_csv_range(
433
+ api_method, start_date, middle, **kwargs
434
+ ) + self._get_prices_csv_range(
435
+ api_method, middle + timedelta(days=1), end_date, **kwargs
436
+ )
437
+
438
+ def _get_prices_csv(
439
+ self,
440
+ api_method: Callable,
441
+ start_date: date | None = None,
442
+ end_date: date | None = None,
443
+ **kwargs: Any,
444
+ ) -> str:
445
+ first, *rest = self._get_prices_csv_range(
446
+ api_method,
447
+ _to_date(start_date, _PRICES_START_DATE),
448
+ _to_date(end_date, _default_end_date()),
449
+ **kwargs,
450
+ )
451
+ if not rest:
452
+ return first
453
+ # keep only the first range's header row
454
+ parts = [first.rstrip("\r\n")]
455
+ parts.extend(text.partition("\n")[2].rstrip("\r\n") for text in rest)
456
+ return "\r\n".join(part for part in parts if part) + "\r\n"
457
+
458
+ def get_market_prices_monthly(
459
+ self, countryiso3: str | None = None, **kwargs: Any
460
+ ) -> list[ViewExtendedMonthlyAggregatedPrice]:
461
+ """Get monthly aggregated market prices from the WFP API.
462
+
463
+ Args:
464
+ countryiso3: Country for which to obtain data. Defaults to all countries.
465
+ **kwargs: Additional filters accepted by
466
+ data_bridges_client's MarketPricesApi.market_prices_price_monthly_get,
467
+ e.g. market_id, commodity_id, price_type_name, currency_id,
468
+ price_flag, start_date, end_date, latest_value_only
469
+
470
+ Returns:
471
+ List of monthly aggregated market prices from the WFP API
472
+ """
473
+ return self._get_all(
474
+ self.market_prices_api.market_prices_price_monthly_get_without_preload_content,
475
+ ViewExtendedMonthlyAggregatedPrice,
476
+ "MarketPrices_PriceMonthly",
477
+ countryiso3,
478
+ "country_code",
479
+ kwargs,
480
+ self._get_prices_csv,
481
+ )
@@ -0,0 +1,129 @@
1
+ "Id","Name","ExtendedName","CreateDate","UpdateDate"
2
+ "131","AED","United Arab Emirates Dirham","8/2/2021 6:24:29 PM","10/5/2023 4:05:48 PM"
3
+ "87","AFN","Afghan Afghani","4/22/2020 10:02:26 PM","10/5/2023 4:06:06 PM"
4
+ "130","ALL","Albanian Lek","8/2/2021 6:22:53 PM","10/5/2023 4:06:11 PM"
5
+ "37","AMD","Armenian Dram","4/22/2020 10:02:26 PM","10/5/2023 4:06:16 PM"
6
+ "96","AOA","Angolan Kwanza","4/22/2020 10:02:26 PM","10/5/2023 3:07:06 PM"
7
+ "106","ARS","Argentine Peso","4/22/2020 10:02:26 PM","10/5/2023 4:06:22 PM"
8
+ "133","AUD","Australian Dollar","8/2/2021 6:28:54 PM","10/5/2023 4:06:28 PM"
9
+ "36","AZN","Azerbaijani Manat","4/22/2020 10:02:26 PM","10/5/2023 4:06:33 PM"
10
+ "25","BDT","Bangladeshi Taka","4/22/2020 10:02:26 PM","10/5/2023 4:06:39 PM"
11
+ "132","BGN","Bulgarian Lev","8/2/2021 6:27:55 PM","10/5/2023 4:06:45 PM"
12
+ "138","BHD","Bahraini Dinar","10/4/2021 6:28:31 PM","10/5/2023 4:06:52 PM"
13
+ "32","BIF","Burundian Franc","4/22/2020 10:02:26 PM","10/5/2023 4:05:30 PM"
14
+ "46","BOB","Boliviano","4/22/2020 10:02:26 PM","10/5/2023 4:07:16 PM"
15
+ "159","BRL","Brazilian Real","1/19/2024 10:08:31 AM",""
16
+ "144","BSD","Bahamian Dollar","11/16/2021 1:56:25 PM","10/5/2023 4:09:05 PM"
17
+ "76","BTN","Bhutanese Ngultrum","4/22/2020 10:02:26 PM","10/5/2023 4:07:41 PM"
18
+ "148","BWP","Botswana Pula","9/9/2022 8:56:59 AM",""
19
+ "107","BYR","Belarusian Ruble","4/22/2020 10:02:26 PM","10/5/2023 4:08:22 PM"
20
+ "27","CDF","Congolese Franc","4/22/2020 10:02:26 PM","10/5/2023 4:08:43 PM"
21
+ "108","CLP","Chilean Peso","4/22/2020 10:02:26 PM","10/5/2023 4:08:58 PM"
22
+ "109","CNY","Renminbi","4/22/2020 10:02:26 PM","10/5/2023 4:09:23 PM"
23
+ "67","COP","Colombian Peso","4/22/2020 10:02:26 PM","10/5/2023 4:09:51 PM"
24
+ "92","CRC","Costa Rican Colon","4/22/2020 10:02:26 PM","10/5/2023 4:10:04 PM"
25
+ "134","CUC","Cuban Convertible Pesos","8/2/2021 7:18:01 PM","10/5/2023 4:10:15 PM"
26
+ "127","CUP","Cuban Peso","7/23/2021 10:23:48 AM","7/23/2021 10:24:03 AM"
27
+ "57","CVE","Cape Verdean Escudo","4/22/2020 10:02:26 PM","10/5/2023 4:10:33 PM"
28
+ "59","DJF","Djiboutian Franc","4/22/2020 10:02:26 PM","10/5/2023 4:10:45 PM"
29
+ "85","DOP","Dominican Peso","4/22/2020 10:02:26 PM","10/5/2023 4:11:12 PM"
30
+ "91","DZD","Algerian Dinar","4/22/2020 10:02:26 PM","10/5/2023 4:11:26 PM"
31
+ "38","EGP","Egyptian Pound","4/22/2020 10:02:26 PM","10/5/2023 4:11:39 PM"
32
+ "110","ERN","Eritrean Nakfa","4/22/2020 10:02:26 PM","10/5/2023 4:11:53 PM"
33
+ "47","ETB","Ethiopian Birr","4/22/2020 10:02:26 PM","10/5/2023 4:12:07 PM"
34
+ "111","EUR","Euro","4/22/2020 10:02:26 PM","10/5/2023 4:12:17 PM"
35
+ "146","FJD","Fijian dollar","11/16/2021 1:59:56 PM",""
36
+ "50","GEL","Georgian Lari","4/22/2020 10:02:26 PM","10/5/2023 4:12:32 PM"
37
+ "35","GHS","Ghanaian Cedi","4/22/2020 10:02:26 PM","10/5/2023 4:12:51 PM"
38
+ "42","GMD","Gambian Dalasi","4/22/2020 10:02:26 PM","10/5/2023 4:13:02 PM"
39
+ "54","GNF","Guinean Franc","4/22/2020 10:02:26 PM","10/5/2023 4:13:27 PM"
40
+ "82","GTQ","Guatemalan Quetzal","4/22/2020 10:02:26 PM","10/5/2023 4:13:42 PM"
41
+ "112","HNL","Honduran Lempira","4/22/2020 10:02:26 PM","10/5/2023 4:14:02 PM"
42
+ "24","HTG","Haitian Gourde","4/22/2020 10:02:26 PM","10/5/2023 4:16:54 PM"
43
+ "34","IDR","Indonesian Rupiah","4/22/2020 10:02:26 PM","10/5/2023 4:51:09 PM"
44
+ "73","ILS","New Israeli Shekel","4/22/2020 10:02:26 PM","9/20/2021 5:55:14 PM"
45
+ "68","INR","Indian Rupee","4/22/2020 10:02:26 PM","10/5/2023 4:51:24 PM"
46
+ "84","IQD","Iraqi Dinar","4/22/2020 10:02:26 PM","10/5/2023 4:51:42 PM"
47
+ "89","IRR","Iranian Rial","4/22/2020 10:02:26 PM","10/5/2023 4:52:01 PM"
48
+ "139","JMD","Jamaican Dollar","11/16/2021 1:50:01 PM","11/16/2021 1:51:47 PM"
49
+ "58","JOD","Jordanian Dinar","4/22/2020 10:02:26 PM","10/5/2023 4:52:13 PM"
50
+ "113","JPY","Japanese Yen","4/22/2020 10:02:26 PM","10/5/2023 4:52:25 PM"
51
+ "30","KES","Kenyan Shilling","4/22/2020 10:02:26 PM","10/5/2023 5:20:23 PM"
52
+ "61","KGS","Kyrgyzstani Som","4/22/2020 10:02:26 PM","10/5/2023 5:20:37 PM"
53
+ "60","KHR","Cambodian Riel","4/22/2020 10:02:26 PM","10/5/2023 5:20:54 PM"
54
+ "149","KMF","Comorian Franc","9/9/2022 8:58:07 AM",""
55
+ "141","KPW","North Korean Won","11/16/2021 1:53:56 PM",""
56
+ "145","KYD","Cayman Islands Dollar","11/16/2021 1:57:58 PM","10/5/2023 5:21:02 PM"
57
+ "99","KZT","Kazakhstani Tenge","4/22/2020 10:02:26 PM","10/5/2023 5:21:22 PM"
58
+ "63","LAK","Lao Kip","4/22/2020 10:02:26 PM","10/5/2023 5:21:33 PM"
59
+ "90","LBP","Lebanese Pound","4/22/2020 10:02:26 PM","10/5/2023 5:21:44 PM"
60
+ "55","LKR","Sri Lankan Rupee","4/22/2020 10:02:26 PM","10/5/2023 5:21:55 PM"
61
+ "40","LRD","Liberian Dollar","4/22/2020 10:02:26 PM","10/5/2023 5:22:08 PM"
62
+ "56","LSL","Lesotho Loti","4/22/2020 10:02:26 PM","10/5/2023 5:22:20 PM"
63
+ "97","LYD","Libyan Dinar","4/22/2020 10:02:26 PM","10/5/2023 5:22:34 PM"
64
+ "101","MAD","Moroccan Dirham","4/22/2020 10:02:26 PM","10/5/2023 5:22:45 PM"
65
+ "100","MDL","Moldovan Leu","4/22/2020 10:02:26 PM","10/5/2023 5:22:56 PM"
66
+ "65","MGA","Malagasy Ariary","4/22/2020 10:02:26 PM","10/5/2023 5:23:11 PM"
67
+ "74","MMK","Myanmar Kyat","4/22/2020 10:02:26 PM","10/5/2023 5:23:21 PM"
68
+ "114","MNT","Mongolian Tögrög","4/22/2020 10:02:26 PM","10/5/2023 5:23:37 PM"
69
+ "64","MRO","Mauritanian Ouguiya","4/22/2020 10:02:26 PM","10/5/2023 5:23:52 PM"
70
+ "147","MRU","Ouguiya","5/18/2022 2:54:28 PM","5/20/2022 2:29:42 PM"
71
+ "66","MWK","Malawian Kwacha","4/22/2020 10:02:26 PM","10/5/2023 5:24:11 PM"
72
+ "115","MXN","Mexican Peso","4/22/2020 10:02:26 PM","10/5/2023 5:24:27 PM"
73
+ "51","MZN","Mozambican Metical","4/22/2020 10:02:26 PM","10/6/2023 1:09:29 PM"
74
+ "116","NAD","Namibian Dollar","4/22/2020 10:02:26 PM","10/6/2023 1:09:36 PM"
75
+ "95","NGN","Nigerian Naira","4/22/2020 10:02:26 PM","10/6/2023 1:09:47 PM"
76
+ "86","NIO","Nicaraguan Córdoba","4/22/2020 10:02:26 PM","10/6/2023 1:10:00 PM"
77
+ "152","NOK","Norwegian krone","10/6/2023 1:10:24 PM",""
78
+ "62","NPR","Nepalese Rupee","4/22/2020 10:02:26 PM","10/6/2023 1:10:37 PM"
79
+ "153","NZD","New Zealand Dollar","10/6/2023 1:11:01 PM",""
80
+ "154","OMR","Omani Rial","10/6/2023 1:11:20 PM",""
81
+ "117","PAB","Panamanian Balboa","4/22/2020 10:02:26 PM","10/6/2023 1:11:45 PM"
82
+ "33","PEN","Peruvian Sol","4/22/2020 10:02:26 PM","10/6/2023 1:11:54 PM"
83
+ "140","PGK","Papua New Guinean Kina","11/16/2021 1:51:13 PM","11/16/2021 1:51:20 PM"
84
+ "29","PHP","Philippine Peso","4/22/2020 10:02:26 PM","10/6/2023 1:12:31 PM"
85
+ "45","PKR","Pakistani Rupee","4/22/2020 10:02:26 PM","10/6/2023 1:12:45 PM"
86
+ "155","PLN","Polish złoty","10/6/2023 1:12:59 PM",""
87
+ "161","PRB","Transnistrian ruble","4/25/2025 3:32:06 PM","4/28/2025 12:12:16 PM"
88
+ "118","PYG","Paraguayan Guaraní","4/22/2020 10:02:26 PM","10/6/2023 1:13:08 PM"
89
+ "156","RON","Romanian Leu","10/6/2023 1:14:01 PM",""
90
+ "102","RUB","Russian Ruble","4/22/2020 10:02:26 PM","10/6/2023 1:13:29 PM"
91
+ "77","RWF","Rwandan Franc","4/22/2020 10:02:26 PM","10/6/2023 1:14:15 PM"
92
+ "103","SAR","Saudi Riyal","4/22/2020 10:02:26 PM","10/6/2023 1:14:24 PM"
93
+ "150","SBD","Solomon Islands Dollar","9/9/2022 10:23:48 AM","10/6/2023 1:14:30 PM"
94
+ "157","SCR","Seychelles Rupee","10/6/2023 1:14:55 PM",""
95
+ "75","SDG","Sudanese Pound","4/22/2020 10:02:26 PM","10/6/2023 1:15:03 PM"
96
+ "158","SEK","Swedish Krona ","10/6/2023 1:15:21 PM",""
97
+ "53","SLL","Sierra Leonean Leone ","4/22/2020 10:02:26 PM","5/13/2026 12:17:23 PM"
98
+ "81","SLS","Somaliland Shilling","4/22/2020 10:02:26 PM","5/13/2020 7:08:07 PM"
99
+ "79","SOS","Somali Shilling","4/22/2020 10:02:26 PM","10/6/2023 1:15:54 PM"
100
+ "137","SRD","Surinamese dollar","9/3/2021 4:47:19 PM",""
101
+ "83","SSP","South Sudanese Pound","4/22/2020 10:02:26 PM","10/6/2023 1:16:09 PM"
102
+ "142","STN","São Tomé and Príncipe Dobra","11/16/2021 1:54:49 PM","2/4/2022 6:49:22 PM"
103
+ "78","SYP","Syrian Pound","4/22/2020 10:02:26 PM","10/6/2023 1:16:20 PM"
104
+ "23","SZL","Swazi Lilangeni","4/22/2020 10:02:26 PM","10/6/2023 1:16:32 PM"
105
+ "119","THB","Thai Baht","4/22/2020 10:02:26 PM","10/6/2023 1:16:43 PM"
106
+ "49","TJS","Tajikistani Somoni","4/22/2020 10:02:26 PM","10/6/2023 1:16:53 PM"
107
+ "105","TMT","Turkmenistan Manat","4/22/2020 10:02:26 PM","10/6/2023 1:17:03 PM"
108
+ "104","TND","Tunisian Dinar","4/22/2020 10:02:26 PM","10/6/2023 1:17:16 PM"
109
+ "88","TRY","Turkish Lira","4/22/2020 10:02:26 PM","10/6/2023 1:17:27 PM"
110
+ "143","TTD","Trinidad and Tobago dollar","11/16/2021 1:55:49 PM",""
111
+ "22","TZS","Tanzanian Shilling","4/22/2020 10:02:26 PM","10/6/2023 1:17:41 PM"
112
+ "93","UAH","Ukrainian Hryvnia","4/22/2020 10:02:26 PM","10/6/2023 1:17:49 PM"
113
+ "39","UGX","Ugandan Shilling","4/22/2020 10:02:26 PM","10/6/2023 1:18:00 PM"
114
+ "28","USD","United States Dollar","4/22/2020 10:02:26 PM","10/6/2023 1:18:14 PM"
115
+ "120","UYU","Uruguayan Peso","4/22/2020 10:02:26 PM","2/4/2022 7:02:54 PM"
116
+ "121","UZS","Uzbekistan Sum","4/22/2020 10:02:26 PM","10/6/2023 1:18:39 PM"
117
+ "98","VEF","","4/22/2020 10:02:26 PM",""
118
+ "135","VES","Venezuelan Bolivar","8/27/2021 3:20:05 PM",""
119
+ "122","VND","Vietnamese Dong","4/22/2020 10:02:26 PM","10/6/2023 1:19:04 PM"
120
+ "151","VUV","Vanuatu Vatu","9/9/2022 10:27:13 AM",""
121
+ "123","WST","Samoan Tala","4/22/2020 10:02:26 PM","10/6/2023 1:19:15 PM"
122
+ "48","XAF","CFA franc BEAC","4/22/2020 10:02:26 PM","10/6/2023 1:19:28 PM"
123
+ "129","XCD","East Caribbean dollar","8/2/2021 6:17:10 PM",""
124
+ "26","XOF","CFA franc BCEAO","4/22/2020 10:02:26 PM","10/6/2023 1:19:46 PM"
125
+ "31","YER","Yemeni Rial","4/22/2020 10:02:26 PM","10/6/2023 1:19:55 PM"
126
+ "124","ZAR","South African Rand","4/22/2020 10:02:26 PM","10/6/2023 1:20:04 PM"
127
+ "160","ZiG","Zimbabwe Gold","5/10/2024 4:34:21 PM","5/16/2024 8:03:21 PM"
128
+ "94","ZMW","Zambian Kwacha","4/22/2020 10:02:26 PM","9/9/2022 10:19:57 AM"
129
+ "125","ZWL","Zimbabwean Dollar","4/22/2020 10:02:26 PM","5/10/2024 4:34:31 PM"