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
@@ -0,0 +1,156 @@
1
+ """W6a §4 (S-60): локальные алерты case-search → bus-free a2a-строки.
2
+
3
+ Эталон паттерна — reminders.py (the wave): sud-cli НИКОГДА не вызывает шину a2a
4
+ сам и вообще не запускает внешние процессы. Модуль читает artifacts
5
+ case_search (alert-файлы и dedup-store), фильтрует окно по created_at и
6
+ печатает готовые shlex-экранированные строки `a2a mail send ...`. Исполняет
7
+ строку оператор или агент-планировщик, вне sud.
8
+
9
+ Числа в теле письма есть => строка несёт --verify "<детерминированная
10
+ команда>" --verify-run (политика a2a, как в the wave).
11
+
12
+ Пустой список — пустой вывод ([]), а не ошибка: отсутствие новостей по суду
13
+ не повод падать.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from datetime import date, timedelta
19
+ from pathlib import Path
20
+ from typing import List
21
+
22
+ BUS_FREE_NOTE = (
23
+ "bus-free: sud-cli не вызывает a2a; ниже готовые экранированные строки "
24
+ "команд — исполнение остаётся за оператором"
25
+ )
26
+
27
+ VERIFY_CMD_TEMPLATE = "/usr/bin/sud-cli case-search alerts --days {days} --json"
28
+
29
+ #: dedup-store лежит в каталоге алертов и не является alert-файлом.
30
+ SEEN_STORE_NAME = "seen_fingerprints.json"
31
+
32
+
33
+ def alerts_search_dirs(state_dir) -> List[Path]:
34
+ """Кандидатские каталоги алертов: sud-state и стандартный cwd-root cw."""
35
+ dirs = [Path(state_dir) / "case-search-alerts"]
36
+ cw_root = Path.cwd() / ".case-search" / "alerts"
37
+ if cw_root not in dirs:
38
+ dirs.append(cw_root)
39
+ return dirs
40
+
41
+
42
+ def _alert_files(alerts_dir: Path) -> List[Path]:
43
+ if not alerts_dir.is_dir():
44
+ return []
45
+ return sorted(p for p in alerts_dir.glob("*.json") if p.name != SEEN_STORE_NAME)
46
+
47
+
48
+ def load_alert_files(alerts_dir) -> List[dict]:
49
+ """Прочитать alert-файлы; битые/чужие JSON молча пропускаются (не угадываем)."""
50
+ out: List[dict] = []
51
+ for path in _alert_files(Path(alerts_dir)):
52
+ try:
53
+ data = json.loads(path.read_text(encoding="utf-8"))
54
+ except (OSError, ValueError, UnicodeDecodeError):
55
+ continue
56
+ if not isinstance(data, dict) or "case_number" not in data or "created_at" not in data:
57
+ continue
58
+ out.append(data)
59
+ return out
60
+
61
+
62
+ def recent_alerts(alerts: List[dict], today: date, days: int) -> List[dict]:
63
+ """Алерты, созданные в окне [today-days, today]; детерминированный порядок."""
64
+ floor_day = today - timedelta(days=days)
65
+ kept = []
66
+ for a in alerts:
67
+ raw = str(a.get("created_at") or "")
68
+ try:
69
+ created = date.fromisoformat(raw[:10])
70
+ except ValueError:
71
+ continue
72
+ if floor_day <= created <= today:
73
+ kept.append(a)
74
+ kept.sort(key=lambda a: (str(a.get("created_at") or ""), str(a.get("case_number")),
75
+ str(a.get("status"))))
76
+ return kept
77
+
78
+
79
+ def a2a_command(alert: dict, days: int) -> str:
80
+ """Одна готовая строка a2a mail send; все значения экранированы shlex."""
81
+ import shlex
82
+ case_number = alert.get("case_number")
83
+ status = alert.get("status")
84
+ reason = alert.get("reason")
85
+ changes = alert.get("changes") or []
86
+ paths = "; ".join(str(c.get("path")) for c in changes[:5] if isinstance(c, dict))
87
+ subject = f"case-search: {case_number} — {status}"
88
+ body = (
89
+ f"Локальный алерт case-search: дело {case_number}, статус {status}, "
90
+ f"причина: {reason}; изменений: {len(changes)}. "
91
+ + (f"Пути: {paths}. " if paths else "")
92
+ + f"Окно выборки: {days} дн. Повторная сверка: {VERIFY_CMD_TEMPLATE.format(days=days)} "
93
+ "Алерт может устареть к моменту отправки — проверь артефакты до исполнения строки."
94
+ )
95
+ verify = VERIFY_CMD_TEMPLATE.format(days=days)
96
+ parts = [
97
+ "a2a", "mail", "send",
98
+ "--to", "colleague-legal",
99
+ "--priority", "today",
100
+ "--subject", subject,
101
+ "--body", body,
102
+ "--verify", verify,
103
+ "--verify-run",
104
+ ]
105
+ return " ".join(shlex.quote(p) for p in parts)
106
+
107
+
108
+ def generate_mail_commands(alerts: List[dict], days: int) -> List[str]:
109
+ return [a2a_command(a, days) for a in alerts]
110
+
111
+
112
+ def alerts_payload(alerts_dir, today: date, days: int) -> dict:
113
+ """Собрать готовый payload команды (используется CLI и MCP одним кодом)."""
114
+ raw = load_alert_files(alerts_dir)
115
+ recent = recent_alerts(raw, today, days)
116
+ return {
117
+ "days": days,
118
+ "today": today.isoformat(),
119
+ "source_dir": str(alerts_dir),
120
+ "total_files": len(raw),
121
+ "alerts": recent,
122
+ "commands": generate_mail_commands(recent, days),
123
+ "bus_free": BUS_FREE_NOTE,
124
+ }
125
+
126
+
127
+ def count_pending_alerts(state_dir) -> int:
128
+ """Сколько алертов ещё не переданы в шину (fingerprint нет в dedup-store).
129
+
130
+ Только чтение локальных файлов; шина не трогается. Для doctor-счётчика.
131
+ """
132
+ from .case_search.alerts import Alert
133
+ from .case_search.alerts_dedup import (
134
+ alert_fingerprint,
135
+ default_seen_path,
136
+ load_seen_fingerprints,
137
+ )
138
+
139
+ seen = set(load_seen_fingerprints(default_seen_path()))
140
+ pending = 0
141
+ for alerts_dir in alerts_search_dirs(state_dir):
142
+ store = alerts_dir / SEEN_STORE_NAME
143
+ if store != default_seen_path():
144
+ seen |= set(load_seen_fingerprints(store))
145
+ for raw in load_alert_files(alerts_dir):
146
+ alert = Alert(
147
+ case_number=str(raw.get("case_number")),
148
+ status=str(raw.get("status") or ""),
149
+ reason=str(raw.get("reason") or ""),
150
+ changes=list(raw.get("changes") or []),
151
+ destination=str(raw.get("destination") or "local-file"),
152
+ created_at=str(raw.get("created_at") or ""),
153
+ )
154
+ if alert_fingerprint(alert.case_number, alert) not in seen:
155
+ pending += 1
156
+ return pending
@@ -0,0 +1,108 @@
1
+ """Transport-agnostic court-source adapter contract.
2
+
3
+ Phase 3 defines the contract and provides one offline synthetic adapter only.
4
+ No live court source is implemented.
5
+ """
6
+
7
+ import datetime
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Protocol
11
+
12
+ STATUS_OK = "ok"
13
+ STATUS_NO_RESULTS = "no_results"
14
+ STATUS_UNAVAILABLE = "unavailable"
15
+ STATUS_BLOCKED = "blocked"
16
+ STATUS_ERROR = "error"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CourtProviderResult:
21
+ """Normalized result from any court-source adapter."""
22
+
23
+ status: str
24
+ items: List[Dict[str, Any]]
25
+ source: str
26
+ source_url: Optional[str] = None
27
+ fetched_at: Optional[str] = None
28
+ pagination: Dict[str, Any] = field(
29
+ default_factory=lambda: {
30
+ "page": 1,
31
+ "page_size": 10,
32
+ "total": 0,
33
+ "pages_total": 0,
34
+ "limit": 10,
35
+ }
36
+ )
37
+ error_message: Optional[str] = None
38
+
39
+
40
+ class CourtSource(Protocol):
41
+ """Transport-agnostic adapter protocol for live and offline court sources."""
42
+
43
+ def fetch_case(self, case_id: str, **kwargs: Any) -> CourtProviderResult:
44
+ ...
45
+
46
+ def search_cases(
47
+ self, query: str, page: int = 1, page_size: int = 10, **kwargs: Any
48
+ ) -> CourtProviderResult:
49
+ ...
50
+
51
+
52
+ class LocalJsonCourtSource:
53
+ """Offline synthetic fixture source.
54
+
55
+ Reads local JSON files in the native ``cases``/``events`` schema and
56
+ returns a typed ``CourtProviderResult``. No network access is performed.
57
+ """
58
+
59
+ def __init__(self, source_label: str = "local-synthetic-json"):
60
+ self.source_label = source_label
61
+
62
+ def import_file(self, path: Path) -> CourtProviderResult:
63
+ from sud_cli.importing import CourtImportError, load_court_case_fixture
64
+
65
+ try:
66
+ cases = load_court_case_fixture(Path(path))
67
+ except CourtImportError as exc:
68
+ return CourtProviderResult(
69
+ status=STATUS_ERROR,
70
+ items=[],
71
+ source=self.source_label,
72
+ error_message=str(exc),
73
+ )
74
+ except Exception as exc:
75
+ return CourtProviderResult(
76
+ status=STATUS_ERROR,
77
+ items=[],
78
+ source=self.source_label,
79
+ error_message=f"unexpected import error: {exc}",
80
+ )
81
+
82
+ if not cases:
83
+ return CourtProviderResult(
84
+ status=STATUS_NO_RESULTS,
85
+ items=[],
86
+ source=self.source_label,
87
+ error_message="no cases in fixture",
88
+ )
89
+
90
+ return CourtProviderResult(
91
+ status=STATUS_OK,
92
+ items=cases,
93
+ source=self.source_label,
94
+ fetched_at=_utc_now(),
95
+ pagination={
96
+ "page": 1,
97
+ "page_size": len(cases),
98
+ "total": len(cases),
99
+ "pages_total": 1,
100
+ "limit": len(cases),
101
+ },
102
+ )
103
+
104
+
105
+ def _utc_now() -> str:
106
+ return (
107
+ datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
108
+ )
@@ -0,0 +1,18 @@
1
+ {
2
+ "meta": {
3
+ "format": "format-checklist",
4
+ "generated_note": "the wave §3 (S-71): структурный чек-лист комплектности документов дела.",
5
+ "disclaimer": "Это формальная проверка структуры шаблона против списка маркеров. Это НЕ юридическая проверка достаточности документов, не юридическая консультация и не норма: ни один маркер ничего не утверждает о требованиях закона. Решение о комплектности остаётся за человеком."
6
+ },
7
+ "marker_hints": {
8
+ "шапка": "в шапке ожидаются блоки [суд], [истец], [ответчик]",
9
+ "Обжалуется": "для апелляционных форм: блок с обжалуемым судебным актом",
10
+ "Основания": "для апелляционных форм: блок с доводами (без нормативных ссылок)"
11
+ },
12
+ "kinds": {
13
+ "isk_apk": ["шапка", "Просьба", "Приложения", "Подпись", "Дата"],
14
+ "claim_apk": ["шапка", "Просьба", "Приложения", "Подпись", "Дата"],
15
+ "appeal_apk": ["шапка", "Обжалуется", "Основания", "Просьба", "Приложения", "Подпись", "Дата"],
16
+ "hodat": ["шапка", "Просьба", "Подпись", "Дата"]
17
+ }
18
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "provenance": "Ставки ЦБ РФ, периоды пересмотра; источник — та же доктрина, что legal_rules.json (JSON единственный источник). Сгенерировано 2026-10-22 из embedded-константы v2.3.1; обновлять по постановлениям ЦБ.",
3
+ "coverage_first": "2023-07-24",
4
+ "coverage_latest_change": "2026-06-15",
5
+ "periods": [
6
+ [
7
+ "2026-06-15",
8
+ "18"
9
+ ],
10
+ [
11
+ "2026-02-16",
12
+ "15"
13
+ ],
14
+ [
15
+ "2025-12-22",
16
+ "16"
17
+ ],
18
+ [
19
+ "2025-09-15",
20
+ "17"
21
+ ],
22
+ [
23
+ "2025-06-09",
24
+ "20"
25
+ ],
26
+ [
27
+ "2024-12-16",
28
+ "21"
29
+ ],
30
+ [
31
+ "2024-10-28",
32
+ "21"
33
+ ],
34
+ [
35
+ "2024-09-16",
36
+ "19"
37
+ ],
38
+ [
39
+ "2024-07-29",
40
+ "18"
41
+ ],
42
+ [
43
+ "2024-06-10",
44
+ "16"
45
+ ],
46
+ [
47
+ "2024-04-29",
48
+ "16"
49
+ ],
50
+ [
51
+ "2023-09-18",
52
+ "13"
53
+ ],
54
+ [
55
+ "2023-07-24",
56
+ "8.5"
57
+ ]
58
+ ]
59
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "gk_rf": {
3
+ "196": [{"effective_date": "1995-01-01", "federal_law": "ГК РФ (часть первая)", "text": "Общий срок исковой давности устанавливается в три года.", "notes": "Редакция на дату запроса определяется по последнему изменению."}],
4
+ "200": [{"effective_date": "1995-01-01", "federal_law": "ГК РФ (часть первая)", "text": "Течение срока исковой давности начинается со дня, когда лицо узнало или должно было узнать о нарушении своего права.", "notes": "Общее правило о начале течения срока."}],
5
+ "395": [
6
+ {"effective_date": "1995-01-01", "federal_law": "ГК РФ (часть первая)", "text": "За пользование чужими денежными средствами вследствие их неправомерного удержания подлежат уплате проценты.", "notes": "Базовая редакция."},
7
+ {"effective_date": "2016-08-01", "federal_law": "Федеральный закон от 03.07.2016 № 315-ФЗ", "text": "За пользование чужими денежными средствами вследствие их неправомерного удержания, уклонения от их возврата, иной просрочки в их уплате либо неосновательного получения или сбережения за счет другого лица подлежат уплате проценты.", "notes": "Редакция после реформы обязательственного права."}
8
+ ]
9
+ },
10
+ "uk_rf": {
11
+ "159": [
12
+ {"effective_date": "1996-06-13", "federal_law": "Федеральный закон от 13.06.1996 № 63-ФЗ", "text": "Мошенничество, то есть хищение чужого имущества или приобретение права на чужое имущество путем обмана или злоупотребления доверием.", "notes": "Базовая редакция УК РФ."},
13
+ {"effective_date": "2003-12-16", "federal_law": "Федеральный закон от 08.12.2003 № 162-ФЗ", "text": "Мошенничество, то есть хищение чужого имущества или приобретение права на чужое имущество путем обмана или злоупотребления доверием.", "notes": "Редакция после изменений Общей и Особенной частей УК РФ."}
14
+ ]
15
+ },
16
+ "nk_rf": {
17
+ "333.19": [{"effective_date": "2000-08-05", "federal_law": "Федеральный закон от 05.08.2000 № 117-ФЗ", "text": "Размеры государственной пошлины по делам, рассматриваемым судами общей юрисдикции.", "notes": "Тарифы могли изменяться последующими федеральными законами."}]
18
+ }
19
+ }