hdx-python-country 4.3.0__tar.gz → 4.3.1__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.3.0 → hdx_python_country-4.3.1}/PKG-INFO +1 -1
- hdx_python_country-4.3.1/docs/decisions/0001-wfp-databridges-fetched-as-csv.md +37 -0
- hdx_python_country-4.3.1/docs/decisions/0002-split-price-date-range-on-504.md +28 -0
- hdx_python_country-4.3.1/docs/decisions/0003-memory-efficient-csv-parsing.md +46 -0
- hdx_python_country-4.3.1/docs/decisions/0004-pse-fetched-as-psw-and-psg.md +21 -0
- hdx_python_country-4.3.1/docs/decisions/0005-wfp-dates-assumed-utc.md +21 -0
- hdx_python_country-4.3.1/docs/decisions/README.md +35 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/_version.py +2 -2
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/wfp_api.py +9 -14
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/wfp_exchangerates.py +1 -3
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.github/workflows/publish.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.gitignore +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.pre-commit-config.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/LICENSE +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/README.md +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/index.md +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/mkdocs.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/pyproject.toml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/Countries & Territories Taxonomy MVP - C&T Taxonomy.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/adminlevel.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/country.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/currency.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/int_timestamp.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/adminlevel.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/adminlevelparent.yaml +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/secondary_historic_rates.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/secondary_rates.json +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_List.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_AFN.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_SYP.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/MarketPrices_PriceMonthly_BTN.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Markets_List_AFG.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/__init__.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/conftest.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_adminlevel.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_country.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_currency.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_wfp_api.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_wfp_exchangerates.py +0 -0
- {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hdx-python-country
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.1
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 0001: Fetch WFP DataBridges data as CSV rather than paged JSON
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-11-03
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
JSON paging of `market_prices_price_monthly_get` is unstable. Confirmed live on NGA: each run
|
|
10
|
+
returned about 1,690 rows twice and left out the same number, so datasets built from it missed
|
|
11
|
+
about 1.4% of rows. The endpoint has no sort parameter that would make offset paging
|
|
12
|
+
deterministic.
|
|
13
|
+
|
|
14
|
+
`format="CSV"` returns every row once in a single response and ignores `page` (NGA: 121,669
|
|
15
|
+
distinct rows in about 10s, compared with about 800s paged). The other `WFPAPI` endpoints accept
|
|
16
|
+
CSV too.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
Every `WFPAPI` endpoint is called with `format="CSV"` through the generated
|
|
21
|
+
`*_without_preload_content` methods, and the JSON paging code is removed.
|
|
22
|
+
|
|
23
|
+
- CSV headers are the JSON keys with the first letter capitalised, and every value is a string.
|
|
24
|
+
`_csv_converters` builds the int, float, bool and datetime converters from each model's pydantic
|
|
25
|
+
annotations.
|
|
26
|
+
- `_get_csv` raises `ApiException` on a non-200 status, as the JSON methods do, so `_with_retry`
|
|
27
|
+
still refreshes the token and retries.
|
|
28
|
+
- Saved data is the CSV that WFP returns, one file per call.
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
|
|
32
|
+
- Blank text fields are `""` where JSON gave `None`.
|
|
33
|
+
- Commodity `qualities` and `processing` are always `None`, since CSV omits them.
|
|
34
|
+
- Datetimes lose milliseconds.
|
|
35
|
+
- Data saved in the old JSON format no longer loads.
|
|
36
|
+
- A whole country comes back in one response (SYR: 516,907 rows, 103 MB), which can hit the
|
|
37
|
+
gateway timeout (see 0002) and needs care with memory (see 0003).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 0002: Split the monthly prices date range on a gateway timeout
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-11-03
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
A single CSV request for a large country can exceed the API gateway's timeout of about 30s. SYR's
|
|
10
|
+
first full request got a 504 after 30s, and the second returned 200 in 35s. Relying on
|
|
11
|
+
`_with_retry` is not acceptable, because the country run uses `wait=1800`, i.e. 30 minutes per
|
|
12
|
+
retry.
|
|
13
|
+
|
|
14
|
+
`start_date` and `end_date` are inclusive and split the data cleanly. On BTN, 1990–2019 plus
|
|
15
|
+
2020–2030 returned exactly the whole set (1,836 + 6,171 = 8,007 rows).
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
`_get_prices_csv_range` requests the whole range (1990-01-01 to 31 December five years ahead,
|
|
20
|
+
since WFP has forecast prices). On a 504 it splits the range at the midpoint and requests each
|
|
21
|
+
half, recursively. A 504 on a single day, or any other error, is raised. `_get_prices_csv` joins
|
|
22
|
+
the pieces, keeping only the first header row.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
- Most countries take one request. Each 504 costs about 30s before the split.
|
|
27
|
+
- On 2026-11-04 the server was slower, and IDN's range was split at least three times.
|
|
28
|
+
- Saved data is still one file per country, whatever the number of requests.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 0003: Parse WFP CSV with shared values and fields sets
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-11-03
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Fetching and parsing IDN's prices (462,027 rows) peaked at 2.31 GB with the first CSV parser, and
|
|
10
|
+
the country run was killed on Jenkins (exit code 137). Each row's
|
|
11
|
+
`ViewExtendedMonthlyAggregatedPrice` took about 3.2 KB:
|
|
12
|
+
|
|
13
|
+
| Part | Bytes |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Object | 72 |
|
|
16
|
+
| `__dict__` with 22 fields | 832 |
|
|
17
|
+
| `__pydantic_fields_set__`, the 22 field names set on this row | 2,264 |
|
|
18
|
+
| Empty `additional_properties` | 64 |
|
|
19
|
+
|
|
20
|
+
Every row also held its own copies of repeated values such as market names, dates and units.
|
|
21
|
+
`io.StringIO` copied the whole text into a buffer of 4 bytes per character.
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
`_parse_csv`:
|
|
26
|
+
|
|
27
|
+
- Splits lines lazily with `text.split("\n")` instead of `io.StringIO`, re-adding the `\n` to each
|
|
28
|
+
line so quoted fields containing line breaks parse the same.
|
|
29
|
+
- Reuses one object per distinct value in each column.
|
|
30
|
+
- Gives rows with the same fields one shared `__pydantic_fields_set__`. Pydantic only adds to it
|
|
31
|
+
when a field is assigned, and every field is already in it.
|
|
32
|
+
|
|
33
|
+
## Consequences
|
|
34
|
+
|
|
35
|
+
Measured on the saved IDN CSV:
|
|
36
|
+
|
|
37
|
+
| | Peak memory | Parse time |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `io.StringIO` | 2,310 MB | 9.5s |
|
|
40
|
+
| Lazy split | 2,059 MB | |
|
|
41
|
+
| Lazy split and reused values | 1,755 MB | |
|
|
42
|
+
| Lazy split, reused values and shared fields set | 746 MB | 5.7s |
|
|
43
|
+
|
|
44
|
+
- The parsed rows are identical to those from the `io.StringIO` parser.
|
|
45
|
+
- Rows share value objects. They are immutable (str, int, float, datetime), so this is not visible
|
|
46
|
+
to callers.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 0004: Fetch PSE as PSW and PSG
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2024-10-31
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
WFP treats the State of Palestine as two areas, the West Bank (`PSW`) and the Gaza Strip (`PSG`).
|
|
10
|
+
Asking WFP for `PSE` returns nothing. Checked live on 2026-11-04, the markets endpoint gave 0
|
|
11
|
+
markets for `PSE`, 12 for `PSW` and 47 for `PSG`.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
`WFPAPI._countryiso3s` turns `PSE` into `["PSW", "PSG"]`, so every country-filtered endpoint makes
|
|
16
|
+
one request per area and returns the combined rows.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
- Callers use the ISO 3166 code `PSE` and get both areas' data.
|
|
21
|
+
- Saved files are per area, e.g. `MarketPrices_PriceMonthly_PSW.csv`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 0005: Treat WFP dates without a timezone as UTC
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-07-20
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
WFP's dates have no timezone. Before the move to the DataBridges client (#88), they were parsed
|
|
10
|
+
with `parse_date`, which returns UTC datetimes. `data_bridges_client` returns naive datetimes
|
|
11
|
+
instead.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
`WFPExchangeRates.get_currency_historic_rates` sets UTC on any naive quotation date before
|
|
16
|
+
converting it to a timestamp. hdx-scraper-wfp-foodprices does the same for price dates.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
- Historic rate timestamps are unchanged from the `parse_date` behaviour.
|
|
21
|
+
- If WFP starts returning dates with a timezone, they are used as given.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# docs/decisions/
|
|
2
|
+
|
|
3
|
+
Decision records: one immutable file per non-trivial design/architecture
|
|
4
|
+
decision — using Michael Nygard's minimal headers (Title, Status, Context,
|
|
5
|
+
Decision, Consequences), plus a `Date:` line under Status. Distill the
|
|
6
|
+
decision itself, not the full planning narrative behind it.
|
|
7
|
+
|
|
8
|
+
A reversed decision gets a new record whose Status reads
|
|
9
|
+
`Superseded by 00NN`; the old file stays as-is. Routine notes belong in
|
|
10
|
+
`CLAUDE.md` instead of here; PR-description-only detail doesn't need a
|
|
11
|
+
record at all.
|
|
12
|
+
|
|
13
|
+
Numbered `NNNN-title.md`, sequential, never reused.
|
|
14
|
+
|
|
15
|
+
## Format
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
# NNNN: <Title>
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
|
|
22
|
+
Accepted — YYYY-MM-DD
|
|
23
|
+
|
|
24
|
+
## Context
|
|
25
|
+
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
...
|
|
35
|
+
```
|
|
@@ -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.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (4, 3,
|
|
21
|
+
__version__ = version = '4.3.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 3, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -114,12 +114,9 @@ def _parse_csv(text: str, model_cls: type) -> list:
|
|
|
114
114
|
"""Parse WFP's CSV output into data_bridges_client model objects. CSV
|
|
115
115
|
headers are the JSON keys with the first letter capitalised and every
|
|
116
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
|
-
|
|
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).
|
|
117
|
+
but blank values of non-string fields become None. Values and pydantic
|
|
118
|
+
fields sets are shared between rows to limit memory use (see
|
|
119
|
+
docs/decisions/0003).
|
|
123
120
|
|
|
124
121
|
Args:
|
|
125
122
|
text: CSV text
|
|
@@ -158,12 +155,10 @@ def _parse_csv(text: str, model_cls: type) -> list:
|
|
|
158
155
|
|
|
159
156
|
class WFPAPI:
|
|
160
157
|
"""Light wrapper around WFP's data-bridges-client library. All data is
|
|
161
|
-
requested as CSV
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
returned by WFP). Token refreshing is delegated to data_bridges_client's
|
|
166
|
-
own WfpApiToken.
|
|
158
|
+
requested as CSV (see docs/decisions/0001). It needs a retriever used
|
|
159
|
+
only to decide whether to load from disk or hit the network and whether
|
|
160
|
+
to persist the network result (as the CSV returned by WFP). Token
|
|
161
|
+
refreshing is delegated to data_bridges_client's own WfpApiToken.
|
|
167
162
|
|
|
168
163
|
Args:
|
|
169
164
|
retriever: Retrieve object for interacting with WFP API
|
|
@@ -278,7 +273,7 @@ class WFPAPI:
|
|
|
278
273
|
|
|
279
274
|
@staticmethod
|
|
280
275
|
def _countryiso3s(countryiso3: str | None) -> list:
|
|
281
|
-
if countryiso3 == "PSE": #
|
|
276
|
+
if countryiso3 == "PSE": # see docs/decisions/0004
|
|
282
277
|
return ["PSW", "PSG"]
|
|
283
278
|
return [countryiso3]
|
|
284
279
|
|
|
@@ -420,7 +415,7 @@ class WFPAPI:
|
|
|
420
415
|
) -> list[str]:
|
|
421
416
|
"""Get monthly aggregated market prices between two dates (inclusive)
|
|
422
417
|
as CSV. If the API gateway times out (504), the date range is split in
|
|
423
|
-
two and each half is requested.
|
|
418
|
+
two and each half is requested (see docs/decisions/0002).
|
|
424
419
|
|
|
425
420
|
Args:
|
|
426
421
|
api_method: Bound market_prices_price_monthly_get_without_preload_content
|
|
@@ -59,9 +59,7 @@ class WFPExchangeRates:
|
|
|
59
59
|
continue
|
|
60
60
|
date = quote.var_date
|
|
61
61
|
if date.tzinfo is None:
|
|
62
|
-
#
|
|
63
|
-
# into naive datetimes; assume UTC to match previous
|
|
64
|
-
# parse_date-based behaviour
|
|
62
|
+
# See docs/decisions/0005
|
|
65
63
|
date = date.replace(tzinfo=UTC)
|
|
66
64
|
timestamp = get_int_timestamp(date)
|
|
67
65
|
historic_rates[timestamp] = quote.value
|
|
File without changes
|
{hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.github/workflows/run-python-tests.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/secondary_historic_rates.csv
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Markets_List_AFG.csv
RENAMED
|
File without changes
|
{hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/Countries_UZB_Deleted.csv
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_wfp_exchangerates.py
RENAMED
|
File without changes
|
|
File without changes
|