nedviga-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nedviga/__init__.py +46 -0
- nedviga/__main__.py +6 -0
- nedviga/_wm.py +93 -0
- nedviga/adaptive_selectors.py +100 -0
- nedviga/auditlog.py +77 -0
- nedviga/auto.py +88 -0
- nedviga/bargain.py +183 -0
- nedviga/browser_native.py +242 -0
- nedviga/callsheet.py +63 -0
- nedviga/chains.py +83 -0
- nedviga/checklist.py +119 -0
- nedviga/cian_card.py +1371 -0
- nedviga/cian_live.py +812 -0
- nedviga/cli.py +3118 -0
- nedviga/compare.py +204 -0
- nedviga/data/legal_flags.json +146 -0
- nedviga/data/release_pubkey.asc +22 -0
- nedviga/deck.py +115 -0
- nedviga/dedup.py +242 -0
- nedviga/developer.py +169 -0
- nedviga/digest.py +205 -0
- nedviga/doctor.py +128 -0
- nedviga/dossier.py +303 -0
- nedviga/eco.py +81 -0
- nedviga/errors.py +22 -0
- nedviga/export_html.py +280 -0
- nedviga/export_pdf.py +96 -0
- nedviga/family_card.py +670 -0
- nedviga/feedback.py +79 -0
- nedviga/geo_routing.py +214 -0
- nedviga/graph.py +149 -0
- nedviga/legal_triage.py +105 -0
- nedviga/litprecheck.py +116 -0
- nedviga/market.py +102 -0
- nedviga/mcp_server.py +195 -0
- nedviga/metro_directory.json +9836 -0
- nedviga/metro_vestibules.py +134 -0
- nedviga/models.py +164 -0
- nedviga/mosdata.py +95 -0
- nedviga/net_guard.py +997 -0
- nedviga/noise.py +156 -0
- nedviga/ops.py +94 -0
- nedviga/outcomes.py +35 -0
- nedviga/pii.py +85 -0
- nedviga/pipeline.py +239 -0
- nedviga/poi.py +180 -0
- nedviga/price_guard.py +73 -0
- nedviga/profiles.py +131 -0
- nedviga/projects.py +112 -0
- nedviga/purge.py +116 -0
- nedviga/py.typed +0 -0
- nedviga/risk.py +127 -0
- nedviga/roles.py +124 -0
- nedviga/routing_cache.py +72 -0
- nedviga/rush.py +139 -0
- nedviga/scorefit.py +127 -0
- nedviga/sources.py +55 -0
- nedviga/sources_avito.py +138 -0
- nedviga/sources_domclick.py +122 -0
- nedviga/stale.py +83 -0
- nedviga/textsearch.py +141 -0
- nedviga/tour.py +180 -0
- nedviga/watch.py +234 -0
- nedviga/wizard.py +106 -0
- nedviga_cli-0.1.0.dist-info/METADATA +130 -0
- nedviga_cli-0.1.0.dist-info/RECORD +71 -0
- nedviga_cli-0.1.0.dist-info/WHEEL +5 -0
- nedviga_cli-0.1.0.dist-info/entry_points.txt +2 -0
- nedviga_cli-0.1.0.dist-info/licenses/LICENSE +661 -0
- nedviga_cli-0.1.0.dist-info/licenses/NOTICE +1 -0
- nedviga_cli-0.1.0.dist-info/top_level.txt +1 -0
nedviga/cli.py
ADDED
|
@@ -0,0 +1,3118 @@
|
|
|
1
|
+
"""CLI entry point for nedviga-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _package_version() -> str:
|
|
14
|
+
# v0.5.1 контроль качества: дерево репо = источник истины; метаданные установленного
|
|
15
|
+
# диста/egg-info могут устаревать между волнами (ловушка 351-прогона).
|
|
16
|
+
from . import __version__ as source_version
|
|
17
|
+
|
|
18
|
+
return source_version
|
|
19
|
+
from .cian_card import ParseError, parse_card
|
|
20
|
+
from .cian_live import ChallengeError, FetchError, get_card, search
|
|
21
|
+
from .doctor import run_doctor
|
|
22
|
+
from .geo_routing import honest_overclaim, walk_route
|
|
23
|
+
from .models import HonestWalkRoute, ListingCard
|
|
24
|
+
from .outcomes import STATUS_EXIT_CODES, USAGE_ERROR_EXIT, make_envelope
|
|
25
|
+
from .pii import sanitize_contacts
|
|
26
|
+
from .profiles import UnknownProfileError, apply_profile, available_profiles
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _NedvigaArgumentParser(argparse.ArgumentParser):
|
|
30
|
+
"""Argparse wrapper that preserves the ApiOutcome contract on usage errors."""
|
|
31
|
+
|
|
32
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
33
|
+
self.print_usage(sys.stderr)
|
|
34
|
+
print(f"nedviga-cli: error: {message}", file=sys.stderr)
|
|
35
|
+
envelope = make_envelope(
|
|
36
|
+
status="error",
|
|
37
|
+
reason="usage_error",
|
|
38
|
+
data={"message": message},
|
|
39
|
+
metadata={"engine": "skel", "cached": False, "elapsed_seconds": 0.0},
|
|
40
|
+
)
|
|
41
|
+
print(
|
|
42
|
+
json.dumps(
|
|
43
|
+
envelope.model_dump(mode="json", exclude_none=False),
|
|
44
|
+
ensure_ascii=False,
|
|
45
|
+
indent=2,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
sys.exit(USAGE_ERROR_EXIT)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _spotlight_external_fields(payload, _src="cian"):
|
|
52
|
+
"""P3-2: пометить внешний (сайтовый) текст spotlight-маркерами netguard.
|
|
53
|
+
|
|
54
|
+
Только поля, приходящие с площадки: title / description_text.
|
|
55
|
+
Идемпотентно: уже помеченное не перемечается.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
from .net_guard import is_spotlighted, spotlight
|
|
59
|
+
except Exception: # noqa: BLE001 — netguard обязателен, но не роняем вывод
|
|
60
|
+
return payload
|
|
61
|
+
if isinstance(payload, dict):
|
|
62
|
+
_src = payload.get("source") or _src
|
|
63
|
+
for key in ("title", "description_text"):
|
|
64
|
+
value = payload.get(key)
|
|
65
|
+
if isinstance(value, str) and value and not is_spotlighted(value):
|
|
66
|
+
payload[key] = spotlight(value, source=_src, data_type="listing_" + key)
|
|
67
|
+
for value in payload.values():
|
|
68
|
+
_spotlight_external_fields(value, _src)
|
|
69
|
+
elif isinstance(payload, list):
|
|
70
|
+
for item in payload:
|
|
71
|
+
_spotlight_external_fields(item, _src)
|
|
72
|
+
return payload
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _dump_envelope(envelope, args: argparse.Namespace) -> str:
|
|
76
|
+
data = envelope.model_dump(mode="json", exclude_none=False)
|
|
77
|
+
if not getattr(args, "no_spotlight", False):
|
|
78
|
+
_spotlight_external_fields(data.get("data"))
|
|
79
|
+
text = json.dumps(
|
|
80
|
+
data,
|
|
81
|
+
ensure_ascii=False,
|
|
82
|
+
indent=2,
|
|
83
|
+
)
|
|
84
|
+
return text
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_output(text: str, args: argparse.Namespace) -> None:
|
|
88
|
+
print(text)
|
|
89
|
+
if getattr(args, "json", None) and args.json != "-":
|
|
90
|
+
with open(args.json, "w", encoding="utf-8") as fh:
|
|
91
|
+
fh.write(text)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _cmd_source(args) -> int:
|
|
95
|
+
"""§13 AGPL: лицензия, репозиторий исходников, режим сборки."""
|
|
96
|
+
from ._wm import source_info
|
|
97
|
+
try:
|
|
98
|
+
from . import __version__ as _ver
|
|
99
|
+
except Exception:
|
|
100
|
+
_ver = "0"
|
|
101
|
+
info = source_info(_ver)
|
|
102
|
+
envelope = make_envelope(status="ok", source="source", data=info,
|
|
103
|
+
metadata=_privacy_metadata({"engine": "source"}, args))
|
|
104
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _privacy_metadata(metadata: dict, args: argparse.Namespace) -> dict:
|
|
109
|
+
try:
|
|
110
|
+
from ._wm import LICENSE as _lic, source_url as _surl
|
|
111
|
+
metadata.setdefault("license", _lic)
|
|
112
|
+
if _surl():
|
|
113
|
+
metadata.setdefault("source_url", _surl())
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
"""v0.4.6 F-13: явный маркер открытия контактов в envelope/metadata.
|
|
117
|
+
|
|
118
|
+
``contacts_open`` присутствует во всех выводах search/export: true только
|
|
119
|
+
при явном ``--with-contacts``, иначе false (клиентский/публичный вывод).
|
|
120
|
+
"""
|
|
121
|
+
meta = dict(metadata)
|
|
122
|
+
meta.setdefault("contacts_open", bool(getattr(args, "with_contacts", False)))
|
|
123
|
+
# v0.5.8: транспортный след (какой egress реально использован)
|
|
124
|
+
try:
|
|
125
|
+
from .net_guard import EGRESS_STATE
|
|
126
|
+
if EGRESS_STATE.get("mode"):
|
|
127
|
+
meta.setdefault("egress_mode", EGRESS_STATE["mode"])
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
from .roles import current_role
|
|
131
|
+
meta.setdefault("role", getattr(args, "role", current_role()))
|
|
132
|
+
if getattr(args, "_contacts_requested", False):
|
|
133
|
+
meta["warning"] = "contacts_masked_for_role"
|
|
134
|
+
return meta
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
138
|
+
source = getattr(args, "source", None)
|
|
139
|
+
if source == "domclick":
|
|
140
|
+
from .sources_domclick import browser_search_domclick
|
|
141
|
+
catalog = getattr(args, "catalog", None)
|
|
142
|
+
inp = getattr(args, "input", None)
|
|
143
|
+
live = bool(getattr(args, "live", False))
|
|
144
|
+
try:
|
|
145
|
+
if inp:
|
|
146
|
+
from .sources_domclick import parse_domclick_text
|
|
147
|
+
cards = parse_domclick_text(Path(inp).read_text(encoding="utf-8"))
|
|
148
|
+
elif live:
|
|
149
|
+
cards = browser_search_domclick(catalog or "", max_n=getattr(args, "max", 20) or 20)
|
|
150
|
+
else:
|
|
151
|
+
envelope = make_envelope(status="error", reason="source_disabled",
|
|
152
|
+
metadata=_privacy_metadata({"engine": "domclick"}, args))
|
|
153
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
154
|
+
return 3
|
|
155
|
+
except RuntimeError as exc:
|
|
156
|
+
reason = str(exc)
|
|
157
|
+
code = 3 if reason.startswith("source_") else 3
|
|
158
|
+
envelope = make_envelope(status="error", reason=reason.split(":")[0],
|
|
159
|
+
metadata=_privacy_metadata({"engine": "domclick"}, args))
|
|
160
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
161
|
+
return code
|
|
162
|
+
data = sanitize_contacts(cards)
|
|
163
|
+
envelope = make_envelope(status="ok", source="domclick", data=data,
|
|
164
|
+
metadata=_privacy_metadata({"engine": "domclick", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
165
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
if source == "avito":
|
|
169
|
+
from .sources_avito import browser_search, parse_livetext
|
|
170
|
+
catalog = getattr(args, "catalog", None)
|
|
171
|
+
live = bool(getattr(args, "live", False))
|
|
172
|
+
inp = getattr(args, "input", None)
|
|
173
|
+
if inp:
|
|
174
|
+
text = Path(inp).read_text(encoding="utf-8")
|
|
175
|
+
from .sources_avito import BLOCK_MARKERS as _BM
|
|
176
|
+
if any(m.lower() in text.lower() for m in _BM):
|
|
177
|
+
envelope = make_envelope(status="error", reason="source_blocked",
|
|
178
|
+
data={"message": "captcha detected"},
|
|
179
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
180
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
181
|
+
return 3
|
|
182
|
+
cards = parse_livetext(text)
|
|
183
|
+
data = sanitize_contacts(cards)
|
|
184
|
+
envelope = make_envelope(status="ok", source="avito", data=data,
|
|
185
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
186
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
187
|
+
return 0
|
|
188
|
+
if live:
|
|
189
|
+
try:
|
|
190
|
+
cards = browser_search(catalog, max_n=getattr(args, "max", 20) or 20)
|
|
191
|
+
except RuntimeError as exc:
|
|
192
|
+
envelope = make_envelope(status="error", reason=str(exc),
|
|
193
|
+
data={"message": str(exc)},
|
|
194
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
195
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
196
|
+
return 3
|
|
197
|
+
data = sanitize_contacts(cards)
|
|
198
|
+
envelope = make_envelope(status="ok", source="avito", data=data,
|
|
199
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
200
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
201
|
+
return 0
|
|
202
|
+
envelope = make_envelope(status="error", reason="source_disabled",
|
|
203
|
+
data={"message": "egress proxy required"},
|
|
204
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
205
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
206
|
+
return 3
|
|
207
|
+
if source != "cian":
|
|
208
|
+
envelope = make_envelope(
|
|
209
|
+
status="error",
|
|
210
|
+
reason="unsupported_source",
|
|
211
|
+
source=source,
|
|
212
|
+
data={"message": f"source '{source}' is not supported; use 'cian'"},
|
|
213
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
214
|
+
)
|
|
215
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
216
|
+
return 3
|
|
217
|
+
|
|
218
|
+
hydrate = bool(getattr(args, "hydrate", False))
|
|
219
|
+
# v0.4.6 (the task brief F-03): именованный профиль задаёт базовые значения,
|
|
220
|
+
# явно переданные CLI-аргументы его переопределяют. Без --profile — как раньше.
|
|
221
|
+
profile_name = getattr(args, "profile", None)
|
|
222
|
+
profile_fields: list[str] = []
|
|
223
|
+
if profile_name:
|
|
224
|
+
try:
|
|
225
|
+
profile_fields = apply_profile(args, profile_name)
|
|
226
|
+
except UnknownProfileError as exc:
|
|
227
|
+
envelope = make_envelope(
|
|
228
|
+
status="error",
|
|
229
|
+
reason="unknown_profile",
|
|
230
|
+
source="cian",
|
|
231
|
+
data={
|
|
232
|
+
"requested_profile": exc.requested,
|
|
233
|
+
"available_profiles": exc.known,
|
|
234
|
+
"message": str(exc),
|
|
235
|
+
},
|
|
236
|
+
metadata=_privacy_metadata(
|
|
237
|
+
{"engine": "native", "cached": False, "elapsed_seconds": 0.0}, args
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
241
|
+
return STATUS_EXIT_CODES["error"]
|
|
242
|
+
# v0.4.0 (ТЗ реалтора): клиентские фильтры требовали полных карточек.
|
|
243
|
+
# v0.4.3 (ТЗ the task brief §2): floor_not_first/min_house_floors/no_gas больше
|
|
244
|
+
# НЕ форсируют гидрацию — работают по сниппет-полям до запросов (экономия);
|
|
245
|
+
# forcing-фильтры (гео/строгий МКАД/апартаменты/доли/юридические/торги)
|
|
246
|
+
# остаются: их полей в сниппете нет, корректность важнее экономии.
|
|
247
|
+
detail_filters = (
|
|
248
|
+
getattr(args, "geo_districts", None) is not None
|
|
249
|
+
or getattr(args, "strict_mkad", False)
|
|
250
|
+
or getattr(args, "only_flat", False)
|
|
251
|
+
or getattr(args, "no_shares", False)
|
|
252
|
+
or getattr(args, "exclude_legal_risks", False)
|
|
253
|
+
or getattr(args, "no_auction", False)
|
|
254
|
+
or getattr(args, "only_auction", False)
|
|
255
|
+
)
|
|
256
|
+
if detail_filters:
|
|
257
|
+
hydrate = True
|
|
258
|
+
|
|
259
|
+
min_house_floors = args.min_house_floors
|
|
260
|
+
if getattr(args, "no_gas", False) and min_house_floors is None:
|
|
261
|
+
min_house_floors = 14
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
cards = search(
|
|
265
|
+
deal_type=args.deal_type,
|
|
266
|
+
rooms=args.rooms,
|
|
267
|
+
price_min=args.price_min,
|
|
268
|
+
price_max=args.price_max,
|
|
269
|
+
kitchen_min=args.kitchen_min,
|
|
270
|
+
floor_not_first=args.floor_not_first,
|
|
271
|
+
min_house_floors=min_house_floors,
|
|
272
|
+
geo_districts=args.geo_districts,
|
|
273
|
+
limit=args.limit,
|
|
274
|
+
page=getattr(args, "page", 1) or 1,
|
|
275
|
+
pages=getattr(args, "pages", None),
|
|
276
|
+
metro_walk_max=getattr(args, "metro_walk_max", None),
|
|
277
|
+
hydrate=hydrate,
|
|
278
|
+
no_auction=bool(getattr(args, "no_auction", False)),
|
|
279
|
+
only_auction=bool(getattr(args, "only_auction", False)),
|
|
280
|
+
workers=getattr(args, "workers", 4) or 4,
|
|
281
|
+
strict_mkad=bool(getattr(args, "strict_mkad", False)),
|
|
282
|
+
only_flat=bool(getattr(args, "only_flat", False)),
|
|
283
|
+
no_shares=bool(getattr(args, "no_shares", False)),
|
|
284
|
+
exclude_legal_risks=bool(getattr(args, "exclude_legal_risks", False)),
|
|
285
|
+
)
|
|
286
|
+
except ChallengeError as exc:
|
|
287
|
+
envelope = make_envelope(
|
|
288
|
+
status="degraded",
|
|
289
|
+
reason=f"challenge_detected: {exc}",
|
|
290
|
+
source="cian",
|
|
291
|
+
data=None,
|
|
292
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
293
|
+
)
|
|
294
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
295
|
+
return STATUS_EXIT_CODES["degraded"]
|
|
296
|
+
except FetchError as exc:
|
|
297
|
+
envelope = make_envelope(
|
|
298
|
+
status="error",
|
|
299
|
+
reason=f"fetch_error: {exc}",
|
|
300
|
+
source="cian",
|
|
301
|
+
data=None,
|
|
302
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
303
|
+
)
|
|
304
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
305
|
+
return STATUS_EXIT_CODES["error"]
|
|
306
|
+
|
|
307
|
+
# v0.4.6 (the task brief F-13): fail-closed маскирование телефонов по умолчанию.
|
|
308
|
+
# Сырые контакты — только при явном --with-contacts (маркер contacts_open).
|
|
309
|
+
with_contacts = bool(getattr(args, "with_contacts", False))
|
|
310
|
+
# v0.5.0: автопополнение проекта
|
|
311
|
+
proj = getattr(args, "project", None)
|
|
312
|
+
if proj:
|
|
313
|
+
try:
|
|
314
|
+
from .projects import project_add
|
|
315
|
+
for card in cards:
|
|
316
|
+
project_add(proj, card.id)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
data = [card.model_dump(mode="json", exclude_none=False) for card in cards]
|
|
320
|
+
if not with_contacts:
|
|
321
|
+
data = sanitize_contacts(data)
|
|
322
|
+
metadata: dict = {"engine": "native", "cached": False, "elapsed_seconds": 0.0}
|
|
323
|
+
if profile_name:
|
|
324
|
+
metadata["profile"] = profile_name
|
|
325
|
+
metadata["profile_fields"] = profile_fields
|
|
326
|
+
if getattr(args, "suggest_relax", False):
|
|
327
|
+
metadata["hints"] = [] if len(cards) >= 3 else [{"relax": "смягчить фильтры", "adds_n": 0}]
|
|
328
|
+
envelope = make_envelope(
|
|
329
|
+
status="ok",
|
|
330
|
+
source="cian",
|
|
331
|
+
data=data,
|
|
332
|
+
metadata=_privacy_metadata(metadata, args),
|
|
333
|
+
)
|
|
334
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
335
|
+
return 0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _embed_walk(card, args):
|
|
339
|
+
"""v0.3.2 (--with-walk, ТЗ реалтора): честный маршрут прямо в карточку.
|
|
340
|
+
|
|
341
|
+
Fail-closed: при любой ошибке карточка остаётся валидной,
|
|
342
|
+
причина — в metadata.with_walk.
|
|
343
|
+
"""
|
|
344
|
+
lat, lon = card.geo.lat, card.geo.lon
|
|
345
|
+
claimed = card.geo.cian_claimed_metro.station if card.geo.cian_claimed_metro else None
|
|
346
|
+
if lat is None or lon is None:
|
|
347
|
+
return {"status": "degraded", "reason": "NO_COORDINATES"}
|
|
348
|
+
if not claimed:
|
|
349
|
+
return {"status": "degraded", "reason": "NO_CLAIMED_METRO"}
|
|
350
|
+
try:
|
|
351
|
+
route = walk_route(lat, lon, claimed)
|
|
352
|
+
if route.get("status") != "ok":
|
|
353
|
+
return {"status": "degraded", "reason": route.get("reason") or "walk_failed"}
|
|
354
|
+
card.geo.honest_walk_route = HonestWalkRoute(
|
|
355
|
+
target_station=route["target_station"],
|
|
356
|
+
target_vestibule=route["target_vestibule"],
|
|
357
|
+
walk_distance_meters=route["walk_distance_meters"],
|
|
358
|
+
walk_duration_minutes=route["walk_duration_minutes"],
|
|
359
|
+
routing_provider=route["routing_provider"],
|
|
360
|
+
route_verified=route["route_verified"],
|
|
361
|
+
)
|
|
362
|
+
claimed_min = (
|
|
363
|
+
card.geo.cian_claimed_metro.claimed_time_minutes
|
|
364
|
+
if card.geo.cian_claimed_metro
|
|
365
|
+
else None
|
|
366
|
+
)
|
|
367
|
+
if claimed_min:
|
|
368
|
+
card.geo.honest_overclaim = honest_overclaim(
|
|
369
|
+
int(route["walk_duration_minutes"]), int(claimed_min)
|
|
370
|
+
)
|
|
371
|
+
return {"status": "ok", "provider": route["routing_provider"]}
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
return {"status": "degraded", "reason": f"walk_error: {exc}"}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _cmd_get_card(args: argparse.Namespace) -> int:
|
|
377
|
+
target = getattr(args, "target", None)
|
|
378
|
+
if not target:
|
|
379
|
+
envelope = make_envelope(
|
|
380
|
+
status="error",
|
|
381
|
+
reason="usage_error",
|
|
382
|
+
source="cian",
|
|
383
|
+
data={"message": "missing URL or listing ID"},
|
|
384
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
385
|
+
)
|
|
386
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
387
|
+
return USAGE_ERROR_EXIT
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
card = get_card(target)
|
|
391
|
+
except ChallengeError as exc:
|
|
392
|
+
envelope = make_envelope(
|
|
393
|
+
status="degraded",
|
|
394
|
+
reason=f"challenge_detected: {exc}",
|
|
395
|
+
source="cian",
|
|
396
|
+
data=None,
|
|
397
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
398
|
+
)
|
|
399
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
400
|
+
return STATUS_EXIT_CODES["degraded"]
|
|
401
|
+
except FetchError as exc:
|
|
402
|
+
envelope = make_envelope(
|
|
403
|
+
status="error",
|
|
404
|
+
reason=f"fetch_error: {exc}",
|
|
405
|
+
source="cian",
|
|
406
|
+
data=None,
|
|
407
|
+
metadata={"engine": "native", "cached": False, "elapsed_seconds": 0.0},
|
|
408
|
+
)
|
|
409
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
410
|
+
return STATUS_EXIT_CODES["error"]
|
|
411
|
+
|
|
412
|
+
walk_note = _embed_walk(card, args) if getattr(args, "with_walk", False) else None
|
|
413
|
+
|
|
414
|
+
with_contacts = bool(getattr(args, "with_contacts", False))
|
|
415
|
+
data = card.model_dump(mode="json", exclude_none=False)
|
|
416
|
+
if not with_contacts:
|
|
417
|
+
data = sanitize_contacts(data)
|
|
418
|
+
metadata = {"engine": "native", "cached": False, "elapsed_seconds": 0.0,
|
|
419
|
+
"with_walk": walk_note}
|
|
420
|
+
metadata = _privacy_metadata(metadata, args)
|
|
421
|
+
envelope = make_envelope(
|
|
422
|
+
status="ok",
|
|
423
|
+
source="cian",
|
|
424
|
+
data=data,
|
|
425
|
+
metadata=metadata,
|
|
426
|
+
)
|
|
427
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
432
|
+
"""v0.3.4 (ТЗ реалтора): экспорт карточек в автономный HTML-каталог."""
|
|
433
|
+
from .export_html import export_html
|
|
434
|
+
|
|
435
|
+
inp = getattr(args, "input", None)
|
|
436
|
+
if not inp:
|
|
437
|
+
envelope = make_envelope(
|
|
438
|
+
status="error", reason="usage_error",
|
|
439
|
+
data={"message": "missing --input (JSON file with cards)"},
|
|
440
|
+
metadata={"engine": "export", "cached": False, "elapsed_seconds": 0.0},
|
|
441
|
+
)
|
|
442
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
443
|
+
return USAGE_ERROR_EXIT
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
raw = json.loads(Path(inp).read_text(encoding="utf-8"))
|
|
447
|
+
cards = [ListingCard(**item) for item in (raw if isinstance(raw, list) else raw.get("cards", []))]
|
|
448
|
+
except Exception as exc:
|
|
449
|
+
envelope = make_envelope(
|
|
450
|
+
status="error", reason=f"invalid_input: {exc}",
|
|
451
|
+
data={"path": inp},
|
|
452
|
+
metadata={"engine": "export", "cached": False, "elapsed_seconds": 0.0},
|
|
453
|
+
)
|
|
454
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
455
|
+
return 3
|
|
456
|
+
|
|
457
|
+
fmt = getattr(args, "format", "html") or "html"
|
|
458
|
+
if fmt != "html":
|
|
459
|
+
envelope = make_envelope(
|
|
460
|
+
status="error", reason=f"unsupported_format: {fmt}",
|
|
461
|
+
data={"supported": ["html"]},
|
|
462
|
+
metadata={"engine": "export", "cached": False, "elapsed_seconds": 0.0},
|
|
463
|
+
)
|
|
464
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
465
|
+
return USAGE_ERROR_EXIT
|
|
466
|
+
|
|
467
|
+
out = getattr(args, "output", None) or "catalog.html"
|
|
468
|
+
embed = bool(getattr(args, "embed_photos", False))
|
|
469
|
+
# v0.4.6 (the task brief F-13): HTML-каталог по умолчанию без сырых телефонов;
|
|
470
|
+
# --with-contacts открывает контакты и помечает вывод в metadata.
|
|
471
|
+
with_contacts = bool(getattr(args, "with_contacts", False))
|
|
472
|
+
try:
|
|
473
|
+
path = export_html(
|
|
474
|
+
cards, Path(out), embed_photos=embed, with_contacts=with_contacts
|
|
475
|
+
)
|
|
476
|
+
except Exception as exc:
|
|
477
|
+
envelope = make_envelope(
|
|
478
|
+
status="error", reason=f"export_failed: {exc}",
|
|
479
|
+
data={"output": out},
|
|
480
|
+
metadata=_privacy_metadata(
|
|
481
|
+
{"engine": "export", "cached": False, "elapsed_seconds": 0.0}, args
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
485
|
+
return 3
|
|
486
|
+
|
|
487
|
+
envelope = make_envelope(
|
|
488
|
+
status="ok",
|
|
489
|
+
source="cian",
|
|
490
|
+
data={"output": str(path), "cards": len(cards), "embed_photos": embed},
|
|
491
|
+
metadata=_privacy_metadata(
|
|
492
|
+
{"engine": "export", "cached": False, "elapsed_seconds": 0.0}, args
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
496
|
+
return 0
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _load_card_input(target: str) -> ListingCard:
|
|
500
|
+
p = Path(target)
|
|
501
|
+
if p.is_file():
|
|
502
|
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
503
|
+
if isinstance(raw, list):
|
|
504
|
+
raw = raw[0] if raw else {}
|
|
505
|
+
if isinstance(raw, dict) and "cards" in raw and isinstance(raw["cards"], list):
|
|
506
|
+
raw = raw["cards"][0] if raw["cards"] else {}
|
|
507
|
+
if isinstance(raw, dict) and "data" in raw and isinstance(raw["data"], (list, dict)):
|
|
508
|
+
d = raw["data"]
|
|
509
|
+
if isinstance(d, list):
|
|
510
|
+
d = d[0] if d else {}
|
|
511
|
+
if isinstance(d, dict):
|
|
512
|
+
raw = d
|
|
513
|
+
return ListingCard(**raw)
|
|
514
|
+
# treat as id
|
|
515
|
+
from .cian_live import get_card as _get_card
|
|
516
|
+
|
|
517
|
+
card = _get_card(target)
|
|
518
|
+
return card
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _cmd_card(args: argparse.Namespace) -> int:
|
|
522
|
+
target = getattr(args, "target", None) or getattr(args, "path_or_id", None)
|
|
523
|
+
if not target:
|
|
524
|
+
envelope = make_envelope(
|
|
525
|
+
status="error", reason="usage_error",
|
|
526
|
+
data={"message": "missing card path or id"},
|
|
527
|
+
metadata={"engine": "card", "cached": False, "elapsed_seconds": 0.0},
|
|
528
|
+
)
|
|
529
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
530
|
+
return USAGE_ERROR_EXIT
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
card = _load_card_input(target)
|
|
534
|
+
except Exception as exc:
|
|
535
|
+
envelope = make_envelope(
|
|
536
|
+
status="error", reason="conversion_error",
|
|
537
|
+
data={"message": str(exc), "target": target},
|
|
538
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
539
|
+
)
|
|
540
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
541
|
+
return 3
|
|
542
|
+
|
|
543
|
+
fmt = getattr(args, "format", "html") or "html"
|
|
544
|
+
if fmt not in {"html", "pdf", "telegram", "all"}:
|
|
545
|
+
envelope = make_envelope(
|
|
546
|
+
status="error", reason="unsupported_format",
|
|
547
|
+
data={"supported": ["html", "pdf", "telegram", "all"]},
|
|
548
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
549
|
+
)
|
|
550
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
551
|
+
return USAGE_ERROR_EXIT
|
|
552
|
+
|
|
553
|
+
with_contacts = bool(getattr(args, "with_contacts", False))
|
|
554
|
+
out_dir = Path(getattr(args, "out", None) or getattr(args, "output", None) or ".")
|
|
555
|
+
try:
|
|
556
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
557
|
+
except Exception as exc:
|
|
558
|
+
envelope = make_envelope(
|
|
559
|
+
status="error", reason=f"output_error: {exc}",
|
|
560
|
+
data={"out": str(out_dir)},
|
|
561
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
562
|
+
)
|
|
563
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
564
|
+
return 3
|
|
565
|
+
|
|
566
|
+
stem = Path(target).stem if Path(target).is_file() else str(target)
|
|
567
|
+
stem = stem or "card"
|
|
568
|
+
|
|
569
|
+
from .family_card import build_family_card, render_html, render_telegram
|
|
570
|
+
|
|
571
|
+
fc = build_family_card(card, with_contacts=with_contacts)
|
|
572
|
+
|
|
573
|
+
files: list[str] = []
|
|
574
|
+
primary_output: Any = None
|
|
575
|
+
text_output: str | None = None
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
if fmt in {"html", "all"}:
|
|
579
|
+
html_path = out_dir / f"{stem}.html"
|
|
580
|
+
html_path.write_text(render_html(fc, with_contacts=with_contacts), encoding="utf-8")
|
|
581
|
+
files.append(str(html_path))
|
|
582
|
+
if primary_output is None and fmt == "html":
|
|
583
|
+
primary_output = str(html_path)
|
|
584
|
+
if fmt in {"telegram", "all"}:
|
|
585
|
+
tg_text = render_telegram(fc, with_contacts=with_contacts)
|
|
586
|
+
text_output = tg_text
|
|
587
|
+
tg_path = out_dir / f"{stem}.txt"
|
|
588
|
+
tg_path.write_text(tg_text, encoding="utf-8")
|
|
589
|
+
files.append(str(tg_path))
|
|
590
|
+
if primary_output is None and fmt == "telegram":
|
|
591
|
+
primary_output = str(tg_path)
|
|
592
|
+
if fmt in {"pdf", "all"}:
|
|
593
|
+
try:
|
|
594
|
+
from .export_pdf import export_pdf
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
raise RuntimeError("pdf-format-unavailable") from exc
|
|
597
|
+
pdf_path = out_dir / f"{stem}.pdf"
|
|
598
|
+
try:
|
|
599
|
+
export_pdf(fc, pdf_path, with_contacts=with_contacts)
|
|
600
|
+
except RuntimeError as exc:
|
|
601
|
+
if "pdf-format-unavailable" in str(exc):
|
|
602
|
+
envelope = make_envelope(
|
|
603
|
+
status="error", reason="pdf-format-unavailable",
|
|
604
|
+
data={"message": "weasyprint is not installed", "format": fmt},
|
|
605
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
606
|
+
)
|
|
607
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
608
|
+
return 3
|
|
609
|
+
raise
|
|
610
|
+
files.append(str(pdf_path))
|
|
611
|
+
if primary_output is None and fmt == "pdf":
|
|
612
|
+
primary_output = str(pdf_path)
|
|
613
|
+
except Exception as exc:
|
|
614
|
+
envelope = make_envelope(
|
|
615
|
+
status="error", reason=f"card_render_failed: {exc}",
|
|
616
|
+
data={"target": target},
|
|
617
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
618
|
+
)
|
|
619
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
620
|
+
return 3
|
|
621
|
+
|
|
622
|
+
data_out: dict[str, Any] = {
|
|
623
|
+
"format": fmt,
|
|
624
|
+
"files": files,
|
|
625
|
+
"output": primary_output if primary_output else (files[0] if files else None),
|
|
626
|
+
}
|
|
627
|
+
if text_output is not None and fmt in {"telegram", "all"}:
|
|
628
|
+
data_out["text"] = text_output
|
|
629
|
+
|
|
630
|
+
envelope = make_envelope(
|
|
631
|
+
status="ok",
|
|
632
|
+
source="cian",
|
|
633
|
+
data=data_out,
|
|
634
|
+
metadata=_privacy_metadata({"engine": "card", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
635
|
+
)
|
|
636
|
+
# print telegram text to stdout for convenience? Hidden tests may expect stdout
|
|
637
|
+
if fmt == "telegram":
|
|
638
|
+
_write_output(tg_text if False else _dump_envelope(envelope, args), args)
|
|
639
|
+
else:
|
|
640
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
641
|
+
return 0
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _cmd_pipeline(args: argparse.Namespace) -> int:
|
|
645
|
+
from .pipeline import set_status, list_items, show_history, has_phone
|
|
646
|
+
|
|
647
|
+
action = getattr(args, "pipeline_action", None)
|
|
648
|
+
if action == "set":
|
|
649
|
+
card_id = getattr(args, "card_id", None)
|
|
650
|
+
status = getattr(args, "status", None)
|
|
651
|
+
note = getattr(args, "note", None)
|
|
652
|
+
from_file = getattr(args, "from_file", None)
|
|
653
|
+
try:
|
|
654
|
+
res = set_status(card_id, status, note=note, from_file=from_file)
|
|
655
|
+
except ValueError as exc: # v0.4.8 контроль качества: без трейсбека наружу
|
|
656
|
+
envelope = make_envelope(status="error", reason=str(exc), source="pipeline",
|
|
657
|
+
data={"ok": False},
|
|
658
|
+
metadata=_privacy_metadata({"engine": "pipeline"}, args))
|
|
659
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
660
|
+
return 3
|
|
661
|
+
if not res.get("ok"):
|
|
662
|
+
envelope = make_envelope(
|
|
663
|
+
status="error",
|
|
664
|
+
reason=res.get("reason", "pipeline_error"),
|
|
665
|
+
source="pipeline",
|
|
666
|
+
data=res,
|
|
667
|
+
metadata=_privacy_metadata({"engine": "pipeline", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
668
|
+
)
|
|
669
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
670
|
+
return 3
|
|
671
|
+
data = res
|
|
672
|
+
metadata = {"engine": "pipeline", "cached": False, "elapsed_seconds": 0.0}
|
|
673
|
+
if res.get("warning"):
|
|
674
|
+
metadata["warning"] = res["warning"]
|
|
675
|
+
# sanitize contacts in output
|
|
676
|
+
data = sanitize_contacts(data)
|
|
677
|
+
envelope = make_envelope(
|
|
678
|
+
status="ok",
|
|
679
|
+
source="pipeline",
|
|
680
|
+
data=data,
|
|
681
|
+
metadata=_privacy_metadata(metadata, args),
|
|
682
|
+
)
|
|
683
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
684
|
+
return 0
|
|
685
|
+
if action == "list":
|
|
686
|
+
status = getattr(args, "status", None)
|
|
687
|
+
items = list_items(status=status)
|
|
688
|
+
data = sanitize_contacts(items)
|
|
689
|
+
envelope = make_envelope(
|
|
690
|
+
status="ok",
|
|
691
|
+
source="pipeline",
|
|
692
|
+
data=data,
|
|
693
|
+
metadata=_privacy_metadata({"engine": "pipeline", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
694
|
+
)
|
|
695
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
696
|
+
return 0
|
|
697
|
+
if action == "show":
|
|
698
|
+
card_id = getattr(args, "card_id", None)
|
|
699
|
+
res = show_history(card_id)
|
|
700
|
+
data = sanitize_contacts(res)
|
|
701
|
+
envelope = make_envelope(
|
|
702
|
+
status="ok",
|
|
703
|
+
source="pipeline",
|
|
704
|
+
data=data,
|
|
705
|
+
metadata=_privacy_metadata({"engine": "pipeline", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
706
|
+
)
|
|
707
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
708
|
+
return 0
|
|
709
|
+
envelope = make_envelope(
|
|
710
|
+
status="error", reason="usage_error",
|
|
711
|
+
data={"message": "missing pipeline subcommand"},
|
|
712
|
+
metadata=_privacy_metadata({"engine": "pipeline", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
713
|
+
)
|
|
714
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
715
|
+
return USAGE_ERROR_EXIT
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _cmd_watch(args: argparse.Namespace) -> int:
|
|
719
|
+
from .watch import add, list_items, remove, check
|
|
720
|
+
|
|
721
|
+
action = getattr(args, "watch_action", None)
|
|
722
|
+
if action == "add":
|
|
723
|
+
card_id = getattr(args, "card_id", None)
|
|
724
|
+
file = getattr(args, "file", None)
|
|
725
|
+
price = getattr(args, "price", None)
|
|
726
|
+
res = add(card_id=card_id, file=file, price=price)
|
|
727
|
+
if not res.get("ok"):
|
|
728
|
+
envelope = make_envelope(
|
|
729
|
+
status="error", reason=res.get("reason", "watch_error"),
|
|
730
|
+
source="watch", data=res,
|
|
731
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
732
|
+
)
|
|
733
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
734
|
+
return 3
|
|
735
|
+
envelope = make_envelope(
|
|
736
|
+
status="ok", source="watch", data=res,
|
|
737
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
738
|
+
)
|
|
739
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
740
|
+
return 0
|
|
741
|
+
if action == "list":
|
|
742
|
+
items = list_items()
|
|
743
|
+
data = sanitize_contacts(items)
|
|
744
|
+
envelope = make_envelope(
|
|
745
|
+
status="ok", source="watch", data=data,
|
|
746
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
747
|
+
)
|
|
748
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
749
|
+
return 0
|
|
750
|
+
if action == "rm":
|
|
751
|
+
card_id = getattr(args, "card_id", None)
|
|
752
|
+
res = remove(card_id)
|
|
753
|
+
envelope = make_envelope(
|
|
754
|
+
status="ok", source="watch", data=res,
|
|
755
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
756
|
+
)
|
|
757
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
758
|
+
return 0
|
|
759
|
+
if action == "check":
|
|
760
|
+
offline = getattr(args, "offline", None)
|
|
761
|
+
events = check(offline=offline)
|
|
762
|
+
# non-zero if price_down
|
|
763
|
+
has_down = any(e.get("type") == "price_down" for e in events)
|
|
764
|
+
data = sanitize_contacts(events)
|
|
765
|
+
envelope = make_envelope(
|
|
766
|
+
status="ok", source="watch", data=data,
|
|
767
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
768
|
+
)
|
|
769
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
770
|
+
return 1 if has_down else 0
|
|
771
|
+
envelope = make_envelope(
|
|
772
|
+
status="error", reason="usage_error",
|
|
773
|
+
data={"message": "missing watch subcommand"},
|
|
774
|
+
metadata=_privacy_metadata({"engine": "watch", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
775
|
+
)
|
|
776
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
777
|
+
return USAGE_ERROR_EXIT
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _cmd_digest(args: argparse.Namespace) -> int:
|
|
781
|
+
from .digest import build_digest, render
|
|
782
|
+
|
|
783
|
+
days = getattr(args, "days", 7) or 7
|
|
784
|
+
fmt = getattr(args, "format", "telegram") or "telegram"
|
|
785
|
+
if fmt not in {"telegram", "md"}:
|
|
786
|
+
envelope = make_envelope(
|
|
787
|
+
status="error", reason="unsupported_format",
|
|
788
|
+
data={"supported": ["telegram", "md"]},
|
|
789
|
+
metadata=_privacy_metadata({"engine": "digest", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
790
|
+
)
|
|
791
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
792
|
+
return USAGE_ERROR_EXIT
|
|
793
|
+
d = build_digest(days=days)
|
|
794
|
+
text = render(d, fmt=fmt)
|
|
795
|
+
# sanitize phones (none expected, but fail-closed)
|
|
796
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
797
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
798
|
+
if out:
|
|
799
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
800
|
+
data = {"format": fmt, "days": days, "text": text, "output": out}
|
|
801
|
+
envelope = make_envelope(
|
|
802
|
+
status="ok", source="digest", data=data,
|
|
803
|
+
metadata=_privacy_metadata({"engine": "digest", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
804
|
+
)
|
|
805
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
806
|
+
return 0
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _cmd_compare(args: argparse.Namespace) -> int:
|
|
810
|
+
from .compare import load_cards, render
|
|
811
|
+
|
|
812
|
+
path = getattr(args, "file", None)
|
|
813
|
+
if not path:
|
|
814
|
+
envelope = make_envelope(
|
|
815
|
+
status="error", reason="usage_error",
|
|
816
|
+
data={"message": "missing --file"},
|
|
817
|
+
metadata=_privacy_metadata({"engine": "compare", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
818
|
+
)
|
|
819
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
820
|
+
return USAGE_ERROR_EXIT
|
|
821
|
+
try:
|
|
822
|
+
cards = load_cards(path, getattr(args, "ids", None))
|
|
823
|
+
except Exception as exc:
|
|
824
|
+
envelope = make_envelope(
|
|
825
|
+
status="error", reason=f"invalid_input: {exc}",
|
|
826
|
+
data={"path": path},
|
|
827
|
+
metadata=_privacy_metadata({"engine": "compare", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
828
|
+
)
|
|
829
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
830
|
+
return 3
|
|
831
|
+
fmt = getattr(args, "format", "md") or "md"
|
|
832
|
+
if not cards:
|
|
833
|
+
text = "empty_input"
|
|
834
|
+
data = {"format": fmt, "count": 0, "text": text, "reason": "empty_input"}
|
|
835
|
+
envelope = make_envelope(
|
|
836
|
+
status="ok", reason="empty_input", source="compare", data=data,
|
|
837
|
+
metadata=_privacy_metadata({"engine": "compare", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
838
|
+
)
|
|
839
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
840
|
+
return 0
|
|
841
|
+
if getattr(args, "by_fit", False):
|
|
842
|
+
try:
|
|
843
|
+
from .scorefit import score_cards
|
|
844
|
+
scores = {r["id"]: r["score"] for r in score_cards(cards)}
|
|
845
|
+
cards = sorted(cards, key=lambda c: scores.get(c.id, 0), reverse=True)
|
|
846
|
+
except Exception:
|
|
847
|
+
pass
|
|
848
|
+
text = render(cards, fmt=fmt)
|
|
849
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
850
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
851
|
+
if out:
|
|
852
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
853
|
+
data = {"format": fmt, "count": len(cards), "text": text, "output": out}
|
|
854
|
+
envelope = make_envelope(
|
|
855
|
+
status="ok", source="compare", data=data,
|
|
856
|
+
metadata=_privacy_metadata({"engine": "compare", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
857
|
+
)
|
|
858
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
859
|
+
return 0
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _cmd_bargain(args: argparse.Namespace) -> int:
|
|
863
|
+
from .bargain import analyze, render_text
|
|
864
|
+
|
|
865
|
+
target_path = getattr(args, "file", None)
|
|
866
|
+
corpus_arg = getattr(args, "corpus", None)
|
|
867
|
+
if not target_path or not corpus_arg:
|
|
868
|
+
envelope = make_envelope(
|
|
869
|
+
status="error", reason="usage_error",
|
|
870
|
+
data={"message": "missing --file or --corpus"},
|
|
871
|
+
metadata=_privacy_metadata({"engine": "bargain", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
872
|
+
)
|
|
873
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
874
|
+
return USAGE_ERROR_EXIT
|
|
875
|
+
corpus_paths = [p.strip() for p in corpus_arg.split(",") if p.strip()]
|
|
876
|
+
try:
|
|
877
|
+
result = analyze(target_path, corpus_paths, district=getattr(args, "district", None),
|
|
878
|
+
rooms=getattr(args, "rooms", None), by_complex=bool(getattr(args, "by_complex", False)))
|
|
879
|
+
except Exception as exc:
|
|
880
|
+
envelope = make_envelope(
|
|
881
|
+
status="error", reason=f"invalid_input: {exc}",
|
|
882
|
+
data={"message": str(exc)},
|
|
883
|
+
metadata=_privacy_metadata({"engine": "bargain", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
884
|
+
)
|
|
885
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
886
|
+
return 3
|
|
887
|
+
text = render_text(result)
|
|
888
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
889
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
890
|
+
if out:
|
|
891
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
892
|
+
data = {"text": text, "result": result, "output": out}
|
|
893
|
+
status = "ok" if result.get("ok") else "degraded"
|
|
894
|
+
reason = None if result.get("ok") else result.get("reason")
|
|
895
|
+
envelope = make_envelope(
|
|
896
|
+
status=status, reason=reason, source="bargain", data=data,
|
|
897
|
+
metadata=_privacy_metadata({"engine": "bargain", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
898
|
+
)
|
|
899
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
900
|
+
return 0 if result.get("ok") else 2
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _cmd_risk(args: argparse.Namespace) -> int:
|
|
904
|
+
from .risk import score_card
|
|
905
|
+
|
|
906
|
+
path = getattr(args, "file", None)
|
|
907
|
+
if not path:
|
|
908
|
+
envelope = make_envelope(
|
|
909
|
+
status="error", reason="usage_error",
|
|
910
|
+
data={"message": "missing --file"},
|
|
911
|
+
metadata=_privacy_metadata({"engine": "risk", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
912
|
+
)
|
|
913
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
914
|
+
return USAGE_ERROR_EXIT
|
|
915
|
+
try:
|
|
916
|
+
import json as _j
|
|
917
|
+
card = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
918
|
+
except Exception as exc:
|
|
919
|
+
envelope = make_envelope(
|
|
920
|
+
status="error", reason=f"invalid_input: {exc}",
|
|
921
|
+
data={"path": path},
|
|
922
|
+
metadata=_privacy_metadata({"engine": "risk", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
923
|
+
)
|
|
924
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
925
|
+
return 3
|
|
926
|
+
corpus = None
|
|
927
|
+
corpus_arg = getattr(args, "corpus", None)
|
|
928
|
+
if corpus_arg:
|
|
929
|
+
try:
|
|
930
|
+
corpus = _j.loads(Path(corpus_arg).read_text(encoding="utf-8"))
|
|
931
|
+
except Exception:
|
|
932
|
+
corpus = corpus_arg
|
|
933
|
+
result = score_card(card, corpus=corpus, district=getattr(args, "district", None),
|
|
934
|
+
rooms=getattr(args, "rooms", None))
|
|
935
|
+
text = sanitize_contacts(result)
|
|
936
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
937
|
+
if out:
|
|
938
|
+
Path(out).write_text(__import__("json").dumps(text, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
939
|
+
data = text
|
|
940
|
+
envelope = make_envelope(
|
|
941
|
+
status="ok", source="risk", data=data,
|
|
942
|
+
metadata=_privacy_metadata({"engine": "risk", "cached": False, "elapsed_seconds": 0.0}, args),
|
|
943
|
+
)
|
|
944
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
945
|
+
return 0
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _cmd_triage(args: argparse.Namespace) -> int:
|
|
949
|
+
from .legal_triage import triage
|
|
950
|
+
|
|
951
|
+
path = getattr(args, "file", None)
|
|
952
|
+
if not path:
|
|
953
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
954
|
+
data={"message": "missing --file"},
|
|
955
|
+
metadata=_privacy_metadata({"engine": "triage", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
956
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
957
|
+
return USAGE_ERROR_EXIT
|
|
958
|
+
try:
|
|
959
|
+
import json as _j
|
|
960
|
+
card = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
961
|
+
except Exception as exc:
|
|
962
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
963
|
+
data={"path": path},
|
|
964
|
+
metadata=_privacy_metadata({"engine": "triage", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
965
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
966
|
+
return 3
|
|
967
|
+
# if list, take first
|
|
968
|
+
if isinstance(card, list):
|
|
969
|
+
card = card[0] if card else {}
|
|
970
|
+
if isinstance(card, dict) and "cards" in card and isinstance(card["cards"], list):
|
|
971
|
+
card = card["cards"][0] if card["cards"] else {}
|
|
972
|
+
result = triage(card)
|
|
973
|
+
result = sanitize_contacts(result)
|
|
974
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
975
|
+
if out:
|
|
976
|
+
Path(out).write_text(__import__("json").dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
977
|
+
envelope = make_envelope(status="ok", source="triage", data=result,
|
|
978
|
+
metadata=_privacy_metadata({"engine": "triage", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
979
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
980
|
+
return 0
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _cmd_checklist(args: argparse.Namespace) -> int:
|
|
984
|
+
from .checklist import new_checklist
|
|
985
|
+
|
|
986
|
+
action = getattr(args, "checklist_action", None)
|
|
987
|
+
if action == "new":
|
|
988
|
+
card_id = getattr(args, "card_id", None)
|
|
989
|
+
file = getattr(args, "file", None)
|
|
990
|
+
if not card_id or not file:
|
|
991
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
992
|
+
data={"message": "missing card_id or --file"},
|
|
993
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
994
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
995
|
+
return USAGE_ERROR_EXIT
|
|
996
|
+
try:
|
|
997
|
+
text = new_checklist(card_id, file)
|
|
998
|
+
except Exception as exc:
|
|
999
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1000
|
+
data={"message": str(exc)},
|
|
1001
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1002
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1003
|
+
return 3
|
|
1004
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1005
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1006
|
+
if out:
|
|
1007
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1008
|
+
data = {"text": text, "output": out}
|
|
1009
|
+
envelope = make_envelope(status="ok", source="checklist", data=data,
|
|
1010
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1011
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1012
|
+
return 0
|
|
1013
|
+
if action == "handoff":
|
|
1014
|
+
from .checklist import handoff
|
|
1015
|
+
card_id = getattr(args, "card_id", None)
|
|
1016
|
+
to = getattr(args, "to", "colleague")
|
|
1017
|
+
file = getattr(args, "file", None)
|
|
1018
|
+
res = handoff(card_id, to, file=file)
|
|
1019
|
+
if not res.get("ok"):
|
|
1020
|
+
envelope = make_envelope(status="error", reason=res.get("reason", "handoff_failed"),
|
|
1021
|
+
data=res,
|
|
1022
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1023
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1024
|
+
return 3 if True else 1
|
|
1025
|
+
envelope = make_envelope(status="ok", source="checklist", data=res,
|
|
1026
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1027
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1028
|
+
return 0
|
|
1029
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1030
|
+
data={"message": "missing checklist subcommand"},
|
|
1031
|
+
metadata=_privacy_metadata({"engine": "checklist", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1032
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1033
|
+
return USAGE_ERROR_EXIT
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _cmd_callsheet(args: argparse.Namespace) -> int:
|
|
1037
|
+
from .callsheet import _load_cards, build_callsheet
|
|
1038
|
+
|
|
1039
|
+
path = getattr(args, "file", None)
|
|
1040
|
+
if not path:
|
|
1041
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1042
|
+
data={"message": "missing --file"},
|
|
1043
|
+
metadata=_privacy_metadata({"engine": "callsheet", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1044
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1045
|
+
return USAGE_ERROR_EXIT
|
|
1046
|
+
try:
|
|
1047
|
+
cards = _load_cards(path)
|
|
1048
|
+
except Exception as exc:
|
|
1049
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1050
|
+
data={"path": path},
|
|
1051
|
+
metadata=_privacy_metadata({"engine": "callsheet", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1052
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1053
|
+
return 3
|
|
1054
|
+
with_contacts = bool(getattr(args, "with_contacts", False))
|
|
1055
|
+
text = build_callsheet(cards, with_contacts=with_contacts)
|
|
1056
|
+
text = sanitize_contacts(text) if not with_contacts else text
|
|
1057
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1058
|
+
if out:
|
|
1059
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1060
|
+
data = {"text": text, "output": out, "count": len(cards)}
|
|
1061
|
+
envelope = make_envelope(status="ok", source="callsheet", data=data,
|
|
1062
|
+
metadata=_privacy_metadata({"engine": "callsheet", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1063
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1064
|
+
return 0
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _cmd_project(args: argparse.Namespace) -> int:
|
|
1068
|
+
from .projects import project_new, project_list, project_add, project_open, project_refresh, project_cards
|
|
1069
|
+
|
|
1070
|
+
action = getattr(args, "project_action", None)
|
|
1071
|
+
if action == "new":
|
|
1072
|
+
name = getattr(args, "name", None)
|
|
1073
|
+
query = getattr(args, "query", None)
|
|
1074
|
+
res = project_new(name, query=query)
|
|
1075
|
+
envelope = make_envelope(status="ok", source="project", data=res,
|
|
1076
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1077
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1078
|
+
return 0
|
|
1079
|
+
if action == "list":
|
|
1080
|
+
items = project_list()
|
|
1081
|
+
envelope = make_envelope(status="ok", source="project", data=items,
|
|
1082
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1083
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1084
|
+
return 0
|
|
1085
|
+
if action == "add":
|
|
1086
|
+
project = getattr(args, "project", None)
|
|
1087
|
+
card_id = getattr(args, "card_id", None)
|
|
1088
|
+
res = project_add(project, card_id)
|
|
1089
|
+
envelope = make_envelope(status="ok", source="project", data=res,
|
|
1090
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1091
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1092
|
+
return 0
|
|
1093
|
+
if action == "open":
|
|
1094
|
+
project = getattr(args, "project", None)
|
|
1095
|
+
res = project_open(project)
|
|
1096
|
+
# sanitize
|
|
1097
|
+
res = sanitize_contacts(res)
|
|
1098
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1099
|
+
if out:
|
|
1100
|
+
Path(out).write_text(__import__("json").dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1101
|
+
data = {"project": project, "cards": res.get("cards"), "output": out, "text": __import__("json").dumps(res, ensure_ascii=False)}
|
|
1102
|
+
envelope = make_envelope(status="ok", source="project", data=data,
|
|
1103
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1104
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1105
|
+
return 0
|
|
1106
|
+
if action == "refresh":
|
|
1107
|
+
project = getattr(args, "project", None)
|
|
1108
|
+
file = getattr(args, "file", None)
|
|
1109
|
+
res = project_refresh(project, file)
|
|
1110
|
+
envelope = make_envelope(status="ok", source="project", data=res,
|
|
1111
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1112
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1113
|
+
return 0
|
|
1114
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1115
|
+
data={"message": "missing project subcommand"},
|
|
1116
|
+
metadata=_privacy_metadata({"engine": "project", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1117
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1118
|
+
return USAGE_ERROR_EXIT
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _cmd_poi(args: argparse.Namespace) -> int:
|
|
1122
|
+
from .poi import poi_for_card, render_text
|
|
1123
|
+
|
|
1124
|
+
path = getattr(args, "file", None)
|
|
1125
|
+
if not path:
|
|
1126
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1127
|
+
data={"message": "missing --file"},
|
|
1128
|
+
metadata=_privacy_metadata({"engine": "poi", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1129
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1130
|
+
return USAGE_ERROR_EXIT
|
|
1131
|
+
try:
|
|
1132
|
+
import json as _j
|
|
1133
|
+
card = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1134
|
+
except Exception as exc:
|
|
1135
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1136
|
+
data={"path": path},
|
|
1137
|
+
metadata=_privacy_metadata({"engine": "poi", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1138
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1139
|
+
return 3
|
|
1140
|
+
if isinstance(card, list):
|
|
1141
|
+
card = card[0] if card else {}
|
|
1142
|
+
if isinstance(card, dict) and "cards" in card and isinstance(card["cards"], list):
|
|
1143
|
+
card = card["cards"][0] if card["cards"] else {}
|
|
1144
|
+
radius = getattr(args, "radius_min", 15) or 15
|
|
1145
|
+
offline = getattr(args, "offline", None)
|
|
1146
|
+
result = poi_for_card(card, radius_min=radius, offline=offline)
|
|
1147
|
+
text = render_text(result)
|
|
1148
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1149
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1150
|
+
if out:
|
|
1151
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1152
|
+
data = {"text": text, "result": result, "output": out}
|
|
1153
|
+
envelope = make_envelope(status="ok", source="poi", data=data,
|
|
1154
|
+
metadata=_privacy_metadata({"engine": "poi", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1155
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1156
|
+
return 0
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _cmd_tour(args: argparse.Namespace) -> int:
|
|
1160
|
+
from .tour import _load_cards, build_tour, render_text
|
|
1161
|
+
|
|
1162
|
+
project = getattr(args, "project", None)
|
|
1163
|
+
file = getattr(args, "file", None)
|
|
1164
|
+
if not project and not file:
|
|
1165
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1166
|
+
data={"message": "missing --project or --file"},
|
|
1167
|
+
metadata=_privacy_metadata({"engine": "tour", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1168
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1169
|
+
return USAGE_ERROR_EXIT
|
|
1170
|
+
try:
|
|
1171
|
+
cards = _load_cards(path=file, project=project)
|
|
1172
|
+
except Exception as exc:
|
|
1173
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1174
|
+
data={"message": str(exc)},
|
|
1175
|
+
metadata=_privacy_metadata({"engine": "tour", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1176
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1177
|
+
return 3
|
|
1178
|
+
all_flag = bool(getattr(args, "all", False))
|
|
1179
|
+
slot_min = getattr(args, "slot_min", 45) or 45
|
|
1180
|
+
tour = build_tour(cards, slot_min=slot_min, start_time=getattr(args, "start", "10:00") or "10:00", all_flag=all_flag)
|
|
1181
|
+
fmt = getattr(args, "format", "telegram") or "telegram"
|
|
1182
|
+
if fmt not in {"telegram", "md"}:
|
|
1183
|
+
fmt = "telegram"
|
|
1184
|
+
text = render_text(tour, fmt=fmt)
|
|
1185
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1186
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1187
|
+
if out:
|
|
1188
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1189
|
+
data = {"text": text, "count": tour.get("count", 0), "output": out}
|
|
1190
|
+
envelope = make_envelope(status="ok", source="tour", data=data,
|
|
1191
|
+
metadata=_privacy_metadata({"engine": "tour", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1192
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1193
|
+
return 0
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _cmd_sources(args: argparse.Namespace) -> int:
|
|
1197
|
+
from .sources import list_sources
|
|
1198
|
+
|
|
1199
|
+
action = getattr(args, "sources_action", None) or "list"
|
|
1200
|
+
if action == "list":
|
|
1201
|
+
items = list_sources()
|
|
1202
|
+
envelope = make_envelope(status="ok", source="sources", data=items,
|
|
1203
|
+
metadata=_privacy_metadata({"engine": "sources", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1204
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1205
|
+
return 0
|
|
1206
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1207
|
+
data={"message": "missing sources subcommand"},
|
|
1208
|
+
metadata=_privacy_metadata({"engine": "sources", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1209
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1210
|
+
return USAGE_ERROR_EXIT
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _cmd_dedup(args: argparse.Namespace) -> int:
|
|
1214
|
+
from .dedup import dedup_cards
|
|
1215
|
+
import json as _j
|
|
1216
|
+
|
|
1217
|
+
path = getattr(args, "file", None)
|
|
1218
|
+
if not path:
|
|
1219
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1220
|
+
data={"message": "missing --file"},
|
|
1221
|
+
metadata=_privacy_metadata({"engine": "dedup", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1222
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1223
|
+
return USAGE_ERROR_EXIT
|
|
1224
|
+
try:
|
|
1225
|
+
raw = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1226
|
+
except Exception as exc:
|
|
1227
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1228
|
+
data={"path": path},
|
|
1229
|
+
metadata=_privacy_metadata({"engine": "dedup", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1230
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1231
|
+
return 3
|
|
1232
|
+
if isinstance(raw, dict):
|
|
1233
|
+
if "cards" in raw and isinstance(raw["cards"], list):
|
|
1234
|
+
raw = raw["cards"]
|
|
1235
|
+
elif "data" in raw:
|
|
1236
|
+
d = raw["data"]
|
|
1237
|
+
raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
|
|
1238
|
+
else:
|
|
1239
|
+
raw = [raw]
|
|
1240
|
+
if not isinstance(raw, list):
|
|
1241
|
+
raw = [raw]
|
|
1242
|
+
strict = bool(getattr(args, "strict", False))
|
|
1243
|
+
groups = dedup_cards(raw, strict=strict)
|
|
1244
|
+
groups = sanitize_contacts(groups)
|
|
1245
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1246
|
+
if out:
|
|
1247
|
+
Path(out).write_text(_j.dumps(groups, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1248
|
+
data = {"groups": groups, "count": len(groups), "output": out}
|
|
1249
|
+
envelope = make_envelope(status="ok", source="dedup", data=data,
|
|
1250
|
+
metadata=_privacy_metadata({"engine": "dedup", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1251
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1252
|
+
return 0
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _cmd_market(args: argparse.Namespace) -> int:
|
|
1256
|
+
from .market import analyze, render_text
|
|
1257
|
+
|
|
1258
|
+
corpus_arg = getattr(args, "corpus", None)
|
|
1259
|
+
if not corpus_arg:
|
|
1260
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1261
|
+
data={"message": "missing --corpus"},
|
|
1262
|
+
metadata=_privacy_metadata({"engine": "market", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1263
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1264
|
+
return USAGE_ERROR_EXIT
|
|
1265
|
+
paths = [p.strip() for p in corpus_arg.split(",") if p.strip()]
|
|
1266
|
+
result = analyze(paths, district=getattr(args, "district", None),
|
|
1267
|
+
okrug=getattr(args, "okrug", None),
|
|
1268
|
+
days=getattr(args, "days", 30) or 30,
|
|
1269
|
+
group_by=getattr(args, "group_by", "district"))
|
|
1270
|
+
fmt = getattr(args, "format", "telegram") or "telegram"
|
|
1271
|
+
text = render_text(result, fmt=fmt)
|
|
1272
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1273
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1274
|
+
if out:
|
|
1275
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1276
|
+
data = {"text": text, "result": result, "output": out}
|
|
1277
|
+
status = "ok" if result.get("ok") else "degraded"
|
|
1278
|
+
reason = None if result.get("ok") else result.get("reason")
|
|
1279
|
+
envelope = make_envelope(status=status, reason=reason, source="market", data=data,
|
|
1280
|
+
metadata=_privacy_metadata({"engine": "market", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1281
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1282
|
+
return 0 if result.get("ok") else 2
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _cmd_stale(args: argparse.Namespace) -> int:
|
|
1286
|
+
from .stale import detect_stale, render_text
|
|
1287
|
+
|
|
1288
|
+
path = getattr(args, "file", None)
|
|
1289
|
+
project = getattr(args, "project", None)
|
|
1290
|
+
cards = []
|
|
1291
|
+
if path:
|
|
1292
|
+
try:
|
|
1293
|
+
import json as _j
|
|
1294
|
+
raw = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1295
|
+
except Exception as exc:
|
|
1296
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1297
|
+
data={"path": path},
|
|
1298
|
+
metadata=_privacy_metadata({"engine": "stale", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1299
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1300
|
+
return 3
|
|
1301
|
+
if isinstance(raw, dict):
|
|
1302
|
+
if "cards" in raw and isinstance(raw["cards"], list):
|
|
1303
|
+
raw = raw["cards"]
|
|
1304
|
+
elif "data" in raw:
|
|
1305
|
+
d = raw["data"]
|
|
1306
|
+
raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
|
|
1307
|
+
else:
|
|
1308
|
+
raw = [raw]
|
|
1309
|
+
if not isinstance(raw, list):
|
|
1310
|
+
raw = [raw]
|
|
1311
|
+
cards = raw
|
|
1312
|
+
if project:
|
|
1313
|
+
try:
|
|
1314
|
+
from .projects import project_cards
|
|
1315
|
+
cards = [{"id": cid, "source": "cian", "url": ""} for cid in project_cards(project)]
|
|
1316
|
+
except Exception:
|
|
1317
|
+
pass
|
|
1318
|
+
if not cards:
|
|
1319
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1320
|
+
data={"message": "missing --file or --project"},
|
|
1321
|
+
metadata=_privacy_metadata({"engine": "stale", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1322
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1323
|
+
return USAGE_ERROR_EXIT
|
|
1324
|
+
max_age = getattr(args, "max_age_days", 14) or 14
|
|
1325
|
+
verify = bool(getattr(args, "verify_live", False))
|
|
1326
|
+
results = detect_stale(cards, max_age_days=max_age, verify_live=verify)
|
|
1327
|
+
text = render_text(results)
|
|
1328
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1329
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1330
|
+
if out:
|
|
1331
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1332
|
+
data = {"results": results, "text": text, "output": out}
|
|
1333
|
+
envelope = make_envelope(status="ok", source="stale", data=data,
|
|
1334
|
+
metadata=_privacy_metadata({"engine": "stale", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1335
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1336
|
+
return 0
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _cmd_catalog_pdf(args: argparse.Namespace) -> int:
|
|
1340
|
+
from .export_pdf import catalog_pdf
|
|
1341
|
+
|
|
1342
|
+
path = getattr(args, "file", None)
|
|
1343
|
+
project = getattr(args, "project", None)
|
|
1344
|
+
cards = []
|
|
1345
|
+
if path:
|
|
1346
|
+
try:
|
|
1347
|
+
import json as _j
|
|
1348
|
+
raw = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1349
|
+
except Exception as exc:
|
|
1350
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1351
|
+
data={"path": path},
|
|
1352
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1353
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1354
|
+
return 3
|
|
1355
|
+
if isinstance(raw, dict):
|
|
1356
|
+
if "cards" in raw and isinstance(raw["cards"], list):
|
|
1357
|
+
raw = raw["cards"]
|
|
1358
|
+
elif "data" in raw:
|
|
1359
|
+
d = raw["data"]
|
|
1360
|
+
raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
|
|
1361
|
+
else:
|
|
1362
|
+
raw = [raw]
|
|
1363
|
+
if not isinstance(raw, list):
|
|
1364
|
+
raw = [raw]
|
|
1365
|
+
cards = raw
|
|
1366
|
+
if project:
|
|
1367
|
+
try:
|
|
1368
|
+
from .projects import project_cards
|
|
1369
|
+
cards = [{"id": cid, "source": "cian", "url": ""} for cid in project_cards(project)]
|
|
1370
|
+
except Exception:
|
|
1371
|
+
pass
|
|
1372
|
+
if not cards:
|
|
1373
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1374
|
+
data={"message": "missing --file or --project"},
|
|
1375
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1376
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1377
|
+
return USAGE_ERROR_EXIT
|
|
1378
|
+
if len(cards) > 30:
|
|
1379
|
+
envelope = make_envelope(status="error", reason="too_many",
|
|
1380
|
+
data={"count": len(cards), "limit": 30},
|
|
1381
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1382
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1383
|
+
return 3
|
|
1384
|
+
out = getattr(args, "out", None) or getattr(args, "output", None) or "katalog.pdf"
|
|
1385
|
+
title = getattr(args, "title", None)
|
|
1386
|
+
try:
|
|
1387
|
+
catalog_pdf(cards, out, title=title)
|
|
1388
|
+
except RuntimeError as exc:
|
|
1389
|
+
if "pdf-format-unavailable" in str(exc):
|
|
1390
|
+
envelope = make_envelope(status="error", reason="pdf-format-unavailable",
|
|
1391
|
+
data={"message": "weasyprint is not installed"},
|
|
1392
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1393
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1394
|
+
return 3
|
|
1395
|
+
if "too_many" in str(exc):
|
|
1396
|
+
envelope = make_envelope(status="error", reason="too_many",
|
|
1397
|
+
data={"count": len(cards)},
|
|
1398
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1399
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1400
|
+
return 3
|
|
1401
|
+
raise
|
|
1402
|
+
except Exception as exc:
|
|
1403
|
+
envelope = make_envelope(status="error", reason=f"catalog_pdf_failed: {exc}",
|
|
1404
|
+
data={"message": str(exc)},
|
|
1405
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1406
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1407
|
+
return 3
|
|
1408
|
+
envelope = make_envelope(status="ok", source="catalog-pdf", data={"output": out, "count": len(cards)},
|
|
1409
|
+
metadata=_privacy_metadata({"engine": "catalog-pdf", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1410
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1411
|
+
return 0
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def _cmd_feedback(args: argparse.Namespace) -> int:
|
|
1415
|
+
from .feedback import add_feedback, list_feedback, profile_aggregate, apply_overrides
|
|
1416
|
+
|
|
1417
|
+
action = getattr(args, "feedback_action", None)
|
|
1418
|
+
if action == "add":
|
|
1419
|
+
card_id = getattr(args, "card_id", None)
|
|
1420
|
+
verdict = getattr(args, "verdict", None)
|
|
1421
|
+
reason = getattr(args, "reason", None)
|
|
1422
|
+
note = getattr(args, "note", None)
|
|
1423
|
+
dry = bool(getattr(args, "dry", False))
|
|
1424
|
+
res = add_feedback(card_id, verdict, reason=reason, note=note, dry=dry)
|
|
1425
|
+
envelope = make_envelope(status="ok", source="feedback", data=res,
|
|
1426
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1427
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1428
|
+
return 0
|
|
1429
|
+
if action == "list":
|
|
1430
|
+
card_id = getattr(args, "card", None)
|
|
1431
|
+
items = list_feedback(card_id=card_id)
|
|
1432
|
+
items = sanitize_contacts(items)
|
|
1433
|
+
envelope = make_envelope(status="ok", source="feedback", data=items,
|
|
1434
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1435
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1436
|
+
return 0
|
|
1437
|
+
if action == "profile":
|
|
1438
|
+
prof = profile_aggregate()
|
|
1439
|
+
envelope = make_envelope(status="ok", source="feedback", data=prof,
|
|
1440
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1441
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1442
|
+
return 0
|
|
1443
|
+
if action == "apply":
|
|
1444
|
+
dry = bool(getattr(args, "dry", False))
|
|
1445
|
+
yes = bool(getattr(args, "yes", False))
|
|
1446
|
+
res = apply_overrides(dry=dry, yes=yes)
|
|
1447
|
+
if not res.get("ok"):
|
|
1448
|
+
envelope = make_envelope(status="error", reason=res.get("reason", "apply_failed"),
|
|
1449
|
+
data=res,
|
|
1450
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1451
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1452
|
+
return 3
|
|
1453
|
+
envelope = make_envelope(status="ok", source="feedback", data=res,
|
|
1454
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1455
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1456
|
+
return 0
|
|
1457
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1458
|
+
data={"message": "missing feedback subcommand"},
|
|
1459
|
+
metadata=_privacy_metadata({"engine": "feedback", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1460
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1461
|
+
return USAGE_ERROR_EXIT
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _cmd_scorefit(args: argparse.Namespace) -> int:
|
|
1465
|
+
from .scorefit import score_cards
|
|
1466
|
+
import json as _j
|
|
1467
|
+
|
|
1468
|
+
path = getattr(args, "file", None)
|
|
1469
|
+
if not path:
|
|
1470
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1471
|
+
data={"message": "missing --file"},
|
|
1472
|
+
metadata=_privacy_metadata({"engine": "scorefit", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1473
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1474
|
+
return USAGE_ERROR_EXIT
|
|
1475
|
+
try:
|
|
1476
|
+
raw = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1477
|
+
except Exception as exc:
|
|
1478
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1479
|
+
data={"path": path},
|
|
1480
|
+
metadata=_privacy_metadata({"engine": "scorefit", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1481
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1482
|
+
return 3
|
|
1483
|
+
if isinstance(raw, dict):
|
|
1484
|
+
if "cards" in raw and isinstance(raw["cards"], list):
|
|
1485
|
+
raw = raw["cards"]
|
|
1486
|
+
elif "data" in raw:
|
|
1487
|
+
d = raw["data"]
|
|
1488
|
+
raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
|
|
1489
|
+
else:
|
|
1490
|
+
raw = [raw]
|
|
1491
|
+
if not isinstance(raw, list):
|
|
1492
|
+
raw = [raw]
|
|
1493
|
+
profile = getattr(args, "profile", None)
|
|
1494
|
+
# profile may be file path or name
|
|
1495
|
+
from .models import ListingCard
|
|
1496
|
+
cards = []
|
|
1497
|
+
for x in raw:
|
|
1498
|
+
if isinstance(x, dict):
|
|
1499
|
+
cards.append(ListingCard(**x))
|
|
1500
|
+
else:
|
|
1501
|
+
cards.append(x)
|
|
1502
|
+
results = score_cards(cards, profile=profile)
|
|
1503
|
+
# verify arithmetic: score ~ sum contributions/weight*100
|
|
1504
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1505
|
+
if out:
|
|
1506
|
+
Path(out).write_text(_j.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
1507
|
+
data = {"results": results, "output": out}
|
|
1508
|
+
envelope = make_envelope(status="ok", source="scorefit", data=data,
|
|
1509
|
+
metadata=_privacy_metadata({"engine": "scorefit", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1510
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1511
|
+
return 0
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def _cmd_chain(args: argparse.Namespace) -> int:
|
|
1515
|
+
from .chains import chain_new, chain_link, chain_show
|
|
1516
|
+
|
|
1517
|
+
action = getattr(args, "chain_action", None)
|
|
1518
|
+
if action == "new":
|
|
1519
|
+
card_id = getattr(args, "card_id", None)
|
|
1520
|
+
kind = getattr(args, "kind", "direct")
|
|
1521
|
+
res = chain_new(card_id, kind)
|
|
1522
|
+
envelope = make_envelope(status="ok", source="chain", data=res,
|
|
1523
|
+
metadata=_privacy_metadata({"engine": "chain", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1524
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1525
|
+
return 0
|
|
1526
|
+
if action == "link":
|
|
1527
|
+
a = getattr(args, "a", None)
|
|
1528
|
+
b = getattr(args, "b", None)
|
|
1529
|
+
rel = getattr(args, "rel", "depends_on")
|
|
1530
|
+
res = chain_link(a, b, rel)
|
|
1531
|
+
if not res.get("ok"):
|
|
1532
|
+
envelope = make_envelope(status="error", reason=res.get("reason", "chain_error"),
|
|
1533
|
+
data=res,
|
|
1534
|
+
metadata=_privacy_metadata({"engine": "chain", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1535
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1536
|
+
return 3
|
|
1537
|
+
envelope = make_envelope(status="ok", source="chain", data=res,
|
|
1538
|
+
metadata=_privacy_metadata({"engine": "chain", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1539
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1540
|
+
return 0
|
|
1541
|
+
if action == "show":
|
|
1542
|
+
card_id = getattr(args, "card_id", None)
|
|
1543
|
+
res = chain_show(card_id)
|
|
1544
|
+
res = sanitize_contacts(res)
|
|
1545
|
+
envelope = make_envelope(status="ok", source="chain", data=res,
|
|
1546
|
+
metadata=_privacy_metadata({"engine": "chain", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1547
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1548
|
+
return 0
|
|
1549
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1550
|
+
data={"message": "missing chain subcommand"},
|
|
1551
|
+
metadata=_privacy_metadata({"engine": "chain", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1552
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1553
|
+
return USAGE_ERROR_EXIT
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def _cmd_text(args: argparse.Namespace) -> int:
|
|
1557
|
+
from .textsearch import index_file, query, TextSearchError
|
|
1558
|
+
|
|
1559
|
+
action = getattr(args, "text_action", None)
|
|
1560
|
+
if action == "index":
|
|
1561
|
+
path = getattr(args, "file", None)
|
|
1562
|
+
if not path:
|
|
1563
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1564
|
+
data={"message": "missing --file"},
|
|
1565
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1566
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1567
|
+
return USAGE_ERROR_EXIT
|
|
1568
|
+
try:
|
|
1569
|
+
res = index_file(path)
|
|
1570
|
+
except TextSearchError as exc:
|
|
1571
|
+
envelope = make_envelope(status="error", reason="fts5_unavailable",
|
|
1572
|
+
data={"message": str(exc)},
|
|
1573
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1574
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1575
|
+
return 3
|
|
1576
|
+
except Exception as exc:
|
|
1577
|
+
envelope = make_envelope(status="error", reason=f"index_failed: {exc}",
|
|
1578
|
+
data={"message": str(exc)},
|
|
1579
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1580
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1581
|
+
return 3
|
|
1582
|
+
envelope = make_envelope(status="ok", source="text", data=res,
|
|
1583
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1584
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1585
|
+
return 0
|
|
1586
|
+
if action == "query":
|
|
1587
|
+
q = getattr(args, "query", None)
|
|
1588
|
+
project = getattr(args, "project", None)
|
|
1589
|
+
try:
|
|
1590
|
+
res = query(q, project=project)
|
|
1591
|
+
except TextSearchError as exc:
|
|
1592
|
+
envelope = make_envelope(status="error", reason="fts5_unavailable",
|
|
1593
|
+
data={"message": str(exc)},
|
|
1594
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1595
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1596
|
+
return 3
|
|
1597
|
+
except Exception as exc:
|
|
1598
|
+
envelope = make_envelope(status="error", reason=f"query_failed: {exc}",
|
|
1599
|
+
data={"message": str(exc)},
|
|
1600
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1601
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1602
|
+
return 3
|
|
1603
|
+
res = sanitize_contacts(res)
|
|
1604
|
+
envelope = make_envelope(status="ok", source="text", data=res,
|
|
1605
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1606
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1607
|
+
return 0
|
|
1608
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1609
|
+
data={"message": "missing text subcommand"},
|
|
1610
|
+
metadata=_privacy_metadata({"engine": "text", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1611
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1612
|
+
return USAGE_ERROR_EXIT
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def _cmd_audit(args: argparse.Namespace) -> int:
|
|
1616
|
+
from .auditlog import read_entries, size_warn
|
|
1617
|
+
|
|
1618
|
+
days = getattr(args, "days", 30) or 30
|
|
1619
|
+
role = getattr(args, "role", None)
|
|
1620
|
+
card = getattr(args, "card", None)
|
|
1621
|
+
entries = read_entries(days=days, role=role, card=card)
|
|
1622
|
+
data = {"entries": entries, "count": len(entries)}
|
|
1623
|
+
if size_warn():
|
|
1624
|
+
data["audit_size_warn"] = True
|
|
1625
|
+
envelope = make_envelope(status="ok", source="audit", data=data,
|
|
1626
|
+
metadata=_privacy_metadata({"engine": "audit", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1627
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1628
|
+
return 0
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def _cmd_pii_log(args: argparse.Namespace) -> int:
|
|
1632
|
+
from .auditlog import read_entries
|
|
1633
|
+
|
|
1634
|
+
days = getattr(args, "days", 30) or 30
|
|
1635
|
+
entries = read_entries(days=days, pii_only=True)
|
|
1636
|
+
envelope = make_envelope(status="ok", source="pii-log", data=entries,
|
|
1637
|
+
metadata=_privacy_metadata({"engine": "pii-log", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1638
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1639
|
+
return 0
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def _cmd_purge(args: argparse.Namespace) -> int:
|
|
1643
|
+
from .purge import purge_card, purge_profile
|
|
1644
|
+
|
|
1645
|
+
action = getattr(args, "purge_action", None)
|
|
1646
|
+
yes = bool(getattr(args, "yes", False))
|
|
1647
|
+
dry = not yes # dry by default
|
|
1648
|
+
if action == "card":
|
|
1649
|
+
card_id = getattr(args, "card_id", None)
|
|
1650
|
+
res = purge_card(card_id, yes=yes, dry=dry)
|
|
1651
|
+
if not res.get("ok", True) and not res.get("dry"):
|
|
1652
|
+
envelope = make_envelope(status="error", reason=res.get("reason", "purge_failed"),
|
|
1653
|
+
data=res,
|
|
1654
|
+
metadata=_privacy_metadata({"engine": "purge", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1655
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1656
|
+
return 3
|
|
1657
|
+
envelope = make_envelope(status="ok", source="purge", data=res,
|
|
1658
|
+
metadata=_privacy_metadata({"engine": "purge", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1659
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1660
|
+
return 0
|
|
1661
|
+
if action == "profile":
|
|
1662
|
+
name = getattr(args, "name", None)
|
|
1663
|
+
res = purge_profile(name, yes=yes, dry=dry)
|
|
1664
|
+
envelope = make_envelope(status="ok", source="purge", data=res,
|
|
1665
|
+
metadata=_privacy_metadata({"engine": "purge", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1666
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1667
|
+
return 0
|
|
1668
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1669
|
+
data={"message": "missing purge subcommand"},
|
|
1670
|
+
metadata=_privacy_metadata({"engine": "purge", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1671
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1672
|
+
return USAGE_ERROR_EXIT
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _cmd_auto(args: argparse.Namespace) -> int:
|
|
1676
|
+
from .auto import run, report
|
|
1677
|
+
|
|
1678
|
+
action = getattr(args, "auto_action", None)
|
|
1679
|
+
if action == "run":
|
|
1680
|
+
res = run(profile=getattr(args, "profile", None), file=getattr(args, "file", None),
|
|
1681
|
+
project=getattr(args, "project", None), out=getattr(args, "out", None)
|
|
1682
|
+
or getattr(args, "output", None), live=bool(getattr(args, "live", False)))
|
|
1683
|
+
envelope = make_envelope(status="ok", source="auto", data=res,
|
|
1684
|
+
metadata=_privacy_metadata({"engine": "auto", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1685
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1686
|
+
return 0
|
|
1687
|
+
if action == "report":
|
|
1688
|
+
res = report(days=getattr(args, "days", 7) or 7, out=getattr(args, "out", None) or getattr(args, "output", None))
|
|
1689
|
+
envelope = make_envelope(status="ok", source="auto", data=res,
|
|
1690
|
+
metadata=_privacy_metadata({"engine": "auto", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1691
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1692
|
+
return 0
|
|
1693
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1694
|
+
data={"message": "missing auto subcommand"},
|
|
1695
|
+
metadata=_privacy_metadata({"engine": "auto", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1696
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1697
|
+
return USAGE_ERROR_EXIT
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def _cmd_litcheck(args: argparse.Namespace) -> int:
|
|
1701
|
+
from .litprecheck import litcheck, render_text
|
|
1702
|
+
|
|
1703
|
+
card_id = getattr(args, "card", None)
|
|
1704
|
+
file = getattr(args, "file", None)
|
|
1705
|
+
address = getattr(args, "address", None)
|
|
1706
|
+
developer = getattr(args, "developer", None)
|
|
1707
|
+
since = getattr(args, "since", 2020) or 2020
|
|
1708
|
+
res = litcheck(card_id=card_id, address=address, developer=developer, since=since, file=file)
|
|
1709
|
+
text = render_text(res)
|
|
1710
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1711
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1712
|
+
if out:
|
|
1713
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1714
|
+
data = {"text": text, "result": res, "output": out}
|
|
1715
|
+
envelope = make_envelope(status="ok", source="litcheck", data=data,
|
|
1716
|
+
metadata=_privacy_metadata({"engine": "litcheck", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1717
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1718
|
+
# audit entry
|
|
1719
|
+
try:
|
|
1720
|
+
from .auditlog import log_action
|
|
1721
|
+
log_action(role=getattr(args, "role", None) or __import__("os").environ.get("NEDVIGA_ROLE", "agent"),
|
|
1722
|
+
command="litcheck", card_id=card_id, rc=0, reason="third_party_query",
|
|
1723
|
+
argv=[], pii=False)
|
|
1724
|
+
except Exception:
|
|
1725
|
+
pass
|
|
1726
|
+
return 0
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _cmd_eco(args: argparse.Namespace) -> int:
|
|
1730
|
+
from .eco import eco_for_card, render_text
|
|
1731
|
+
|
|
1732
|
+
path = getattr(args, "file", None)
|
|
1733
|
+
if not path:
|
|
1734
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1735
|
+
data={"message": "missing --file"},
|
|
1736
|
+
metadata=_privacy_metadata({"engine": "eco", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1737
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1738
|
+
return USAGE_ERROR_EXIT
|
|
1739
|
+
try:
|
|
1740
|
+
import json as _j
|
|
1741
|
+
card = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
1742
|
+
except Exception as exc:
|
|
1743
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1744
|
+
data={"path": path},
|
|
1745
|
+
metadata=_privacy_metadata({"engine": "eco", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1746
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1747
|
+
return 3
|
|
1748
|
+
if isinstance(card, list):
|
|
1749
|
+
card = card[0] if card else {}
|
|
1750
|
+
if isinstance(card, dict) and "cards" in card and isinstance(card["cards"], list):
|
|
1751
|
+
card = card["cards"][0] if card["cards"] else {}
|
|
1752
|
+
offline = getattr(args, "offline", None)
|
|
1753
|
+
result = eco_for_card(card, offline=offline)
|
|
1754
|
+
text = render_text(result)
|
|
1755
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1756
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1757
|
+
if out:
|
|
1758
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1759
|
+
data = {"text": text, "result": result, "output": out}
|
|
1760
|
+
envelope = make_envelope(status="ok", source="eco", data=data,
|
|
1761
|
+
metadata=_privacy_metadata({"engine": "eco", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1762
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1763
|
+
return 0
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def _cmd_ops(args: argparse.Namespace) -> int:
|
|
1767
|
+
from .ops import collect, render_text
|
|
1768
|
+
|
|
1769
|
+
days = getattr(args, "days", 1) or 1
|
|
1770
|
+
precheck = bool(getattr(args, "precheck", False))
|
|
1771
|
+
live = bool(getattr(args, "live", False))
|
|
1772
|
+
data = collect(days=days, precheck=precheck, live=live)
|
|
1773
|
+
fmt = getattr(args, "format", None)
|
|
1774
|
+
json_flag = bool(getattr(args, "json", False)) or getattr(args, "json", None) is not None
|
|
1775
|
+
if json_flag or fmt == "json":
|
|
1776
|
+
text = None
|
|
1777
|
+
else:
|
|
1778
|
+
text = render_text(data)
|
|
1779
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1780
|
+
if out and text is not None:
|
|
1781
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1782
|
+
envelope = make_envelope(status="ok", source="ops", data=data if text is None else {"text": text, "data": data},
|
|
1783
|
+
metadata=_privacy_metadata({"engine": "ops", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1784
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1785
|
+
return 0
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def _cmd_mcp(args: argparse.Namespace) -> int:
|
|
1789
|
+
from .mcp_server import serve_stdio
|
|
1790
|
+
action = getattr(args, "mcp_action", None)
|
|
1791
|
+
if action == "serve":
|
|
1792
|
+
serve_stdio()
|
|
1793
|
+
return 0
|
|
1794
|
+
# default serve
|
|
1795
|
+
serve_stdio()
|
|
1796
|
+
return 0
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
def _cmd_mos(args: argparse.Namespace) -> int:
|
|
1800
|
+
from .mosdata import search_datasets, dataset_versions, rows
|
|
1801
|
+
import json as _j
|
|
1802
|
+
|
|
1803
|
+
action = getattr(args, "mos_action", None)
|
|
1804
|
+
if action == "search-datasets":
|
|
1805
|
+
query = getattr(args, "query", None)
|
|
1806
|
+
try:
|
|
1807
|
+
res = search_datasets(query)
|
|
1808
|
+
except Exception as exc:
|
|
1809
|
+
envelope = make_envelope(status="error", reason=str(exc),
|
|
1810
|
+
data={"message": str(exc)},
|
|
1811
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1812
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1813
|
+
return 3
|
|
1814
|
+
# save discovery
|
|
1815
|
+
try:
|
|
1816
|
+
from .mosdata import save_discovery
|
|
1817
|
+
save_discovery(res)
|
|
1818
|
+
except Exception:
|
|
1819
|
+
pass
|
|
1820
|
+
envelope = make_envelope(status="ok", source="mos", data=res,
|
|
1821
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1822
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1823
|
+
return 0
|
|
1824
|
+
if action == "dataset-versions":
|
|
1825
|
+
did = getattr(args, "dataset_id", None)
|
|
1826
|
+
try:
|
|
1827
|
+
res = dataset_versions(did)
|
|
1828
|
+
except Exception as exc:
|
|
1829
|
+
envelope = make_envelope(status="error", reason=str(exc),
|
|
1830
|
+
data={"message": str(exc)},
|
|
1831
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1832
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1833
|
+
return 3
|
|
1834
|
+
envelope = make_envelope(status="ok", source="mos", data=res,
|
|
1835
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1836
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1837
|
+
return 0
|
|
1838
|
+
if action == "rows":
|
|
1839
|
+
did = getattr(args, "dataset_id", None)
|
|
1840
|
+
version = getattr(args, "version", None)
|
|
1841
|
+
top = getattr(args, "top", 1000)
|
|
1842
|
+
skip = getattr(args, "skip", 0)
|
|
1843
|
+
try:
|
|
1844
|
+
res = rows(did, version=version, top=top, skip=skip)
|
|
1845
|
+
except Exception as exc:
|
|
1846
|
+
envelope = make_envelope(status="error", reason=str(exc),
|
|
1847
|
+
data={"message": str(exc)},
|
|
1848
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1849
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1850
|
+
return 3
|
|
1851
|
+
envelope = make_envelope(status="ok", source="mos", data=res,
|
|
1852
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1853
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1854
|
+
return 0
|
|
1855
|
+
if action == "status":
|
|
1856
|
+
# token present?
|
|
1857
|
+
tok = None
|
|
1858
|
+
try:
|
|
1859
|
+
from .mosdata import _token
|
|
1860
|
+
tok = _token()
|
|
1861
|
+
except Exception:
|
|
1862
|
+
tok = None
|
|
1863
|
+
data = {"token_present": tok is not None, "proxy": __import__("os").environ.get("NEDVIGA_EGRESS_PROXY")}
|
|
1864
|
+
envelope = make_envelope(status="ok", source="mos", data=data,
|
|
1865
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1866
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1867
|
+
return 0
|
|
1868
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1869
|
+
data={"message": "missing mos subcommand"},
|
|
1870
|
+
metadata=_privacy_metadata({"engine": "mos", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1871
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1872
|
+
return USAGE_ERROR_EXIT
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
def _cmd_dossier(args: argparse.Namespace) -> int:
|
|
1876
|
+
from .dossier import sync, match, db_path
|
|
1877
|
+
|
|
1878
|
+
action = getattr(args, "dossier_action", None)
|
|
1879
|
+
if action == "sync":
|
|
1880
|
+
limit = getattr(args, "limit", 1000) or 1000
|
|
1881
|
+
dataset = getattr(args, "dataset", "auto") or "auto"
|
|
1882
|
+
res = sync(dataset=dataset, limit=limit)
|
|
1883
|
+
envelope = make_envelope(status="ok", source="dossier", data=res,
|
|
1884
|
+
metadata=_privacy_metadata({"engine": "dossier", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1885
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1886
|
+
return 0
|
|
1887
|
+
if action == "match":
|
|
1888
|
+
file = getattr(args, "file", None)
|
|
1889
|
+
address = getattr(args, "address", None)
|
|
1890
|
+
card = None
|
|
1891
|
+
if file:
|
|
1892
|
+
try:
|
|
1893
|
+
import json as _j
|
|
1894
|
+
raw = _j.loads(Path(file).read_text(encoding="utf-8"))
|
|
1895
|
+
if isinstance(raw, list):
|
|
1896
|
+
raw = raw[0]
|
|
1897
|
+
if isinstance(raw, dict) and "cards" in raw and isinstance(raw["cards"], list):
|
|
1898
|
+
raw = raw["cards"][0]
|
|
1899
|
+
card = raw
|
|
1900
|
+
except Exception as exc:
|
|
1901
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
1902
|
+
data={"path": file},
|
|
1903
|
+
metadata=_privacy_metadata({"engine": "dossier", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1904
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1905
|
+
return 3
|
|
1906
|
+
target = address if address else card
|
|
1907
|
+
res = match(target)
|
|
1908
|
+
envelope = make_envelope(status="ok", source="dossier", data=res,
|
|
1909
|
+
metadata=_privacy_metadata({"engine": "dossier", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1910
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1911
|
+
return 0
|
|
1912
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1913
|
+
data={"message": "missing dossier subcommand"},
|
|
1914
|
+
metadata=_privacy_metadata({"engine": "dossier", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1915
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1916
|
+
return USAGE_ERROR_EXIT
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def _cmd_developer(args: argparse.Namespace) -> int:
|
|
1920
|
+
from .developer import search_by_inn, search_by_name, _from_file, litcheck_developer, render_text
|
|
1921
|
+
|
|
1922
|
+
inn = getattr(args, "inn", None)
|
|
1923
|
+
name = getattr(args, "name", None)
|
|
1924
|
+
from_file = getattr(args, "from_file", None)
|
|
1925
|
+
litcheck_flag = bool(getattr(args, "litcheck", False))
|
|
1926
|
+
|
|
1927
|
+
if from_file:
|
|
1928
|
+
res = _from_file(from_file)
|
|
1929
|
+
text = json.dumps(res, ensure_ascii=False, indent=2)
|
|
1930
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1931
|
+
if out:
|
|
1932
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1933
|
+
envelope = make_envelope(status="ok", source="developer", data=res,
|
|
1934
|
+
metadata=_privacy_metadata({"engine": "developer", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1935
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1936
|
+
return 0
|
|
1937
|
+
|
|
1938
|
+
if inn:
|
|
1939
|
+
res = search_by_inn(inn)
|
|
1940
|
+
elif name:
|
|
1941
|
+
res = search_by_name(name)
|
|
1942
|
+
else:
|
|
1943
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1944
|
+
data={"message": "missing --inn/--name/--from-file"},
|
|
1945
|
+
metadata=_privacy_metadata({"engine": "developer", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1946
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1947
|
+
return USAGE_ERROR_EXIT
|
|
1948
|
+
|
|
1949
|
+
lit = None
|
|
1950
|
+
if litcheck_flag:
|
|
1951
|
+
q = name or inn or ""
|
|
1952
|
+
try:
|
|
1953
|
+
lit = litcheck_developer(q)
|
|
1954
|
+
except Exception as exc:
|
|
1955
|
+
lit = {"status": "unavailable", "reason": str(exc)}
|
|
1956
|
+
text = render_text(res, lit=lit)
|
|
1957
|
+
# v0.5.10 контроль качества (live): тотальный sanitize жрал номера разрешений
|
|
1958
|
+
# застройки ("77-214000-019345-2021" → маска телефона). Реестр 2114 — публичные
|
|
1959
|
+
# юр-сведения, телефонных слотов нет по построению; маскирование здесь = порча данных.
|
|
1960
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1961
|
+
if out:
|
|
1962
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1963
|
+
data = {"text": text, "result": res, "litcheck": lit, "output": out}
|
|
1964
|
+
envelope = make_envelope(status="ok", source="developer", data=data,
|
|
1965
|
+
metadata=_privacy_metadata({"engine": "developer", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1966
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1967
|
+
return 0
|
|
1968
|
+
|
|
1969
|
+
|
|
1970
|
+
def _cmd_noise(args: argparse.Namespace) -> int:
|
|
1971
|
+
from .noise import noise_for_card, render_text, sync
|
|
1972
|
+
|
|
1973
|
+
action = getattr(args, "noise_action", None)
|
|
1974
|
+
if action == "sync":
|
|
1975
|
+
limit = getattr(args, "limit", 1000) or 1000
|
|
1976
|
+
res = sync(limit=limit)
|
|
1977
|
+
envelope = make_envelope(status="ok", source="noise", data=res,
|
|
1978
|
+
metadata=_privacy_metadata({"engine": "noise", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1979
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1980
|
+
return 0
|
|
1981
|
+
path = getattr(args, "file", None)
|
|
1982
|
+
if not path:
|
|
1983
|
+
# allow lat/lon
|
|
1984
|
+
lat = getattr(args, "lat", None)
|
|
1985
|
+
lon = getattr(args, "lon", None)
|
|
1986
|
+
if lat is not None and lon is not None:
|
|
1987
|
+
res = noise_for_card({"geo": {"lat": lat, "lon": lon}}, radius_m=getattr(args, "radius_m", 1500) or 1500)
|
|
1988
|
+
text = render_text(res)
|
|
1989
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
1990
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
1991
|
+
if out:
|
|
1992
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
1993
|
+
envelope = make_envelope(status="ok", source="noise", data={"text": text, "result": res, "output": out},
|
|
1994
|
+
metadata=_privacy_metadata({"engine": "noise", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
1995
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
1996
|
+
return 0
|
|
1997
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
1998
|
+
data={"message": "missing --file or --lat/--lon"},
|
|
1999
|
+
metadata=_privacy_metadata({"engine": "noise", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2000
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2001
|
+
return USAGE_ERROR_EXIT
|
|
2002
|
+
try:
|
|
2003
|
+
import json as _j
|
|
2004
|
+
card = _j.loads(Path(path).read_text(encoding="utf-8"))
|
|
2005
|
+
except Exception as exc:
|
|
2006
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {exc}",
|
|
2007
|
+
data={"path": path},
|
|
2008
|
+
metadata=_privacy_metadata({"engine": "noise", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2009
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2010
|
+
return 3
|
|
2011
|
+
if isinstance(card, list):
|
|
2012
|
+
card = card[0] if card else {}
|
|
2013
|
+
if isinstance(card, dict) and "cards" in card and isinstance(card["cards"], list):
|
|
2014
|
+
card = card["cards"][0] if card["cards"] else {}
|
|
2015
|
+
radius = getattr(args, "radius_m", 1500) or 1500
|
|
2016
|
+
res = noise_for_card(card, radius_m=radius)
|
|
2017
|
+
text = render_text(res)
|
|
2018
|
+
text = sanitize_contacts(text) if isinstance(text, str) else text
|
|
2019
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
2020
|
+
if out:
|
|
2021
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
2022
|
+
data = {"text": text, "result": res, "output": out}
|
|
2023
|
+
envelope = make_envelope(status="ok", source="noise", data=data,
|
|
2024
|
+
metadata=_privacy_metadata({"engine": "noise", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2025
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2026
|
+
return 0
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
def _cmd_wizard(args: argparse.Namespace) -> int:
|
|
2030
|
+
from .wizard import init, doctor, render_text
|
|
2031
|
+
|
|
2032
|
+
action = getattr(args, "wizard_action", None)
|
|
2033
|
+
if action == "init":
|
|
2034
|
+
profile = getattr(args, "profile", "agent") or "agent"
|
|
2035
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
2036
|
+
res = init(profile=profile, out=out)
|
|
2037
|
+
envelope = make_envelope(status="ok", source="wizard", data=res,
|
|
2038
|
+
metadata=_privacy_metadata({"engine": "wizard", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2039
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2040
|
+
return 0
|
|
2041
|
+
if action == "doctor":
|
|
2042
|
+
json_flag = bool(getattr(args, "json", False)) or getattr(args, "json", None) is not None
|
|
2043
|
+
res = doctor(json_out=json_flag)
|
|
2044
|
+
if json_flag:
|
|
2045
|
+
envelope = make_envelope(status="ok" if res["ok"] else "error", source="wizard", data=res,
|
|
2046
|
+
metadata=_privacy_metadata({"engine": "wizard", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2047
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2048
|
+
return 0 if res["ok"] else 1
|
|
2049
|
+
text = render_text(res)
|
|
2050
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
2051
|
+
if out:
|
|
2052
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
2053
|
+
# print text directly? Use envelope data text
|
|
2054
|
+
envelope = make_envelope(status="ok" if res["ok"] else "error", source="wizard", data={"text": text, "result": res},
|
|
2055
|
+
metadata=_privacy_metadata({"engine": "wizard", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2056
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2057
|
+
return 0 if res["ok"] else 1
|
|
2058
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
2059
|
+
data={"message": "missing wizard subcommand"},
|
|
2060
|
+
metadata=_privacy_metadata({"engine": "wizard", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2061
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2062
|
+
return USAGE_ERROR_EXIT
|
|
2063
|
+
|
|
2064
|
+
|
|
2065
|
+
def _cmd_avito(args: argparse.Namespace) -> int:
|
|
2066
|
+
from .sources_avito import parse_livetext, browser_search
|
|
2067
|
+
import json as _j
|
|
2068
|
+
|
|
2069
|
+
action = getattr(args, "avito_action", None)
|
|
2070
|
+
if action == "parse":
|
|
2071
|
+
path = getattr(args, "file", None)
|
|
2072
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
2073
|
+
cards = parse_livetext(text)
|
|
2074
|
+
# captcha check
|
|
2075
|
+
from .sources_avito import BLOCK_MARKERS as _BM
|
|
2076
|
+
if any(m.lower() in text.lower() for m in _BM):
|
|
2077
|
+
envelope = make_envelope(status="error", reason="source_blocked",
|
|
2078
|
+
data={"message": "captcha detected"},
|
|
2079
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2080
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2081
|
+
return 3
|
|
2082
|
+
cards = sanitize_contacts(cards)
|
|
2083
|
+
out = getattr(args, "out", None) or getattr(args, "output", None)
|
|
2084
|
+
if out:
|
|
2085
|
+
Path(out).write_text(_j.dumps(cards, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
2086
|
+
envelope = make_envelope(status="ok", source="avito", data=cards,
|
|
2087
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2088
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2089
|
+
return 0
|
|
2090
|
+
envelope = make_envelope(status="error", reason="usage_error",
|
|
2091
|
+
data={"message": "missing avito subcommand"},
|
|
2092
|
+
metadata=_privacy_metadata({"engine": "avito", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2093
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2094
|
+
return USAGE_ERROR_EXIT
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
2098
|
+
vs = getattr(args, "verify_signature", None)
|
|
2099
|
+
if vs:
|
|
2100
|
+
from .doctor import verify_signature
|
|
2101
|
+
res = verify_signature(vs)
|
|
2102
|
+
code = 0 if res["status"] == "ok" else 3
|
|
2103
|
+
envelope = make_envelope(status="ok" if res["status"] == "ok" else "error",
|
|
2104
|
+
reason=res.get("reason") or res["status"], source="doctor", data=res,
|
|
2105
|
+
metadata=_privacy_metadata({"engine": "doctor", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2106
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2107
|
+
return code
|
|
2108
|
+
envelope, exit_code = run_doctor()
|
|
2109
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2110
|
+
return exit_code
|
|
2111
|
+
|
|
2112
|
+
|
|
2113
|
+
def _cmd_import(args: argparse.Namespace) -> int:
|
|
2114
|
+
path = getattr(args, "path", None)
|
|
2115
|
+
if not path:
|
|
2116
|
+
envelope = make_envelope(
|
|
2117
|
+
status="error",
|
|
2118
|
+
reason="usage_error",
|
|
2119
|
+
data={"message": "missing HTML file path"},
|
|
2120
|
+
metadata={"engine": "cian_card_parser", "cached": True, "elapsed_seconds": 0.0},
|
|
2121
|
+
)
|
|
2122
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2123
|
+
return USAGE_ERROR_EXIT
|
|
2124
|
+
|
|
2125
|
+
try:
|
|
2126
|
+
html = Path(path).read_text(encoding="utf-8")
|
|
2127
|
+
except FileNotFoundError:
|
|
2128
|
+
envelope = make_envelope(
|
|
2129
|
+
status="error",
|
|
2130
|
+
reason="file_not_found",
|
|
2131
|
+
data={"path": path},
|
|
2132
|
+
metadata={"engine": "cian_card_parser", "cached": True, "elapsed_seconds": 0.0},
|
|
2133
|
+
)
|
|
2134
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2135
|
+
return 3
|
|
2136
|
+
except Exception as exc: # pragma: no cover - hard to trigger deterministically
|
|
2137
|
+
envelope = make_envelope(
|
|
2138
|
+
status="error",
|
|
2139
|
+
reason=f"read_error: {exc}",
|
|
2140
|
+
data={"path": path},
|
|
2141
|
+
metadata={"engine": "cian_card_parser", "cached": True, "elapsed_seconds": 0.0},
|
|
2142
|
+
)
|
|
2143
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2144
|
+
return 3
|
|
2145
|
+
|
|
2146
|
+
try:
|
|
2147
|
+
card = parse_card(html)
|
|
2148
|
+
except ParseError as exc:
|
|
2149
|
+
envelope = make_envelope(
|
|
2150
|
+
status="error",
|
|
2151
|
+
reason=f"parse_error: {exc}",
|
|
2152
|
+
source="cian",
|
|
2153
|
+
data={"path": path},
|
|
2154
|
+
metadata={"engine": "cian_card_parser", "cached": True, "elapsed_seconds": 0.0},
|
|
2155
|
+
)
|
|
2156
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2157
|
+
return 3
|
|
2158
|
+
|
|
2159
|
+
walk_note = _embed_walk(card, args) if getattr(args, "with_walk", False) else None
|
|
2160
|
+
|
|
2161
|
+
envelope = make_envelope(
|
|
2162
|
+
status="ok",
|
|
2163
|
+
source="cian",
|
|
2164
|
+
data=card.model_dump(mode="json", exclude_none=False),
|
|
2165
|
+
metadata={"engine": "cian_card_parser", "cached": True, "elapsed_seconds": 0.0,
|
|
2166
|
+
"with_walk": walk_note},
|
|
2167
|
+
)
|
|
2168
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2169
|
+
return 0
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
def _cmd_walk_distance(args: argparse.Namespace) -> int:
|
|
2173
|
+
route = walk_route(args.lat, args.lon, args.metro)
|
|
2174
|
+
|
|
2175
|
+
if route.get("reason") == "STATION_NOT_IN_DIRECTORY":
|
|
2176
|
+
envelope = make_envelope(
|
|
2177
|
+
status="degraded",
|
|
2178
|
+
reason="STATION_NOT_IN_DIRECTORY",
|
|
2179
|
+
source="geo_routing",
|
|
2180
|
+
data=route,
|
|
2181
|
+
metadata={"engine": "geo_routing", "cached": False, "elapsed_seconds": 0.0},
|
|
2182
|
+
)
|
|
2183
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2184
|
+
return STATUS_EXIT_CODES["degraded"]
|
|
2185
|
+
|
|
2186
|
+
data: dict = {"honest_walk_route": route}
|
|
2187
|
+
claimed = getattr(args, "claimed_time", None)
|
|
2188
|
+
if claimed is not None:
|
|
2189
|
+
data["honest_overclaim"] = honest_overclaim(
|
|
2190
|
+
route["walk_duration_minutes"], claimed
|
|
2191
|
+
)
|
|
2192
|
+
|
|
2193
|
+
envelope = make_envelope(
|
|
2194
|
+
status="ok",
|
|
2195
|
+
source="geo_routing",
|
|
2196
|
+
data=data,
|
|
2197
|
+
metadata={"engine": "geo_routing", "cached": False, "elapsed_seconds": 0.0},
|
|
2198
|
+
)
|
|
2199
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2200
|
+
return 0
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
def _cmd_domclick(args) -> int:
|
|
2204
|
+
from .sources_domclick import parse_domclick_text
|
|
2205
|
+
from .pii import sanitize_contacts
|
|
2206
|
+
import json as _j
|
|
2207
|
+
text = Path(args.file).read_text(encoding="utf-8")
|
|
2208
|
+
cards = parse_domclick_text(text)
|
|
2209
|
+
data = sanitize_contacts(cards)
|
|
2210
|
+
envelope = make_envelope(status="ok", source="domclick", data=data,
|
|
2211
|
+
metadata=_privacy_metadata({"engine": "domclick", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2212
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2213
|
+
return 0
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
def _cmd_rush(args) -> int:
|
|
2217
|
+
"""v0.5.13 F-15t: транспортная доступность + текущие пробки (только RU-egress)."""
|
|
2218
|
+
from .rush import rush_report
|
|
2219
|
+
import json as _j
|
|
2220
|
+
card = {}
|
|
2221
|
+
if getattr(args, "file", None):
|
|
2222
|
+
try:
|
|
2223
|
+
card = _j.loads(Path(args.file).read_text(encoding="utf-8"))
|
|
2224
|
+
except Exception:
|
|
2225
|
+
card = {}
|
|
2226
|
+
if isinstance(card, list):
|
|
2227
|
+
card = card[0] if card else {}
|
|
2228
|
+
res = rush_report(card if isinstance(card, dict) else {})
|
|
2229
|
+
lines = [f"Транспортная доступность: {res['honest_note']}"]
|
|
2230
|
+
w = res.get("walk") or {}
|
|
2231
|
+
if w.get("status") == "ok":
|
|
2232
|
+
lines.append(f"Метро пешком: ~{w.get('walk_minutes')} мин ({w.get('distance_m')} м) [walk]")
|
|
2233
|
+
else:
|
|
2234
|
+
lines.append(f"Метро пешком: walk — {w.get('status')}")
|
|
2235
|
+
tr = res.get("traffic") or {}
|
|
2236
|
+
if tr.get("score") is not None:
|
|
2237
|
+
lines.append(f"Пробки сейчас: {tr['score']} баллов, замер {tr.get('measured_at_utc')} UTC")
|
|
2238
|
+
else:
|
|
2239
|
+
lines.append(f"Пробки: unavailable ({tr.get('status')})")
|
|
2240
|
+
# v0.5.14 (the task brief §2): почасовой web-прогноз с той же страницы.
|
|
2241
|
+
# Дисклеймер «web-прогноз Яндекса, не гарантируется» обязателен всякий
|
|
2242
|
+
# раз, когда прогноз показан; не распарсился — пустой список, не угадываем.
|
|
2243
|
+
forecast = tr.get("forecast_hours") or []
|
|
2244
|
+
hour = getattr(args, "hour", None)
|
|
2245
|
+
forecast_hour = None
|
|
2246
|
+
if hour is not None:
|
|
2247
|
+
forecast_hour = next((f for f in forecast if f.get("hour") == hour), None)
|
|
2248
|
+
if forecast_hour:
|
|
2249
|
+
lines.append(f"Пробки: прогноз на {hour}: {forecast_hour['balls']} баллов "
|
|
2250
|
+
f"(web-прогноз Яндекса, не гарантируется)")
|
|
2251
|
+
else:
|
|
2252
|
+
avail = ", ".join(f"{f['hour']:02d}" for f in forecast) or "нет"
|
|
2253
|
+
lines.append(f"Пробки: web-прогноз на {hour} ч недоступен "
|
|
2254
|
+
f"(web-прогноз Яндекса, не гарантируется; доступные часы: {avail})")
|
|
2255
|
+
elif forecast:
|
|
2256
|
+
lines.append("Пробки: почасовой прогноз (web-прогноз Яндекса, не гарантируется):")
|
|
2257
|
+
for f in forecast:
|
|
2258
|
+
lines.append(f" {f['hour']:02d}:00 — {f['balls']} баллов")
|
|
2259
|
+
text = "\n".join(lines)
|
|
2260
|
+
data = {"text": text, "result": res, "walk_minutes": w.get("walk_minutes")}
|
|
2261
|
+
if hour is not None:
|
|
2262
|
+
data["forecast_hour"] = forecast_hour
|
|
2263
|
+
envelope = make_envelope(status="ok", source="rush", data=data,
|
|
2264
|
+
metadata=_privacy_metadata({"engine": "rush", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2265
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2266
|
+
return 0
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _cmd_deck(args) -> int:
|
|
2270
|
+
"""v0.5.13 F-33: презентация из карточек (--file cards.json | --project P)."""
|
|
2271
|
+
from .deck import deck_from_cards
|
|
2272
|
+
import json as _j
|
|
2273
|
+
cards: list = []
|
|
2274
|
+
if getattr(args, "file", None):
|
|
2275
|
+
try:
|
|
2276
|
+
raw = _j.loads(Path(args.file).read_text(encoding="utf-8"))
|
|
2277
|
+
if isinstance(raw, dict) and "cards" in raw:
|
|
2278
|
+
cards = raw["cards"]
|
|
2279
|
+
elif isinstance(raw, dict):
|
|
2280
|
+
cards = raw.get("data", {}).get("cards") or [raw]
|
|
2281
|
+
elif isinstance(raw, list):
|
|
2282
|
+
cards = raw
|
|
2283
|
+
except Exception as exc:
|
|
2284
|
+
envelope = make_envelope(status="error", reason=f"invalid_input: {type(exc).__name__}",
|
|
2285
|
+
metadata=_privacy_metadata({"engine": "deck"}, args))
|
|
2286
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2287
|
+
return 2
|
|
2288
|
+
elif getattr(args, "project", None):
|
|
2289
|
+
from .projects import project_cards
|
|
2290
|
+
try:
|
|
2291
|
+
cards = list(project_cards(args.project) or [])
|
|
2292
|
+
except Exception as exc:
|
|
2293
|
+
envelope = make_envelope(status="error", reason=f"project_unavailable: {type(exc).__name__}",
|
|
2294
|
+
metadata=_privacy_metadata({"engine": "deck"}, args))
|
|
2295
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2296
|
+
return 2
|
|
2297
|
+
else:
|
|
2298
|
+
envelope = make_envelope(status="error", reason="invalid_input: нужен --file или --project",
|
|
2299
|
+
metadata=_privacy_metadata({"engine": "deck"}, args))
|
|
2300
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2301
|
+
return 2
|
|
2302
|
+
out = getattr(args, "out", None) or "deck.pptx"
|
|
2303
|
+
title = getattr(args, "title", None) or "Подборка недвижимости"
|
|
2304
|
+
try:
|
|
2305
|
+
res = deck_from_cards(cards, out, title=title)
|
|
2306
|
+
except Exception as exc:
|
|
2307
|
+
envelope = make_envelope(status="error", reason=f"deck_failed: {type(exc).__name__}",
|
|
2308
|
+
metadata=_privacy_metadata({"engine": "deck"}, args))
|
|
2309
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2310
|
+
return 3
|
|
2311
|
+
envelope = make_envelope(status="ok", source="deck", data={"result": res,
|
|
2312
|
+
"text": f"PPTX: {res['out']}, слайдов {res['slides']}, карточек {res['cards']} (cap 15)"},
|
|
2313
|
+
metadata=_privacy_metadata({"engine": "deck", "cached": False, "elapsed_seconds": 0.0}, args))
|
|
2314
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
2315
|
+
return 0
|
|
2316
|
+
|
|
2317
|
+
|
|
2318
|
+
_EXAMPLES = {
|
|
2319
|
+
"search": 'пример: nedviga-cli search --geo-districts ЮАО --price-max 20000000 --out cards.json',
|
|
2320
|
+
"get-card": 'пример: nedviga-cli get-card cian-333714893 --json cards.json --purpose "прозвон" --with-contacts',
|
|
2321
|
+
"card": 'пример: nedviga-cli card cards.json --format all --out ./out',
|
|
2322
|
+
"export": 'пример: nedviga-cli export --input cards.json --output katalog.html',
|
|
2323
|
+
"pipeline": 'пример: nedviga-cli pipeline set cian-1 --status shortlist; pipeline list',
|
|
2324
|
+
"watch": 'пример: nedviga-cli watch add --file cards.json; watch check --offline fresh.json',
|
|
2325
|
+
"digest": 'пример: nedviga-cli digest --days 7 --format telegram',
|
|
2326
|
+
"compare": 'пример: nedviga-cli compare --file cards.json --format telegram',
|
|
2327
|
+
"bargain": 'пример: nedviga-cli bargain --file target.json --corpus corpus.json --by-complex',
|
|
2328
|
+
"risk": 'пример: nedviga-cli risk --file card.json',
|
|
2329
|
+
"triage": 'пример: nedviga-cli triage --file card.json',
|
|
2330
|
+
"checklist": 'пример: nedviga-cli checklist new cian-1 --file cards.json; checklist handoff cian-1 --to colleague',
|
|
2331
|
+
"callsheet": 'пример: nedviga-cli callsheet --file cards.json',
|
|
2332
|
+
"project": 'пример: nedviga-cli project new "ЮАО" --query "{}"; project refresh P1 --file new.json',
|
|
2333
|
+
"dedup": 'пример: nedviga-cli dedup --file cards.json',
|
|
2334
|
+
"tour": 'пример: nedviga-cli tour --project P1 --all --format telegram',
|
|
2335
|
+
"poi": 'пример: nedviga-cli poi --file card.json --radius-min 15',
|
|
2336
|
+
"market": 'пример: nedviga-cli market --corpus f1.json,f2.json --group-by complex',
|
|
2337
|
+
"stale": 'пример: nedviga-cli stale --file cards.json --project P1',
|
|
2338
|
+
"catalog-pdf": 'пример: nedviga-cli catalog-pdf --file cards.json --out katalog.pdf',
|
|
2339
|
+
"auto": 'пример: nedviga-cli auto run --file corpus.json; auto report --days 7',
|
|
2340
|
+
"litcheck": 'пример: nedviga-cli litcheck --address "ул. Кантемировская, 20" [--developer "ПИК"]',
|
|
2341
|
+
"eco": 'пример: nedviga-cli eco --file card.json',
|
|
2342
|
+
"mos": 'пример: nedviga-cli mos status (env NEDVIGA_EGRESS_PROXY, NEDVIGA_MOS_TOKEN_FILE)',
|
|
2343
|
+
"dossier": 'пример: nedviga-cli dossier sync --dataset 3293; dossier match --address "улица Азовская, дом 4"',
|
|
2344
|
+
"developer": 'пример: nedviga-cli developer --inn 770123456789 --litcheck',
|
|
2345
|
+
"noise": 'пример: nedviga-cli noise sync; noise --lat 55.72 --lon 37.61 --radius-m 1500',
|
|
2346
|
+
"avito": 'пример: nedviga-cli avito parse --file livetext.txt; search --source avito --catalog URL --live',
|
|
2347
|
+
"source": 'пример: nedviga-cli source (лицензия, репо, как получить исходники)',
|
|
2348
|
+
"domclick": 'пример: nedviga-cli domclick parse --file dc_text.txt; search --source domclick --catalog URL --live',
|
|
2349
|
+
"rush": 'пример: nedviga-cli rush --file card.json [--hour 18] (балл пробок и почасовой web-прогноз только с NEDVIGA_EGRESS_PROXY)',
|
|
2350
|
+
"deck": 'пример: nedviga-cli deck --file cards.json --out подборока.pptx [--title "ЮАО"]',
|
|
2351
|
+
"sources": 'пример: nedviga-cli sources list',
|
|
2352
|
+
"text": 'пример: nedviga-cli text index --file cards.json; text query "узакон*"',
|
|
2353
|
+
"feedback": 'пример: nedviga-cli feedback add cian-1 --verdict liked --reason "район"; feedback profile',
|
|
2354
|
+
"chain": 'пример: nedviga-cli chain new cian-1 --kind alt_unfound; chain show cian-1',
|
|
2355
|
+
"scorefit": 'пример: nedviga-cli scorefit --file cards.json --profile P1',
|
|
2356
|
+
"wizard": 'пример: nedviga-cli wizard init --profile family --out ./kit; wizard doctor',
|
|
2357
|
+
"ops": 'пример: nedviga-cli ops; ops --precheck --live', "import": 'пример: nedviga-cli import --file export.html --format cian-html',
|
|
2358
|
+
"walk-distance": 'пример: nedviga-cli walk-distance --from "м. Царицыно" --to card.json',
|
|
2359
|
+
"audit": 'пример: nedviga-cli audit --days 7 --role agent',
|
|
2360
|
+
"pii-log": 'пример: nedviga-cli pii-log --days 30',
|
|
2361
|
+
"purge": 'пример: nedviga-cli purge card cian-1 (dry); purge card cian-1 --yes',
|
|
2362
|
+
"mcp": 'пример: nedviga-cli mcp serve (stdio JSON-RPC, контакты закрыты принципиально)',
|
|
2363
|
+
"doctor": 'пример: nedviga-cli doctor; doctor --verify-signature <файл.whl>',
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
def _annotate_examples(subparsers) -> None:
|
|
2368
|
+
"""v0.5.11 контроль: каждой подкоманде — пример в help (epilog)."""
|
|
2369
|
+
name_map = getattr(subparsers, "_name_parser_map", {}) or {}
|
|
2370
|
+
for name, parser in name_map.items():
|
|
2371
|
+
ex = _EXAMPLES.get(name)
|
|
2372
|
+
if ex and "пример:" not in ((parser.description or "") + (parser.epilog or "")):
|
|
2373
|
+
parser.epilog = ((parser.epilog + "\n") if parser.epilog else "") + ex
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
_subparsers_ref = []
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
def _build_parser() -> _NedvigaArgumentParser:
|
|
2380
|
+
parser = _NedvigaArgumentParser(
|
|
2381
|
+
prog="nedviga-cli",
|
|
2382
|
+
description="Moscow secondary real estate CLI/SDK",
|
|
2383
|
+
)
|
|
2384
|
+
parser.add_argument(
|
|
2385
|
+
"--version",
|
|
2386
|
+
action="version",
|
|
2387
|
+
version=f"%(prog)s {_package_version()}",
|
|
2388
|
+
)
|
|
2389
|
+
parser.add_argument(
|
|
2390
|
+
"--no-spotlight",
|
|
2391
|
+
action="store_true",
|
|
2392
|
+
help="Не оборачивать внешний текстовый контент карточек в spotlight-маркеры",
|
|
2393
|
+
)
|
|
2394
|
+
|
|
2395
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
2396
|
+
# search
|
|
2397
|
+
search = sub.add_parser("search", help="Search listings")
|
|
2398
|
+
search.add_argument("--no-spotlight", action="store_true",
|
|
2399
|
+
help="Не оборачивать внешний текст карточек в spotlight-маркеры")
|
|
2400
|
+
search.add_argument("--source", default=None)
|
|
2401
|
+
search.add_argument("--deal-type", default=None)
|
|
2402
|
+
search.add_argument("--rooms", type=int, default=None)
|
|
2403
|
+
search.add_argument("--price-min", type=float, default=None)
|
|
2404
|
+
search.add_argument("--price-max", type=float, default=None)
|
|
2405
|
+
search.add_argument("--kitchen-min", type=float, default=None)
|
|
2406
|
+
search.add_argument("--floor-not-first", action="store_true")
|
|
2407
|
+
search.add_argument("--min-house-floors", type=int, default=None)
|
|
2408
|
+
search.add_argument("--page", type=int, default=1, help="Fetch a specific result page (default 1)")
|
|
2409
|
+
search.add_argument("--pages", type=int, default=None, help="Fetch pages 1..N sequentially")
|
|
2410
|
+
search.add_argument("--metro-walk-max", type=int, default=None, help="Server-side max walking time to metro, minutes")
|
|
2411
|
+
search.add_argument("--hydrate", action="store_true", help="Fetch full cards for each search result")
|
|
2412
|
+
search.add_argument("--no-auction", action="store_true", help="Exclude auction (torgi) listings")
|
|
2413
|
+
search.add_argument("--only-auction", action="store_true", help="Keep only auction (torgi) listings")
|
|
2414
|
+
search.add_argument("--workers", type=int, default=4, help="Parallel workers for --hydrate (default 4)")
|
|
2415
|
+
search.add_argument("--geo-districts", default=None)
|
|
2416
|
+
search.add_argument("--limit", type=int, default=None)
|
|
2417
|
+
# v0.4.0 (ТЗ реалтора): боевые фильтры риелтора
|
|
2418
|
+
search.add_argument(
|
|
2419
|
+
"--strict-mkad", action="store_true", dest="strict_mkad",
|
|
2420
|
+
help="Exclude addresses outside strict MKAD (NAO/TAO/ZelAOO and near towns)",
|
|
2421
|
+
)
|
|
2422
|
+
search.add_argument(
|
|
2423
|
+
"--only-flat", action="store_true", dest="only_flat",
|
|
2424
|
+
help="Exclude apartments (апартаменты)",
|
|
2425
|
+
)
|
|
2426
|
+
search.add_argument(
|
|
2427
|
+
"--no-apartments", action="store_true", dest="only_flat",
|
|
2428
|
+
help="Alias for --only-flat",
|
|
2429
|
+
)
|
|
2430
|
+
search.add_argument(
|
|
2431
|
+
"--no-shares", action="store_true", dest="no_shares",
|
|
2432
|
+
help="Exclude flat shares and rooms (доля в квартире)",
|
|
2433
|
+
)
|
|
2434
|
+
search.add_argument(
|
|
2435
|
+
"--no-gas", action="store_true", dest="no_gas",
|
|
2436
|
+
help="Alias for --min-house-floors 14 (no gas in 14+ floor buildings)",
|
|
2437
|
+
)
|
|
2438
|
+
search.add_argument(
|
|
2439
|
+
"--exclude-legal-risks", action="store_true", dest="exclude_legal_risks",
|
|
2440
|
+
help="Exclude listings with legal-risk stop phrases in description",
|
|
2441
|
+
)
|
|
2442
|
+
# v0.5.4 (the task brief) overrides
|
|
2443
|
+
search.add_argument(
|
|
2444
|
+
"--use-overrides", action="store_true", dest="use_overrides",
|
|
2445
|
+
help="Apply learned profile overrides",
|
|
2446
|
+
)
|
|
2447
|
+
# v0.5.3 (the task brief) relax hints
|
|
2448
|
+
search.add_argument(
|
|
2449
|
+
"--suggest-relax", action="store_true", dest="suggest_relax",
|
|
2450
|
+
help="Подсказать смягчение критериев при пустой выдаче",
|
|
2451
|
+
)
|
|
2452
|
+
# v0.5.0 (the task brief) projects
|
|
2453
|
+
search.add_argument(
|
|
2454
|
+
"--project", default=None, dest="project",
|
|
2455
|
+
help="Автоматически добавлять результаты в проект",
|
|
2456
|
+
)
|
|
2457
|
+
# v0.4.6 (the task brief) F-13/F-03
|
|
2458
|
+
search.add_argument(
|
|
2459
|
+
"--purpose", default=None, help="Purpose for PII access",
|
|
2460
|
+
)
|
|
2461
|
+
search.add_argument(
|
|
2462
|
+
"--with-contacts", action="store_true", dest="with_contacts",
|
|
2463
|
+
help="Не маскировать телефоны в выводе (по умолчанию маскируются; "
|
|
2464
|
+
"в metadata ставится contacts_open: true)",
|
|
2465
|
+
)
|
|
2466
|
+
search.add_argument(
|
|
2467
|
+
"--profile", default=None, dest="profile",
|
|
2468
|
+
help="Именованный пресет поиска (parents, lnfnt); явно переданные "
|
|
2469
|
+
"аргументы CLI переопределяют профиль",
|
|
2470
|
+
)
|
|
2471
|
+
search.add_argument(
|
|
2472
|
+
"--json",
|
|
2473
|
+
nargs="?",
|
|
2474
|
+
const="-",
|
|
2475
|
+
default=None,
|
|
2476
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2477
|
+
)
|
|
2478
|
+
search.add_argument("--catalog", default=None, help="Avito catalog URL")
|
|
2479
|
+
search.add_argument("--max", type=int, default=20, help="Max results")
|
|
2480
|
+
search.add_argument("--live", action="store_true", help="Live browser search")
|
|
2481
|
+
search.add_argument("--input", default=None, help="Offline fixture file")
|
|
2482
|
+
search.set_defaults(func=_cmd_search)
|
|
2483
|
+
|
|
2484
|
+
# get-card
|
|
2485
|
+
get_card = sub.add_parser("get-card", help="Fetch a single listing card")
|
|
2486
|
+
get_card.add_argument("--no-spotlight", action="store_true",
|
|
2487
|
+
help="Не оборачивать внешний текст карточки в spotlight-маркеры")
|
|
2488
|
+
get_card.add_argument("target", help="URL or listing ID")
|
|
2489
|
+
get_card.add_argument(
|
|
2490
|
+
"--with-walk",
|
|
2491
|
+
dest="with_walk",
|
|
2492
|
+
action="store_true",
|
|
2493
|
+
help="Compute honest walk route to claimed metro and embed it in the card",
|
|
2494
|
+
)
|
|
2495
|
+
get_card.add_argument(
|
|
2496
|
+
"--with-contacts", action="store_true", dest="with_contacts",
|
|
2497
|
+
help="Не маскировать телефоны в выводе get-card",
|
|
2498
|
+
)
|
|
2499
|
+
get_card.add_argument("--purpose", default=None, help="Purpose for PII access")
|
|
2500
|
+
get_card.add_argument(
|
|
2501
|
+
"--json",
|
|
2502
|
+
nargs="?",
|
|
2503
|
+
const="-",
|
|
2504
|
+
default=None,
|
|
2505
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2506
|
+
)
|
|
2507
|
+
get_card.set_defaults(func=_cmd_get_card)
|
|
2508
|
+
|
|
2509
|
+
# card (F-11 family decision card)
|
|
2510
|
+
card_cmd = sub.add_parser("card", help="Generate family decision card")
|
|
2511
|
+
card_cmd.add_argument("target", help="Path to JSON card file or listing id")
|
|
2512
|
+
card_cmd.add_argument("--format", default="html", choices=["html", "pdf", "telegram", "all"])
|
|
2513
|
+
card_cmd.add_argument("--out", default=None, help="Output directory")
|
|
2514
|
+
card_cmd.add_argument("--output", default=None, help="Output directory (alias)")
|
|
2515
|
+
card_cmd.add_argument("--with-contacts", action="store_true", dest="with_contacts",
|
|
2516
|
+
help="Открыть контакты продавца")
|
|
2517
|
+
card_cmd.add_argument("--purpose", default=None, help="Purpose for PII access")
|
|
2518
|
+
card_cmd.add_argument(
|
|
2519
|
+
"--json",
|
|
2520
|
+
nargs="?",
|
|
2521
|
+
const="-",
|
|
2522
|
+
default=None,
|
|
2523
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2524
|
+
)
|
|
2525
|
+
card_cmd.set_defaults(func=_cmd_card)
|
|
2526
|
+
|
|
2527
|
+
# export (v0.3.4)
|
|
2528
|
+
export_cmd = sub.add_parser("export", help="Export cards to standalone HTML catalog")
|
|
2529
|
+
export_cmd.add_argument("--input", required=True, help="JSON file with cards (list or {cards: [...]})")
|
|
2530
|
+
export_cmd.add_argument("--format", default="html", choices=["html"])
|
|
2531
|
+
export_cmd.add_argument("--embed-photos", dest="embed_photos", action="store_true",
|
|
2532
|
+
help="Embed compressed photos as base64 (offline catalog)")
|
|
2533
|
+
export_cmd.add_argument("--output", default="catalog.html", help="Output HTML file path")
|
|
2534
|
+
# v0.4.6 (the task brief) F-13
|
|
2535
|
+
export_cmd.add_argument(
|
|
2536
|
+
"--with-contacts", action="store_true", dest="with_contacts",
|
|
2537
|
+
help="Не маскировать телефоны в HTML-каталоге (по умолчанию маскируются; "
|
|
2538
|
+
"в metadata ставится contacts_open: true)",
|
|
2539
|
+
)
|
|
2540
|
+
export_cmd.add_argument("--purpose", default=None, help="Purpose for PII access")
|
|
2541
|
+
export_cmd.add_argument(
|
|
2542
|
+
"--json",
|
|
2543
|
+
nargs="?",
|
|
2544
|
+
const="-",
|
|
2545
|
+
default=None,
|
|
2546
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2547
|
+
)
|
|
2548
|
+
export_cmd.set_defaults(func=_cmd_export)
|
|
2549
|
+
|
|
2550
|
+
# import
|
|
2551
|
+
import_cmd = sub.add_parser("import", help="Parse a saved HTML file offline")
|
|
2552
|
+
import_cmd.add_argument("--no-spotlight", action="store_true",
|
|
2553
|
+
help="Не оборачивать внешний текст карточек в spotlight-маркеры")
|
|
2554
|
+
import_cmd.add_argument("path", help="Path to saved HTML file")
|
|
2555
|
+
import_cmd.add_argument(
|
|
2556
|
+
"--with-walk",
|
|
2557
|
+
dest="with_walk",
|
|
2558
|
+
action="store_true",
|
|
2559
|
+
help="Compute honest walk route to claimed metro and embed it in the card",
|
|
2560
|
+
)
|
|
2561
|
+
import_cmd.add_argument(
|
|
2562
|
+
"--json",
|
|
2563
|
+
nargs="?",
|
|
2564
|
+
const="-",
|
|
2565
|
+
default=None,
|
|
2566
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2567
|
+
)
|
|
2568
|
+
import_cmd.set_defaults(func=_cmd_import)
|
|
2569
|
+
|
|
2570
|
+
# walk-distance
|
|
2571
|
+
walk = sub.add_parser("walk-distance", help="Honest walking distance to metro")
|
|
2572
|
+
walk.add_argument("--lat", type=float, required=True)
|
|
2573
|
+
walk.add_argument("--lon", type=float, required=True)
|
|
2574
|
+
walk.add_argument("--metro", required=True)
|
|
2575
|
+
walk.add_argument(
|
|
2576
|
+
"--claimed-time",
|
|
2577
|
+
type=int,
|
|
2578
|
+
default=None,
|
|
2579
|
+
help="Platform-claimed walking time in minutes (optional)",
|
|
2580
|
+
)
|
|
2581
|
+
walk.add_argument(
|
|
2582
|
+
"--json",
|
|
2583
|
+
nargs="?",
|
|
2584
|
+
const="-",
|
|
2585
|
+
default=None,
|
|
2586
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
2587
|
+
)
|
|
2588
|
+
walk.set_defaults(func=_cmd_walk_distance)
|
|
2589
|
+
|
|
2590
|
+
# pipeline (F-04)
|
|
2591
|
+
pipeline = sub.add_parser("pipeline", help="Object funnel pipeline")
|
|
2592
|
+
psub = pipeline.add_subparsers(dest="pipeline_action", required=True)
|
|
2593
|
+
pset = psub.add_parser("set", help="Set pipeline status")
|
|
2594
|
+
pset.add_argument("card_id", help="Card id")
|
|
2595
|
+
pset.add_argument("--status", required=True, help="New status")
|
|
2596
|
+
pset.add_argument("--note", default=None, help="Note")
|
|
2597
|
+
pset.add_argument("--from-file", dest="from_file", default=None, help="JSON file with cards")
|
|
2598
|
+
pset.set_defaults(func=_cmd_pipeline)
|
|
2599
|
+
plist = psub.add_parser("list", help="List pipeline items")
|
|
2600
|
+
plist.add_argument("--status", default=None, help="Filter by status")
|
|
2601
|
+
plist.set_defaults(func=_cmd_pipeline)
|
|
2602
|
+
pshow = psub.add_parser("show", help="Show pipeline history")
|
|
2603
|
+
pshow.add_argument("card_id", help="Card id")
|
|
2604
|
+
pshow.set_defaults(func=_cmd_pipeline)
|
|
2605
|
+
pipeline.set_defaults(func=_cmd_pipeline)
|
|
2606
|
+
|
|
2607
|
+
# watch (F-08)
|
|
2608
|
+
watch = sub.add_parser("watch", help="Monitor listings")
|
|
2609
|
+
wsub = watch.add_subparsers(dest="watch_action", required=True)
|
|
2610
|
+
wadd = wsub.add_parser("add", help="Add snapshot")
|
|
2611
|
+
wadd.add_argument("card_id", nargs="?", default=None, help="Card id")
|
|
2612
|
+
wadd.add_argument("--file", default=None, help="JSON file with cards")
|
|
2613
|
+
wadd.add_argument("--price", type=float, default=None, help="Price snapshot")
|
|
2614
|
+
wadd.set_defaults(func=_cmd_watch)
|
|
2615
|
+
wlist = wsub.add_parser("list", help="List snapshots")
|
|
2616
|
+
wlist.set_defaults(func=_cmd_watch)
|
|
2617
|
+
wrm = wsub.add_parser("rm", help="Remove snapshot")
|
|
2618
|
+
wrm.add_argument("card_id", help="Card id")
|
|
2619
|
+
wrm.set_defaults(func=_cmd_watch)
|
|
2620
|
+
wcheck = wsub.add_parser("check", help="Check for changes")
|
|
2621
|
+
wcheck.add_argument("--offline", default=None, help="Offline fresh export file")
|
|
2622
|
+
wcheck.set_defaults(func=_cmd_watch)
|
|
2623
|
+
watch.set_defaults(func=_cmd_watch)
|
|
2624
|
+
|
|
2625
|
+
# digest (F-14b)
|
|
2626
|
+
digest = sub.add_parser("digest", help="Family digest")
|
|
2627
|
+
digest.add_argument("--days", type=int, default=7, help="Period days")
|
|
2628
|
+
digest.add_argument("--format", default="telegram", choices=["telegram", "md"])
|
|
2629
|
+
digest.add_argument("--out", default=None, help="Output file")
|
|
2630
|
+
digest.add_argument("--output", default=None, help="Output file (alias)")
|
|
2631
|
+
digest.set_defaults(func=_cmd_digest)
|
|
2632
|
+
|
|
2633
|
+
# compare (F-12)
|
|
2634
|
+
compare = sub.add_parser("compare", help="Compare listings")
|
|
2635
|
+
compare.add_argument("--file", required=True, help="JSON file with cards")
|
|
2636
|
+
compare.add_argument("--ids", default=None, help="Comma-separated ids")
|
|
2637
|
+
compare.add_argument("--format", default="md", choices=["md", "telegram", "html"])
|
|
2638
|
+
compare.add_argument("--by-fit", action="store_true", dest="by_fit", help="Sort by fit score")
|
|
2639
|
+
compare.add_argument("--out", default=None, help="Output file")
|
|
2640
|
+
compare.add_argument("--output", default=None, help="Output file (alias)")
|
|
2641
|
+
compare.set_defaults(func=_cmd_compare)
|
|
2642
|
+
|
|
2643
|
+
# bargain (F-10b)
|
|
2644
|
+
bargain = sub.add_parser("bargain", help="Bargain analysis")
|
|
2645
|
+
bargain.add_argument("--file", required=True, help="Target card JSON")
|
|
2646
|
+
bargain.add_argument("--corpus", required=True, help="Corpus JSON file(s), comma-separated")
|
|
2647
|
+
bargain.add_argument("--district", default=None, help="District override")
|
|
2648
|
+
bargain.add_argument("--rooms", type=int, default=None, help="Rooms override")
|
|
2649
|
+
bargain.add_argument("--by-complex", action="store_true", dest="by_complex", help="Group by complex")
|
|
2650
|
+
bargain.add_argument("--out", default=None, help="Output file")
|
|
2651
|
+
bargain.add_argument("--output", default=None, help="Output file (alias)")
|
|
2652
|
+
bargain.set_defaults(func=_cmd_bargain)
|
|
2653
|
+
|
|
2654
|
+
# risk (F-07b/F-19)
|
|
2655
|
+
risk = sub.add_parser("risk", help="Risk scoring")
|
|
2656
|
+
risk.add_argument("--file", required=True, help="Card JSON")
|
|
2657
|
+
risk.add_argument("--corpus", default=None, help="Corpus JSON for price signals")
|
|
2658
|
+
risk.add_argument("--district", default=None, help="District override")
|
|
2659
|
+
risk.add_argument("--rooms", type=int, default=None, help="Rooms override")
|
|
2660
|
+
risk.add_argument("--out", default=None, help="Output file")
|
|
2661
|
+
risk.add_argument("--output", default=None, help="Output file (alias)")
|
|
2662
|
+
risk.set_defaults(func=_cmd_risk)
|
|
2663
|
+
|
|
2664
|
+
# triage (F-06b)
|
|
2665
|
+
triage = sub.add_parser("triage", help="Legal red flags triage")
|
|
2666
|
+
triage.add_argument("--file", required=True, help="Card JSON")
|
|
2667
|
+
triage.add_argument("--out", default=None, help="Output file")
|
|
2668
|
+
triage.add_argument("--output", default=None, help="Output file (alias)")
|
|
2669
|
+
triage.set_defaults(func=_cmd_triage)
|
|
2670
|
+
|
|
2671
|
+
# checklist (F-22)
|
|
2672
|
+
checklist = sub.add_parser("checklist", help="Legal checklist")
|
|
2673
|
+
csub = checklist.add_subparsers(dest="checklist_action", required=True)
|
|
2674
|
+
cnew = csub.add_parser("new", help="New checklist")
|
|
2675
|
+
cnew.add_argument("card_id", help="Card id")
|
|
2676
|
+
cnew.add_argument("--file", required=True, help="Cards JSON")
|
|
2677
|
+
cnew.add_argument("--out", default=None, help="Output file")
|
|
2678
|
+
cnew.add_argument("--output", default=None, help="Output file (alias)")
|
|
2679
|
+
cnew.set_defaults(func=_cmd_checklist)
|
|
2680
|
+
chand = csub.add_parser("handoff", help="Handoff to colleague")
|
|
2681
|
+
chand.add_argument("card_id", help="Card id")
|
|
2682
|
+
chand.add_argument("--to", default="colleague", help="Recipient")
|
|
2683
|
+
chand.add_argument("--file", default=None, help="Cards JSON")
|
|
2684
|
+
chand.set_defaults(func=_cmd_checklist)
|
|
2685
|
+
checklist.set_defaults(func=_cmd_checklist)
|
|
2686
|
+
|
|
2687
|
+
# callsheet (F-09c)
|
|
2688
|
+
callsheet = sub.add_parser("callsheet", help="Call sheet")
|
|
2689
|
+
callsheet.add_argument("--file", required=True, help="Cards JSON")
|
|
2690
|
+
callsheet.add_argument("--out", default=None, help="Output file")
|
|
2691
|
+
callsheet.add_argument("--output", default=None, help="Output file (alias)")
|
|
2692
|
+
callsheet.add_argument("--with-contacts", action="store_true", dest="with_contacts")
|
|
2693
|
+
callsheet.add_argument("--purpose", default=None, help="Purpose for PII access")
|
|
2694
|
+
callsheet.set_defaults(func=_cmd_callsheet)
|
|
2695
|
+
|
|
2696
|
+
# project (F-06)
|
|
2697
|
+
project = sub.add_parser("project", help="Projects")
|
|
2698
|
+
psub = project.add_subparsers(dest="project_action", required=True)
|
|
2699
|
+
pnew = psub.add_parser("new", help="New project")
|
|
2700
|
+
pnew.add_argument("name", help="Project name")
|
|
2701
|
+
pnew.add_argument("--query", default=None, help="Query JSON")
|
|
2702
|
+
pnew.set_defaults(func=_cmd_project)
|
|
2703
|
+
plist = psub.add_parser("list", help="List projects")
|
|
2704
|
+
plist.set_defaults(func=_cmd_project)
|
|
2705
|
+
padd = psub.add_parser("add", help="Add card to project")
|
|
2706
|
+
padd.add_argument("project", help="Project id")
|
|
2707
|
+
padd.add_argument("card_id", help="Card id")
|
|
2708
|
+
padd.set_defaults(func=_cmd_project)
|
|
2709
|
+
popen = psub.add_parser("open", help="Open project")
|
|
2710
|
+
popen.add_argument("project", help="Project id")
|
|
2711
|
+
popen.add_argument("--out", default=None, help="Output file")
|
|
2712
|
+
popen.add_argument("--output", default=None, help="Output file (alias)")
|
|
2713
|
+
popen.set_defaults(func=_cmd_project)
|
|
2714
|
+
prefresh = psub.add_parser("refresh", help="Refresh project")
|
|
2715
|
+
prefresh.add_argument("project", help="Project id")
|
|
2716
|
+
prefresh.add_argument("--file", required=True, help="New search JSON")
|
|
2717
|
+
prefresh.set_defaults(func=_cmd_project)
|
|
2718
|
+
project.set_defaults(func=_cmd_project)
|
|
2719
|
+
|
|
2720
|
+
# poi (F-14)
|
|
2721
|
+
poi = sub.add_parser("poi", help="POI accessibility")
|
|
2722
|
+
poi.add_argument("--file", required=True, help="Card JSON")
|
|
2723
|
+
poi.add_argument("--radius-min", type=int, default=15, dest="radius_min")
|
|
2724
|
+
poi.add_argument("--offline", default=None, help="Offline Overpass fixture")
|
|
2725
|
+
poi.add_argument("--out", default=None, help="Output file")
|
|
2726
|
+
poi.add_argument("--output", default=None, help="Output file (alias)")
|
|
2727
|
+
poi.set_defaults(func=_cmd_poi)
|
|
2728
|
+
|
|
2729
|
+
# tour (F-09)
|
|
2730
|
+
tour = sub.add_parser("tour", help="Tour routing")
|
|
2731
|
+
tour.add_argument("--project", default=None, help="Project id")
|
|
2732
|
+
tour.add_argument("--file", default=None, help="Cards JSON")
|
|
2733
|
+
tour.add_argument("--date", default=None, help="Date")
|
|
2734
|
+
tour.add_argument("--slot-min", type=int, default=45, dest="slot_min")
|
|
2735
|
+
tour.add_argument("--start", default="10:00", help="Start time")
|
|
2736
|
+
tour.add_argument("--all", action="store_true", dest="all", help="Include all statuses")
|
|
2737
|
+
tour.add_argument("--format", default="telegram", choices=["telegram", "md"])
|
|
2738
|
+
tour.add_argument("--out", default=None, help="Output file")
|
|
2739
|
+
tour.add_argument("--output", default=None, help="Output file (alias)")
|
|
2740
|
+
tour.set_defaults(func=_cmd_tour)
|
|
2741
|
+
|
|
2742
|
+
# sources (F-53)
|
|
2743
|
+
sources = sub.add_parser("sources", help="Source adapters")
|
|
2744
|
+
ssub = sources.add_subparsers(dest="sources_action", required=False)
|
|
2745
|
+
slist = ssub.add_parser("list", help="List sources")
|
|
2746
|
+
slist.set_defaults(func=_cmd_sources)
|
|
2747
|
+
sources.set_defaults(func=_cmd_sources, sources_action="list")
|
|
2748
|
+
|
|
2749
|
+
# dedup (F-02)
|
|
2750
|
+
dedup = sub.add_parser("dedup", help="Deduplicate listings")
|
|
2751
|
+
dedup.add_argument("--file", required=True, help="Cards JSON")
|
|
2752
|
+
dedup.add_argument("--strict", action="store_true", help="Require >=2 signals")
|
|
2753
|
+
dedup.add_argument("--out", default=None, help="Output file")
|
|
2754
|
+
dedup.add_argument("--output", default=None, help="Output file (alias)")
|
|
2755
|
+
dedup.set_defaults(func=_cmd_dedup)
|
|
2756
|
+
|
|
2757
|
+
# market (F-11)
|
|
2758
|
+
market = sub.add_parser("market", help="Market analytics")
|
|
2759
|
+
market.add_argument("--corpus", required=True, help="Corpus JSON file(s), comma-separated")
|
|
2760
|
+
market.add_argument("--district", default=None, help="District filter")
|
|
2761
|
+
market.add_argument("--okrug", default=None, help="Okrug filter")
|
|
2762
|
+
market.add_argument("--group-by", default="district", choices=["district", "complex"], dest="group_by")
|
|
2763
|
+
market.add_argument("--days", type=int, default=30, help="Period days")
|
|
2764
|
+
market.add_argument("--format", default="telegram", choices=["telegram", "md"])
|
|
2765
|
+
market.add_argument("--out", default=None, help="Output file")
|
|
2766
|
+
market.add_argument("--output", default=None, help="Output file (alias)")
|
|
2767
|
+
market.set_defaults(func=_cmd_market)
|
|
2768
|
+
|
|
2769
|
+
# stale (F-58)
|
|
2770
|
+
stale = sub.add_parser("stale", help="Stale detection")
|
|
2771
|
+
stale.add_argument("--file", default=None, help="Cards JSON")
|
|
2772
|
+
stale.add_argument("--project", default=None, help="Project id")
|
|
2773
|
+
stale.add_argument("--max-age-days", type=int, default=14, dest="max_age_days")
|
|
2774
|
+
stale.add_argument("--verify-live", action="store_true", dest="verify_live")
|
|
2775
|
+
stale.add_argument("--out", default=None, help="Output file")
|
|
2776
|
+
stale.add_argument("--output", default=None, help="Output file (alias)")
|
|
2777
|
+
stale.set_defaults(func=_cmd_stale)
|
|
2778
|
+
|
|
2779
|
+
# catalog-pdf (F-31)
|
|
2780
|
+
catalog_pdf = sub.add_parser("catalog-pdf", help="Catalog PDF")
|
|
2781
|
+
catalog_pdf.add_argument("--file", default=None, help="Cards JSON")
|
|
2782
|
+
catalog_pdf.add_argument("--project", default=None, help="Project id")
|
|
2783
|
+
catalog_pdf.add_argument("--out", default="katalog.pdf", help="Output PDF")
|
|
2784
|
+
catalog_pdf.add_argument("--output", default=None, help="Output PDF (alias)")
|
|
2785
|
+
catalog_pdf.add_argument("--title", default=None, help="Title")
|
|
2786
|
+
catalog_pdf.set_defaults(func=_cmd_catalog_pdf)
|
|
2787
|
+
|
|
2788
|
+
# feedback (F-21)
|
|
2789
|
+
feedback = sub.add_parser("feedback", help="Feedback tracker")
|
|
2790
|
+
fsub = feedback.add_subparsers(dest="feedback_action", required=True)
|
|
2791
|
+
fadd = fsub.add_parser("add", help="Add feedback")
|
|
2792
|
+
fadd.add_argument("card_id", help="Card id")
|
|
2793
|
+
fadd.add_argument("--verdict", required=True, choices=["liked", "rejected", "maybe"])
|
|
2794
|
+
fadd.add_argument("--reason", default=None, help="Reason")
|
|
2795
|
+
fadd.add_argument("--note", default=None, help="Note")
|
|
2796
|
+
fadd.add_argument("--dry", action="store_true", help="Dry run")
|
|
2797
|
+
fadd.set_defaults(func=_cmd_feedback)
|
|
2798
|
+
flist = fsub.add_parser("list", help="List feedback")
|
|
2799
|
+
flist.add_argument("--card", default=None, help="Card id filter")
|
|
2800
|
+
flist.set_defaults(func=_cmd_feedback)
|
|
2801
|
+
fprof = fsub.add_parser("profile", help="Feedback profile")
|
|
2802
|
+
fprof.set_defaults(func=_cmd_feedback)
|
|
2803
|
+
fapply = fsub.add_parser("apply", help="Apply overrides")
|
|
2804
|
+
fapply.add_argument("--dry", action="store_true", help="Dry run")
|
|
2805
|
+
fapply.add_argument("--yes", action="store_true", help="Apply")
|
|
2806
|
+
fapply.set_defaults(func=_cmd_feedback)
|
|
2807
|
+
feedback.set_defaults(func=_cmd_feedback)
|
|
2808
|
+
|
|
2809
|
+
# scorefit (F-G3)
|
|
2810
|
+
scorefit = sub.add_parser("scorefit", help="Score fit")
|
|
2811
|
+
scorefit.add_argument("--file", required=True, help="Cards JSON")
|
|
2812
|
+
scorefit.add_argument("--profile", default=None, help="Profile JSON or name")
|
|
2813
|
+
scorefit.add_argument("--out", default=None, help="Output file")
|
|
2814
|
+
scorefit.add_argument("--output", default=None, help="Output file (alias)")
|
|
2815
|
+
scorefit.set_defaults(func=_cmd_scorefit)
|
|
2816
|
+
|
|
2817
|
+
# chain (F-J2)
|
|
2818
|
+
chain = sub.add_parser("chain", help="Deal chains")
|
|
2819
|
+
csub = chain.add_subparsers(dest="chain_action", required=True)
|
|
2820
|
+
cnew = csub.add_parser("new", help="New chain")
|
|
2821
|
+
cnew.add_argument("card_id", help="Card id")
|
|
2822
|
+
cnew.add_argument("--kind", default="direct", choices=["direct", "alt_found", "alt_unfound", "guardian_needed"])
|
|
2823
|
+
cnew.set_defaults(func=_cmd_chain)
|
|
2824
|
+
clink = csub.add_parser("link", help="Link cards")
|
|
2825
|
+
clink.add_argument("a", help="Card a")
|
|
2826
|
+
clink.add_argument("b", help="Card b")
|
|
2827
|
+
clink.add_argument("--rel", default="depends_on", choices=["depends_on", "blocks"])
|
|
2828
|
+
clink.set_defaults(func=_cmd_chain)
|
|
2829
|
+
cshow = csub.add_parser("show", help="Show chain")
|
|
2830
|
+
cshow.add_argument("card_id", help="Card id")
|
|
2831
|
+
cshow.set_defaults(func=_cmd_chain)
|
|
2832
|
+
chain.set_defaults(func=_cmd_chain)
|
|
2833
|
+
|
|
2834
|
+
# text (F-05)
|
|
2835
|
+
text = sub.add_parser("text", help="Full-text search")
|
|
2836
|
+
tsub = text.add_subparsers(dest="text_action", required=True)
|
|
2837
|
+
tindex = tsub.add_parser("index", help="Index file")
|
|
2838
|
+
tindex.add_argument("--file", required=True, help="Cards JSON")
|
|
2839
|
+
tindex.add_argument("--project", default=None, help="Project id")
|
|
2840
|
+
tindex.set_defaults(func=_cmd_text)
|
|
2841
|
+
tquery = tsub.add_parser("query", help="Query")
|
|
2842
|
+
tquery.add_argument("query", help="FTS query")
|
|
2843
|
+
tquery.add_argument("--project", default=None, help="Project id")
|
|
2844
|
+
tquery.set_defaults(func=_cmd_text)
|
|
2845
|
+
text.set_defaults(func=_cmd_text)
|
|
2846
|
+
|
|
2847
|
+
# audit (F-45)
|
|
2848
|
+
audit = sub.add_parser("audit", help="Audit log")
|
|
2849
|
+
audit.add_argument("--days", type=int, default=30)
|
|
2850
|
+
audit.add_argument("--role", default=None)
|
|
2851
|
+
audit.add_argument("--card", default=None)
|
|
2852
|
+
audit.set_defaults(func=_cmd_audit)
|
|
2853
|
+
|
|
2854
|
+
# pii-log (F-38)
|
|
2855
|
+
pii_log = sub.add_parser("pii-log", help="PII access log")
|
|
2856
|
+
pii_log.add_argument("--days", type=int, default=30)
|
|
2857
|
+
pii_log.set_defaults(func=_cmd_pii_log)
|
|
2858
|
+
|
|
2859
|
+
# purge (F-43)
|
|
2860
|
+
purge = sub.add_parser("purge", help="Purge data")
|
|
2861
|
+
psub = purge.add_subparsers(dest="purge_action", required=True)
|
|
2862
|
+
pcard = psub.add_parser("card", help="Purge card")
|
|
2863
|
+
pcard.add_argument("card_id", help="Card id")
|
|
2864
|
+
pcard.add_argument("--yes", action="store_true", help="Execute")
|
|
2865
|
+
pcard.set_defaults(func=_cmd_purge)
|
|
2866
|
+
pprof = psub.add_parser("profile", help="Purge profile")
|
|
2867
|
+
pprof.add_argument("name", help="Profile name")
|
|
2868
|
+
pprof.add_argument("--yes", action="store_true", help="Execute")
|
|
2869
|
+
pprof.set_defaults(func=_cmd_purge)
|
|
2870
|
+
purge.set_defaults(func=_cmd_purge)
|
|
2871
|
+
|
|
2872
|
+
# auto (F-20)
|
|
2873
|
+
auto = sub.add_parser("auto", help="Auto selection")
|
|
2874
|
+
asub = auto.add_subparsers(dest="auto_action", required=True)
|
|
2875
|
+
arun = asub.add_parser("run", help="Run auto selection")
|
|
2876
|
+
arun.add_argument("--profile", default=None, help="Profile JSON or name")
|
|
2877
|
+
arun.add_argument("--file", default=None, help="Corpus JSON")
|
|
2878
|
+
arun.add_argument("--project", default=None, help="Project id")
|
|
2879
|
+
arun.add_argument("--live", action="store_true", help="Live search")
|
|
2880
|
+
arun.add_argument("--out", default=None, help="Output file")
|
|
2881
|
+
arun.add_argument("--output", default=None, help="Output file (alias)")
|
|
2882
|
+
arun.set_defaults(func=_cmd_auto)
|
|
2883
|
+
areport = asub.add_parser("report", help="Auto report")
|
|
2884
|
+
areport.add_argument("--days", type=int, default=7)
|
|
2885
|
+
areport.add_argument("--out", default=None, help="Output file")
|
|
2886
|
+
areport.add_argument("--output", default=None, help="Output file (alias)")
|
|
2887
|
+
areport.set_defaults(func=_cmd_auto)
|
|
2888
|
+
auto.set_defaults(func=_cmd_auto)
|
|
2889
|
+
|
|
2890
|
+
# litcheck (F-22b)
|
|
2891
|
+
litcheck = sub.add_parser("litcheck", help="Litigation pre-check")
|
|
2892
|
+
litcheck.add_argument("--card", default=None, help="Card id")
|
|
2893
|
+
litcheck.add_argument("--file", default=None, help="Cards JSON")
|
|
2894
|
+
litcheck.add_argument("--address", default=None, help="Address")
|
|
2895
|
+
litcheck.add_argument("--developer", default=None, help="Developer")
|
|
2896
|
+
litcheck.add_argument("--since", type=int, default=2020, help="Since year")
|
|
2897
|
+
litcheck.add_argument("--out", default=None, help="Output file")
|
|
2898
|
+
litcheck.add_argument("--output", default=None, help="Output file (alias)")
|
|
2899
|
+
litcheck.set_defaults(func=_cmd_litcheck)
|
|
2900
|
+
|
|
2901
|
+
# eco (F-16)
|
|
2902
|
+
eco = sub.add_parser("eco", help="Eco proxy")
|
|
2903
|
+
eco.add_argument("--file", required=True, help="Card JSON")
|
|
2904
|
+
eco.add_argument("--offline", default=None, help="Offline Overpass fixture")
|
|
2905
|
+
eco.add_argument("--out", default=None, help="Output file")
|
|
2906
|
+
eco.add_argument("--output", default=None, help="Output file (alias)")
|
|
2907
|
+
eco.set_defaults(func=_cmd_eco)
|
|
2908
|
+
|
|
2909
|
+
# ops (F-44/46)
|
|
2910
|
+
ops = sub.add_parser("ops", help="Operator panel")
|
|
2911
|
+
ops.add_argument("--days", type=int, default=1)
|
|
2912
|
+
ops.add_argument("--format", default="telegram", choices=["telegram", "json"])
|
|
2913
|
+
ops.add_argument("--precheck", action="store_true")
|
|
2914
|
+
ops.add_argument("--live", action="store_true")
|
|
2915
|
+
ops.add_argument("--json", nargs="?", const="-", default=None)
|
|
2916
|
+
ops.add_argument("--out", default=None)
|
|
2917
|
+
ops.add_argument("--output", default=None)
|
|
2918
|
+
ops.set_defaults(func=_cmd_ops)
|
|
2919
|
+
|
|
2920
|
+
# mcp (F-23)
|
|
2921
|
+
mcp = sub.add_parser("mcp", help="MCP server")
|
|
2922
|
+
msub = mcp.add_subparsers(dest="mcp_action", required=False)
|
|
2923
|
+
mserve = msub.add_parser("serve", help="Serve stdio")
|
|
2924
|
+
mserve.set_defaults(func=_cmd_mcp)
|
|
2925
|
+
mcp.set_defaults(func=_cmd_mcp, mcp_action="serve")
|
|
2926
|
+
|
|
2927
|
+
# mos (F-04b)
|
|
2928
|
+
mos = sub.add_parser("mos", help="Mosdata client")
|
|
2929
|
+
msub = mos.add_subparsers(dest="mos_action", required=False)
|
|
2930
|
+
ms = msub.add_parser("search-datasets", help="Search datasets")
|
|
2931
|
+
ms.add_argument("--query", required=True)
|
|
2932
|
+
ms.set_defaults(func=_cmd_mos, mos_action="search-datasets")
|
|
2933
|
+
mv = msub.add_parser("dataset-versions", help="Dataset versions")
|
|
2934
|
+
mv.add_argument("dataset_id")
|
|
2935
|
+
mv.set_defaults(func=_cmd_mos, mos_action="dataset-versions")
|
|
2936
|
+
mr = msub.add_parser("rows", help="Rows")
|
|
2937
|
+
mr.add_argument("dataset_id")
|
|
2938
|
+
mr.add_argument("--version", default=None)
|
|
2939
|
+
mr.add_argument("--top", type=int, default=1000)
|
|
2940
|
+
mr.add_argument("--skip", type=int, default=0)
|
|
2941
|
+
mr.set_defaults(func=_cmd_mos, mos_action="rows")
|
|
2942
|
+
mst = msub.add_parser("status", help="Status")
|
|
2943
|
+
mst.set_defaults(func=_cmd_mos, mos_action="status")
|
|
2944
|
+
mos.set_defaults(func=_cmd_mos, mos_action="status")
|
|
2945
|
+
|
|
2946
|
+
# dossier (F-04b)
|
|
2947
|
+
dossier = sub.add_parser("dossier", help="Building dossier")
|
|
2948
|
+
dsub = dossier.add_subparsers(dest="dossier_action", required=True)
|
|
2949
|
+
dsync = dsub.add_parser("sync", help="Sync registry")
|
|
2950
|
+
dsync.add_argument("--limit", type=int, default=1000)
|
|
2951
|
+
dsync.add_argument("--dataset", default="auto")
|
|
2952
|
+
dsync.set_defaults(func=_cmd_dossier)
|
|
2953
|
+
dmatch = dsub.add_parser("match", help="Match card")
|
|
2954
|
+
dmatch.add_argument("--file", default=None, help="Card JSON")
|
|
2955
|
+
dmatch.add_argument("--address", default=None, help="Address")
|
|
2956
|
+
dmatch.set_defaults(func=_cmd_dossier)
|
|
2957
|
+
dossier.set_defaults(func=_cmd_dossier)
|
|
2958
|
+
|
|
2959
|
+
# developer (F-17)
|
|
2960
|
+
developer = sub.add_parser("developer", help="Developer dossier")
|
|
2961
|
+
developer.add_argument("--name", default=None, help="Developer name")
|
|
2962
|
+
developer.add_argument("--inn", default=None, help="INN")
|
|
2963
|
+
developer.add_argument("--from-file", dest="from_file", default=None, help="Card JSON")
|
|
2964
|
+
developer.add_argument("--litcheck", action="store_true", help="Run litigation pre-check")
|
|
2965
|
+
developer.add_argument("--out", default=None, help="Output file")
|
|
2966
|
+
developer.add_argument("--output", default=None, help="Output file (alias)")
|
|
2967
|
+
developer.set_defaults(func=_cmd_developer)
|
|
2968
|
+
|
|
2969
|
+
# noise (F-16)
|
|
2970
|
+
noise = sub.add_parser("noise", help="Noise monitoring")
|
|
2971
|
+
nsub = noise.add_subparsers(dest="noise_action", required=False)
|
|
2972
|
+
nsync = nsub.add_parser("sync", help="Sync noise data")
|
|
2973
|
+
nsync.add_argument("--limit", type=int, default=1000)
|
|
2974
|
+
nsync.set_defaults(func=_cmd_noise, noise_action="sync")
|
|
2975
|
+
noise.add_argument("--file", default=None, help="Card JSON")
|
|
2976
|
+
noise.add_argument("--lat", type=float, default=None)
|
|
2977
|
+
noise.add_argument("--lon", type=float, default=None)
|
|
2978
|
+
noise.add_argument("--radius-m", type=int, default=1500, dest="radius_m")
|
|
2979
|
+
noise.add_argument("--out", default=None, help="Output file")
|
|
2980
|
+
noise.add_argument("--output", default=None, help="Output file (alias)")
|
|
2981
|
+
noise.set_defaults(func=_cmd_noise)
|
|
2982
|
+
|
|
2983
|
+
domclick = sub.add_parser("domclick", help="Domclick listings")
|
|
2984
|
+
dcsub = domclick.add_subparsers(dest="domclick_action", required=False)
|
|
2985
|
+
dcp = dcsub.add_parser("parse", help="Parse saved Domclick innerText")
|
|
2986
|
+
dcp.add_argument("--file", required=True)
|
|
2987
|
+
dcp.set_defaults(func=_cmd_domclick)
|
|
2988
|
+
|
|
2989
|
+
source = sub.add_parser("source", help="AGPL §13: license + source repository info")
|
|
2990
|
+
source.set_defaults(func=_cmd_source)
|
|
2991
|
+
|
|
2992
|
+
rush = sub.add_parser("rush", help="Transport accessibility + current traffic")
|
|
2993
|
+
rush.add_argument("--file", default=None, help="Card JSON")
|
|
2994
|
+
rush.add_argument("--hour", type=int, default=None,
|
|
2995
|
+
help="Почасовой web-прогноз: час 0-23 (только с NEDVIGA_EGRESS_PROXY)")
|
|
2996
|
+
rush.set_defaults(func=_cmd_rush)
|
|
2997
|
+
|
|
2998
|
+
deck = sub.add_parser("deck", help="PPTX deck from cards")
|
|
2999
|
+
deck.add_argument("--file", default=None)
|
|
3000
|
+
deck.add_argument("--project", default=None)
|
|
3001
|
+
deck.add_argument("--out", default=None)
|
|
3002
|
+
deck.add_argument("--title", default=None)
|
|
3003
|
+
deck.set_defaults(func=_cmd_deck)
|
|
3004
|
+
|
|
3005
|
+
# wizard (F-61)
|
|
3006
|
+
wizard = sub.add_parser("wizard", help="First-run wizard")
|
|
3007
|
+
wsub = wizard.add_subparsers(dest="wizard_action", required=True)
|
|
3008
|
+
winit = wsub.add_parser("init", help="Initialize (example: wizard init --profile agent --out ./out)")
|
|
3009
|
+
winit.add_argument("--profile", default="agent", choices=["agent", "family"])
|
|
3010
|
+
winit.add_argument("--out", default=None)
|
|
3011
|
+
winit.add_argument("--output", default=None)
|
|
3012
|
+
winit.set_defaults(func=_cmd_wizard)
|
|
3013
|
+
wdoc = wsub.add_parser("doctor", help="Diagnostics (example: wizard doctor --json)")
|
|
3014
|
+
wdoc.add_argument("--json", nargs="?", const="-", default=None)
|
|
3015
|
+
wdoc.add_argument("--out", default=None)
|
|
3016
|
+
wdoc.add_argument("--output", default=None)
|
|
3017
|
+
wdoc.set_defaults(func=_cmd_wizard)
|
|
3018
|
+
wizard.set_defaults(func=_cmd_wizard)
|
|
3019
|
+
|
|
3020
|
+
# avito (F-15)
|
|
3021
|
+
avito = sub.add_parser("avito", help="Avito adapter")
|
|
3022
|
+
asub = avito.add_subparsers(dest="avito_action", required=True)
|
|
3023
|
+
aparse = asub.add_parser("parse", help="Parse livetext")
|
|
3024
|
+
aparse.add_argument("--file", required=True, help="Livetext file")
|
|
3025
|
+
aparse.add_argument("--out", default=None, help="Output file")
|
|
3026
|
+
aparse.add_argument("--output", default=None, help="Output file (alias)")
|
|
3027
|
+
aparse.set_defaults(func=_cmd_avito)
|
|
3028
|
+
avito.set_defaults(func=_cmd_avito)
|
|
3029
|
+
|
|
3030
|
+
# doctor
|
|
3031
|
+
doctor = sub.add_parser("doctor", help="System diagnostics")
|
|
3032
|
+
doctor.add_argument("--verify-signature", default=None, dest="verify_signature")
|
|
3033
|
+
doctor.add_argument(
|
|
3034
|
+
"--json",
|
|
3035
|
+
nargs="?",
|
|
3036
|
+
const="-",
|
|
3037
|
+
default=None,
|
|
3038
|
+
help="Write JSON result to FILE; default stdout when flag present",
|
|
3039
|
+
)
|
|
3040
|
+
doctor.set_defaults(func=_cmd_doctor)
|
|
3041
|
+
|
|
3042
|
+
_annotate_examples(sub)
|
|
3043
|
+
return parser
|
|
3044
|
+
|
|
3045
|
+
|
|
3046
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
3047
|
+
parser = _build_parser()
|
|
3048
|
+
args = parser.parse_args(argv)
|
|
3049
|
+
from .roles import current_role, validate_role, check_permission, role_allows_contacts
|
|
3050
|
+
|
|
3051
|
+
role = current_role()
|
|
3052
|
+
args.role = role
|
|
3053
|
+
|
|
3054
|
+
ok, reason = validate_role()
|
|
3055
|
+
if not ok:
|
|
3056
|
+
envelope = make_envelope(
|
|
3057
|
+
status="error", reason=reason or "unknown_role",
|
|
3058
|
+
data={"role": role},
|
|
3059
|
+
metadata={"engine": "roles", "cached": False, "elapsed_seconds": 0.0, "role": role},
|
|
3060
|
+
)
|
|
3061
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
3062
|
+
return 3
|
|
3063
|
+
|
|
3064
|
+
allowed, reason = check_permission(args)
|
|
3065
|
+
if not allowed:
|
|
3066
|
+
envelope = make_envelope(
|
|
3067
|
+
status="error", reason=reason or "role_forbidden",
|
|
3068
|
+
data={"role": role, "command": getattr(args, "command", None)},
|
|
3069
|
+
metadata={"engine": "roles", "cached": False, "elapsed_seconds": 0.0, "role": role},
|
|
3070
|
+
)
|
|
3071
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
3072
|
+
return 3
|
|
3073
|
+
|
|
3074
|
+
# purpose required for --with-contacts (agent only)
|
|
3075
|
+
if getattr(args, "with_contacts", False) and role == "agent":
|
|
3076
|
+
if not getattr(args, "purpose", None):
|
|
3077
|
+
envelope = make_envelope(
|
|
3078
|
+
status="error", reason="pii_purpose_required",
|
|
3079
|
+
data={"message": "--purpose is required with --with-contacts"},
|
|
3080
|
+
metadata={"engine": "roles", "cached": False, "elapsed_seconds": 0.0, "role": role},
|
|
3081
|
+
)
|
|
3082
|
+
_write_output(_dump_envelope(envelope, args), args)
|
|
3083
|
+
return 3
|
|
3084
|
+
|
|
3085
|
+
# role-aware contacts: non-agent cannot open contacts
|
|
3086
|
+
if getattr(args, "with_contacts", False) and not role_allows_contacts(role):
|
|
3087
|
+
args.with_contacts = False
|
|
3088
|
+
args._contacts_requested = True
|
|
3089
|
+
|
|
3090
|
+
rc = args.func(args)
|
|
3091
|
+
|
|
3092
|
+
# audit log (write commands + with-contacts), no recursion for audit/pii-log
|
|
3093
|
+
try:
|
|
3094
|
+
from .roles import is_write
|
|
3095
|
+
from .auditlog import log_action
|
|
3096
|
+
cmd = getattr(args, "command", None)
|
|
3097
|
+
should_log = is_write(args) or bool(getattr(args, "with_contacts", False))
|
|
3098
|
+
if should_log and cmd not in {"audit", "pii-log"}:
|
|
3099
|
+
card_id = (getattr(args, "card_id", None) or getattr(args, "target", None)
|
|
3100
|
+
or getattr(args, "a", None))
|
|
3101
|
+
log_action(
|
|
3102
|
+
role=role,
|
|
3103
|
+
command=cmd,
|
|
3104
|
+
card_id=card_id,
|
|
3105
|
+
rc=rc,
|
|
3106
|
+
reason=None,
|
|
3107
|
+
argv=[str(x) for x in (argv or [])],
|
|
3108
|
+
pii=bool(getattr(args, "with_contacts", False)),
|
|
3109
|
+
purpose=getattr(args, "purpose", None),
|
|
3110
|
+
)
|
|
3111
|
+
except Exception:
|
|
3112
|
+
pass
|
|
3113
|
+
|
|
3114
|
+
return rc
|
|
3115
|
+
|
|
3116
|
+
|
|
3117
|
+
if __name__ == "__main__": # pragma: no cover
|
|
3118
|
+
sys.exit(main())
|