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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notoecd
3
- Version: 0.1.1
4
- Summary: Library for interacting with the OECD Data Explorer through Python
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.
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.1.2"
2
2
 
3
3
  from .calls import get_df
4
4
  from .structure import get_structure
@@ -18,43 +18,51 @@ NS = {
18
18
  _ws_re = re.compile(r"\s+")
19
19
  _tag_re = re.compile(r"<[^>]+>")
20
20
 
21
- def _clean_text(s: str | None) -> str | None:
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
- headers = {
29
- "Accept": "application/vnd.sdmx.structure+xml;version=2.1"
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
- r = requests.get(url, headers=headers, timeout=30)
33
- r.raise_for_status()
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
- ch for ch in unicodedata.normalize("NFKD", x)
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.1
4
- Summary: Library for interacting with the OECD Data Explorer through Python
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.1"
8
- description = "Library for interacting with the OECD Data Explorer through Python"
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 test_datasets_dataframe_built_on_import(monkeypatch):
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
- assert isinstance(datasets_mod.datasets, pd.DataFrame)
59
- assert {"agencyID", "dataflowID", "name", "description"}.issubset(datasets_mod.datasets.columns)
60
- assert len(datasets_mod.datasets) == 3
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