oda-data 2.7.0__tar.gz → 2.8.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.7.0 → oda_data-2.8.0}/PKG-INFO +2 -3
- oda_data-2.8.0/pyproject.toml +226 -0
- oda_data-2.7.0/pyproject.toml → oda_data-2.8.0/pyproject.toml.orig +17 -3
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/api/sources.py +106 -3
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/cache/__init__.py +7 -3
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/cache/api.py +122 -14
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/cache/config.py +10 -2
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/cache/types.py +24 -0
- oda_data-2.8.0/src/oda_data/clean_data/channel_share_proxies.csv +9 -0
- oda_data-2.8.0/src/oda_data/clean_data/channels.py +346 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/clean_data/common.py +8 -6
- oda_data-2.8.0/src/oda_data/clean_data/crs_channel_mapping.csv +447 -0
- oda_data-2.8.0/src/oda_data/clean_data/crs_channel_mapping_vintage.json +8 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/clean_data/dtypes.py +77 -2
- oda_data-2.8.0/src/oda_data/clean_data/multilateral_channel_crosswalk.csv +155 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/config.py +19 -1
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/crs_functions.py +15 -36
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/one.py +5 -6
- oda_data-2.8.0/src/oda_data/indicators/research/imputation_quality.py +202 -0
- oda_data-2.8.0/src/oda_data/indicators/research/imputation_shares.py +707 -0
- oda_data-2.8.0/src/oda_data/indicators/research/sector_imputations.py +748 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/provider_groupings.json +2 -1
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/cache.py +105 -0
- oda_data-2.7.0/src/oda_data/clean_data/channels.py +0 -640
- oda_data-2.7.0/src/oda_data/clean_data/crs_channel_mapping.csv +0 -306
- oda_data-2.7.0/src/oda_data/indicators/research/sector_imputations.py +0 -376
- {oda_data-2.7.0 → oda_data-2.8.0}/README.md +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/api/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/api/constants.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/api/oecd.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/api/representations.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/cache/_migrate.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/clean_data/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/clean_data/schema.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/clean_data/validation.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/common.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/common.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/crs_indicators.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/crs/oecd.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/common.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/dac1_functions.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/dac1_indicators.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/oecd.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac1/one.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/common.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/dac2a_functions.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/dac2a_indicators.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/oecd.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/dac2a/one.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/indicator.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/research/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/research/eu.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/indicators/research/policy_markers.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/logger.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/oda_data_indicators.csv +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/crs_flow_types.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/crs_modalities.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/crs_policy_markers.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/crs_purpose.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/crs_type_of_finance.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/dac1_aid_flow_types.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/dac2a_aid_type.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/flow_types.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/indicators.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/key_columns.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/recipient_groupings.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/settings/v1_indicators_mapping.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/compatibility.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/gni.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/groupings.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/__init__.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/add.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/create_mapping.py +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/crs_names.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/dac1_names.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.0}/src/oda_data/tools/names/dac2a_names.json +0 -0
- {oda_data-2.7.0 → oda_data-2.8.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.8.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,12 +18,11 @@ 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.9.0
|
|
22
22
|
Requires-Dist: platformdirs>=4.0.0
|
|
23
23
|
Requires-Dist: pandas[excel,parquet,performance]>=2.2.0
|
|
24
24
|
Requires-Dist: pydeflate>=2.3.3
|
|
25
25
|
Requires-Dist: reader>=3.18
|
|
26
|
-
Requires-Dist: thefuzz>=0.22.1
|
|
27
26
|
Requires-Python: >=3.11
|
|
28
27
|
Project-URL: Homepage, https://github.com/ONEcampaign/oda_data_package
|
|
29
28
|
Project-URL: Repository, https://github.com/ONEcampaign/oda_data_package
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oda_data"
|
|
3
|
+
version = "2.8.0"
|
|
4
|
+
description = "A python package to work with Official Development Assistance data from the OECD DAC."
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Development Status :: 5 - Production/Stable",
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"Intended Audience :: Science/Research",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"cachetools>=6.2.0",
|
|
22
|
+
"filelock>=3.20.0",
|
|
23
|
+
"oda-reader>=1.9.0",
|
|
24
|
+
"platformdirs>=4.0.0",
|
|
25
|
+
"pandas[excel,parquet,performance]>=2.2.0",
|
|
26
|
+
"pydeflate>=2.3.3",
|
|
27
|
+
"reader>=3.18",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.license]
|
|
31
|
+
text = "MIT"
|
|
32
|
+
|
|
33
|
+
[[project.authors]]
|
|
34
|
+
name = "Jorge Rivera"
|
|
35
|
+
email = "jorge.rivera@one.org"
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Miguel Haro Ruiz"
|
|
39
|
+
email = "miguel.haroruiz@one.org"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/ONEcampaign/oda_data_package"
|
|
43
|
+
Repository = "https://github.com/ONEcampaign/oda_data_package"
|
|
44
|
+
Documentation = "https://github.com/ONEcampaign/oda_data_package"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["uv_build>=0.11.8,<0.12"]
|
|
48
|
+
build-backend = "uv_build"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"pre-commit>=4.5.0",
|
|
53
|
+
"ruff>=0.14.9",
|
|
54
|
+
"ty>=0.0.33",
|
|
55
|
+
"mkdocs-material>=9.7.0",
|
|
56
|
+
]
|
|
57
|
+
test = [
|
|
58
|
+
"pytest>=8.0.0",
|
|
59
|
+
"pytest-mock>=3.12.0",
|
|
60
|
+
"pytest-cov>=4.1.0",
|
|
61
|
+
"pytest-xdist>=3.5.0",
|
|
62
|
+
"freezegun>=1.4.0",
|
|
63
|
+
]
|
|
64
|
+
maintenance = [
|
|
65
|
+
"resolvekit==0.1.12 ; python_version >= '3.12'",
|
|
66
|
+
"pyyaml>=6.0 ; python_version >= '3.12'",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.uv]
|
|
70
|
+
default-groups = [
|
|
71
|
+
"dev",
|
|
72
|
+
"test",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 88
|
|
77
|
+
target-version = "py311"
|
|
78
|
+
extend-include = ["*.ipynb"]
|
|
79
|
+
exclude = [
|
|
80
|
+
".git",
|
|
81
|
+
".venv",
|
|
82
|
+
"venv",
|
|
83
|
+
"__pycache__",
|
|
84
|
+
"build",
|
|
85
|
+
"dist",
|
|
86
|
+
"*.egg-info",
|
|
87
|
+
".pytest_cache",
|
|
88
|
+
"htmlcov",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint]
|
|
92
|
+
select = [
|
|
93
|
+
"E",
|
|
94
|
+
"W",
|
|
95
|
+
"F",
|
|
96
|
+
"I",
|
|
97
|
+
"N",
|
|
98
|
+
"UP",
|
|
99
|
+
"B",
|
|
100
|
+
"C4",
|
|
101
|
+
"SIM",
|
|
102
|
+
"PL",
|
|
103
|
+
"RUF",
|
|
104
|
+
"ERA",
|
|
105
|
+
"PERF",
|
|
106
|
+
"ANN",
|
|
107
|
+
]
|
|
108
|
+
ignore = [
|
|
109
|
+
"E501",
|
|
110
|
+
"PLR0913",
|
|
111
|
+
"PLR0912",
|
|
112
|
+
"PLR0914",
|
|
113
|
+
"PLR0915",
|
|
114
|
+
"PLR2004",
|
|
115
|
+
"PLC0415",
|
|
116
|
+
"ANN401",
|
|
117
|
+
]
|
|
118
|
+
fixable = ["ALL"]
|
|
119
|
+
unfixable = []
|
|
120
|
+
|
|
121
|
+
[tool.ruff.lint.per-file-ignores]
|
|
122
|
+
"tests/**/*.py" = [
|
|
123
|
+
"ANN",
|
|
124
|
+
"E501",
|
|
125
|
+
"PLR2004",
|
|
126
|
+
"RUF002",
|
|
127
|
+
"RUF003",
|
|
128
|
+
"PLW0603",
|
|
129
|
+
"ERA001",
|
|
130
|
+
]
|
|
131
|
+
"src/oda_data/__init__.py" = [
|
|
132
|
+
"PLW0603",
|
|
133
|
+
"PLW0602",
|
|
134
|
+
"F811",
|
|
135
|
+
]
|
|
136
|
+
"src/oda_data/cache/config.py" = ["PLW0603"]
|
|
137
|
+
"src/oda_data/cache/api.py" = ["PLW0603"]
|
|
138
|
+
"src/oda_data/tools/cache.py" = ["PLW2901"]
|
|
139
|
+
"src/oda_data/api/oecd.py" = ["PLR0912"]
|
|
140
|
+
"src/oda_data/indicators/crs/crs_functions.py" = ["B023"]
|
|
141
|
+
"src/oda_data/indicators/research/sector_imputations.py" = ["B023"]
|
|
142
|
+
"src/oda_data/tools/sector_lists.py" = ["N816"]
|
|
143
|
+
|
|
144
|
+
[tool.ruff.lint.pydocstyle]
|
|
145
|
+
convention = "google"
|
|
146
|
+
|
|
147
|
+
[tool.ruff.format]
|
|
148
|
+
quote-style = "double"
|
|
149
|
+
indent-style = "space"
|
|
150
|
+
skip-magic-trailing-comma = false
|
|
151
|
+
line-ending = "auto"
|
|
152
|
+
|
|
153
|
+
[tool.ty.environment]
|
|
154
|
+
python-version = "3.11"
|
|
155
|
+
|
|
156
|
+
[tool.ty.src]
|
|
157
|
+
include = ["src/oda_data"]
|
|
158
|
+
|
|
159
|
+
[tool.ty.analysis]
|
|
160
|
+
replace-imports-with-any = [
|
|
161
|
+
"pandas",
|
|
162
|
+
"pandas.**",
|
|
163
|
+
"numpy",
|
|
164
|
+
"numpy.**",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
[tool.ty.rules]
|
|
168
|
+
possibly-missing-attribute = "ignore"
|
|
169
|
+
possibly-unresolved-reference = "ignore"
|
|
170
|
+
|
|
171
|
+
[tool.pytest.ini_options]
|
|
172
|
+
testpaths = ["tests"]
|
|
173
|
+
python_files = ["test_*.py"]
|
|
174
|
+
python_classes = ["Test*"]
|
|
175
|
+
python_functions = ["test_*"]
|
|
176
|
+
addopts = """
|
|
177
|
+
-v
|
|
178
|
+
--strict-markers
|
|
179
|
+
--tb=short
|
|
180
|
+
--cov=src/oda_data
|
|
181
|
+
--cov-report=term-missing
|
|
182
|
+
--cov-report=html
|
|
183
|
+
--cov-branch
|
|
184
|
+
-m "not integration and not network"
|
|
185
|
+
"""
|
|
186
|
+
markers = [
|
|
187
|
+
"""slow: marks tests as slow (deselect with '-m "not slow"')""",
|
|
188
|
+
"integration: marks tests that call real OECD API (deselected by default)",
|
|
189
|
+
"network: marks tests requiring live network access (opt-in via RUN_NETWORK_TESTS=1; deselected by default)",
|
|
190
|
+
"unit: marks tests as unit tests",
|
|
191
|
+
"cache: marks tests related to caching functionality",
|
|
192
|
+
"validation: marks tests related to input validation",
|
|
193
|
+
"cleaning: marks tests related to data cleaning",
|
|
194
|
+
]
|
|
195
|
+
norecursedirs = [
|
|
196
|
+
".git",
|
|
197
|
+
".venv",
|
|
198
|
+
"venv",
|
|
199
|
+
"__pycache__",
|
|
200
|
+
"*.egg-info",
|
|
201
|
+
".pytest_cache",
|
|
202
|
+
"htmlcov",
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
[tool.coverage.run]
|
|
206
|
+
source = ["src/oda_data"]
|
|
207
|
+
branch = true
|
|
208
|
+
omit = [
|
|
209
|
+
"*/tests/*",
|
|
210
|
+
"*/__init__.py",
|
|
211
|
+
"*/config.py",
|
|
212
|
+
"*/logger.py",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
[tool.coverage.report]
|
|
216
|
+
precision = 2
|
|
217
|
+
show_missing = true
|
|
218
|
+
exclude_lines = [
|
|
219
|
+
"pragma: no cover",
|
|
220
|
+
"def __repr__",
|
|
221
|
+
"raise AssertionError",
|
|
222
|
+
"raise NotImplementedError",
|
|
223
|
+
"if __name__ == .__main__.:",
|
|
224
|
+
"if TYPE_CHECKING:",
|
|
225
|
+
"@abstractmethod",
|
|
226
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oda_data"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.8.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,12 +28,11 @@ classifiers = [
|
|
|
28
28
|
dependencies = [
|
|
29
29
|
"cachetools>=6.2.0",
|
|
30
30
|
"filelock>=3.20.0",
|
|
31
|
-
"oda-reader>=1.
|
|
31
|
+
"oda-reader>=1.9.0",
|
|
32
32
|
"platformdirs>=4.0.0",
|
|
33
33
|
"pandas[excel,parquet,performance]>=2.2.0",
|
|
34
34
|
"pydeflate>=2.3.3",
|
|
35
35
|
"reader>=3.18",
|
|
36
|
-
"thefuzz>=0.22.1",
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
[project.urls]
|
|
@@ -59,6 +58,21 @@ test = [
|
|
|
59
58
|
"pytest-xdist>=3.5.0",
|
|
60
59
|
"freezegun>=1.4.0",
|
|
61
60
|
]
|
|
61
|
+
# Maintenance-only tooling for scripts/refresh_channel_crosswalk.py. Never a
|
|
62
|
+
# runtime dependency of the oda_data package; excluded from the default
|
|
63
|
+
# groups below so a plain `uv sync` never requires it (resolvekit needs
|
|
64
|
+
# Python >=3.12, oda_data supports >=3.11). Install explicitly with
|
|
65
|
+
# `uv sync --group maintenance`.
|
|
66
|
+
maintenance = [
|
|
67
|
+
"resolvekit==0.1.12 ; python_version >= '3.12'",
|
|
68
|
+
"pyyaml>=6.0 ; python_version >= '3.12'",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.uv]
|
|
72
|
+
# Preserve today's implicit default (dev + test both installed by a plain
|
|
73
|
+
# `uv sync` / `uv run`); without this, adding the "maintenance" group above
|
|
74
|
+
# would otherwise be pulled into that same default set.
|
|
75
|
+
default-groups = ["dev", "test"]
|
|
62
76
|
|
|
63
77
|
[tool.ruff]
|
|
64
78
|
# Set the maximum line length
|
|
@@ -308,6 +308,17 @@ class DACSource(Source):
|
|
|
308
308
|
)
|
|
309
309
|
self.de_sectors = check_strings(sectors)
|
|
310
310
|
|
|
311
|
+
def _extra_hash_components(self) -> list[tuple]:
|
|
312
|
+
"""Extra (name, predicate, value) tuples folded into the cache-key hash.
|
|
313
|
+
|
|
314
|
+
They bypass the read-filter path, never reaching a parquet predicate
|
|
315
|
+
or a query string. Folding them into the hash lets a constructor
|
|
316
|
+
flag that changes row-level behaviour, without being expressed as a
|
|
317
|
+
filter tuple, still produce a different cache key. Subclasses
|
|
318
|
+
override this; the default is a no-op.
|
|
319
|
+
"""
|
|
320
|
+
return []
|
|
321
|
+
|
|
311
322
|
def _get_filtered_download_filters(self) -> dict:
|
|
312
323
|
"""Generates a dictionary of filters for downloading the dataset.
|
|
313
324
|
|
|
@@ -358,6 +369,15 @@ class DACSource(Source):
|
|
|
358
369
|
except Exception:
|
|
359
370
|
return "unknown"
|
|
360
371
|
|
|
372
|
+
def _get_release_id(self) -> str | None:
|
|
373
|
+
"""Best-effort upstream release identity for the bulk cache manifest.
|
|
374
|
+
|
|
375
|
+
Overridden by subclasses that go through the OECD bulk file service
|
|
376
|
+
(CRSData, MultiSystemData) with a cheap, non-download probe. The
|
|
377
|
+
default is None, which falls back to version/TTL-only staleness.
|
|
378
|
+
"""
|
|
379
|
+
return None
|
|
380
|
+
|
|
361
381
|
def _fetch_from_bulk_cache(
|
|
362
382
|
self,
|
|
363
383
|
filters: list[tuple] | None,
|
|
@@ -382,6 +402,7 @@ class DACSource(Source):
|
|
|
382
402
|
fetcher=self._create_bulk_fetcher(),
|
|
383
403
|
ttl_days=30,
|
|
384
404
|
version=self._get_package_version(),
|
|
405
|
+
release_id=self._get_release_id(),
|
|
385
406
|
)
|
|
386
407
|
|
|
387
408
|
# This blocks if another thread is downloading (prevents concurrent waste)
|
|
@@ -438,12 +459,24 @@ class DACSource(Source):
|
|
|
438
459
|
"""
|
|
439
460
|
# Create filters and generate cache key
|
|
440
461
|
filters = self._get_read_filters(additional_filters=additional_filters)
|
|
441
|
-
param_hash = generate_param_hash(
|
|
462
|
+
param_hash = generate_param_hash(
|
|
463
|
+
(filters if filters else []) + self._extra_hash_components()
|
|
464
|
+
)
|
|
442
465
|
|
|
443
466
|
# When refresh=True, skip the upper cache tiers entirely so the bulk
|
|
444
467
|
# fetcher's refresh=True actually re-downloads and the upper tiers see
|
|
445
468
|
# the fresh data. Without this guard a warm memory/query cache would
|
|
446
|
-
# short-circuit the refresh, returning stale data.
|
|
469
|
+
# short-circuit the refresh, returning stale data. Also evict the
|
|
470
|
+
# memory-tier entries this call is about to overwrite up front,
|
|
471
|
+
# rather than relying solely on the post-fetch write — this closes
|
|
472
|
+
# the window in which a concurrent reader could still observe the
|
|
473
|
+
# stale value while the refresh is in flight (#162).
|
|
474
|
+
if refresh:
|
|
475
|
+
self.memory_cache.pop(param_hash, None)
|
|
476
|
+
if columns:
|
|
477
|
+
self.memory_cache.pop(
|
|
478
|
+
generate_projection_hash(param_hash, columns), None
|
|
479
|
+
)
|
|
447
480
|
if not refresh:
|
|
448
481
|
# 1. Try memory cache (thread-safe, fastest)
|
|
449
482
|
_cached = self.memory_cache.get(param_hash)
|
|
@@ -640,6 +673,7 @@ class CRSData(DACSource):
|
|
|
640
673
|
years: list[int] | range | int | None = None,
|
|
641
674
|
providers: list[int] | int | None = None,
|
|
642
675
|
recipients: list[int] | int | None = None,
|
|
676
|
+
exclude_multilateral_core: bool = True,
|
|
643
677
|
) -> None:
|
|
644
678
|
"""
|
|
645
679
|
Initialize CRSData.
|
|
@@ -648,9 +682,17 @@ class CRSData(DACSource):
|
|
|
648
682
|
years (list[int] | range): List or range of years to filter.
|
|
649
683
|
providers (Optional[list[int]]): List of provider codes.
|
|
650
684
|
recipients (Optional[list[int]]): List of recipient codes.
|
|
685
|
+
exclude_multilateral_core (bool): Whether to exclude CRS rows that report
|
|
686
|
+
a donor's core (unearmarked) contribution to a multilateral
|
|
687
|
+
organisation (``bi_multi == 2``). The OECD's CRS changelog states
|
|
688
|
+
these rows must be excluded to avoid double-counting bilateral
|
|
689
|
+
flows. Defaults to True. Rows with a missing ``bi_multi`` value
|
|
690
|
+
are always kept. Pass False to get the raw CRS totals including
|
|
691
|
+
those core-contribution rows.
|
|
651
692
|
"""
|
|
652
693
|
super().__init__()
|
|
653
694
|
self._init_filters(years=years, providers=providers, recipients=recipients)
|
|
695
|
+
self.exclude_multilateral_core = exclude_multilateral_core
|
|
654
696
|
self._param_hash = None
|
|
655
697
|
|
|
656
698
|
def _create_bulk_fetcher(self) -> Callable[[Path], None]:
|
|
@@ -660,6 +702,61 @@ class CRSData(DACSource):
|
|
|
660
702
|
"""
|
|
661
703
|
return create_crs_bulk_fetcher()
|
|
662
704
|
|
|
705
|
+
def _extra_hash_components(self) -> list[tuple]:
|
|
706
|
+
return [("exclude_multilateral_core", "==", self.exclude_multilateral_core)]
|
|
707
|
+
|
|
708
|
+
def _get_release_id(self) -> str | None:
|
|
709
|
+
"""Cheap upstream release id for the CRS bulk file (#162)."""
|
|
710
|
+
from oda_data.tools.cache import get_crs_release_id
|
|
711
|
+
|
|
712
|
+
return get_crs_release_id()
|
|
713
|
+
|
|
714
|
+
def _exclude_core_contributions(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
715
|
+
"""Drop rows reporting a donor's core contribution to a multilateral organisation.
|
|
716
|
+
|
|
717
|
+
No-op when ``exclude_multilateral_core`` is False, or when the ``bi_multi``
|
|
718
|
+
column isn't present (e.g. it was excluded from a column projection).
|
|
719
|
+
Rows with a missing ``bi_multi`` value are kept. A plain ``!= 2`` filter
|
|
720
|
+
would drop them, since a null comparison is null rather than true.
|
|
721
|
+
"""
|
|
722
|
+
if not self.exclude_multilateral_core:
|
|
723
|
+
return df
|
|
724
|
+
column = self.schema.BI_MULTI
|
|
725
|
+
if column not in df.columns:
|
|
726
|
+
return df
|
|
727
|
+
return df.loc[(df[column] != 2) | df[column].isna()].reset_index(drop=True)
|
|
728
|
+
|
|
729
|
+
def _fetch_from_bulk_cache(
|
|
730
|
+
self,
|
|
731
|
+
filters: list[tuple] | None,
|
|
732
|
+
columns: list | None,
|
|
733
|
+
*,
|
|
734
|
+
refresh: bool = False,
|
|
735
|
+
) -> pd.DataFrame:
|
|
736
|
+
"""Read from the bulk cache, then apply the core-contribution exclusion.
|
|
737
|
+
|
|
738
|
+
``bi_multi`` is read even when not in the requested ``columns``, so the
|
|
739
|
+
exclusion can be applied before column projection, then dropped again if
|
|
740
|
+
the caller didn't ask for it.
|
|
741
|
+
"""
|
|
742
|
+
read_columns = columns
|
|
743
|
+
added_bi_multi = False
|
|
744
|
+
if (
|
|
745
|
+
self.exclude_multilateral_core
|
|
746
|
+
and columns is not None
|
|
747
|
+
and self.schema.BI_MULTI not in columns
|
|
748
|
+
):
|
|
749
|
+
read_columns = [*columns, self.schema.BI_MULTI]
|
|
750
|
+
added_bi_multi = True
|
|
751
|
+
|
|
752
|
+
df = super()._fetch_from_bulk_cache(filters, read_columns, refresh=refresh)
|
|
753
|
+
df = self._exclude_core_contributions(df)
|
|
754
|
+
|
|
755
|
+
if added_bi_multi:
|
|
756
|
+
df = df.drop(columns=[self.schema.BI_MULTI])
|
|
757
|
+
|
|
758
|
+
return df
|
|
759
|
+
|
|
663
760
|
def download(self) -> pd.DataFrame:
|
|
664
761
|
"""Downloads CRS data via API (filtered query).
|
|
665
762
|
|
|
@@ -672,7 +769,7 @@ class CRSData(DACSource):
|
|
|
672
769
|
filters=self._get_filtered_download_filters(),
|
|
673
770
|
)
|
|
674
771
|
logger.info("CRS data downloaded successfully.")
|
|
675
|
-
return clean_raw_df(df)
|
|
772
|
+
return self._exclude_core_contributions(clean_raw_df(df))
|
|
676
773
|
|
|
677
774
|
|
|
678
775
|
class MultiSystemData(DACSource):
|
|
@@ -722,6 +819,12 @@ class MultiSystemData(DACSource):
|
|
|
722
819
|
"""
|
|
723
820
|
return create_multisystem_bulk_fetcher()
|
|
724
821
|
|
|
822
|
+
def _get_release_id(self) -> str | None:
|
|
823
|
+
"""Cheap upstream release id for the MultiSystem bulk file (#162)."""
|
|
824
|
+
from oda_data.tools.cache import get_multisystem_release_id
|
|
825
|
+
|
|
826
|
+
return get_multisystem_release_id()
|
|
827
|
+
|
|
725
828
|
def download(self) -> pd.DataFrame:
|
|
726
829
|
"""Downloads MultiSystem data via API (filtered query).
|
|
727
830
|
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Public symbols re-exported from sub-modules:
|
|
4
4
|
|
|
5
|
-
- ``path``, ``entries``, ``clear``, ``size``, ``invalidate``,
|
|
5
|
+
- ``path``, ``entries``, ``clear``, ``size``, ``invalidate``, ``release_info``,
|
|
6
6
|
``enable_cache``, ``disable_cache`` — from :mod:`oda_data.cache.api`
|
|
7
7
|
- ``migrate`` — from :mod:`oda_data.cache._migrate`
|
|
8
8
|
- ``set_cache_root`` — from :mod:`oda_data.cache.config`
|
|
9
|
-
- ``CacheRecord``, ``MigrationResult``, ``Scope`` — from
|
|
9
|
+
- ``CacheRecord``, ``MigrationResult``, ``ReleaseInfo``, ``Scope`` — from
|
|
10
|
+
:mod:`oda_data.cache.types`
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from oda_data.cache._migrate import migrate
|
|
@@ -17,14 +18,16 @@ from oda_data.cache.api import (
|
|
|
17
18
|
entries,
|
|
18
19
|
invalidate,
|
|
19
20
|
path,
|
|
21
|
+
release_info,
|
|
20
22
|
size,
|
|
21
23
|
)
|
|
22
24
|
from oda_data.cache.config import set_cache_root
|
|
23
|
-
from oda_data.cache.types import CacheRecord, MigrationResult, Scope
|
|
25
|
+
from oda_data.cache.types import CacheRecord, MigrationResult, ReleaseInfo, Scope
|
|
24
26
|
|
|
25
27
|
__all__ = [
|
|
26
28
|
"CacheRecord",
|
|
27
29
|
"MigrationResult",
|
|
30
|
+
"ReleaseInfo",
|
|
28
31
|
"Scope",
|
|
29
32
|
"clear",
|
|
30
33
|
"disable_cache",
|
|
@@ -33,6 +36,7 @@ __all__ = [
|
|
|
33
36
|
"invalidate",
|
|
34
37
|
"migrate",
|
|
35
38
|
"path",
|
|
39
|
+
"release_info",
|
|
36
40
|
"set_cache_root",
|
|
37
41
|
"size",
|
|
38
42
|
]
|