hdx-python-country 4.2.8__tar.gz → 4.3.0__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.
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/PKG-INFO +3 -3
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/pyproject.toml +3 -3
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/_version.py +2 -2
- hdx_python_country-4.3.0/src/hdx/location/wfp_api.py +496 -0
- hdx_python_country-4.3.0/tests/fixtures/wfp/Currency_List.csv +129 -0
- hdx_python_country-4.3.0/tests/fixtures/wfp/Currency_UsdIndirectQuotation_AFN.csv +8695 -0
- hdx_python_country-4.3.0/tests/fixtures/wfp/Currency_UsdIndirectQuotation_SYP.csv +4 -0
- hdx_python_country-4.3.0/tests/fixtures/wfp/MarketPrices_PriceMonthly_BTN.csv +5 -0
- hdx_python_country-4.3.0/tests/fixtures/wfp/Markets_List_AFG.csv +3 -0
- hdx_python_country-4.3.0/tests/hdx/location/test_wfp_api.py +230 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/uv.lock +197 -177
- hdx_python_country-4.2.8/src/hdx/location/wfp_api.py +0 -323
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_List_1.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_List_2.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_1.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_10.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_2.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_3.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_4.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_5.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_6.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_7.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_8.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Currency_UsdIndirectQuotation_9.json +0 -1
- hdx_python_country-4.2.8/tests/fixtures/wfp/Markets_List_AFG_1.json +0 -28
- hdx_python_country-4.2.8/tests/fixtures/wfp/Markets_List_AFG_2.json +0 -5
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/.github/workflows/publish.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/.gitignore +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/.pre-commit-config.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/LICENSE +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/README.md +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/documentation/index.md +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/documentation/mkdocs.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/adminlevel.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/country.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/currency.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/int_timestamp.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/src/hdx/location/wfp_exchangerates.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/adminlevel.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/adminlevelparent.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/secondary_historic_rates.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/fixtures/secondary_rates.json +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/conftest.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/test_adminlevel.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/test_country.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/tests/hdx/location/test_currency.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.3.0}/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.
|
|
3
|
+
Version: 4.3.0
|
|
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.
|
|
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.
|
|
34
|
+
Requires-Dist: data-bridges-client<10,>=9.0.3; extra == 'wfp'
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
37
37
|
[](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.
|
|
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.
|
|
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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (4,
|
|
21
|
+
__version__ = version = '4.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import logging
|
|
3
|
+
import types
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from functools import cache, partial
|
|
7
|
+
from os import getenv
|
|
8
|
+
from typing import Annotated, Any, ClassVar, Union, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
from data_bridges_client import (
|
|
11
|
+
ApiClient,
|
|
12
|
+
ApiException,
|
|
13
|
+
CommoditiesApi,
|
|
14
|
+
Configuration,
|
|
15
|
+
CurrencyApi,
|
|
16
|
+
MarketPricesApi,
|
|
17
|
+
MarketsApi,
|
|
18
|
+
)
|
|
19
|
+
from data_bridges_client.exceptions import ServiceException
|
|
20
|
+
from data_bridges_client.models.commodity_dto import CommodityDTO
|
|
21
|
+
from data_bridges_client.models.currency_dto import CurrencyDTO
|
|
22
|
+
from data_bridges_client.models.market_dto import MarketDTO
|
|
23
|
+
from data_bridges_client.models.usd_indirect_quotation import UsdIndirectQuotation
|
|
24
|
+
from data_bridges_client.models.view_extended_monthly_aggregated_price import (
|
|
25
|
+
ViewExtendedMonthlyAggregatedPrice,
|
|
26
|
+
)
|
|
27
|
+
from data_bridges_client.token import WfpApiToken
|
|
28
|
+
from hdx.utilities.retriever import Retrieve
|
|
29
|
+
from tenacity import (
|
|
30
|
+
Retrying,
|
|
31
|
+
after_log,
|
|
32
|
+
retry_if_exception_type,
|
|
33
|
+
stop_after_attempt,
|
|
34
|
+
wait_fixed,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
_CSV_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
|
40
|
+
_PRICES_START_DATE = date(1990, 1, 1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _default_end_date() -> date:
|
|
44
|
+
# WFP has forecast prices a year or more ahead
|
|
45
|
+
return date(date.today().year + 5, 12, 31)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _to_date(value: date | None, default: date) -> date:
|
|
49
|
+
if value is None:
|
|
50
|
+
return default
|
|
51
|
+
if isinstance(value, datetime):
|
|
52
|
+
return value.date()
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _csv_datetime(value: str) -> datetime:
|
|
57
|
+
return datetime.strptime(value, _CSV_DATE_FORMAT)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _csv_bool(value: str) -> bool:
|
|
61
|
+
return value == "True"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _csv_none(value: str) -> None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _field_types(annotation: Any) -> set:
|
|
69
|
+
origin = get_origin(annotation)
|
|
70
|
+
if origin is Annotated:
|
|
71
|
+
return _field_types(get_args(annotation)[0])
|
|
72
|
+
if origin in (Union, types.UnionType):
|
|
73
|
+
return set().union(
|
|
74
|
+
*(_field_types(x) for x in get_args(annotation) if x is not type(None))
|
|
75
|
+
)
|
|
76
|
+
return {origin or annotation}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cache
|
|
80
|
+
def _csv_converters(model_cls: type) -> dict[str, Callable]:
|
|
81
|
+
"""Map the JSON key of each non-string field of a data_bridges_client
|
|
82
|
+
model to a function that converts its CSV string value, based on the
|
|
83
|
+
field's type. List fields (CommodityDTO's qualities and processing) are
|
|
84
|
+
not in WFP's CSV output, so are always None.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
model_cls: data_bridges_client model class
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Map from JSON key to converter function
|
|
91
|
+
"""
|
|
92
|
+
converters = {}
|
|
93
|
+
for name, field in model_cls.model_fields.items():
|
|
94
|
+
if name == "additional_properties":
|
|
95
|
+
continue
|
|
96
|
+
field_types = _field_types(field.annotation)
|
|
97
|
+
if list in field_types:
|
|
98
|
+
converter = _csv_none
|
|
99
|
+
elif bool in field_types:
|
|
100
|
+
converter = _csv_bool
|
|
101
|
+
elif float in field_types:
|
|
102
|
+
converter = float
|
|
103
|
+
elif int in field_types:
|
|
104
|
+
converter = int
|
|
105
|
+
elif datetime in field_types:
|
|
106
|
+
converter = _csv_datetime
|
|
107
|
+
else:
|
|
108
|
+
continue
|
|
109
|
+
converters[field.alias or name] = converter
|
|
110
|
+
return converters
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_csv(text: str, model_cls: type) -> list:
|
|
114
|
+
"""Parse WFP's CSV output into data_bridges_client model objects. CSV
|
|
115
|
+
headers are the JSON keys with the first letter capitalised and every
|
|
116
|
+
value is a string. Blank strings are kept as "" (as JSON returns them)
|
|
117
|
+
but blank values of non-string fields become None.
|
|
118
|
+
|
|
119
|
+
To limit memory use (IDN has 462,027 price rows), lines are split
|
|
120
|
+
lazily rather than with io.StringIO, which holds 4 bytes per character,
|
|
121
|
+
each column reuses one object per distinct value and rows with the same
|
|
122
|
+
fields share one pydantic fields set (2,264 bytes each otherwise).
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
text: CSV text
|
|
126
|
+
model_cls: data_bridges_client model class of each row
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of model_cls objects
|
|
130
|
+
"""
|
|
131
|
+
converters = _csv_converters(model_cls)
|
|
132
|
+
reader = csv.reader(f"{line}\n" for line in text.split("\n"))
|
|
133
|
+
keys = [header[0].lower() + header[1:] for header in next(reader, [])]
|
|
134
|
+
columns = [(key, converters.get(key), {}) for key in keys]
|
|
135
|
+
fields_sets = {}
|
|
136
|
+
items = []
|
|
137
|
+
for row in reader:
|
|
138
|
+
if not row:
|
|
139
|
+
continue
|
|
140
|
+
obj = {}
|
|
141
|
+
for (key, converter, values), value in zip(columns, row):
|
|
142
|
+
try:
|
|
143
|
+
obj[key] = values[value]
|
|
144
|
+
except KeyError:
|
|
145
|
+
if converter:
|
|
146
|
+
converted = converter(value) if value else None
|
|
147
|
+
else:
|
|
148
|
+
converted = value
|
|
149
|
+
values[value] = converted
|
|
150
|
+
obj[key] = converted
|
|
151
|
+
item = model_cls.from_dict(obj)
|
|
152
|
+
fields_set = frozenset(item.__pydantic_fields_set__)
|
|
153
|
+
fields_set = fields_sets.setdefault(fields_set, item.__pydantic_fields_set__)
|
|
154
|
+
object.__setattr__(item, "__pydantic_fields_set__", fields_set)
|
|
155
|
+
items.append(item)
|
|
156
|
+
return items
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class WFPAPI:
|
|
160
|
+
"""Light wrapper around WFP's data-bridges-client library. All data is
|
|
161
|
+
requested as CSV, which returns every row in one response, rather than
|
|
162
|
+
JSON, whose paging returns some rows twice and leaves others out. It
|
|
163
|
+
needs a retriever used only to decide whether to load from disk or hit
|
|
164
|
+
the network and whether to persist the network result (as the CSV
|
|
165
|
+
returned by WFP). Token refreshing is delegated to data_bridges_client's
|
|
166
|
+
own WfpApiToken.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
retriever: Retrieve object for interacting with WFP API
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
default_retry_params: ClassVar[dict[str, Any]] = {
|
|
173
|
+
"retry": retry_if_exception_type(ApiException),
|
|
174
|
+
"after": after_log(logger, logging.INFO),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
retriever: Retrieve,
|
|
180
|
+
wfp_key: str | None = None,
|
|
181
|
+
wfp_secret: str | None = None,
|
|
182
|
+
):
|
|
183
|
+
self.retriever = retriever
|
|
184
|
+
self.retry_params = {"attempts": 1, "wait": 1}
|
|
185
|
+
if wfp_key:
|
|
186
|
+
self._wfp_key = wfp_key
|
|
187
|
+
else:
|
|
188
|
+
self._wfp_key = getenv("WFP_KEY")
|
|
189
|
+
if wfp_secret:
|
|
190
|
+
self._wfp_secret = wfp_secret
|
|
191
|
+
else:
|
|
192
|
+
self._wfp_secret = getenv("WFP_SECRET")
|
|
193
|
+
self.token = WfpApiToken(api_key=self._wfp_key, api_secret=self._wfp_secret)
|
|
194
|
+
self.configuration = Configuration()
|
|
195
|
+
self.api_client = ApiClient(self.configuration)
|
|
196
|
+
self.currency_api = CurrencyApi(self.api_client)
|
|
197
|
+
self.markets_api = MarketsApi(self.api_client)
|
|
198
|
+
self.commodities_api = CommoditiesApi(self.api_client)
|
|
199
|
+
self.market_prices_api = MarketPricesApi(self.api_client)
|
|
200
|
+
|
|
201
|
+
def get_retry_params(self) -> dict:
|
|
202
|
+
return self.retry_params
|
|
203
|
+
|
|
204
|
+
def update_retry_params(self, attempts: int, wait: int) -> dict:
|
|
205
|
+
self.retry_params["attempts"] = attempts
|
|
206
|
+
self.retry_params["wait"] = wait
|
|
207
|
+
return self.retry_params
|
|
208
|
+
|
|
209
|
+
def refresh_token(self) -> None:
|
|
210
|
+
self.configuration.access_token = self.token.refresh()
|
|
211
|
+
|
|
212
|
+
def _with_retry(self, api_method: Callable, **kwargs: Any) -> Any:
|
|
213
|
+
retryer = Retrying(
|
|
214
|
+
retry=self.default_retry_params["retry"],
|
|
215
|
+
after=self.default_retry_params["after"],
|
|
216
|
+
stop=stop_after_attempt(self.retry_params["attempts"]),
|
|
217
|
+
wait=wait_fixed(self.retry_params["wait"]),
|
|
218
|
+
)
|
|
219
|
+
for attempt in retryer:
|
|
220
|
+
with attempt:
|
|
221
|
+
try:
|
|
222
|
+
return api_method(**kwargs)
|
|
223
|
+
except ApiException as err:
|
|
224
|
+
if err.status not in (104, 401, 403):
|
|
225
|
+
raise
|
|
226
|
+
self.refresh_token()
|
|
227
|
+
return api_method(**kwargs)
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _get_csv(api_method: Callable, **kwargs: Any) -> str:
|
|
231
|
+
"""Call a data_bridges_client API method's _without_preload_content
|
|
232
|
+
variant with format CSV, raising ApiException (as the methods that
|
|
233
|
+
parse JSON do) if the response is not OK.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
api_method: Bound _without_preload_content method
|
|
237
|
+
**kwargs: Parameters to pass to api_method
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
CSV text
|
|
241
|
+
"""
|
|
242
|
+
response = api_method(format="CSV", **kwargs)
|
|
243
|
+
text = response.data.decode("utf-8-sig")
|
|
244
|
+
if response.status != 200:
|
|
245
|
+
ApiException.from_response(http_resp=response, body=text, data=None)
|
|
246
|
+
return text
|
|
247
|
+
|
|
248
|
+
def _call(
|
|
249
|
+
self, csv_method: Callable, filename: str, log: str, **kwargs: Any
|
|
250
|
+
) -> str | None:
|
|
251
|
+
"""Get CSV text from csv_method, loading from or saving to disk as
|
|
252
|
+
Retrieve does.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
csv_method: Function returning CSV text
|
|
256
|
+
filename: Filename of saved file
|
|
257
|
+
log: Text to use in log string to describe the call
|
|
258
|
+
**kwargs: Parameters to pass to csv_method
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
CSV text, or None if there is no saved data
|
|
262
|
+
"""
|
|
263
|
+
filename, _ = self.retriever.get_filename("", filename)
|
|
264
|
+
saved_path = self.retriever.saved_dir / filename
|
|
265
|
+
if self.retriever.use_saved:
|
|
266
|
+
logger.info(f"Using saved {log} in {saved_path}")
|
|
267
|
+
try:
|
|
268
|
+
with open(saved_path, encoding="utf-8", newline="") as f:
|
|
269
|
+
return f.read()
|
|
270
|
+
except FileNotFoundError:
|
|
271
|
+
return None
|
|
272
|
+
text = self._with_retry(csv_method, **kwargs)
|
|
273
|
+
if self.retriever.save:
|
|
274
|
+
logger.info(f"Saving {log} in {saved_path}")
|
|
275
|
+
with open(saved_path, "w", encoding="utf-8", newline="") as f:
|
|
276
|
+
f.write(text)
|
|
277
|
+
return text
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _countryiso3s(countryiso3: str | None) -> list:
|
|
281
|
+
if countryiso3 == "PSE": # hack as PSE is treated by WFP as 2 areas
|
|
282
|
+
return ["PSW", "PSG"]
|
|
283
|
+
return [countryiso3]
|
|
284
|
+
|
|
285
|
+
def _get_all(
|
|
286
|
+
self,
|
|
287
|
+
api_method: Callable,
|
|
288
|
+
model_cls: type,
|
|
289
|
+
base_filename: str,
|
|
290
|
+
countryiso3: str | None,
|
|
291
|
+
country_param: str,
|
|
292
|
+
extra_params: dict,
|
|
293
|
+
get_csv: Callable | None = None,
|
|
294
|
+
) -> list:
|
|
295
|
+
csv_method = partial(get_csv or self._get_csv, api_method)
|
|
296
|
+
all_items = []
|
|
297
|
+
for country in self._countryiso3s(countryiso3):
|
|
298
|
+
kwargs = dict(extra_params)
|
|
299
|
+
if country is None:
|
|
300
|
+
filename = f"{base_filename}.csv"
|
|
301
|
+
log = base_filename
|
|
302
|
+
else:
|
|
303
|
+
kwargs[country_param] = country
|
|
304
|
+
filename = f"{base_filename}_{country}.csv"
|
|
305
|
+
log = f"{base_filename} for {country}"
|
|
306
|
+
text = self._call(csv_method, filename, log, **kwargs)
|
|
307
|
+
if text:
|
|
308
|
+
all_items.extend(_parse_csv(text, model_cls))
|
|
309
|
+
return all_items
|
|
310
|
+
|
|
311
|
+
def get_currencies(
|
|
312
|
+
self, countryiso3: str | None = None, currency: str | None = None
|
|
313
|
+
) -> list[CurrencyDTO]:
|
|
314
|
+
"""Get list of currencies from the WFP API.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
318
|
+
currency: Currency 3-letter code to filter by. Defaults to all currencies.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of currencies from the WFP API
|
|
322
|
+
"""
|
|
323
|
+
if currency:
|
|
324
|
+
base_filename = f"Currency_List_{currency.upper()}"
|
|
325
|
+
extra_params = {"currency_name": currency}
|
|
326
|
+
else:
|
|
327
|
+
base_filename = "Currency_List"
|
|
328
|
+
extra_params = {}
|
|
329
|
+
return self._get_all(
|
|
330
|
+
self.currency_api.currency_list_get_without_preload_content,
|
|
331
|
+
CurrencyDTO,
|
|
332
|
+
base_filename,
|
|
333
|
+
countryiso3,
|
|
334
|
+
"country_code",
|
|
335
|
+
extra_params,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_currency_usd_indirect_quotations(
|
|
339
|
+
self, currency: str, countryiso3: str | None = None
|
|
340
|
+
) -> list[UsdIndirectQuotation]:
|
|
341
|
+
"""Get USD indirect quotations for a currency from the WFP API.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
currency: Currency 3-letter code
|
|
345
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of USD indirect quotations from the WFP API
|
|
349
|
+
"""
|
|
350
|
+
return self._get_all(
|
|
351
|
+
self.currency_api.currency_usd_indirect_quotation_get_without_preload_content,
|
|
352
|
+
UsdIndirectQuotation,
|
|
353
|
+
f"Currency_UsdIndirectQuotation_{currency.upper()}",
|
|
354
|
+
countryiso3,
|
|
355
|
+
# this endpoint uses country_iso3, unlike every other one here which
|
|
356
|
+
# uses country_code - don't "fix" this to country_code
|
|
357
|
+
"country_iso3",
|
|
358
|
+
{"currency_name": currency},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def get_markets(self, countryiso3: str | None = None) -> list[MarketDTO]:
|
|
362
|
+
"""Get list of markets from the WFP API.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List of markets from the WFP API
|
|
369
|
+
"""
|
|
370
|
+
return self._get_all(
|
|
371
|
+
self.markets_api.markets_list_get_without_preload_content,
|
|
372
|
+
MarketDTO,
|
|
373
|
+
"Markets_List",
|
|
374
|
+
countryiso3,
|
|
375
|
+
"country_code",
|
|
376
|
+
{},
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def get_commodities(self, countryiso3: str | None = None) -> list[CommodityDTO]:
|
|
380
|
+
"""Get list of commodities from the WFP API. WFP's CSV output does not
|
|
381
|
+
include qualities and processing, so they are None.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of commodities from the WFP API
|
|
388
|
+
"""
|
|
389
|
+
return self._get_all(
|
|
390
|
+
self.commodities_api.commodities_list_get_without_preload_content,
|
|
391
|
+
CommodityDTO,
|
|
392
|
+
"Commodities_List",
|
|
393
|
+
countryiso3,
|
|
394
|
+
"country_code",
|
|
395
|
+
{},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def get_commodity_categories(
|
|
399
|
+
self, countryiso3: str | None = None
|
|
400
|
+
) -> list[CommodityDTO]:
|
|
401
|
+
"""Get list of commodity categories from the WFP API.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of commodity categories from the WFP API
|
|
408
|
+
"""
|
|
409
|
+
return self._get_all(
|
|
410
|
+
self.commodities_api.commodities_categories_list_get_without_preload_content,
|
|
411
|
+
CommodityDTO,
|
|
412
|
+
"Commodities_Categories_List",
|
|
413
|
+
countryiso3,
|
|
414
|
+
"country_code",
|
|
415
|
+
{},
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def _get_prices_csv_range(
|
|
419
|
+
self, api_method: Callable, start_date: date, end_date: date, **kwargs: Any
|
|
420
|
+
) -> list[str]:
|
|
421
|
+
"""Get monthly aggregated market prices between two dates (inclusive)
|
|
422
|
+
as CSV. If the API gateway times out (504), the date range is split in
|
|
423
|
+
two and each half is requested.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
api_method: Bound market_prices_price_monthly_get_without_preload_content
|
|
427
|
+
start_date: Start date
|
|
428
|
+
end_date: End date
|
|
429
|
+
**kwargs: Parameters to pass to api_method
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
List of CSV texts, one per date range, in date order
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
return [
|
|
436
|
+
self._get_csv(
|
|
437
|
+
api_method, start_date=start_date, end_date=end_date, **kwargs
|
|
438
|
+
)
|
|
439
|
+
]
|
|
440
|
+
except ServiceException as err:
|
|
441
|
+
if err.status != 504 or start_date >= end_date:
|
|
442
|
+
raise
|
|
443
|
+
middle = start_date + (end_date - start_date) // 2
|
|
444
|
+
logger.info(
|
|
445
|
+
f"Timeout getting prices from {start_date} to {end_date}, splitting at {middle}"
|
|
446
|
+
)
|
|
447
|
+
return self._get_prices_csv_range(
|
|
448
|
+
api_method, start_date, middle, **kwargs
|
|
449
|
+
) + self._get_prices_csv_range(
|
|
450
|
+
api_method, middle + timedelta(days=1), end_date, **kwargs
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _get_prices_csv(
|
|
454
|
+
self,
|
|
455
|
+
api_method: Callable,
|
|
456
|
+
start_date: date | None = None,
|
|
457
|
+
end_date: date | None = None,
|
|
458
|
+
**kwargs: Any,
|
|
459
|
+
) -> str:
|
|
460
|
+
first, *rest = self._get_prices_csv_range(
|
|
461
|
+
api_method,
|
|
462
|
+
_to_date(start_date, _PRICES_START_DATE),
|
|
463
|
+
_to_date(end_date, _default_end_date()),
|
|
464
|
+
**kwargs,
|
|
465
|
+
)
|
|
466
|
+
if not rest:
|
|
467
|
+
return first
|
|
468
|
+
# keep only the first range's header row
|
|
469
|
+
parts = [first.rstrip("\r\n")]
|
|
470
|
+
parts.extend(text.partition("\n")[2].rstrip("\r\n") for text in rest)
|
|
471
|
+
return "\r\n".join(part for part in parts if part) + "\r\n"
|
|
472
|
+
|
|
473
|
+
def get_market_prices_monthly(
|
|
474
|
+
self, countryiso3: str | None = None, **kwargs: Any
|
|
475
|
+
) -> list[ViewExtendedMonthlyAggregatedPrice]:
|
|
476
|
+
"""Get monthly aggregated market prices from the WFP API.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
countryiso3: Country for which to obtain data. Defaults to all countries.
|
|
480
|
+
**kwargs: Additional filters accepted by
|
|
481
|
+
data_bridges_client's MarketPricesApi.market_prices_price_monthly_get,
|
|
482
|
+
e.g. market_id, commodity_id, price_type_name, currency_id,
|
|
483
|
+
price_flag, start_date, end_date, latest_value_only
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of monthly aggregated market prices from the WFP API
|
|
487
|
+
"""
|
|
488
|
+
return self._get_all(
|
|
489
|
+
self.market_prices_api.market_prices_price_monthly_get_without_preload_content,
|
|
490
|
+
ViewExtendedMonthlyAggregatedPrice,
|
|
491
|
+
"MarketPrices_PriceMonthly",
|
|
492
|
+
countryiso3,
|
|
493
|
+
"country_code",
|
|
494
|
+
kwargs,
|
|
495
|
+
self._get_prices_csv,
|
|
496
|
+
)
|