cv-study-utils 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.
@@ -0,0 +1,25 @@
1
+ """Convenient helpers for the computer vision exam materials."""
2
+
3
+ from .api import (
4
+ all_practice,
5
+ all_theory,
6
+ copy_practice,
7
+ copy_theory,
8
+ find,
9
+ list_practice,
10
+ list_theory,
11
+ practice,
12
+ theory,
13
+ )
14
+
15
+ __all__ = [
16
+ "all_practice",
17
+ "all_theory",
18
+ "copy_practice",
19
+ "copy_theory",
20
+ "find",
21
+ "list_practice",
22
+ "list_theory",
23
+ "practice",
24
+ "theory",
25
+ ]
cv_study_utils/api.py ADDED
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import html
5
+ import json
6
+ import re
7
+ import textwrap
8
+ from typing import Any, Iterable
9
+
10
+ from .practice_solutions import PRACTICE
11
+ from .theory_answers import THEORY
12
+ from .theory_extras import THEORY_EXTRAS
13
+
14
+
15
+ def _normalize(text: str) -> str:
16
+ return re.sub(r"\s+", " ", text.casefold()).strip()
17
+
18
+
19
+ def _score(query: str, item_text: str) -> float:
20
+ q = _normalize(query)
21
+ hay = _normalize(item_text)
22
+ if not q:
23
+ return 0.0
24
+ if q in hay:
25
+ return 1.0
26
+ words = [w for w in re.split(r"\W+", q) if len(w) > 2]
27
+ if words:
28
+ overlap = sum(1 for w in words if w in hay) / len(words)
29
+ else:
30
+ overlap = 0.0
31
+ ratio = difflib.SequenceMatcher(None, q, hay[: max(200, len(q) * 4)]).ratio()
32
+ return max(overlap, ratio)
33
+
34
+
35
+ def _item_text(item: dict[str, Any], kind: str) -> str:
36
+ if kind == "theory":
37
+ extra = THEORY_EXTRAS.get(item["id"], {})
38
+ formulae = " ".join(extra.get("formulae", []))
39
+ structure = " ".join(extra.get("structure", []))
40
+ return f"{item['question']} {item['answer']} {formulae} {structure}"
41
+ return f"{item['task']} {item['description']} {item['code']}"
42
+
43
+
44
+ def _title_text(item: dict[str, Any]) -> str:
45
+ return item.get("question") or item.get("task") or ""
46
+
47
+
48
+ def _rank_score(query: str, item: dict[str, Any], kind: str) -> float:
49
+ base = _score(query, _item_text(item, kind))
50
+ q = _normalize(query)
51
+ title = _normalize(_title_text(item))
52
+ if q and q in title:
53
+ base += 0.75
54
+ return base
55
+
56
+
57
+ def _get_by_number(items: list[dict[str, Any]], number: int) -> dict[str, Any]:
58
+ for item in items:
59
+ if item["id"] == number:
60
+ return item
61
+ raise ValueError(f"Нет элемента с номером {number}.")
62
+
63
+
64
+ def _best_match(items: list[dict[str, Any]], query: str, kind: str) -> dict[str, Any]:
65
+ ranked = sorted(
66
+ ((_rank_score(query, item, kind), item) for item in items),
67
+ key=lambda pair: pair[0],
68
+ reverse=True,
69
+ )
70
+ if not ranked or ranked[0][0] < 0.18:
71
+ raise ValueError(f"Ничего не найдено по запросу: {query!r}.")
72
+ return ranked[0][1]
73
+
74
+
75
+ def _display_markdown(markdown: str) -> None:
76
+ try:
77
+ from IPython.display import Markdown, display
78
+
79
+ display(Markdown(markdown))
80
+ except Exception:
81
+ print(markdown)
82
+
83
+
84
+ def _display_code(code: str) -> None:
85
+ try:
86
+ from IPython.display import Markdown, display
87
+
88
+ display(Markdown("```python\n" + code.strip() + "\n```"))
89
+ except Exception:
90
+ print(code)
91
+
92
+
93
+ def copy_text(text: str) -> str:
94
+ """Copy text if possible; in notebooks, fall back to a Copy button."""
95
+ try:
96
+ import pyperclip
97
+
98
+ pyperclip.copy(text)
99
+ return "clipboard"
100
+ except Exception:
101
+ pass
102
+
103
+ try:
104
+ from IPython.display import HTML, display
105
+
106
+ payload = json.dumps(text)
107
+ display(
108
+ HTML(
109
+ """
110
+ <button style="padding:6px 10px;border:1px solid #888;border-radius:6px"
111
+ onclick='navigator.clipboard.writeText(%s); this.textContent="Copied";'>
112
+ Copy answer
113
+ </button>
114
+ """
115
+ % payload
116
+ )
117
+ )
118
+ return "button"
119
+ except Exception as exc:
120
+ raise RuntimeError(
121
+ "Не удалось скопировать текст. Установите pyperclip или используйте Jupyter/Colab."
122
+ ) from exc
123
+
124
+
125
+ def _format_theory_item(item: dict[str, Any]) -> str:
126
+ extra = THEORY_EXTRAS.get(item["id"], {})
127
+ parts = [f"### {item['id']}. {item['question']}", "", item["answer"]]
128
+ if extra.get("formulae"):
129
+ parts.extend(["", "**Формулы:**"])
130
+ parts.extend(f"- {line}" for line in extra["formulae"])
131
+ if extra.get("structure"):
132
+ parts.extend(["", "**Структура / схема:**"])
133
+ parts.extend(f"- {line}" for line in extra["structure"])
134
+ sources = ", ".join([*item.get("sources", []), *extra.get("sources", [])])
135
+ source_line = f"\n\nИсточники: {sources}" if sources else ""
136
+ return "\n".join(parts) + source_line
137
+
138
+
139
+ def _format_practice_item(item: dict[str, Any], include_task: bool = True) -> str:
140
+ header = ""
141
+ if include_task:
142
+ header = (
143
+ f"# Практика {item['id']}. {item['task']}\n"
144
+ f"# {item['description']}\n\n"
145
+ )
146
+ return header + item["code"].strip() + "\n"
147
+
148
+
149
+ def list_theory(display: bool = True) -> str:
150
+ text = "\n".join(f"{item['id']}. {item['question']}" for item in THEORY)
151
+ if display:
152
+ _display_markdown(text)
153
+ return text
154
+
155
+
156
+ def list_practice(display: bool = True) -> str:
157
+ text = "\n".join(f"{item['id']}. {item['task']}" for item in PRACTICE)
158
+ if display:
159
+ _display_markdown(text)
160
+ return text
161
+
162
+
163
+ def theory(
164
+ number: int | str | None = None,
165
+ *,
166
+ query: str | None = None,
167
+ copy: bool = False,
168
+ display: bool = True,
169
+ ) -> str:
170
+ """Show a theory answer by number or approximate query."""
171
+ if isinstance(number, str) and query is None:
172
+ query = number
173
+ number = None
174
+ if number is None and query is None:
175
+ return list_theory(display=display)
176
+ item = _best_match(THEORY, query, "theory") if query else _get_by_number(THEORY, int(number))
177
+ text = _format_theory_item(item)
178
+ if copy:
179
+ copy_text(text)
180
+ if display:
181
+ _display_markdown(text)
182
+ return text
183
+
184
+
185
+ def practice(
186
+ number: int | str | None = None,
187
+ *,
188
+ query: str | None = None,
189
+ copy: bool = False,
190
+ display: bool = True,
191
+ include_task: bool = True,
192
+ ) -> str:
193
+ """Show a practical code template by number or approximate query."""
194
+ if isinstance(number, str) and query is None:
195
+ query = number
196
+ number = None
197
+ if number is None and query is None:
198
+ return list_practice(display=display)
199
+ item = _best_match(PRACTICE, query, "practice") if query else _get_by_number(PRACTICE, int(number))
200
+ code = _format_practice_item(item, include_task=include_task)
201
+ if copy:
202
+ copy_text(code)
203
+ if display:
204
+ _display_code(code)
205
+ return code
206
+
207
+
208
+ def find(query: str, *, kind: str = "all", limit: int = 8, display: bool = True) -> list[dict[str, Any]]:
209
+ """Search theory and/or practice by a text fragment."""
210
+ pools: list[tuple[str, list[dict[str, Any]]]] = []
211
+ if kind in {"all", "theory"}:
212
+ pools.append(("theory", THEORY))
213
+ if kind in {"all", "practice"}:
214
+ pools.append(("practice", PRACTICE))
215
+ if not pools:
216
+ raise ValueError("kind должен быть 'all', 'theory' или 'practice'.")
217
+
218
+ found: list[dict[str, Any]] = []
219
+ for pool_kind, items in pools:
220
+ for item in items:
221
+ score = _rank_score(query, item, pool_kind)
222
+ if score >= 0.18:
223
+ found.append(
224
+ {
225
+ "kind": pool_kind,
226
+ "id": item["id"],
227
+ "title": _title_text(item),
228
+ "score": round(score, 3),
229
+ }
230
+ )
231
+ found.sort(key=lambda row: row["score"], reverse=True)
232
+ found = found[:limit]
233
+ if display:
234
+ if found:
235
+ lines = [
236
+ f"- `{row['kind']}({row['id']})` - {html.escape(row['title'])} "
237
+ f"(score {row['score']})"
238
+ for row in found
239
+ ]
240
+ _display_markdown("\n".join(lines))
241
+ else:
242
+ _display_markdown("Ничего не найдено.")
243
+ return found
244
+
245
+
246
+ def all_theory(*, copy: bool = False, display: bool = True) -> str:
247
+ text = "\n\n".join(_format_theory_item(item) for item in THEORY)
248
+ if copy:
249
+ copy_text(text)
250
+ if display:
251
+ _display_markdown(text)
252
+ return text
253
+
254
+
255
+ def all_practice(*, copy: bool = False, display: bool = True) -> str:
256
+ text = "\n\n".join(
257
+ "```python\n" + _format_practice_item(item).strip() + "\n```" for item in PRACTICE
258
+ )
259
+ if copy:
260
+ copy_text(text)
261
+ if display:
262
+ _display_markdown(text)
263
+ return text
264
+
265
+
266
+ def copy_theory(number: int) -> str:
267
+ return theory(number, copy=True, display=False)
268
+
269
+
270
+ def copy_practice(number: int) -> str:
271
+ return practice(number, copy=True, display=False)
cv_study_utils/cli.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from .api import all_practice, all_theory, find, list_practice, list_theory, practice, theory
6
+
7
+
8
+ def main() -> None:
9
+ parser = argparse.ArgumentParser(prog="cvstudy")
10
+ sub = parser.add_subparsers(dest="command", required=True)
11
+
12
+ p_theory = sub.add_parser("theory", help="Show a theory answer")
13
+ p_theory.add_argument("number", nargs="?")
14
+ p_theory.add_argument("--query", "-q")
15
+ p_theory.add_argument("--copy", action="store_true")
16
+ p_theory.add_argument("--all", action="store_true")
17
+
18
+ p_practice = sub.add_parser("practice", help="Show a practical code template")
19
+ p_practice.add_argument("number", nargs="?")
20
+ p_practice.add_argument("--query", "-q")
21
+ p_practice.add_argument("--copy", action="store_true")
22
+ p_practice.add_argument("--all", action="store_true")
23
+
24
+ p_find = sub.add_parser("find", help="Search theory and practice")
25
+ p_find.add_argument("query")
26
+ p_find.add_argument("--kind", choices=["all", "theory", "practice"], default="all")
27
+ p_find.add_argument("--limit", type=int, default=8)
28
+
29
+ args = parser.parse_args()
30
+ if args.command == "theory":
31
+ if args.all:
32
+ print(all_theory(copy=args.copy, display=False))
33
+ elif args.number is None and args.query is None:
34
+ print(list_theory(display=False))
35
+ else:
36
+ number = int(args.number) if args.number and args.number.isdigit() else args.number
37
+ print(theory(number, query=args.query, copy=args.copy, display=False))
38
+ elif args.command == "practice":
39
+ if args.all:
40
+ print(all_practice(copy=args.copy, display=False))
41
+ elif args.number is None and args.query is None:
42
+ print(list_practice(display=False))
43
+ else:
44
+ number = int(args.number) if args.number and args.number.isdigit() else args.number
45
+ print(practice(number, query=args.query, copy=args.copy, display=False))
46
+ elif args.command == "find":
47
+ rows = find(args.query, kind=args.kind, limit=args.limit, display=False)
48
+ for row in rows:
49
+ print(f"{row['kind']}({row['id']}): {row['title']} [score={row['score']}]")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()