personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

apps: add reflections app and pdf view

+1201 -2
+6
apps/chat/_chat_event.html
··· 34 34 <span class="chat-talent-card-label">{{ ev.name }} errored</span> 35 35 {% if ev.reason %}<span class="chat-talent-card-reason">{{ ev.reason }}</span>{% endif %} 36 36 </button> 37 + {% elif ev.kind == "reflection_ready" %} 38 + <article class="chat-reflection-card"> 39 + <span class="chat-reflection-card-label">weekly reflection ready</span> 40 + <span class="chat-reflection-card-week">week of {{ ev.day }}</span> 41 + <a class="chat-reflection-card-link" href="{{ ev.url }}">open reflection</a> 42 + </article> 37 43 {% elif ev.kind == "chat_error" %} 38 44 <div class="chat-error-block">chat had trouble — try again</div> 39 45 {% endif %}
+8
apps/chat/tests/test_routes.py
··· 151 151 reason="network", 152 152 use_id="use-4", 153 153 ) 154 + append_chat_event( 155 + "reflection_ready", 156 + ts=_ms(2099, 1, 2, 9, 6), 157 + day="20981228", 158 + url="/app/reflections/20981228", 159 + ) 154 160 155 161 response = env.client.get(f"/app/chat/{day}") 156 162 html = response.get_data(as_text=True) ··· 161 167 assert 'title="full note"' in html 162 168 assert 'data-talent-use-id="use-2"' in html 163 169 assert 'data-talent-use-id="use-3"' in html 170 + assert "weekly reflection ready" in html 171 + assert 'href="/app/reflections/20981228"' in html 164 172 assert "chat had trouble" in html 165 173 166 174
+30 -1
apps/chat/workspace.html
··· 153 153 name: msg.name || '', 154 154 task: msg.task || '', 155 155 summary: msg.summary || '', 156 - reason: msg.reason || '' 156 + reason: msg.reason || '', 157 + day: msg.day || '', 158 + url: msg.url || '' 157 159 }); 158 160 if (!item) return; 159 161 ··· 171 173 'talent_spawned', 172 174 'talent_finished', 173 175 'talent_errored', 176 + 'reflection_ready', 174 177 'chat_error' 175 178 ].includes(event.kind)) { 176 179 return null; ··· 203 206 } 204 207 if (event.kind === 'talent_errored') { 205 208 return buildTalentCard(event.name + ' errored', event.reason || '', event.use_id, 'errored', 'chat-talent-card--errored'); 209 + } 210 + if (event.kind === 'reflection_ready') { 211 + return buildReflectionCard(event.day || '', event.url || ''); 206 212 } 207 213 208 214 const block = document.createElement('div'); ··· 248 254 detailNode.textContent = detail; 249 255 card.appendChild(detailNode); 250 256 } 257 + 258 + return card; 259 + } 260 + 261 + function buildReflectionCard(dayValue, url) { 262 + const card = document.createElement('article'); 263 + card.className = 'chat-reflection-card'; 264 + 265 + const labelNode = document.createElement('span'); 266 + labelNode.className = 'chat-reflection-card-label'; 267 + labelNode.textContent = 'weekly reflection ready'; 268 + card.appendChild(labelNode); 269 + 270 + const weekNode = document.createElement('span'); 271 + weekNode.className = 'chat-reflection-card-week'; 272 + weekNode.textContent = 'week of ' + dayValue; 273 + card.appendChild(weekNode); 274 + 275 + const linkNode = document.createElement('a'); 276 + linkNode.className = 'chat-reflection-card-link'; 277 + linkNode.href = url; 278 + linkNode.textContent = 'open reflection'; 279 + card.appendChild(linkNode); 251 280 252 281 return card; 253 282 }
+25
apps/home/routes.py
··· 19 19 20 20 from convey.apps import _resolve_attention 21 21 from convey.bridge import get_cached_state 22 + from convey.utils import DATE_RE, format_date 22 23 from think import skills as think_skills 23 24 from think.awareness import get_current 24 25 from think.capture_health import get_capture_health ··· 92 93 except ValueError: 93 94 return 0 94 95 return max(0, (today_dt - earliest).days) 96 + 97 + 98 + def _load_latest_weekly_reflection() -> dict[str, str] | None: 99 + reflections_dir = Path(get_journal()) / "reflections" / "weekly" 100 + if not reflections_dir.is_dir(): 101 + return None 102 + 103 + days = sorted( 104 + path.stem 105 + for path in reflections_dir.glob("*.md") 106 + if path.is_file() and DATE_RE.fullmatch(path.stem) 107 + ) 108 + if not days: 109 + return None 110 + 111 + day = days[-1] 112 + return { 113 + "day": day, 114 + "label": format_date(day), 115 + "url": f"/app/reflections/{day}", 116 + } 95 117 96 118 97 119 def _load_flow_md(today: str) -> tuple[str | None, float | None]: ··· 1271 1293 entities = _collect_entities_today(today) 1272 1294 routines = _collect_routines() 1273 1295 skills = _collect_skills() 1296 + latest_weekly_reflection = _load_latest_weekly_reflection() 1274 1297 1275 1298 last_observe_relative = None 1276 1299 if last_observe_ts: ··· 1306 1329 and not briefing_exists 1307 1330 and not attention 1308 1331 and not pulse_needs 1332 + and not latest_weekly_reflection 1309 1333 ) 1310 1334 1311 1335 # Section summaries for collapsed state ··· 1427 1451 "briefing_needs_deduped": briefing_needs_deduped, 1428 1452 "briefing_needs_shared_count": briefing_needs_shared_count, 1429 1453 "briefing_needs_badge": briefing_needs_badge, 1454 + "latest_weekly_reflection": latest_weekly_reflection, 1430 1455 "yesterday_processing": yesterday_processing, 1431 1456 "show_welcome": show_welcome, 1432 1457 "narrative_summary": narrative_summary,
+39
apps/home/workspace.html
··· 191 191 font-size: 0.85rem; 192 192 } 193 193 194 + .pulse-reflection { 195 + padding: 1.25rem; 196 + background: #fff; 197 + border-radius: 10px; 198 + border: 1px solid #d1d5db; 199 + } 200 + 201 + .pulse-reflection-link { 202 + display: inline-flex; 203 + align-items: center; 204 + gap: 0.35rem; 205 + color: #b45309; 206 + font-size: 0.95rem; 207 + font-weight: 600; 208 + text-decoration: none; 209 + } 210 + 211 + .pulse-reflection-link:hover { 212 + text-decoration: underline; 213 + } 214 + 215 + .pulse-reflection-link:focus-visible { 216 + outline: 2px solid #b45309; 217 + outline-offset: 2px; 218 + border-radius: 4px; 219 + } 220 + 194 221 /* Today Section */ 195 222 .pulse-today { 196 223 padding: 1.25rem; ··· 854 881 /* Shared left padding for indicator gutter */ 855 882 .pulse-vitals, 856 883 .pulse-narrative, 884 + .pulse-reflection, 857 885 .pulse-today, 858 886 .pulse-needs, 859 887 .pulse-routines, ··· 933 961 } 934 962 935 963 .pulse-narrative, 964 + .pulse-reflection, 936 965 .pulse-routines, 937 966 .pulse-skills, 938 967 .pulse-today, ··· 951 980 952 981 .pulse-vitals, 953 982 .pulse-narrative, 983 + .pulse-reflection, 954 984 .pulse-today, 955 985 .pulse-needs, 956 986 .pulse-routines, ··· 1184 1214 {% endfor %} 1185 1215 </div> 1186 1216 </div> 1217 + </div> 1218 + {% endif %} 1219 + 1220 + {% if latest_weekly_reflection %} 1221 + <div class="pulse-reflection"> 1222 + <h2 class="pulse-section-header">weekly reflection</h2> 1223 + <a class="pulse-reflection-link" href="{{ latest_weekly_reflection.url }}"> 1224 + week of {{ latest_weekly_reflection.label }} → 1225 + </a> 1187 1226 </div> 1188 1227 {% endif %} 1189 1228
+9
apps/reflections/app.json
··· 1 + { 2 + "icon": "🪞", 3 + "label": "Reflections", 4 + "date_nav": true, 5 + "allow_future_dates": false, 6 + "facets": { 7 + "disabled": true 8 + } 9 + }
+61
apps/reflections/pdf.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <title>Weekly Reflection</title> 6 + <style> 7 + body { 8 + font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; 9 + color: #1f2937; 10 + margin: 0; 11 + padding: 0; 12 + } 13 + 14 + main { 15 + padding: 2.5rem 2.75rem 3rem; 16 + } 17 + 18 + .pdf-kicker { 19 + margin: 0 0 0.3rem; 20 + font-size: 0.8rem; 21 + font-weight: 700; 22 + letter-spacing: 0.08em; 23 + text-transform: uppercase; 24 + color: #92400e; 25 + } 26 + 27 + h1 { 28 + margin: 0 0 1.5rem; 29 + font-size: 1.9rem; 30 + line-height: 1.1; 31 + color: #111827; 32 + } 33 + 34 + h2, h3 { 35 + color: #111827; 36 + } 37 + 38 + p, li { 39 + line-height: 1.6; 40 + } 41 + 42 + blockquote { 43 + margin: 1.25rem 0; 44 + padding-left: 1rem; 45 + border-left: 3px solid #f59e0b; 46 + color: #6b7280; 47 + } 48 + 49 + code { 50 + font-size: 0.9em; 51 + } 52 + </style> 53 + </head> 54 + <body> 55 + <main> 56 + <p class="pdf-kicker">weekly reflection</p> 57 + <h1>week of {{ week_label }}</h1> 58 + {{ reflection_html|safe }} 59 + </main> 60 + </body> 61 + </html>
+210
apps/reflections/routes.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + from datetime import datetime 7 + from pathlib import Path 8 + from typing import Any 9 + from urllib.parse import urlsplit 10 + 11 + import frontmatter 12 + from flask import Blueprint, Response, jsonify, redirect, render_template, url_for 13 + from markdown import Markdown 14 + from weasyprint import HTML, default_url_fetcher 15 + 16 + from convey.utils import DATE_RE, format_date 17 + from think.utils import get_journal, get_owner_timezone, sunday_of_week 18 + 19 + reflections_bp = Blueprint( 20 + "app:reflections", 21 + __name__, 22 + url_prefix="/app/reflections", 23 + ) 24 + 25 + 26 + def _reflections_dir() -> Path: 27 + return Path(get_journal()) / "reflections" / "weekly" 28 + 29 + 30 + def _plain_not_found( 31 + message: str = "Reflection not found", 32 + ) -> tuple[str, int, dict[str, str]]: 33 + return (message, 404, {"Content-Type": "text/plain; charset=utf-8"}) 34 + 35 + 36 + def _parse_day_token(day: str) -> datetime | None: 37 + if not DATE_RE.fullmatch(day): 38 + return None 39 + try: 40 + return datetime.strptime(day, "%Y%m%d") 41 + except ValueError: 42 + return None 43 + 44 + 45 + def _canonical_week_day(day: str) -> str | None: 46 + day_dt = _parse_day_token(day) 47 + if day_dt is None: 48 + return None 49 + return sunday_of_week(day_dt, get_owner_timezone()) 50 + 51 + 52 + def _reflection_path(day: str) -> Path: 53 + return _reflections_dir() / f"{day}.md" 54 + 55 + 56 + def _list_reflection_days() -> list[str]: 57 + reflections_dir = _reflections_dir() 58 + if not reflections_dir.is_dir(): 59 + return [] 60 + days = [ 61 + path.stem 62 + for path in reflections_dir.glob("*.md") 63 + if path.is_file() and DATE_RE.fullmatch(path.stem) 64 + ] 65 + return sorted(days, reverse=True) 66 + 67 + 68 + def _load_reflection(day: str) -> tuple[Path, str, frontmatter.Post]: 69 + path = _reflection_path(day) 70 + if not path.is_file(): 71 + raise FileNotFoundError(day) 72 + raw_markdown = path.read_text(encoding="utf-8") 73 + return path, raw_markdown, frontmatter.loads(raw_markdown) 74 + 75 + 76 + def _safe_pdf_url_fetcher(url: str, *args: Any, **kwargs: Any) -> dict[str, Any]: 77 + scheme = urlsplit(url).scheme.lower() 78 + if scheme in {"http", "https"}: 79 + raise ValueError("Remote assets are disabled for reflection PDFs") 80 + return default_url_fetcher(url, *args, **kwargs) 81 + 82 + 83 + def _render_reflection_pdf(path: Path, post: frontmatter.Post) -> bytes: 84 + markdown = Markdown(extensions=["extra", "sane_lists"]) 85 + body_html = markdown.convert(post.content) 86 + html = render_template( 87 + "reflections/pdf.html", 88 + week_label=format_date(path.stem), 89 + reflection_html=body_html, 90 + ) 91 + return HTML( 92 + string=html, 93 + base_url=path.parent.resolve().as_uri(), 94 + url_fetcher=_safe_pdf_url_fetcher, 95 + ).write_pdf() 96 + 97 + 98 + def _canonical_redirect(endpoint: str, day: str) -> Response | None: 99 + canonical_day = _canonical_week_day(day) 100 + if canonical_day is None: 101 + return None 102 + if canonical_day == day: 103 + return None 104 + return redirect(url_for(endpoint, day=canonical_day), code=302) 105 + 106 + 107 + @reflections_bp.route("/") 108 + def index() -> str: 109 + weeks = [ 110 + { 111 + "day": day, 112 + "label": format_date(day), 113 + "url": url_for("app:reflections.week_view", day=day), 114 + } 115 + for day in _list_reflection_days() 116 + ] 117 + return render_template( 118 + "app.html", 119 + app="reflections", 120 + view_mode="index", 121 + weeks=weeks, 122 + ) 123 + 124 + 125 + @reflections_bp.route("/<day>") 126 + def week_view(day: str) -> Any: 127 + redirect_response = _canonical_redirect("app:reflections.week_view", day) 128 + if redirect_response is not None: 129 + return redirect_response 130 + 131 + canonical_day = _canonical_week_day(day) 132 + if canonical_day is None: 133 + return _plain_not_found("Reflection not found") 134 + 135 + try: 136 + _path, _raw_markdown, post = _load_reflection(canonical_day) 137 + except FileNotFoundError: 138 + return _plain_not_found("Reflection not found") 139 + 140 + return render_template( 141 + "app.html", 142 + app="reflections", 143 + day=canonical_day, 144 + view_mode="detail", 145 + reflection_day=canonical_day, 146 + reflection_week_label=format_date(canonical_day), 147 + reflection_markdown=post.content, 148 + raw_url=url_for("app:reflections.week_raw", day=canonical_day), 149 + pdf_url=url_for("app:reflections.week_pdf", day=canonical_day), 150 + ) 151 + 152 + 153 + @reflections_bp.route("/<day>/raw") 154 + def week_raw(day: str) -> Any: 155 + redirect_response = _canonical_redirect("app:reflections.week_raw", day) 156 + if redirect_response is not None: 157 + return redirect_response 158 + 159 + canonical_day = _canonical_week_day(day) 160 + if canonical_day is None: 161 + return _plain_not_found("Reflection not found") 162 + 163 + try: 164 + _path, raw_markdown, _post = _load_reflection(canonical_day) 165 + except FileNotFoundError: 166 + return _plain_not_found("Reflection not found") 167 + 168 + return ( 169 + raw_markdown, 170 + 200, 171 + {"Content-Type": "text/markdown; charset=utf-8"}, 172 + ) 173 + 174 + 175 + @reflections_bp.route("/<day>/pdf") 176 + def week_pdf(day: str) -> Any: 177 + redirect_response = _canonical_redirect("app:reflections.week_pdf", day) 178 + if redirect_response is not None: 179 + return redirect_response 180 + 181 + canonical_day = _canonical_week_day(day) 182 + if canonical_day is None: 183 + return _plain_not_found("Reflection not found") 184 + 185 + try: 186 + path, _raw_markdown, post = _load_reflection(canonical_day) 187 + pdf_bytes = _render_reflection_pdf(path, post) 188 + except FileNotFoundError: 189 + return _plain_not_found("Reflection not found") 190 + except ValueError as exc: 191 + return (str(exc), 400, {"Content-Type": "text/plain; charset=utf-8"}) 192 + 193 + return Response( 194 + pdf_bytes, 195 + mimetype="application/pdf", 196 + headers={ 197 + "Content-Disposition": ( 198 + f'attachment; filename="reflection-{canonical_day}.pdf"' 199 + ) 200 + }, 201 + ) 202 + 203 + 204 + @reflections_bp.route("/api/stats/<month>") 205 + def api_stats(month: str) -> Any: 206 + if len(month) != 6 or not month.isdigit(): 207 + return jsonify({"error": "Invalid month format, expected YYYYMM"}), 400 208 + 209 + stats = {day: 1 for day in _list_reflection_days() if day.startswith(month)} 210 + return jsonify(stats)
+251
apps/reflections/workspace.html
··· 1 + <style> 2 + .reflection-shell { 3 + max-width: 900px; 4 + margin: 0 auto; 5 + padding: 2rem 2rem 5rem; 6 + color: #1f2937; 7 + } 8 + 9 + .reflection-header { 10 + display: flex; 11 + justify-content: space-between; 12 + gap: 1rem; 13 + align-items: flex-start; 14 + margin-bottom: 1.5rem; 15 + } 16 + 17 + .reflection-kicker { 18 + margin: 0 0 0.35rem; 19 + font-size: 0.85rem; 20 + font-weight: 600; 21 + letter-spacing: 0.08em; 22 + text-transform: uppercase; 23 + color: #92400e; 24 + } 25 + 26 + .reflection-title { 27 + margin: 0; 28 + font-size: clamp(1.8rem, 4vw, 2.7rem); 29 + line-height: 1.1; 30 + color: #111827; 31 + } 32 + 33 + .reflection-subtitle { 34 + margin: 0.35rem 0 0; 35 + font-size: 1rem; 36 + color: #6b7280; 37 + } 38 + 39 + .reflection-actions { 40 + display: flex; 41 + gap: 0.75rem; 42 + flex-wrap: wrap; 43 + } 44 + 45 + .reflection-button, 46 + .reflection-link-button { 47 + display: inline-flex; 48 + align-items: center; 49 + justify-content: center; 50 + min-height: 42px; 51 + padding: 0 1rem; 52 + border-radius: 999px; 53 + border: 1px solid #d6d3d1; 54 + background: #fffdf8; 55 + color: #1f2937; 56 + font: inherit; 57 + text-decoration: none; 58 + cursor: pointer; 59 + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; 60 + } 61 + 62 + .reflection-button:hover, 63 + .reflection-link-button:hover { 64 + background: #fef3c7; 65 + border-color: #f59e0b; 66 + } 67 + 68 + .reflection-body, 69 + .reflection-index { 70 + background: #fff; 71 + border: 1px solid #e7e5e4; 72 + border-radius: 18px; 73 + padding: 1.5rem; 74 + box-shadow: 0 20px 40px rgba(15, 23, 42, 0.04); 75 + } 76 + 77 + .reflection-body h1, 78 + .reflection-body h2, 79 + .reflection-body h3 { 80 + color: #111827; 81 + } 82 + 83 + .reflection-body p, 84 + .reflection-body li, 85 + .reflection-index p, 86 + .reflection-index li { 87 + line-height: 1.7; 88 + } 89 + 90 + .reflection-body blockquote { 91 + margin: 1.5rem 0; 92 + padding-left: 1rem; 93 + border-left: 3px solid #f59e0b; 94 + color: #6b7280; 95 + } 96 + 97 + .reflection-body a, 98 + .reflection-index a { 99 + color: #b45309; 100 + } 101 + 102 + .reflection-body a:hover, 103 + .reflection-index a:hover { 104 + color: #92400e; 105 + } 106 + 107 + .reflection-index-list { 108 + list-style: none; 109 + margin: 1.25rem 0 0; 110 + padding: 0; 111 + display: grid; 112 + gap: 0.75rem; 113 + } 114 + 115 + .reflection-index-link { 116 + display: flex; 117 + justify-content: space-between; 118 + gap: 1rem; 119 + align-items: center; 120 + padding: 1rem 1.1rem; 121 + border-radius: 14px; 122 + background: #fffbeb; 123 + border: 1px solid #fcd34d; 124 + text-decoration: none; 125 + color: #1f2937; 126 + } 127 + 128 + .reflection-index-link:hover { 129 + background: #fef3c7; 130 + } 131 + 132 + .reflection-index-day { 133 + font-weight: 600; 134 + } 135 + 136 + .reflection-index-token { 137 + color: #78716c; 138 + font-size: 0.9rem; 139 + } 140 + 141 + @media (max-width: 720px) { 142 + .reflection-shell { 143 + padding: 1.25rem 1rem 4rem; 144 + } 145 + 146 + .reflection-header { 147 + flex-direction: column; 148 + } 149 + 150 + .reflection-actions { 151 + width: 100%; 152 + } 153 + } 154 + </style> 155 + 156 + <script src="{{ vendor_lib('dompurify') }}"></script> 157 + 158 + <div class="reflection-shell"> 159 + {% if view_mode == "index" %} 160 + <header class="reflection-header"> 161 + <div> 162 + <p class="reflection-kicker">weekly reflection</p> 163 + <h2 class="reflection-title">Reflections</h2> 164 + <p class="reflection-subtitle">Available weekly reflections, newest first.</p> 165 + </div> 166 + </header> 167 + <section class="reflection-index"> 168 + {% if weeks %} 169 + <ul class="reflection-index-list"> 170 + {% for week in weeks %} 171 + <li> 172 + <a class="reflection-index-link" href="{{ week.url }}"> 173 + <span class="reflection-index-day">week of {{ week.label }}</span> 174 + <span class="reflection-index-token">{{ week.day }}</span> 175 + </a> 176 + </li> 177 + {% endfor %} 178 + </ul> 179 + {% else %} 180 + <p>No weekly reflections yet.</p> 181 + {% endif %} 182 + </section> 183 + {% else %} 184 + <header class="reflection-header"> 185 + <div> 186 + <p class="reflection-kicker">weekly reflection</p> 187 + <h2 class="reflection-title">{{ reflection_week_label }}</h2> 188 + <p class="reflection-subtitle">week of {{ reflection_week_label }}</p> 189 + </div> 190 + <div class="reflection-actions"> 191 + <button type="button" class="reflection-button" id="copyReflectionButton">Copy</button> 192 + <a class="reflection-link-button" href="{{ pdf_url }}">Download PDF</a> 193 + </div> 194 + </header> 195 + 196 + <section class="reflection-body" id="reflectionBody"></section> 197 + {% endif %} 198 + </div> 199 + 200 + {% if view_mode == "detail" %} 201 + <script> 202 + (() => { 203 + const markdownSource = {{ reflection_markdown|tojson|safe }}; 204 + const reflectionBody = document.getElementById('reflectionBody'); 205 + const copyButton = document.getElementById('copyReflectionButton'); 206 + const rawUrl = {{ raw_url|tojson|safe }}; 207 + 208 + function renderMarkdown(raw) { 209 + return DOMPurify.sanitize(marked.parse(raw || '', { breaks: true, gfm: true })); 210 + } 211 + 212 + function copyText(text) { 213 + if (navigator.clipboard && navigator.clipboard.writeText) { 214 + return navigator.clipboard.writeText(text); 215 + } 216 + 217 + const area = document.createElement('textarea'); 218 + area.value = text; 219 + area.style.position = 'fixed'; 220 + area.style.opacity = '0'; 221 + document.body.appendChild(area); 222 + area.select(); 223 + document.execCommand('copy'); 224 + document.body.removeChild(area); 225 + return Promise.resolve(); 226 + } 227 + 228 + if (reflectionBody) { 229 + reflectionBody.innerHTML = renderMarkdown(markdownSource); 230 + } 231 + 232 + if (copyButton) { 233 + copyButton.addEventListener('click', async () => { 234 + const originalLabel = copyButton.textContent; 235 + try { 236 + const response = await fetch(rawUrl, { credentials: 'same-origin' }); 237 + if (!response.ok) throw new Error('copy failed'); 238 + await copyText(await response.text()); 239 + copyButton.textContent = 'Copied'; 240 + } catch (_error) { 241 + copyButton.textContent = 'Failed'; 242 + } finally { 243 + window.setTimeout(() => { 244 + copyButton.textContent = originalLabel; 245 + }, 2000); 246 + } 247 + }); 248 + } 249 + })(); 250 + </script> 251 + {% endif %}
+5
convey/chat_stream.py
··· 34 34 "talent_spawned": ("use_id", "name", "task", "started_at"), 35 35 "talent_finished": ("use_id", "name", "summary"), 36 36 "talent_errored": ("use_id", "name", "reason"), 37 + "reflection_ready": ("day", "url"), 37 38 "chat_error": ("reason", "use_id"), 38 39 } 39 40 _TRIGGER_KINDS = {"owner_message", "talent_finished", "talent_errored"} ··· 151 152 152 153 if kind == "talent_errored": 153 154 active_talents.pop(str(event["use_id"]), None) 155 + continue 156 + 157 + if kind == "reflection_ready": 158 + continue 154 159 155 160 return { 156 161 "latest_sol_message": latest_sol_message,
+33 -1
convey/static/app.css
··· 2452 2452 cursor: pointer; 2453 2453 } 2454 2454 2455 + .chat-reflection-card { 2456 + max-width: 70%; 2457 + padding: 10px 12px; 2458 + border-radius: 14px; 2459 + border: 1px solid rgba(180, 83, 9, 0.18); 2460 + background: rgba(245, 158, 11, 0.08); 2461 + display: flex; 2462 + flex-direction: column; 2463 + gap: 4px; 2464 + text-align: left; 2465 + } 2466 + 2455 2467 .chat-talent-card--spawned { 2456 2468 border-color: rgba(176, 106, 26, 0.22); 2457 2469 background: rgba(176, 106, 26, 0.08); ··· 2474 2486 font-weight: 600; 2475 2487 } 2476 2488 2489 + .chat-reflection-card-label { 2490 + font-size: 13px; 2491 + font-weight: 600; 2492 + color: #92400e; 2493 + } 2494 + 2477 2495 .chat-talent-card-task, 2478 2496 .chat-talent-card-summary, 2479 2497 .chat-talent-card-reason, 2480 - .chat-talent-card-detail { 2498 + .chat-talent-card-detail, 2499 + .chat-reflection-card-week { 2481 2500 font-size: 12px; 2482 2501 line-height: 1.4; 2502 + } 2503 + 2504 + .chat-reflection-card-link { 2505 + width: fit-content; 2506 + font-size: 12px; 2507 + font-weight: 600; 2508 + color: #92400e; 2509 + text-decoration: none; 2510 + } 2511 + 2512 + .chat-reflection-card-link:hover { 2513 + text-decoration: underline; 2483 2514 } 2484 2515 2485 2516 .chat-error-block { ··· 2532 2563 2533 2564 .chat-bubble, 2534 2565 .chat-talent-card, 2566 + .chat-reflection-card, 2535 2567 .chat-error-block { 2536 2568 max-width: 88%; 2537 2569 }
+1
pyproject.toml
··· 64 64 "pypdf", 65 65 "pdf2image", 66 66 "pytesseract", 67 + "weasyprint", 67 68 "icalendar", 68 69 "blessed>=1.20.0", 69 70 "psutil",
+146
tests/test_app_reflections.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + from pathlib import Path 7 + from unittest.mock import patch 8 + 9 + from convey import create_app 10 + 11 + REFLECTION_FIXTURE = Path("tests/fixtures/journal/reflections/weekly/20260308.md") 12 + 13 + 14 + def _seed_reflection(journal: Path, content: str | None = None) -> None: 15 + target = journal / "reflections" / "weekly" / "20260308.md" 16 + target.parent.mkdir(parents=True, exist_ok=True) 17 + target.write_text( 18 + content 19 + if content is not None 20 + else REFLECTION_FIXTURE.read_text(encoding="utf-8"), 21 + encoding="utf-8", 22 + ) 23 + 24 + 25 + def _make_client(journal: Path): 26 + app = create_app(str(journal)) 27 + app.config["TESTING"] = True 28 + client = app.test_client() 29 + with client.session_transaction() as session: 30 + session["logged_in"] = True 31 + session.permanent = True 32 + return client 33 + 34 + 35 + def test_reflections_index_lists_available_weeks(journal_copy): 36 + _seed_reflection(journal_copy) 37 + client = _make_client(journal_copy) 38 + 39 + response = client.get("/app/reflections/") 40 + html = response.get_data(as_text=True) 41 + 42 + assert response.status_code == 200 43 + assert "Available weekly reflections" in html 44 + assert 'href="/app/reflections/20260308"' in html 45 + 46 + 47 + def test_reflections_detail_renders_week(journal_copy): 48 + _seed_reflection(journal_copy) 49 + client = _make_client(journal_copy) 50 + 51 + response = client.get("/app/reflections/20260308") 52 + html = response.get_data(as_text=True) 53 + 54 + assert response.status_code == 200 55 + assert "weekly reflection" in html 56 + assert "week of Sunday March 8th" in html 57 + assert ">Copy<" in html 58 + assert ">Download PDF<" in html 59 + 60 + 61 + def test_reflections_detail_canonicalizes_to_sunday(journal_copy): 62 + _seed_reflection(journal_copy) 63 + client = _make_client(journal_copy) 64 + 65 + response = client.get("/app/reflections/20260310") 66 + 67 + assert response.status_code == 302 68 + assert response.headers["Location"].endswith("/app/reflections/20260308") 69 + 70 + 71 + def test_reflections_missing_week_returns_plain_text_404(journal_copy): 72 + client = _make_client(journal_copy) 73 + 74 + response = client.get("/app/reflections/20260315") 75 + 76 + assert response.status_code == 404 77 + assert response.mimetype == "text/plain" 78 + assert "Reflection not found" in response.get_data(as_text=True) 79 + 80 + 81 + def test_reflections_raw_returns_markdown(journal_copy): 82 + _seed_reflection(journal_copy) 83 + client = _make_client(journal_copy) 84 + 85 + response = client.get("/app/reflections/20260308/raw") 86 + text = response.get_data(as_text=True) 87 + 88 + assert response.status_code == 200 89 + assert response.headers["Content-Type"] == "text/markdown; charset=utf-8" 90 + assert text.startswith("---\ntype: weekly_reflection") 91 + 92 + 93 + def test_reflections_pdf_returns_attachment(journal_copy): 94 + _seed_reflection(journal_copy) 95 + client = _make_client(journal_copy) 96 + 97 + response = client.get("/app/reflections/20260308/pdf") 98 + 99 + assert response.status_code == 200 100 + assert response.mimetype == "application/pdf" 101 + assert ( 102 + response.headers["Content-Disposition"] 103 + == 'attachment; filename="reflection-20260308.pdf"' 104 + ) 105 + assert response.data.startswith(b"%PDF") 106 + 107 + 108 + def test_reflections_pdf_rejects_remote_assets(journal_copy): 109 + _seed_reflection( 110 + journal_copy, 111 + """--- 112 + type: weekly_reflection 113 + week: 20260308 114 + generated: 2026-03-10T19:00:00Z 115 + model: openai/gpt-5 116 + sources: 117 + newsletters: 0 118 + activities: 0 119 + decisions: 0 120 + followups: 0 121 + todos: 0 122 + relationship_signals: 0 123 + gaps: [] 124 + --- 125 + 126 + ![remote](https://example.com/reflection.png) 127 + """, 128 + ) 129 + client = _make_client(journal_copy) 130 + 131 + with patch("apps.reflections.routes.default_url_fetcher") as mock_fetcher: 132 + response = client.get("/app/reflections/20260308/pdf") 133 + 134 + assert response.status_code == 200 135 + assert response.mimetype == "application/pdf" 136 + mock_fetcher.assert_not_called() 137 + 138 + 139 + def test_reflections_stats_returns_month_counts(journal_copy): 140 + _seed_reflection(journal_copy) 141 + client = _make_client(journal_copy) 142 + 143 + response = client.get("/app/reflections/api/stats/202603") 144 + 145 + assert response.status_code == 200 146 + assert response.get_json() == {"20260308": 1}
+22
tests/test_chat_stream.py
··· 335 335 reason="chat had trouble — try again", 336 336 use_id=None, 337 337 ) 338 + append_chat_event( 339 + "reflection_ready", 340 + ts=start + 7_000, 341 + day="20260308", 342 + url="/app/reflections/20260308", 343 + ) 338 344 339 345 reduced = reduce_chat_state("20260420") 340 346 ··· 363 369 "finished_at": start + 3_000, 364 370 } 365 371 ] 372 + 373 + 374 + def test_append_reflection_ready_event(tmp_path, monkeypatch): 375 + _setup_journal(tmp_path, monkeypatch) 376 + ts = _ms(2026, 4, 20, 12, 0, 0) 377 + 378 + event = append_chat_event( 379 + "reflection_ready", 380 + ts=ts, 381 + day="20260308", 382 + url="/app/reflections/20260308", 383 + ) 384 + 385 + assert event["kind"] == "reflection_ready" 386 + assert event["day"] == "20260308" 387 + assert event["url"] == "/app/reflections/20260308" 366 388 367 389 368 390 def test_find_unresponded_trigger_owner_message(tmp_path, monkeypatch):
+104
tests/test_home_reflections.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + from datetime import datetime 7 + 8 + from convey import create_app 9 + 10 + 11 + def _make_client(journal_path: str): 12 + app = create_app(journal_path) 13 + app.config["TESTING"] = True 14 + client = app.test_client() 15 + with client.session_transaction() as session: 16 + session["logged_in"] = True 17 + session.permanent = True 18 + return client 19 + 20 + 21 + def _minimal_pulse_context(latest_weekly_reflection): 22 + return { 23 + "today": "20260310", 24 + "now": datetime(2026, 3, 10, 12, 0, 0), 25 + "capture_status": "active", 26 + "last_observe_relative": None, 27 + "attention": None, 28 + "pipeline_status": None, 29 + "segment_count": 0, 30 + "duration_minutes": 0, 31 + "facet_data": {}, 32 + "narrative_content": None, 33 + "narrative_updated_at": None, 34 + "narrative_source": None, 35 + "narrative_header": "today's flow", 36 + "pulse_needs": [], 37 + "flow_content": None, 38 + "flow_updated_at": None, 39 + "anticipated_activities": [], 40 + "activities": [], 41 + "todos": [], 42 + "entities": [], 43 + "routines": [], 44 + "skills": [], 45 + "skills_summary": "", 46 + "skills_content": {}, 47 + "briefing_sections": {}, 48 + "briefing_meta": None, 49 + "briefing_phase": "eod", 50 + "briefing_exists": False, 51 + "briefing_summary": None, 52 + "briefing_needs_deduped": [], 53 + "briefing_needs_shared_count": 0, 54 + "briefing_needs_badge": None, 55 + "latest_weekly_reflection": latest_weekly_reflection, 56 + "yesterday_processing": { 57 + "has_story": False, 58 + "framing": "", 59 + "summary": "", 60 + "details": [], 61 + "label": "", 62 + }, 63 + "show_welcome": False, 64 + "narrative_summary": "", 65 + "routines_summary": "", 66 + "today_summary": "", 67 + "needs_summary": "", 68 + "network_summary": "", 69 + } 70 + 71 + 72 + def test_home_shows_latest_weekly_reflection_link(monkeypatch, journal_copy): 73 + client = _make_client(str(journal_copy)) 74 + monkeypatch.setattr( 75 + "apps.home.routes._build_pulse_context", 76 + lambda: _minimal_pulse_context( 77 + { 78 + "day": "20260308", 79 + "label": "Sunday March 8th", 80 + "url": "/app/reflections/20260308", 81 + } 82 + ), 83 + ) 84 + 85 + response = client.get("/app/home/") 86 + html = response.get_data(as_text=True) 87 + 88 + assert response.status_code == 200 89 + assert "weekly reflection" in html 90 + assert 'href="/app/reflections/20260308"' in html 91 + 92 + 93 + def test_home_omits_weekly_reflection_card_when_missing(monkeypatch, journal_copy): 94 + client = _make_client(str(journal_copy)) 95 + monkeypatch.setattr( 96 + "apps.home.routes._build_pulse_context", 97 + lambda: _minimal_pulse_context(None), 98 + ) 99 + 100 + response = client.get("/app/home/") 101 + html = response.get_data(as_text=True) 102 + 103 + assert response.status_code == 200 104 + assert 'href="/app/reflections/' not in html
+251
uv.lock
··· 260 260 ] 261 261 262 262 [[package]] 263 + name = "brotli" 264 + version = "1.2.0" 265 + source = { registry = "https://pypi.org/simple" } 266 + sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } 267 + wheels = [ 268 + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, 269 + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, 270 + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, 271 + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, 272 + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, 273 + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, 274 + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, 275 + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, 276 + { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" }, 277 + { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" }, 278 + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, 279 + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, 280 + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, 281 + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, 282 + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, 283 + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, 284 + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, 285 + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, 286 + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, 287 + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, 288 + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, 289 + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, 290 + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, 291 + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, 292 + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, 293 + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, 294 + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, 295 + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, 296 + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, 297 + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, 298 + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, 299 + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, 300 + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, 301 + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, 302 + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, 303 + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, 304 + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, 305 + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, 306 + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, 307 + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, 308 + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, 309 + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, 310 + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, 311 + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, 312 + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, 313 + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, 314 + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, 315 + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, 316 + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, 317 + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, 318 + ] 319 + 320 + [[package]] 321 + name = "brotlicffi" 322 + version = "1.2.0.1" 323 + source = { registry = "https://pypi.org/simple" } 324 + dependencies = [ 325 + { name = "cffi" }, 326 + ] 327 + sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } 328 + wheels = [ 329 + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, 330 + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, 331 + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, 332 + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, 333 + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, 334 + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, 335 + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, 336 + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, 337 + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, 338 + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, 339 + { url = "https://files.pythonhosted.org/packages/7f/53/6262c2256513e6f530d81642477cb19367270922063eaa2d7b781d8c723d/brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851", size = 402265, upload-time = "2026-03-05T19:54:05.858Z" }, 340 + { url = "https://files.pythonhosted.org/packages/1f/d9/d5340b43cf5fbe7fe5a083d237e5338cc1caa73bea523be1c5e452c26290/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf", size = 406710, upload-time = "2026-03-05T19:54:07.272Z" }, 341 + { url = "https://files.pythonhosted.org/packages/a3/82/dbced4c1e0792efdf23fd90ff6d2a320c64ff4dfef7aacc85c04fde9ddd2/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4", size = 402787, upload-time = "2026-03-05T19:54:08.73Z" }, 342 + { url = "https://files.pythonhosted.org/packages/ef/6f/534205ba7590c9a8716a614f270c5c2ec419b5b7079b3f9cd31b7b5580de/brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1", size = 375108, upload-time = "2026-03-05T19:54:10.079Z" }, 343 + ] 344 + 345 + [[package]] 263 346 name = "certifi" 264 347 version = "2026.1.4" 265 348 source = { registry = "https://pypi.org/simple" } ··· 625 708 ] 626 709 627 710 [[package]] 711 + name = "cssselect2" 712 + version = "0.9.0" 713 + source = { registry = "https://pypi.org/simple" } 714 + dependencies = [ 715 + { name = "tinycss2" }, 716 + { name = "webencodings" }, 717 + ] 718 + sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } 719 + wheels = [ 720 + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, 721 + ] 722 + 723 + [[package]] 628 724 name = "ctranslate2" 629 725 version = "4.7.1" 630 726 source = { registry = "https://pypi.org/simple" } ··· 855 951 ] 856 952 857 953 [[package]] 954 + name = "fonttools" 955 + version = "4.62.1" 956 + source = { registry = "https://pypi.org/simple" } 957 + sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } 958 + wheels = [ 959 + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, 960 + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, 961 + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, 962 + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, 963 + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, 964 + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, 965 + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, 966 + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, 967 + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, 968 + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, 969 + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, 970 + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, 971 + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, 972 + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, 973 + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, 974 + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, 975 + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, 976 + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, 977 + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, 978 + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, 979 + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, 980 + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, 981 + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, 982 + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, 983 + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, 984 + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, 985 + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, 986 + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, 987 + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, 988 + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, 989 + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, 990 + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, 991 + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, 992 + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, 993 + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, 994 + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, 995 + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, 996 + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, 997 + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, 998 + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, 999 + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, 1000 + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, 1001 + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, 1002 + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, 1003 + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, 1004 + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, 1005 + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, 1006 + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, 1007 + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, 1008 + ] 1009 + 1010 + [package.optional-dependencies] 1011 + woff = [ 1012 + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, 1013 + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, 1014 + { name = "zopfli" }, 1015 + ] 1016 + 1017 + [[package]] 858 1018 name = "freezegun" 859 1019 version = "1.5.5" 860 1020 source = { registry = "https://pypi.org/simple" } ··· 2588 2748 ] 2589 2749 2590 2750 [[package]] 2751 + name = "pydyf" 2752 + version = "0.12.1" 2753 + source = { registry = "https://pypi.org/simple" } 2754 + sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" } 2755 + wheels = [ 2756 + { url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" }, 2757 + ] 2758 + 2759 + [[package]] 2591 2760 name = "pyee" 2592 2761 version = "13.0.0" 2593 2762 source = { registry = "https://pypi.org/simple" } ··· 2645 2814 sdist = { url = "https://files.pythonhosted.org/packages/10/45/8340de1c752bfda2da912ea0fa8c9a432f7de3f6315e82f1c0847811dff6/pypdf-6.7.0.tar.gz", hash = "sha256:eb95e244d9f434e6cfd157272283339ef586e593be64ee699c620f756d5c3f7e", size = 5299947, upload-time = "2026-02-08T14:47:11.897Z" } 2646 2815 wheels = [ 2647 2816 { url = "https://files.pythonhosted.org/packages/ed/f1/c92e75a0eb18bb10845e792054ded113010de958b6d4998e201c029417bb/pypdf-6.7.0-py3-none-any.whl", hash = "sha256:62e85036d50839cbdf45b8067c2c1a1b925517514d7cba4cbe8755a6c2829bc9", size = 330557, upload-time = "2026-02-08T14:47:10.111Z" }, 2817 + ] 2818 + 2819 + [[package]] 2820 + name = "pyphen" 2821 + version = "0.17.2" 2822 + source = { registry = "https://pypi.org/simple" } 2823 + sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" } 2824 + wheels = [ 2825 + { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, 2648 2826 ] 2649 2827 2650 2828 [[package]] ··· 3606 3784 { name = "timefhuman" }, 3607 3785 { name = "typer" }, 3608 3786 { name = "tzlocal" }, 3787 + { name = "weasyprint" }, 3609 3788 { name = "webrtcvad-wheels" }, 3610 3789 { name = "websockets" }, 3611 3790 ] ··· 3658 3837 { name = "timefhuman" }, 3659 3838 { name = "typer" }, 3660 3839 { name = "tzlocal" }, 3840 + { name = "weasyprint" }, 3661 3841 { name = "webrtcvad-wheels", specifier = ">=2.0.12" }, 3662 3842 { name = "websockets", specifier = ">=13.0" }, 3663 3843 ] ··· 3829 4009 ] 3830 4010 3831 4011 [[package]] 4012 + name = "tinycss2" 4013 + version = "1.5.1" 4014 + source = { registry = "https://pypi.org/simple" } 4015 + dependencies = [ 4016 + { name = "webencodings" }, 4017 + ] 4018 + sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } 4019 + wheels = [ 4020 + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, 4021 + ] 4022 + 4023 + [[package]] 4024 + name = "tinyhtml5" 4025 + version = "2.1.0" 4026 + source = { registry = "https://pypi.org/simple" } 4027 + dependencies = [ 4028 + { name = "webencodings" }, 4029 + ] 4030 + sdist = { url = "https://files.pythonhosted.org/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" } 4031 + wheels = [ 4032 + { url = "https://files.pythonhosted.org/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" }, 4033 + ] 4034 + 4035 + [[package]] 3832 4036 name = "tokenizers" 3833 4037 version = "0.22.2" 3834 4038 source = { registry = "https://pypi.org/simple" } ··· 4135 4339 ] 4136 4340 4137 4341 [[package]] 4342 + name = "weasyprint" 4343 + version = "68.1" 4344 + source = { registry = "https://pypi.org/simple" } 4345 + dependencies = [ 4346 + { name = "cffi" }, 4347 + { name = "cssselect2" }, 4348 + { name = "fonttools", extra = ["woff"] }, 4349 + { name = "pillow" }, 4350 + { name = "pydyf" }, 4351 + { name = "pyphen" }, 4352 + { name = "tinycss2" }, 4353 + { name = "tinyhtml5" }, 4354 + ] 4355 + sdist = { url = "https://files.pythonhosted.org/packages/db/3e/65c0f176e6fb5c2b0a1ac13185b366f727d9723541babfa7fa4309998169/weasyprint-68.1.tar.gz", hash = "sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e", size = 1542379, upload-time = "2026-02-06T15:04:11.203Z" } 4356 + wheels = [ 4357 + { url = "https://files.pythonhosted.org/packages/dd/dd/14eb73cea481ad8162d3b18a4850d4a84d6e804a22840cca207648532265/weasyprint-68.1-py3-none-any.whl", hash = "sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be", size = 319789, upload-time = "2026-02-06T15:04:09.189Z" }, 4358 + ] 4359 + 4360 + [[package]] 4361 + name = "webencodings" 4362 + version = "0.5.1" 4363 + source = { registry = "https://pypi.org/simple" } 4364 + sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } 4365 + wheels = [ 4366 + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, 4367 + ] 4368 + 4369 + [[package]] 4138 4370 name = "webrtcvad-wheels" 4139 4371 version = "2.0.14" 4140 4372 source = { registry = "https://pypi.org/simple" } ··· 4434 4666 wheels = [ 4435 4667 { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, 4436 4668 ] 4669 + 4670 + [[package]] 4671 + name = "zopfli" 4672 + version = "0.4.1" 4673 + source = { registry = "https://pypi.org/simple" } 4674 + sdist = { url = "https://files.pythonhosted.org/packages/0a/4d/a8cc1768b2eda3c0c7470bf8059dcb94ef96d45dd91fc6edd29430d44072/zopfli-0.4.1.tar.gz", hash = "sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e", size = 179001, upload-time = "2026-02-13T14:17:27.156Z" } 4675 + wheels = [ 4676 + { url = "https://files.pythonhosted.org/packages/e1/2f/1a7082e9163ae3703b27d571720bf3c954a02a9cf1fdce47c51e70639256/zopfli-0.4.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54", size = 291570, upload-time = "2026-02-13T14:17:12.556Z" }, 4677 + { url = "https://files.pythonhosted.org/packages/dd/6f/4a1a88edf9fa0ce102703f38ab4dfb285b7cd2dde5389184264ec759e06e/zopfli-0.4.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07", size = 829437, upload-time = "2026-02-13T14:17:14.431Z" }, 4678 + { url = "https://files.pythonhosted.org/packages/e3/77/d231012ddcaac9d2e184bd7808e106a8a0048855912e2e1c902b3f383413/zopfli-0.4.1-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57", size = 818542, upload-time = "2026-02-13T14:17:16.353Z" }, 4679 + { url = "https://files.pythonhosted.org/packages/0d/4e/9b23690c4ca14fbeae2a8f7f6b2006611bf4cd7d5bcb2d9e6c718bd4b0e9/zopfli-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c", size = 1778034, upload-time = "2026-02-13T14:17:17.509Z" }, 4680 + { url = "https://files.pythonhosted.org/packages/e3/1b/51f7c28d4cde639cac4f5d47ff615548c1d9809f43cbacdd66eba5cd679d/zopfli-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b", size = 1863957, upload-time = "2026-02-13T14:17:19.259Z" }, 4681 + { url = "https://files.pythonhosted.org/packages/ae/4d/1ef17017d38eabe7ae28f18ef0f16d48966cc23a5657e4555fff61704539/zopfli-0.4.1-cp310-abi3-win32.whl", hash = "sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd", size = 82314, upload-time = "2026-02-13T14:17:20.795Z" }, 4682 + { url = "https://files.pythonhosted.org/packages/0f/94/806bc84b389c7d70051d7c9a0179cff52de8b9f8dc2fc25bcf0bca302986/zopfli-0.4.1-cp310-abi3-win_amd64.whl", hash = "sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b", size = 102186, upload-time = "2026-02-13T14:17:21.622Z" }, 4683 + { url = "https://files.pythonhosted.org/packages/15/53/0afc94574553bad50d7add81f54eed1a864e13f91c3a342c99775a947ff9/zopfli-0.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760", size = 147127, upload-time = "2026-02-13T14:17:22.995Z" }, 4684 + { url = "https://files.pythonhosted.org/packages/45/2b/0d9e4bdfd3d646a36b8516a01dec4ccd2967554603801e7c2d6c72fede3d/zopfli-0.4.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a93c2ecafff372de6c0aa2212eff18a75f6c71a100372fee7b4b129cc0b6f9a7", size = 127349, upload-time = "2026-02-13T14:17:24.107Z" }, 4685 + { url = "https://files.pythonhosted.org/packages/23/f0/ad6e26aa06943ce9f1be4ae6738513a7b69d8ea1f3b13e46009a249a3f73/zopfli-0.4.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb136a74d14a4ecfae29cb0fdecece58a6c115abc9a74c12bc6ac62e80f229d7", size = 124371, upload-time = "2026-02-13T14:17:24.976Z" }, 4686 + { url = "https://files.pythonhosted.org/packages/7b/36/3c15d564db6dfdd740919b205bdb69be75113e9919c422cde658e6d013c0/zopfli-0.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2f992ac7d83cbddd889e1813ace576cbc91a05d5d7a0a21b366e2e5f492e7707", size = 102199, upload-time = "2026-02-13T14:17:26.246Z" }, 4687 + ]