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.
- oda_reader/__init__.py +163 -0
- oda_reader/_cache/README.md +556 -0
- oda_reader/_cache/__init__.py +84 -0
- oda_reader/_cache/config.py +130 -0
- oda_reader/_cache/dataframe.py +241 -0
- oda_reader/_cache/legacy.py +213 -0
- oda_reader/_cache/manager.py +338 -0
- oda_reader/_http_primitives.py +146 -0
- oda_reader/aiddata.py +96 -0
- oda_reader/codelists/__init__.py +63 -0
- oda_reader/codelists/_agencies.py +365 -0
- oda_reader/codelists/_categories.py +412 -0
- oda_reader/codelists/_fetch.py +589 -0
- oda_reader/codelists/_parse.py +631 -0
- oda_reader/codelists/_reconcile.py +693 -0
- oda_reader/codelists/_types.py +238 -0
- oda_reader/common.py +334 -0
- oda_reader/cpa.py +78 -0
- oda_reader/crs.py +175 -0
- oda_reader/dac1.py +51 -0
- oda_reader/dac2a.py +111 -0
- oda_reader/download/__init__.py +0 -0
- oda_reader/download/_deflate64.py +53 -0
- oda_reader/download/download_tools.py +725 -0
- oda_reader/download/query_builder.py +366 -0
- oda_reader/download/version_discovery.py +164 -0
- oda_reader/exceptions.py +395 -0
- oda_reader/multisystem.py +118 -0
- oda_reader/py.typed +0 -0
- oda_reader/schemas/__init__.py +0 -0
- oda_reader/schemas/crs_translation.py +53 -0
- oda_reader/schemas/dac1_translation.py +59 -0
- oda_reader/schemas/dac2_translation.py +53 -0
- oda_reader/schemas/mappings/_provenance.json +14 -0
- oda_reader/schemas/mappings/aidData_schema.json +758 -0
- oda_reader/schemas/mappings/area_code_corrections.json +10 -0
- oda_reader/schemas/mappings/code_prices_corrections.json +4 -0
- oda_reader/schemas/mappings/crs_dotstat.json +507 -0
- oda_reader/schemas/mappings/dac1_codes_area.json +443 -0
- oda_reader/schemas/mappings/dac1_codes_flow_types.json +5 -0
- oda_reader/schemas/mappings/dac1_codes_prices.json +4 -0
- oda_reader/schemas/mappings/dac1_dotstat.json +152 -0
- oda_reader/schemas/mappings/dac2_codes_area.json +445 -0
- oda_reader/schemas/mappings/dac2a_dotstat.json +142 -0
- oda_reader/schemas/mappings/multisystem_dotstat.json +202 -0
- oda_reader/schemas/multisystem_translation.py +43 -0
- oda_reader/schemas/schema_tools.py +243 -0
- oda_reader/schemas/xml_tools.py +50 -0
- oda_reader/tools.py +42 -0
- oda_reader-1.7.0.dist-info/METADATA +736 -0
- oda_reader-1.7.0.dist-info/RECORD +52 -0
- 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
|
+
______________________________________________________________________
|