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.
Files changed (71) hide show
  1. nedviga/__init__.py +46 -0
  2. nedviga/__main__.py +6 -0
  3. nedviga/_wm.py +93 -0
  4. nedviga/adaptive_selectors.py +100 -0
  5. nedviga/auditlog.py +77 -0
  6. nedviga/auto.py +88 -0
  7. nedviga/bargain.py +183 -0
  8. nedviga/browser_native.py +242 -0
  9. nedviga/callsheet.py +63 -0
  10. nedviga/chains.py +83 -0
  11. nedviga/checklist.py +119 -0
  12. nedviga/cian_card.py +1371 -0
  13. nedviga/cian_live.py +812 -0
  14. nedviga/cli.py +3118 -0
  15. nedviga/compare.py +204 -0
  16. nedviga/data/legal_flags.json +146 -0
  17. nedviga/data/release_pubkey.asc +22 -0
  18. nedviga/deck.py +115 -0
  19. nedviga/dedup.py +242 -0
  20. nedviga/developer.py +169 -0
  21. nedviga/digest.py +205 -0
  22. nedviga/doctor.py +128 -0
  23. nedviga/dossier.py +303 -0
  24. nedviga/eco.py +81 -0
  25. nedviga/errors.py +22 -0
  26. nedviga/export_html.py +280 -0
  27. nedviga/export_pdf.py +96 -0
  28. nedviga/family_card.py +670 -0
  29. nedviga/feedback.py +79 -0
  30. nedviga/geo_routing.py +214 -0
  31. nedviga/graph.py +149 -0
  32. nedviga/legal_triage.py +105 -0
  33. nedviga/litprecheck.py +116 -0
  34. nedviga/market.py +102 -0
  35. nedviga/mcp_server.py +195 -0
  36. nedviga/metro_directory.json +9836 -0
  37. nedviga/metro_vestibules.py +134 -0
  38. nedviga/models.py +164 -0
  39. nedviga/mosdata.py +95 -0
  40. nedviga/net_guard.py +997 -0
  41. nedviga/noise.py +156 -0
  42. nedviga/ops.py +94 -0
  43. nedviga/outcomes.py +35 -0
  44. nedviga/pii.py +85 -0
  45. nedviga/pipeline.py +239 -0
  46. nedviga/poi.py +180 -0
  47. nedviga/price_guard.py +73 -0
  48. nedviga/profiles.py +131 -0
  49. nedviga/projects.py +112 -0
  50. nedviga/purge.py +116 -0
  51. nedviga/py.typed +0 -0
  52. nedviga/risk.py +127 -0
  53. nedviga/roles.py +124 -0
  54. nedviga/routing_cache.py +72 -0
  55. nedviga/rush.py +139 -0
  56. nedviga/scorefit.py +127 -0
  57. nedviga/sources.py +55 -0
  58. nedviga/sources_avito.py +138 -0
  59. nedviga/sources_domclick.py +122 -0
  60. nedviga/stale.py +83 -0
  61. nedviga/textsearch.py +141 -0
  62. nedviga/tour.py +180 -0
  63. nedviga/watch.py +234 -0
  64. nedviga/wizard.py +106 -0
  65. nedviga_cli-0.1.0.dist-info/METADATA +130 -0
  66. nedviga_cli-0.1.0.dist-info/RECORD +71 -0
  67. nedviga_cli-0.1.0.dist-info/WHEEL +5 -0
  68. nedviga_cli-0.1.0.dist-info/entry_points.txt +2 -0
  69. nedviga_cli-0.1.0.dist-info/licenses/LICENSE +661 -0
  70. nedviga_cli-0.1.0.dist-info/licenses/NOTICE +1 -0
  71. nedviga_cli-0.1.0.dist-info/top_level.txt +1 -0
nedviga/watch.py ADDED
@@ -0,0 +1,234 @@
1
+ """v0.4.8 (the task brief) F-08: price/state monitoring."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ from . import pipeline
9
+
10
+
11
+ def _extract_price(card: dict) -> Optional[float]:
12
+ if not isinstance(card, dict):
13
+ return None
14
+ if "price_total_rub" in card and card.get("price_total_rub") is not None:
15
+ try:
16
+ return float(card["price_total_rub"])
17
+ except (TypeError, ValueError):
18
+ pass
19
+ pricing = card.get("pricing")
20
+ if isinstance(pricing, dict):
21
+ v = pricing.get("price_total_rub")
22
+ if v is not None:
23
+ try:
24
+ return float(v)
25
+ except (TypeError, ValueError):
26
+ return None
27
+ return None
28
+
29
+
30
+ def _extract_status(card: dict) -> Optional[str]:
31
+ if not isinstance(card, dict):
32
+ return None
33
+ for key in ("status", "sale_status"):
34
+ if card.get(key):
35
+ return str(card[key])
36
+ return None
37
+
38
+
39
+ def _extract_floors(card: dict) -> Optional[int]:
40
+ if not isinstance(card, dict):
41
+ return None
42
+ params = card.get("parameters") or {}
43
+ if isinstance(params, dict):
44
+ v = params.get("floors_total")
45
+ if v is not None:
46
+ try:
47
+ return int(v)
48
+ except (TypeError, ValueError):
49
+ pass
50
+ v = card.get("floors_total")
51
+ if v is not None:
52
+ try:
53
+ return int(v)
54
+ except (TypeError, ValueError):
55
+ pass
56
+ return None
57
+
58
+
59
+ def add(card_id: Optional[str] = None, file: Optional[str] = None, price: Optional[float] = None,
60
+ status: str = "active", floors_total: Optional[int] = None, db=None) -> dict[str, Any]:
61
+ conn = pipeline._connect(db)
62
+ try:
63
+ if file:
64
+ raw = json.loads(Path(file).read_text(encoding="utf-8"))
65
+ if isinstance(raw, dict):
66
+ if "cards" in raw and isinstance(raw["cards"], list):
67
+ raw = raw["cards"]
68
+ elif "data" in raw:
69
+ d = raw["data"]
70
+ raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
71
+ else:
72
+ raw = [raw]
73
+ if not isinstance(raw, list):
74
+ raw = [raw]
75
+ added = []
76
+ for item in raw:
77
+ if not isinstance(item, dict):
78
+ continue
79
+ cid = item.get("id")
80
+ if not cid:
81
+ continue
82
+ p = price if price is not None else _extract_price(item)
83
+ st = status if status else (_extract_status(item) or "active")
84
+ fl = floors_total if floors_total is not None else _extract_floors(item)
85
+ now = pipeline._now()
86
+ conn.execute(
87
+ "INSERT OR REPLACE INTO watch_snapshots (card_id, price, status, floors_total, snapshot_at, raw_json) VALUES (?,?,?,?,?,?)",
88
+ (str(cid), p, st, fl, now, json.dumps(item, ensure_ascii=False)),
89
+ )
90
+ conn.execute(
91
+ "INSERT INTO watch_history (card_id, price, status, at) VALUES (?,?,?,?)",
92
+ (str(cid), p, st, now),
93
+ )
94
+ added.append(str(cid))
95
+ conn.commit()
96
+ return {"ok": True, "added": added}
97
+ if not card_id:
98
+ return {"ok": False, "reason": "missing_card_id"}
99
+ now = pipeline._now()
100
+ conn.execute(
101
+ "INSERT OR REPLACE INTO watch_snapshots (card_id, price, status, floors_total, snapshot_at, raw_json) VALUES (?,?,?,?,?,?)",
102
+ (card_id, price, status, floors_total, now, None),
103
+ )
104
+ conn.execute(
105
+ "INSERT INTO watch_history (card_id, price, status, at) VALUES (?,?,?,?)",
106
+ (card_id, price, status, now),
107
+ )
108
+ conn.commit()
109
+ return {"ok": True, "added": [card_id]}
110
+ finally:
111
+ conn.close()
112
+
113
+
114
+ def list_items(db=None) -> list[dict[str, Any]]:
115
+ conn = pipeline._connect(db)
116
+ try:
117
+ rows = conn.execute("SELECT * FROM watch_snapshots ORDER BY card_id").fetchall()
118
+ return [dict(r) for r in rows]
119
+ finally:
120
+ conn.close()
121
+
122
+
123
+ def remove(card_id: str, db=None) -> dict[str, Any]:
124
+ conn = pipeline._connect(db)
125
+ try:
126
+ conn.execute("DELETE FROM watch_snapshots WHERE card_id=?", (card_id,))
127
+ conn.commit()
128
+ return {"ok": True, "removed": card_id}
129
+ finally:
130
+ conn.close()
131
+
132
+
133
+ def _normalize_fresh(raw) -> dict[str, dict]:
134
+ if isinstance(raw, dict):
135
+ if "cards" in raw and isinstance(raw["cards"], list):
136
+ raw = raw["cards"]
137
+ elif "data" in raw:
138
+ d = raw["data"]
139
+ raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
140
+ else:
141
+ raw = [raw]
142
+ if not isinstance(raw, list):
143
+ raw = [raw]
144
+ out = {}
145
+ for item in raw:
146
+ if isinstance(item, dict) and item.get("id"):
147
+ out[str(item["id"])] = item
148
+ return out
149
+
150
+
151
+ def check(offline: Optional[str] = None, db=None) -> list[dict[str, Any]]:
152
+ conn = pipeline._connect(db)
153
+ try:
154
+ rows = conn.execute("SELECT * FROM watch_snapshots").fetchall()
155
+ finally:
156
+ conn.close()
157
+
158
+ fresh_map: dict[str, dict] = {}
159
+ if offline:
160
+ try:
161
+ raw = json.loads(Path(offline).read_text(encoding="utf-8"))
162
+ fresh_map = _normalize_fresh(raw)
163
+ except Exception as exc:
164
+ # v0.4.8 контроль качества: битый файл раньше молча превращался в {} и
165
+ # каждая карточка получала ложное "disappeared". Только явный degraded.
166
+ return [{"card_id": "*", "type": "source_degraded",
167
+ "reason": f"offline_file_unreadable: {type(exc).__name__}"}]
168
+
169
+ events: list[dict[str, Any]] = []
170
+
171
+ for row in rows:
172
+ cid = row["card_id"]
173
+ if offline:
174
+ fresh = fresh_map.get(cid)
175
+ if fresh is None:
176
+ events.append({"card_id": cid, "type": "disappeared"})
177
+ _h = pipeline._connect(db)
178
+ try:
179
+ _h.execute("INSERT INTO watch_history (card_id, price, status, at) VALUES (?,?,?,?)",
180
+ (cid, None, "disappeared", pipeline._now()))
181
+ _h.commit()
182
+ finally:
183
+ _h.close()
184
+ continue
185
+ old_price = row["price"]
186
+ new_price = _extract_price(fresh)
187
+ if old_price is not None and new_price is not None:
188
+ if new_price < old_price:
189
+ events.append({"card_id": cid, "type": "price_down", "amount": old_price - new_price})
190
+ elif new_price > old_price:
191
+ events.append({"card_id": cid, "type": "price_up", "amount": new_price - old_price})
192
+ st = _extract_status(fresh)
193
+ if st and st.lower() in {"sold", "removed", "inactive"}:
194
+ events.append({"card_id": cid, "type": "sold" if st.lower() == "sold" else "removed"})
195
+ title = str(fresh.get("title") or "").lower()
196
+ if "продан" in title or "снят" in title:
197
+ events.append({"card_id": cid, "type": "sold"})
198
+ _h = pipeline._connect(db)
199
+ try:
200
+ _h.execute("INSERT INTO watch_history (card_id, price, status, at) VALUES (?,?,?,?)",
201
+ (cid, new_price, st or "active", pipeline._now()))
202
+ _h.commit()
203
+ finally:
204
+ _h.close()
205
+ continue
206
+
207
+ # live path
208
+ try:
209
+ from .cian_live import get_card
210
+ card = get_card(cid)
211
+ except Exception as exc:
212
+ msg = str(exc).lower()
213
+ if "404" in msg or "not found" in msg or "removed" in msg:
214
+ events.append({"card_id": cid, "type": "disappeared"})
215
+ else:
216
+ events.append({"card_id": cid, "type": "source_degraded", "reason": str(exc)})
217
+ continue
218
+ # extract from card
219
+ try:
220
+ data = card.model_dump(mode="json", exclude_none=False) if hasattr(card, "model_dump") else {}
221
+ except Exception:
222
+ data = {}
223
+ new_price = _extract_price(data)
224
+ old_price = row["price"]
225
+ if old_price is not None and new_price is not None:
226
+ if new_price < old_price:
227
+ events.append({"card_id": cid, "type": "price_down", "amount": old_price - new_price})
228
+ elif new_price > old_price:
229
+ events.append({"card_id": cid, "type": "price_up", "amount": new_price - old_price})
230
+ st = _extract_status(data)
231
+ if st and st.lower() in {"sold", "removed", "inactive"}:
232
+ events.append({"card_id": cid, "type": "sold" if st.lower() == "sold" else "removed"})
233
+
234
+ return events
nedviga/wizard.py ADDED
@@ -0,0 +1,106 @@
1
+ """v0.5.11 (the task brief) wizard."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from .roles import load_roles
10
+
11
+
12
+ def init(profile: str = "agent", out: Optional[str] = None) -> dict:
13
+ base = Path(out) if out else Path.cwd() / "wizard_out"
14
+ base.mkdir(parents=True, exist_ok=True)
15
+ # roles.json
16
+ roles = [{"name": "agent", "role": "agent", "since": "2026-01-01"}]
17
+ if profile == "family":
18
+ roles.append({"name": "family", "role": "family", "since": "2026-01-01"})
19
+ (base / "roles.json").write_text(json.dumps(roles, ensure_ascii=False, indent=2), encoding="utf-8")
20
+ # env template
21
+ (base / "env.sh").write_text(
22
+ 'export NEDVIGA_EGRESS_PROXY="http://host:port"\n'
23
+ 'export NEDVIGA_MOS_TOKEN_FILE="/path/token.txt"\n'
24
+ 'export NEDVIGA_STATE_DIR="$HOME/.local/share/nedviga"\n',
25
+ encoding="utf-8",
26
+ )
27
+ # profile template
28
+ (base / "profile.json").write_text(json.dumps({"rooms": 2, "price_max": 20000000}, ensure_ascii=False, indent=2), encoding="utf-8")
29
+ # cron example
30
+ (base / "cron.txt").write_text(
31
+ "0 */6 * * * nedviga-cli watch check --offline /path/fresh.json\n"
32
+ "0 8 * * * nedviga-cli digest --days 1 --format telegram\n",
33
+ encoding="utf-8",
34
+ )
35
+ # templates dir
36
+ tdir = base / "profiles" / "templates"
37
+ tdir.mkdir(parents=True, exist_ok=True)
38
+ (tdir / "metro_secondary.json").write_text(json.dumps({"rooms": 2, "metro_walk_max": 15}, ensure_ascii=False), encoding="utf-8")
39
+ (tdir / "novostroyka.json").write_text(json.dumps({"rooms": 2, "price_max": 15000000}, ensure_ascii=False), encoding="utf-8")
40
+ (tdir / "investment.json").write_text(json.dumps({"price_max": 12000000}, ensure_ascii=False), encoding="utf-8")
41
+ return {"ok": True, "out": str(base)}
42
+
43
+
44
+ def doctor(json_out: bool = False) -> dict:
45
+ import sys
46
+ state_dir = Path(os.environ.get("NEDVIGA_STATE_DIR", str(Path.home() / ".local" / "share" / "nedviga")))
47
+ checks = []
48
+ checks.append({"name": "python", "ok": sys.version_info >= (3, 11), "detail": sys.version.split()[0]})
49
+ checks.append({"name": "state_dir_writable", "ok": _writable(state_dir), "detail": str(state_dir)})
50
+ egress = os.environ.get("NEDVIGA_EGRESS_PROXY")
51
+ checks.append({"name": "egress_mode", "ok": bool(egress), "detail": egress or "not set (direct mode)"})
52
+ tok_file = os.environ.get("NEDVIGA_MOS_TOKEN_FILE")
53
+ tok_ok = False
54
+ if tok_file:
55
+ try:
56
+ tok_ok = Path(tok_file).is_file() and Path(tok_file).stat().st_size > 0
57
+ except Exception:
58
+ tok_ok = False
59
+ checks.append({"name": "token_file", "ok": tok_ok, "detail": tok_file or "not set"})
60
+ # netguard import
61
+ try:
62
+ import nedviga.net_guard
63
+ netguard_ok = True
64
+ except Exception:
65
+ netguard_ok = False
66
+ checks.append({"name": "netguard", "ok": netguard_ok, "detail": "importable" if netguard_ok else "fail"})
67
+ # registry counts
68
+ try:
69
+ import sqlite3
70
+ db = state_dir / "mos_mkd.sqlite"
71
+ if db.exists():
72
+ conn = sqlite3.connect(str(db))
73
+ c1 = conn.execute("SELECT COUNT(*) FROM mkd").fetchone()[0]
74
+ c2 = 0
75
+ try:
76
+ c2 = conn.execute("SELECT COUNT(*) FROM mkd520").fetchone()[0]
77
+ except Exception:
78
+ c2 = 0
79
+ conn.close()
80
+ checks.append({"name": "registry", "ok": True, "detail": f"mkd={c1}, mkd520={c2}"})
81
+ else:
82
+ checks.append({"name": "registry", "ok": True, "detail": "no db yet"})
83
+ except Exception as exc:
84
+ checks.append({"name": "registry", "ok": False, "detail": str(exc)})
85
+ ok = all(c["ok"] for c in checks if c["name"] in {"python", "state_dir_writable", "netguard"})
86
+ return {"ok": ok, "checks": checks}
87
+
88
+
89
+ def _writable(p: Path) -> bool:
90
+ try:
91
+ p.mkdir(parents=True, exist_ok=True)
92
+ test = p / ".wizard_write_test"
93
+ test.write_text("x")
94
+ test.unlink()
95
+ return True
96
+ except Exception:
97
+ return False
98
+
99
+
100
+ def render_text(res: dict) -> str:
101
+ lines = []
102
+ for c in res["checks"]:
103
+ mark = "OK" if c["ok"] else "FAIL"
104
+ lines.append(f"[{mark}] {c['name']}: {c['detail']}")
105
+ lines.append("overall: " + ("OK" if res["ok"] else "FAIL"))
106
+ return "\n".join(lines)
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: nedviga-cli
3
+ Version: 0.1.0
4
+ Summary: CLI and SDK for Moscow secondary real estate monitoring
5
+ Author-email: lnfnt <lnfnt@proton.me>
6
+ License-Expression: AGPL-3.0-only
7
+ Keywords: real-estate,pricing,offline-analysis,cli
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Requires-Dist: pydantic>=2
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: requests>=2.28
15
+ Requires-Dist: camoufox[geoip]>=0.4
16
+ Requires-Dist: Pillow>=10.0
17
+ Requires-Dist: beautifulsoup4[lxml]>=4.12
18
+ Provides-Extra: cli
19
+ Requires-Dist: click>=8; extra == "cli"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Provides-Extra: pdf
23
+ Requires-Dist: weasyprint>=60; extra == "pdf"
24
+ Dynamic: license-file
25
+
26
+ # nedviga-cli
27
+
28
+ ![license](https://img.shields.io/badge/license-AGPL--3.0-orange) ![python](https://img.shields.io/badge/python-3.11%2B-blue) ![release](https://img.shields.io/badge/release-v0.1.0-informational) ![MCP](https://img.shields.io/badge/MCP-ready-brightgreen) ![artifacts](https://img.shields.io/badge/artifacts-GPG--signed-green)
29
+
30
+ <p align="center"><img src="assets/poster.jpg" width="820" alt="nedviga-cli poster"></p>
31
+
32
+ CLI и Python SDK для подбора и анализа жилой недвижимости Москвы. Рабочий цикл: поиск,
33
+ карточка решения, сравнение, торг, проверка новостроек/застройщиков, инфраструктура,
34
+ мониторинг изменений, отчёт. Для ИИ-агентов: MCP-сервер, JSON-конверты, роли, аудит.
35
+
36
+ **Русский** · [<img src="assets/flags/gb.svg" height="16" align="top" alt="EN"> English](README.en.md)
37
+
38
+ ## Установка
39
+
40
+ ```bash
41
+ pip install git+https://github.com/lnfnt/nedviga-cli.git
42
+ nedviga-cli wizard doctor
43
+ ```
44
+
45
+ На PyPI пакет не опубликован. Checkout: `python3.11 -m venv .venv && source .venv/bin/activate
46
+ && pip install -e ".[cli,dev]"`. Python >= 3.11.
47
+ Один раз подготовить браузерный рантайм для live-адаптеров:
48
+
49
+ ```bash
50
+ XDG_CACHE_HOME="$HOME/.cache/nedviga-py" python3 -m camoufox fetch
51
+ ```
52
+
53
+ ## Возможности
54
+
55
+ - **Источники:** ЦИАН, Авито, Домклик (только через ваш egress `NEDVIGA_EGRESS_PROXY`), открытые
56
+ данные Москвы (досья домов, ЖКХ, реестр новостроек, застройщики по ИНН с банками и судами,
57
+ шум-мониторинг). ЕГРН не используется — модуль приёма и проверки выписки от собственника.
58
+ - **Аналитика решения:** семейная карта объекта, сравнительная матрица, медианы торга по
59
+ комплексу (`insufficient_sample` при малой выборке), антиприманочные флаги, risk-скоринг,
60
+ юридические триаж-маркеры (11 флагов).
61
+ - **Цикл сделки:** пайплайн статусов (shortlist→offer→…), watch цены/снятия/перевыставления,
62
+ дайджесты, автоподбор по профилю, цепочки альтернатив, скоринг соответствия.
63
+ - **Артефакты:** PDF/HTML-каталог, PPTX-презентации (`deck`), план показов, прозвон-листы,
64
+ чек-листы передачи юристу.
65
+ - **Инфраструктура:** POI в пешей доступности, загруженность дорог и почасовой прогноз
66
+ (web-оценка, помечена как таковая), замеры шума с датами и дистанциями.
67
+ - **Безопасность:** fail-closed сетевой страж, spotlighting внешнего текста
68
+ `<external_untrusted_data>`, роли и per-record аудит, ПДн-минимизация (контакты — только по
69
+ явному флагу с указанной целью), идемпотентный purge.
70
+
71
+ ## Быстрый старт
72
+
73
+ ```bash
74
+ nedviga-cli wizard init --profile family --out ./kit
75
+ nedviga-cli search --source cian --rooms 2 --price-max 20000000 \
76
+ --geo-districts "ЮАО,ЮЗАО" --out cards.json
77
+ nedviga-cli compare --file cards.json --format telegram
78
+ nedviga-cli card cards.json --format all --out ./out
79
+ nedviga-cli watch add --file cards.json
80
+ nedviga-cli watch check --offline fresh.json
81
+ nedviga-cli noise sync && nedviga-cli noise --lat 55.72 --lon 37.61
82
+ nedviga-cli dossier match --address "улица Эталонная, дом 4"
83
+ nedviga-cli source
84
+ ```
85
+
86
+ Примеры по каждой команде — в `--help`.
87
+
88
+ ### Конфигурация
89
+
90
+ | Переменная | Назначение |
91
+ |---|---|
92
+ | `NEDVIGA_EGRESS_PROXY` | HTTP-выход для браузерных адаптеров; без него живые источники = `source_disabled` |
93
+ | `NEDVIGA_MOS_TOKEN_FILE` | файл ключа api.mos.ru (не значение в env) |
94
+ | `NEDVIGA_STATE_DIR` | каталог состояния |
95
+ | `NEDVIGA_FEED_URL` | опциональный сервер манифестов свежести адаптеров |
96
+
97
+ ## Контракт вывода
98
+
99
+ Один JSON-конверт в stdout (`ok/status/reason/source/data/metadata`), логи в stderr.
100
+
101
+ | Код | Значение |
102
+ |---|---|
103
+ | 0 | данные получены, контракт валиден |
104
+ | 1 | критично для окружения (нет браузерного рантайма/кэша) |
105
+ | 2 | деградация источника (WAF, капча, rate-limit) |
106
+ | 3 | ошибка источника/парсера (изменение DOM, `source_disabled`, `source_blocked`) |
107
+ | 4 | невалидные аргументы CLI |
108
+
109
+ ## Ограничения
110
+
111
+ - Данные московские; для других городов адаптеров нет.
112
+ - Прогноза цен нет: медианы и watch-дельты — факты с датой.
113
+ - Прогноз транспорта — web-оценка, помечается как негарантийная.
114
+ - Контакты ПДн извлекаются только по явному флагу.
115
+
116
+ ## Python API
117
+
118
+ ```python
119
+ from nedviga.sources_avito import parse_livetext
120
+ from nedviga.deck import deck_from_cards
121
+ cards = parse_livetext(open("avito_text.txt").read())
122
+ deck_from_cards(cards, "подборка.pptx", title="ЮАО, двушки")
123
+ ```
124
+
125
+ ## Лицензия и проверка релиза
126
+
127
+ AGPL-3.0-only; сетевой сервис на этом коде обязан предоставить исходники (§13,
128
+ `nedviga-cli source`). Артефакты: `sha256sum -c SHA256SUMS`, `gpg --verify *.asc` (ключ в релизе).
129
+
130
+ Автор и правообладатель: [lnfnt](mailto:lnfnt@proton.me).
@@ -0,0 +1,71 @@
1
+ nedviga/__init__.py,sha256=YizGV0QnpF4IqZqnYfix-mI4jMjVdUV3DwfervHfnj8,1239
2
+ nedviga/__main__.py,sha256=ThepMt9BtHVElr7tT2VsFgwGReNtd5Pfofq1oRWCmPM,87
3
+ nedviga/_wm.py,sha256=pnU0CJvreeFOnQZ8gEZbsQA3olYOjdUB07jmr9KO_48,4409
4
+ nedviga/adaptive_selectors.py,sha256=3FfVBWOkYFtJYThIWJz0cUCs7K_cTMWYMlEBhElXKcE,4210
5
+ nedviga/auditlog.py,sha256=-CkvZJ8rkziK0_WDgohZ13AlyeP6kzQMrqSZLp2wv2o,2721
6
+ nedviga/auto.py,sha256=L5WC550XC2l9m7KPQL1rWbsOIp6GhKuky00H5sYZAxA,2770
7
+ nedviga/bargain.py,sha256=CFy_q5HcQkvmnmBWMbcx6GsFwrCy3wqLDXS22KXsR24,6688
8
+ nedviga/browser_native.py,sha256=IDjIwUChtkJDhspcmfmOP0Wth2xghDb8YMysu8A6MUY,8479
9
+ nedviga/callsheet.py,sha256=7z6P-IW2qiTkJKJk04O9iV-I-vghFgS7FXHUccntNA4,2562
10
+ nedviga/chains.py,sha256=wuDKTWRixEK77BvmpZDQdfVqWwil3jvs1Rkt1WRU8AY,2745
11
+ nedviga/checklist.py,sha256=_yUwMrv6lh9WX6tzWadfcBMWGmnrITqdBnfg0Z7YWE8,4238
12
+ nedviga/cian_card.py,sha256=YJtC0fxP4JlYgnB3_-zyaAV4v8isfdG70sHPgPOdSZk,51986
13
+ nedviga/cian_live.py,sha256=6kutTTjvwvP4JIMbohTddw2VAxwSVzXJDb1MDPSxvmg,32197
14
+ nedviga/cli.py,sha256=NqeKXU-UY5HD4CX_qcSkTiDiDu2L8Bmp4KRqTisiPqI,147630
15
+ nedviga/compare.py,sha256=wLQGiLp2dP__1bn94TfpgsxzU9cWr5PqgJ8LFzAGUMQ,7124
16
+ nedviga/deck.py,sha256=Th4Iz_XTr_UDTJ03I_FcQQvp6Itahegm7TMCHVd17K8,5391
17
+ nedviga/dedup.py,sha256=N524Ylh_V0L_KOnehla6_2LkXWwH8cr-_bVODu-1-5Y,7759
18
+ nedviga/developer.py,sha256=R5vwt80ixqIzSuCBWwbLAFoI94ieOUoFYTdcrs43SWQ,5685
19
+ nedviga/digest.py,sha256=Ko9ELqXQmPElxgCtVlD9vwMLy5sXM4hxO4AfTzOgAMw,7126
20
+ nedviga/doctor.py,sha256=KzOZiIzfmFta30GEnz4G5sEwVThj6j_3lI8Zjl5Urnk,5064
21
+ nedviga/dossier.py,sha256=-f_Rnz2PSg1K-O9YAn_1YWVmr9-TCMRkkqZPvtaHtYg,15611
22
+ nedviga/eco.py,sha256=w1XpFE6eh3p2RT2EeWhCmmxVMNDGIPH6kQt2lqSdeS4,3155
23
+ nedviga/errors.py,sha256=iQJK2bDJFOdu1qzsBgrLkfPoNr80sLe-aP0eIZMqui8,610
24
+ nedviga/export_html.py,sha256=NJsZPDXHc1kt3zonYTQl__lmc6K7o72GWNB78HxNMp0,11987
25
+ nedviga/export_pdf.py,sha256=0jeGnWEjhHVeNT6TK7m4D5Uw1Nkn0XJ8EWjgL2WOmDk,3544
26
+ nedviga/family_card.py,sha256=5xfjmuPIyHG0SmTLFdttiUi_Q0t2bRLlpy1DoJhuoxE,26761
27
+ nedviga/feedback.py,sha256=AFoppVCjIYysXT7PvFDbTdgDahA_cPOut50R6blmSLU,2867
28
+ nedviga/geo_routing.py,sha256=_k0t-dwCK9Md0gjzlahMrVNjPXTE6xgR4b0dkiJN8gA,7720
29
+ nedviga/graph.py,sha256=KqPElp4kAg2eAI7osogek7KjaSs1vokYc0fXFsCTyVA,6164
30
+ nedviga/legal_triage.py,sha256=KWWT2SOuBT7nN7FMvi3POILNj9B8Ud-FXYVj34G9QV4,3160
31
+ nedviga/litprecheck.py,sha256=-6V9qQd5mVnBNBPqCpzriOSIkapgOqMAI2_wi_2EAUs,3823
32
+ nedviga/market.py,sha256=-zzoXBOj7mnez0eGa-nZqK0DWK779sl-a8FAZ4vzROU,3581
33
+ nedviga/mcp_server.py,sha256=FYDcSABQSNWA60wYIRYXOOoU2KqW46pVDAWVu452c_s,7124
34
+ nedviga/metro_directory.json,sha256=Pnf89sBjX0pWYIAhk7Lw3AIx9yNDmTnhpL7uslIvq8M,164807
35
+ nedviga/metro_vestibules.py,sha256=-9rO1iF0em1Tl1SNs6F3cYh_IaTXnkJ-dSqFjIX_hoQ,6226
36
+ nedviga/models.py,sha256=j6natBfa3xMwTKx-9fn4iV3TTyTMqbZ8zpQJcNYoLQs,6057
37
+ nedviga/mosdata.py,sha256=rWUFbL3ydnAQFCT8Yw-KHx44IY6y_BVBYfB476DLJVU,3393
38
+ nedviga/net_guard.py,sha256=qTYZJl-ohEIl9467YXCNLNCWjeIGwQdfemBc0OFYeWY,39271
39
+ nedviga/noise.py,sha256=EGRcN2TCf6H1DIw3VP-LDpREkszTJgYMPOJIViuo-Ik,6906
40
+ nedviga/ops.py,sha256=sKaL2z3FVW3ntJHQr0x0SQcj8mzPzTkEVC8rg8GbK2w,3385
41
+ nedviga/outcomes.py,sha256=6GrsVITn1JofjoDNXakuP64oO7bkdzCz41fmcfuaB9I,791
42
+ nedviga/pii.py,sha256=QZh5IAkvLVLzM18x2TAwuzqc9t5e0lM6J4oAL-NjLh4,3497
43
+ nedviga/pipeline.py,sha256=iuaWwMQoOwiT5KrBk5K6tTi-kmuVRR--raCUhmBVj3I,8533
44
+ nedviga/poi.py,sha256=7Yt7qnJtdBCSEvL2Oi2_3h1aPmQbFsHoUinxn0k3tsI,6480
45
+ nedviga/price_guard.py,sha256=4_i0vZs-1ZPk-rn6ituzXHeplZPf_SqGCROazfcJLW0,3111
46
+ nedviga/profiles.py,sha256=7BsDTcc2ZO96_Dvm7cRpZoq4BfDsXWZ_plaQL-VsS8o,5103
47
+ nedviga/projects.py,sha256=DTHvP7peCYPwfEZynrSuLH3MmaG3IR3Ax6lYDJy8SCc,3985
48
+ nedviga/purge.py,sha256=TpgS2tLpJ7Yvc73Vmo4Cr7O1GlXVf8ZrbDCpTj--VKw,4175
49
+ nedviga/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ nedviga/risk.py,sha256=DrzE_SPxW_2uvfG6ECWbyAk6W7-SYnGPk664ZVItT4Q,4267
51
+ nedviga/roles.py,sha256=gpHq5R9RzAKqV0XR9rVoVJuAAX4ztfGL3MXbdsHHTVQ,3702
52
+ nedviga/routing_cache.py,sha256=-MsnZ-6R0Q5N6hens0VdRbO5AySwYJrS6H8yCI52eR4,2489
53
+ nedviga/rush.py,sha256=6CWpo0VpFpe5O3cxsTUxGdOObRWVolEZ051ZX2hIcaw,6245
54
+ nedviga/scorefit.py,sha256=wYaaY2ewiIozCInrgLEQjjqbiQReQs8JptK3ECm9rwA,4340
55
+ nedviga/sources.py,sha256=I4UOAqcMnO8YZiNTWzEZHaI6w9xJ5yMRMRQBiAjJq50,1345
56
+ nedviga/sources_avito.py,sha256=RaWo845E6EKqwkROFcsItRVK8K-Z7-QLmtJQcALUiqo,5773
57
+ nedviga/sources_domclick.py,sha256=GeH6AsrF3Z3IzJmAvENDDhD4L1hQk6fG9E8eTZYeLtE,5631
58
+ nedviga/stale.py,sha256=IhQVZmGVT_1AjpCBGz_jqDY8yIObIyWPPpRbdEDpt9o,2935
59
+ nedviga/textsearch.py,sha256=S7VxWhNtWwkpvNe_IG0Ax19Q5QHu11rPwCe1b7_2mIs,5497
60
+ nedviga/tour.py,sha256=m-nHVbBY_HJvII2hPTj8n9Tic46TmdX0pukLGn4Xxi0,5979
61
+ nedviga/watch.py,sha256=n25_sUlN0_RiOfrg40bgXhCjTrGhzBDNBno6iaVTgf0,8970
62
+ nedviga/wizard.py,sha256=5PaolSOQre1SevyW6h4dKEcGQiZGwodkIV3TTSE4Aoc,4377
63
+ nedviga/data/legal_flags.json,sha256=1enDxBota7ilVsvlZBRLAyNJgmjzHckZ3vsZ0l4BwWU,12812
64
+ nedviga/data/release_pubkey.asc,sha256=RD6vXL0Q8LAeeIeLwjDF-pYqvRbLfkKZo7NVd9n5K1k,1217
65
+ nedviga_cli-0.1.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
66
+ nedviga_cli-0.1.0.dist-info/licenses/NOTICE,sha256=tsPyhh3tjjN8WqjLmHXMZXI995f8pShogFxTEJAKCc8,84
67
+ nedviga_cli-0.1.0.dist-info/METADATA,sha256=FBzidmrOJzNq53AlsKLizsLdWMB2mlb7X55nNtjAY1g,7031
68
+ nedviga_cli-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
69
+ nedviga_cli-0.1.0.dist-info/entry_points.txt,sha256=0QlKa7YpuULWp69G2FOS31vK0a8zZ_UN2-exYarYNFg,49
70
+ nedviga_cli-0.1.0.dist-info/top_level.txt,sha256=q_geT-MbWo9NAzfkOWH5WrRX-qVClPc_TIp-AQK4YXA,8
71
+ nedviga_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nedviga-cli = nedviga.cli:main