Skip to content

Commit 7294374

Browse files
committed
fix(sdmx): Address all review comments
This commit addresses all outstanding review comments by: - Refactoring the SDMX utilities into an `SdmxClient` class to improve design and remove duplication. - Sanitizing the `dataflow_id` for use in filenames. - Modifying the sample scripts to write their output to a dedicated `output/` directory within the samples folder. - Removing redundant `try...except` blocks from the sample scripts and ensuring non-zero exit codes on failure.
1 parent 5f900b0 commit 7294374

6 files changed

Lines changed: 133 additions & 197 deletions

File tree

tools/sdmx/dataflow.py

Lines changed: 97 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""
22
dataflow.py
33
4-
This module provides reusable, generalized functions to interact with SDMX APIs
5-
by connecting to a specified REST endpoint.
4+
This module provides a client class for interacting with SDMX APIs.
65
"""
76

87
import logging
@@ -12,159 +11,101 @@
1211
from typing import Dict, Any
1312

1413

15-
def _get_sdmx_client(agency_id: str, endpoint: str) -> sdmx.Client:
16-
"""
17-
Creates and configures an sdmx.Client for a given agency and endpoint.
18-
19-
A unique source ID is created for each agency-endpoint combination to avoid
20-
caching issues. The source is overridden to ensure the correct endpoint is
21-
always used.
22-
"""
23-
source_id = agency_id
24-
custom_source = {
25-
'id': source_id,
26-
'url': endpoint,
27-
'name': f'Custom source for {agency_id}'
28-
}
29-
sdmx.add_source(custom_source, override=True)
30-
return sdmx.Client(source_id)
31-
32-
33-
def fetch_and_save_metadata(dataflow_id: str, agency_id: str, output_path: str,
34-
endpoint: str):
35-
"""
36-
Fetches the complete metadata for a dataflow and saves the raw
37-
SDMX-ML (XML) response to a file.
38-
39-
Args:
40-
dataflow_id (str): The ID of the dataflow (e.g., 'DF_QNA_EXPENDITURE_GROWTH_OECD').
41-
agency_id (str): The ID of the agency providing the data (e.g., 'OECD.SDD.NAD').
42-
output_path (str): The file path where the raw XML metadata will be saved.
43-
endpoint (str): The base URL of the SDMX REST API endpoint.
44-
45-
Raises:
46-
HTTPError: If a network error occurs during the API request.
47-
Exception: For other unexpected errors.
48-
49-
Usage:
50-
fetch_and_save_metadata(
51-
dataflow_id="DSD_NAMAIN1@DF_QNA_EXPENDITURE_GROWTH_OECD",
52-
agency_id="OECD.SDD.NAD",
53-
output_path="gdp_growth_metadata.xml",
54-
endpoint="https://sdmx.oecd.org/public/rest/"
55-
)
56-
"""
57-
try:
58-
client = _get_sdmx_client(agency_id, endpoint)
59-
60-
logging.info(f"Fetching raw metadata for dataflow: {dataflow_id}...")
61-
62-
flow_msg = client.dataflow(dataflow_id,
63-
agency_id=agency_id,
64-
params={'references': 'all'})
65-
logging.info(
66-
f"Successfully received response from the server: {flow_msg.response.url}"
67-
)
68-
69-
raw_metadata_content = flow_msg.response.text
70-
with open(output_path, "w", encoding="utf-8") as f:
71-
f.write(raw_metadata_content)
72-
logging.info(f"Successfully saved raw metadata to '{output_path}'")
73-
74-
except HTTPError as e:
75-
logging.error(
76-
f"Network error while downloading dataflow metadata for {agency_id}/{dataflow_id}: {e}"
77-
)
78-
if e.response:
79-
safe_df_id = dataflow_id.replace('@', '_')
80-
error_filename = f"metadata_error_{safe_df_id}.html"
81-
with open(error_filename, "w", encoding="utf-8") as f:
82-
f.write(e.response.text)
83-
logging.error(f"URL: {e.response.url}")
84-
logging.error(
85-
f"Response content saved to '{error_filename}' for debugging.")
86-
raise
87-
except Exception as e:
88-
logging.error(
89-
f"An error occurred while processing dataflow metadata for {agency_id}/{dataflow_id}: {e}"
90-
)
91-
raise
92-
93-
94-
def fetch_and_save_data_as_csv(dataflow_id: str, agency_id: str,
95-
key: Dict[str, Any], params: Dict[str, Any],
96-
output_path: str, endpoint: str):
97-
"""
98-
Fetches data from an SDMX API, converts it to a tidy pandas DataFrame,
99-
and saves it as a CSV file.
100-
101-
Args:
102-
dataflow_id (str): The ID of the dataflow.
103-
agency_id (str): The ID of the agency.
104-
key (dict): A dictionary defining the slice of data to query.
105-
params (dict): A dictionary of query parameters (e.g., startPeriod).
106-
output_path (str): The file path where the final CSV data will be saved.
107-
endpoint (str): The base URL of the SDMX REST API endpoint.
108-
109-
Raises:
110-
HTTPError: If a network error occurs during the API request.
111-
Exception: For other unexpected errors.
112-
113-
Usage:
114-
DATA_KEY = {
115-
'FREQ': 'Q',
116-
'REF_AREA': 'USA+CAN+MEX',
117-
'TRANSACTION': 'B1GQ',
118-
'TRANSFORMATION': 'G1'
14+
class SdmxClient:
15+
"""A client for fetching data and metadata from an SDMX REST API."""
16+
17+
def __init__(self, endpoint: str, agency_id: str):
18+
"""
19+
Initializes the SdmxClient.
20+
21+
Args:
22+
endpoint (str): The base URL of the SDMX REST API endpoint.
23+
agency_id (str): The ID of the agency providing the data.
24+
"""
25+
self.agency_id = agency_id
26+
self.endpoint = endpoint
27+
self.client = self._get_sdmx_client()
28+
29+
def _get_sdmx_client(self) -> sdmx.Client:
30+
"""
31+
Creates and configures an sdmx.Client for the specified endpoint and agency.
32+
"""
33+
source_id = self.agency_id
34+
custom_source = {
35+
'id': source_id,
36+
'url': self.endpoint,
37+
'name': f'Custom source for {self.agency_id}'
11938
}
120-
DATA_PARAMS = {
121-
'startPeriod': '2022',
122-
'endPeriod': '2023'
123-
}
124-
fetch_and_save_data_as_csv(
125-
dataflow_id="DSD_NAMAIN1@DF_QNA_EXPENDITURE_GROWTH_OECD",
126-
agency_id="OECD.SDD.NAD",
127-
key=DATA_KEY,
128-
params=DATA_PARAMS,
129-
output_path="gdp_growth_data.csv",
130-
endpoint="https://sdmx.oecd.org/public/rest/"
131-
)
132-
"""
133-
try:
134-
client = _get_sdmx_client(agency_id, endpoint)
135-
136-
logging.info(f"Fetching data for key: {key}")
137-
138-
data_msg = client.data(dataflow_id,
139-
key=key,
140-
params=params,
141-
agency_id=agency_id)
142-
logging.info(
143-
f"Successfully received response from the server: {data_msg.response.url}"
144-
)
145-
146-
data_series = sdmx.to_pandas(data_msg)
147-
df_tidy = data_series.reset_index()
148-
149-
df_tidy.to_csv(output_path, index=False)
150-
logging.info(
151-
f"Successfully converted to CSV data and saved to '{output_path}'")
152-
153-
except HTTPError as e:
154-
logging.error(
155-
f"Network error while downloading data for {agency_id}/{dataflow_id}: {e}"
156-
)
157-
if e.response:
158-
safe_df_id = dataflow_id.replace('@', '_')
159-
error_filename = f"data_error_{safe_df_id}.html"
160-
with open(error_filename, "w", encoding="utf-8") as f:
161-
f.write(e.response.text)
162-
logging.error(f"URL: {e.response.url}")
39+
sdmx.add_source(custom_source, override=True)
40+
return sdmx.Client(source_id)
41+
42+
def fetch_and_save_metadata(self, dataflow_id: str, output_path: str):
43+
"""
44+
Fetches the complete metadata for a dataflow and saves the raw
45+
SDMX-ML (XML) response to a file.
46+
"""
47+
try:
48+
logging.info(
49+
f"Fetching raw metadata for dataflow: {dataflow_id}...")
50+
flow_msg = self.client.dataflow(dataflow_id,
51+
agency_id=self.agency_id,
52+
params={'references': 'all'})
53+
logging.info(
54+
f"Successfully received response: {flow_msg.response.url}")
55+
56+
raw_content = flow_msg.response.text
57+
with open(output_path, "w", encoding="utf-8") as f:
58+
f.write(raw_content)
59+
logging.info(f"Successfully saved metadata to '{output_path}'")
60+
61+
except HTTPError as e:
62+
logging.error(
63+
f"Network error for {self.agency_id}/{dataflow_id}: {e}")
64+
if e.response:
65+
safe_df_id = dataflow_id.replace('@', '_')
66+
error_filename = f"metadata_error_{safe_df_id}.html"
67+
with open(error_filename, "w", encoding="utf-8") as f:
68+
f.write(e.response.text)
69+
logging.error(f"URL: {e.response.url}")
70+
logging.error(f"Response saved to '{error_filename}'")
71+
raise
72+
except Exception as e:
73+
logging.error(
74+
f"Error processing metadata for {self.agency_id}/{dataflow_id}: {e}"
75+
)
76+
raise
77+
78+
def fetch_and_save_data_as_csv(self, dataflow_id: str, key: Dict[str, Any],
79+
params: Dict[str, Any], output_path: str):
80+
"""
81+
Fetches data, converts it to a pandas DataFrame, and saves as CSV.
82+
"""
83+
try:
84+
logging.info(f"Fetching data for key: {key}")
85+
data_msg = self.client.data(dataflow_id,
86+
key=key,
87+
params=params,
88+
agency_id=self.agency_id)
89+
logging.info(
90+
f"Successfully received response: {data_msg.response.url}")
91+
92+
df = sdmx.to_pandas(data_msg).reset_index()
93+
df.to_csv(output_path, index=False)
94+
logging.info(f"Successfully saved data to '{output_path}'")
95+
96+
except HTTPError as e:
97+
logging.error(
98+
f"Network error for {self.agency_id}/{dataflow_id}: {e}")
99+
if e.response:
100+
safe_df_id = dataflow_id.replace('@', '_')
101+
error_filename = f"data_error_{safe_df_id}.html"
102+
with open(error_filename, "w", encoding="utf-8") as f:
103+
f.write(e.response.text)
104+
logging.error(f"URL: {e.response.url}")
105+
logging.error(f"Response saved to '{error_filename}'")
106+
raise
107+
except Exception as e:
163108
logging.error(
164-
f"Response content saved to '{error_filename}' for debugging.")
165-
raise
166-
except Exception as e:
167-
logging.error(
168-
f"An error occurred while processing data for {agency_id}/{dataflow_id}: {e}"
169-
)
170-
raise
109+
f"Error processing data for {self.agency_id}/{dataflow_id}: {e}"
110+
)
111+
raise

tools/sdmx/samples/fetch_eurostat_gdp_data.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.append(
1414
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
1515

16-
from tools.sdmx import dataflow
16+
from tools.sdmx.dataflow import SdmxClient
1717

1818
# Configure logging
1919
logging.basicConfig(level=logging.INFO,
@@ -44,13 +44,12 @@ def main():
4444

4545
logging.info(f"--- Fetching Eurostat Data: {dataflow_id} ---")
4646

47-
# --- 2. Use the Reusable Function ---
48-
dataflow.fetch_and_save_data_as_csv(dataflow_id=dataflow_id,
49-
agency_id=agency_id,
50-
key=data_key,
51-
params=data_params,
52-
output_path=output_path,
53-
endpoint=endpoint)
47+
# --- 2. Use the SdmxClient ---
48+
client = SdmxClient(endpoint, agency_id)
49+
client.fetch_and_save_data_as_csv(dataflow_id=dataflow_id,
50+
key=data_key,
51+
params=data_params,
52+
output_path=output_path)
5453
logging.info(f"--- Successfully downloaded data to {output_path} ---")
5554

5655

tools/sdmx/samples/fetch_eurostat_gdp_metadata.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.append(
1414
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
1515

16-
from tools.sdmx import dataflow
16+
from tools.sdmx.dataflow import SdmxClient
1717

1818
# Configure logging
1919
logging.basicConfig(level=logging.INFO,
@@ -34,11 +34,10 @@ def main():
3434

3535
logging.info(f"--- Fetching Eurostat Metadata: {dataflow_id} ---")
3636

37-
# --- 2. Use the Reusable Function ---
38-
dataflow.fetch_and_save_metadata(dataflow_id=dataflow_id,
39-
agency_id=agency_id,
40-
output_path=output_path,
41-
endpoint=endpoint)
37+
# --- 2. Use the SdmxClient ---
38+
client = SdmxClient(endpoint, agency_id)
39+
client.fetch_and_save_metadata(dataflow_id=dataflow_id,
40+
output_path=output_path)
4241
logging.info(f"--- Successfully downloaded metadata to {output_path} ---")
4342

4443

tools/sdmx/samples/fetch_oecd_full_gdp_dataset.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.append(
1414
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
1515

16-
from tools.sdmx import dataflow
16+
from tools.sdmx.dataflow import SdmxClient
1717

1818
# Configure logging
1919
logging.basicConfig(level=logging.INFO,
@@ -34,13 +34,14 @@ def main():
3434
"oecd_gdp_full_metadata.xml")
3535
data_output_path = os.path.join(output_dir, "oecd_gdp_full_data.csv")
3636

37-
# --- 2. Fetch Metadata ---
37+
# --- 2. Initialize Client ---
38+
client = SdmxClient(endpoint, agency_id)
39+
40+
# --- 3. Fetch Metadata ---
3841
logging.info("--- Step 1: Starting Metadata Download ---")
3942
try:
40-
dataflow.fetch_and_save_metadata(dataflow_id=dataflow_id,
41-
agency_id=agency_id,
42-
output_path=metadata_output_path,
43-
endpoint=endpoint)
43+
client.fetch_and_save_metadata(dataflow_id=dataflow_id,
44+
output_path=metadata_output_path)
4445
logging.info(
4546
f"--- Successfully downloaded metadata to {metadata_output_path} ---"
4647
)
@@ -49,19 +50,17 @@ def main():
4950
# Exit with a non-zero status code to indicate failure.
5051
sys.exit(1)
5152

52-
# --- 3. Fetch Full Data Series ---
53+
# --- 4. Fetch Full Data Series ---
5354
logging.info("\n--- Step 2: Starting Full Data Download ---")
5455
# For the full dataset, we use an empty key and no time parameters
5556
data_key = {}
5657
data_params = {}
5758

5859
try:
59-
dataflow.fetch_and_save_data_as_csv(dataflow_id=dataflow_id,
60-
agency_id=agency_id,
61-
key=data_key,
62-
params=data_params,
63-
output_path=data_output_path,
64-
endpoint=endpoint)
60+
client.fetch_and_save_data_as_csv(dataflow_id=dataflow_id,
61+
key=data_key,
62+
params=data_params,
63+
output_path=data_output_path)
6564
logging.info(
6665
f"--- Successfully downloaded data to {data_output_path} ---")
6766
except Exception as e:

0 commit comments

Comments
 (0)