@nomadamas/k-skill 0.1.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/bin/k-skill.js +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only KOSIS (kosis.kr) Open API helper.
|
|
3
|
+
|
|
4
|
+
The script wraps four KOSIS endpoints needed to answer everyday Korean
|
|
5
|
+
official-statistics questions:
|
|
6
|
+
|
|
7
|
+
- statisticsSearch.do : keyword search of statistical tables
|
|
8
|
+
- statisticsData.do?getMeta : table metadata (dimensions, units)
|
|
9
|
+
- statisticsParameterData.do : actual data cells filtered by classifier
|
|
10
|
+
- statisticsBigData.do : large datasets (requires userStatsId)
|
|
11
|
+
|
|
12
|
+
It only reads. It never registers user statistics, edits anything, or
|
|
13
|
+
performs aggressive polling.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.parse
|
|
24
|
+
import urllib.request
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
SEARCH_URL = "https://kosis.kr/openapi/statisticsSearch.do"
|
|
30
|
+
META_URL = "https://kosis.kr/openapi/statisticsData.do"
|
|
31
|
+
DATA_URL = "https://kosis.kr/openapi/Param/statisticsParameterData.do"
|
|
32
|
+
BIGDATA_URL = "https://kosis.kr/openapi/statisticsBigData.do"
|
|
33
|
+
LIST_URL = "https://kosis.kr/openapi/statisticsList.do"
|
|
34
|
+
EXPLAIN_URL = "https://kosis.kr/openapi/statisticsExplData.do"
|
|
35
|
+
INDICATOR_URL = "https://kosis.kr/openapi/pkNumberService.do"
|
|
36
|
+
PROXY_BASE_URL_ENV_VAR = "KSKILL_PROXY_BASE_URL"
|
|
37
|
+
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
38
|
+
|
|
39
|
+
DEFAULT_TIMEOUT = 30
|
|
40
|
+
PRD_SE_VALUES = {"M", "Q", "S", "Y", "F", "IR"}
|
|
41
|
+
# `xls` is intentionally omitted: KOSIS returns it as a binary Excel payload,
|
|
42
|
+
# but the helper streams text-only output. Use bigdata json/sdmx/csv (text)
|
|
43
|
+
# for now; download xls files manually from the KOSIS web UI if you need them.
|
|
44
|
+
BIGDATA_FORMATS = {"json", "sdmx", "csv"}
|
|
45
|
+
|
|
46
|
+
# statisticsList.do service view codes
|
|
47
|
+
VIEW_CODES = {
|
|
48
|
+
"MT_ZTITLE", "MT_OTITLE", "MT_GTITLE01", "MT_GTITLE02",
|
|
49
|
+
"MT_CHOSUN_TITLE", "MT_HANKUK_TITLE", "MT_STOP_TITLE",
|
|
50
|
+
"MT_RTITLE", "MT_BUKHAN", "MT_TM1_TITLE", "MT_TM2_TITLE", "MT_ETITLE",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# pkNumberService.do sub-services
|
|
54
|
+
INDICATOR_SERVICES = {"1", "2", "3"}
|
|
55
|
+
|
|
56
|
+
ERROR_CODE_HINTS: dict[str, str] = {
|
|
57
|
+
"10": "인증키가 누락되었습니다. KSKILL_KOSIS_API_KEY 환경변수를 확인하세요.",
|
|
58
|
+
"11": "인증키가 만료되었거나 해당 endpoint에서 무효입니다. https://kosis.kr/openapi/ 에서 갱신하거나, bigdata는 본인이 등록한 userStatsId 인지 확인하세요.",
|
|
59
|
+
"20": "필수 요청 변수가 누락되었습니다. `meta --table-id <ID> --meta-type ITM --json` 으로 ITM 안에 들어 있는 OBJ_ID(분류 차원)와 코드를 확인하세요(많은 표가 OBJ 메타는 비어 있고 분류가 ITM 안에 들어 있음). 그 뒤 `--obj-l 1=<코드> --obj-l 2=<코드>` 형태로 필요한 차원을 모두 지정해 재호출하세요. 별도 OBJ 메타가 있는 표는 `--meta-type OBJ` 로도 확인 가능합니다.",
|
|
60
|
+
"21": "잘못된 요청 변수입니다. orgId/tblId/기간 형식을 재확인하세요. tblId가 의심되면 `search --query <키워드>` 로 정확한 ID를 다시 찾으세요.",
|
|
61
|
+
"30": "조회 결과가 없습니다. 키워드를 더 짧게(예: '1인 가구' → '가구') 또는 다른 표현으로 재검색하거나, 기간/분류 필터를 완화하세요. meta 호출에서 이 에러가 나면 해당 메타 타입을 표가 지원하지 않는 경우이므로 다른 `--meta-type` 을 시도하세요.",
|
|
62
|
+
"31": "조회 결과가 한도(40,000셀)를 초과했습니다. 기간을 좁히거나(예: 5년→1년) 분류 필터의 ALL 을 특정 코드로 바꾸세요(예: `--obj-l 1=ALL` → `--obj-l 1=11` 서울만). 그래도 부족하면 `bigdata` 서브커맨드 + 사전 등록한 userStatsId 를 사용하세요.",
|
|
63
|
+
"40": "분당 호출 한도(1,000건)를 초과했습니다. 잠시 대기 후 재시도하거나 호출 간 sleep 을 두세요.",
|
|
64
|
+
"41": "1회 호출 ROW 한도를 초과했습니다. 기간이나 분류를 좁혀 쿼리를 분할하세요.",
|
|
65
|
+
"42": "사용자별 이용이 제한되었습니다. KOSIS 운영팀에 문의하세요.",
|
|
66
|
+
"50": "KOSIS 서버 오류입니다. 1~2초 대기 후 재시도하세요.",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class KosisConfig:
|
|
72
|
+
api_key: str
|
|
73
|
+
timeout: int = DEFAULT_TIMEOUT
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class KosisError(RuntimeError):
|
|
77
|
+
def __init__(self, code: str | None, message: str) -> None:
|
|
78
|
+
self.code = code or ""
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_resultcount(value: str) -> int:
|
|
83
|
+
try:
|
|
84
|
+
result = int(value)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
87
|
+
if not 1 <= result <= 5000:
|
|
88
|
+
raise argparse.ArgumentTypeError("must be between 1 and 5000")
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_prd_se(value: str) -> str:
|
|
93
|
+
upper = value.strip().upper()
|
|
94
|
+
if upper not in PRD_SE_VALUES:
|
|
95
|
+
raise argparse.ArgumentTypeError(
|
|
96
|
+
"must be one of: " + ", ".join(sorted(PRD_SE_VALUES))
|
|
97
|
+
)
|
|
98
|
+
return upper
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_bigdata_format(value: str) -> str:
|
|
102
|
+
lower = value.strip().lower()
|
|
103
|
+
if lower not in BIGDATA_FORMATS:
|
|
104
|
+
raise argparse.ArgumentTypeError(
|
|
105
|
+
"must be one of: " + ", ".join(sorted(BIGDATA_FORMATS))
|
|
106
|
+
)
|
|
107
|
+
return lower
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_view_code(value: str) -> str:
|
|
111
|
+
upper = value.strip().upper()
|
|
112
|
+
if upper not in VIEW_CODES:
|
|
113
|
+
raise argparse.ArgumentTypeError(
|
|
114
|
+
"must be one of: " + ", ".join(sorted(VIEW_CODES))
|
|
115
|
+
)
|
|
116
|
+
return upper
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_indicator_service(value: str) -> str:
|
|
120
|
+
if value not in INDICATOR_SERVICES:
|
|
121
|
+
raise argparse.ArgumentTypeError(
|
|
122
|
+
"must be one of: " + ", ".join(sorted(INDICATOR_SERVICES))
|
|
123
|
+
)
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _add_common_flags(parser: argparse.ArgumentParser) -> None:
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--timeout",
|
|
130
|
+
type=int,
|
|
131
|
+
default=DEFAULT_TIMEOUT,
|
|
132
|
+
help=f"HTTP timeout in seconds (default {DEFAULT_TIMEOUT}).",
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"--dry-run",
|
|
136
|
+
action="store_true",
|
|
137
|
+
help="Print the request URL and parameters without calling KOSIS.",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--proxy-base-url",
|
|
141
|
+
help=(
|
|
142
|
+
"k-skill-proxy base URL for search/meta/data "
|
|
143
|
+
f"(default {DEFAULT_PROXY_BASE_URL}; override with {PROXY_BASE_URL_ENV_VAR})."
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--direct",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Call KOSIS directly with KSKILL_KOSIS_API_KEY instead of k-skill-proxy.",
|
|
150
|
+
)
|
|
151
|
+
output = parser.add_mutually_exclusive_group()
|
|
152
|
+
output.add_argument("--json", action="store_true", help="Print JSON output.")
|
|
153
|
+
output.add_argument("--text", action="store_true", help="Print human-readable output.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
157
|
+
parser = argparse.ArgumentParser(
|
|
158
|
+
description="Read-only KOSIS Open API helper. "
|
|
159
|
+
"Place output flags (--json/--text/--dry-run/--timeout) AFTER the subcommand.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
163
|
+
|
|
164
|
+
search = sub.add_parser("search", help="Search statistical tables by keyword.")
|
|
165
|
+
_add_common_flags(search)
|
|
166
|
+
search.add_argument("--query", required=True, help="Korean keyword (e.g. '1인 가구').")
|
|
167
|
+
search.add_argument(
|
|
168
|
+
"--result-count",
|
|
169
|
+
type=parse_resultcount,
|
|
170
|
+
default=20,
|
|
171
|
+
help="Result count, 1-5000 (default 20).",
|
|
172
|
+
)
|
|
173
|
+
search.add_argument(
|
|
174
|
+
"--start-count",
|
|
175
|
+
type=int,
|
|
176
|
+
default=1,
|
|
177
|
+
help="Result offset for pagination (default 1).",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
meta = sub.add_parser("meta", help="Fetch table metadata (dimensions, units).")
|
|
181
|
+
_add_common_flags(meta)
|
|
182
|
+
meta.add_argument("--org-id", default="101", help="Organization ID (default 101).")
|
|
183
|
+
meta.add_argument("--table-id", required=True, help="KOSIS table ID, e.g. DT_1IN0001.")
|
|
184
|
+
meta.add_argument(
|
|
185
|
+
"--meta-type",
|
|
186
|
+
default="TBL",
|
|
187
|
+
choices=["TBL", "ITM", "OBJ"],
|
|
188
|
+
help="Meta type (default TBL).",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
data = sub.add_parser("data", help="Fetch table data filtered by classifiers.")
|
|
192
|
+
_add_common_flags(data)
|
|
193
|
+
data.add_argument("--org-id", default="101", help="Organization ID (default 101).")
|
|
194
|
+
data.add_argument("--table-id", required=True, help="KOSIS table ID.")
|
|
195
|
+
data.add_argument(
|
|
196
|
+
"--prd-se",
|
|
197
|
+
type=parse_prd_se,
|
|
198
|
+
required=True,
|
|
199
|
+
help="Period frequency: M Q S Y F IR.",
|
|
200
|
+
)
|
|
201
|
+
data.add_argument("--start", required=True, help="Start period (format depends on --prd-se).")
|
|
202
|
+
data.add_argument("--end", required=True, help="End period.")
|
|
203
|
+
data.add_argument("--itm-id", default="ALL", help="Item ID filter (default ALL).")
|
|
204
|
+
data.add_argument(
|
|
205
|
+
"--obj-l",
|
|
206
|
+
action="append",
|
|
207
|
+
default=[],
|
|
208
|
+
metavar="N=VALUE",
|
|
209
|
+
help="Classifier filter, e.g. --obj-l 1=ALL --obj-l 2=00. Repeatable. "
|
|
210
|
+
"If omitted, --obj-l 1=ALL is used.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
bigdata = sub.add_parser(
|
|
214
|
+
"bigdata",
|
|
215
|
+
help="Fetch large datasets via statisticsBigData (requires userStatsId).",
|
|
216
|
+
)
|
|
217
|
+
_add_common_flags(bigdata)
|
|
218
|
+
bigdata.add_argument(
|
|
219
|
+
"--user-stats-id",
|
|
220
|
+
required=True,
|
|
221
|
+
help="userStatsId pre-registered on KOSIS (개발가이드 > 대용량 통계자료 > URL생성).",
|
|
222
|
+
)
|
|
223
|
+
bigdata.add_argument(
|
|
224
|
+
"--format",
|
|
225
|
+
dest="bigdata_format",
|
|
226
|
+
type=parse_bigdata_format,
|
|
227
|
+
default="json",
|
|
228
|
+
help="Output format: json sdmx csv xls (default json).",
|
|
229
|
+
)
|
|
230
|
+
bigdata.add_argument(
|
|
231
|
+
"--prd-se",
|
|
232
|
+
type=parse_prd_se,
|
|
233
|
+
help="Period frequency override.",
|
|
234
|
+
)
|
|
235
|
+
bigdata.add_argument(
|
|
236
|
+
"--new-est-prd-cnt",
|
|
237
|
+
type=int,
|
|
238
|
+
help="Count of latest periods to fetch (alternative to start/end).",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
list_cmd = sub.add_parser(
|
|
242
|
+
"list",
|
|
243
|
+
help="Browse statistical table categories (statisticsList.do).",
|
|
244
|
+
)
|
|
245
|
+
_add_common_flags(list_cmd)
|
|
246
|
+
list_cmd.add_argument(
|
|
247
|
+
"--vw-cd",
|
|
248
|
+
type=parse_view_code,
|
|
249
|
+
required=True,
|
|
250
|
+
help="Service view code: MT_ZTITLE(국내통계주제별) MT_OTITLE(기관별) "
|
|
251
|
+
"MT_GTITLE01/02(e-지방지표) MT_RTITLE(국제통계) MT_BUKHAN(북한통계) etc.",
|
|
252
|
+
)
|
|
253
|
+
list_cmd.add_argument(
|
|
254
|
+
"--parent-id",
|
|
255
|
+
default="",
|
|
256
|
+
help="Parent list ID for sub-categories (sent as parentListId). Empty string = top-level.",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
explain = sub.add_parser(
|
|
260
|
+
"explain",
|
|
261
|
+
help="Fetch statistical survey description (statisticsExplData.do).",
|
|
262
|
+
)
|
|
263
|
+
_add_common_flags(explain)
|
|
264
|
+
explain.add_argument(
|
|
265
|
+
"--stat-id",
|
|
266
|
+
help="Statistics survey ID (alternative to --org-id/--table-id).",
|
|
267
|
+
)
|
|
268
|
+
explain.add_argument("--org-id", help="Organization ID (with --table-id).")
|
|
269
|
+
explain.add_argument("--table-id", help="KOSIS table ID (with --org-id).")
|
|
270
|
+
explain.add_argument(
|
|
271
|
+
"--meta-itm",
|
|
272
|
+
default="All",
|
|
273
|
+
choices=["All", *_EXPLAIN_FIELD_LABELS.keys()] if "_EXPLAIN_FIELD_LABELS" in globals() else None,
|
|
274
|
+
help="Requested field: All(전체) or one field name (statsNm, statsKind, writingPurps, ...). Default All.",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
indicator = sub.add_parser(
|
|
278
|
+
"indicator",
|
|
279
|
+
help="Fetch key indicator metadata (pkNumberService.do).",
|
|
280
|
+
)
|
|
281
|
+
_add_common_flags(indicator)
|
|
282
|
+
indicator.add_argument(
|
|
283
|
+
"--service",
|
|
284
|
+
type=parse_indicator_service,
|
|
285
|
+
default="1",
|
|
286
|
+
help="Sub-service: 1(개념) 2(산정방법·출처) 3(전체설명). Default 1.",
|
|
287
|
+
)
|
|
288
|
+
indicator.add_argument(
|
|
289
|
+
"--jipyo-id",
|
|
290
|
+
required=True,
|
|
291
|
+
help="Indicator ID (e.g. 160).",
|
|
292
|
+
)
|
|
293
|
+
indicator.add_argument(
|
|
294
|
+
"--page-no",
|
|
295
|
+
type=int,
|
|
296
|
+
default=1,
|
|
297
|
+
help="Page number (default 1).",
|
|
298
|
+
)
|
|
299
|
+
indicator.add_argument(
|
|
300
|
+
"--num-of-rows",
|
|
301
|
+
type=int,
|
|
302
|
+
default=10,
|
|
303
|
+
help="Rows per page (default 10).",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return parser.parse_args(argv)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def load_secrets_env(path: Path) -> dict[str, str]:
|
|
310
|
+
if not path.exists():
|
|
311
|
+
return {}
|
|
312
|
+
secrets: dict[str, str] = {}
|
|
313
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
314
|
+
line = raw.strip()
|
|
315
|
+
if not line or line.startswith("#"):
|
|
316
|
+
continue
|
|
317
|
+
if "=" not in line:
|
|
318
|
+
continue
|
|
319
|
+
key, _, value = line.partition("=")
|
|
320
|
+
secrets[key.strip()] = value.strip().strip('"').strip("'")
|
|
321
|
+
return secrets
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def resolve_api_key(
|
|
325
|
+
*,
|
|
326
|
+
env: dict[str, str] | None = None,
|
|
327
|
+
secrets_path: Path | None = None,
|
|
328
|
+
) -> str:
|
|
329
|
+
env_map = env if env is not None else os.environ
|
|
330
|
+
direct = env_map.get("KSKILL_KOSIS_API_KEY")
|
|
331
|
+
if direct:
|
|
332
|
+
return direct.strip()
|
|
333
|
+
|
|
334
|
+
candidate = secrets_path or Path("~/.config/k-skill/secrets.env").expanduser()
|
|
335
|
+
secrets = load_secrets_env(candidate)
|
|
336
|
+
fallback = secrets.get("KSKILL_KOSIS_API_KEY")
|
|
337
|
+
if fallback:
|
|
338
|
+
return fallback.strip()
|
|
339
|
+
|
|
340
|
+
raise SystemExit(
|
|
341
|
+
"missing required environment variable: KSKILL_KOSIS_API_KEY\n"
|
|
342
|
+
"발급: https://kosis.kr/openapi/ (무료, KOSIS 회원가입 후 활용신청)\n"
|
|
343
|
+
"참조: kosis-stats/references/kosis-openapi-guide.md"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def resolve_proxy_base_url(
|
|
348
|
+
explicit_base_url: str | None = None,
|
|
349
|
+
env: dict[str, str] | None = None,
|
|
350
|
+
) -> str:
|
|
351
|
+
env_map = env if env is not None else os.environ
|
|
352
|
+
candidate = (explicit_base_url or env_map.get(PROXY_BASE_URL_ENV_VAR) or "").strip()
|
|
353
|
+
if candidate.casefold() in {"off", "false", "0", "disable", "disabled", "none"}:
|
|
354
|
+
raise SystemExit(f"{PROXY_BASE_URL_ENV_VAR} is disabled; pass --direct to use KSKILL_KOSIS_API_KEY.")
|
|
355
|
+
if candidate and candidate != "replace-me":
|
|
356
|
+
return candidate.rstrip("/")
|
|
357
|
+
return DEFAULT_PROXY_BASE_URL
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def parse_obj_l(values: list[str]) -> dict[str, str]:
|
|
361
|
+
objs: dict[str, str] = {}
|
|
362
|
+
for raw in values:
|
|
363
|
+
if "=" not in raw:
|
|
364
|
+
raise SystemExit(f"--obj-l must be N=VALUE, got: {raw}")
|
|
365
|
+
key, _, value = raw.partition("=")
|
|
366
|
+
key = key.strip()
|
|
367
|
+
if not key.isdigit() or not 1 <= int(key) <= 8:
|
|
368
|
+
raise SystemExit(f"--obj-l index must be 1..8, got: {key}")
|
|
369
|
+
objs[f"objL{key}"] = value.strip() or "ALL"
|
|
370
|
+
if not objs:
|
|
371
|
+
objs["objL1"] = "ALL"
|
|
372
|
+
return objs
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def build_search_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
376
|
+
return {
|
|
377
|
+
"method": "getList",
|
|
378
|
+
"apiKey": api_key,
|
|
379
|
+
"format": "json",
|
|
380
|
+
"jsonVD": "Y",
|
|
381
|
+
"searchNm": args.query,
|
|
382
|
+
"resultCount": str(args.result_count),
|
|
383
|
+
"startCount": str(args.start_count),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def build_meta_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
388
|
+
return {
|
|
389
|
+
"method": "getMeta",
|
|
390
|
+
"type": args.meta_type,
|
|
391
|
+
"apiKey": api_key,
|
|
392
|
+
"format": "json",
|
|
393
|
+
"jsonVD": "Y",
|
|
394
|
+
"orgId": args.org_id,
|
|
395
|
+
"tblId": args.table_id,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def build_data_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
400
|
+
params: dict[str, str] = {
|
|
401
|
+
"method": "getList",
|
|
402
|
+
"apiKey": api_key,
|
|
403
|
+
"format": "json",
|
|
404
|
+
"jsonVD": "Y",
|
|
405
|
+
"orgId": args.org_id,
|
|
406
|
+
"tblId": args.table_id,
|
|
407
|
+
"itmId": args.itm_id,
|
|
408
|
+
"prdSe": args.prd_se,
|
|
409
|
+
"startPrdDe": args.start,
|
|
410
|
+
"endPrdDe": args.end,
|
|
411
|
+
}
|
|
412
|
+
params.update(parse_obj_l(args.obj_l))
|
|
413
|
+
return params
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def build_bigdata_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
417
|
+
params: dict[str, str] = {
|
|
418
|
+
"method": "getList",
|
|
419
|
+
"apiKey": api_key,
|
|
420
|
+
"format": args.bigdata_format,
|
|
421
|
+
"jsonVD": "Y",
|
|
422
|
+
"userStatsId": args.user_stats_id,
|
|
423
|
+
}
|
|
424
|
+
if args.prd_se:
|
|
425
|
+
params["prdSe"] = args.prd_se
|
|
426
|
+
if args.new_est_prd_cnt is not None:
|
|
427
|
+
params["newEstPrdCnt"] = str(args.new_est_prd_cnt)
|
|
428
|
+
return params
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def build_list_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
432
|
+
return {
|
|
433
|
+
"method": "getList",
|
|
434
|
+
"apiKey": api_key,
|
|
435
|
+
"format": "json",
|
|
436
|
+
"jsonVD": "Y",
|
|
437
|
+
"vwCd": args.vw_cd,
|
|
438
|
+
"parentListId": args.parent_id,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def build_explain_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
443
|
+
params: dict[str, str] = {
|
|
444
|
+
"method": "getList",
|
|
445
|
+
"apiKey": api_key,
|
|
446
|
+
"format": "json",
|
|
447
|
+
"jsonVD": "Y",
|
|
448
|
+
"metaItm": args.meta_itm,
|
|
449
|
+
}
|
|
450
|
+
if args.stat_id:
|
|
451
|
+
params["statId"] = args.stat_id
|
|
452
|
+
elif args.org_id and args.table_id:
|
|
453
|
+
params["orgId"] = args.org_id
|
|
454
|
+
params["tblId"] = args.table_id
|
|
455
|
+
else:
|
|
456
|
+
raise SystemExit(
|
|
457
|
+
"explain requires either --stat-id or both --org-id and --table-id."
|
|
458
|
+
)
|
|
459
|
+
return params
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def build_indicator_params(api_key: str, args: argparse.Namespace) -> dict[str, str]:
|
|
463
|
+
return {
|
|
464
|
+
"method": "getList",
|
|
465
|
+
"apiKey": api_key,
|
|
466
|
+
"format": "json",
|
|
467
|
+
"jsonVD": "Y",
|
|
468
|
+
"service": args.service,
|
|
469
|
+
"serviceDetail": "pkNotion" if args.service == "1" else "pkCalcSource"
|
|
470
|
+
if args.service == "2" else "pkAll",
|
|
471
|
+
"jipyoId": args.jipyo_id,
|
|
472
|
+
"pageNo": str(args.page_no),
|
|
473
|
+
"numOfRows": str(args.num_of_rows),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def build_url(base: str, params: dict[str, str]) -> str:
|
|
478
|
+
query = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
|
|
479
|
+
return f"{base}?{query}"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def is_proxy_not_configured_body(body: str) -> bool:
|
|
483
|
+
try:
|
|
484
|
+
payload = json.loads(body)
|
|
485
|
+
except json.JSONDecodeError:
|
|
486
|
+
return False
|
|
487
|
+
return isinstance(payload, dict) and payload.get("error") == "upstream_not_configured"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def fetch_text(url: str, timeout: int, *, via_proxy: bool = False) -> str:
|
|
491
|
+
request = urllib.request.Request(url, headers={"User-Agent": "k-skill/kosis-stats"})
|
|
492
|
+
try:
|
|
493
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
494
|
+
charset = response.headers.get_content_charset() or "utf-8"
|
|
495
|
+
return response.read().decode(charset, errors="replace")
|
|
496
|
+
except urllib.error.HTTPError as exc:
|
|
497
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
498
|
+
if exc.code == 503 and is_proxy_not_configured_body(body):
|
|
499
|
+
raise KosisError("503", "k-skill-proxy에 필요한 KOSIS API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요.") from exc
|
|
500
|
+
raise KosisError(str(exc.code), f"HTTP {exc.code}: {body[:200]}") from exc
|
|
501
|
+
except urllib.error.URLError as exc:
|
|
502
|
+
if not via_proxy:
|
|
503
|
+
raise KosisError(None, f"network error: {exc.reason}") from exc
|
|
504
|
+
raise KosisError(
|
|
505
|
+
None,
|
|
506
|
+
"설정된 k-skill-proxy 서버가 응답하지 않습니다. "
|
|
507
|
+
f"잠시 후 재시도하거나 운영자에게 문의하세요. (상세: {exc.reason})"
|
|
508
|
+
) from exc
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def fix_unquoted_keys(text: str) -> str:
|
|
512
|
+
"""KOSIS sometimes returns JSON with unquoted keys."""
|
|
513
|
+
return re.sub(r'([{,])\s*([A-Za-z_][A-Za-z0-9_]*)\s*:', r'\1"\2":', text)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def parse_kosis_json(text: str) -> Any:
|
|
517
|
+
body = text.strip()
|
|
518
|
+
try:
|
|
519
|
+
return json.loads(body)
|
|
520
|
+
except json.JSONDecodeError:
|
|
521
|
+
return json.loads(fix_unquoted_keys(body))
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
_XML_ERROR_RE = re.compile(
|
|
525
|
+
r"<error>\s*<err>([^<]*)</err>\s*<errMsg>([^<]*)</errMsg>", re.IGNORECASE
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def detect_xml_error(text: str) -> KosisError | None:
|
|
530
|
+
match = _XML_ERROR_RE.search(text)
|
|
531
|
+
if not match:
|
|
532
|
+
return None
|
|
533
|
+
code = match.group(1).strip()
|
|
534
|
+
message = match.group(2).strip()
|
|
535
|
+
hint = ERROR_CODE_HINTS.get(code, "")
|
|
536
|
+
full = f"KOSIS error {code or '?'}: {message}"
|
|
537
|
+
if hint:
|
|
538
|
+
full += f" ({hint})"
|
|
539
|
+
return KosisError(code, full)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def detect_kosis_error(payload: Any) -> KosisError | None:
|
|
543
|
+
if isinstance(payload, dict):
|
|
544
|
+
message = payload.get("errMsg")
|
|
545
|
+
if message:
|
|
546
|
+
code = str(payload.get("err") or payload.get("errCode") or "").strip()
|
|
547
|
+
hint = ERROR_CODE_HINTS.get(code, "")
|
|
548
|
+
full = f"KOSIS error {code or '?'}: {message}"
|
|
549
|
+
if hint:
|
|
550
|
+
full += f" ({hint})"
|
|
551
|
+
return KosisError(code, full)
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def call_kosis(url: str, timeout: int, *, format_hint: str = "json", via_proxy: bool = False) -> Any:
|
|
556
|
+
text = fetch_text(url, timeout, via_proxy=via_proxy)
|
|
557
|
+
xml_err = detect_xml_error(text)
|
|
558
|
+
if xml_err is not None:
|
|
559
|
+
raise xml_err
|
|
560
|
+
if format_hint != "json":
|
|
561
|
+
stripped = text.lstrip()
|
|
562
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
563
|
+
try:
|
|
564
|
+
payload = parse_kosis_json(text)
|
|
565
|
+
except json.JSONDecodeError:
|
|
566
|
+
return text
|
|
567
|
+
err = detect_kosis_error(payload)
|
|
568
|
+
if err is not None:
|
|
569
|
+
raise err
|
|
570
|
+
return text
|
|
571
|
+
payload = parse_kosis_json(text)
|
|
572
|
+
err = detect_kosis_error(payload)
|
|
573
|
+
if err is not None:
|
|
574
|
+
raise err
|
|
575
|
+
return payload
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def render_search_text(payload: Any) -> str:
|
|
579
|
+
if not isinstance(payload, list) or not payload:
|
|
580
|
+
return (
|
|
581
|
+
"조회 결과가 없습니다. 키워드를 더 짧게(예: '1인 가구' → '가구') "
|
|
582
|
+
"또는 다른 표현으로 재검색해 보세요. "
|
|
583
|
+
"`--result-count` 와 `--start-count` 로 더 많은 후보를 페이징할 수도 있습니다."
|
|
584
|
+
)
|
|
585
|
+
lines = []
|
|
586
|
+
for entry in payload:
|
|
587
|
+
if not isinstance(entry, dict):
|
|
588
|
+
continue
|
|
589
|
+
org = entry.get("ORG_NM", "?")
|
|
590
|
+
tbl = entry.get("TBL_NM", "?")
|
|
591
|
+
org_id = entry.get("ORG_ID", "?")
|
|
592
|
+
tbl_id = entry.get("TBL_ID", "?")
|
|
593
|
+
prd = f"{entry.get('STRT_PRD_DE', '?')}~{entry.get('END_PRD_DE', '?')}"
|
|
594
|
+
lines.append(f"- [{org_id}/{tbl_id}] {org} / {tbl} ({prd})")
|
|
595
|
+
lines.append(
|
|
596
|
+
"\nNext: `meta --table-id <ID>` 로 분류·항목·단위 확인 → "
|
|
597
|
+
"`data --table-id <ID> --prd-se <Y|M|Q|...> --start ... --end ...` 로 작은 슬라이스부터 받기."
|
|
598
|
+
)
|
|
599
|
+
return "\n".join(lines)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def render_meta_text(payload: Any) -> str:
|
|
603
|
+
if not isinstance(payload, list) or not payload:
|
|
604
|
+
return (
|
|
605
|
+
"메타 정보가 없습니다. 표가 해당 메타 타입을 지원하지 않을 수 있습니다. "
|
|
606
|
+
"다른 `--meta-type` (TBL/ITM/OBJ) 을 시도해 보세요."
|
|
607
|
+
)
|
|
608
|
+
lines = []
|
|
609
|
+
for entry in payload:
|
|
610
|
+
if not isinstance(entry, dict):
|
|
611
|
+
continue
|
|
612
|
+
kr = entry.get("TBL_NM") or entry.get("ITM_NM") or entry.get("C_NM") or "?"
|
|
613
|
+
en = entry.get("TBL_NM_ENG") or entry.get("ITM_NM_ENG") or entry.get("C_NM_ENG") or ""
|
|
614
|
+
suffix = f" / {en}" if en else ""
|
|
615
|
+
lines.append(f"- {kr}{suffix}")
|
|
616
|
+
return "\n".join(lines)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def render_data_text(payload: Any) -> str:
|
|
620
|
+
if not isinstance(payload, list) or not payload:
|
|
621
|
+
return (
|
|
622
|
+
"데이터가 없습니다. 기간(`--start`/`--end`), 항목(`--itm-id`), "
|
|
623
|
+
"분류(`--obj-l`) 필터를 완화하거나 `meta` 로 표 구조를 다시 확인하세요."
|
|
624
|
+
)
|
|
625
|
+
lines = []
|
|
626
|
+
units: set[str] = set()
|
|
627
|
+
periods: set[str] = set()
|
|
628
|
+
for entry in payload[:50]:
|
|
629
|
+
if not isinstance(entry, dict):
|
|
630
|
+
continue
|
|
631
|
+
prd = str(entry.get("PRD_DE", "?"))
|
|
632
|
+
unit = str(entry.get("UNIT_NM", "")).strip()
|
|
633
|
+
item = entry.get("ITM_NM", "?")
|
|
634
|
+
c1 = entry.get("C1_NM", "")
|
|
635
|
+
value = entry.get("DT", "?")
|
|
636
|
+
suffix = f" ({c1})" if c1 else ""
|
|
637
|
+
unit_suffix = f" {unit}" if unit else ""
|
|
638
|
+
lines.append(f"- {prd} | {item}{suffix} = {value}{unit_suffix}".rstrip())
|
|
639
|
+
if unit:
|
|
640
|
+
units.add(unit)
|
|
641
|
+
periods.add(prd)
|
|
642
|
+
if len(payload) > 50:
|
|
643
|
+
lines.append(f"... ({len(payload) - 50} rows omitted; --json 으로 전체 받기)")
|
|
644
|
+
summary_parts = [f"rows={len(payload)}"]
|
|
645
|
+
if periods:
|
|
646
|
+
period_list = sorted(p for p in periods if p and p != "?")
|
|
647
|
+
if period_list:
|
|
648
|
+
summary_parts.append(
|
|
649
|
+
f"period={period_list[0]}~{period_list[-1]}"
|
|
650
|
+
if len(period_list) > 1 else f"period={period_list[0]}"
|
|
651
|
+
)
|
|
652
|
+
if units:
|
|
653
|
+
summary_parts.append("unit=" + ",".join(sorted(units)))
|
|
654
|
+
else:
|
|
655
|
+
summary_parts.append("unit=(KOSIS 응답에 UNIT_NM 미포함)")
|
|
656
|
+
lines.append("\n[summary] " + ", ".join(summary_parts))
|
|
657
|
+
return "\n".join(lines)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
_EXPLAIN_FIELD_LABELS: dict[str, str] = {
|
|
661
|
+
"statsNm": "조사명",
|
|
662
|
+
"statsKind": "작성유형",
|
|
663
|
+
"statsEnd": "통계종류",
|
|
664
|
+
"statsContinue": "계속여부",
|
|
665
|
+
"basisLaw": "법적근거",
|
|
666
|
+
"writingPurps": "조사목적",
|
|
667
|
+
"examinPd": "조사기간",
|
|
668
|
+
"statsPeriod": "조사주기",
|
|
669
|
+
"writingSystem": "조사체계",
|
|
670
|
+
"writingTel": "연락처",
|
|
671
|
+
"statsField": "통계분야·실태",
|
|
672
|
+
"examinObjrange": "조사대상범위",
|
|
673
|
+
"examinObjArea": "조사대상지역",
|
|
674
|
+
"josaUnit": "조사단위·규모",
|
|
675
|
+
"applyGroup": "적용분류",
|
|
676
|
+
"josaItm": "조사항목",
|
|
677
|
+
"pubPeriod": "공표주기",
|
|
678
|
+
"pubExtent": "공표범위",
|
|
679
|
+
"pubDate": "공표시기",
|
|
680
|
+
"publictMth": "공표방법/URL",
|
|
681
|
+
"examinTrgetPd": "조사대상기간/기준시점",
|
|
682
|
+
"dataUserNote": "자료이용시 유의사항",
|
|
683
|
+
"mainTermExpl": "주요 용어해설",
|
|
684
|
+
"dataCollectMth": "자료 수집방법",
|
|
685
|
+
"examinHistory": "조사연혁",
|
|
686
|
+
"confmNo": "승인번호",
|
|
687
|
+
"confmDt": "승인일자",
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def render_list_text(payload: Any) -> str:
|
|
692
|
+
if not isinstance(payload, list) or not payload:
|
|
693
|
+
return (
|
|
694
|
+
"조회 결과가 없습니다. --parent-id 를 비워 최상위 카테고리를 보거나, "
|
|
695
|
+
"다른 --vw-cd (서비스뷰 코드)를 시도하세요."
|
|
696
|
+
)
|
|
697
|
+
lines: list[str] = []
|
|
698
|
+
for entry in payload:
|
|
699
|
+
if not isinstance(entry, dict):
|
|
700
|
+
continue
|
|
701
|
+
list_id = entry.get("LIST_ID", "")
|
|
702
|
+
list_nm = entry.get("LIST_NM", "?")
|
|
703
|
+
org_id = entry.get("ORG_ID", "")
|
|
704
|
+
tbl_id = entry.get("TBL_ID", "")
|
|
705
|
+
tbl_nm = entry.get("TBL_NM", "")
|
|
706
|
+
send_de = entry.get("SEND_DE", "")
|
|
707
|
+
if tbl_id:
|
|
708
|
+
lines.append(f"- [{list_id}] {list_nm} → [{org_id}/{tbl_id}] {tbl_nm} (갱신: {send_de})")
|
|
709
|
+
else:
|
|
710
|
+
lines.append(f"- [{list_id}] {list_nm}")
|
|
711
|
+
lines.append(
|
|
712
|
+
"\nNext: 하위 카테고리는 `list --vw-cd <코드> --parent-id <LIST_ID>`. "
|
|
713
|
+
"통계표는 `meta --table-id <TBL_ID>` 또는 `data --table-id <TBL_ID> ...`."
|
|
714
|
+
)
|
|
715
|
+
return "\n".join(lines)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def render_explain_text(payload: Any) -> str:
|
|
719
|
+
if not isinstance(payload, list) or not payload:
|
|
720
|
+
return (
|
|
721
|
+
"설명 정보가 없습니다. statId 또는 orgId+tblId 를 재확인하세요. "
|
|
722
|
+
"`search --query <키워드>` 로 정확한 ID를 찾을 수 있습니다."
|
|
723
|
+
)
|
|
724
|
+
lines: list[str] = []
|
|
725
|
+
for entry in payload:
|
|
726
|
+
if not isinstance(entry, dict):
|
|
727
|
+
continue
|
|
728
|
+
lines.append("---")
|
|
729
|
+
for field, label in _EXPLAIN_FIELD_LABELS.items():
|
|
730
|
+
value = entry.get(field, "")
|
|
731
|
+
if value:
|
|
732
|
+
lines.append(f"{label}: {value}")
|
|
733
|
+
if len(lines) == 1 and lines[0] == "---":
|
|
734
|
+
return "설명 정보 필드가 모두 비어 있습니다. 다른 --meta-itm 을 시도하세요."
|
|
735
|
+
return "\n".join(lines)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def render_indicator_text(payload: Any) -> str:
|
|
739
|
+
if not isinstance(payload, list) or not payload:
|
|
740
|
+
return (
|
|
741
|
+
"지표 정보가 없습니다. --jipyo-id 를 확인하세요. "
|
|
742
|
+
"KOSIS 개발가이드에서 사용 가능한 지표ID 목록을 볼 수 있습니다."
|
|
743
|
+
)
|
|
744
|
+
lines: list[str] = []
|
|
745
|
+
for entry in payload:
|
|
746
|
+
if not isinstance(entry, dict):
|
|
747
|
+
continue
|
|
748
|
+
jipyo_id = entry.get("statJipyoId", entry.get("jipyoId", "?"))
|
|
749
|
+
jipyo_nm = entry.get("statJipyoNm", entry.get("jipyoNm", "?"))
|
|
750
|
+
title = entry.get("jipyoExplan", "")
|
|
751
|
+
details = [
|
|
752
|
+
("개념", entry.get("jipyoExplan1", entry.get("jipyoCalc", ""))),
|
|
753
|
+
("산정방법", entry.get("jipyoExplan2", "")),
|
|
754
|
+
("출처", entry.get("jipyoExplan3", "")),
|
|
755
|
+
]
|
|
756
|
+
lines.append(f"- [{jipyo_id}] {jipyo_nm}")
|
|
757
|
+
if title:
|
|
758
|
+
lines.append(f" 제목: {title}")
|
|
759
|
+
for label, value in details:
|
|
760
|
+
if not value:
|
|
761
|
+
continue
|
|
762
|
+
display = value[:500] if len(value) > 500 else value
|
|
763
|
+
lines.append(f" {label}: {display}")
|
|
764
|
+
if len(value) > 500:
|
|
765
|
+
lines.append(f" ... ({len(value) - 500}자 생략, --json으로 전체)")
|
|
766
|
+
return "\n".join(lines)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def render_text(command: str, payload: Any) -> str:
|
|
770
|
+
if command == "search":
|
|
771
|
+
return render_search_text(payload)
|
|
772
|
+
if command == "meta":
|
|
773
|
+
return render_meta_text(payload)
|
|
774
|
+
if command == "data":
|
|
775
|
+
return render_data_text(payload)
|
|
776
|
+
if command == "list":
|
|
777
|
+
return render_list_text(payload)
|
|
778
|
+
if command == "explain":
|
|
779
|
+
return render_explain_text(payload)
|
|
780
|
+
if command == "indicator":
|
|
781
|
+
return render_indicator_text(payload)
|
|
782
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def cite_endpoint(command: str) -> str:
|
|
786
|
+
return {
|
|
787
|
+
"search": SEARCH_URL,
|
|
788
|
+
"meta": META_URL,
|
|
789
|
+
"data": DATA_URL,
|
|
790
|
+
"bigdata": BIGDATA_URL,
|
|
791
|
+
"list": LIST_URL,
|
|
792
|
+
"explain": EXPLAIN_URL,
|
|
793
|
+
"indicator": INDICATOR_URL,
|
|
794
|
+
}[command]
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def should_use_proxy(args: argparse.Namespace) -> bool:
|
|
798
|
+
return args.command in {"search", "meta", "data", "list", "explain", "indicator"} and not args.direct
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def proxy_endpoint(command: str, base_url: str) -> str:
|
|
802
|
+
path = {
|
|
803
|
+
"search": "/v1/kosis/search",
|
|
804
|
+
"meta": "/v1/kosis/meta",
|
|
805
|
+
"data": "/v1/kosis/data",
|
|
806
|
+
"list": "/v1/kosis/list",
|
|
807
|
+
"explain": "/v1/kosis/explain",
|
|
808
|
+
"indicator": "/v1/kosis/indicator",
|
|
809
|
+
}[command]
|
|
810
|
+
return f"{base_url.rstrip('/')}{path}"
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def params_without_api_key(params: dict[str, str]) -> dict[str, str]:
|
|
814
|
+
return {key: value for key, value in params.items() if key != "apiKey"}
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def run(args: argparse.Namespace) -> int:
|
|
818
|
+
use_json = args.json or not args.text
|
|
819
|
+
|
|
820
|
+
use_proxy = should_use_proxy(args)
|
|
821
|
+
|
|
822
|
+
if use_proxy or args.dry_run:
|
|
823
|
+
api_key = "<PROXY>" if use_proxy else "<DRY-RUN>"
|
|
824
|
+
else:
|
|
825
|
+
api_key = resolve_api_key()
|
|
826
|
+
|
|
827
|
+
builder = {
|
|
828
|
+
"search": build_search_params,
|
|
829
|
+
"meta": build_meta_params,
|
|
830
|
+
"data": build_data_params,
|
|
831
|
+
"bigdata": build_bigdata_params,
|
|
832
|
+
"list": build_list_params,
|
|
833
|
+
"explain": build_explain_params,
|
|
834
|
+
"indicator": build_indicator_params,
|
|
835
|
+
}[args.command]
|
|
836
|
+
base = cite_endpoint(args.command)
|
|
837
|
+
params = builder(api_key, args)
|
|
838
|
+
if use_proxy:
|
|
839
|
+
call_base = proxy_endpoint(args.command, resolve_proxy_base_url(args.proxy_base_url))
|
|
840
|
+
call_params = params_without_api_key(params)
|
|
841
|
+
else:
|
|
842
|
+
call_base = base
|
|
843
|
+
call_params = params
|
|
844
|
+
url = build_url(call_base, call_params)
|
|
845
|
+
|
|
846
|
+
if args.dry_run:
|
|
847
|
+
redacted = dict(call_params)
|
|
848
|
+
if not use_proxy and "apiKey" in redacted:
|
|
849
|
+
redacted["apiKey"] = "<DRY-RUN>"
|
|
850
|
+
if use_json:
|
|
851
|
+
print(json.dumps({
|
|
852
|
+
"endpoint": call_base,
|
|
853
|
+
"upstream_endpoint": base,
|
|
854
|
+
"via_proxy": use_proxy,
|
|
855
|
+
"params": redacted,
|
|
856
|
+
"url": build_url(call_base, redacted)
|
|
857
|
+
}, ensure_ascii=False, indent=2))
|
|
858
|
+
else:
|
|
859
|
+
print(f"endpoint: {call_base}")
|
|
860
|
+
print(f"upstream_endpoint: {base}")
|
|
861
|
+
print(f"via_proxy: {str(use_proxy).lower()}")
|
|
862
|
+
print(f"url: {build_url(call_base, redacted)}")
|
|
863
|
+
for key, value in redacted.items():
|
|
864
|
+
print(f" {key}={value}")
|
|
865
|
+
return 0
|
|
866
|
+
|
|
867
|
+
format_hint = params.get("format", "json")
|
|
868
|
+
try:
|
|
869
|
+
payload = call_kosis(url, args.timeout, format_hint=format_hint, via_proxy=use_proxy)
|
|
870
|
+
except KosisError as exc:
|
|
871
|
+
sys.stderr.write(f"{exc}\n")
|
|
872
|
+
return 2
|
|
873
|
+
|
|
874
|
+
if use_json:
|
|
875
|
+
if isinstance(payload, str):
|
|
876
|
+
sys.stdout.write(payload)
|
|
877
|
+
if not payload.endswith("\n"):
|
|
878
|
+
sys.stdout.write("\n")
|
|
879
|
+
else:
|
|
880
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
881
|
+
else:
|
|
882
|
+
print(render_text(args.command, payload))
|
|
883
|
+
print(f"\nsource: {base}")
|
|
884
|
+
if use_proxy:
|
|
885
|
+
print(f"via: {call_base}")
|
|
886
|
+
|
|
887
|
+
return 0
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def main(argv: list[str] | None = None) -> int:
|
|
891
|
+
args = parse_args(argv)
|
|
892
|
+
return run(args)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
if __name__ == "__main__":
|
|
896
|
+
sys.exit(main())
|