notoecd 0.1.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.
- notoecd-0.1.0/PKG-INFO +114 -0
- notoecd-0.1.0/README.md +102 -0
- notoecd-0.1.0/notoecd/notoecd.egg-info/PKG-INFO +114 -0
- notoecd-0.1.0/notoecd/notoecd.egg-info/SOURCES.txt +11 -0
- notoecd-0.1.0/notoecd/notoecd.egg-info/dependency_links.txt +1 -0
- notoecd-0.1.0/notoecd/notoecd.egg-info/requires.txt +2 -0
- notoecd-0.1.0/notoecd/notoecd.egg-info/top_level.txt +1 -0
- notoecd-0.1.0/pyproject.toml +30 -0
- notoecd-0.1.0/setup.cfg +4 -0
- notoecd-0.1.0/tests/test_api.py +11 -0
- notoecd-0.1.0/tests/test_calls.py +70 -0
- notoecd-0.1.0/tests/test_datasets.py +100 -0
- notoecd-0.1.0/tests/test_structure.py +98 -0
notoecd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notoecd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Library for interacting with the OECD Data Explorer through Python
|
|
5
|
+
Author-email: Daniel Vegara Balsa <daniel.vegarabalsa@oecd.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dani-37/notoecd
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pandas>=2.0
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
|
|
13
|
+
# notoecd
|
|
14
|
+
|
|
15
|
+
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
16
|
+
|
|
17
|
+
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD regional datasets.
|
|
18
|
+
The package provides utilities for:
|
|
19
|
+
|
|
20
|
+
- Discovering dataset metadata
|
|
21
|
+
- Searching for relevant datasets using keyword matching
|
|
22
|
+
- Exploring the structure and code lists of a dataset
|
|
23
|
+
- Fetching filtered SDMX data directly into a pandas DataFrame
|
|
24
|
+
|
|
25
|
+
------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
You can install the package by running:
|
|
30
|
+
|
|
31
|
+
pip install notoecd
|
|
32
|
+
|
|
33
|
+
------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
import notoecd
|
|
38
|
+
|
|
39
|
+
The main functions in this module are:
|
|
40
|
+
|
|
41
|
+
search_keywords(keywords) -> pd.DataFrame
|
|
42
|
+
get_structure(agencyID, dataflowID) -> Structure
|
|
43
|
+
get_df(agencyID, dataflowID, filters) -> pd.DataFrame
|
|
44
|
+
|
|
45
|
+
------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
## Searching for datasets
|
|
48
|
+
|
|
49
|
+
`search_keywords` performs:
|
|
50
|
+
|
|
51
|
+
- Normalized text matching
|
|
52
|
+
- Accent-insensitive search
|
|
53
|
+
- Multi-keyword OR matching
|
|
54
|
+
- Ranking by number of matched keywords
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
hits = notoecd.search_keywords(['gross domestic product', 'tl2', 'tl3'])
|
|
59
|
+
|
|
60
|
+
This returns datasets that mention GDP and regional levels (TL2/TL3). It gives their name, description, and identifiers (agencyID and dataflowID), which we will need for the next step.
|
|
61
|
+
|
|
62
|
+
------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
## Inspecting dataset structure
|
|
65
|
+
|
|
66
|
+
Once a dataset is identified, load its SDMX structure:
|
|
67
|
+
|
|
68
|
+
dataset = 'Gross domestic product - Regions'
|
|
69
|
+
agencyID = 'OECD.CFE.EDS'
|
|
70
|
+
dataflowID = 'DSD_REG_ECO@DF_GDP'
|
|
71
|
+
|
|
72
|
+
s = notoecd.get_structure(agencyID, dataflowID)
|
|
73
|
+
|
|
74
|
+
### Table of contents
|
|
75
|
+
|
|
76
|
+
s.toc
|
|
77
|
+
|
|
78
|
+
This shows all filters and their available values.
|
|
79
|
+
|
|
80
|
+
### Exploring code values
|
|
81
|
+
|
|
82
|
+
s.explain_vals('MEASURE')
|
|
83
|
+
s.explain_vals('UNIT_MEASURE')
|
|
84
|
+
|
|
85
|
+
This shows the available measures and units used in the dataset.
|
|
86
|
+
|
|
87
|
+
------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
## Filtering and downloading data
|
|
90
|
+
|
|
91
|
+
To download data, build a dictionary of filters.
|
|
92
|
+
Keys correspond to SDMX dimensions, values are strings or lists (for multiple values):
|
|
93
|
+
|
|
94
|
+
filters = {
|
|
95
|
+
'territorial_level': ['tl2', 'tl3'],
|
|
96
|
+
'measure': 'gdp',
|
|
97
|
+
'prices': 'Q',
|
|
98
|
+
'unit_measure': 'USD_PPP_PS'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Fetch the filtered dataset:
|
|
102
|
+
|
|
103
|
+
df = notoecd.get_df(agency, dataflow, filters)
|
|
104
|
+
df.head()
|
|
105
|
+
|
|
106
|
+
The returned object is a pandas DataFrame containing the requested subset of OECD SDMX data.
|
|
107
|
+
|
|
108
|
+
------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
You can see this full example as a notebook called example.ipynb.
|
|
113
|
+
|
|
114
|
+
|
notoecd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# notoecd
|
|
2
|
+
|
|
3
|
+
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
4
|
+
|
|
5
|
+
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD regional datasets.
|
|
6
|
+
The package provides utilities for:
|
|
7
|
+
|
|
8
|
+
- Discovering dataset metadata
|
|
9
|
+
- Searching for relevant datasets using keyword matching
|
|
10
|
+
- Exploring the structure and code lists of a dataset
|
|
11
|
+
- Fetching filtered SDMX data directly into a pandas DataFrame
|
|
12
|
+
|
|
13
|
+
------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
You can install the package by running:
|
|
18
|
+
|
|
19
|
+
pip install notoecd
|
|
20
|
+
|
|
21
|
+
------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
import notoecd
|
|
26
|
+
|
|
27
|
+
The main functions in this module are:
|
|
28
|
+
|
|
29
|
+
search_keywords(keywords) -> pd.DataFrame
|
|
30
|
+
get_structure(agencyID, dataflowID) -> Structure
|
|
31
|
+
get_df(agencyID, dataflowID, filters) -> pd.DataFrame
|
|
32
|
+
|
|
33
|
+
------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
## Searching for datasets
|
|
36
|
+
|
|
37
|
+
`search_keywords` performs:
|
|
38
|
+
|
|
39
|
+
- Normalized text matching
|
|
40
|
+
- Accent-insensitive search
|
|
41
|
+
- Multi-keyword OR matching
|
|
42
|
+
- Ranking by number of matched keywords
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
|
|
46
|
+
hits = notoecd.search_keywords(['gross domestic product', 'tl2', 'tl3'])
|
|
47
|
+
|
|
48
|
+
This returns datasets that mention GDP and regional levels (TL2/TL3). It gives their name, description, and identifiers (agencyID and dataflowID), which we will need for the next step.
|
|
49
|
+
|
|
50
|
+
------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
## Inspecting dataset structure
|
|
53
|
+
|
|
54
|
+
Once a dataset is identified, load its SDMX structure:
|
|
55
|
+
|
|
56
|
+
dataset = 'Gross domestic product - Regions'
|
|
57
|
+
agencyID = 'OECD.CFE.EDS'
|
|
58
|
+
dataflowID = 'DSD_REG_ECO@DF_GDP'
|
|
59
|
+
|
|
60
|
+
s = notoecd.get_structure(agencyID, dataflowID)
|
|
61
|
+
|
|
62
|
+
### Table of contents
|
|
63
|
+
|
|
64
|
+
s.toc
|
|
65
|
+
|
|
66
|
+
This shows all filters and their available values.
|
|
67
|
+
|
|
68
|
+
### Exploring code values
|
|
69
|
+
|
|
70
|
+
s.explain_vals('MEASURE')
|
|
71
|
+
s.explain_vals('UNIT_MEASURE')
|
|
72
|
+
|
|
73
|
+
This shows the available measures and units used in the dataset.
|
|
74
|
+
|
|
75
|
+
------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
## Filtering and downloading data
|
|
78
|
+
|
|
79
|
+
To download data, build a dictionary of filters.
|
|
80
|
+
Keys correspond to SDMX dimensions, values are strings or lists (for multiple values):
|
|
81
|
+
|
|
82
|
+
filters = {
|
|
83
|
+
'territorial_level': ['tl2', 'tl3'],
|
|
84
|
+
'measure': 'gdp',
|
|
85
|
+
'prices': 'Q',
|
|
86
|
+
'unit_measure': 'USD_PPP_PS'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Fetch the filtered dataset:
|
|
90
|
+
|
|
91
|
+
df = notoecd.get_df(agency, dataflow, filters)
|
|
92
|
+
df.head()
|
|
93
|
+
|
|
94
|
+
The returned object is a pandas DataFrame containing the requested subset of OECD SDMX data.
|
|
95
|
+
|
|
96
|
+
------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
## Examples
|
|
99
|
+
|
|
100
|
+
You can see this full example as a notebook called example.ipynb.
|
|
101
|
+
|
|
102
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notoecd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Library for interacting with the OECD Data Explorer through Python
|
|
5
|
+
Author-email: Daniel Vegara Balsa <daniel.vegarabalsa@oecd.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dani-37/notoecd
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: pandas>=2.0
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
|
|
13
|
+
# notoecd
|
|
14
|
+
|
|
15
|
+
⚠️ **Unofficial package, not endorsed by the OECD.**
|
|
16
|
+
|
|
17
|
+
A lightweight Python interface for exploring OECD SDMX structures and downloading OECD regional datasets.
|
|
18
|
+
The package provides utilities for:
|
|
19
|
+
|
|
20
|
+
- Discovering dataset metadata
|
|
21
|
+
- Searching for relevant datasets using keyword matching
|
|
22
|
+
- Exploring the structure and code lists of a dataset
|
|
23
|
+
- Fetching filtered SDMX data directly into a pandas DataFrame
|
|
24
|
+
|
|
25
|
+
------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
You can install the package by running:
|
|
30
|
+
|
|
31
|
+
pip install notoecd
|
|
32
|
+
|
|
33
|
+
------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
import notoecd
|
|
38
|
+
|
|
39
|
+
The main functions in this module are:
|
|
40
|
+
|
|
41
|
+
search_keywords(keywords) -> pd.DataFrame
|
|
42
|
+
get_structure(agencyID, dataflowID) -> Structure
|
|
43
|
+
get_df(agencyID, dataflowID, filters) -> pd.DataFrame
|
|
44
|
+
|
|
45
|
+
------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
## Searching for datasets
|
|
48
|
+
|
|
49
|
+
`search_keywords` performs:
|
|
50
|
+
|
|
51
|
+
- Normalized text matching
|
|
52
|
+
- Accent-insensitive search
|
|
53
|
+
- Multi-keyword OR matching
|
|
54
|
+
- Ranking by number of matched keywords
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
hits = notoecd.search_keywords(['gross domestic product', 'tl2', 'tl3'])
|
|
59
|
+
|
|
60
|
+
This returns datasets that mention GDP and regional levels (TL2/TL3). It gives their name, description, and identifiers (agencyID and dataflowID), which we will need for the next step.
|
|
61
|
+
|
|
62
|
+
------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
## Inspecting dataset structure
|
|
65
|
+
|
|
66
|
+
Once a dataset is identified, load its SDMX structure:
|
|
67
|
+
|
|
68
|
+
dataset = 'Gross domestic product - Regions'
|
|
69
|
+
agencyID = 'OECD.CFE.EDS'
|
|
70
|
+
dataflowID = 'DSD_REG_ECO@DF_GDP'
|
|
71
|
+
|
|
72
|
+
s = notoecd.get_structure(agencyID, dataflowID)
|
|
73
|
+
|
|
74
|
+
### Table of contents
|
|
75
|
+
|
|
76
|
+
s.toc
|
|
77
|
+
|
|
78
|
+
This shows all filters and their available values.
|
|
79
|
+
|
|
80
|
+
### Exploring code values
|
|
81
|
+
|
|
82
|
+
s.explain_vals('MEASURE')
|
|
83
|
+
s.explain_vals('UNIT_MEASURE')
|
|
84
|
+
|
|
85
|
+
This shows the available measures and units used in the dataset.
|
|
86
|
+
|
|
87
|
+
------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
## Filtering and downloading data
|
|
90
|
+
|
|
91
|
+
To download data, build a dictionary of filters.
|
|
92
|
+
Keys correspond to SDMX dimensions, values are strings or lists (for multiple values):
|
|
93
|
+
|
|
94
|
+
filters = {
|
|
95
|
+
'territorial_level': ['tl2', 'tl3'],
|
|
96
|
+
'measure': 'gdp',
|
|
97
|
+
'prices': 'Q',
|
|
98
|
+
'unit_measure': 'USD_PPP_PS'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Fetch the filtered dataset:
|
|
102
|
+
|
|
103
|
+
df = notoecd.get_df(agency, dataflow, filters)
|
|
104
|
+
df.head()
|
|
105
|
+
|
|
106
|
+
The returned object is a pandas DataFrame containing the requested subset of OECD SDMX data.
|
|
107
|
+
|
|
108
|
+
------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
You can see this full example as a notebook called example.ipynb.
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
notoecd/notoecd.egg-info/PKG-INFO
|
|
4
|
+
notoecd/notoecd.egg-info/SOURCES.txt
|
|
5
|
+
notoecd/notoecd.egg-info/dependency_links.txt
|
|
6
|
+
notoecd/notoecd.egg-info/requires.txt
|
|
7
|
+
notoecd/notoecd.egg-info/top_level.txt
|
|
8
|
+
tests/test_api.py
|
|
9
|
+
tests/test_calls.py
|
|
10
|
+
tests/test_datasets.py
|
|
11
|
+
tests/test_structure.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "notoecd"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Library for interacting with the OECD Data Explorer through Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Daniel Vegara Balsa", email = "daniel.vegarabalsa@oecd.org" }
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pandas>=2.0",
|
|
20
|
+
"requests>=2.31"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/dani-37/notoecd"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = {"" = "notoecd"}
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["notoecd"]
|
notoecd-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import notoecd
|
|
2
|
+
|
|
3
|
+
def test_public_api_exports():
|
|
4
|
+
assert callable(notoecd.get_df)
|
|
5
|
+
assert callable(notoecd.get_structure)
|
|
6
|
+
assert callable(notoecd.search_keywords)
|
|
7
|
+
|
|
8
|
+
def test_import_package():
|
|
9
|
+
import importlib
|
|
10
|
+
m = importlib.import_module("notoecd")
|
|
11
|
+
assert m is not None
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
import notoecd.calls as calls
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _fake_structure_with_toc_titles(titles):
|
|
8
|
+
toc = pd.DataFrame({"title": titles})
|
|
9
|
+
return SimpleNamespace(toc=toc)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_build_filter_expression_orders_by_toc_and_uppercases():
|
|
13
|
+
fake_s = _fake_structure_with_toc_titles(["PRICES", "UNIT_MEASURE", "MEASURE"])
|
|
14
|
+
filters = {"prices": "q", "unit_measure": ["USD_PPP_PS"], "measure": "gdp"}
|
|
15
|
+
|
|
16
|
+
with patch("notoecd.calls.get_structure", return_value=fake_s):
|
|
17
|
+
expr = calls._build_filter_expression("A", "B", filters)
|
|
18
|
+
|
|
19
|
+
assert expr == "Q.USD_PPP_PS.GDP"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_build_filter_expression_missing_dims_are_empty_parts():
|
|
23
|
+
fake_s = _fake_structure_with_toc_titles(["A", "B", "C"])
|
|
24
|
+
|
|
25
|
+
with patch("notoecd.calls.get_structure", return_value=fake_s):
|
|
26
|
+
expr = calls._build_filter_expression("A", "B", {"b": "x"})
|
|
27
|
+
|
|
28
|
+
assert expr == ".X."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_build_filter_expression_multi_value_joins_plus():
|
|
32
|
+
fake_s = _fake_structure_with_toc_titles(["territorial_level"])
|
|
33
|
+
|
|
34
|
+
with patch("notoecd.calls.get_structure", return_value=fake_s):
|
|
35
|
+
expr = calls._build_filter_expression("A", "B", {"territorial_level": ["tl2", "tl3"]})
|
|
36
|
+
|
|
37
|
+
assert expr == "TL2+TL3"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_get_df_builds_url_and_returns_copy():
|
|
41
|
+
calls._fetch_df.cache_clear()
|
|
42
|
+
|
|
43
|
+
fake_s = _fake_structure_with_toc_titles(["PRICES"])
|
|
44
|
+
fake_df = pd.DataFrame({"x": [1, 2]})
|
|
45
|
+
|
|
46
|
+
with patch("notoecd.calls.get_structure", return_value=fake_s), \
|
|
47
|
+
patch("notoecd.calls.pd.read_csv", return_value=fake_df) as mock_read_csv:
|
|
48
|
+
out = calls.get_df("OECD.CFE.EDS", "DSD_REG_ECO@DF_GDP", {"prices": "q"})
|
|
49
|
+
|
|
50
|
+
assert out.equals(fake_df)
|
|
51
|
+
assert out is not fake_df # must be a copy()
|
|
52
|
+
|
|
53
|
+
(url,), kwargs = mock_read_csv.call_args
|
|
54
|
+
assert url.startswith("https://sdmx.oecd.org/public/rest/data/")
|
|
55
|
+
assert "OECD.CFE.EDS,DSD_REG_ECO@DF_GDP," in url
|
|
56
|
+
assert "/Q" in url
|
|
57
|
+
assert "dimensionAtObservation=AllDimensions" in url
|
|
58
|
+
assert "format=csvfile" in url
|
|
59
|
+
assert kwargs["storage_options"]["User-Agent"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_get_df_accepts_string_filter_expression_and_uppercases():
|
|
63
|
+
calls._fetch_df.cache_clear()
|
|
64
|
+
|
|
65
|
+
fake_df = pd.DataFrame({"x": [1]})
|
|
66
|
+
with patch("notoecd.calls.pd.read_csv", return_value=fake_df) as mock_read_csv:
|
|
67
|
+
_ = calls.get_df("A", "B", " tl2+tl3..gdp ")
|
|
68
|
+
|
|
69
|
+
(url,), _ = mock_read_csv.call_args
|
|
70
|
+
assert "/TL2+TL3..GDP" in url
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _fake_dataflow_all_xml() -> bytes:
|
|
7
|
+
xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
8
|
+
<message:Structure
|
|
9
|
+
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message"
|
|
10
|
+
xmlns:structure="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/structure"
|
|
11
|
+
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common"
|
|
12
|
+
xmlns:xml="http://www.w3.org/XML/1998/namespace"
|
|
13
|
+
>
|
|
14
|
+
<message:Structures>
|
|
15
|
+
<structure:Dataflows>
|
|
16
|
+
<structure:Dataflow id="DSD_REG_ECO@DF_GDP" agencyID="OECD.CFE.EDS">
|
|
17
|
+
<common:Name xml:lang="en">Gross domestic product - Regions</common:Name>
|
|
18
|
+
<common:Description xml:lang="en">GDP by region</common:Description>
|
|
19
|
+
</structure:Dataflow>
|
|
20
|
+
|
|
21
|
+
<structure:Dataflow id="DF_CAFE" agencyID="OECD.TEST">
|
|
22
|
+
<common:Name xml:lang="en">Café prices</common:Name>
|
|
23
|
+
<common:Description xml:lang="en">Prices in cafes</common:Description>
|
|
24
|
+
</structure:Dataflow>
|
|
25
|
+
|
|
26
|
+
<structure:Dataflow id="DF_OTHER" agencyID="OECD">
|
|
27
|
+
<common:Name xml:lang="en">Other dataset</common:Name>
|
|
28
|
+
<common:Description xml:lang="en">Other description</common:Description>
|
|
29
|
+
</structure:Dataflow>
|
|
30
|
+
</structure:Dataflows>
|
|
31
|
+
</message:Structures>
|
|
32
|
+
</message:Structure>
|
|
33
|
+
"""
|
|
34
|
+
return xml.encode("utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Resp:
|
|
38
|
+
def __init__(self, content: bytes, status_code: int = 200):
|
|
39
|
+
self.content = content
|
|
40
|
+
self.status_code = status_code
|
|
41
|
+
|
|
42
|
+
def raise_for_status(self):
|
|
43
|
+
if self.status_code >= 400:
|
|
44
|
+
raise requests.HTTPError(f"HTTP {self.status_code}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_datasets_dataframe_built_on_import(monkeypatch):
|
|
48
|
+
def fake_get(url, *args, **kwargs):
|
|
49
|
+
if url.endswith("/public/rest/dataflow/all"):
|
|
50
|
+
return _Resp(_fake_dataflow_all_xml())
|
|
51
|
+
raise AssertionError(f"Unexpected URL in test_datasets: {url}")
|
|
52
|
+
|
|
53
|
+
monkeypatch.setattr(requests, "get", fake_get)
|
|
54
|
+
|
|
55
|
+
datasets_mod = importlib.import_module("notoecd.datasets")
|
|
56
|
+
importlib.reload(datasets_mod)
|
|
57
|
+
|
|
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
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_search_keywords_or_and_normalization(monkeypatch):
|
|
64
|
+
def fake_get(url, *args, **kwargs):
|
|
65
|
+
if url.endswith("/public/rest/dataflow/all"):
|
|
66
|
+
return _Resp(_fake_dataflow_all_xml())
|
|
67
|
+
raise AssertionError(f"Unexpected URL in test_datasets: {url}")
|
|
68
|
+
|
|
69
|
+
monkeypatch.setattr(requests, "get", fake_get)
|
|
70
|
+
|
|
71
|
+
datasets_mod = importlib.import_module("notoecd.datasets")
|
|
72
|
+
importlib.reload(datasets_mod)
|
|
73
|
+
|
|
74
|
+
# OR behavior: should match GDP OR tl2 (not present) OR cafe (accent-insensitive)
|
|
75
|
+
hits = datasets_mod.search_keywords(["gross domestic product", "cafe"])
|
|
76
|
+
|
|
77
|
+
assert len(hits) == 2
|
|
78
|
+
assert any(hits["dataflowID"] == "DSD_REG_ECO@DF_GDP")
|
|
79
|
+
assert any(hits["dataflowID"] == "DF_CAFE")
|
|
80
|
+
|
|
81
|
+
names = " ".join(hits["name"].fillna("").tolist()).lower()
|
|
82
|
+
assert ("gross domestic product" in names) or ("café" in names) or ("cafe" in names)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_search_keywords_rejects_empty(monkeypatch):
|
|
86
|
+
def fake_get(url, *args, **kwargs):
|
|
87
|
+
if url.endswith("/public/rest/dataflow/all"):
|
|
88
|
+
return _Resp(_fake_dataflow_all_xml())
|
|
89
|
+
raise AssertionError(f"Unexpected URL in test_datasets: {url}")
|
|
90
|
+
|
|
91
|
+
monkeypatch.setattr(requests, "get", fake_get)
|
|
92
|
+
|
|
93
|
+
datasets_mod = importlib.import_module("notoecd.datasets")
|
|
94
|
+
importlib.reload(datasets_mod)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
datasets_mod.search_keywords([" ", ""])
|
|
98
|
+
raise AssertionError("Expected ValueError for empty keywords")
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _fake_structure_xml() -> bytes:
|
|
7
|
+
return b"""<?xml version="1.0" encoding="UTF-8"?>
|
|
8
|
+
<message:Structure
|
|
9
|
+
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message"
|
|
10
|
+
xmlns:structure="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/structure"
|
|
11
|
+
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common"
|
|
12
|
+
xmlns:xml="http://www.w3.org/XML/1998/namespace"
|
|
13
|
+
>
|
|
14
|
+
<message:Structures>
|
|
15
|
+
|
|
16
|
+
<structure:Concepts>
|
|
17
|
+
<structure:ConceptScheme>
|
|
18
|
+
<structure:Concept id="PRICES">
|
|
19
|
+
<common:Name xml:lang="en">Prices</common:Name>
|
|
20
|
+
<structure:CoreRepresentation>
|
|
21
|
+
<structure:Enumeration>
|
|
22
|
+
<Ref id="CL_PRICES"/>
|
|
23
|
+
</structure:Enumeration>
|
|
24
|
+
</structure:CoreRepresentation>
|
|
25
|
+
</structure:Concept>
|
|
26
|
+
</structure:ConceptScheme>
|
|
27
|
+
</structure:Concepts>
|
|
28
|
+
|
|
29
|
+
<structure:Codelists>
|
|
30
|
+
<structure:Codelist id="CL_PRICES">
|
|
31
|
+
<structure:Code id="Q">
|
|
32
|
+
<common:Name xml:lang="en">Quarterly</common:Name>
|
|
33
|
+
</structure:Code>
|
|
34
|
+
<structure:Code id="V">
|
|
35
|
+
<common:Name xml:lang="en">Volume</common:Name>
|
|
36
|
+
</structure:Code>
|
|
37
|
+
</structure:Codelist>
|
|
38
|
+
</structure:Codelists>
|
|
39
|
+
|
|
40
|
+
<structure:Constraints>
|
|
41
|
+
<structure:ContentConstraint>
|
|
42
|
+
<structure:CubeRegion>
|
|
43
|
+
<common:KeyValue id="PRICES">
|
|
44
|
+
<common:Value>Q</common:Value>
|
|
45
|
+
<common:Value>V</common:Value>
|
|
46
|
+
</common:KeyValue>
|
|
47
|
+
</structure:CubeRegion>
|
|
48
|
+
</structure:ContentConstraint>
|
|
49
|
+
</structure:Constraints>
|
|
50
|
+
|
|
51
|
+
<structure:DataStructures>
|
|
52
|
+
<structure:DataStructure>
|
|
53
|
+
<structure:DataStructureComponents>
|
|
54
|
+
<structure:DimensionList>
|
|
55
|
+
<structure:Dimension id="PRICES" position="1"/>
|
|
56
|
+
</structure:DimensionList>
|
|
57
|
+
</structure:DataStructureComponents>
|
|
58
|
+
</structure:DataStructure>
|
|
59
|
+
</structure:DataStructures>
|
|
60
|
+
|
|
61
|
+
</message:Structures>
|
|
62
|
+
</message:Structure>
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Resp:
|
|
67
|
+
def __init__(self, content: bytes, status_code: int = 200):
|
|
68
|
+
self.content = content
|
|
69
|
+
self.status_code = status_code
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_get_structure_builds_toc_values_and_explain(monkeypatch):
|
|
73
|
+
def fake_get(url, *args, **kwargs):
|
|
74
|
+
if "/public/rest/dataflow/" in url and "?references=all" in url:
|
|
75
|
+
return _Resp(_fake_structure_xml())
|
|
76
|
+
raise AssertionError(f"Unexpected URL in test_structure: {url}")
|
|
77
|
+
|
|
78
|
+
# Patch the requests used by notoecd.structure
|
|
79
|
+
monkeypatch.setattr(requests, "get", fake_get)
|
|
80
|
+
|
|
81
|
+
structure_mod = importlib.import_module("notoecd.structure")
|
|
82
|
+
importlib.reload(structure_mod)
|
|
83
|
+
|
|
84
|
+
# Clear cache so test is isolated
|
|
85
|
+
structure_mod.get_structure.cache_clear()
|
|
86
|
+
|
|
87
|
+
s = structure_mod.get_structure("OECD.CFE.EDS", "DSD_REG_ECO@DF_GDP")
|
|
88
|
+
|
|
89
|
+
assert isinstance(s.toc, pd.DataFrame)
|
|
90
|
+
assert list(s.toc["title"]) == ["PRICES"]
|
|
91
|
+
assert s.toc.loc[0, "values"] == ["Q", "V"]
|
|
92
|
+
|
|
93
|
+
assert isinstance(s.concepts, dict)
|
|
94
|
+
assert "CODELISTS" in s.concepts
|
|
95
|
+
assert "PRICES" in s.concepts
|
|
96
|
+
|
|
97
|
+
d = s.explain_vals("PRICES")
|
|
98
|
+
assert d == {"Q": "Quarterly", "V": "Volume"}
|