|
1 | 1 | """ |
2 | 2 | dataflow.py |
3 | 3 |
|
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. |
6 | 5 | """ |
7 | 6 |
|
8 | 7 | import logging |
|
12 | 11 | from typing import Dict, Any |
13 | 12 |
|
14 | 13 |
|
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}' |
119 | 38 | } |
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: |
163 | 108 | 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 |
0 commit comments