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