moleg-api 0.2.1__tar.gz → 0.2.3__tar.gz

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.
Files changed (133) hide show
  1. {moleg_api-0.2.1 → moleg_api-0.2.3}/PKG-INFO +4 -1
  2. {moleg_api-0.2.1 → moleg_api-0.2.3}/README.md +3 -0
  3. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api/__init__.py +2 -0
  4. moleg_api-0.2.3/moleg_api/_cli/__init__.py +14 -0
  5. moleg_api-0.2.3/moleg_api/_cli/catalog.py +51 -0
  6. moleg_api-0.2.3/moleg_api/_cli/constants.py +71 -0
  7. moleg_api-0.2.3/moleg_api/_cli/data.py +22 -0
  8. moleg_api-0.2.3/moleg_api/_cli/dispatch.py +114 -0
  9. moleg_api-0.2.3/moleg_api/_cli/foundation.py +30 -0
  10. moleg_api-0.2.3/moleg_api/_cli/main.py +117 -0
  11. moleg_api-0.2.3/moleg_api/_cli/parser.py +183 -0
  12. moleg_api-0.2.3/moleg_api/_cli/signal_helpers.py +129 -0
  13. moleg_api-0.2.3/moleg_api/_cli/signals.py +220 -0
  14. moleg_api-0.2.3/moleg_api/_cli/signals_meta.py +231 -0
  15. moleg_api-0.2.3/moleg_api/_laws/__init__.py +6 -0
  16. moleg_api-0.2.3/moleg_api/_laws/admin_scope.py +131 -0
  17. moleg_api-0.2.3/moleg_api/_laws/annex_tables.py +215 -0
  18. moleg_api-0.2.3/moleg_api/_laws/api.py +44 -0
  19. moleg_api-0.2.3/moleg_api/_laws/api_admin_rules.py +210 -0
  20. moleg_api-0.2.3/moleg_api/_laws/api_annex.py +168 -0
  21. moleg_api-0.2.3/moleg_api/_laws/api_article_context.py +126 -0
  22. moleg_api-0.2.3/moleg_api/_laws/api_authority_context.py +93 -0
  23. moleg_api-0.2.3/moleg_api/_laws/api_bundle.py +118 -0
  24. moleg_api-0.2.3/moleg_api/_laws/api_comparable.py +127 -0
  25. moleg_api-0.2.3/moleg_api/_laws/api_compare.py +51 -0
  26. moleg_api-0.2.3/moleg_api/_laws/api_delegated_criteria.py +76 -0
  27. moleg_api-0.2.3/moleg_api/_laws/api_followups.py +65 -0
  28. moleg_api-0.2.3/moleg_api/_laws/api_history.py +89 -0
  29. moleg_api-0.2.3/moleg_api/_laws/api_institutional.py +82 -0
  30. moleg_api-0.2.3/moleg_api/_laws/api_interpretations.py +126 -0
  31. moleg_api-0.2.3/moleg_api/_laws/api_judicial.py +214 -0
  32. moleg_api-0.2.3/moleg_api/_laws/api_law_loaders.py +134 -0
  33. moleg_api-0.2.3/moleg_api/_laws/api_query_expansion.py +157 -0
  34. moleg_api-0.2.3/moleg_api/_laws/api_search.py +98 -0
  35. moleg_api-0.2.3/moleg_api/_laws/api_structure.py +65 -0
  36. moleg_api-0.2.3/moleg_api/_laws/api_versions.py +144 -0
  37. moleg_api-0.2.3/moleg_api/_laws/article_gaps.py +244 -0
  38. moleg_api-0.2.3/moleg_api/_laws/authority_article_gaps.py +123 -0
  39. moleg_api-0.2.3/moleg_api/_laws/authority_context_details.py +154 -0
  40. moleg_api-0.2.3/moleg_api/_laws/authority_context_pipeline.py +137 -0
  41. moleg_api-0.2.3/moleg_api/_laws/authority_sources.py +170 -0
  42. moleg_api-0.2.3/moleg_api/_laws/authority_temporal_filters.py +119 -0
  43. moleg_api-0.2.3/moleg_api/_laws/authority_temporal_gaps.py +206 -0
  44. moleg_api-0.2.3/moleg_api/_laws/bridge.py +246 -0
  45. moleg_api-0.2.3/moleg_api/_laws/bundle_candidates.py +189 -0
  46. moleg_api-0.2.3/moleg_api/_laws/bundle_eager.py +129 -0
  47. moleg_api-0.2.3/moleg_api/_laws/bundle_finalize.py +87 -0
  48. moleg_api-0.2.3/moleg_api/_laws/bundle_modes.py +256 -0
  49. moleg_api-0.2.3/moleg_api/_laws/bundle_primary.py +169 -0
  50. moleg_api-0.2.3/moleg_api/_laws/bundle_state.py +59 -0
  51. moleg_api-0.2.3/moleg_api/_laws/candidates.py +264 -0
  52. moleg_api-0.2.3/moleg_api/_laws/config.py +228 -0
  53. moleg_api-0.2.3/moleg_api/_laws/context_load_gaps.py +157 -0
  54. moleg_api-0.2.3/moleg_api/_laws/delegated_criteria_details.py +125 -0
  55. moleg_api-0.2.3/moleg_api/_laws/delegated_criteria_pipeline.py +171 -0
  56. moleg_api-0.2.3/moleg_api/_laws/delegated_scope.py +178 -0
  57. moleg_api-0.2.3/moleg_api/_laws/followup_basic.py +233 -0
  58. moleg_api-0.2.3/moleg_api/_laws/followup_hits.py +134 -0
  59. moleg_api-0.2.3/moleg_api/_laws/followup_identities.py +139 -0
  60. moleg_api-0.2.3/moleg_api/_laws/followup_routing.py +210 -0
  61. moleg_api-0.2.3/moleg_api/_laws/followup_routing_authority.py +61 -0
  62. moleg_api-0.2.3/moleg_api/_laws/followup_routing_bundle.py +50 -0
  63. moleg_api-0.2.3/moleg_api/_laws/followup_searches.py +166 -0
  64. moleg_api-0.2.3/moleg_api/_laws/foundation.py +98 -0
  65. moleg_api-0.2.3/moleg_api/_laws/history_identity.py +134 -0
  66. moleg_api-0.2.3/moleg_api/_laws/identity_params.py +205 -0
  67. moleg_api-0.2.3/moleg_api/_laws/institutional_candidates.py +164 -0
  68. moleg_api-0.2.3/moleg_api/_laws/institutional_pipeline.py +265 -0
  69. moleg_api-0.2.3/moleg_api/_laws/institutional_resolution.py +64 -0
  70. moleg_api-0.2.3/moleg_api/_laws/limits_intents.py +108 -0
  71. moleg_api-0.2.3/moleg_api/_laws/ranking.py +242 -0
  72. moleg_api-0.2.3/moleg_api/_laws/requested_load_gaps.py +186 -0
  73. moleg_api-0.2.3/moleg_api/_laws/source_matching.py +228 -0
  74. moleg_api-0.2.3/moleg_api/_laws/support.py +30 -0
  75. moleg_api-0.2.3/moleg_api/_laws/temporal_gaps.py +144 -0
  76. moleg_api-0.2.3/moleg_api/_laws/validation.py +91 -0
  77. moleg_api-0.2.3/moleg_api/_models/__init__.py +15 -0
  78. moleg_api-0.2.3/moleg_api/_models/admin.py +85 -0
  79. moleg_api-0.2.3/moleg_api/_models/annex.py +68 -0
  80. moleg_api-0.2.3/moleg_api/_models/authority.py +102 -0
  81. moleg_api-0.2.3/moleg_api/_models/bundles.py +112 -0
  82. moleg_api-0.2.3/moleg_api/_models/followups.py +45 -0
  83. moleg_api-0.2.3/moleg_api/_models/laws.py +193 -0
  84. moleg_api-0.2.3/moleg_api/_models/query.py +84 -0
  85. moleg_api-0.2.3/moleg_api/_models/serialization.py +123 -0
  86. moleg_api-0.2.3/moleg_api/_models/types.py +26 -0
  87. moleg_api-0.2.3/moleg_api/_normalization/__init__.py +19 -0
  88. moleg_api-0.2.3/moleg_api/_normalization/annex.py +85 -0
  89. moleg_api-0.2.3/moleg_api/_normalization/article_units.py +187 -0
  90. moleg_api-0.2.3/moleg_api/_normalization/articles.py +137 -0
  91. moleg_api-0.2.3/moleg_api/_normalization/authority.py +212 -0
  92. moleg_api-0.2.3/moleg_api/_normalization/candidates.py +99 -0
  93. moleg_api-0.2.3/moleg_api/_normalization/delegation.py +90 -0
  94. moleg_api-0.2.3/moleg_api/_normalization/history_events.py +114 -0
  95. moleg_api-0.2.3/moleg_api/_normalization/history_html.py +170 -0
  96. moleg_api-0.2.3/moleg_api/_normalization/identities.py +94 -0
  97. moleg_api-0.2.3/moleg_api/_normalization/primitives.py +258 -0
  98. moleg_api-0.2.3/moleg_api/_normalization/references.py +122 -0
  99. moleg_api-0.2.3/moleg_api/_normalization/row_format.py +118 -0
  100. moleg_api-0.2.3/moleg_api/_normalization/source_refs.py +56 -0
  101. moleg_api-0.2.3/moleg_api/_normalization/structure.py +157 -0
  102. moleg_api-0.2.3/moleg_api/_normalization/unwrap.py +113 -0
  103. moleg_api-0.2.3/moleg_api/cli.py +5 -0
  104. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api/errors.py +22 -0
  105. moleg_api-0.2.3/moleg_api/laws.py +5 -0
  106. moleg_api-0.2.3/moleg_api/models.py +83 -0
  107. moleg_api-0.2.3/moleg_api/normalization.py +9 -0
  108. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api.egg-info/PKG-INFO +4 -1
  109. moleg_api-0.2.3/moleg_api.egg-info/SOURCES.txt +126 -0
  110. {moleg_api-0.2.1 → moleg_api-0.2.3}/pyproject.toml +1 -1
  111. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_cli.py +2 -1
  112. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_laws.py +7 -5
  113. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_live_smoke.py +47 -0
  114. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_models.py +1 -0
  115. moleg_api-0.2.3/tests/test_refactor_compat.py +68 -0
  116. moleg_api-0.2.3/tests/test_sdk_fixes_0_2_2.py +344 -0
  117. moleg_api-0.2.1/moleg_api/cli.py +0 -1054
  118. moleg_api-0.2.1/moleg_api/laws.py +0 -7353
  119. moleg_api-0.2.1/moleg_api/models.py +0 -740
  120. moleg_api-0.2.1/moleg_api/normalization.py +0 -1719
  121. moleg_api-0.2.1/moleg_api.egg-info/SOURCES.txt +0 -25
  122. {moleg_api-0.2.1 → moleg_api-0.2.3}/LICENSE +0 -0
  123. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api/__main__.py +0 -0
  124. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api/py.typed +0 -0
  125. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api/source.py +0 -0
  126. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api.egg-info/dependency_links.txt +0 -0
  127. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api.egg-info/entry_points.txt +0 -0
  128. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api.egg-info/requires.txt +0 -0
  129. {moleg_api-0.2.1 → moleg_api-0.2.3}/moleg_api.egg-info/top_level.txt +0 -0
  130. {moleg_api-0.2.1 → moleg_api-0.2.3}/setup.cfg +0 -0
  131. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_sdk_fixes_0_2_1.py +0 -0
  132. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_source.py +0 -0
  133. {moleg_api-0.2.1 → moleg_api-0.2.3}/tests/test_versions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleg-api
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Task-level Python interface for loading Korean legal sources from law.go.kr
5
5
  Author: MOLEG-API maintainers
6
6
  License-Expression: MIT
@@ -65,6 +65,8 @@ python -m moleg_api get-article --law 001248 제3조 # load the current artic
65
65
  python -m moleg_api get-article --law 001248 --as-of 2021-01-01 제3조 # the version in force then
66
66
  ```
67
67
 
68
+ The CLI exposes 27 task subcommands plus `catalog`; `catalog` is the source of truth for the command list, routing rules, and result `kind` values.
69
+
68
70
  Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (raw payloads omitted unless `include_raw=True`).
69
71
 
70
72
  ## What it covers
@@ -87,6 +89,7 @@ Full documentation is in the [`wiki/`](wiki/Home.md) folder:
87
89
  - [CLI Reference](wiki/CLI-Reference.md) · [API Reference](wiki/API-Reference.md)
88
90
  - [Historical Versions](wiki/Historical-Versions.md) · [Sources & Coverage](wiki/Sources-and-Coverage.md)
89
91
  - [Gotchas](wiki/Gotchas.md) · [Error Handling](wiki/Error-Handling.md)
92
+ - [Maintainer Notes](wiki/Maintainer-Notes.md)
90
93
 
91
94
  ## Status
92
95
 
@@ -36,6 +36,8 @@ python -m moleg_api get-article --law 001248 제3조 # load the current artic
36
36
  python -m moleg_api get-article --law 001248 --as-of 2021-01-01 제3조 # the version in force then
37
37
  ```
38
38
 
39
+ The CLI exposes 27 task subcommands plus `catalog`; `catalog` is the source of truth for the command list, routing rules, and result `kind` values.
40
+
39
41
  Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (raw payloads omitted unless `include_raw=True`).
40
42
 
41
43
  ## What it covers
@@ -58,6 +60,7 @@ Full documentation is in the [`wiki/`](wiki/Home.md) folder:
58
60
  - [CLI Reference](wiki/CLI-Reference.md) · [API Reference](wiki/API-Reference.md)
59
61
  - [Historical Versions](wiki/Historical-Versions.md) · [Sources & Coverage](wiki/Sources-and-Coverage.md)
60
62
  - [Gotchas](wiki/Gotchas.md) · [Error Handling](wiki/Error-Handling.md)
63
+ - [Maintainer Notes](wiki/Maintainer-Notes.md)
61
64
 
62
65
  ## Status
63
66
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .errors import (
4
4
  AmbiguousLawError,
5
+ AsOfBeforeCoverageError,
5
6
  MolegApiError,
6
7
  NoResultError,
7
8
  ParseFailureError,
@@ -124,6 +125,7 @@ __all__ = [
124
125
  "MolegApiError",
125
126
  "NoResultError",
126
127
  "ParseFailureError",
128
+ "AsOfBeforeCoverageError",
127
129
  "RateLimitError",
128
130
  "RetryExhaustedError",
129
131
  "SourceApiError",
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+ from .constants import *
5
+ from .data import *
6
+ from .signals_meta import *
7
+ from .signal_helpers import *
8
+ from .signals import *
9
+ from .parser import *
10
+ from .dispatch import *
11
+ from .catalog import *
12
+ from .main import *
13
+
14
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+
5
+ CATALOG = {
6
+ "convention": [
7
+ "kind 접미사가 후보≠본문을 구조로 표시: *_hit/*_candidate/*_planning = 검색 후보(인용 불가), *_text/*_context/*_identity = 로드된 본문.",
8
+ "인용은 항상 로드된 본문에서만 — search/expand/find-comparable 결과는 '다음에 무엇을 로드할지의 메뉴'일 뿐.",
9
+ "출처 권위 보존: 법제처 법령 > 법제처 해석 > 부처 해석 ≠ 판례 ≠ 헌재. flags.source_type/라벨을 답에 반영.",
10
+ "0건·호출 실패 ≠ 부재. 종료코드로 구분: 0 ok(0건 포함) · 2 모호 · 3 소스접근실패 · 4 no-result · 5 usage/순서위반.",
11
+ "load 계열에 법령명을 주면 needs_search_first(exit 5) — 먼저 search-laws로 law_id를 얻어라.",
12
+ "deferred는 data.deferred[i]를 load-followup --json -로 파이프(손타이핑 금지).",
13
+ "로더(get-law/get-article)는 기본이 현행 통합본, --as-of <날짜>(YYYY-MM-DD/YYYYMMDD만)면 그 시점 시행 버전을 로드한다. 반환 effective_date를 오늘과 대조해 현행/미시행을 판정하라 — flags.not_effective_as_of=공포됐으나 미시행(미래 시행일, 현행 인용 금지), version_mismatch={requested,loaded}=요청 시점과 반환 버전 시행일 불일치, version_request_unfulfilled=요청보다 이후 버전이 반환됨. 요청 시점이 통합본 커버리지보다 이르면 kind:version_request_unfulfilled+earliest_available(→trace-law-history).",
14
+ "검색 스코프 뉘앙스: 헌재·판례는 기본이 제목(사건명) 검색이라 doctrine·본문 키워드는 --search-body(본문 전체)라야 매칭(0건이면 next가 재시도 제시). 별표는 --search-scope title(별표 제목)/source(소관 법령명, 토큰 근접)이라 0건이면 반대 스코프로. 부처 해석은 --source all_ministries(법제처+부처 혼합)로만 나오고, 부처 본문 로드는 --source ministry --ministry <기관> 필수(검색 hit의 follow_up.filters를 그대로 로더 인자로).",
15
+ "개정 전후 델타=compare-law-versions는 소스가 주는 최근 두 버전만 비교하고 changes[].article은 본문에서 파싱한 실제 조문번호(매핑 불가 시 null). 임의 두 시점 델타는 get-article --as-of를 전후 날짜로 두 번 로드해 대조.",
16
+ ],
17
+ "routing_rules": [
18
+ "본문 로드: 살아있는 조문=get-article / 이동·삭제 가능성 있는 조문=load-article-context(기본이 이동추적).",
19
+ "개정: 무엇이 바뀌었나(전후 문구 델타)=compare-law-versions / 어떤 개정들이 있었나(연혁)=trace-law-history.",
20
+ "이 법 아래 무엇이 있나: 계층 조망=get-law-structure(위임 증명 아님) / 조문 단위 위임 규정=find-delegated-rules.",
21
+ "넓은 탐색: 넓은 질의의 용어·관련법 조사계획=expand-legal-query / 유사 제도(비슷한 기제)를 가진 법 후보(설계용)=find-comparable-mechanisms.",
22
+ "묶음 로더 — authority=특정 조문의 해석/판례/헌재 권위 / bundle=진입점 모를 때 단일법·넓은 질문(--mode) / institutional=명시된 다법령 집합(--statute 반복) / delegated=단일법의 하위규칙·별표 집행기준 본문.",
23
+ "별표 금액·기준표: 위임된 시행령·시행규칙의 별표를 search-annex-forms --search-scope source <법령명> → get-annex-form-body --id(=--annex-id)로 로드. 표 파싱이 무너지면 structured_data.parsing_confidence=low — 금액은 text를 1차로 인용하라. bare id 로드는 소관법령ID·pdf_link 등 링크 메타를 복구하지 못하니 현행성은 모법 버전으로 확인.",
24
+ ],
25
+ "commands": {
26
+ "검색·계획(후보)": [
27
+ "search-laws", "resolve-promulgated-law", "search-administrative-rules", "search-annex-forms",
28
+ "search-interpretations", "search-cases", "search-constitutional-decisions",
29
+ "expand-legal-query", "find-comparable-mechanisms",
30
+ ],
31
+ "본문 로드": [
32
+ "get-law", "get-article", "load-article-context", "get-administrative-rule",
33
+ "load-administrative-rule-context", "get-annex-form-body", "get-interpretation",
34
+ "get-case", "get-constitutional-decision",
35
+ ],
36
+ "연혁·체계·위임": ["trace-law-history", "compare-law-versions", "find-delegated-rules", "get-law-structure"],
37
+ "권위·묶음": ["load-authority-context", "load-legal-context-bundle", "load-institutional-system", "load-delegated-criteria", "load-followup"],
38
+ },
39
+ "kinds": [
40
+ "law_hit_list", "admin_rule_hit_list", "annex_form_hit_list", "interpretation_hit_list",
41
+ "case_hit_list", "constitutional_hit_list", "comparable_planning_list", "query_expansion_planning",
42
+ "law_text", "article_text", "article_context", "admin_rule_text", "admin_rule_context",
43
+ "annex_form_text", "interpretation_text", "case_text", "constitutional_text", "law_identity",
44
+ "law_history", "law_diff", "delegation_graph", "law_structure_hierarchy_only",
45
+ "legal_context_bundle", "authority_context",
46
+ "ambiguous", "source_access_error", "parse_error", "no_result",
47
+ "needs_search_first", "usage_error", "unsupported", "error",
48
+ ],
49
+ }
50
+
51
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+
5
+ EXIT_OK = 0 # includes a zero-hit search (ok:true, count:0)
6
+ EXIT_AMBIGUOUS = 2 # multiple plausible identities — surface, don't pick
7
+ EXIT_SOURCE = 3 # transient source access failure (rate limit / retry / source)
8
+ EXIT_NO_RESULT = 4 # a load found no source text for a valid identifier
9
+ EXIT_USAGE = 5 # bad arguments, or a loader was handed a law name (search first)
10
+
11
+ # Public MolegApi methods reachable through load-followup rehydration. Guards the
12
+ # candidate->body discipline: an arbitrary interface string cannot smuggle a
13
+ # candidate in as if it were loaded text.
14
+ FOLLOWUP_INTERFACES = frozenset(
15
+ {
16
+ "expand_legal_query",
17
+ "find_comparable_mechanisms",
18
+ "resolve_promulgated_law",
19
+ "search_laws",
20
+ "get_law",
21
+ "get_article",
22
+ "load_article_context",
23
+ "search_administrative_rules",
24
+ "get_administrative_rule",
25
+ "load_administrative_rule_context",
26
+ "search_annex_forms",
27
+ "get_annex_form_body",
28
+ "search_interpretations",
29
+ "get_interpretation",
30
+ "search_cases",
31
+ "get_case",
32
+ "search_constitutional_decisions",
33
+ "get_constitutional_decision",
34
+ "load_authority_context",
35
+ "find_delegated_rules",
36
+ "get_law_structure",
37
+ "trace_law_history",
38
+ "compare_law_versions",
39
+ "load_legal_context_bundle",
40
+ "load_institutional_system",
41
+ "load_delegated_criteria",
42
+ }
43
+ )
44
+ # Handoffs that are valid follow-up interfaces but belong to other sources.
45
+ FOLLOWUP_HANDOFFS = frozenset({"websearch", "congress-db"})
46
+
47
+ # Searches return a list; a zero-hit search is a scoped ok:true result (count 0),
48
+ # never an error — even when the SDK signals "nothing found" by raising.
49
+ SEARCH_COMMANDS = frozenset(
50
+ {
51
+ "search-laws",
52
+ "search-administrative-rules",
53
+ "search-annex-forms",
54
+ "search-interpretations",
55
+ "search-cases",
56
+ "search-constitutional-decisions",
57
+ }
58
+ )
59
+
60
+
61
+ class CliError(Exception):
62
+ """A CLI-level failure carrying the envelope kind and exit code to emit."""
63
+
64
+ def __init__(self, message: str, *, kind: str, exit_code: int, extra: dict[str, Any] | None = None):
65
+ super().__init__(message)
66
+ self.message = message
67
+ self.kind = kind
68
+ self.exit_code = exit_code
69
+ self.extra = extra or {}
70
+
71
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+
5
+ def _to_data(result: Any, *, include_raw: bool) -> Any:
6
+ if isinstance(result, list):
7
+ return [_to_data(item, include_raw=include_raw) for item in result]
8
+ if hasattr(result, "to_dict"):
9
+ return result.to_dict(include_raw=include_raw)
10
+ return result
11
+
12
+
13
+ def _statute_args(values: list[str]) -> list[str]:
14
+ if not values:
15
+ raise CliError(
16
+ "load-institutional-system needs at least one --statute law_id",
17
+ kind="usage",
18
+ exit_code=EXIT_USAGE,
19
+ )
20
+ return values
21
+
22
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+ from .constants import CliError, EXIT_USAGE, FOLLOWUP_HANDOFFS, FOLLOWUP_INTERFACES
5
+ from .signals_meta import parse_as_of
6
+
7
+ def _call(api: MolegApi, args: argparse.Namespace) -> Any:
8
+ c = args.command
9
+ # Strictly validate --as-of once, for every command that carries it, so a
10
+ # malformed date is a usage error rather than a silent wrong-version load.
11
+ if getattr(args, "as_of", None):
12
+ args.as_of = parse_as_of(args.as_of)
13
+ if c == "search-laws":
14
+ return api.search_laws(args.query, as_of=args.as_of, basis=args.basis,
15
+ law_type=args.law_type, ministry=args.ministry, display=args.display)
16
+ if c == "resolve-promulgated-law":
17
+ return api.resolve_promulgated_law(prom_law_nm=args.prom_law_nm, prom_no=args.prom_no, promulgation_dt=args.promulgation_dt)
18
+ if c == "search-administrative-rules":
19
+ return api.search_administrative_rules(args.query, ministry=args.ministry, rule_type=args.rule_type,
20
+ issued_on=args.issued_on, include_history=args.include_history, display=args.display)
21
+ if c == "search-annex-forms":
22
+ return api.search_annex_forms(args.query, source=args.source, search_scope=args.search_scope,
23
+ annex_type=args.annex_type, ministry=args.ministry, display=args.display)
24
+ if c == "search-interpretations":
25
+ return api.search_interpretations(args.query, source=args.source, ministry=args.ministry,
26
+ search_body=args.search_body, interpreted_on=args.interpreted_on, display=args.display)
27
+ if c == "search-cases":
28
+ return api.search_cases(args.query, court=args.court, court_name=args.court_name, search_body=args.search_body,
29
+ decided_on=args.decided_on, case_number=args.case_number, display=args.display)
30
+ if c == "search-constitutional-decisions":
31
+ return api.search_constitutional_decisions(args.query, search_body=args.search_body,
32
+ decided_on=args.decided_on, case_number=args.case_number, display=args.display)
33
+ if c == "expand-legal-query":
34
+ return api.expand_legal_query(args.query, display=args.display, include_websearch_hint=not args.no_websearch_hint)
35
+ if c == "find-comparable-mechanisms":
36
+ return api.find_comparable_mechanisms(args.concept, display=args.display)
37
+ if c == "get-law":
38
+ return api.get_law(args.law, as_of=args.as_of, basis=args.basis,
39
+ articles=args.article or None, include_metadata=not args.no_metadata)
40
+ if c == "get-article":
41
+ return api.get_article(args.law, args.article, as_of=args.as_of, basis=args.basis)
42
+ if c == "load-article-context":
43
+ return api.load_article_context(args.law, args.article,
44
+ as_of=args.as_of, basis=args.basis, follow_moved=not args.no_follow_moved)
45
+ if c == "get-administrative-rule":
46
+ return api.get_administrative_rule(args.identifier, articles=args.article or None, include_metadata=not args.no_metadata)
47
+ if c == "load-administrative-rule-context":
48
+ return api.load_administrative_rule_context(args.identifier, articles=args.article or None,
49
+ include_metadata=not args.no_metadata, follow_moved=not args.no_follow_moved)
50
+ if c == "get-annex-form-body":
51
+ return api.get_annex_form_body(args.identifier, source=args.source, title=args.title,
52
+ include_metadata=not args.no_metadata, attempt_structuring=not args.no_structuring)
53
+ if c == "get-interpretation":
54
+ return api.get_interpretation(args.identifier, source=args.source, ministry=args.ministry, include_metadata=not args.no_metadata)
55
+ if c == "get-case":
56
+ return api.get_case(args.identifier, include_metadata=not args.no_metadata)
57
+ if c == "get-constitutional-decision":
58
+ return api.get_constitutional_decision(args.identifier, include_metadata=not args.no_metadata)
59
+ if c == "trace-law-history":
60
+ date_range = (args.date_from, args.date_to) if (args.date_from and args.date_to) else None
61
+ return api.trace_law_history(args.law, date_range=date_range, article=args.article)
62
+ if c == "compare-law-versions":
63
+ return api.compare_law_versions(args.law, article=args.article)
64
+ if c == "find-delegated-rules":
65
+ return api.find_delegated_rules(args.law, article=args.article)
66
+ if c == "get-law-structure":
67
+ return api.get_law_structure(args.law, depth=args.depth)
68
+ if c == "load-authority-context":
69
+ return api.load_authority_context(args.law, articles=args.article,
70
+ query=args.query, budget=args.budget, as_of=args.as_of)
71
+ if c == "load-legal-context-bundle":
72
+ bridge = _bridge(args)
73
+ law = args.law or None
74
+ return api.load_legal_context_bundle(args.query, promulgation_bridge=bridge, law_identifier=law,
75
+ articles=args.article or None, mode=args.mode, budget=args.budget, as_of=args.as_of)
76
+ if c == "load-institutional-system":
77
+ return api.load_institutional_system(_statute_args(args.statute), articles=args.article or None, budget=args.budget, as_of=args.as_of)
78
+ if c == "load-delegated-criteria":
79
+ return api.load_delegated_criteria(args.law, articles=args.article or None,
80
+ query=args.query, budget=args.budget, as_of=args.as_of)
81
+ if c == "load-followup":
82
+ return api.load_followup(_read_followup(args.json_arg))
83
+ raise CliError(f"unknown command: {c}", kind="usage", exit_code=EXIT_USAGE)
84
+
85
+
86
+ def _bridge(args: argparse.Namespace) -> dict[str, Any] | None:
87
+ keys = {"prom_law_nm": args.prom_law_nm, "prom_no": args.prom_no, "promulgation_dt": args.promulgation_dt}
88
+ keys = {k: v for k, v in keys.items() if v}
89
+ return keys or None
90
+
91
+
92
+ def _read_followup(json_arg: str) -> DeferredLookup:
93
+ text = sys.stdin.read() if json_arg == "-" else json_arg
94
+ try:
95
+ obj = json.loads(text)
96
+ except json.JSONDecodeError as exc:
97
+ raise CliError(f"--json is not valid JSON: {exc}", kind="usage", exit_code=EXIT_USAGE) from exc
98
+ if isinstance(obj, list):
99
+ raise CliError("--json expects one deferred object (data.deferred[i]), not a list", kind="usage", exit_code=EXIT_USAGE)
100
+ interface = obj.get("interface")
101
+ if interface not in FOLLOWUP_INTERFACES and interface not in FOLLOWUP_HANDOFFS and not any(str(interface).startswith(h) for h in FOLLOWUP_HANDOFFS):
102
+ raise CliError(
103
+ f"unknown follow-up interface {interface!r} — pass a deferred object from a prior bundle/expand response, not a hand-written one",
104
+ kind="usage", exit_code=EXIT_USAGE,
105
+ )
106
+ return DeferredLookup(
107
+ interface=str(interface),
108
+ query=str(obj.get("query", "")),
109
+ reason=str(obj.get("reason", "")),
110
+ source_type=obj.get("source_type"),
111
+ filters=obj.get("filters") or {},
112
+ )
113
+
114
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from datetime import date, datetime
7
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
8
+ from typing import Any
9
+
10
+ from ..errors import (
11
+ AmbiguousLawError,
12
+ AsOfBeforeCoverageError,
13
+ MolegApiError,
14
+ NoResultError,
15
+ ParseFailureError,
16
+ RateLimitError,
17
+ RetryExhaustedError,
18
+ UnsupportedFormatError,
19
+ )
20
+ from ..laws import MolegApi
21
+ from ..models import DeferredLookup
22
+
23
+
24
+ def _pkg_version() -> str:
25
+ try:
26
+ return _dist_version("moleg-api")
27
+ except PackageNotFoundError:
28
+ return "unknown"
29
+
30
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+ from .catalog import CATALOG
5
+ from .constants import (
6
+ CliError,
7
+ EXIT_AMBIGUOUS,
8
+ EXIT_NO_RESULT,
9
+ EXIT_OK,
10
+ EXIT_SOURCE,
11
+ EXIT_USAGE,
12
+ SEARCH_COMMANDS,
13
+ )
14
+ from .data import _to_data
15
+ from .dispatch import _call
16
+ from .parser import build_parser
17
+ from .signals import signals_for
18
+
19
+ def _emit(payload: dict[str, Any]) -> None:
20
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
21
+
22
+
23
+ def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
24
+ parser = build_parser()
25
+ try:
26
+ args = parser.parse_args(argv)
27
+ except SystemExit as exc:
28
+ # argparse exits 0 after printing --help; anything else is a usage error.
29
+ # Remap to EXIT_USAGE so it never collides with EXIT_AMBIGUOUS (2).
30
+ if exc.code in (0, None):
31
+ return EXIT_OK
32
+ _emit({"ok": False, "command": None, "kind": "usage_error",
33
+ "error": "잘못된 인자 — `moleg <command> --help` 또는 `moleg catalog` 참고."})
34
+ return EXIT_USAGE
35
+
36
+ if not args.command:
37
+ _emit({"ok": False, "command": None, "kind": "usage_error",
38
+ "error": "서브커맨드가 필요합니다 — `moleg catalog`로 목록을 보라."})
39
+ return EXIT_USAGE
40
+ if args.command == "catalog":
41
+ _emit({"ok": True, "command": "catalog", "kind": "catalog", "source": "moleg CLI", "data": CATALOG})
42
+ return EXIT_OK
43
+
44
+ client = api or MolegApi()
45
+ try:
46
+ result = _call(client, args)
47
+ except CliError as exc:
48
+ _emit({"ok": False, "command": args.command, "kind": exc.kind, "error": exc.message, **exc.extra})
49
+ return exc.exit_code
50
+ except AmbiguousLawError as exc:
51
+ candidates = _to_data(getattr(exc, "candidates", []), include_raw=False)
52
+ _emit({"ok": False, "command": args.command, "kind": "ambiguous", "error": str(exc),
53
+ "flags": {"candidates": candidates},
54
+ "discipline": ["모호성이지 첫 후보를 고를 허가가 아님 — 후보를 사용자에게 제시."]})
55
+ return EXIT_AMBIGUOUS
56
+ except (RateLimitError, RetryExhaustedError) as exc:
57
+ _emit({"ok": False, "command": args.command, "kind": "source_access_error", "error": str(exc),
58
+ "discipline": ["일시적 소스 접근 실패이지 법령 부재 아님 — 잠시 후 재시도. 시행일 등은 폴백 규율로 채우되 위헌·판례류는 '1차 확인 필요'로 남겨라."]})
59
+ return EXIT_SOURCE
60
+ except AsOfBeforeCoverageError as exc:
61
+ law_id = getattr(exc, "law_id", None) or getattr(args, "law", "")
62
+ _emit({"ok": False, "command": args.command, "kind": "version_request_unfulfilled", "error": str(exc),
63
+ "flags": {"as_of": getattr(args, "as_of", None), "earliest_available": getattr(exc, "earliest_available", None)},
64
+ "discipline": ["요청 시점이 이 법령의 통합본 제공 최초 시행일보다 앞선다(일시적 실패 아님) — 그 이전 본문은 통합본으로 제공되지 않으니 연혁으로 확인하라."],
65
+ "next": [{"why": "가용 최초 버전·개정 연혁 확인", "cmd": f"moleg trace-law-history --law {law_id}"}]})
66
+ return EXIT_NO_RESULT
67
+ except ParseFailureError as exc:
68
+ _emit({"ok": False, "command": args.command, "kind": "parse_error", "error": str(exc),
69
+ "discipline": ["소스 응답을 정규화하지 못함(소스접근 실패·법령 부재 아님) — 다른 경로/커맨드로 확인하라."]})
70
+ return EXIT_SOURCE
71
+ except NoResultError as exc:
72
+ msg = str(exc)
73
+ if args.command in SEARCH_COMMANDS:
74
+ # A search that finds nothing is a scoped ok:true result, not an error.
75
+ result = []
76
+ elif "law name" in msg or "search_laws" in msg:
77
+ search_cmd = None
78
+ law = getattr(args, "law", None)
79
+ if law:
80
+ search_cmd = [{"why": "먼저 신원 검색", "cmd": f"moleg search-laws {json.dumps(law, ensure_ascii=False)}"}]
81
+ _emit({"ok": False, "command": args.command, "kind": "needs_search_first", "error": msg,
82
+ "discipline": ["로더에 법령명이 들어옴 — search-laws로 law_id를 먼저 얻어 --law에 넘겨라."],
83
+ "next": search_cmd or []})
84
+ return EXIT_USAGE
85
+ else:
86
+ _emit({"ok": False, "command": args.command, "kind": "no_result", "error": msg,
87
+ "discipline": ["이 식별자·조회로 소스 본문 없음 — 검색어·범위를 밝히고 대체 경로를 시도하기 전 부재로 단정 금지."]})
88
+ return EXIT_NO_RESULT
89
+ except UnsupportedFormatError as exc:
90
+ _emit({"ok": False, "command": args.command, "kind": "unsupported", "error": str(exc),
91
+ "discipline": ["이 소스는 이 형식/경로로 제공 안 됨 — websearch·congress 등 다른 출처로 넘겨라."]})
92
+ return EXIT_USAGE
93
+ except MolegApiError as exc:
94
+ _emit({"ok": False, "command": args.command, "kind": "error", "error": str(exc)})
95
+ return EXIT_SOURCE
96
+
97
+ include_raw = getattr(args, "raw", False)
98
+ sig = signals_for(args.command, result, args)
99
+ envelope = {
100
+ "ok": True,
101
+ "command": args.command,
102
+ "kind": sig["kind"],
103
+ "source": sig["source"],
104
+ }
105
+ if "count" in sig:
106
+ envelope["count"] = sig["count"]
107
+ envelope["data"] = _to_data(result, include_raw=include_raw)
108
+ if sig["flags"]:
109
+ envelope["flags"] = sig["flags"]
110
+ if sig["discipline"]:
111
+ envelope["discipline"] = sig["discipline"]
112
+ if sig["next"]:
113
+ envelope["next"] = sig["next"]
114
+ _emit(envelope)
115
+ return EXIT_OK
116
+
117
+ __all__ = [name for name in globals() if not name.startswith("__")]