oda-data 2.4.2__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {oda_data-2.4.2 → oda_data-2.6.0}/PKG-INFO +20 -11
- {oda_data-2.4.2 → oda_data-2.6.0}/README.md +17 -9
- {oda_data-2.4.2 → oda_data-2.6.0}/pyproject.toml +12 -3
- oda_data-2.6.0/src/oda_data/__init__.py +113 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/api/sources.py +105 -87
- oda_data-2.6.0/src/oda_data/cache/__init__.py +38 -0
- oda_data-2.6.0/src/oda_data/cache/_migrate.py +388 -0
- oda_data-2.6.0/src/oda_data/cache/api.py +513 -0
- oda_data-2.6.0/src/oda_data/cache/config.py +114 -0
- oda_data-2.6.0/src/oda_data/cache/types.py +70 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/common.py +1 -1
- oda_data-2.6.0/src/oda_data/config.py +34 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/provider_groupings.json +2 -1
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/cache.py +232 -68
- oda_data-2.4.2/src/oda_data/__init__.py +0 -59
- oda_data-2.4.2/src/oda_data/config.py +0 -17
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/api/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/api/constants.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/api/oecd.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/api/representations.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/channels.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/crs_channel_mapping.csv +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/dtypes.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/schema.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/clean_data/validation.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/common.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/common.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/crs_functions.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/crs_indicators.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/oecd.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/crs/one.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/common.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/dac1_functions.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/dac1_indicators.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/oecd.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac1/one.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/common.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/dac2a_functions.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/dac2a_indicators.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/oecd.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/dac2a/one.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/indicator.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/research/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/research/eu.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/research/policy_markers.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/indicators/research/sector_imputations.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/logger.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/oda_data_indicators.csv +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/crs_flow_types.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/crs_modalities.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/crs_policy_markers.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/crs_purpose.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/crs_type_of_finance.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/dac1_aid_flow_types.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/dac2a_aid_type.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/flow_types.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/indicators.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/key_columns.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/recipient_groupings.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/settings/v1_indicators_mapping.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/compatibility.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/gni.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/groupings.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/__init__.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/add.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/create_mapping.py +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/crs_names.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/dac1_names.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/names/dac2a_names.json +0 -0
- {oda_data-2.4.2 → oda_data-2.6.0}/src/oda_data/tools/sector_lists.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oda-data
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: A python package to work with Official Development Assistance data from the OECD DAC.
|
|
5
5
|
Author: Jorge Rivera, Miguel Haro Ruiz
|
|
6
6
|
Author-email: Jorge Rivera <jorge.rivera@one.org>, Miguel Haro Ruiz <miguel.haroruiz@one.org>
|
|
@@ -18,7 +18,8 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Dist: cachetools>=6.2.0
|
|
20
20
|
Requires-Dist: filelock>=3.20.0
|
|
21
|
-
Requires-Dist: oda-reader>=1.
|
|
21
|
+
Requires-Dist: oda-reader>=1.6.0
|
|
22
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
22
23
|
Requires-Dist: pandas[excel,parquet,performance]>=2.2.0
|
|
23
24
|
Requires-Dist: pydeflate>=2.3.3
|
|
24
25
|
Requires-Dist: reader>=3.18
|
|
@@ -448,21 +449,29 @@ dac_members = providers["dac_members"]
|
|
|
448
449
|
|
|
449
450
|
### Cache Management
|
|
450
451
|
|
|
451
|
-
|
|
452
|
+
`oda_data` stores downloaded data in a per-OS, version-segmented directory
|
|
453
|
+
(`~/Library/Caches/oda-data/<version>/` on macOS, `~/.cache/oda-data/<version>/`
|
|
454
|
+
on Linux, and the equivalent `AppData\Local` path on Windows). The location can be
|
|
455
|
+
overridden with `oda_data.set_cache_root(path)` or the `ODA_DATA_CACHE_DIR`
|
|
456
|
+
environment variable. See [CACHING.md](CACHING.md) for full details on disk
|
|
457
|
+
locations, scopes, multi-process behavior, and migration from older versions.
|
|
452
458
|
|
|
453
|
-
|
|
454
|
-
from oda_data import clear_cache, disable_cache, enable_cache
|
|
459
|
+
The fastest recovery path for a corrupt or stale cache is:
|
|
455
460
|
|
|
456
|
-
|
|
457
|
-
|
|
461
|
+
```python
|
|
462
|
+
from oda_data import cache
|
|
458
463
|
|
|
459
|
-
#
|
|
460
|
-
|
|
464
|
+
# Clear only the raw zip archives and let them re-download on next read:
|
|
465
|
+
cache.clear("raw")
|
|
461
466
|
|
|
462
|
-
#
|
|
463
|
-
|
|
467
|
+
# Or wipe everything:
|
|
468
|
+
cache.clear("all")
|
|
464
469
|
```
|
|
465
470
|
|
|
471
|
+
The legacy helpers `clear_cache()`, `disable_cache()`, and `enable_cache()` continue
|
|
472
|
+
to work and are thin wrappers over `cache.clear("all")`, `cache.disable_cache("all")`,
|
|
473
|
+
and `cache.enable_cache("all")` respectively.
|
|
474
|
+
|
|
466
475
|
### Compatibility with Version 1.x
|
|
467
476
|
|
|
468
477
|
For users migrating from v1.x, a compatibility layer is available:
|
|
@@ -417,21 +417,29 @@ dac_members = providers["dac_members"]
|
|
|
417
417
|
|
|
418
418
|
### Cache Management
|
|
419
419
|
|
|
420
|
-
|
|
420
|
+
`oda_data` stores downloaded data in a per-OS, version-segmented directory
|
|
421
|
+
(`~/Library/Caches/oda-data/<version>/` on macOS, `~/.cache/oda-data/<version>/`
|
|
422
|
+
on Linux, and the equivalent `AppData\Local` path on Windows). The location can be
|
|
423
|
+
overridden with `oda_data.set_cache_root(path)` or the `ODA_DATA_CACHE_DIR`
|
|
424
|
+
environment variable. See [CACHING.md](CACHING.md) for full details on disk
|
|
425
|
+
locations, scopes, multi-process behavior, and migration from older versions.
|
|
421
426
|
|
|
422
|
-
|
|
423
|
-
from oda_data import clear_cache, disable_cache, enable_cache
|
|
427
|
+
The fastest recovery path for a corrupt or stale cache is:
|
|
424
428
|
|
|
425
|
-
|
|
426
|
-
|
|
429
|
+
```python
|
|
430
|
+
from oda_data import cache
|
|
427
431
|
|
|
428
|
-
#
|
|
429
|
-
|
|
432
|
+
# Clear only the raw zip archives and let them re-download on next read:
|
|
433
|
+
cache.clear("raw")
|
|
430
434
|
|
|
431
|
-
#
|
|
432
|
-
|
|
435
|
+
# Or wipe everything:
|
|
436
|
+
cache.clear("all")
|
|
433
437
|
```
|
|
434
438
|
|
|
439
|
+
The legacy helpers `clear_cache()`, `disable_cache()`, and `enable_cache()` continue
|
|
440
|
+
to work and are thin wrappers over `cache.clear("all")`, `cache.disable_cache("all")`,
|
|
441
|
+
and `cache.enable_cache("all")` respectively.
|
|
442
|
+
|
|
435
443
|
### Compatibility with Version 1.x
|
|
436
444
|
|
|
437
445
|
For users migrating from v1.x, a compatibility layer is available:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oda_data"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.6.0"
|
|
4
4
|
description = "A python package to work with Official Development Assistance data from the OECD DAC."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
|
|
@@ -28,7 +28,8 @@ classifiers = [
|
|
|
28
28
|
dependencies = [
|
|
29
29
|
"cachetools>=6.2.0",
|
|
30
30
|
"filelock>=3.20.0",
|
|
31
|
-
"oda-reader>=1.
|
|
31
|
+
"oda-reader>=1.6.0",
|
|
32
|
+
"platformdirs>=4.0.0",
|
|
32
33
|
"pandas[excel,parquet,performance]>=2.2.0",
|
|
33
34
|
"pydeflate>=2.3.3",
|
|
34
35
|
"reader>=3.18",
|
|
@@ -109,11 +110,16 @@ unfixable = []
|
|
|
109
110
|
|
|
110
111
|
[tool.ruff.lint.per-file-ignores]
|
|
111
112
|
# Allow test files to have longer lines and more assertions
|
|
112
|
-
"tests/**/*.py" = ["E501", "PLR2004", "RUF002", "RUF003"] # Allow × in comments/docstrings
|
|
113
|
+
"tests/**/*.py" = ["E501", "PLR2004", "RUF002", "RUF003", "PLW0603"] # Allow × in comments/docstrings; PLW0603 for spawn-pool worker globals
|
|
113
114
|
# Allow global variable usage in __init__.py (architectural decision)
|
|
114
115
|
"src/oda_data/__init__.py" = ["PLW0603", "PLW0602", "F811"]
|
|
116
|
+
# Lazy module-level state (override flag, chained-root cache, auto-migrate gate, 5GB warning latch).
|
|
117
|
+
"src/oda_data/cache/config.py" = ["PLW0603"]
|
|
118
|
+
"src/oda_data/cache/api.py" = ["PLW0603"]
|
|
115
119
|
# Allow tmpdir reassignment pattern (Path conversion)
|
|
116
120
|
"src/oda_data/tools/cache.py" = ["PLW2901"]
|
|
121
|
+
# Existing complexity in OECDClient._apply_filters — out of scope for refactor here.
|
|
122
|
+
"src/oda_data/api/oecd.py" = ["PLR0912"]
|
|
117
123
|
# Allow loop variable binding patterns (intentional for lambda closures)
|
|
118
124
|
"src/oda_data/indicators/crs/crs_functions.py" = ["B023"]
|
|
119
125
|
"src/oda_data/indicators/research/sector_imputations.py" = ["B023"]
|
|
@@ -132,3 +138,6 @@ skip-magic-trailing-comma = false
|
|
|
132
138
|
|
|
133
139
|
# Like Black, automatically detect line endings
|
|
134
140
|
line-ending = "auto"
|
|
141
|
+
|
|
142
|
+
# Note: pytest configuration lives in `pytest.ini` (which takes precedence over
|
|
143
|
+
# this file). Markers and addopts must be added there, not here.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from oda_data import cache
|
|
4
|
+
from oda_data.api.oecd import OECDClient
|
|
5
|
+
from oda_data.api.sources import (
|
|
6
|
+
AidDataData,
|
|
7
|
+
CRSData,
|
|
8
|
+
DAC1Data,
|
|
9
|
+
DAC2AData,
|
|
10
|
+
MultiSystemData,
|
|
11
|
+
)
|
|
12
|
+
from oda_data.cache.config import set_cache_root
|
|
13
|
+
from oda_data.indicators.research import sector_imputations
|
|
14
|
+
from oda_data.indicators.research.policy_markers import bilateral_policy_marker
|
|
15
|
+
from oda_data.tools.compatibility import ODAData
|
|
16
|
+
from oda_data.tools.gni import add_gni_share_column
|
|
17
|
+
from oda_data.tools.groupings import provider_groupings, recipient_groupings
|
|
18
|
+
from oda_data.tools.names.add import add_names_columns
|
|
19
|
+
from oda_data.tools.sector_lists import add_broad_sectors, add_sectors
|
|
20
|
+
|
|
21
|
+
_DEPRECATION_WARNED_SET_DATA_PATH = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clear_cache() -> None:
|
|
25
|
+
"""Back-compat alias for oda_data.cache.clear('all').
|
|
26
|
+
|
|
27
|
+
Clears all scopes (bulk, query, http, raw). Prefer ``oda_data.cache.clear()``.
|
|
28
|
+
"""
|
|
29
|
+
from oda_data.cache import clear
|
|
30
|
+
|
|
31
|
+
clear("all")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def disable_cache() -> None:
|
|
35
|
+
"""Back-compat alias for oda_data.cache.disable_cache('all').
|
|
36
|
+
|
|
37
|
+
Disables all cache scopes. Prefer ``oda_data.cache.disable_cache()``.
|
|
38
|
+
"""
|
|
39
|
+
from oda_data.cache import disable_cache as _disable
|
|
40
|
+
|
|
41
|
+
_disable("all")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def enable_cache() -> None:
|
|
45
|
+
"""Back-compat alias for oda_data.cache.enable_cache('all').
|
|
46
|
+
|
|
47
|
+
Enables all cache scopes. Prefer ``oda_data.cache.enable_cache()``.
|
|
48
|
+
"""
|
|
49
|
+
from oda_data.cache import enable_cache as _enable
|
|
50
|
+
|
|
51
|
+
_enable("all")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_data_path(path) -> None:
|
|
55
|
+
"""Set the path for data outputs (parquet exports).
|
|
56
|
+
|
|
57
|
+
Deprecated: cache location is now controlled via set_cache_root() or
|
|
58
|
+
the ODA_DATA_CACHE_DIR env var. A one-time DeprecationWarning is emitted
|
|
59
|
+
on first call. Shim preserved through 2.x, removed in 3.0.
|
|
60
|
+
"""
|
|
61
|
+
global _DEPRECATION_WARNED_SET_DATA_PATH
|
|
62
|
+
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
from pydeflate import set_pydeflate_path
|
|
66
|
+
|
|
67
|
+
from oda_data.config import ODAPaths
|
|
68
|
+
|
|
69
|
+
if not _DEPRECATION_WARNED_SET_DATA_PATH:
|
|
70
|
+
warnings.warn(
|
|
71
|
+
"oda_data.set_data_path() now governs only data outputs (parquet exports),"
|
|
72
|
+
" not the cache. If you previously called set_data_path() to control where"
|
|
73
|
+
" downloads are cached, also call"
|
|
74
|
+
" oda_data.set_cache_root('<your_path>') or set the ODA_DATA_CACHE_DIR env"
|
|
75
|
+
" var. The set_data_path() shim itself is preserved through oda_data 2.x"
|
|
76
|
+
" and removed in 3.0.",
|
|
77
|
+
DeprecationWarning,
|
|
78
|
+
stacklevel=2,
|
|
79
|
+
)
|
|
80
|
+
_DEPRECATION_WARNED_SET_DATA_PATH = True
|
|
81
|
+
|
|
82
|
+
ODAPaths.raw_data = Path(path).resolve()
|
|
83
|
+
# Create on explicit user request; avoids import-time side effects.
|
|
84
|
+
ODAPaths.raw_data.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
ODAPaths.pydeflate = ODAPaths.raw_data / ".pydeflate"
|
|
86
|
+
set_pydeflate_path(ODAPaths.raw_data)
|
|
87
|
+
# Do NOT call set_cache_dir here — cache chaining is handled lazily by
|
|
88
|
+
# oda_data_cache_root() inside cache/config.py.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"AidDataData",
|
|
93
|
+
"CRSData",
|
|
94
|
+
"DAC1Data",
|
|
95
|
+
"DAC2AData",
|
|
96
|
+
"MultiSystemData",
|
|
97
|
+
"ODAData",
|
|
98
|
+
"OECDClient",
|
|
99
|
+
"add_broad_sectors",
|
|
100
|
+
"add_gni_share_column",
|
|
101
|
+
"add_names_columns",
|
|
102
|
+
"add_sectors",
|
|
103
|
+
"bilateral_policy_marker",
|
|
104
|
+
"cache",
|
|
105
|
+
"clear_cache",
|
|
106
|
+
"disable_cache",
|
|
107
|
+
"enable_cache",
|
|
108
|
+
"provider_groupings",
|
|
109
|
+
"recipient_groupings",
|
|
110
|
+
"sector_imputations",
|
|
111
|
+
"set_cache_root",
|
|
112
|
+
"set_data_path",
|
|
113
|
+
]
|
|
@@ -123,41 +123,37 @@ class Source:
|
|
|
123
123
|
|
|
124
124
|
@property
|
|
125
125
|
def bulk_cache(self) -> BulkCacheManager:
|
|
126
|
-
"""Get bulk cache manager singleton.
|
|
127
|
-
|
|
128
|
-
Creates one bulk cache per process, recreating if data directory changes.
|
|
129
|
-
"""
|
|
130
|
-
current_raw = Path(ODAPaths.raw_data).resolve()
|
|
131
|
-
# Fast-path without lock
|
|
132
|
-
bc = Source._shared_bulk_cache
|
|
133
|
-
if bc is not None and bc.base_dir.parent.resolve() == current_raw:
|
|
134
|
-
return bc
|
|
135
|
-
|
|
136
|
-
# Slow-path with lock
|
|
137
|
-
with Source._cache_lock:
|
|
138
|
-
bc = Source._shared_bulk_cache
|
|
139
|
-
if bc is None or bc.base_dir.parent.resolve() != current_raw:
|
|
140
|
-
Source._shared_bulk_cache = BulkCacheManager(current_raw)
|
|
141
|
-
return Source._shared_bulk_cache
|
|
126
|
+
"""Get bulk cache manager singleton, recreating if the cache root changed."""
|
|
127
|
+
return self._shared_manager(BulkCacheManager, "_shared_bulk_cache")
|
|
142
128
|
|
|
143
129
|
@property
|
|
144
130
|
def query_cache(self) -> QueryCacheManager:
|
|
145
|
-
"""Get query cache manager singleton.
|
|
131
|
+
"""Get query cache manager singleton, recreating if the cache root changed."""
|
|
132
|
+
return self._shared_manager(QueryCacheManager, "_shared_query_cache")
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _shared_manager(cls, manager_cls, slot: str):
|
|
136
|
+
"""Return a process-wide manager rooted at ``cache_root / "oda-data"``.
|
|
146
137
|
|
|
147
|
-
|
|
138
|
+
The singleton is intentionally anchored on ``Source`` (not on the
|
|
139
|
+
runtime subclass) so that every subclass shares the same instance.
|
|
140
|
+
|
|
141
|
+
The unresolved equality check on the fast path avoids a ``realpath``
|
|
142
|
+
syscall on every property access; we only ``.resolve()`` when the
|
|
143
|
+
configured root visibly changed.
|
|
148
144
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Source
|
|
160
|
-
return Source
|
|
145
|
+
current = Path(ODAPaths.cache_root) / "oda-data"
|
|
146
|
+
existing = getattr(Source, slot)
|
|
147
|
+
if existing is not None and existing.base_dir.parent == current:
|
|
148
|
+
return existing
|
|
149
|
+
with cls._cache_lock:
|
|
150
|
+
existing = getattr(Source, slot)
|
|
151
|
+
if (
|
|
152
|
+
existing is None
|
|
153
|
+
or existing.base_dir.parent.resolve() != current.resolve()
|
|
154
|
+
):
|
|
155
|
+
setattr(Source, slot, manager_cls(current))
|
|
156
|
+
return getattr(Source, slot)
|
|
161
157
|
|
|
162
158
|
def _add_filter(self, column: str, predicate: str, value: str | int | list) -> None:
|
|
163
159
|
"""Adds a filter to the dataset, ensuring no duplicate columns.
|
|
@@ -348,14 +344,23 @@ class DACSource(Source):
|
|
|
348
344
|
return "unknown"
|
|
349
345
|
|
|
350
346
|
def _fetch_from_bulk_cache(
|
|
351
|
-
self,
|
|
347
|
+
self,
|
|
348
|
+
filters: list[tuple] | None,
|
|
349
|
+
columns: list | None,
|
|
350
|
+
*,
|
|
351
|
+
refresh: bool = False,
|
|
352
352
|
) -> pd.DataFrame:
|
|
353
353
|
"""Fetch data from bulk cache with download coordination.
|
|
354
354
|
|
|
355
|
-
This method uses FileLock via BulkCacheManager
|
|
355
|
+
This method uses FileLock via BulkCacheManager — if another thread is
|
|
356
356
|
downloading, this call blocks until download completes.
|
|
357
357
|
|
|
358
358
|
Uses PyArrow predicate pushdown and column projection for maximum efficiency.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
filters: Optional parquet predicate filters.
|
|
362
|
+
columns: Optional column subset.
|
|
363
|
+
refresh: If True, bypass the bulk cache and re-download.
|
|
359
364
|
"""
|
|
360
365
|
entry = BulkCacheEntry(
|
|
361
366
|
key=f"{self.__class__.__name__}_bulk",
|
|
@@ -365,7 +370,7 @@ class DACSource(Source):
|
|
|
365
370
|
)
|
|
366
371
|
|
|
367
372
|
# This blocks if another thread is downloading (prevents concurrent waste)
|
|
368
|
-
bulk_path = self.bulk_cache.ensure(entry, refresh=
|
|
373
|
+
bulk_path = self.bulk_cache.ensure(entry, refresh=refresh)
|
|
369
374
|
|
|
370
375
|
pyarrow_filters = filters if filters else None
|
|
371
376
|
|
|
@@ -396,6 +401,8 @@ class DACSource(Source):
|
|
|
396
401
|
using_bulk_download: bool = False,
|
|
397
402
|
additional_filters: list[tuple] | None = None,
|
|
398
403
|
columns: list | None = None,
|
|
404
|
+
*,
|
|
405
|
+
refresh: bool = False,
|
|
399
406
|
):
|
|
400
407
|
"""Reads a dataset with 3-tier cache coordination.
|
|
401
408
|
|
|
@@ -408,6 +415,8 @@ class DACSource(Source):
|
|
|
408
415
|
using_bulk_download: Whether to read from bulk cache
|
|
409
416
|
additional_filters: Additional filters to apply
|
|
410
417
|
columns: Subset of columns to read
|
|
418
|
+
refresh: If True, bypass the bulk cache and re-download. Only has
|
|
419
|
+
effect when ``using_bulk_download=True``.
|
|
411
420
|
|
|
412
421
|
Returns:
|
|
413
422
|
Filtered and cleaned DataFrame
|
|
@@ -416,45 +425,49 @@ class DACSource(Source):
|
|
|
416
425
|
filters = self._get_read_filters(additional_filters=additional_filters)
|
|
417
426
|
param_hash = generate_param_hash(filters if filters else [])
|
|
418
427
|
|
|
419
|
-
#
|
|
420
|
-
|
|
421
|
-
|
|
428
|
+
# When refresh=True, skip the upper cache tiers entirely so the bulk
|
|
429
|
+
# fetcher's refresh=True actually re-downloads and the upper tiers see
|
|
430
|
+
# the fresh data. Without this guard a warm memory/query cache would
|
|
431
|
+
# short-circuit the refresh, returning stale data.
|
|
432
|
+
if not refresh:
|
|
433
|
+
# 1. Try memory cache (thread-safe, fastest)
|
|
434
|
+
df = self.memory_cache.get(param_hash)
|
|
422
435
|
# Only use if all requested columns are available
|
|
423
|
-
if not
|
|
436
|
+
if df is not None and (
|
|
437
|
+
not columns or all(c in df.columns for c in columns)
|
|
438
|
+
):
|
|
424
439
|
logger.info("Cache hit: memory")
|
|
425
|
-
return self._apply_columns_and_clean(
|
|
426
|
-
df, columns, already_cleaned=True
|
|
427
|
-
)
|
|
440
|
+
return self._apply_columns_and_clean(df, columns, already_cleaned=True)
|
|
428
441
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
442
|
+
# For projected reads, also check a projection-specific memory cache
|
|
443
|
+
if columns:
|
|
444
|
+
proj_hash = generate_projection_hash(param_hash, columns)
|
|
445
|
+
df = self.memory_cache.get(proj_hash)
|
|
446
|
+
if df is not None:
|
|
447
|
+
logger.info("Cache hit: memory (projection)")
|
|
448
|
+
return df
|
|
449
|
+
|
|
450
|
+
# 2. Try query cache (on-disk parquet, fast)
|
|
451
|
+
df = self.query_cache.load(
|
|
452
|
+
self.__class__.__name__, param_hash, None, columns
|
|
453
|
+
)
|
|
433
454
|
if df is not None:
|
|
434
|
-
logger.info("Cache hit:
|
|
455
|
+
logger.info("Cache hit: query cache")
|
|
456
|
+
# Only cache in memory if we loaded full data (no column selection)
|
|
457
|
+
# This way memory cache is reusable for queries with different column subsets
|
|
458
|
+
if not columns:
|
|
459
|
+
self._cache_in_memory(param_hash, df)
|
|
460
|
+
# Data is already cleaned and column-selected at parquet read level
|
|
435
461
|
return df
|
|
436
462
|
|
|
437
|
-
# 2. Try query cache (on-disk parquet, fast)
|
|
438
|
-
df = self.query_cache.load(self.__class__.__name__, param_hash, None, columns)
|
|
439
|
-
if df is not None:
|
|
440
|
-
logger.info("Cache hit: query cache")
|
|
441
|
-
# Only cache in memory if we loaded full data (no column selection)
|
|
442
|
-
# This way memory cache is reusable for queries with different column subsets
|
|
443
|
-
if not columns:
|
|
444
|
-
self._cache_in_memory(param_hash, df)
|
|
445
|
-
# Data is already cleaned and column-selected at parquet read level
|
|
446
|
-
return df
|
|
447
|
-
|
|
448
463
|
# 3. Cache miss (query/memory) - need to fetch data
|
|
449
464
|
if using_bulk_download:
|
|
450
465
|
logger.info("Query cache miss - reading from bulk cache")
|
|
451
|
-
df = self._fetch_from_bulk_cache(filters, columns)
|
|
466
|
+
df = self._fetch_from_bulk_cache(filters, columns, refresh=refresh)
|
|
452
467
|
if columns:
|
|
453
468
|
# Cache projected result under a projection-specific key
|
|
454
469
|
# so it doesn't pollute the shared filter-only cache
|
|
455
|
-
self._cache_in_memory(
|
|
456
|
-
generate_projection_hash(param_hash, columns), df
|
|
457
|
-
)
|
|
470
|
+
self._cache_in_memory(generate_projection_hash(param_hash, columns), df)
|
|
458
471
|
return df
|
|
459
472
|
else:
|
|
460
473
|
# Download via API (uses init-time filters only)
|
|
@@ -802,12 +815,15 @@ class AidDataData(AidDataSource):
|
|
|
802
815
|
self,
|
|
803
816
|
additional_filters: list[tuple] | None = None,
|
|
804
817
|
columns: list | None = None,
|
|
818
|
+
*,
|
|
819
|
+
refresh: bool = False,
|
|
805
820
|
):
|
|
806
821
|
"""Reads AidData from bulk cache with filtering.
|
|
807
822
|
|
|
808
823
|
Args:
|
|
809
824
|
additional_filters: Additional filters to apply
|
|
810
825
|
columns: Subset of columns to read
|
|
826
|
+
refresh: If True, bypass the bulk cache and re-download.
|
|
811
827
|
|
|
812
828
|
Returns:
|
|
813
829
|
Filtered DataFrame
|
|
@@ -816,35 +832,39 @@ class AidDataData(AidDataSource):
|
|
|
816
832
|
filters = self._get_read_filters(additional_filters=additional_filters)
|
|
817
833
|
param_hash = generate_param_hash(filters if filters else [])
|
|
818
834
|
|
|
819
|
-
#
|
|
820
|
-
|
|
821
|
-
if
|
|
835
|
+
# When refresh=True, skip the upper cache tiers so the bulk fetcher's
|
|
836
|
+
# refresh=True actually re-downloads. See DACSource.read for context.
|
|
837
|
+
if not refresh:
|
|
838
|
+
# 1. Try memory cache
|
|
839
|
+
df = self.memory_cache.get(param_hash)
|
|
822
840
|
# Only use if all requested columns are available
|
|
823
|
-
if not
|
|
841
|
+
if df is not None and (
|
|
842
|
+
not columns or all(c in df.columns for c in columns)
|
|
843
|
+
):
|
|
824
844
|
logger.info("Cache hit: memory")
|
|
825
|
-
return self._apply_columns_and_clean(
|
|
826
|
-
df, columns, already_cleaned=True
|
|
827
|
-
)
|
|
845
|
+
return self._apply_columns_and_clean(df, columns, already_cleaned=True)
|
|
828
846
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
847
|
+
# For projected reads, also check a projection-specific memory cache
|
|
848
|
+
if columns:
|
|
849
|
+
proj_hash = generate_projection_hash(param_hash, columns)
|
|
850
|
+
df = self.memory_cache.get(proj_hash)
|
|
851
|
+
if df is not None:
|
|
852
|
+
logger.info("Cache hit: memory (projection)")
|
|
853
|
+
return df
|
|
854
|
+
|
|
855
|
+
# 2. Try query cache
|
|
856
|
+
# Use column selection at parquet read level for efficiency
|
|
857
|
+
df = self.query_cache.load(
|
|
858
|
+
self.__class__.__name__, param_hash, None, columns
|
|
859
|
+
)
|
|
833
860
|
if df is not None:
|
|
834
|
-
logger.info("Cache hit:
|
|
861
|
+
logger.info("Cache hit: query cache")
|
|
862
|
+
# Only cache in memory if we loaded full data (no column selection)
|
|
863
|
+
if not columns:
|
|
864
|
+
self._cache_in_memory(param_hash, df)
|
|
865
|
+
# Data is already cleaned and column-selected at parquet read level
|
|
835
866
|
return df
|
|
836
867
|
|
|
837
|
-
# 2. Try query cache
|
|
838
|
-
# Use column selection at parquet read level for efficiency
|
|
839
|
-
df = self.query_cache.load(self.__class__.__name__, param_hash, None, columns)
|
|
840
|
-
if df is not None:
|
|
841
|
-
logger.info("Cache hit: query cache")
|
|
842
|
-
# Only cache in memory if we loaded full data (no column selection)
|
|
843
|
-
if not columns:
|
|
844
|
-
self._cache_in_memory(param_hash, df)
|
|
845
|
-
# Data is already cleaned and column-selected at parquet read level
|
|
846
|
-
return df
|
|
847
|
-
|
|
848
868
|
# 3. Fetch from bulk cache (always bulk for AidData)
|
|
849
869
|
entry = BulkCacheEntry(
|
|
850
870
|
key=f"{self.__class__.__name__}_bulk",
|
|
@@ -853,7 +873,7 @@ class AidDataData(AidDataSource):
|
|
|
853
873
|
version=self._get_package_version(),
|
|
854
874
|
)
|
|
855
875
|
|
|
856
|
-
bulk_path = self.bulk_cache.ensure(entry, refresh=
|
|
876
|
+
bulk_path = self.bulk_cache.ensure(entry, refresh=refresh)
|
|
857
877
|
|
|
858
878
|
# Use PyArrow predicate pushdown and column selection for maximum efficiency
|
|
859
879
|
pyarrow_filters = filters if filters else None
|
|
@@ -868,9 +888,7 @@ class AidDataData(AidDataSource):
|
|
|
868
888
|
)
|
|
869
889
|
# Cache under projection-specific key to avoid polluting
|
|
870
890
|
# the shared filter-only cache
|
|
871
|
-
self._cache_in_memory(
|
|
872
|
-
generate_projection_hash(param_hash, columns), df
|
|
873
|
-
)
|
|
891
|
+
self._cache_in_memory(generate_projection_hash(param_hash, columns), df)
|
|
874
892
|
return df
|
|
875
893
|
else:
|
|
876
894
|
logger.info("Query cache miss - reading from bulk cache")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""oda_data.cache — typed cache management surface.
|
|
2
|
+
|
|
3
|
+
Public symbols re-exported from sub-modules:
|
|
4
|
+
|
|
5
|
+
- ``path``, ``entries``, ``clear``, ``size``, ``invalidate``,
|
|
6
|
+
``enable_cache``, ``disable_cache`` — from :mod:`oda_data.cache.api`
|
|
7
|
+
- ``migrate`` — from :mod:`oda_data.cache._migrate`
|
|
8
|
+
- ``set_cache_root`` — from :mod:`oda_data.cache.config`
|
|
9
|
+
- ``CacheRecord``, ``MigrationResult``, ``Scope`` — from :mod:`oda_data.cache.types`
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from oda_data.cache._migrate import migrate
|
|
13
|
+
from oda_data.cache.api import (
|
|
14
|
+
clear,
|
|
15
|
+
disable_cache,
|
|
16
|
+
enable_cache,
|
|
17
|
+
entries,
|
|
18
|
+
invalidate,
|
|
19
|
+
path,
|
|
20
|
+
size,
|
|
21
|
+
)
|
|
22
|
+
from oda_data.cache.config import set_cache_root
|
|
23
|
+
from oda_data.cache.types import CacheRecord, MigrationResult, Scope
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CacheRecord",
|
|
27
|
+
"MigrationResult",
|
|
28
|
+
"Scope",
|
|
29
|
+
"clear",
|
|
30
|
+
"disable_cache",
|
|
31
|
+
"enable_cache",
|
|
32
|
+
"entries",
|
|
33
|
+
"invalidate",
|
|
34
|
+
"migrate",
|
|
35
|
+
"path",
|
|
36
|
+
"set_cache_root",
|
|
37
|
+
"size",
|
|
38
|
+
]
|