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.
Files changed (46) hide show
  1. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/PKG-INFO +1 -1
  2. hdx_python_country-4.3.1/docs/decisions/0001-wfp-databridges-fetched-as-csv.md +37 -0
  3. hdx_python_country-4.3.1/docs/decisions/0002-split-price-date-range-on-504.md +28 -0
  4. hdx_python_country-4.3.1/docs/decisions/0003-memory-efficient-csv-parsing.md +46 -0
  5. hdx_python_country-4.3.1/docs/decisions/0004-pse-fetched-as-psw-and-psg.md +21 -0
  6. hdx_python_country-4.3.1/docs/decisions/0005-wfp-dates-assumed-utc.md +21 -0
  7. hdx_python_country-4.3.1/docs/decisions/README.md +35 -0
  8. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/_version.py +2 -2
  9. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/wfp_api.py +9 -14
  10. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/wfp_exchangerates.py +1 -3
  11. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.github/workflows/publish.yaml +0 -0
  12. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.github/workflows/run-python-tests.yaml +0 -0
  13. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.gitignore +0 -0
  14. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/.pre-commit-config.yaml +0 -0
  15. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/LICENSE +0 -0
  16. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/README.md +0 -0
  17. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/.readthedocs.yaml +0 -0
  18. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/index.md +0 -0
  19. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/documentation/mkdocs.yaml +0 -0
  20. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/pyproject.toml +0 -0
  21. {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
  22. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/__init__.py +0 -0
  23. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/adminlevel.py +0 -0
  24. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/country.py +0 -0
  25. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/currency.py +0 -0
  26. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/src/hdx/location/int_timestamp.py +0 -0
  27. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/adminlevel.yaml +0 -0
  28. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/adminlevelparent.yaml +0 -0
  29. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/fixtures-download-global-pcode-lengths.csv +0 -0
  30. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/fixtures-download-global-pcodes-adm-1-2.csv +0 -0
  31. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/secondary_historic_rates.csv +0 -0
  32. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/secondary_rates.json +0 -0
  33. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_List.csv +0 -0
  34. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_AFN.csv +0 -0
  35. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Currency_UsdIndirectQuotation_SYP.csv +0 -0
  36. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/MarketPrices_PriceMonthly_BTN.csv +0 -0
  37. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/fixtures/wfp/Markets_List_AFG.csv +0 -0
  38. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/Countries_UZB_Deleted.csv +0 -0
  39. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/__init__.py +0 -0
  40. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/conftest.py +0 -0
  41. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_adminlevel.py +0 -0
  42. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_country.py +0 -0
  43. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_currency.py +0 -0
  44. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_wfp_api.py +0 -0
  45. {hdx_python_country-4.3.0 → hdx_python_country-4.3.1}/tests/hdx/location/test_wfp_exchangerates.py +0 -0
  46. {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.0
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.0'
22
- __version_tuple__ = version_tuple = (4, 3, 0)
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
- 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).
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, 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.
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": # hack as PSE is treated by WFP as 2 areas
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
- # data_bridges_client parses WFP's timezone-less date strings
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