jibsa 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. jibsa/__init__.py +1 -0
  2. jibsa/__main__.py +5 -0
  3. jibsa/cli/__init__.py +0 -0
  4. jibsa/cli/app.py +210 -0
  5. jibsa/cli/daangn.py +1141 -0
  6. jibsa/cli/naver.py +955 -0
  7. jibsa/cli/options.py +17 -0
  8. jibsa/cli/output.py +325 -0
  9. jibsa/cli/zigbang.py +1045 -0
  10. jibsa/core/__init__.py +0 -0
  11. jibsa/core/errors.py +107 -0
  12. jibsa/core/geo.py +217 -0
  13. jibsa/core/http.py +421 -0
  14. jibsa/core/korean.py +67 -0
  15. jibsa/core/listing.py +488 -0
  16. jibsa/core/ratelimit.py +353 -0
  17. jibsa/core/schema.py +46 -0
  18. jibsa/llm.md +124 -0
  19. jibsa/providers/__init__.py +0 -0
  20. jibsa/providers/daangn/__init__.py +46 -0
  21. jibsa/providers/daangn/api/__init__.py +1 -0
  22. jibsa/providers/daangn/api/client.py +1887 -0
  23. jibsa/providers/daangn/api/parser.py +155 -0
  24. jibsa/providers/daangn/api/relay.py +106 -0
  25. jibsa/providers/daangn/api/request/__init__.py +1 -0
  26. jibsa/providers/daangn/api/request/common.py +288 -0
  27. jibsa/providers/daangn/api/request/commute.py +69 -0
  28. jibsa/providers/daangn/api/request/compare.py +44 -0
  29. jibsa/providers/daangn/api/request/complex.py +152 -0
  30. jibsa/providers/daangn/api/request/development.py +50 -0
  31. jibsa/providers/daangn/api/request/listing.py +17 -0
  32. jibsa/providers/daangn/api/request/neighborhood.py +64 -0
  33. jibsa/providers/daangn/api/request/popular.py +29 -0
  34. jibsa/providers/daangn/api/request/real_price.py +183 -0
  35. jibsa/providers/daangn/api/request/region.py +80 -0
  36. jibsa/providers/daangn/api/request/review.py +47 -0
  37. jibsa/providers/daangn/api/request/school.py +88 -0
  38. jibsa/providers/daangn/api/request/search.py +161 -0
  39. jibsa/providers/daangn/api/response/__init__.py +1 -0
  40. jibsa/providers/daangn/api/response/common.py +749 -0
  41. jibsa/providers/daangn/api/response/commute.py +157 -0
  42. jibsa/providers/daangn/api/response/compare.py +536 -0
  43. jibsa/providers/daangn/api/response/complex.py +557 -0
  44. jibsa/providers/daangn/api/response/development.py +178 -0
  45. jibsa/providers/daangn/api/response/listing.py +21 -0
  46. jibsa/providers/daangn/api/response/neighborhood.py +91 -0
  47. jibsa/providers/daangn/api/response/popular.py +38 -0
  48. jibsa/providers/daangn/api/response/real_price.py +309 -0
  49. jibsa/providers/daangn/api/response/region.py +208 -0
  50. jibsa/providers/daangn/api/response/review.py +116 -0
  51. jibsa/providers/daangn/api/response/school.py +271 -0
  52. jibsa/providers/daangn/api/response/search.py +201 -0
  53. jibsa/providers/daangn/service/__init__.py +5 -0
  54. jibsa/providers/daangn/service/common.py +904 -0
  55. jibsa/providers/daangn/service/commute.py +464 -0
  56. jibsa/providers/daangn/service/compare.py +571 -0
  57. jibsa/providers/daangn/service/complex.py +883 -0
  58. jibsa/providers/daangn/service/development.py +324 -0
  59. jibsa/providers/daangn/service/listing.py +325 -0
  60. jibsa/providers/daangn/service/neighborhood.py +206 -0
  61. jibsa/providers/daangn/service/popular.py +102 -0
  62. jibsa/providers/daangn/service/real_price.py +956 -0
  63. jibsa/providers/daangn/service/region.py +744 -0
  64. jibsa/providers/daangn/service/review.py +192 -0
  65. jibsa/providers/daangn/service/school.py +490 -0
  66. jibsa/providers/daangn/service/search.py +1025 -0
  67. jibsa/providers/naver/__init__.py +42 -0
  68. jibsa/providers/naver/api/__init__.py +1 -0
  69. jibsa/providers/naver/api/client.py +1795 -0
  70. jibsa/providers/naver/api/request/__init__.py +1 -0
  71. jibsa/providers/naver/api/request/common.py +299 -0
  72. jibsa/providers/naver/api/request/complex.py +102 -0
  73. jibsa/providers/naver/api/request/listing.py +152 -0
  74. jibsa/providers/naver/api/request/market_price.py +32 -0
  75. jibsa/providers/naver/api/request/real_price.py +73 -0
  76. jibsa/providers/naver/api/request/redevelopment.py +179 -0
  77. jibsa/providers/naver/api/request/region.py +77 -0
  78. jibsa/providers/naver/api/request/school.py +36 -0
  79. jibsa/providers/naver/api/request/search.py +195 -0
  80. jibsa/providers/naver/api/request/trend.py +59 -0
  81. jibsa/providers/naver/api/response/__init__.py +1 -0
  82. jibsa/providers/naver/api/response/common.py +945 -0
  83. jibsa/providers/naver/api/response/complex.py +232 -0
  84. jibsa/providers/naver/api/response/listing.py +542 -0
  85. jibsa/providers/naver/api/response/market_price.py +44 -0
  86. jibsa/providers/naver/api/response/real_price.py +128 -0
  87. jibsa/providers/naver/api/response/redevelopment.py +98 -0
  88. jibsa/providers/naver/api/response/region.py +251 -0
  89. jibsa/providers/naver/api/response/school.py +112 -0
  90. jibsa/providers/naver/api/response/search.py +67 -0
  91. jibsa/providers/naver/api/response/trend.py +87 -0
  92. jibsa/providers/naver/service/__init__.py +5 -0
  93. jibsa/providers/naver/service/common.py +816 -0
  94. jibsa/providers/naver/service/complex.py +662 -0
  95. jibsa/providers/naver/service/listing.py +780 -0
  96. jibsa/providers/naver/service/market_price.py +178 -0
  97. jibsa/providers/naver/service/real_price.py +330 -0
  98. jibsa/providers/naver/service/redevelopment.py +451 -0
  99. jibsa/providers/naver/service/region.py +698 -0
  100. jibsa/providers/naver/service/school.py +237 -0
  101. jibsa/providers/naver/service/search.py +782 -0
  102. jibsa/providers/naver/service/trend.py +253 -0
  103. jibsa/providers/zigbang/__init__.py +12 -0
  104. jibsa/providers/zigbang/api/__init__.py +1 -0
  105. jibsa/providers/zigbang/api/client.py +1395 -0
  106. jibsa/providers/zigbang/api/request/__init__.py +1 -0
  107. jibsa/providers/zigbang/api/request/apt.py +108 -0
  108. jibsa/providers/zigbang/api/request/common.py +266 -0
  109. jibsa/providers/zigbang/api/request/complex.py +33 -0
  110. jibsa/providers/zigbang/api/request/listing.py +123 -0
  111. jibsa/providers/zigbang/api/request/real_price.py +47 -0
  112. jibsa/providers/zigbang/api/request/region.py +51 -0
  113. jibsa/providers/zigbang/api/request/stay.py +268 -0
  114. jibsa/providers/zigbang/api/request/store.py +94 -0
  115. jibsa/providers/zigbang/api/response/__init__.py +1 -0
  116. jibsa/providers/zigbang/api/response/apt.py +95 -0
  117. jibsa/providers/zigbang/api/response/common.py +501 -0
  118. jibsa/providers/zigbang/api/response/complex.py +112 -0
  119. jibsa/providers/zigbang/api/response/listing.py +778 -0
  120. jibsa/providers/zigbang/api/response/real_price.py +86 -0
  121. jibsa/providers/zigbang/api/response/region.py +64 -0
  122. jibsa/providers/zigbang/api/response/stay.py +442 -0
  123. jibsa/providers/zigbang/api/response/store.py +219 -0
  124. jibsa/providers/zigbang/service/__init__.py +5 -0
  125. jibsa/providers/zigbang/service/apt.py +592 -0
  126. jibsa/providers/zigbang/service/common.py +623 -0
  127. jibsa/providers/zigbang/service/complex.py +598 -0
  128. jibsa/providers/zigbang/service/detail.py +77 -0
  129. jibsa/providers/zigbang/service/listing.py +838 -0
  130. jibsa/providers/zigbang/service/real_price.py +312 -0
  131. jibsa/providers/zigbang/service/region.py +401 -0
  132. jibsa/providers/zigbang/service/stay.py +996 -0
  133. jibsa/providers/zigbang/service/store.py +589 -0
  134. jibsa-0.2.0.dist-info/METADATA +252 -0
  135. jibsa-0.2.0.dist-info/RECORD +138 -0
  136. jibsa-0.2.0.dist-info/WHEEL +4 -0
  137. jibsa-0.2.0.dist-info/entry_points.txt +2 -0
  138. jibsa-0.2.0.dist-info/licenses/LICENSE +21 -0
jibsa/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """집사: 부동산 매물 조회 CLI."""
jibsa/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from jibsa.cli.app import main
2
+
3
+
4
+ if __name__ == '__main__':
5
+ main()
jibsa/cli/__init__.py ADDED
File without changes
jibsa/cli/app.py ADDED
@@ -0,0 +1,210 @@
1
+ import json
2
+ import logging
3
+ import sys
4
+ import unicodedata
5
+ from collections.abc import Sequence
6
+ from importlib.metadata import distribution
7
+ from importlib.resources import files
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ from typer._click.exceptions import ( # Typer 0.27 은 Click 을 내부에 포함한다. 비공개 경로라 Typer 업그레이드 시 확인 필요.
12
+ ClickException,
13
+ NoArgsIsHelpError,
14
+ UsageError,
15
+ )
16
+
17
+ from jibsa.cli import (
18
+ daangn,
19
+ naver,
20
+ zigbang,
21
+ )
22
+ from jibsa.cli.output import (
23
+ emit,
24
+ emit_error,
25
+ handle_errors,
26
+ split_csv,
27
+ )
28
+ from jibsa.core.errors import JibsaError
29
+ from jibsa.core.ratelimit import state_dir
30
+ from jibsa.providers.daangn.api.client import DaangnClient
31
+ from jibsa.providers.naver.api.client import NaverClient
32
+ from jibsa.providers.zigbang.api.client import ZigbangClient
33
+
34
+ from typing import Annotated
35
+
36
+
37
+ # SIGINT 로 끝난 프로세스의 관례적인 exit code (128 + 2). Typer 도 KeyboardInterrupt 를 이 값으로 바꾼다.
38
+ INTERRUPTED_EXIT_CODE: int = 130
39
+ INTERRUPTED_HINT: str = '호출한 쪽이 실행을 끊었습니다. 결과가 필요하면 같은 명령을 다시 실행하세요'
40
+ # 설치본에는 pyproject.toml 의 force-include 로 패키지 안에, 편집 가능 설치에는 저장소 docs/ 에만 있다
41
+ GUIDE_FILE: str = 'llm.md'
42
+ GUIDE_URL: str = 'https://github.com/Rukkakka/jibsa/blob/main/docs/llm.md'
43
+ # `git log --oneline` 과 같은 짧은 커밋 해시 길이
44
+ SHORT_COMMIT_LENGTH: int = 7
45
+
46
+ app = typer.Typer(
47
+ name='jibsa',
48
+ help=(
49
+ '부동산 매물 조회 CLI (직방·네이버·당근). 결과는 JSON 으로 stdout 에, 에러는 '
50
+ '{"error": {...}} JSON 으로 stderr 에 쓴다. 금액은 모두 만원 단위. '
51
+ 'LLM 에이전트는 처음 쓰기 전에 `jibsa guide` 를 읽는다.'
52
+ ),
53
+ no_args_is_help=True,
54
+ pretty_exceptions_enable=False,
55
+ )
56
+ app.add_typer(zigbang.app, name='zigbang')
57
+ app.add_typer(naver.app, name='naver')
58
+ app.add_typer(daangn.app, name='daangn')
59
+
60
+
61
+ def show_version(value: bool) -> None:
62
+ """`--version` 이 주어지면 버전 한 줄을 stdout 에 쓰고 명령을 실행하지 않고 끝낸다."""
63
+ if not value:
64
+ return
65
+ # 메타데이터가 없는 깨진 설치도 에러 JSON 으로 낸다
66
+ with handle_errors('version'):
67
+ typer.echo(version_text())
68
+ raise typer.Exit()
69
+
70
+
71
+ @app.callback()
72
+ def configure_logging(
73
+ verbose: Annotated[bool, typer.Option('--verbose', '-v', help='재시도 등 진행 로그를 stderr 에 출력')] = False,
74
+ version: Annotated[
75
+ bool,
76
+ typer.Option('--version', callback=show_version, is_eager=True, help='설치된 jibsa 버전을 출력하고 끝낸다'),
77
+ ] = False,
78
+ ) -> None:
79
+ logging.basicConfig(
80
+ level=logging.INFO if verbose else logging.ERROR,
81
+ format='%(levelname)s %(name)s: %(message)s',
82
+ )
83
+
84
+
85
+ @app.command('limits')
86
+ def limits_command(
87
+ reset: Annotated[
88
+ list[str] | None,
89
+ typer.Option('--reset', help='쿨다운·요청 기록을 지울 소스 (zigbang, naver, daangn). 반복하거나 쉼표로 구분')
90
+ ] = None,
91
+ pretty: Annotated[
92
+ bool | None,
93
+ typer.Option('--pretty/--compact', help='JSON 들여쓰기'),
94
+ ] = None,
95
+ ) -> None:
96
+ """소스별 요청 제한 상태 (쿨다운 남은 시간, 최근 1분 요청 수, 정책).
97
+
98
+ 상태는 모든 jibsa 프로세스가 공유한다 (JIBSA_STATE_DIR, 기본 ~/.cache/jibsa).
99
+ """
100
+ clients = {
101
+ 'zigbang': ZigbangClient,
102
+ 'naver': NaverClient,
103
+ 'daangn': DaangnClient,
104
+ }
105
+ sources = split_csv(reset, tuple(clients), '--reset')
106
+ # 상태 파일을 읽고 쓰므로 권한·디스크 오류도 에러 JSON 으로 낸다
107
+ with handle_errors('limits'):
108
+ for source in sources:
109
+ clients[source]().rate_limiter.reset()
110
+ emit(
111
+ {
112
+ 'state_dir': str(state_dir()),
113
+ 'sources': [client().rate_limiter.status() for client in clients.values()],
114
+ },
115
+ pretty,
116
+ )
117
+
118
+
119
+ @app.command('guide')
120
+ def guide_command() -> None:
121
+ """LLM 에이전트용 사용 가이드를 출력한다 (명령 고르기, 출력 읽기, 에러 대응).
122
+
123
+ `--help` 처럼 JSON 이 아닌 마크다운 텍스트를 stdout 에 쓴다. 네트워크 요청은 보내지 않는다.
124
+ """
125
+ # 파일이 없는 깨진 설치도 에러 JSON 으로 낸다
126
+ with handle_errors('guide'):
127
+ typer.echo(read_guide(), nl=False)
128
+
129
+
130
+ def read_guide() -> str:
131
+ """패키지에 들어 있는 가이드를, 없으면(편집 가능 설치) 저장소의 `docs/llm.md` 를 읽는다.
132
+
133
+ Raises:
134
+ JibsaError: 둘 다 없을 때 (`INTERNAL_ERROR`, 설치가 깨짐).
135
+ """
136
+ packaged = files('jibsa').joinpath(GUIDE_FILE)
137
+ if packaged.is_file():
138
+ return packaged.read_text(encoding='utf-8')
139
+ # src/jibsa/cli/app.py → 저장소 루트
140
+ in_repo = Path(__file__).resolve().parents[3] / 'docs' / GUIDE_FILE
141
+ if in_repo.is_file():
142
+ return in_repo.read_text(encoding='utf-8')
143
+ # 일반 내부 오류 힌트(다른 소스를 쓰라는 안내)는 여기에 맞지 않는다
144
+ raise JibsaError(
145
+ code='INTERNAL_ERROR',
146
+ message=f'설치된 jibsa 에 LLM 가이드 파일({GUIDE_FILE})이 없습니다',
147
+ hint=f'README 의 설치 방법대로 jibsa 를 다시 설치하세요. 같은 내용은 {GUIDE_URL} 에서도 볼 수 있습니다',
148
+ )
149
+
150
+
151
+ def version_text() -> str:
152
+ """`jibsa --version` 이 쓰는 한 줄 (`jibsa 0.2.0 (커밋 a1b2c3d)`).
153
+
154
+ 버전은 pyproject.toml 에만 적고 설치 메타데이터에서 읽는다. `main` 을 git 주소로 설치하면 릴리스 사이의 커밋도
155
+ 버전이 같으므로, pip·uv 가 남긴 `direct_url.json`(PEP 610)의 커밋을 붙인다. PyPI 설치는 버전만 쓴다. 편집 가능
156
+ 설치는 저장소 코드를 그대로 따라가 버전만으로는 어느 코드인지 알 수 없어 `(개발 설치)` 를 붙인다.
157
+ """
158
+ dist = distribution('jibsa')
159
+ text = f'jibsa {dist.version}'
160
+ origin = json.loads(dist.read_text('direct_url.json') or '{}')
161
+ commit = origin.get('vcs_info', {}).get('commit_id')
162
+ if commit:
163
+ return f'{text} (커밋 {commit[:SHORT_COMMIT_LENGTH]})'
164
+ if origin.get('dir_info', {}).get('editable'):
165
+ return f'{text} (개발 설치)'
166
+ return text
167
+
168
+
169
+ def run(argv: Sequence[str] | None = None) -> int:
170
+ """CLI 를 실행하고 exit code 를 돌려준다.
171
+
172
+ Typer 를 standalone 모드 없이 돌려 사용법 오류도 `{"error": {...}}` JSON 으로
173
+ stderr 에 쓴다 (exit 2). 그래서 stdout 은 결과 JSON, stderr 는 에러 JSON 이라는
174
+ 규칙에 예외가 없다. 인자 없이 실행하면 `--help` 처럼 도움말을 stdout 에 쓴다.
175
+ Ctrl+C 로 끊기면 INTERRUPTED 에러 JSON 을 쓰고 130 으로 끝난다.
176
+ """
177
+ # macOS 에서 복사한 한글은 자모가 나뉜 NFD 로 올 수 있다. 직방·당근 검색은 NFD 를 다른 글자로 봐 0건을 준다
178
+ # (2026-10-11 실측: NFD '역삼동' 이 직방·당근 REGION_NOT_FOUND, 네이버만 정상). 화면에는 같아 보여 원인을 알 수 없다
179
+ args = [unicodedata.normalize('NFC', arg) for arg in (sys.argv[1:] if argv is None else argv)]
180
+ try:
181
+ result = app(args=args, prog_name='jibsa', standalone_mode=False)
182
+ except NoArgsIsHelpError as exc:
183
+ typer.echo(exc.format_message())
184
+ return 0
185
+ except UsageError as exc:
186
+ command = exc.ctx.command_path if exc.ctx is not None else 'jibsa'
187
+ emit_error(
188
+ 'INVALID_ARGUMENT',
189
+ exc.format_message(),
190
+ getattr(exc, 'hint', None) or f"'{command} --help' 로 사용법을 확인하세요",
191
+ command=command,
192
+ )
193
+ return exc.exit_code
194
+ except ClickException as exc:
195
+ emit_error('CLI_ERROR', exc.format_message())
196
+ return exc.exit_code
197
+ except typer.Abort:
198
+ # 프롬프트 입력 중 EOF. jibsa 에는 프롬프트가 없지만 중단은 모두 같은 코드로 알린다
199
+ emit_error('INTERRUPTED', '중단되었습니다', INTERRUPTED_HINT)
200
+ return 1
201
+ if result == INTERRUPTED_EXIT_CODE:
202
+ # Typer 는 KeyboardInterrupt 를 아무것도 쓰지 않고 Exit(130) 으로 바꿔 돌려준다.
203
+ # handle_errors 도 BaseException 인 KeyboardInterrupt 는 잡지 않으므로 여기서 에러 JSON 을 쓴다
204
+ emit_error('INTERRUPTED', '인터럽트(Ctrl+C, SIGINT)로 중단되었습니다', INTERRUPTED_HINT)
205
+ return result
206
+ return result if isinstance(result, int) else 0
207
+
208
+
209
+ def main() -> None:
210
+ sys.exit(run())