kpubdata 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import NoReturn, cast
6
+
7
+ from kpubdata.config import KPubDataConfig
8
+ from kpubdata.core.models import (
9
+ DatasetRef,
10
+ Query,
11
+ RecordBatch,
12
+ SchemaDescriptor,
13
+ )
14
+ from kpubdata.exceptions import (
15
+ AuthError,
16
+ DatasetNotFoundError,
17
+ InvalidRequestError,
18
+ ParseError,
19
+ ProviderResponseError,
20
+ RateLimitError,
21
+ ServiceUnavailableError,
22
+ )
23
+ from kpubdata.providers._common import build_schema_from_metadata, coerce_int, load_catalogue
24
+ from kpubdata.transport.decode import decode_json
25
+ from kpubdata.transport.http import HttpTransport, TransportConfig
26
+
27
+ logger = logging.getLogger("kpubdata.provider.bok")
28
+
29
+
30
+ class BokAdapter:
31
+ def __init__(
32
+ self,
33
+ *,
34
+ config: KPubDataConfig | None = None,
35
+ transport: HttpTransport | None = None,
36
+ catalogue: Sequence[DatasetRef] | None = None,
37
+ ) -> None:
38
+ self._config: KPubDataConfig = config or KPubDataConfig()
39
+ transport_config = TransportConfig(
40
+ timeout=self._config.timeout,
41
+ max_retries=self._config.max_retries,
42
+ )
43
+ self._transport: HttpTransport = transport or HttpTransport(transport_config)
44
+
45
+ datasets = tuple(catalogue) if catalogue is not None else self._load_default_catalogue()
46
+ self._datasets: tuple[DatasetRef, ...] = datasets
47
+ self._datasets_by_key: dict[str, DatasetRef] = {
48
+ dataset.dataset_key: dataset for dataset in self._datasets
49
+ }
50
+
51
+ @property
52
+ def name(self) -> str:
53
+ return "bok"
54
+
55
+ def list_datasets(self) -> list[DatasetRef]:
56
+ return list(self._datasets)
57
+
58
+ def search_datasets(self, text: str) -> list[DatasetRef]:
59
+ needle = text.casefold()
60
+ return [
61
+ dataset
62
+ for dataset in self._datasets
63
+ if needle in dataset.id.casefold() or needle in dataset.name.casefold()
64
+ ]
65
+
66
+ def get_dataset(self, dataset_key: str) -> DatasetRef:
67
+ dataset = self._datasets_by_key.get(dataset_key)
68
+ if dataset is not None:
69
+ return dataset
70
+
71
+ raise DatasetNotFoundError(
72
+ f"Dataset not found: bok.{dataset_key}",
73
+ provider="bok",
74
+ dataset_id=f"bok.{dataset_key}",
75
+ )
76
+
77
+ def query_records(self, dataset: DatasetRef, query: Query) -> RecordBatch:
78
+ page = query.page or 1
79
+ page_size = query.page_size or 10
80
+ frequency = self._resolve_frequency(query)
81
+ start_date = query.start_date or self._resolve_string_param(query, "start_date")
82
+ end_date = query.end_date or self._resolve_string_param(query, "end_date")
83
+
84
+ if start_date is None or end_date is None:
85
+ raise InvalidRequestError(
86
+ "BOK ECOS queries require start_date and end_date",
87
+ provider="bok",
88
+ dataset_id=dataset.id,
89
+ )
90
+
91
+ all_items: list[dict[str, object]] = []
92
+ raw_pages: list[dict[str, object]] = []
93
+ total_count: int | None = None
94
+
95
+ while True:
96
+ start_index = (page - 1) * page_size + 1
97
+ end_index = page * page_size
98
+ url = self._build_request_url(
99
+ dataset,
100
+ operation=None,
101
+ start_index=start_index,
102
+ end_index=end_index,
103
+ frequency=frequency,
104
+ start_date=start_date,
105
+ end_date=end_date,
106
+ )
107
+
108
+ payload = self._request_and_decode(url)
109
+ raw_pages.append(payload)
110
+
111
+ body, items = self._validate_envelope(payload, dataset.id)
112
+ all_items.extend(items)
113
+
114
+ total_count = coerce_int(body.get("list_total_count"), 0)
115
+ if not items:
116
+ break
117
+ if len(items) < page_size:
118
+ break
119
+ if page * page_size >= total_count:
120
+ break
121
+
122
+ page += 1
123
+
124
+ return RecordBatch(
125
+ items=all_items,
126
+ dataset=dataset,
127
+ total_count=total_count,
128
+ next_page=None,
129
+ raw=raw_pages,
130
+ )
131
+
132
+ def get_record(self, _dataset: DatasetRef, _key: dict[str, object]) -> dict[str, object] | None:
133
+ raise NotImplementedError("TODO: implement bok get_record")
134
+
135
+ def get_schema(self, dataset: DatasetRef) -> SchemaDescriptor | None:
136
+ return build_schema_from_metadata(dataset)
137
+
138
+ def call_raw(self, dataset: DatasetRef, operation: str, params: dict[str, object]) -> object:
139
+ frequency = self._string_param(params, "frequency") or "M"
140
+ start_date = self._require_param(params, "start_date")
141
+ end_date = self._require_param(params, "end_date")
142
+ start_index = self._int_param(params, "start_index", 1)
143
+ end_index = self._int_param(params, "end_index", 10)
144
+
145
+ url = self._build_request_url(
146
+ dataset,
147
+ operation=operation,
148
+ start_index=start_index,
149
+ end_index=end_index,
150
+ frequency=frequency,
151
+ start_date=start_date,
152
+ end_date=end_date,
153
+ )
154
+ payload = self._request_and_decode(url)
155
+ _ = self._validate_envelope(payload, dataset.id)
156
+ return payload
157
+
158
+ def _require_api_key(self) -> str:
159
+ return self._config.require_provider_key("bok")
160
+
161
+ def _build_request_url(
162
+ self,
163
+ dataset: DatasetRef,
164
+ *,
165
+ operation: str | None,
166
+ start_index: int,
167
+ end_index: int,
168
+ frequency: str,
169
+ start_date: str,
170
+ end_date: str,
171
+ ) -> str:
172
+ base_url_raw = dataset.raw_metadata.get("base_url")
173
+ if not isinstance(base_url_raw, str) or not base_url_raw:
174
+ raise ProviderResponseError(
175
+ "Dataset metadata missing base_url",
176
+ provider="bok",
177
+ dataset_id=dataset.id,
178
+ )
179
+
180
+ selected_operation = operation or dataset.raw_metadata.get("default_operation")
181
+ if not isinstance(selected_operation, str) or not selected_operation:
182
+ raise ProviderResponseError(
183
+ "Dataset metadata missing default_operation",
184
+ provider="bok",
185
+ dataset_id=dataset.id,
186
+ )
187
+
188
+ stat_code = self._require_dataset_metadata(dataset, "stat_code")
189
+ item_code1 = self._require_dataset_metadata(dataset, "item_code1")
190
+ api_key = self._require_api_key()
191
+ return (
192
+ f"{base_url_raw}/{api_key}/json/{selected_operation}/"
193
+ f"{start_index}/{end_index}/{stat_code}/{frequency}/{start_date}/{end_date}/{item_code1}"
194
+ )
195
+
196
+ def _request_and_decode(self, url: str) -> dict[str, object]:
197
+ response = self._transport.request("GET", url)
198
+
199
+ try:
200
+ decoded_obj: object = decode_json(response.content)
201
+ except ParseError as exc:
202
+ exc.provider = "bok"
203
+ raise
204
+
205
+ if isinstance(decoded_obj, dict):
206
+ return cast(dict[str, object], decoded_obj)
207
+
208
+ raise ParseError("Decoded payload is not an object", provider="bok")
209
+
210
+ def _validate_envelope(
211
+ self, payload: dict[str, object], dataset_id: str = ""
212
+ ) -> tuple[dict[str, object], list[dict[str, object]]]:
213
+ self._raise_for_result(payload, dataset_id)
214
+
215
+ body_obj = payload.get("StatisticSearch")
216
+ if not isinstance(body_obj, dict):
217
+ raise ProviderResponseError(
218
+ "Malformed response envelope: missing StatisticSearch",
219
+ provider="bok",
220
+ dataset_id=dataset_id or None,
221
+ )
222
+
223
+ body_dict = cast(dict[str, object], body_obj)
224
+ items = self._normalize_rows(body_dict.get("row"))
225
+ return body_dict, items
226
+
227
+ def _raise_for_result(self, payload: Mapping[str, object], dataset_id: str) -> None:
228
+ result_obj = payload.get("RESULT")
229
+ if not isinstance(result_obj, dict):
230
+ return
231
+
232
+ result_dict = cast(dict[str, object], result_obj)
233
+ code_raw = result_dict.get("CODE")
234
+ message_raw = result_dict.get("MESSAGE")
235
+ code = code_raw if isinstance(code_raw, str) else "ERROR"
236
+ message = message_raw if isinstance(message_raw, str) else "Provider returned error"
237
+ logger.debug(
238
+ "BOK ECOS result",
239
+ extra={"result_code": code, "result_msg": message, "dataset_id": dataset_id},
240
+ )
241
+
242
+ if code != "ERROR":
243
+ return
244
+
245
+ self._raise_for_result_code(code, message, dataset_id)
246
+
247
+ def _raise_for_result_code(self, code: str, msg: str, dataset_id: str) -> NoReturn:
248
+ normalized_msg = msg.casefold()
249
+ if "인증키" in msg or "api key" in normalized_msg or "auth" in normalized_msg:
250
+ raise AuthError(msg, provider="bok", provider_code=code, dataset_id=dataset_id or None)
251
+ if "호출한도" in msg or "too many" in normalized_msg or "rate limit" in normalized_msg:
252
+ raise RateLimitError(
253
+ msg, provider="bok", provider_code=code, dataset_id=dataset_id or None
254
+ )
255
+ if (
256
+ "점검" in msg
257
+ or "service unavailable" in normalized_msg
258
+ or "temporarily unavailable" in normalized_msg
259
+ ):
260
+ raise ServiceUnavailableError(
261
+ msg,
262
+ provider="bok",
263
+ provider_code=code,
264
+ dataset_id=dataset_id or None,
265
+ )
266
+ if "필수" in msg or "invalid" in normalized_msg or "잘못" in msg:
267
+ raise InvalidRequestError(
268
+ msg, provider="bok", provider_code=code, dataset_id=dataset_id or None
269
+ )
270
+ raise ProviderResponseError(
271
+ msg, provider="bok", provider_code=code, dataset_id=dataset_id or None
272
+ )
273
+
274
+ def _resolve_frequency(self, query: Query) -> str:
275
+ return self._resolve_string_param(query, "frequency") or "M"
276
+
277
+ def _resolve_string_param(self, query: Query, key: str) -> str | None:
278
+ if key in query.extra:
279
+ extra_value = query.extra[key]
280
+ if isinstance(extra_value, str) and extra_value:
281
+ return extra_value
282
+ if key in query.filters:
283
+ filter_value = query.filters[key]
284
+ if isinstance(filter_value, str) and filter_value:
285
+ return filter_value
286
+ return None
287
+
288
+ def _require_dataset_metadata(self, dataset: DatasetRef, key: str) -> str:
289
+ value = dataset.raw_metadata.get(key)
290
+ if isinstance(value, str) and value:
291
+ return value
292
+ raise ProviderResponseError(
293
+ f"Dataset metadata missing {key}",
294
+ provider="bok",
295
+ dataset_id=dataset.id,
296
+ )
297
+
298
+ def _normalize_rows(self, rows_wrapper: object) -> list[dict[str, object]]:
299
+ if rows_wrapper is None:
300
+ return []
301
+ if isinstance(rows_wrapper, list):
302
+ rows = cast(list[object], rows_wrapper)
303
+ return [cast(dict[str, object], item) for item in rows if isinstance(item, dict)]
304
+ if isinstance(rows_wrapper, dict):
305
+ return [cast(dict[str, object], rows_wrapper)]
306
+ return []
307
+
308
+ @staticmethod
309
+ def _string_param(params: Mapping[str, object], key: str) -> str | None:
310
+ value = params.get(key)
311
+ if isinstance(value, str) and value:
312
+ return value
313
+ return None
314
+
315
+ @classmethod
316
+ def _require_param(cls, params: Mapping[str, object], key: str) -> str:
317
+ value = cls._string_param(params, key)
318
+ if value is not None:
319
+ return value
320
+ raise InvalidRequestError(f"BOK ECOS raw calls require {key}", provider="bok")
321
+
322
+ @classmethod
323
+ def _int_param(cls, params: Mapping[str, object], key: str, default: int) -> int:
324
+ value = params.get(key)
325
+ coerced = coerce_int(value, default)
326
+ return coerced if coerced > 0 else default
327
+
328
+ @staticmethod
329
+ def _load_default_catalogue() -> tuple[DatasetRef, ...]:
330
+ return load_catalogue("kpubdata.providers.bok", "bok")
331
+
332
+
333
+ __all__ = ["BokAdapter"]
@@ -0,0 +1,25 @@
1
+ [
2
+ {
3
+ "dataset_key": "base_rate",
4
+ "name": "한국은행 기준금리 (BOK Base Rate)",
5
+ "base_url": "https://ecos.bok.or.kr/api/StatisticSearch",
6
+ "default_operation": "StatisticSearch",
7
+ "representation": "api_json",
8
+ "stat_code": "722Y001",
9
+ "item_code1": "0101000",
10
+ "description": "Bank of Korea base interest rate historical data",
11
+ "operations": ["list", "raw"],
12
+ "query_support": {
13
+ "pagination": "offset",
14
+ "max_page_size": 1000
15
+ },
16
+ "fields": [
17
+ {"name": "TIME", "title": "기간", "type": "string"},
18
+ {"name": "DATA_VALUE", "title": "기준금리", "type": "string"},
19
+ {"name": "UNIT_NAME", "title": "단위", "type": "string"},
20
+ {"name": "STAT_CODE", "title": "통계표코드", "type": "string"},
21
+ {"name": "ITEM_CODE1", "title": "항목코드", "type": "string"},
22
+ {"name": "ITEM_NAME1", "title": "항목명", "type": "string"}
23
+ ]
24
+ }
25
+ ]
@@ -0,0 +1,7 @@
1
+ """Data.go.kr provider adapter package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .adapter import DataGoAdapter
6
+
7
+ __all__ = ["DataGoAdapter"]
@@ -0,0 +1,313 @@
1
+ """Data.go.kr adapter with curated dataset catalogue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import NoReturn, cast
8
+
9
+ from kpubdata.config import KPubDataConfig
10
+ from kpubdata.core.models import (
11
+ DatasetRef,
12
+ Query,
13
+ RecordBatch,
14
+ SchemaDescriptor,
15
+ )
16
+ from kpubdata.exceptions import (
17
+ AuthError,
18
+ DatasetNotFoundError,
19
+ InvalidRequestError,
20
+ ParseError,
21
+ ProviderResponseError,
22
+ RateLimitError,
23
+ ServiceUnavailableError,
24
+ )
25
+ from kpubdata.providers._common import build_schema_from_metadata, coerce_int, load_catalogue
26
+ from kpubdata.transport.decode import decode_json, decode_xml, detect_content_type
27
+ from kpubdata.transport.http import HttpTransport, TransportConfig
28
+
29
+ logger = logging.getLogger("kpubdata.provider.datago")
30
+
31
+
32
+ class DataGoAdapter:
33
+ """Adapter for data.go.kr (공공데이터포털).
34
+
35
+ Provides a curated catalogue of supported datasets from the
36
+ apis.data.go.kr endpoint family.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ config: KPubDataConfig | None = None,
43
+ transport: HttpTransport | None = None,
44
+ catalogue: Sequence[DatasetRef] | None = None,
45
+ ) -> None:
46
+ self._config: KPubDataConfig = config or KPubDataConfig()
47
+ transport_config = TransportConfig(
48
+ timeout=self._config.timeout,
49
+ max_retries=self._config.max_retries,
50
+ )
51
+ self._transport: HttpTransport = transport or HttpTransport(transport_config)
52
+
53
+ datasets = tuple(catalogue) if catalogue is not None else self._load_default_catalogue()
54
+ self._datasets: tuple[DatasetRef, ...] = datasets
55
+ self._datasets_by_key: dict[str, DatasetRef] = {
56
+ dataset.dataset_key: dataset for dataset in self._datasets
57
+ }
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ """Return canonical provider key."""
62
+
63
+ return "datago"
64
+
65
+ def list_datasets(self) -> list[DatasetRef]:
66
+ """List datasets available from data.go.kr."""
67
+
68
+ return list(self._datasets)
69
+
70
+ def search_datasets(self, text: str) -> list[DatasetRef]:
71
+ """Search datasets available from data.go.kr."""
72
+
73
+ needle = text.casefold()
74
+ return [
75
+ dataset
76
+ for dataset in self._datasets
77
+ if needle in dataset.id.casefold() or needle in dataset.name.casefold()
78
+ ]
79
+
80
+ def get_dataset(self, dataset_key: str) -> DatasetRef:
81
+ """Resolve provider-local dataset key for data.go.kr."""
82
+
83
+ dataset = self._datasets_by_key.get(dataset_key)
84
+ if dataset is not None:
85
+ return dataset
86
+
87
+ raise DatasetNotFoundError(
88
+ f"Dataset not found: datago.{dataset_key}",
89
+ provider="datago",
90
+ dataset_id=f"datago.{dataset_key}",
91
+ )
92
+
93
+ def query_records(self, dataset: DatasetRef, query: Query) -> RecordBatch:
94
+ """Query records from a data.go.kr dataset."""
95
+
96
+ page = query.page or 1
97
+ page_size = query.page_size or 10
98
+
99
+ all_items: list[dict[str, object]] = []
100
+ raw_pages: list[dict[str, object]] = []
101
+ total_count: int | None = None
102
+
103
+ while True:
104
+ url = self._build_request_url(dataset)
105
+ params = self._build_base_params(dataset)
106
+ params["pageNo"] = str(page)
107
+ params["numOfRows"] = str(page_size)
108
+
109
+ reserved = {params_key.lower() for params_key in params}
110
+ reserved.update({"pageno", "numofrows"})
111
+ for key, raw_value in query.filters.items():
112
+ if key.lower() not in reserved:
113
+ value: object = raw_value
114
+ params[key] = str(value)
115
+
116
+ payload = self._request_and_decode(url, params)
117
+ raw_pages.append(payload)
118
+
119
+ body, items = self._validate_envelope(payload, dataset.id)
120
+ all_items.extend(items)
121
+
122
+ total_count = coerce_int(body.get("totalCount"), 0)
123
+ current_num_of_rows = coerce_int(body.get("numOfRows"), page_size)
124
+
125
+ if not items:
126
+ break
127
+ if len(items) < current_num_of_rows:
128
+ break
129
+ if page * page_size >= total_count:
130
+ break
131
+
132
+ page += 1
133
+
134
+ return RecordBatch(
135
+ items=all_items,
136
+ dataset=dataset,
137
+ total_count=total_count,
138
+ next_page=None,
139
+ raw=raw_pages,
140
+ )
141
+
142
+ def get_record(self, _dataset: DatasetRef, _key: dict[str, object]) -> dict[str, object] | None:
143
+ """Get a single record from a data.go.kr dataset."""
144
+
145
+ raise NotImplementedError("TODO: implement datago get_record")
146
+
147
+ def get_schema(self, dataset: DatasetRef) -> SchemaDescriptor | None:
148
+ """Get schema metadata for a data.go.kr dataset.
149
+
150
+ Returns schema from curated catalogue metadata when available.
151
+ data.go.kr has no live schema discovery endpoint, so this
152
+ returns ``None`` for datasets without explicitly curated field
153
+ definitions in the catalogue.
154
+ """
155
+ return build_schema_from_metadata(dataset)
156
+
157
+ def call_raw(self, dataset: DatasetRef, operation: str, params: dict[str, object]) -> object:
158
+ """Call provider-native data.go.kr API operation."""
159
+
160
+ url = self._build_request_url(dataset, operation)
161
+ request_params = self._build_base_params(dataset)
162
+
163
+ service_key_param = str(dataset.raw_metadata.get("service_key_param", "serviceKey"))
164
+ for key, value in params.items():
165
+ if key != service_key_param:
166
+ request_params[key] = str(value)
167
+
168
+ payload = self._request_and_decode(url, request_params)
169
+ _ = self._validate_envelope(payload, dataset.id)
170
+ return payload
171
+
172
+ def _require_api_key(self) -> str:
173
+ return self._config.require_provider_key("datago")
174
+
175
+ def _build_request_url(self, dataset: DatasetRef, operation: str | None = None) -> str:
176
+ base_url_raw = dataset.raw_metadata.get("base_url")
177
+ if not isinstance(base_url_raw, str) or not base_url_raw:
178
+ raise ProviderResponseError(
179
+ "Dataset metadata missing base_url",
180
+ provider="datago",
181
+ dataset_id=dataset.id,
182
+ )
183
+ selected_operation = operation or dataset.raw_metadata.get("default_operation")
184
+ if isinstance(selected_operation, str) and selected_operation:
185
+ return f"{base_url_raw}/{selected_operation}"
186
+ return base_url_raw
187
+
188
+ def _build_base_params(self, dataset: DatasetRef) -> dict[str, str]:
189
+ api_key = self._require_api_key()
190
+ service_key_param_raw = dataset.raw_metadata.get("service_key_param", "serviceKey")
191
+ format_param_raw = dataset.raw_metadata.get("format_param", "resultType")
192
+ service_key_param = (
193
+ service_key_param_raw
194
+ if isinstance(service_key_param_raw, str) and service_key_param_raw
195
+ else "serviceKey"
196
+ )
197
+ format_param = (
198
+ format_param_raw
199
+ if isinstance(format_param_raw, str) and format_param_raw
200
+ else "resultType"
201
+ )
202
+ return {service_key_param: api_key, format_param: "json"}
203
+
204
+ def _request_and_decode(self, url: str, params: Mapping[str, object]) -> dict[str, object]:
205
+ string_params = {key: str(value) for key, value in params.items()}
206
+ response = self._transport.request("GET", url, params=string_params)
207
+
208
+ try:
209
+ content_type = detect_content_type(response)
210
+ if content_type == "json":
211
+ decoded = decode_json(response.content)
212
+ elif content_type == "xml":
213
+ decoded = decode_xml(response.content)
214
+ else:
215
+ decoded = decode_json(response.content)
216
+ except ParseError as exc:
217
+ exc.provider = "datago"
218
+ raise
219
+ except ImportError as exc:
220
+ raise ParseError("Failed to parse data.go.kr response", provider="datago") from exc
221
+
222
+ if isinstance(decoded, dict):
223
+ return cast(dict[str, object], decoded)
224
+
225
+ raise ParseError("Decoded payload is not an object", provider="datago")
226
+
227
+ def _validate_envelope(
228
+ self, payload: dict[str, object], dataset_id: str = ""
229
+ ) -> tuple[dict[str, object], list[dict[str, object]]]:
230
+ response_obj = payload.get("response")
231
+ if not isinstance(response_obj, dict):
232
+ raise ProviderResponseError(
233
+ "Malformed response envelope: missing response",
234
+ provider="datago",
235
+ dataset_id=dataset_id or None,
236
+ )
237
+
238
+ header_obj = response_obj.get("header")
239
+ if not isinstance(header_obj, dict):
240
+ raise ProviderResponseError(
241
+ "Malformed response envelope: missing header",
242
+ provider="datago",
243
+ dataset_id=dataset_id or None,
244
+ )
245
+
246
+ header_dict = cast(dict[str, object], header_obj)
247
+ result_code = header_dict.get("resultCode")
248
+ if not isinstance(result_code, str):
249
+ raise ProviderResponseError(
250
+ "Malformed response envelope: missing resultCode",
251
+ provider="datago",
252
+ dataset_id=dataset_id or None,
253
+ )
254
+
255
+ result_msg_raw = header_dict.get("resultMsg")
256
+ result_msg = (
257
+ result_msg_raw if isinstance(result_msg_raw, str) else "Provider returned error"
258
+ )
259
+ logger.debug(
260
+ "data.go.kr result",
261
+ extra={"result_code": result_code, "result_msg": result_msg, "dataset_id": dataset_id},
262
+ )
263
+ if result_code != "00":
264
+ self._raise_for_result_code(result_code, result_msg, dataset_id)
265
+
266
+ body_obj = response_obj.get("body")
267
+ body_dict: dict[str, object] = (
268
+ cast(dict[str, object], body_obj) if isinstance(body_obj, dict) else {}
269
+ )
270
+ items = self._normalize_items(body_dict.get("items"))
271
+ return body_dict, items
272
+
273
+ def _raise_for_result_code(self, code: str, msg: str, dataset_id: str) -> NoReturn:
274
+ if code in {"30", "31", "20", "32"}:
275
+ raise AuthError(msg, provider="datago", provider_code=code)
276
+ if code == "22":
277
+ raise RateLimitError(msg, provider="datago", provider_code=code, retryable=False)
278
+ if code == "10":
279
+ raise InvalidRequestError(msg, provider="datago", provider_code=code)
280
+ if code == "12":
281
+ raise DatasetNotFoundError(
282
+ msg,
283
+ provider="datago",
284
+ provider_code=code,
285
+ dataset_id=dataset_id,
286
+ )
287
+ if code in {"01", "02"}:
288
+ raise ServiceUnavailableError(msg, provider="datago", provider_code=code)
289
+ raise ProviderResponseError(msg, provider="datago", provider_code=code)
290
+
291
+ def _normalize_items(self, items_wrapper: object) -> list[dict[str, object]]:
292
+ if items_wrapper is None:
293
+ return []
294
+
295
+ if isinstance(items_wrapper, dict):
296
+ item_value = items_wrapper.get("item")
297
+ if isinstance(item_value, list):
298
+ return [item for item in item_value if isinstance(item, dict)]
299
+ if isinstance(item_value, dict):
300
+ return [item_value]
301
+ return []
302
+
303
+ if isinstance(items_wrapper, list):
304
+ return [item for item in items_wrapper if isinstance(item, dict)]
305
+
306
+ return []
307
+
308
+ @staticmethod
309
+ def _load_default_catalogue() -> tuple[DatasetRef, ...]:
310
+ return load_catalogue("kpubdata.providers.datago", "datago")
311
+
312
+
313
+ __all__ = ["DataGoAdapter"]