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/compare.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""v0.4.9 (the task brief) F-12: comparison matrix."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from .models import ListingCard
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _listing(card: Any) -> ListingCard:
|
|
11
|
+
if isinstance(card, ListingCard):
|
|
12
|
+
return card
|
|
13
|
+
if isinstance(card, dict):
|
|
14
|
+
return ListingCard(**card)
|
|
15
|
+
raise TypeError("unsupported card")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from .family_card import _human as _decoat # v0.4.9 контроль качества: тот же контракт очистки
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _md_cell(s):
|
|
24
|
+
return _decoat(str(s)).replace("|", "\|")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _html_cell(s):
|
|
28
|
+
from html import escape
|
|
29
|
+
return escape(_decoat(str(s)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_cards(path: str, ids: Optional[str] = None) -> list[ListingCard]:
|
|
33
|
+
raw = __import__("json").loads(Path(path).read_text(encoding="utf-8"))
|
|
34
|
+
if isinstance(raw, dict):
|
|
35
|
+
if "cards" in raw and isinstance(raw["cards"], list):
|
|
36
|
+
raw = raw["cards"]
|
|
37
|
+
elif "data" in raw:
|
|
38
|
+
d = raw["data"]
|
|
39
|
+
raw = d if isinstance(d, list) else ([d] if isinstance(d, dict) else [])
|
|
40
|
+
else:
|
|
41
|
+
raw = [raw]
|
|
42
|
+
if not isinstance(raw, list):
|
|
43
|
+
raw = [raw]
|
|
44
|
+
cards = [_listing(x) for x in raw if isinstance(x, (dict, ListingCard))]
|
|
45
|
+
if ids:
|
|
46
|
+
wanted = {x.strip() for x in ids.split(",") if x.strip()}
|
|
47
|
+
cards = [c for c in cards if c.id in wanted]
|
|
48
|
+
return cards
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ppm2(c: ListingCard) -> float:
|
|
52
|
+
v = c.pricing.price_per_m2_rub
|
|
53
|
+
return float(v) if v is not None else float("inf")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _walk(c: ListingCard) -> str:
|
|
57
|
+
w = c.geo.honest_walk_route
|
|
58
|
+
if w and w.walk_duration_minutes is not None:
|
|
59
|
+
return f"{int(w.walk_duration_minutes)} мин ({w.walk_distance_meters} м)"
|
|
60
|
+
claimed = c.geo.cian_claimed_metro.claimed_time_minutes if c.geo.cian_claimed_metro else None
|
|
61
|
+
if claimed is not None:
|
|
62
|
+
return f"заявлено {claimed} мин"
|
|
63
|
+
return "нет данных"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _missing(c: ListingCard) -> str:
|
|
67
|
+
mf = getattr(c, "missing_fields", None) or []
|
|
68
|
+
return ", ".join(mf) if mf else ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _risk_str(c: ListingCard) -> str:
|
|
72
|
+
try:
|
|
73
|
+
from .risk import score_card
|
|
74
|
+
r = score_card(c)
|
|
75
|
+
if r.get("risk_score", 0) >= 0.5:
|
|
76
|
+
return "⚠ risk"
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sort_cards(cards: list[ListingCard]) -> list[ListingCard]:
|
|
83
|
+
return sorted(cards, key=_ppm2)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _best_worst(cards: list[ListingCard]) -> tuple[Optional[str], Optional[str]]:
|
|
87
|
+
if not cards:
|
|
88
|
+
return None, None
|
|
89
|
+
s = sort_cards(cards)
|
|
90
|
+
return s[0].id, s[-1].id
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def render_markdown(cards: list[ListingCard]) -> str:
|
|
94
|
+
if not cards:
|
|
95
|
+
return "empty_input"
|
|
96
|
+
cards = sort_cards(cards)
|
|
97
|
+
best, worst = _best_worst(cards)
|
|
98
|
+
lines = ["| # | Адрес/ID | Цена | ₽/м² | Район | Этаж | Пешком | Площадь | Серия/год | ⚠ |",
|
|
99
|
+
"|---|---|---|---|---|---|---|---|---|---|"]
|
|
100
|
+
for i, c in enumerate(cards, 1):
|
|
101
|
+
mark = ""
|
|
102
|
+
if c.id == best:
|
|
103
|
+
mark = " (best)"
|
|
104
|
+
if c.id == worst:
|
|
105
|
+
mark = " (worst)"
|
|
106
|
+
price = f"{c.pricing.price_total_rub:,.0f}".replace(",", " ") if c.pricing.price_total_rub else "—"
|
|
107
|
+
ppm2 = f"{c.pricing.price_per_m2_rub:,.0f}".replace(",", " ") if c.pricing.price_per_m2_rub else "—"
|
|
108
|
+
floor = f"{c.parameters.floor}/{c.parameters.floors_total}" if c.parameters.floor else "—"
|
|
109
|
+
series = c.building.house_series or ""
|
|
110
|
+
year = c.building.year_built or ""
|
|
111
|
+
series_year = f"{series} {year}".strip() or "—"
|
|
112
|
+
missing = _missing(c)
|
|
113
|
+
risk = _risk_str(c)
|
|
114
|
+
triage = _triage_str(c)
|
|
115
|
+
from collections import Counter
|
|
116
|
+
# compute dup inns once outside? do here cheap
|
|
117
|
+
dev_marker = ""
|
|
118
|
+
try:
|
|
119
|
+
inns = Counter(_developer_inn(x) for x in cards)
|
|
120
|
+
if _developer_inn(c) and inns[_developer_inn(c)] > 1:
|
|
121
|
+
dev_marker = "≈застр. " + _developer_inn(c)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
warn = "; ".join(x for x in (missing, risk, triage, dev_marker) if x) or "—"
|
|
125
|
+
lines.append(
|
|
126
|
+
f"| {i} | {_md_cell(c.title or c.id)}{mark} | {price} | {ppm2} | {c.geo.district or '—'} | {floor} | {_walk(c)} | {c.parameters.area_total_m2 or '—'} | {_md_cell(series_year)} | {_md_cell(warn)} |"
|
|
127
|
+
)
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def render_telegram(cards: list[ListingCard]) -> str:
|
|
132
|
+
if not cards:
|
|
133
|
+
return "empty_input"
|
|
134
|
+
cards = sort_cards(cards)
|
|
135
|
+
best, worst = _best_worst(cards)
|
|
136
|
+
lines = []
|
|
137
|
+
for c in cards:
|
|
138
|
+
mark = ""
|
|
139
|
+
if c.id == best:
|
|
140
|
+
mark = " ⭐"
|
|
141
|
+
if c.id == worst:
|
|
142
|
+
mark = " ⬇"
|
|
143
|
+
ppm2 = f"{c.pricing.price_per_m2_rub:,.0f}".replace(",", " ") if c.pricing.price_per_m2_rub else "—"
|
|
144
|
+
price = f"{c.pricing.price_total_rub:,.0f}".replace(",", " ") if c.pricing.price_total_rub else "—"
|
|
145
|
+
lines.append(f"• {_decoat(c.title or c.id)}{mark}: {price} ₽, {ppm2} ₽/м², {c.geo.district or '—'}, {_walk(c)}")
|
|
146
|
+
mf = _missing(c)
|
|
147
|
+
if mf:
|
|
148
|
+
lines.append(f" ⚠ Не хватает данных: {mf}")
|
|
149
|
+
rs = _risk_str(c)
|
|
150
|
+
if rs:
|
|
151
|
+
lines.append(f" {rs}")
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def render_html(cards: list[ListingCard]) -> str:
|
|
156
|
+
if not cards:
|
|
157
|
+
return "empty_input"
|
|
158
|
+
cards = sort_cards(cards)
|
|
159
|
+
best, worst = _best_worst(cards)
|
|
160
|
+
rows = ""
|
|
161
|
+
for c in cards:
|
|
162
|
+
mark = ""
|
|
163
|
+
if c.id == best:
|
|
164
|
+
mark = " (best)"
|
|
165
|
+
if c.id == worst:
|
|
166
|
+
mark = " (worst)"
|
|
167
|
+
price = f"{c.pricing.price_total_rub:,.0f}".replace(",", " ") if c.pricing.price_total_rub else "—"
|
|
168
|
+
ppm2 = f"{c.pricing.price_per_m2_rub:,.0f}".replace(",", " ") if c.pricing.price_per_m2_rub else "—"
|
|
169
|
+
rows += f"<tr><td>{_html_cell(c.title or c.id)}{mark}</td><td>{price}</td><td>{ppm2}</td><td>{_html_cell(c.geo.district if c.geo else '')}</td><td>{c.parameters.floor or ''}/{c.parameters.floors_total or ''}</td><td>{_walk(c)}</td></tr>\n"
|
|
170
|
+
return f"<!DOCTYPE html><html><body><table>{rows}</table></body></html>"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render(cards: list[ListingCard], fmt: str = "md") -> str:
|
|
174
|
+
if not cards:
|
|
175
|
+
return "empty_input"
|
|
176
|
+
if fmt == "telegram":
|
|
177
|
+
return render_telegram(cards)
|
|
178
|
+
if fmt == "html":
|
|
179
|
+
return render_html(cards)
|
|
180
|
+
return render_markdown(cards)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _developer_inn(c: ListingCard) -> str:
|
|
184
|
+
try:
|
|
185
|
+
import re
|
|
186
|
+
text = (c.description_text or "") + " " + (c.title or "")
|
|
187
|
+
m = re.search(r"(?<!\d)(\d{10}|\d{12})(?!\d)", text)
|
|
188
|
+
if m and ("застройщик" in text.lower() or "девелопер" in text.lower()):
|
|
189
|
+
return m.group(1)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
return ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _triage_str(c: ListingCard) -> str:
|
|
196
|
+
try:
|
|
197
|
+
from .legal_triage import triage
|
|
198
|
+
t = triage(c)
|
|
199
|
+
codes = [f["code"] for f in t.get("flags", [])]
|
|
200
|
+
if codes:
|
|
201
|
+
return "⚠ " + ", ".join(codes)
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
return ""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.1",
|
|
3
|
+
"approved_by": "colleague",
|
|
4
|
+
"source": "internal data-provenance review (v1.0.0) + litcheck v1.0.1",
|
|
5
|
+
"disclaimer": "Выявленные маркеры не являются юридическим заключением или правовой консультацией. Сведения носят предварительный ориентировочный характер и требуют проверки документов профильным юристом.",
|
|
6
|
+
"flags": [
|
|
7
|
+
{
|
|
8
|
+
"code": "SHARE_IN_RIGHT",
|
|
9
|
+
"severity": "amber",
|
|
10
|
+
"title": "Маркер долевой собственности",
|
|
11
|
+
"markers": [
|
|
12
|
+
"доля",
|
|
13
|
+
"долей",
|
|
14
|
+
"доле"
|
|
15
|
+
],
|
|
16
|
+
"check": "Требуется установить размер доли, порядок пользования объектом и соблюдение преимущественного права покупки сособственниками (ст. 250 ГК РФ)."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"code": "APARTMENTS_NON_RESIDENTIAL",
|
|
20
|
+
"severity": "amber",
|
|
21
|
+
"title": "Маркер нежилого фонда (апартаменты)",
|
|
22
|
+
"markers": [
|
|
23
|
+
"апартамент"
|
|
24
|
+
],
|
|
25
|
+
"check": "Требуется проверить правовой статус помещения (нежилое помещение, отсутствие статуса жилого фонда, особенности налогообложения, тарифов ЖКУ и невозможности постоянной регистрации по месту жительства)."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"code": "BANKRUPTCY_AUCTION_MARKERS",
|
|
29
|
+
"severity": "red",
|
|
30
|
+
"title": "Маркер реализации на торгах / банкротства",
|
|
31
|
+
"markers": [
|
|
32
|
+
"торги",
|
|
33
|
+
"торгов",
|
|
34
|
+
"банкрот",
|
|
35
|
+
"конкурс",
|
|
36
|
+
"конкурсн"
|
|
37
|
+
],
|
|
38
|
+
"check": "Требуется проверить основания отчуждения, статус процедуры банкротства продавца/правопредшественника по картотеке ЕФРСБ и kad.arbitr.ru, полномочия финансового управляющего и судебные акты."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"code": "ENCUMBRANCE_RESTRICTION",
|
|
42
|
+
"severity": "red",
|
|
43
|
+
"title": "Маркер обременения / ограничения прав",
|
|
44
|
+
"markers": [
|
|
45
|
+
"обременен",
|
|
46
|
+
"арест",
|
|
47
|
+
"ипотек",
|
|
48
|
+
"залог"
|
|
49
|
+
],
|
|
50
|
+
"check": "Требуется истребовать актуальную выписку из ЕГРН от собственника для проверки действующих записей об ипотеке, арестах, запретах на совершение регистрационных действий и судебных спорах."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"code": "RECENT_INHERITANCE",
|
|
54
|
+
"severity": "conditional",
|
|
55
|
+
"title": "Маркер основания приобретения права по наследству",
|
|
56
|
+
"markers": [
|
|
57
|
+
"наследств",
|
|
58
|
+
"по наследству"
|
|
59
|
+
],
|
|
60
|
+
"severity_rule": {
|
|
61
|
+
"if_text": "лет",
|
|
62
|
+
"threshold_years": 3,
|
|
63
|
+
"red_if_under": true,
|
|
64
|
+
"else": "amber"
|
|
65
|
+
},
|
|
66
|
+
"check": "Требуется проверить основание приобретения права (по закону / по завещанию), состав наследственной массы, круг наследников и риски появления непризванных наследников или восстановления сроков принятия наследства (ст. 1155 ГК РФ)."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"code": "MINOR_OWNERS",
|
|
70
|
+
"severity": "red",
|
|
71
|
+
"title": "Маркер участия несовершеннолетних",
|
|
72
|
+
"markers": [
|
|
73
|
+
"несовершеннолетн",
|
|
74
|
+
"малолетн",
|
|
75
|
+
"детей-собственник"
|
|
76
|
+
],
|
|
77
|
+
"check": "Требуется проверить наличие несовершеннолетних собственников/сособственников и необходимость получения предварительного разрешения органа опеки и попечительства на отчуждение имущества (ст. 37 ГК РФ, ст. 21 ФЗ N 48-ФЗ)."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"code": "MATERNITY_CAPITAL",
|
|
81
|
+
"severity": "red",
|
|
82
|
+
"title": "Маркер использования средств материнского капитала",
|
|
83
|
+
"markers": [
|
|
84
|
+
"материнск",
|
|
85
|
+
"маткапитал",
|
|
86
|
+
"семейного капитал"
|
|
87
|
+
],
|
|
88
|
+
"check": "Требуется проверить факт и полноту исполнения обязательства по выделению долей детям и супругу при покупке/погашении ипотеки с привлечением средств материнского (семейного) капитала (ФЗ N 256-ФЗ)."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"code": "REDEVELOPMENT_UNAPPROVED",
|
|
92
|
+
"severity": "amber",
|
|
93
|
+
"title": "Маркер перепланировки / переустройства",
|
|
94
|
+
"markers": [
|
|
95
|
+
"перепланиров",
|
|
96
|
+
"узаконим",
|
|
97
|
+
"узаконена",
|
|
98
|
+
"соединил",
|
|
99
|
+
"объединил"
|
|
100
|
+
],
|
|
101
|
+
"check": "Требуется проверить соответствие фактической планировки поэтажному плану и экспликации БТИ/техпаспорту, наличие акта приемочной комиссии и решения о согласовании перепланировки (ст. 26, 28 ЖК РФ)."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"code": "POWER_OF_ATTORNEY",
|
|
105
|
+
"severity": "red",
|
|
106
|
+
"title": "Маркер отчуждения по доверенности",
|
|
107
|
+
"markers": [
|
|
108
|
+
"по доверенност",
|
|
109
|
+
"доверенность",
|
|
110
|
+
"доверенност"
|
|
111
|
+
],
|
|
112
|
+
"check": "Требуется проверить действительность доверенности через реестр доверенностей ФНП (reestr-dover.ru), объем специальных полномочий на продажу и получение денежных средств, а также факт личного волеизъявления и правоспособности собственника."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"code": "RENTAL_TENANCY_BURDEN",
|
|
116
|
+
"severity": "amber",
|
|
117
|
+
"title": "Маркер действующего договора найма/аренды",
|
|
118
|
+
"markers": [
|
|
119
|
+
"с арендатор",
|
|
120
|
+
"сдается",
|
|
121
|
+
"арендный бизнес",
|
|
122
|
+
"проживают нанимател",
|
|
123
|
+
"снимают"
|
|
124
|
+
],
|
|
125
|
+
"check": "Требуется проверить условия договора найма/аренды, основания и сроки прекращения пользования, а также соблюдение прав нанимателей при смене собственника (ст. 675 ГК РФ)."
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"checklist": [
|
|
129
|
+
"Истребовать актуальную выписку из ЕГРН об основных характеристиках и зарегистрированных правах, полученную собственником (включая проверку общедоступных сведений и данных о правообладателе с учетом ограничений ст. 36.3 ФЗ N 218-ФЗ).",
|
|
130
|
+
"Запросить и проверить правоустанавливающие документы продавца (договор купли-продажи / дарения / приватизации, свидетельство о праве на наследство, решение суда) и историю перехода прав.",
|
|
131
|
+
"Проверить сведения о продавце и правопредшественниках в Едином федеральном реестре сведений о банкротстве (ЕФРСБ) и картотеке арбитражных дел (kad.arbitr.ru) на предмет процедур банкротства или заявлений о несостоятельности.",
|
|
132
|
+
"Проверить наличие судебных споров и исполнительных производств в отношении собственника по базам судов общей юрисдикции и Банку данных исполнительных производств ФССП России.",
|
|
133
|
+
"Истребовать документ жилищного учета: единый жилищный документ (ЕЖД) либо выписку из домовой книги (справку о зарегистрированных лицах, включая архивную/расширенную) и справку об отсутствии задолженности по оплате ЖКУ и взносам на капитальный ремонт.",
|
|
134
|
+
"Запросить нотариально удостоверенное согласие супруга продавца на отчуждение объекта (ст. 35 СК РФ) либо нотариальное заявление/брачный договор об отсутствии брачных обязательств в отношении объекта.",
|
|
135
|
+
"Проверить привлечение средств материнского капитала: истребовать справку СФР об остатке маткапитала (при наличии детей) и проверить исполнение обязательства по наделению долями несовершеннолетних.",
|
|
136
|
+
"При участии несовершеннолетних собственников: истребовать предварительное разрешение органа опеки и попечительства на совершение сделки по отчуждению имущества.",
|
|
137
|
+
"При отчуждении по доверенности: сверить реквизиты доверенности по публичному сервису проверки ФНП, подтвердить полномочия на отчуждение и получение средств, установить факт личного подтверждения намерений доверителем.",
|
|
138
|
+
"Сверить фактическое состояние объекта с поэтажным планом и экспликацией БТИ / техническим паспортом на предмет несогласованных перепланировок и переустройств.",
|
|
139
|
+
"Уточнить структуру и условия сделки (прямая продажа либо альтернативная цепочка, сроки освобождения объекта и порядок расчетов)."
|
|
140
|
+
],
|
|
141
|
+
"litcheck_phrases": {
|
|
142
|
+
"found": "По запросу «{query}» в судебных базах выявлено записей: {count}. Сведения носят ориентировочный характер, не являются подтверждением порока объекта/стороны и не подменяют юридическую экспертизу. Требуется проверка совпадения сторон, предмета спора (ст. 209, 301, 302 ГК РФ) и процессуального статуса дел.",
|
|
143
|
+
"unavailable": "Судебная проверка по запросу «{query}» недоступна (сервис sud-cli вернул статус unavailable). Сведения об отсутствии судебных споров не подтверждены. Вывод о юридической чистоте объекта/субъекта не сформирован.",
|
|
144
|
+
"checklist_addendum": "При наличии сигналов судебной проверки: сверить предмет найденных дел с кадастровым номером и историей объекта, исключить однофамильцев и споры третьих лиц по смежным помещениям."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
2
|
+
|
|
3
|
+
mDMEatuNdRYJKwYBBAHaRw8BAQdAhCwTDMQJHb058gVK5gVxruoOWkVkNQca6ypB
|
|
4
|
+
6iFKTb20F2xuZm50IDxsbmZudEBwcm90b24ubWU+iJYEExYKAD4CGyMFCwkIBwIC
|
|
5
|
+
IgIGFQoJCAsCBBYCAwECHgcCF4AWIQT0SS1Ht18S0CVADBkpHkrKyliQKgUCaty5
|
|
6
|
+
+gIZAQAKCRApHkrKyliQKtzGAQC9QT7oLj4VtKVuZGtGO4aKeD59amBS5swMNmmV
|
|
7
|
+
0udlvgD/ckrnxQgdbeYd4SsOMwXMgEnErARQ+zFuB7xGpRMTuQ60KUxFWCBSZWxl
|
|
8
|
+
YXNlIFNpZ25pbmcgPHJlbGVhc2VAbGV4LmludmFsaWQ+iHgEMBYKACAWIQT0SS1H
|
|
9
|
+
t18S0CVADBkpHkrKyliQKgUCaty7SwIdIAAKCRApHkrKyliQKrk7AP451tr+kWhv
|
|
10
|
+
3NU036l2oob3N1qieaOdfW/OU//6EP3z3AD/fVEUIRAMSWk5pWO3nr88oNSFCsOp
|
|
11
|
+
ljlBeVe+6bz9pQGIkwQTFgoAOxYhBPRJLUe3XxLQJUAMGSkeSsrKWJAqBQJq2411
|
|
12
|
+
AhsjBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJECkeSsrKWJAqLYYBAIB2
|
|
13
|
+
97PGcYgWCMay3OCdZLHaw94Y8EM7cANUXkEkXZBaAP9TqDXXdIckVD0PgaR7uqlf
|
|
14
|
+
xy5joOiiJpD+7DNTCOAwCbgzBGrbjXUWCSsGAQQB2kcPAQEHQK0hQe3FIYrB81xc
|
|
15
|
+
pAMzpslpaPtnsw/lnrM+nwh/bGdsiO8EGBYKACAWIQT0SS1Ht18S0CVADBkpHkrK
|
|
16
|
+
yliQKgUCatuNdQIbIgCBCRApHkrKyliQKnYgBBkWCgAdFiEEGcY2JlH+o1CD/Xpo
|
|
17
|
+
0srydffPPYgFAmrbjXUACgkQ0srydffPPYg2mwEAwWHbAUoLVkt7IqRZWFw+PzQ9
|
|
18
|
+
aypjfPDE+U1cIBsO7tMA/AzivcCd8eeePj1ZG6bg2IC+w+6wAGNnU3B3AXtbUs4K
|
|
19
|
+
dI0BALKkwaKcnXvjBgkUTU4Oam/sLX5D6Mi592/fZ/FCjS84AP4xpNOfHT3RafH3
|
|
20
|
+
qxiLNL+Ar2wmyztibvEbVDkJ/xYUAg==
|
|
21
|
+
=ogkP
|
|
22
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
nedviga/deck.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""v0.5.13 F-33: генератор презентаций (python-pptx) из подборок nedviga.
|
|
2
|
+
|
|
3
|
+
Шаблон нейтральный встроенный (мастер-слиды дефолтные), никаких внешних
|
|
4
|
+
файлов — продукт отдаёт структуру и данные, «дизайн» = чистая таблица.
|
|
5
|
+
Последний слайд — обязательный дисклеймер внешних источников.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import statistics
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
DISCLAIMER = ("Данные получены из внешних источников (ЦИАН, Авито, open data Москвы) "
|
|
14
|
+
"и могут быть неактуальны. Проверяйте цену, статус объявления и юридические "
|
|
15
|
+
"параметры на источнике перед решениями. Прогноз пробок не включён: "
|
|
16
|
+
"текущая загруженность ≠ прогноз час-пика.")
|
|
17
|
+
|
|
18
|
+
_CAP = 15
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fmt_money(v) -> str:
|
|
22
|
+
try:
|
|
23
|
+
return f"{int(v):,}".replace(",", " ")
|
|
24
|
+
except (TypeError, ValueError):
|
|
25
|
+
return "—"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _card_cells(c: dict) -> list[str]:
|
|
29
|
+
pr = c.get("pricing") or {}
|
|
30
|
+
pa = c.get("parameters") or {}
|
|
31
|
+
bu = c.get("building") or {}
|
|
32
|
+
loc = c.get("location") or {}
|
|
33
|
+
return [
|
|
34
|
+
str(c.get("id") or "—"),
|
|
35
|
+
_fmt_money(pr.get("price_total_rub")),
|
|
36
|
+
_fmt_money(pr.get("price_per_m2_rub")),
|
|
37
|
+
f"{pa.get('rooms', '—')}/{pa.get('area_total_m2', '—')}/{pa.get('floor', '—')}-{pa.get('floors_total', '—')}",
|
|
38
|
+
(bu.get("address_raw") or loc.get("address") or c.get("title") or "—")[:60],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def deck_from_cards(cards: list[dict], out_path: str, title: str = "Подборка недвижимости") -> dict:
|
|
43
|
+
"""Собирает pptx: титул → таблица топ-N → сводка рынка → дисклеймер
|
|
44
|
+
(+ family-слайд если карточка семейной карты присутствует)."""
|
|
45
|
+
from pptx import Presentation
|
|
46
|
+
from pptx.util import Inches, Pt
|
|
47
|
+
|
|
48
|
+
cards = cards[:_CAP]
|
|
49
|
+
prs = Presentation()
|
|
50
|
+
|
|
51
|
+
# 1. титул
|
|
52
|
+
s = prs.slides.add_slide(prs.slide_layouts[5])
|
|
53
|
+
s.shapes.title.text = title
|
|
54
|
+
sub = s.placeholders[1] if len(s.placeholders) > 1 else None
|
|
55
|
+
if sub is not None:
|
|
56
|
+
sub.text = f"nedviga-cli · {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')} · {len(cards)} объектов"
|
|
57
|
+
|
|
58
|
+
# 2. таблица карточек
|
|
59
|
+
s = prs.slides.add_slide(prs.slide_layouts[5])
|
|
60
|
+
s.shapes.title.text = "Объекты"
|
|
61
|
+
rows, cols = len(cards) + 1, 5
|
|
62
|
+
tbl = s.shapes.add_table(rows, cols, Inches(0.3), Inches(1.4),
|
|
63
|
+
Inches(9.4), Inches(0.4 * rows)).table
|
|
64
|
+
for j, h in enumerate(("ID", "Цена ₽", "₽/м²", "комн/м²/эт", "Адрес")):
|
|
65
|
+
cell = tbl.cell(0, j)
|
|
66
|
+
cell.text = h
|
|
67
|
+
for p in cell.text_frame.paragraphs:
|
|
68
|
+
for r in p.runs:
|
|
69
|
+
r.font.size = Pt(11)
|
|
70
|
+
r.font.bold = True
|
|
71
|
+
for i, c in enumerate(cards, start=1):
|
|
72
|
+
for j, v in enumerate(_card_cells(c)):
|
|
73
|
+
cell = tbl.cell(i, j)
|
|
74
|
+
cell.text = v
|
|
75
|
+
for p in cell.text_frame.paragraphs:
|
|
76
|
+
for r in p.runs:
|
|
77
|
+
r.font.size = Pt(10)
|
|
78
|
+
|
|
79
|
+
# 3. family-слайд если есть
|
|
80
|
+
fam = next((c for c in cards if c.get("family_card")), None)
|
|
81
|
+
if fam is not None:
|
|
82
|
+
s = prs.slides.add_slide(prs.slide_layouts[1])
|
|
83
|
+
s.shapes.title.text = "Семейная карта"
|
|
84
|
+
body = s.placeholders[1] if len(s.placeholders) > 1 else None
|
|
85
|
+
if body is not None:
|
|
86
|
+
body.text = str(fam.get("verdict") or fam.get("summary") or "")[:800]
|
|
87
|
+
|
|
88
|
+
# 4. сводка рынка (все числа с n — продуктовая константа)
|
|
89
|
+
prices = [c["pricing"]["price_total_rub"] for c in cards
|
|
90
|
+
if (c.get("pricing") or {}).get("price_total_rub")]
|
|
91
|
+
ppms = [c["pricing"]["price_per_m2_rub"] for c in cards
|
|
92
|
+
if (c.get("pricing") or {}).get("price_per_m2_rub")]
|
|
93
|
+
s = prs.slides.add_slide(prs.slide_layouts[1])
|
|
94
|
+
s.shapes.title.text = "Сводка"
|
|
95
|
+
body = s.placeholders[1] if len(s.placeholders) > 1 else None
|
|
96
|
+
lines = [f"Объектов в подборке: n={len(cards)}"]
|
|
97
|
+
if prices:
|
|
98
|
+
lines.append(f"Цена: медиана {_fmt_money(statistics.median(prices))} ₽, "
|
|
99
|
+
f"разброс {_fmt_money(min(prices))}–{_fmt_money(max(prices))} ₽ (n={len(prices)})")
|
|
100
|
+
if ppms:
|
|
101
|
+
lines.append(f"₽/м²: медиана {_fmt_money(statistics.median(ppms))} (n={len(ppms)})")
|
|
102
|
+
if body is not None:
|
|
103
|
+
body.text = "\n".join(lines)
|
|
104
|
+
|
|
105
|
+
# 5. обязательный дисклеймер
|
|
106
|
+
s = prs.slides.add_slide(prs.slide_layouts[1])
|
|
107
|
+
s.shapes.title.text = "Ограничения"
|
|
108
|
+
body = s.placeholders[1] if len(s.placeholders) > 1 else None
|
|
109
|
+
if body is not None:
|
|
110
|
+
body.text = DISCLAIMER
|
|
111
|
+
|
|
112
|
+
out = Path(out_path)
|
|
113
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
prs.save(str(out))
|
|
115
|
+
return {"status": "ok", "slides": len(prs.slides), "cards": len(cards), "out": str(out)}
|