@nomadamas/k-skill 0.4.3 → 0.6.0
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.
- package/package.json +1 -1
- package/skills/animal-pharmacy-search/instruction.md +150 -0
- package/skills/animal-pharmacy-search/scripts/animal_pharmacy_mcp.py +276 -0
- package/skills/animal-pharmacy-search/skill.json +8 -0
- package/skills/consumer-price-safety-search/instruction.md +30 -0
- package/skills/consumer-price-safety-search/skill.json +6 -0
- package/skills/government-support-survey/instruction.md +52 -0
- package/skills/government-support-survey/references/NOTICE.md +30 -0
- package/skills/government-support-survey/scripts/run_survey.py +71 -0
- package/skills/government-support-survey/skill.json +10 -0
- package/skills/kamis-food-price/instruction.md +66 -0
- package/skills/kamis-food-price/scripts/run_kamis.py +110 -0
- package/skills/kamis-food-price/skill.json +9 -0
- package/skills/korean-cinema-search/instruction.md +7 -1
- package/skills/mofa-travel-safety/instruction.md +62 -0
- package/skills/mofa-travel-safety/scripts/run_mofa_travel_safety.py +57 -0
- package/skills/mofa-travel-safety/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +22 -2
- package/skills/real-estate-search/scripts/nationwide_daily_report.py +545 -0
- package/skills/seoul-weather-risk/instruction.md +4 -1
- package/skills/seoul-weather-risk/scripts/seoul_weather_risk.py +99 -3
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
- package/templates/browser.md +1 -1
|
@@ -33,9 +33,18 @@ STATUS_CODES = {
|
|
|
33
33
|
403: "forbidden",
|
|
34
34
|
404: "unknown_product",
|
|
35
35
|
409: "cursor_expired",
|
|
36
|
+
422: "query_window_unavailable",
|
|
36
37
|
429: "rate_limited",
|
|
37
38
|
503: "product_not_ready",
|
|
38
39
|
}
|
|
40
|
+
WINDOW_INSTANT_LENGTH = len("YYYY-MM-DD HH:MM:SS")
|
|
41
|
+
QUERY_WINDOW_DETAIL_FIELDS = (
|
|
42
|
+
"requested_from_at",
|
|
43
|
+
"requested_to_at",
|
|
44
|
+
"available_from_at",
|
|
45
|
+
"available_to_at",
|
|
46
|
+
"publication_id",
|
|
47
|
+
)
|
|
39
48
|
|
|
40
49
|
|
|
41
50
|
class SkillError(RuntimeError):
|
|
@@ -89,13 +98,20 @@ def _error_payload(raw: bytes) -> dict[str, Any]:
|
|
|
89
98
|
def _problem_error(status: int, raw: bytes, headers: Any) -> SkillError:
|
|
90
99
|
problem = _error_payload(raw)
|
|
91
100
|
code = problem.get("code") if isinstance(problem.get("code"), str) else STATUS_CODES.get(status, "api_error")
|
|
92
|
-
|
|
101
|
+
raw_detail = problem.get("detail")
|
|
102
|
+
message = raw_detail if isinstance(raw_detail, str) else problem.get("title")
|
|
93
103
|
if not isinstance(message, str) or not message:
|
|
94
104
|
message = f"ASK Seoul API가 HTTP {status} 응답을 반환했습니다."
|
|
95
|
-
details = {"status": status}
|
|
105
|
+
details: dict[str, Any] = {"status": status}
|
|
96
106
|
for name in ("type", "title", "product_id", "blockers", "request_id"):
|
|
97
107
|
if name in problem:
|
|
98
108
|
details[name] = problem[name]
|
|
109
|
+
if isinstance(raw_detail, dict):
|
|
110
|
+
details["detail"] = raw_detail
|
|
111
|
+
for name in QUERY_WINDOW_DETAIL_FIELDS:
|
|
112
|
+
value = raw_detail.get(name)
|
|
113
|
+
if isinstance(value, str) and value:
|
|
114
|
+
details[name] = value
|
|
99
115
|
retry_after = headers.get("Retry-After") if headers else None
|
|
100
116
|
if retry_after:
|
|
101
117
|
details["retry_after"] = retry_after
|
|
@@ -238,6 +254,72 @@ def _time_bound(value: str, edge: str) -> str:
|
|
|
238
254
|
return value
|
|
239
255
|
|
|
240
256
|
|
|
257
|
+
def _window_sort_key(value: str) -> str | None:
|
|
258
|
+
text = " ".join(value.strip().replace("T", " ", 1).split())
|
|
259
|
+
if len(text) < WINDOW_INSTANT_LENGTH:
|
|
260
|
+
return None
|
|
261
|
+
key = text[:WINDOW_INSTANT_LENGTH]
|
|
262
|
+
if (
|
|
263
|
+
key[4] == "-"
|
|
264
|
+
and key[7] == "-"
|
|
265
|
+
and key[10] == " "
|
|
266
|
+
and key[13] == ":"
|
|
267
|
+
and key[16] == ":"
|
|
268
|
+
and key[:4].isdigit()
|
|
269
|
+
and key[5:7].isdigit()
|
|
270
|
+
and key[8:10].isdigit()
|
|
271
|
+
and key[11:13].isdigit()
|
|
272
|
+
and key[14:16].isdigit()
|
|
273
|
+
and key[17:19].isdigit()
|
|
274
|
+
):
|
|
275
|
+
return key
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _intersect_query_window(
|
|
280
|
+
requested_from: str,
|
|
281
|
+
requested_to: str,
|
|
282
|
+
available_from: str,
|
|
283
|
+
available_to: str,
|
|
284
|
+
) -> tuple[str, str] | None:
|
|
285
|
+
req_from = _window_sort_key(requested_from)
|
|
286
|
+
req_to = _window_sort_key(requested_to)
|
|
287
|
+
avail_from = _window_sort_key(available_from)
|
|
288
|
+
avail_to = _window_sort_key(available_to)
|
|
289
|
+
if req_from is None or req_to is None or avail_from is None or avail_to is None:
|
|
290
|
+
return None
|
|
291
|
+
clipped_from = max(req_from, avail_from)
|
|
292
|
+
clipped_to = min(req_to, avail_to)
|
|
293
|
+
if clipped_from > clipped_to:
|
|
294
|
+
return None
|
|
295
|
+
return clipped_from, clipped_to
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _query_window_bounds(query: dict[str, str], error: SkillError) -> tuple[str, str, str, str] | None:
|
|
299
|
+
if error.details.get("status") != 422:
|
|
300
|
+
return None
|
|
301
|
+
available_from = error.details.get("available_from_at")
|
|
302
|
+
available_to = error.details.get("available_to_at")
|
|
303
|
+
requested_from = error.details.get("requested_from_at") or query.get("from")
|
|
304
|
+
requested_to = error.details.get("requested_to_at") or query.get("to")
|
|
305
|
+
if not all(isinstance(value, str) and value for value in (requested_from, requested_to, available_from, available_to)):
|
|
306
|
+
return None
|
|
307
|
+
return requested_from, requested_to, available_from, available_to
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _retry_query_after_unavailable_window(query: dict[str, str], error: SkillError) -> dict[str, str] | None:
|
|
311
|
+
bounds = _query_window_bounds(query, error)
|
|
312
|
+
if bounds is None or query.get("cursor"):
|
|
313
|
+
return None
|
|
314
|
+
clipped = _intersect_query_window(*bounds)
|
|
315
|
+
if clipped is None:
|
|
316
|
+
return None
|
|
317
|
+
clipped_from, clipped_to = clipped
|
|
318
|
+
if query.get("from") == clipped_from and query.get("to") == clipped_to:
|
|
319
|
+
return None
|
|
320
|
+
return {**query, "from": clipped_from, "to": clipped_to}
|
|
321
|
+
|
|
322
|
+
|
|
241
323
|
def _normalize_location_name(value: str) -> str:
|
|
242
324
|
return " ".join(unicodedata.normalize("NFC", value).strip().split())
|
|
243
325
|
|
|
@@ -358,7 +440,21 @@ def _detail(config: ApiConfig, product_id: str) -> dict[str, Any]:
|
|
|
358
440
|
|
|
359
441
|
def _data(config: ApiConfig, product_id: str, query: dict[str, str], limit: int) -> dict[str, Any]:
|
|
360
442
|
_validate_product_id(product_id)
|
|
361
|
-
|
|
443
|
+
try:
|
|
444
|
+
payload = _request_json(config, f"{PROXY_ROUTE_ROOT}/data", query)
|
|
445
|
+
except SkillError as exc:
|
|
446
|
+
retry_query = _retry_query_after_unavailable_window(query, exc)
|
|
447
|
+
if retry_query is None:
|
|
448
|
+
bounds = _query_window_bounds(query, exc)
|
|
449
|
+
if bounds is not None and _intersect_query_window(*bounds) is None:
|
|
450
|
+
raise SkillError(
|
|
451
|
+
"query_window_unavailable",
|
|
452
|
+
"요청한 조회 구간이 현재 제공 가능한 예보 window와 겹치지 않습니다.",
|
|
453
|
+
exc.details,
|
|
454
|
+
) from exc
|
|
455
|
+
raise
|
|
456
|
+
payload = _request_json(config, f"{PROXY_ROUTE_ROOT}/data", retry_query)
|
|
457
|
+
return _validate_data(payload, product_id, limit)
|
|
362
458
|
|
|
363
459
|
|
|
364
460
|
def _parser() -> argparse.ArgumentParser:
|
package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc
CHANGED
|
Binary file
|
package/templates/browser.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!-- mode:dolshoi -->
|
|
2
2
|
- Use the built-in browser tool backed by CloakBrowser first. Fall back to `k-skill-browser-runtime` providers only when CloakBrowser is unavailable.
|
|
3
3
|
<!-- mode:generic -->
|
|
4
|
-
- Use `k-skill-browser-runtime` (provider `auto
|
|
4
|
+
- Use `k-skill-browser-runtime` (provider `auto`) for logged-in or rendered-page automation. On macOS it tries Aside Browser first, then BrowserOS CDP, then user-launched Chrome/Chromium CDP; on other platforms it tries BrowserOS CDP, then Aside Browser, then user-launched Chrome/Chromium CDP. Do not launch or close the user's browser, and never solve CAPTCHA, identity proofing, or e-signature flows.
|