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,92 @@
1
+ [
2
+ {
3
+ "dataset_key": "village_fcst",
4
+ "name": "동네예보 조회서비스 (Village Forecast)",
5
+ "base_url": "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0",
6
+ "default_operation": "getVilageFcst",
7
+ "representation": "api_json",
8
+ "service_key_param": "serviceKey",
9
+ "format_param": "dataType",
10
+ "description": "Korea Meteorological Administration short-range forecast",
11
+ "operations": ["list", "raw"],
12
+ "query_support": {
13
+ "pagination": "offset",
14
+ "max_page_size": 1000
15
+ }
16
+ },
17
+ {
18
+ "dataset_key": "ultra_srt_ncst",
19
+ "name": "초단기실황 조회서비스 (Ultra Short-term Nowcast)",
20
+ "base_url": "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0",
21
+ "default_operation": "getUltraSrtNcst",
22
+ "representation": "api_json",
23
+ "service_key_param": "serviceKey",
24
+ "format_param": "dataType",
25
+ "description": "KMA ultra short-term observed weather conditions",
26
+ "operations": ["list", "raw"],
27
+ "query_support": {
28
+ "pagination": "offset",
29
+ "max_page_size": 1000
30
+ }
31
+ },
32
+ {
33
+ "dataset_key": "air_quality",
34
+ "name": "대기오염정보 조회서비스 (Air Quality)",
35
+ "base_url": "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc",
36
+ "default_operation": "getCtprvnRltmMesureDnsty",
37
+ "representation": "api_json",
38
+ "service_key_param": "serviceKey",
39
+ "format_param": "returnType",
40
+ "description": "AirKorea real-time particulate matter and atmospheric pollution",
41
+ "operations": ["list", "raw"],
42
+ "query_support": {
43
+ "pagination": "offset",
44
+ "max_page_size": 1000
45
+ }
46
+ },
47
+ {
48
+ "dataset_key": "bus_arrival",
49
+ "name": "경기도 버스도착정보 조회서비스 (Bus Arrival)",
50
+ "base_url": "http://apis.data.go.kr/6410000/busarrivalservice",
51
+ "default_operation": "getBusArrivalList",
52
+ "representation": "api_json",
53
+ "service_key_param": "serviceKey",
54
+ "format_param": "_type",
55
+ "description": "Gyeonggi-do real-time bus arrival information",
56
+ "operations": ["list", "raw"],
57
+ "query_support": {
58
+ "pagination": "offset",
59
+ "max_page_size": 1000
60
+ }
61
+ },
62
+ {
63
+ "dataset_key": "hospital_info",
64
+ "name": "병원정보서비스 (Hospital Information)",
65
+ "base_url": "http://apis.data.go.kr/B551182/hospInfoServicev2",
66
+ "default_operation": "getHospBasisList",
67
+ "representation": "api_json",
68
+ "service_key_param": "serviceKey",
69
+ "format_param": "_type",
70
+ "description": "Hospital basic information and medical institution lookup",
71
+ "operations": ["list", "raw"],
72
+ "query_support": {
73
+ "pagination": "offset",
74
+ "max_page_size": 1000
75
+ }
76
+ },
77
+ {
78
+ "dataset_key": "apt_trade",
79
+ "name": "아파트매매 실거래자료 (Apartment Trade)",
80
+ "base_url": "http://apis.data.go.kr/1613000/RTMSDataSvcAptTradeDev",
81
+ "default_operation": "getRTMSDataSvcAptTradeDev",
82
+ "representation": "api_json",
83
+ "service_key_param": "serviceKey",
84
+ "format_param": "resultType",
85
+ "description": "MOLIT apartment trade real transaction data",
86
+ "operations": ["list", "raw"],
87
+ "query_support": {
88
+ "pagination": "offset",
89
+ "max_page_size": 1000
90
+ }
91
+ }
92
+ ]
@@ -0,0 +1,7 @@
1
+ """KOSTAT KOSIS provider adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .adapter import KosisAdapter
6
+
7
+ __all__ = ["KosisAdapter"]
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import NoReturn, cast
6
+ from urllib.parse import urlencode
7
+
8
+ from kpubdata.config import KPubDataConfig
9
+ from kpubdata.core.models import (
10
+ DatasetRef,
11
+ Query,
12
+ RecordBatch,
13
+ SchemaDescriptor,
14
+ )
15
+ from kpubdata.exceptions import (
16
+ AuthError,
17
+ DatasetNotFoundError,
18
+ InvalidRequestError,
19
+ ParseError,
20
+ ProviderResponseError,
21
+ )
22
+ from kpubdata.providers._common import build_schema_from_metadata, load_catalogue
23
+ from kpubdata.transport.decode import decode_json
24
+ from kpubdata.transport.http import HttpTransport, TransportConfig
25
+
26
+ logger = logging.getLogger("kpubdata.provider.kosis")
27
+
28
+
29
+ class KosisAdapter:
30
+ def __init__(
31
+ self,
32
+ *,
33
+ config: KPubDataConfig | None = None,
34
+ transport: HttpTransport | None = None,
35
+ catalogue: Sequence[DatasetRef] | None = None,
36
+ ) -> None:
37
+ self._config: KPubDataConfig = config or KPubDataConfig()
38
+ transport_config = TransportConfig(
39
+ timeout=self._config.timeout,
40
+ max_retries=self._config.max_retries,
41
+ )
42
+ self._transport: HttpTransport = transport or HttpTransport(transport_config)
43
+
44
+ datasets = tuple(catalogue) if catalogue is not None else self._load_default_catalogue()
45
+ self._datasets: tuple[DatasetRef, ...] = datasets
46
+ self._datasets_by_key: dict[str, DatasetRef] = {
47
+ dataset.dataset_key: dataset for dataset in self._datasets
48
+ }
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ return "kosis"
53
+
54
+ def list_datasets(self) -> list[DatasetRef]:
55
+ return list(self._datasets)
56
+
57
+ def search_datasets(self, text: str) -> list[DatasetRef]:
58
+ needle = text.casefold()
59
+ return [
60
+ dataset
61
+ for dataset in self._datasets
62
+ if needle in dataset.id.casefold() or needle in dataset.name.casefold()
63
+ ]
64
+
65
+ def get_dataset(self, dataset_key: str) -> DatasetRef:
66
+ dataset = self._datasets_by_key.get(dataset_key)
67
+ if dataset is not None:
68
+ return dataset
69
+
70
+ raise DatasetNotFoundError(
71
+ f"Dataset not found: kosis.{dataset_key}",
72
+ provider="kosis",
73
+ dataset_id=f"kosis.{dataset_key}",
74
+ )
75
+
76
+ def query_records(self, dataset: DatasetRef, query: Query) -> RecordBatch:
77
+ url = self._build_request_url(dataset, query)
78
+ payload = self._request_and_decode(url)
79
+ items = self._extract_items(payload, dataset.id)
80
+
81
+ return RecordBatch(
82
+ items=items,
83
+ dataset=dataset,
84
+ total_count=len(items),
85
+ next_page=None,
86
+ raw=payload,
87
+ )
88
+
89
+ def get_record(self, _dataset: DatasetRef, _key: dict[str, object]) -> dict[str, object] | None:
90
+ raise NotImplementedError("TODO: implement kosis get_record")
91
+
92
+ def get_schema(self, dataset: DatasetRef) -> SchemaDescriptor | None:
93
+ return build_schema_from_metadata(dataset)
94
+
95
+ def call_raw(self, dataset: DatasetRef, operation: str, params: dict[str, object]) -> object:
96
+ url = self._build_raw_url(dataset, operation, params)
97
+ payload = self._request_and_decode(url)
98
+ if isinstance(payload, dict):
99
+ self._raise_for_error_payload(cast(dict[str, object], payload), dataset.id)
100
+ return payload
101
+
102
+ def _require_api_key(self) -> str:
103
+ return self._config.require_provider_key("kosis")
104
+
105
+ def _build_request_url(self, dataset: DatasetRef, query: Query) -> str:
106
+ start_date = query.start_date
107
+ end_date = query.end_date
108
+ if not isinstance(start_date, str) or not start_date:
109
+ raise InvalidRequestError(
110
+ "KOSIS queries require start_date",
111
+ provider="kosis",
112
+ dataset_id=dataset.id,
113
+ )
114
+ if not isinstance(end_date, str) or not end_date:
115
+ raise InvalidRequestError(
116
+ "KOSIS queries require end_date",
117
+ provider="kosis",
118
+ dataset_id=dataset.id,
119
+ )
120
+
121
+ filters: dict[str, object] = dict(query.filters)
122
+ params = self._build_base_params(dataset)
123
+ params["startPrdDe"] = start_date
124
+ params["endPrdDe"] = end_date
125
+
126
+ reserved = {key.casefold() for key in params}
127
+ for key in ("objL1", "objL2", "itmId", "prdSe"):
128
+ if key in filters:
129
+ params[key] = str(filters.pop(key))
130
+
131
+ for key, value in filters.items():
132
+ if key.casefold() not in reserved:
133
+ params[key] = str(value)
134
+
135
+ return self._compose_url(dataset, params)
136
+
137
+ def _build_raw_url(
138
+ self,
139
+ dataset: DatasetRef,
140
+ operation: str,
141
+ params: Mapping[str, object],
142
+ ) -> str:
143
+ request_params = self._build_base_params(dataset)
144
+ selected_operation = operation.strip()
145
+ if selected_operation and selected_operation != "statisticsParameterData":
146
+ request_params["operation"] = selected_operation
147
+ for key, value in params.items():
148
+ if key != "apiKey":
149
+ request_params[key] = str(value)
150
+ return self._compose_url(dataset, request_params)
151
+
152
+ def _build_base_params(self, dataset: DatasetRef) -> dict[str, str]:
153
+ api_key = self._require_api_key()
154
+ org_id = self._require_dataset_metadata(dataset, "org_id")
155
+ tbl_id = self._require_dataset_metadata(dataset, "tbl_id")
156
+ return {
157
+ "apiKey": api_key,
158
+ "format": "json",
159
+ "jsonVD": "Y",
160
+ "orgId": org_id,
161
+ "tblId": tbl_id,
162
+ "objL1": "ALL",
163
+ "objL2": "ALL",
164
+ "itmId": "ALL",
165
+ "prdSe": "M",
166
+ }
167
+
168
+ def _compose_url(self, dataset: DatasetRef, params: Mapping[str, str]) -> str:
169
+ base_url = self._require_dataset_metadata(dataset, "base_url")
170
+ query_string = urlencode(params)
171
+ return f"{base_url}?{query_string}"
172
+
173
+ def _request_and_decode(self, url: str) -> object:
174
+ response = self._transport.request("GET", url)
175
+
176
+ try:
177
+ decoded: object = decode_json(response.content)
178
+ except ParseError as exc:
179
+ exc.provider = "kosis"
180
+ raise
181
+
182
+ if isinstance(decoded, list):
183
+ return cast(list[object], decoded)
184
+ if isinstance(decoded, dict):
185
+ return cast(dict[str, object], decoded)
186
+
187
+ raise ParseError("Decoded payload is neither an object nor an array", provider="kosis")
188
+
189
+ def _extract_items(self, payload: object, dataset_id: str) -> list[dict[str, object]]:
190
+ if isinstance(payload, dict):
191
+ self._raise_for_error_payload(cast(dict[str, object], payload), dataset_id)
192
+
193
+ if not isinstance(payload, list):
194
+ raise ProviderResponseError(
195
+ "Malformed KOSIS response: expected array payload",
196
+ provider="kosis",
197
+ dataset_id=dataset_id,
198
+ )
199
+
200
+ payload_items = cast(list[object], payload)
201
+ return [cast(dict[str, object], item) for item in payload_items if isinstance(item, dict)]
202
+
203
+ def _raise_for_error_payload(self, payload: Mapping[str, object], dataset_id: str) -> NoReturn:
204
+ code_raw = payload.get("err")
205
+ message_raw = payload.get("errMsg")
206
+ code = code_raw if isinstance(code_raw, str) else None
207
+ message = message_raw if isinstance(message_raw, str) else "KOSIS returned an error"
208
+
209
+ logger.debug(
210
+ "KOSIS error response",
211
+ extra={"provider_code": code, "message": message, "dataset_id": dataset_id},
212
+ )
213
+
214
+ if code == "30":
215
+ raise AuthError(
216
+ message,
217
+ provider="kosis",
218
+ dataset_id=dataset_id,
219
+ provider_code=code,
220
+ detail=dict(payload),
221
+ )
222
+ if code == "10":
223
+ raise InvalidRequestError(
224
+ message,
225
+ provider="kosis",
226
+ dataset_id=dataset_id,
227
+ provider_code=code,
228
+ detail=dict(payload),
229
+ )
230
+ raise ProviderResponseError(
231
+ message,
232
+ provider="kosis",
233
+ dataset_id=dataset_id,
234
+ provider_code=code,
235
+ detail=dict(payload),
236
+ )
237
+
238
+ @staticmethod
239
+ def _require_dataset_metadata(dataset: DatasetRef, field_name: str) -> str:
240
+ value = dataset.raw_metadata.get(field_name)
241
+ if isinstance(value, str) and value:
242
+ return value
243
+ raise ProviderResponseError(
244
+ f"Dataset metadata missing {field_name}",
245
+ provider="kosis",
246
+ dataset_id=dataset.id,
247
+ )
248
+
249
+ @staticmethod
250
+ def _load_default_catalogue() -> tuple[DatasetRef, ...]:
251
+ return load_catalogue("kpubdata.providers.kosis", "kosis")
252
+
253
+
254
+ __all__ = ["KosisAdapter"]
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "dataset_key": "population_migration",
4
+ "name": "시도별 이동자수 (Population Migration by Region)",
5
+ "base_url": "https://kosis.kr/openapi/Param/statisticsParameterData.do",
6
+ "default_operation": "statisticsParameterData",
7
+ "representation": "api_json",
8
+ "org_id": "101",
9
+ "tbl_id": "DT_1B26003_A01",
10
+ "description": "KOSTAT inter-regional population migration statistics",
11
+ "operations": ["list", "raw"],
12
+ "query_support": {
13
+ "pagination": "offset",
14
+ "max_page_size": 1000
15
+ },
16
+ "fields": [
17
+ {"name": "PRD_DE", "title": "기간", "type": "string"},
18
+ {"name": "C1", "title": "지역코드", "type": "string"},
19
+ {"name": "C1_NM", "title": "지역명", "type": "string"},
20
+ {"name": "ITM_ID", "title": "항목코드", "type": "string"},
21
+ {"name": "ITM_NM", "title": "항목명", "type": "string"},
22
+ {"name": "DT", "title": "값", "type": "string"},
23
+ {"name": "UNIT_NM", "title": "단위", "type": "string"}
24
+ ]
25
+ }
26
+ ]
@@ -0,0 +1,7 @@
1
+ """Local Finance 365 (LOFIN) provider adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .adapter import LofinAdapter
6
+
7
+ __all__ = ["LofinAdapter"]