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.
- kpubdata/__init__.py +52 -0
- kpubdata/catalog.py +101 -0
- kpubdata/client.py +179 -0
- kpubdata/config.py +119 -0
- kpubdata/core/__init__.py +25 -0
- kpubdata/core/capability.py +56 -0
- kpubdata/core/dataset.py +153 -0
- kpubdata/core/models.py +156 -0
- kpubdata/core/protocol.py +60 -0
- kpubdata/core/representation.py +19 -0
- kpubdata/exceptions.py +116 -0
- kpubdata/providers/__init__.py +5 -0
- kpubdata/providers/_common.py +204 -0
- kpubdata/providers/bok/__init__.py +7 -0
- kpubdata/providers/bok/adapter.py +333 -0
- kpubdata/providers/bok/catalogue.json +25 -0
- kpubdata/providers/datago/__init__.py +7 -0
- kpubdata/providers/datago/adapter.py +313 -0
- kpubdata/providers/datago/catalogue.json +92 -0
- kpubdata/providers/kosis/__init__.py +7 -0
- kpubdata/providers/kosis/adapter.py +254 -0
- kpubdata/providers/kosis/catalogue.json +26 -0
- kpubdata/providers/lofin/__init__.py +7 -0
- kpubdata/providers/lofin/adapter.py +329 -0
- kpubdata/providers/lofin/catalogue.json +127 -0
- kpubdata/py.typed +0 -0
- kpubdata/registry.py +135 -0
- kpubdata/transport/__init__.py +16 -0
- kpubdata/transport/decode.py +74 -0
- kpubdata/transport/http.py +380 -0
- kpubdata/transport/retry.py +68 -0
- kpubdata-0.1.0.dist-info/METADATA +380 -0
- kpubdata-0.1.0.dist-info/RECORD +35 -0
- kpubdata-0.1.0.dist-info/WHEEL +4 -0
- kpubdata-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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
|
+
]
|