notoecd 0.1.1__tar.gz → 0.1.2__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.
- {notoecd-0.1.1 → notoecd-0.1.2}/PKG-INFO +3 -3
- {notoecd-0.1.1 → notoecd-0.1.2}/README.md +2 -2
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd/__init__.py +1 -1
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd/datasets.py +41 -34
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd.egg-info/PKG-INFO +3 -3
- {notoecd-0.1.1 → notoecd-0.1.2}/pyproject.toml +2 -2
- {notoecd-0.1.1 → notoecd-0.1.2}/tests/test_datasets.py +16 -5
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd/calls.py +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd/structure.py +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd.egg-info/SOURCES.txt +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd.egg-info/dependency_links.txt +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd.egg-info/requires.txt +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/notoecd.egg-info/top_level.txt +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/setup.cfg +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/tests/test_api.py +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/tests/test_calls.py +0 -0
- {notoecd-0.1.1 → notoecd-0.1.2}/tests/test_structure.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: notoecd
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Unofficial library for interacting with the OECD Data Explorer through Python.
|
|
5
5
|
Author-email: Daniel Vegara Balsa <daniel.vegarabalsa@oecd.org>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dani-37/notoecd
|
|
@@ -15,6 +15,7 @@ Requires-Dist: requests>=2.31
|
|
|
15
15
|
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
16
16
|
|
|
17
17
|
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD datasets.
|
|
18
|
+
|
|
18
19
|
The package provides utilities for:
|
|
19
20
|
|
|
20
21
|
- Discovering dataset metadata
|
|
@@ -110,4 +111,3 @@ The returned object is a pandas DataFrame containing the requested subset of OEC
|
|
|
110
111
|
## Examples
|
|
111
112
|
|
|
112
113
|
You can see this full example as a notebook called example.ipynb.
|
|
113
|
-
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
4
4
|
|
|
5
5
|
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD datasets.
|
|
6
|
+
|
|
6
7
|
The package provides utilities for:
|
|
7
8
|
|
|
8
9
|
- Discovering dataset metadata
|
|
@@ -97,5 +98,4 @@ The returned object is a pandas DataFrame containing the requested subset of OEC
|
|
|
97
98
|
|
|
98
99
|
## Examples
|
|
99
100
|
|
|
100
|
-
You can see this full example as a notebook called example.ipynb.
|
|
101
|
-
|
|
101
|
+
You can see this full example as a notebook called example.ipynb.
|
|
@@ -18,43 +18,51 @@ NS = {
|
|
|
18
18
|
_ws_re = re.compile(r"\s+")
|
|
19
19
|
_tag_re = re.compile(r"<[^>]+>")
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def _clean(s: str | None) -> str | None:
|
|
22
22
|
if s is None: return None
|
|
23
23
|
s = html.unescape(s)
|
|
24
24
|
s = _tag_re.sub("", s)
|
|
25
25
|
s = _ws_re.sub(" ", s).strip()
|
|
26
26
|
return s or None
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# Cache
|
|
29
|
+
_datasets: pd.DataFrame | None = None
|
|
30
|
+
|
|
31
|
+
def _load_datasets() -> pd.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
Loads OECD datasets and keeps them in memory.
|
|
34
|
+
"""
|
|
35
|
+
global _datasets
|
|
36
|
+
if _datasets is not None: return _datasets
|
|
37
|
+
|
|
38
|
+
headers = {"Accept": "application/vnd.sdmx.structure+xml;version=2.1"}
|
|
39
|
+
r = requests.get(url, headers=headers, timeout=30)
|
|
40
|
+
r.raise_for_status()
|
|
41
|
+
root = ET.fromstring(r.content)
|
|
42
|
+
|
|
43
|
+
rows = []
|
|
44
|
+
for df in root.findall(".//structure:Dataflow", NS):
|
|
45
|
+
dataflow_id = df.attrib.get("id")
|
|
46
|
+
agency_id = df.attrib.get("agencyID")
|
|
47
|
+
|
|
48
|
+
name_elem = df.find("common:Name[@xml:lang='en']", NS)
|
|
49
|
+
desc_elem = df.find("common:Description[@xml:lang='en']", NS)
|
|
50
|
+
|
|
51
|
+
name = _clean("".join(name_elem.itertext())) if name_elem is not None else None
|
|
52
|
+
desc_raw = "".join(desc_elem.itertext()) if desc_elem is not None else None
|
|
53
|
+
desc = _clean(desc_raw)
|
|
54
|
+
|
|
55
|
+
rows.append(
|
|
56
|
+
{
|
|
57
|
+
"dataflowID": dataflow_id,
|
|
58
|
+
"agencyID": agency_id,
|
|
59
|
+
"name": name,
|
|
60
|
+
"description": desc,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
root = ET.fromstring(r.content)
|
|
35
|
-
|
|
36
|
-
rows = []
|
|
37
|
-
for df in root.findall(".//structure:Dataflow", NS):
|
|
38
|
-
dataflow_id = df.attrib.get("id")
|
|
39
|
-
agency_id = df.attrib.get("agencyID")
|
|
40
|
-
|
|
41
|
-
name_elem = df.find("common:Name[@xml:lang='en']", NS)
|
|
42
|
-
desc_elem = df.find("common:Description[@xml:lang='en']", NS)
|
|
43
|
-
|
|
44
|
-
name = _clean_text("".join(name_elem.itertext())) if name_elem is not None else None
|
|
45
|
-
desc_raw = "".join(desc_elem.itertext()) if desc_elem is not None else None
|
|
46
|
-
desc = _clean_text(desc_raw)
|
|
47
|
-
|
|
48
|
-
rows.append(
|
|
49
|
-
{
|
|
50
|
-
"dataflowID": dataflow_id,
|
|
51
|
-
"agencyID": agency_id,
|
|
52
|
-
"name": name,
|
|
53
|
-
"description": desc,
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
datasets = pd.DataFrame(rows)
|
|
64
|
+
_datasets = pd.DataFrame(rows)
|
|
65
|
+
return _datasets
|
|
58
66
|
|
|
59
67
|
def search_keywords(keywords: Union[str, List[str]]) -> pd.DataFrame:
|
|
60
68
|
"""
|
|
@@ -66,6 +74,7 @@ def search_keywords(keywords: Union[str, List[str]]) -> pd.DataFrame:
|
|
|
66
74
|
Returns:
|
|
67
75
|
pd.DataFrame: Matching rows.
|
|
68
76
|
"""
|
|
77
|
+
datasets = _load_datasets()
|
|
69
78
|
|
|
70
79
|
# Normalize keywords input
|
|
71
80
|
if isinstance(keywords, str): keywords = [keywords]
|
|
@@ -78,10 +87,8 @@ def search_keywords(keywords: Union[str, List[str]]) -> pd.DataFrame:
|
|
|
78
87
|
def _normalize_series(s: pd.Series) -> pd.Series:
|
|
79
88
|
s = s.fillna("").astype(str).str.lower()
|
|
80
89
|
return s.map(
|
|
81
|
-
lambda x: "".join(
|
|
82
|
-
|
|
83
|
-
if not unicodedata.combining(ch)
|
|
84
|
-
)
|
|
90
|
+
lambda x: "".join(ch for ch in unicodedata.normalize("NFKD", x)
|
|
91
|
+
if not unicodedata.combining(ch))
|
|
85
92
|
)
|
|
86
93
|
|
|
87
94
|
# Combined normalized text for each row
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: notoecd
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Unofficial library for interacting with the OECD Data Explorer through Python.
|
|
5
5
|
Author-email: Daniel Vegara Balsa <daniel.vegarabalsa@oecd.org>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dani-37/notoecd
|
|
@@ -15,6 +15,7 @@ Requires-Dist: requests>=2.31
|
|
|
15
15
|
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
16
16
|
|
|
17
17
|
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD datasets.
|
|
18
|
+
|
|
18
19
|
The package provides utilities for:
|
|
19
20
|
|
|
20
21
|
- Discovering dataset metadata
|
|
@@ -110,4 +111,3 @@ The returned object is a pandas DataFrame containing the requested subset of OEC
|
|
|
110
111
|
## Examples
|
|
111
112
|
|
|
112
113
|
You can see this full example as a notebook called example.ipynb.
|
|
113
|
-
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "notoecd"
|
|
7
|
-
version = "0.1.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Unofficial library for interacting with the OECD Data Explorer through Python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
import requests
|
|
3
|
-
import pandas as pd
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def _fake_dataflow_all_xml() -> bytes:
|
|
@@ -44,9 +43,12 @@ class _Resp:
|
|
|
44
43
|
raise requests.HTTPError(f"HTTP {self.status_code}")
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def
|
|
46
|
+
def test_datasets_lazy_loaded_and_cached_in_memory(monkeypatch):
|
|
47
|
+
calls = {"n": 0}
|
|
48
|
+
|
|
48
49
|
def fake_get(url, *args, **kwargs):
|
|
49
50
|
if url.endswith("/public/rest/dataflow/all"):
|
|
51
|
+
calls["n"] += 1
|
|
50
52
|
return _Resp(_fake_dataflow_all_xml())
|
|
51
53
|
raise AssertionError(f"Unexpected URL in test_datasets: {url}")
|
|
52
54
|
|
|
@@ -55,9 +57,18 @@ def test_datasets_dataframe_built_on_import(monkeypatch):
|
|
|
55
57
|
datasets_mod = importlib.import_module("notoecd.datasets")
|
|
56
58
|
importlib.reload(datasets_mod)
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
assert
|
|
60
|
-
|
|
60
|
+
# Import should not fetch
|
|
61
|
+
assert calls["n"] == 0
|
|
62
|
+
|
|
63
|
+
# First search triggers load
|
|
64
|
+
hits = datasets_mod.search_keywords(["gdp"])
|
|
65
|
+
assert calls["n"] == 1
|
|
66
|
+
assert len(hits) == 1
|
|
67
|
+
|
|
68
|
+
# Second search should reuse in-memory cache (no extra fetch)
|
|
69
|
+
hits2 = datasets_mod.search_keywords(["cafe"])
|
|
70
|
+
assert calls["n"] == 1
|
|
71
|
+
assert len(hits2) == 1
|
|
61
72
|
|
|
62
73
|
|
|
63
74
|
def test_search_keywords_or_and_normalization(monkeypatch):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|