oda_reader 1.7.0__py3-none-any.whl

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 (52) hide show
  1. oda_reader/__init__.py +163 -0
  2. oda_reader/_cache/README.md +556 -0
  3. oda_reader/_cache/__init__.py +84 -0
  4. oda_reader/_cache/config.py +130 -0
  5. oda_reader/_cache/dataframe.py +241 -0
  6. oda_reader/_cache/legacy.py +213 -0
  7. oda_reader/_cache/manager.py +338 -0
  8. oda_reader/_http_primitives.py +146 -0
  9. oda_reader/aiddata.py +96 -0
  10. oda_reader/codelists/__init__.py +63 -0
  11. oda_reader/codelists/_agencies.py +365 -0
  12. oda_reader/codelists/_categories.py +412 -0
  13. oda_reader/codelists/_fetch.py +589 -0
  14. oda_reader/codelists/_parse.py +631 -0
  15. oda_reader/codelists/_reconcile.py +693 -0
  16. oda_reader/codelists/_types.py +238 -0
  17. oda_reader/common.py +334 -0
  18. oda_reader/cpa.py +78 -0
  19. oda_reader/crs.py +175 -0
  20. oda_reader/dac1.py +51 -0
  21. oda_reader/dac2a.py +111 -0
  22. oda_reader/download/__init__.py +0 -0
  23. oda_reader/download/_deflate64.py +53 -0
  24. oda_reader/download/download_tools.py +725 -0
  25. oda_reader/download/query_builder.py +366 -0
  26. oda_reader/download/version_discovery.py +164 -0
  27. oda_reader/exceptions.py +395 -0
  28. oda_reader/multisystem.py +118 -0
  29. oda_reader/py.typed +0 -0
  30. oda_reader/schemas/__init__.py +0 -0
  31. oda_reader/schemas/crs_translation.py +53 -0
  32. oda_reader/schemas/dac1_translation.py +59 -0
  33. oda_reader/schemas/dac2_translation.py +53 -0
  34. oda_reader/schemas/mappings/_provenance.json +14 -0
  35. oda_reader/schemas/mappings/aidData_schema.json +758 -0
  36. oda_reader/schemas/mappings/area_code_corrections.json +10 -0
  37. oda_reader/schemas/mappings/code_prices_corrections.json +4 -0
  38. oda_reader/schemas/mappings/crs_dotstat.json +507 -0
  39. oda_reader/schemas/mappings/dac1_codes_area.json +443 -0
  40. oda_reader/schemas/mappings/dac1_codes_flow_types.json +5 -0
  41. oda_reader/schemas/mappings/dac1_codes_prices.json +4 -0
  42. oda_reader/schemas/mappings/dac1_dotstat.json +152 -0
  43. oda_reader/schemas/mappings/dac2_codes_area.json +445 -0
  44. oda_reader/schemas/mappings/dac2a_dotstat.json +142 -0
  45. oda_reader/schemas/mappings/multisystem_dotstat.json +202 -0
  46. oda_reader/schemas/multisystem_translation.py +43 -0
  47. oda_reader/schemas/schema_tools.py +243 -0
  48. oda_reader/schemas/xml_tools.py +50 -0
  49. oda_reader/tools.py +42 -0
  50. oda_reader-1.7.0.dist-info/METADATA +736 -0
  51. oda_reader-1.7.0.dist-info/RECORD +52 -0
  52. oda_reader-1.7.0.dist-info/WHEEL +4 -0
oda_reader/__init__.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ This oda_reader package is a simple python wrapper for the OECD explorer API,
3
+ specifically designed to work with OECD DAC data.
4
+
5
+ DAC-area codelists are available via ``oda_reader.codelists``, which is not
6
+ re-exported here and must be imported explicitly (see the docs). That
7
+ package also provides ``reconcile``, which merges a fresh codelist snapshot
8
+ against a previous table with never-delete lineage semantics.
9
+ """
10
+
11
+ import sys
12
+ import warnings
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from oda_reader._cache import (
17
+ bulk_cache_manager,
18
+ cache_dir, # Deprecated alias
19
+ dataframe_cache,
20
+ enforce_cache_limits,
21
+ get_cache_dir,
22
+ reset_cache_dir,
23
+ )
24
+ from oda_reader._cache.config import set_cache_dir as _impl_set_cache_dir
25
+ from oda_reader._cache.legacy import clear_cache as _impl_clear_cache
26
+ from oda_reader._cache.legacy import disable_cache as _impl_disable_cache
27
+ from oda_reader._cache.legacy import enable_cache as _impl_enable_cache
28
+ from oda_reader.aiddata import download_aiddata
29
+ from oda_reader.common import (
30
+ API_RATE_LIMITER,
31
+ clear_http_cache,
32
+ disable_http_cache,
33
+ enable_http_cache,
34
+ get_http_cache_info,
35
+ )
36
+ from oda_reader.cpa import download_cpa
37
+ from oda_reader.crs import bulk_download_crs, download_crs, download_crs_file
38
+ from oda_reader.dac1 import download_dac1
39
+ from oda_reader.dac2a import bulk_download_dac2a, download_dac2a
40
+ from oda_reader.download.query_builder import QueryBuilder
41
+ from oda_reader.download.version_discovery import clear_version_cache
42
+
43
+ # NOTE: oda_reader.codelists is deliberately NOT imported here. It performs
44
+ # network extraction and pulls pandas eagerly; `import oda_reader` and every
45
+ # convert_* call must stay network-free. Callers import it explicitly:
46
+ # from oda_reader.codelists import fetch_codelists # noqa: ERA001
47
+ from oda_reader.exceptions import (
48
+ BulkDownloadHTTPError,
49
+ BulkPayloadCorruptError,
50
+ CodelistError,
51
+ CodelistFetchError,
52
+ CodelistShapeError,
53
+ CodelistSourceError,
54
+ CodelistValidationError,
55
+ )
56
+ from oda_reader.multisystem import bulk_download_multisystem, download_multisystem
57
+ from oda_reader.tools import get_available_filters
58
+
59
+ # Each shim emits a one-time-per-session DeprecationWarning when oda_data is
60
+ # also imported (umbrella users should migrate to oda_data.cache.*); standalone
61
+ # oda_reader users see no warning.
62
+ _WARNED_SHIMS: set[str] = set()
63
+
64
+
65
+ def _warn_once_if_oda_data_imported(name: str, replacement: str) -> None:
66
+ if name in _WARNED_SHIMS or "oda_data" not in sys.modules:
67
+ return
68
+ warnings.warn(
69
+ f"oda_reader.{name} is deprecated for users who also import oda_data; "
70
+ f"use {replacement} for the umbrella API. This shim is preserved for "
71
+ "standalone oda_reader users through 1.x and removed in 2.0.",
72
+ DeprecationWarning,
73
+ stacklevel=3,
74
+ )
75
+ _WARNED_SHIMS.add(name)
76
+
77
+
78
+ def _make_deprecation_shim(
79
+ name: str, replacement: str, impl: Callable[..., Any], one_liner: str
80
+ ) -> Callable[..., Any]:
81
+ def shim(*args: Any, **kwargs: Any) -> Any:
82
+ _warn_once_if_oda_data_imported(name, replacement)
83
+ return impl(*args, **kwargs)
84
+
85
+ shim.__name__ = name
86
+ shim.__qualname__ = name
87
+ shim.__doc__ = (
88
+ f"{one_liner} Deprecated under the oda_data umbrella; use {replacement}."
89
+ )
90
+ return shim
91
+
92
+
93
+ clear_cache = _make_deprecation_shim(
94
+ "clear_cache",
95
+ "oda_data.cache.clear('all')",
96
+ _impl_clear_cache,
97
+ "Clear the cache directory.",
98
+ )
99
+ set_cache_dir = _make_deprecation_shim(
100
+ "set_cache_dir",
101
+ "oda_data.set_cache_root() or the ODA_DATA_CACHE_DIR env var",
102
+ _impl_set_cache_dir,
103
+ "Set a custom cache directory path.",
104
+ )
105
+ enable_cache = _make_deprecation_shim(
106
+ "enable_cache",
107
+ "oda_data.cache.enable_cache('all')",
108
+ _impl_enable_cache,
109
+ "Enable caching globally.",
110
+ )
111
+ disable_cache = _make_deprecation_shim(
112
+ "disable_cache",
113
+ "oda_data.cache.disable_cache('all')",
114
+ _impl_disable_cache,
115
+ "Disable caching globally.",
116
+ )
117
+
118
+
119
+ __all__ = [
120
+ # Boundary contract
121
+ "BulkPayloadCorruptError",
122
+ "BulkDownloadHTTPError",
123
+ "CodelistError",
124
+ "CodelistFetchError",
125
+ "CodelistSourceError",
126
+ "CodelistShapeError",
127
+ "CodelistValidationError",
128
+ # Data download
129
+ "QueryBuilder",
130
+ "download_dac1",
131
+ "download_dac2a",
132
+ "bulk_download_dac2a",
133
+ "download_multisystem",
134
+ "bulk_download_multisystem",
135
+ "download_crs",
136
+ "bulk_download_crs",
137
+ "download_crs_file",
138
+ "download_cpa",
139
+ "download_aiddata",
140
+ "get_available_filters",
141
+ # Cache configuration
142
+ "get_cache_dir",
143
+ "set_cache_dir",
144
+ "reset_cache_dir",
145
+ # HTTP cache management
146
+ "enable_http_cache",
147
+ "disable_http_cache",
148
+ "clear_http_cache",
149
+ "get_http_cache_info",
150
+ # Version discovery cache
151
+ "clear_version_cache",
152
+ # DataFrame and bulk cache managers
153
+ "dataframe_cache",
154
+ "bulk_cache_manager",
155
+ # Rate limiting
156
+ "API_RATE_LIMITER",
157
+ # Legacy (backward compatibility - deprecated for oda_data users)
158
+ "enable_cache",
159
+ "disable_cache",
160
+ "clear_cache",
161
+ "enforce_cache_limits",
162
+ "cache_dir",
163
+ ]
@@ -0,0 +1,556 @@
1
+ # Cache Management in oda_reader
2
+
3
+ This document provides a comprehensive guide to the caching system in `oda_reader`, including architecture details, usage instructions, and backward compatibility notes.
4
+
5
+ ______________________________________________________________________
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 1. [Architecture](#architecture)
11
+ 1. [Quick Start](#quick-start)
12
+ 1. [Configuration](#configuration)
13
+ 1. [Cache Management](#cache-management)
14
+ 1. [Backward Compatibility](#backward-compatibility)
15
+ 1. [Performance Considerations](#performance-considerations)
16
+ 1. [Troubleshooting](#troubleshooting)
17
+ 1. [Technical Details](#technical-details)
18
+
19
+ ______________________________________________________________________
20
+
21
+ ## Overview
22
+
23
+ The `oda_reader` package uses a **three-tier caching system** to optimize data downloads from the OECD API:
24
+
25
+ 1. **HTTP Cache** (requests-cache): Caches raw API responses for 7 days
26
+ 1. **DataFrame Cache**: Caches processed DataFrames with preprocessing parameters
27
+ 1. **Bulk File Cache**: Caches large bulk downloads (CRS, Multisystem, AidData)
28
+
29
+ This multi-layer approach provides:
30
+
31
+ - **Fast repeated queries** (10-90x speedup on cache hits)
32
+ - **Correct data** (cache keys include all processing parameters)
33
+ - **Efficient storage** (parquet format, automatic cleanup)
34
+ - **Platform-aware paths** (follows OS conventions)
35
+
36
+ ______________________________________________________________________
37
+
38
+ ## Architecture
39
+
40
+ ```
41
+ User Request
42
+
43
+ ┌─────────────────────────────────────────┐
44
+ │ DataFrame Cache │
45
+ │ - Parquet files │
46
+ │ - Includes preprocessing params │
47
+ │ - Key: hash(url + pre_process + ...) │
48
+ └─────────────────────────────────────────┘
49
+ ↓ (cache miss)
50
+ ┌─────────────────────────────────────────┐
51
+ │ HTTP Cache (requests-cache) │
52
+ │ - Filesystem backend │
53
+ │ - 7-day TTL │
54
+ │ - Caches 200 and 404 responses │
55
+ └─────────────────────────────────────────┘
56
+ ↓ (cache miss)
57
+ ┌─────────────────────────────────────────┐
58
+ │ OECD API │
59
+ │ - Rate limited (20 calls/60s) │
60
+ └─────────────────────────────────────────┘
61
+ ```
62
+
63
+ ### Cache Directory Structure
64
+
65
+ Default location: `~/.cache/oda-reader/{version}/` (macOS/Linux) or `%LOCALAPPDATA%\oda-reader\Cache\{version}` (Windows)
66
+
67
+ ```
68
+ ~/.cache/oda-reader/1.2.2/
69
+ ├── http_cache/ # HTTP response cache (filesystem backend)
70
+ │ ├── <hash1>
71
+ │ └── <hash2>
72
+ ├── dataframes/ # Processed DataFrames
73
+ │ ├── 2986243275235237.parquet
74
+ │ └── 00b396b02a62f1cb.parquet
75
+ └── bulk_files/ # Bulk downloads
76
+ ├── manifest.json
77
+ ├── .cache.lock
78
+ └── 6545c6d5a9c7d8a.zip
79
+ ```
80
+
81
+ **Note**: Cache is automatically versioned - upgrading `oda_reader` creates a new cache directory, ensuring compatibility.
82
+
83
+ ______________________________________________________________________
84
+
85
+ ## Quick Start
86
+
87
+ ### Basic Usage (No Configuration Required)
88
+
89
+ Caching is **enabled by default** and works automatically:
90
+
91
+ ```python
92
+ import oda_reader
93
+
94
+ # First call: fetches from API (~2s)
95
+ df = oda_reader.download_dac1(start_year=2022, end_year=2022)
96
+
97
+ # Second call: loads from cache (~0.02s) - 100x faster!
98
+ df = oda_reader.download_dac1(start_year=2022, end_year=2022)
99
+ ```
100
+
101
+ ### Check Cache Status
102
+
103
+ ```python
104
+ import oda_reader
105
+
106
+ # HTTP cache stats
107
+ print(oda_reader.get_http_cache_info())
108
+ # {'response_count': 8, 'redirects_count': 0}
109
+
110
+ # DataFrame cache stats
111
+ print(oda_reader.dataframe_cache().stats())
112
+ # {'total_entries': 2, 'total_size_mb': 0.37}
113
+
114
+ # Bulk file cache stats
115
+ print(oda_reader.bulk_cache_manager().stats())
116
+ # {'total_entries': 1, 'total_size_mb': 878.5, 'stale_entries': 0}
117
+ ```
118
+
119
+ ______________________________________________________________________
120
+
121
+ ## Configuration
122
+
123
+ ### Cache Directory
124
+
125
+ By default, cache is stored in a platform-specific location. You can customize this:
126
+
127
+ ```python
128
+ import oda_reader
129
+
130
+ # Get current cache directory
131
+ print(oda_reader.get_cache_dir())
132
+ # /Users/jorge/Library/Caches/oda-reader/1.2.2
133
+
134
+ # Set custom cache directory
135
+ oda_reader.set_cache_dir("/custom/path/to/cache")
136
+
137
+ # Reset to default
138
+ oda_reader.reset_cache_dir()
139
+ ```
140
+
141
+ **Environment Variable**: You can also set `ODA_READER_CACHE_DIR`:
142
+
143
+ ```bash
144
+ export ODA_READER_CACHE_DIR="/custom/cache/path"
145
+ ```
146
+
147
+ Priority order:
148
+
149
+ 1. `set_cache_dir()` (programmatic override)
150
+ 1. `ODA_READER_CACHE_DIR` (environment variable)
151
+ 1. Platform default (via platformdirs)
152
+
153
+ ### Disable Caching
154
+
155
+ For testing or when you need fresh data:
156
+
157
+ ```python
158
+ import oda_reader
159
+
160
+ # Disable all caching
161
+ oda_reader.disable_cache()
162
+
163
+ # Disable only HTTP caching
164
+ oda_reader.disable_http_cache()
165
+
166
+ # Disable only DataFrame caching
167
+ oda_reader.dataframe_cache().disable()
168
+
169
+ # Re-enable
170
+ oda_reader.enable_cache()
171
+ ```
172
+
173
+ ### Rate Limiting
174
+
175
+ API rate limiting is independent of caching:
176
+
177
+ ```python
178
+ import oda_reader
179
+
180
+ # Default: 20 calls per 60 seconds
181
+ oda_reader.API_RATE_LIMITER.max_calls = 10
182
+ oda_reader.API_RATE_LIMITER.period = 60
183
+ ```
184
+
185
+ ______________________________________________________________________
186
+
187
+ ## Cache Management
188
+
189
+ ### Clearing Cache
190
+
191
+ ```python
192
+ import oda_reader
193
+
194
+ # Clear all cache (entire directory)
195
+ oda_reader.clear_cache()
196
+
197
+ # Clear only HTTP cache
198
+ oda_reader.clear_http_cache()
199
+
200
+ # Clear only DataFrame cache
201
+ oda_reader.dataframe_cache().clear()
202
+
203
+ # Clear only bulk files
204
+ oda_reader.bulk_cache_manager().clear()
205
+
206
+ # Clear specific bulk file
207
+ oda_reader.bulk_cache_manager().clear("crs_full")
208
+ ```
209
+
210
+ ### Inspecting Cache
211
+
212
+ ```python
213
+ import oda_reader
214
+
215
+ # List all bulk file cache entries
216
+ for record in oda_reader.bulk_cache_manager().list_records():
217
+ print(f"{record['key']}: {record['size_mb']:.1f} MB, "
218
+ f"age: {record['age_days']:.1f} days, "
219
+ f"stale: {record['is_stale']}")
220
+ ```
221
+
222
+ ### Manual Cache Enforcement (Advanced)
223
+
224
+ ```python
225
+ import oda_reader
226
+
227
+ # Enforce cache limits (max size, max age)
228
+ oda_reader.enforce_cache_limits(
229
+ max_size_mb=2500, # 2.5 GB
230
+ max_age_hours=168 # 7 days
231
+ )
232
+ ```
233
+
234
+ **Note**: This is called automatically on first cache access, not at import time.
235
+
236
+ ______________________________________________________________________
237
+
238
+ ## Backward Compatibility
239
+
240
+ ### Legacy API (Still Supported)
241
+
242
+ All old functions continue to work:
243
+
244
+ ```python
245
+ import oda_reader
246
+
247
+ # Old API (still works)
248
+ oda_reader.cache_dir() # Returns cache directory
249
+ oda_reader.enable_cache() # Enables all caching
250
+ oda_reader.disable_cache() # Disables all caching
251
+ oda_reader.clear_cache() # Clears entire cache
252
+ oda_reader.enforce_cache_limits() # Enforces size/age limits
253
+ ```
254
+
255
+ ### Migration Guide
256
+
257
+ If you have code using the old caching system:
258
+
259
+ **Before** (oda_reader < 1.2.2):
260
+
261
+ ```python
262
+ from oda_reader._cache import memory, set_cache_dir
263
+
264
+ # Old joblib-based caching
265
+ mem = memory()
266
+ if mem.store_backend:
267
+ print("Cache enabled")
268
+
269
+ set_cache_dir("/custom/path")
270
+ ```
271
+
272
+ **After** (oda_reader >= 1.2.2):
273
+
274
+ ```python
275
+ from oda_reader import get_cache_dir, set_cache_dir
276
+
277
+ # New requests-cache + DataFrame cache
278
+ print(f"Cache at: {get_cache_dir()}")
279
+ set_cache_dir("/custom/path")
280
+ ```
281
+
282
+ **Key Changes**:
283
+
284
+ - ✅ No breaking changes - old code continues to work
285
+ - ✅ Cache location moved from `src/oda_reader/.cache/` to platform directory
286
+ - ✅ joblib replaced with requests-cache + parquet files
287
+ - ✅ Cache keys now include preprocessing parameters (fixes correctness issue)
288
+ - ✅ No import-time side effects (was: `enforce_cache_limits()` ran on import)
289
+
290
+ ### Why the Refactor?
291
+
292
+ The old caching system had several issues:
293
+
294
+ 1. **Data correctness bug**: Cache didn't include `pre_process` or `dotstat_codes` parameters
295
+
296
+ ```python
297
+ # Before: These returned the SAME cached data (wrong!)
298
+ df1 = download_dac1(2022, 2022, pre_process=True, dotstat_codes=True)
299
+ df2 = download_dac1(2022, 2022, pre_process=False, dotstat_codes=False)
300
+
301
+ # After: These correctly return different data
302
+ ```
303
+
304
+ 1. **Bad cache location**: Cache was in `src/oda_reader/.cache/` (polluted source tree)
305
+
306
+ 1. **Import-time slowdown**: `enforce_cache_limits()` walked entire cache on every import
307
+
308
+ 1. **No observability**: No way to inspect cache contents or hit/miss rates
309
+
310
+ All these issues are now fixed while maintaining full backward compatibility.
311
+
312
+ ______________________________________________________________________
313
+
314
+ ## Performance Considerations
315
+
316
+ ### Cache Hit Performance
317
+
318
+ Typical speedups with cache hits:
319
+
320
+ - **HTTP cache hit**: ~2-5x faster (avoids network request)
321
+ - **DataFrame cache hit**: ~10-90x faster (avoids parsing + processing)
322
+
323
+ Example benchmark:
324
+
325
+ ```
326
+ First download: 2.71s (API + processing)
327
+ Second download: 0.03s (DataFrame cache) - 90x faster
328
+ ```
329
+
330
+ ### Storage Usage
331
+
332
+ Typical cache sizes:
333
+
334
+ - HTTP cache: 1-10 MB per response (filesystem backend, can handle >2GB responses)
335
+ - DataFrame cache: 0.1-1 MB per query (compressed parquet)
336
+ - Bulk files: 100-1000 MB per file (CRS full dataset ~900 MB)
337
+
338
+ ### Cache Expiration
339
+
340
+ - **HTTP cache**: 7 days (604800 seconds)
341
+ - **DataFrame cache**: No automatic expiration (cleared manually or on size limit)
342
+ - **Bulk files**: Configurable TTL (default: 30 days for CRS, 180 days for AidData)
343
+
344
+ ### When Cache Is NOT Used
345
+
346
+ Cache is bypassed when:
347
+
348
+ 1. Caching is disabled (`disable_cache()` or `disable_http_cache()`)
349
+ 1. Cache entry has expired (HTTP: 7 days, bulk files: per-entry TTL)
350
+ 1. Different parameters are used (cache keys are unique per parameter combination)
351
+
352
+ ______________________________________________________________________
353
+
354
+ ## Troubleshooting
355
+
356
+ ### Cache Not Working
357
+
358
+ **Check if caching is enabled:**
359
+
360
+ ```python
361
+ import oda_reader
362
+
363
+ # This should show cached responses after first download
364
+ print(oda_reader.get_http_cache_info())
365
+ print(oda_reader.dataframe_cache().stats())
366
+ ```
367
+
368
+ **Common issues:**
369
+
370
+ - Caching disabled: Call `oda_reader.enable_cache()`
371
+ - Cache full: Call `oda_reader.clear_cache()` or increase limits
372
+ - Different parameters: Cache keys are unique per parameter combination
373
+
374
+ ### Cache Growing Too Large
375
+
376
+ **Check cache size:**
377
+
378
+ ```python
379
+ import oda_reader
380
+
381
+ # Check overall cache size
382
+ from oda_reader._cache import get_cache_size_mb
383
+ print(f"Cache size: {get_cache_size_mb():.1f} MB")
384
+
385
+ # Check individual caches
386
+ print(oda_reader.dataframe_cache().stats())
387
+ print(oda_reader.bulk_cache_manager().stats())
388
+ ```
389
+
390
+ **Solutions:**
391
+
392
+ ```python
393
+ # Clear specific caches
394
+ oda_reader.dataframe_cache().clear() # Usually the culprit
395
+ oda_reader.clear_http_cache()
396
+
397
+ # Or clear everything
398
+ oda_reader.clear_cache()
399
+
400
+ # Adjust limits
401
+ oda_reader.enforce_cache_limits(max_size_mb=1000) # 1 GB limit
402
+ ```
403
+
404
+ ### Cache Returning Stale Data
405
+
406
+ **Force fresh data:**
407
+
408
+ ```python
409
+ # Option 1: Clear cache before download
410
+ oda_reader.clear_http_cache()
411
+
412
+ # Option 2: Temporarily disable cache
413
+ oda_reader.disable_cache()
414
+ # download
415
+ oda_reader.enable_cache()
416
+ ```
417
+
418
+ ### Permission Errors
419
+
420
+ If you get permission errors accessing the cache:
421
+
422
+ ```python
423
+ import oda_reader
424
+
425
+ # Set cache to a writable location
426
+ oda_reader.set_cache_dir("/tmp/oda_cache")
427
+ ```
428
+
429
+ ### Multi-Process Issues
430
+
431
+ The bulk file cache uses `FileLock` for multi-process safety. If you see lock timeout errors:
432
+
433
+ ```python
434
+ from oda_reader._cache import CacheManager
435
+
436
+ # Increase lock timeout (default: 1200s)
437
+ manager = CacheManager()
438
+ manager._lock = FileLock(manager.lock_path, timeout=2000)
439
+ ```
440
+
441
+ ______________________________________________________________________
442
+
443
+ ## Technical Details
444
+
445
+ ### Module Structure
446
+
447
+ ```
448
+ oda_reader/_cache/
449
+ ├── __init__.py # Public API exports
450
+ ├── config.py # Cache directory configuration
451
+ ├── manager.py # Bulk file cache (pydeflate-style)
452
+ ├── dataframe.py # DataFrame caching layer
453
+ └── legacy.py # Backward-compatible functions
454
+ ```
455
+
456
+ ### Cache Key Generation
457
+
458
+ **DataFrame cache keys** are SHA256 hashes of:
459
+
460
+ ```python
461
+ {
462
+ "dataflow_id": "DSD_DAC1@DF_DAC1",
463
+ "dataflow_version": "1.6",
464
+ "url": "https://...",
465
+ "pre_process": True,
466
+ "dotstat_codes": True,
467
+ # ... any other parameters
468
+ }
469
+ ```
470
+
471
+ This ensures different preprocessing options get separate cache entries.
472
+
473
+ **Bulk file cache keys** are simple strings like `"crs_full"`, `"aiddata"`.
474
+
475
+ ### HTTP Cache Backend
476
+
477
+ Uses `requests-cache` with filesystem backend:
478
+
479
+ - Directory: `{cache_dir}/http_cache/`
480
+ - Stores responses, redirects, and metadata as individual files
481
+ - Handles large responses (>2GB) without issues
482
+ - Automatic cleanup on expiration
483
+ - Thread-safe for concurrent requests
484
+
485
+ ### DataFrame Cache Format
486
+
487
+ - Format: Apache Parquet (compressed, column-oriented)
488
+ - Location: `{cache_dir}/dataframes/{cache_key}.parquet`
489
+ - Compression: Snappy (default)
490
+ - Typical compression ratio: 5-10x
491
+
492
+ ### Bulk File Cache
493
+
494
+ Follows pydeflate design:
495
+
496
+ - Manifest: `{cache_dir}/bulk_files/manifest.json`
497
+ - Lock file: `{cache_dir}/bulk_files/.cache.lock` (FileLock)
498
+ - Atomic writes: temp-file-then-rename pattern
499
+ - Metadata: download timestamp, version, TTL, size
500
+
501
+ ### Version-Based Cache Invalidation
502
+
503
+ Cache directory includes package version:
504
+
505
+ ```
506
+ ~/.cache/oda-reader/1.2.2/ # Version 1.2.2
507
+ ~/.cache/oda-reader/1.3.0/ # Version 1.3.0 (new cache)
508
+ ```
509
+
510
+ This ensures:
511
+
512
+ - No cache corruption after upgrades
513
+ - Schema changes don't break existing cache
514
+ - Automatic cleanup of old versions
515
+
516
+ ______________________________________________________________________
517
+
518
+ ## API Reference
519
+
520
+ ### Configuration Functions
521
+
522
+ - `get_cache_dir() -> Path`: Get current cache directory
523
+ - `set_cache_dir(path: str | Path) -> None`: Set custom cache directory
524
+ - `reset_cache_dir() -> None`: Reset to platform default
525
+
526
+ ### HTTP Cache Functions
527
+
528
+ - `enable_http_cache() -> None`: Enable HTTP caching
529
+ - `disable_http_cache() -> None`: Disable HTTP caching
530
+ - `clear_http_cache() -> None`: Clear all HTTP responses
531
+ - `get_http_cache_info() -> dict`: Get cache statistics
532
+
533
+ ### DataFrame Cache
534
+
535
+ - `dataframe_cache() -> DataFrameCache`: Get DataFrame cache instance
536
+ - `DataFrameCache.stats() -> dict`: Get statistics
537
+ - `DataFrameCache.clear() -> None`: Clear all cached DataFrames
538
+ - `DataFrameCache.enable() -> None`: Enable caching
539
+ - `DataFrameCache.disable() -> None`: Disable caching
540
+
541
+ ### Bulk File Cache
542
+
543
+ - `bulk_cache_manager() -> CacheManager`: Get bulk cache instance
544
+ - `CacheManager.stats() -> dict`: Get statistics
545
+ - `CacheManager.list_records() -> list[dict]`: List all cached files
546
+ - `CacheManager.clear(key: str | None) -> None`: Clear cache entries
547
+
548
+ ### Legacy Functions (Backward Compatibility)
549
+
550
+ - `cache_dir() -> Path`: Alias for `get_cache_dir()`
551
+ - `enable_cache() -> None`: Enable all caching
552
+ - `disable_cache() -> None`: Disable all caching
553
+ - `clear_cache() -> None`: Clear entire cache directory
554
+ - `enforce_cache_limits() -> None`: Enforce size/age limits
555
+
556
+ ______________________________________________________________________