sud-cli 0.1.1__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 (74) hide show
  1. sud_cli/__init__.py +3 -0
  2. sud_cli/__main__.py +4 -0
  3. sud_cli/_wm.py +118 -0
  4. sud_cli/agenda.py +90 -0
  5. sud_cli/autofill.py +192 -0
  6. sud_cli/autofill_markers.json +56 -0
  7. sud_cli/backup.py +256 -0
  8. sud_cli/calendar_ics.py +191 -0
  9. sud_cli/case_diff.py +165 -0
  10. sud_cli/case_search_bridge.py +1322 -0
  11. sud_cli/chain.py +226 -0
  12. sud_cli/cli.py +3721 -0
  13. sud_cli/completeness.py +105 -0
  14. sud_cli/config.py +20 -0
  15. sud_cli/conflict.py +210 -0
  16. sud_cli/conflicts.py +207 -0
  17. sud_cli/court_alerts.py +156 -0
  18. sud_cli/court_source.py +108 -0
  19. sud_cli/data/completeness_rules.json +18 -0
  20. sud_cli/data/key_rates.json +59 -0
  21. sud_cli/data/npa_historical_editions.json +19 -0
  22. sud_cli/db.py +2072 -0
  23. sud_cli/deadline_matrix.py +99 -0
  24. sud_cli/deadline_state.py +25 -0
  25. sud_cli/deadlines.py +8 -0
  26. sud_cli/deadlines_state.py +1 -0
  27. sud_cli/deferral.py +5 -0
  28. sud_cli/efrsb_manual.py +85 -0
  29. sud_cli/efrsb_public.py +210 -0
  30. sud_cli/egrul_nalog.py +150 -0
  31. sud_cli/explain.py +190 -0
  32. sud_cli/fees_extra.py +104 -0
  33. sud_cli/forms.py +204 -0
  34. sud_cli/fssp_manual.py +91 -0
  35. sud_cli/fssp_semi.py +285 -0
  36. sud_cli/fssp_stats.py +105 -0
  37. sud_cli/government_transfers.json +8 -0
  38. sud_cli/holidays_rf.json +62 -0
  39. sud_cli/idempotency.py +78 -0
  40. sud_cli/importing.py +575 -0
  41. sud_cli/interest.py +144 -0
  42. sud_cli/jurisdiction.py +177 -0
  43. sud_cli/jurisdiction_rules.json +265 -0
  44. sud_cli/legal_deadlines.py +278 -0
  45. sud_cli/legal_engine.py +558 -0
  46. sud_cli/legal_rules.json +1058 -0
  47. sud_cli/limitation.py +70 -0
  48. sud_cli/mcp_server.py +1594 -0
  49. sud_cli/metrics.py +142 -0
  50. sud_cli/npa_as_of.py +21 -0
  51. sud_cli/output.py +89 -0
  52. sud_cli/payment.py +24 -0
  53. sud_cli/pdn.py +259 -0
  54. sud_cli/plenum_corpus.py +134 -0
  55. sud_cli/plenums_seed.json +45 -0
  56. sud_cli/practice_status.py +136 -0
  57. sud_cli/pravo_publication.py +361 -0
  58. sud_cli/pretrial.py +151 -0
  59. sud_cli/production_calendar.py +233 -0
  60. sud_cli/providers.py +95 -0
  61. sud_cli/readiness.py +234 -0
  62. sud_cli/regional_holidays.py +37 -0
  63. sud_cli/reminders.py +92 -0
  64. sud_cli/reports.py +349 -0
  65. sud_cli/sud_netguard.py +943 -0
  66. sud_cli/timeline_audit.py +80 -0
  67. sud_cli/verify.py +201 -0
  68. sud_cli-0.1.1.dist-info/METADATA +119 -0
  69. sud_cli-0.1.1.dist-info/RECORD +74 -0
  70. sud_cli-0.1.1.dist-info/WHEEL +4 -0
  71. sud_cli-0.1.1.dist-info/entry_points.txt +2 -0
  72. sud_cli-0.1.1.dist-info/licenses/AUTHORS +5 -0
  73. sud_cli-0.1.1.dist-info/licenses/LICENSE.md +661 -0
  74. sud_cli-0.1.1.dist-info/licenses/NOTICE +7 -0
sud_cli/reports.py ADDED
@@ -0,0 +1,349 @@
1
+ """the wave §4 (S-84): отчёты портфеля и дела — только счётчики из реальных таблиц.
2
+
3
+ Правило «не выдуманных метрик»: здесь нет ни «успешности», ни «рисков», ни
4
+ «средних» — только то, что читается из cases/events/amounts/deadline_states/
5
+ evidence_items/case_representatives. Горизонт near/overdue переиспользует
6
+ логику agenda (W6a) — вторая копия окна не заводится, копии всегда расходятся.
7
+ Пустые секции выводятся явно [] / {}, а не «нет данных».
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from datetime import date as _date
12
+ from decimal import Decimal
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from .db import (
16
+ LEGAL_CASE_STATUSES,
17
+ get_case,
18
+ get_case_card,
19
+ get_connection,
20
+ init_db,
21
+ list_cases,
22
+ summarize_case_amounts,
23
+ representatives_card_section,
24
+ verify_case_evidence,
25
+ evidence_summary,
26
+ )
27
+
28
+ NEAR_HORIZON_DAYS = 30
29
+
30
+ REPORT_DISCLAIMER = (
31
+ "Отчёт справочный: считает только строки локального реестра sud-cli. "
32
+ "Статусы дел — внесённые оператором, не факт суда; суммы — внесённые "
33
+ "расходы; сроки — computed|confirmed из deadline_states. Метрик успеха, "
34
+ "риска или «качества ведения» здесь нет и быть не может: sud-cli их не "
35
+ "наблюдает."
36
+ )
37
+
38
+
39
+ def _zero() -> Decimal:
40
+ return Decimal("0")
41
+
42
+
43
+ def _near_overdue(state_dir: Any, today: _date, days: int):
44
+ """Сроки в окне [today, today+days] и просроченные — из agenda-билдера (W6a).
45
+
46
+ Окно не переопределяется второй копией: near/overdue берутся из
47
+ agenda_payload — там же живёт правило «закрытое дело не плодит пунктов»
48
+ и то же определение computed|confirmed. Здесь только суждение до
49
+ kind=deadline и подстановка days_left.
50
+ """
51
+ from .agenda import agenda_payload
52
+
53
+ payload = agenda_payload(state_dir, days=days, today=today)
54
+
55
+ def _pick(items):
56
+ out = []
57
+ for item in items:
58
+ if item.get("kind") != "deadline":
59
+ continue
60
+ out.append({
61
+ "case_id": item.get("case_id"),
62
+ "deadline_key": item.get("deadline_key"),
63
+ "due_at": item.get("due_at"),
64
+ "state": item.get("state"),
65
+ "days_left": (_date.fromisoformat(str(item["due_at"])[:10])
66
+ - today).days,
67
+ })
68
+ return out
69
+
70
+ return _pick(payload["upcoming"]), _pick(payload["overdue"]), payload
71
+
72
+
73
+ def _closed_dropped_deadlines(conn) -> List[dict]:
74
+ """Сроки закрытых дел, которые agenda-правило не показывает.
75
+
76
+ Переиспользование окна (W6a) означает, что у закрытого дела его computed/
77
+ confirmed сроки в near/overdue не попадают. Это не повод терять их молча:
78
+ они считаются здесь и выводятся отдельным списком с причиной, чтобы
79
+ приёмщик видел, что именно скрыло правило, а не «сроков нет».
80
+ """
81
+ from .db import list_all_deadline_states
82
+ from .reminders import _latest_per_key
83
+
84
+ closed = {c["case_id"] for c in list_cases(conn) if c.get("status") == "closed"}
85
+ dropped = []
86
+ for s in _latest_per_key(list_all_deadline_states(conn)):
87
+ if s.get("case_id") not in closed:
88
+ continue
89
+ if s.get("state") not in ("computed", "confirmed") or not s.get("due_at"):
90
+ continue
91
+ dropped.append({
92
+ "case_id": s.get("case_id"),
93
+ "deadline_key": s.get("deadline_key"),
94
+ "due_at": s.get("due_at"),
95
+ "state": s.get("state"),
96
+ "reason": "agenda-правило: закрытое дело не участвует в near/overdue",
97
+ })
98
+ dropped.sort(key=lambda i: (str(i["due_at"]), str(i["case_id"]),
99
+ str(i["deadline_key"])))
100
+ return dropped
101
+
102
+
103
+ def portfolio_payload(state_dir: Any, as_of: Optional[str] = None,
104
+ days: int = NEAR_HORIZON_DAYS) -> Dict[str, Any]:
105
+ init_db(state_dir)
106
+ conn = get_connection(state_dir)
107
+ try:
108
+ cases = list_cases(conn)
109
+ today = _date.today()
110
+ if as_of:
111
+ today = _date.fromisoformat(str(as_of)[:10])
112
+
113
+ by_status: Dict[str, int] = {}
114
+ for c in cases:
115
+ key = c.get("status") or "unknown"
116
+ by_status[key] = by_status.get(key, 0) + 1
117
+ # статусы enum'а всегда присутствуют в выводе: пустой портфель даёт
118
+ # нули по всем LEGAL_CASE_STATUSES, а не «секции нет».
119
+ statuses = {s: int(by_status.get(s, 0)) for s in LEGAL_CASE_STATUSES}
120
+ for s, n in by_status.items(): # значения вне enum — честно видно
121
+ if s not in statuses:
122
+ statuses[s] = n
123
+
124
+ spent = _zero()
125
+ planned = _zero()
126
+ refunded = _zero()
127
+ by_kind: Dict[str, str] = {}
128
+ amounts_cases = 0
129
+ for c in cases:
130
+ summary = summarize_case_amounts(conn, c["case_id"])
131
+ rows = summary["rows"]
132
+ if rows:
133
+ amounts_cases += 1
134
+ for r in rows:
135
+ dec = Decimal(str(r["total"]))
136
+ by_kind[r["kind"]] = str(Decimal(by_kind.get(r["kind"], "0")) + dec)
137
+ status = r["status"] or "planned"
138
+ if status == "paid":
139
+ spent += dec
140
+ elif status == "refunded":
141
+ refunded += dec
142
+ else:
143
+ planned += dec
144
+
145
+ reps_total = 0
146
+ reps_by_side: Dict[str, int] = {}
147
+ for c in cases:
148
+ section = representatives_card_section(conn, c["case_id"])
149
+ reps_total += section["count"]
150
+ for side, n in section["by_side"].items():
151
+ reps_by_side[side] = reps_by_side.get(side, 0) + n
152
+
153
+ evidence_total = int(conn.execute(
154
+ "SELECT COUNT(*) AS n FROM evidence_items").fetchone()["n"])
155
+ evidence_problems = int(conn.execute(
156
+ "SELECT COUNT(*) AS n FROM evidence_items WHERE sha256 IS NULL").fetchone()["n"])
157
+ documents_total = int(conn.execute(
158
+ "SELECT COUNT(*) AS n FROM case_documents").fetchone()["n"])
159
+ documents_revisions = int(conn.execute(
160
+ "SELECT COUNT(*) AS n FROM case_documents WHERE supersedes IS NOT NULL"
161
+ ).fetchone()["n"])
162
+ events_total = int(conn.execute(
163
+ "SELECT COUNT(*) AS n FROM events").fetchone()["n"])
164
+
165
+ near, overdue, _agenda = _near_overdue(state_dir, today, days)
166
+ closed_dropped = _closed_dropped_deadlines(conn)
167
+ # problems — только счётчики из таблиц состояния (ТЗ §4: без verify,
168
+ # который пересчитывает файлы: это уже не «counts из state tables»).
169
+ problems = [
170
+ {
171
+ "case_id": c["case_id"],
172
+ "status": c.get("status") or "unknown",
173
+ "events": int(conn.execute(
174
+ "SELECT COUNT(*) AS n FROM events WHERE case_id = ?",
175
+ (c["case_id"],)).fetchone()["n"]),
176
+ "deadline_states_without_due": int(conn.execute(
177
+ "SELECT COUNT(*) AS n FROM deadline_states "
178
+ "WHERE case_id = ? AND due_at IS NULL",
179
+ (c["case_id"],)).fetchone()["n"]),
180
+ }
181
+ for c in cases
182
+ ]
183
+ finally:
184
+ conn.close()
185
+
186
+ return {
187
+ "as_of": today.isoformat(),
188
+ "horizon_days": days,
189
+ "cases": {
190
+ "total": len(cases),
191
+ "with_amounts": amounts_cases,
192
+ "by_status": statuses,
193
+ },
194
+ "events": {"total": events_total},
195
+ "deadlines": {
196
+ "near": near,
197
+ "near_count": len(near),
198
+ "overdue": overdue,
199
+ "overdue_count": len(overdue),
200
+ # окно — agenda-правило (W6a): у закрытого дела сроки в near/overdue
201
+ # не попадают; они перечислены здесь, а не потеряны молча.
202
+ "closed_excluded": closed_dropped,
203
+ "closed_excluded_count": len(closed_dropped),
204
+ },
205
+ "amounts": {
206
+ "cases_with_amounts": amounts_cases,
207
+ "by_kind": by_kind,
208
+ "totals": {
209
+ "paid": str(spent),
210
+ "planned": str(planned),
211
+ "refunded": str(refunded),
212
+ },
213
+ "currency": "RUB",
214
+ },
215
+ "representatives": {
216
+ "total": reps_total,
217
+ "by_side": {s: int(reps_by_side.get(s, 0)) for s in ("plaintiff", "defendant")},
218
+ },
219
+ "documents": {"total": documents_total, "revisions": documents_revisions},
220
+ "evidence": {"total": evidence_total, "without_hash": evidence_problems},
221
+ "problems": problems,
222
+ "portfolio": [
223
+ {
224
+ "case_id": c["case_id"],
225
+ "case_number": c.get("case_number"),
226
+ "court": c.get("court"),
227
+ "status": c.get("status"),
228
+ "instance": c.get("instance"),
229
+ "owner": c.get("owner"),
230
+ }
231
+ for c in cases
232
+ ],
233
+ "disclaimer": REPORT_DISCLAIMER,
234
+ }
235
+
236
+
237
+ def case_payload(state_dir: Any, case_id: str) -> Optional[Dict[str, Any]]:
238
+ """Печатная развёртка дела: секции card + матрица сроков + evidence-итог."""
239
+ from .deadline_matrix import build_matrix
240
+
241
+ init_db(state_dir)
242
+ conn = get_connection(state_dir)
243
+ try:
244
+ if get_case(conn, case_id) is None:
245
+ return None
246
+ card = get_case_card(conn, case_id)
247
+ amounts = summarize_case_amounts(conn, case_id)
248
+ items = verify_case_evidence(conn, case_id)
249
+ summary = evidence_summary(items)
250
+ finally:
251
+ conn.close()
252
+
253
+ instance, points = build_matrix(state_dir, case_id)
254
+ matrix_summary = {
255
+ "instance": instance,
256
+ "points_total": len(points),
257
+ "anchored": [p["key"] for p in points if not p.get("needs_anchor")],
258
+ "needs_anchor": [p["key"] for p in points if p.get("needs_anchor")],
259
+ "computed": [p["key"] for p in points if p.get("due_at")],
260
+ }
261
+
262
+ sections = {
263
+ "case": card.get("case"),
264
+ "parties": card.get("parties"),
265
+ "amounts": card.get("amounts"),
266
+ "documents": card.get("documents"),
267
+ "documents_current": card.get("documents_current"),
268
+ "notes": card.get("notes"),
269
+ "events": card.get("events"),
270
+ "deadlines": card.get("deadlines"),
271
+ "evidence": card.get("evidence"),
272
+ "representatives": card.get("representatives"),
273
+ }
274
+ return {
275
+ "case_id": case_id,
276
+ "sections": sections,
277
+ "amounts_summary": amounts,
278
+ "evidence_verify": {k: v for k, v in summary.items() if k != "problem_items"},
279
+ "evidence_problems": summary["problem_items"],
280
+ "matrix": matrix_summary,
281
+ "disclaimer": REPORT_DISCLAIMER,
282
+ }
283
+
284
+
285
+ def render_portfolio_text(payload: Dict[str, Any]) -> List[str]:
286
+ """Human-рендер портфеля: строки по фактическим секциям, без «красивых» выводов."""
287
+ lines = [
288
+ f"Портфель на {payload['as_of']} (горизонт {payload['horizon_days']} дн.)",
289
+ f"дел: {payload['cases']['total']}",
290
+ ]
291
+ lines.append("по статусам:")
292
+ for status, count in sorted(payload["cases"]["by_status"].items()):
293
+ lines.append(f" {status}: {count}")
294
+ lines.append(f"сроки near (<= {payload['horizon_days']} дн.): "
295
+ f"{payload['deadlines']['near_count']}")
296
+ for item in payload["deadlines"]["near"]:
297
+ lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
298
+ f"{item['state']} ({item['days_left']} дн.)")
299
+ lines.append(f"просрочено: {payload['deadlines']['overdue_count']}")
300
+ for item in payload["deadlines"]["overdue"]:
301
+ lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
302
+ f"{item['state']}")
303
+ dropped = payload["deadlines"].get("closed_excluded") or []
304
+ lines.append(f"вне окна (закрытые дела, правило agenda): {len(dropped)}")
305
+ for item in dropped:
306
+ lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
307
+ f"{item['state']}")
308
+ totals = payload["amounts"]["totals"]
309
+ lines.append(f"расходы: paid={totals['paid']} planned={totals['planned']} "
310
+ f"refunded={totals['refunded']} RUB")
311
+ lines.append(f"представители: {payload['representatives']['total']} "
312
+ f"(plaintiff={payload['representatives']['by_side']['plaintiff']}, "
313
+ f"defendant={payload['representatives']['by_side']['defendant']})")
314
+ lines.append(f"документы: {payload['documents']['total']} "
315
+ f"(ревизий: {payload['documents']['revisions']})")
316
+ lines.append(f"доказательства: {payload['evidence']['total']} "
317
+ f"(без hash: {payload['evidence']['without_hash']})")
318
+ lines.append(f"problems (по делам): {len(payload['problems'])}")
319
+ for p in payload["problems"]:
320
+ if p["deadline_states_without_due"] or p["events"]:
321
+ lines.append(
322
+ f" {p['case_id']}: status={p['status']} events={p['events']} "
323
+ f"сроков без due_at={p['deadline_states_without_due']}"
324
+ )
325
+ lines.append(payload["disclaimer"])
326
+ return lines
327
+
328
+
329
+ def render_case_text(payload: Dict[str, Any]) -> List[str]:
330
+ case = payload["sections"].get("case") or {}
331
+ lines = [
332
+ f"Дело {payload['case_id']}: {case.get('case_number')} ({case.get('court')})",
333
+ f"статус: {case.get('status')} | instance: {case.get('instance')} "
334
+ f"| owner: {case.get('owner')}",
335
+ f"стороны: истец {case.get('plaintiff')} / ответчик {case.get('defendant')}",
336
+ f"событий: {len(payload['sections'].get('events') or [])}",
337
+ f"заметок: {len(payload['sections'].get('notes') or [])}",
338
+ f"документов: {len(payload['sections'].get('documents') or [])} "
339
+ f"(текущих: {len(payload['sections'].get('documents_current') or [])})",
340
+ f"доказательств: {payload['sections']['evidence']['count']} "
341
+ f"(проблемных: {len(payload['evidence_problems'])})",
342
+ f"представителей: {payload['sections']['representatives']['count']}",
343
+ f"расходы: {payload['amounts_summary']['totals_by_status']}",
344
+ f"матрица: instance={payload['matrix']['instance']} "
345
+ f"точек={payload['matrix']['points_total']} "
346
+ f"нужен якорь={len(payload['matrix']['needs_anchor'])}",
347
+ payload["disclaimer"],
348
+ ]
349
+ return lines