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.
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/PKG-INFO +3 -3
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/pyproject.toml +3 -3
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/_version.py +2 -2
- hdx_python_country-4.2.9/src/hdx/location/wfp_api.py +481 -0
- hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_List.csv +129 -0
- hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_UsdIndirectQuotation_AFN.csv +8695 -0
- hdx_python_country-4.2.9/tests/fixtures/wfp/Currency_UsdIndirectQuotation_SYP.csv +4 -0
- hdx_python_country-4.2.9/tests/fixtures/wfp/MarketPrices_PriceMonthly_BTN.csv +5 -0
- hdx_python_country-4.2.9/tests/fixtures/wfp/Markets_List_AFG.csv +3 -0
- hdx_python_country-4.2.9/tests/hdx/location/test_wfp_api.py +230 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/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.2.9}/.github/workflows/publish.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.gitignore +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/.pre-commit-config.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/LICENSE +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/README.md +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/index.md +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/documentation/mkdocs.yaml +0 -0
- {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
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/adminlevel.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/country.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/currency.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/int_timestamp.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/src/hdx/location/wfp_exchangerates.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/adminlevel.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/adminlevelparent.yaml +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/secondary_historic_rates.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/fixtures/secondary_rates.json +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/conftest.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_adminlevel.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_country.py +0 -0
- {hdx_python_country-4.2.8 → hdx_python_country-4.2.9}/tests/hdx/location/test_currency.py +0 -0
- {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.
|
|
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.
|
|
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.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (4, 2,
|
|
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"
|