Bor Hodošček commited on
Commit
a388ec1
·
unverified ·
1 Parent(s): 13fd8f6

feat: initial working version

Browse files
Files changed (6) hide show
  1. Dockerfile +21 -0
  2. README.md +1 -1
  3. app.py +1247 -0
  4. development.md +8 -0
  5. pyproject.toml +12 -0
  6. uv.lock +600 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ COPY --from=ghcr.io/astral-sh/uv:0.7.13 /uv /bin/uv
3
+
4
+ RUN useradd -m -u 1000 user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+ ENV UV_SYSTEM_PYTHON=1
7
+
8
+ WORKDIR /app
9
+
10
+ RUN apt update && apt install -y curl unzip gcc g++
11
+ RUN mkdir -p /app && chown -R user:user /app
12
+
13
+ COPY --chown=user ./pyproject.toml ./uv.lock ./app.py /app
14
+
15
+ RUN chmod -R u+w /app
16
+
17
+ USER user
18
+
19
+ RUN uv sync
20
+
21
+ CMD ["uv", "run", "marimo", "run", "app.py", "--no-sandbox", "--include-code", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Llm Text Preprocessing
3
  emoji: 🐢
4
  colorFrom: pink
5
  colorTo: red
 
1
  ---
2
+ title: LLM Text Preprocessing Checker
3
  emoji: 🐢
4
  colorFrom: pink
5
  colorTo: red
app.py ADDED
@@ -0,0 +1,1247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.12"
3
+ # dependencies = [
4
+ # "charset-normalizer",
5
+ # "great-tables",
6
+ # "marimo",
7
+ # "pandas",
8
+ # ]
9
+ # ///
10
+ import marimo
11
+
12
+ __generated_with = "0.14.6"
13
+ app = marimo.App(width="full", app_title="LLM Text Preprocessing Checker")
14
+
15
+
16
+ @app.cell
17
+ def _():
18
+ import marimo as mo
19
+
20
+ return (mo,)
21
+
22
+
23
+ @app.cell
24
+ def _(mo):
25
+ mo.md(
26
+ r"""
27
+ # LLM Text Preprocessing Checker
28
+
29
+ Checks two files and provides the diff output as well as metrics on deleted and inserted characters.
30
+ Additionaly, provides a breakdown by Unicode character class of deletions and insertions.
31
+
32
+ Note that this uses a pure-Python Myers diff algorithm for the comparison and may not be performant for larger diffs.
33
+ """
34
+ )
35
+ return
36
+
37
+
38
+ @app.cell
39
+ def _():
40
+ import unicodedata
41
+ from typing import List, Dict, Any
42
+ from dataclasses import dataclass
43
+ from enum import IntEnum
44
+ import html as python_html
45
+ from great_tables import GT, loc, style
46
+ import pandas as pd
47
+
48
+ class Operation(IntEnum):
49
+ DELETE = 0
50
+ INSERT = 1
51
+ EQUAL = 2
52
+
53
+ @dataclass(slots=True)
54
+ class Edit:
55
+ operation: Operation
56
+ old_start: int
57
+ old_end: int
58
+ new_start: int
59
+ new_end: int
60
+ old_text: str = ""
61
+ new_text: str = ""
62
+
63
+ DEL_STYLE = "background-color:#ffcccc;color:#880000;text-decoration:line-through;"
64
+ INS_STYLE = "background-color:#ccffcc;color:#008800;"
65
+ EQUAL_STYLE = "color:#666666;"
66
+ CONTAINER_STYLE = (
67
+ "font-family: ui-monospace, monospace; "
68
+ "white-space: pre-wrap; "
69
+ "line-height: 1.6; "
70
+ "padding: 20px; "
71
+ "background-color: #f8f9fa; "
72
+ "border-radius: 8px; "
73
+ "border: 1px solid #dee2e6;"
74
+ )
75
+
76
+ def classify_char(char: str) -> str:
77
+ """Classify a character using Unicode categories."""
78
+ if not char:
79
+ return "empty"
80
+
81
+ category = unicodedata.category(char)
82
+
83
+ # Map Unicode categories to readable classifications
84
+ category_map = {
85
+ "Ll": "lowercase",
86
+ "Lu": "uppercase",
87
+ "Lt": "titlecase",
88
+ "Lm": "modifier_letter",
89
+ "Lo": "other_letter",
90
+ "Nd": "decimal_digit",
91
+ "Nl": "letter_number",
92
+ "No": "other_number",
93
+ "Pc": "connector_punctuation",
94
+ "Pd": "dash_punctuation",
95
+ "Ps": "open_punctuation",
96
+ "Pe": "close_punctuation",
97
+ "Pi": "initial_punctuation",
98
+ "Pf": "final_punctuation",
99
+ "Po": "other_punctuation",
100
+ "Sm": "math_symbol",
101
+ "Sc": "currency_symbol",
102
+ "Sk": "modifier_symbol",
103
+ "So": "other_symbol",
104
+ "Zs": "space",
105
+ "Zl": "line_separator",
106
+ "Zp": "paragraph_separator",
107
+ "Cc": "control",
108
+ "Cf": "format",
109
+ "Co": "private_use",
110
+ "Cn": "unassigned",
111
+ }
112
+
113
+ # Special handling for CJK
114
+ if "\u4e00" <= char <= "\u9fff":
115
+ return "cjk_ideograph"
116
+ elif "\u3040" <= char <= "\u309f":
117
+ return "hiragana"
118
+ elif "\u30a0" <= char <= "\u30ff":
119
+ return "katakana"
120
+ elif "\uac00" <= char <= "\ud7af":
121
+ return "hangul"
122
+
123
+ return category_map.get(category, category)
124
+
125
+ def _myers_backtrack(trace: List[List[int]], a: str, b: str) -> List[Edit]:
126
+ """Back-tracking helper to materialise the edit script."""
127
+ edits: List[Edit] = []
128
+ n, m = len(a), len(b)
129
+ x, y = n, m
130
+ offset = len(trace[0]) // 2
131
+
132
+ # Walk the layers backwards
133
+ for d in range(len(trace) - 1, 0, -1):
134
+ v = trace[d]
135
+ k = x - y
136
+ idx = k + offset
137
+
138
+ # Determine the predecessor k'
139
+ if k == -d or (k != d and v[idx - 1] < v[idx + 1]):
140
+ k_prev = k + 1 # came from below (insertion)
141
+ else:
142
+ k_prev = k - 1 # came from right (deletion)
143
+
144
+ x_prev = trace[d - 1][k_prev + offset]
145
+ y_prev = x_prev - k_prev
146
+
147
+ # Emit the matching "snake"
148
+ while x > x_prev and y > y_prev:
149
+ x -= 1
150
+ y -= 1
151
+ edits.append(Edit(Operation.EQUAL, x, x + 1, y, y + 1, a[x], b[y]))
152
+
153
+ # Emit the single edit (INSERT or DELETE) that led to the snake
154
+ if x_prev == x: # insertion
155
+ y -= 1
156
+ edits.append(Edit(Operation.INSERT, x, x, y, y + 1, "", b[y]))
157
+ else: # deletion
158
+ x -= 1
159
+ edits.append(Edit(Operation.DELETE, x, x + 1, y, y, a[x], ""))
160
+
161
+ # Leading snake (d = 0) – everything matched at the start
162
+ while x > 0 and y > 0:
163
+ x -= 1
164
+ y -= 1
165
+ edits.append(Edit(Operation.EQUAL, x, x + 1, y, y + 1, a[x], b[y]))
166
+
167
+ # Any remaining leading insertions / deletions
168
+ while x > 0:
169
+ x -= 1
170
+ edits.append(Edit(Operation.DELETE, x, x + 1, y, y, a[x], ""))
171
+ while y > 0:
172
+ y -= 1
173
+ edits.append(Edit(Operation.INSERT, x, x, y, y + 1, "", b[y]))
174
+
175
+ edits.reverse()
176
+ return edits
177
+
178
+ def myers_diff(a: str, b: str) -> List[Edit]:
179
+ """
180
+ Very fast Myers diff (O((N+M)·D) time, O(N+M) memory).
181
+
182
+ Returns a list of Edit objects (DELETE / INSERT / EQUAL).
183
+ """
184
+ n, m = len(a), len(b)
185
+ if n == 0:
186
+ return [Edit(Operation.INSERT, 0, 0, 0, m, "", b)] if m else []
187
+ if m == 0:
188
+ return [Edit(Operation.DELETE, 0, n, 0, 0, a, "")] if n else []
189
+
190
+ max_d = n + m
191
+ offset = max_d # map k ∈ [-max_d .. +max_d] → index
192
+ v = [0] * (2 * max_d + 1) # current frontier
193
+ trace = [] # keeps a copy of v for every d
194
+
195
+ # Forward phase – build the "trace" that will be backtracked
196
+ for d in range(max_d + 1):
197
+ v_next = v[:] # copy *once* per layer
198
+ for k in range(-d, d + 1, 2):
199
+ idx = k + offset
200
+
201
+ # Choosing the predecessor (insertion vs deletion)
202
+ if k == -d or (k != d and v[idx - 1] < v[idx + 1]):
203
+ x = v[idx + 1] # insertion (move down)
204
+ else:
205
+ x = v[idx - 1] + 1 # deletion (move right)
206
+
207
+ y = x - k
208
+
209
+ # Greedy snake – march diagonally while chars match
210
+ while x < n and y < m and a[x] == b[y]:
211
+ x += 1
212
+ y += 1
213
+
214
+ v_next[idx] = x
215
+
216
+ # Reached the end – stop early
217
+ if x >= n and y >= m:
218
+ trace.append(v_next)
219
+ return _myers_backtrack(trace, a, b)
220
+
221
+ trace.append(v_next)
222
+ v = v_next # reuse buffer
223
+
224
+ # Should never get here
225
+ raise RuntimeError("diff failed")
226
+
227
+ def classify_text(text: str) -> Dict[str, int]:
228
+ """Count characters by classification."""
229
+ if not text:
230
+ return {}
231
+
232
+ classifications = {}
233
+ for char in text:
234
+ char_class = classify_char(char)
235
+ classifications[char_class] = classifications.get(char_class, 0) + 1
236
+
237
+ return classifications
238
+
239
+ def classify_edits(edits: List[Edit]) -> Dict[Operation, Dict[str, int]]:
240
+ """
241
+ Classify edit operations by character class.
242
+ Returns a nested dictionary: {operation: {char_class: count}}
243
+ """
244
+ # Filter out EQUAL operations to save memory
245
+ change_edits = [e for e in edits if e.operation != Operation.EQUAL]
246
+
247
+ # Group all edits by operation type (not consecutive grouping)
248
+ edits_by_op = {}
249
+ for edit in change_edits:
250
+ if edit.operation not in edits_by_op:
251
+ edits_by_op[edit.operation] = []
252
+ edits_by_op[edit.operation].append(edit)
253
+
254
+ result = {}
255
+ for op, edit_list in edits_by_op.items():
256
+ combined_text = ""
257
+ if op == Operation.DELETE:
258
+ combined_text = "".join(e.old_text for e in edit_list)
259
+ elif op == Operation.INSERT:
260
+ combined_text = "".join(e.new_text for e in edit_list)
261
+
262
+ result[op] = classify_text(combined_text)
263
+
264
+ return result
265
+
266
+ def calculate_change_metrics(
267
+ original: str,
268
+ edits: List[Edit],
269
+ classifications: Dict[Operation, Dict[str, int]],
270
+ ) -> Dict[str, Any]:
271
+ """Calculate detailed change metrics including percentages."""
272
+ metrics = {
273
+ "total_original_chars": len(original),
274
+ "total_deleted_chars": 0,
275
+ "total_inserted_chars": 0,
276
+ "deletion_percentage": 0.0,
277
+ "insertion_percentage": 0.0,
278
+ "net_change_percentage": 0.0,
279
+ "char_class_metrics": {},
280
+ }
281
+
282
+ # Calculate total changes
283
+ for edit in edits:
284
+ if edit.operation == Operation.DELETE:
285
+ metrics["total_deleted_chars"] += len(edit.old_text)
286
+ elif edit.operation == Operation.INSERT:
287
+ metrics["total_inserted_chars"] += len(edit.new_text)
288
+
289
+ # Calculate percentages
290
+ if metrics["total_original_chars"] > 0:
291
+ metrics["deletion_percentage"] = (
292
+ metrics["total_deleted_chars"] / metrics["total_original_chars"]
293
+ ) * 100
294
+ metrics["insertion_percentage"] = (
295
+ metrics["total_inserted_chars"] / metrics["total_original_chars"]
296
+ ) * 100
297
+ net_change = (
298
+ metrics["total_inserted_chars"] - metrics["total_deleted_chars"]
299
+ )
300
+ metrics["net_change_percentage"] = (
301
+ net_change / metrics["total_original_chars"]
302
+ ) * 100
303
+
304
+ # Get character classification of original text
305
+ original_classifications = classify_text(original)
306
+
307
+ # Calculate per-character-class metrics
308
+ all_char_classes = set()
309
+ for op_classes in classifications.values():
310
+ all_char_classes.update(op_classes.keys())
311
+ all_char_classes.update(original_classifications.keys())
312
+
313
+ for char_class in all_char_classes:
314
+ original_count = original_classifications.get(char_class, 0)
315
+ deleted_count = classifications.get(Operation.DELETE, {}).get(char_class, 0)
316
+ inserted_count = classifications.get(Operation.INSERT, {}).get(
317
+ char_class, 0
318
+ )
319
+
320
+ class_metrics = {
321
+ "original_count": original_count,
322
+ "deleted_count": deleted_count,
323
+ "inserted_count": inserted_count,
324
+ "deletion_percentage": 0.0,
325
+ "insertion_percentage": 0.0,
326
+ }
327
+
328
+ if original_count > 0:
329
+ class_metrics["deletion_percentage"] = (
330
+ deleted_count / original_count
331
+ ) * 100
332
+
333
+ # Insertion percentage relative to original count of this class
334
+ if original_count > 0:
335
+ class_metrics["insertion_percentage"] = (
336
+ inserted_count / original_count
337
+ ) * 100
338
+ elif inserted_count > 0:
339
+ # If there were none originally, show as new
340
+ class_metrics["insertion_percentage"] = float("inf")
341
+
342
+ metrics["char_class_metrics"][char_class] = class_metrics
343
+
344
+ return metrics
345
+
346
+ def escape_html(text: str) -> str:
347
+ """Escape HTML and make whitespace visible."""
348
+ # First escape HTML
349
+ text = python_html.escape(text)
350
+ # Make whitespace visible
351
+ ws_trans = str.maketrans({" ": "·", "\t": "→ ", "\n": "¶\n"})
352
+ return text.translate(ws_trans)
353
+
354
+ def generate_html_diff(
355
+ edits: List[Edit], show_equal: bool = True, max_equal_length: int = 100
356
+ ) -> str:
357
+ """Generate HTML visualization of the diff with performance optimizations."""
358
+ # Pre-allocate list for better performance
359
+ html_parts = []
360
+
361
+ # Group consecutive edits of the same type to reduce HTML tags
362
+ grouped_edits = []
363
+ current_group = []
364
+ current_op = None
365
+
366
+ for edit in edits:
367
+ if (
368
+ edit.operation == current_op and len(current_group) < 100
369
+ ): # Batch up to 100
370
+ current_group.append(edit)
371
+ else:
372
+ if current_group:
373
+ grouped_edits.append((current_op, current_group))
374
+ current_group = [edit]
375
+ current_op = edit.operation
376
+
377
+ if current_group:
378
+ grouped_edits.append((current_op, current_group))
379
+
380
+ # Process grouped edits
381
+ for op, group in grouped_edits:
382
+ if op == Operation.DELETE:
383
+ combined_text = "".join(e.old_text for e in group)
384
+ escaped = escape_html(combined_text)
385
+ html_parts.append(
386
+ f'<span style="{DEL_STYLE}" title="Deleted">{escaped}</span>'
387
+ )
388
+ elif op == Operation.INSERT:
389
+ combined_text = "".join(e.new_text for e in group)
390
+ escaped = escape_html(combined_text)
391
+ html_parts.append(
392
+ f'<span style="{INS_STYLE}" title="Added">{escaped}</span>'
393
+ )
394
+ elif op == Operation.EQUAL and show_equal:
395
+ combined_text = "".join(e.old_text for e in group)
396
+ # Truncate very long equal sections
397
+ if len(combined_text) > max_equal_length:
398
+ start = escape_html(combined_text[: max_equal_length // 2])
399
+ end = escape_html(combined_text[-max_equal_length // 2 :])
400
+ omitted = len(combined_text) - max_equal_length
401
+ html_parts.append(
402
+ f'<span style="{EQUAL_STYLE}">{start}'
403
+ f"<em>...{omitted} chars omitted...</em>"
404
+ f"{end}</span>"
405
+ )
406
+ else:
407
+ escaped = escape_html(combined_text)
408
+ html_parts.append(f'<span style="{EQUAL_STYLE}">{escaped}</span>')
409
+
410
+ return f'<div style="{CONTAINER_STYLE}">{"".join(html_parts)}</div>'
411
+
412
+ def generate_side_by_side_html(edits: List[Edit]) -> str:
413
+ """Generate side-by-side HTML diff view."""
414
+ old_parts = []
415
+ new_parts = []
416
+
417
+ for edit in edits:
418
+ if edit.operation == Operation.DELETE:
419
+ escaped = escape_html(edit.old_text)
420
+ old_parts.append(f'<span style="{DEL_STYLE}">{escaped}</span>')
421
+ elif edit.operation == Operation.INSERT:
422
+ escaped = escape_html(edit.new_text)
423
+ new_parts.append(f'<span style="{INS_STYLE}">{escaped}</span>')
424
+ elif edit.operation == Operation.EQUAL:
425
+ escaped = escape_html(edit.old_text)
426
+ old_parts.append(f"<span>{escaped}</span>")
427
+ new_parts.append(f"<span>{escaped}</span>")
428
+
429
+ return f'''
430
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
431
+ <div>
432
+ <h4 style="margin: 0 0 10px 0;">Original</h4>
433
+ <div style="{CONTAINER_STYLE}">{"".join(old_parts)}</div>
434
+ </div>
435
+ <div>
436
+ <h4 style="margin: 0 0 10px 0;">Processed</h4>
437
+ <div style="{CONTAINER_STYLE}">{"".join(new_parts)}</div>
438
+ </div>
439
+ </div>
440
+ '''
441
+
442
+ def generate_html_diff_fast(edits: List[Edit], context_chars: int = 5) -> str:
443
+ """
444
+ Ultra-fast HTML diff generation showing only changes with context.
445
+ """
446
+ html_parts = []
447
+
448
+ # Filter to only show changes and surrounding context
449
+ change_indices = [
450
+ i for i, e in enumerate(edits) if e.operation != Operation.EQUAL
451
+ ]
452
+
453
+ if not change_indices:
454
+ return '<div style="{CONTAINER_STYLE}">No changes found.</div>'
455
+
456
+ # Build ranges to show (change + context)
457
+ ranges_to_show = []
458
+ start = max(0, change_indices[0] - context_chars)
459
+ end = min(len(edits), change_indices[0] + context_chars + 1)
460
+
461
+ for idx in change_indices[1:]:
462
+ if idx - end <= context_chars * 2:
463
+ # Extend current range
464
+ end = min(len(edits), idx + context_chars + 1)
465
+ else:
466
+ # Save current range and start new one
467
+ ranges_to_show.append((start, end))
468
+ start = max(0, idx - context_chars)
469
+ end = min(len(edits), idx + context_chars + 1)
470
+
471
+ ranges_to_show.append((start, end))
472
+
473
+ # Generate HTML for ranges
474
+ for i, (start, end) in enumerate(ranges_to_show):
475
+ if i > 0:
476
+ html_parts.append(
477
+ '<div style="color:#999;text-align:center;margin:10px 0;">...</div>'
478
+ )
479
+
480
+ for j in range(start, end):
481
+ edit = edits[j]
482
+ if edit.operation == Operation.DELETE:
483
+ escaped = escape_html(edit.old_text)
484
+ html_parts.append(f'<span style="{DEL_STYLE}">{escaped}</span>')
485
+ elif edit.operation == Operation.INSERT:
486
+ escaped = escape_html(edit.new_text)
487
+ html_parts.append(f'<span style="{INS_STYLE}">{escaped}</span>')
488
+ else: # EQUAL
489
+ escaped = escape_html(edit.old_text)
490
+ html_parts.append(f'<span style="{EQUAL_STYLE}">{escaped}</span>')
491
+
492
+ return f'<div style="{CONTAINER_STYLE}">{"".join(html_parts)}</div>'
493
+
494
+ def generate_side_by_side_html_fast(
495
+ edits: List[Edit], context_chars: int = 5
496
+ ) -> str:
497
+ """
498
+ Fast side-by-side HTML diff generation showing only changes with context.
499
+ """
500
+ # Filter to only show changes and surrounding context
501
+ change_indices = [
502
+ i for i, e in enumerate(edits) if e.operation != Operation.EQUAL
503
+ ]
504
+
505
+ if not change_indices:
506
+ return """
507
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
508
+ <div>
509
+ <h4 style="margin: 0 0 10px 0;">Original</h4>
510
+ <div style="{CONTAINER_STYLE}">No changes found.</div>
511
+ </div>
512
+ <div>
513
+ <h4 style="margin: 0 0 10px 0;">Processed</h4>
514
+ <div style="{CONTAINER_STYLE}">No changes found.</div>
515
+ </div>
516
+ </div>
517
+ """
518
+
519
+ # Build ranges to show (change + context)
520
+ ranges_to_show = []
521
+ start = max(0, change_indices[0] - context_chars)
522
+ end = min(len(edits), change_indices[0] + context_chars + 1)
523
+
524
+ for idx in change_indices[1:]:
525
+ if idx - end <= context_chars * 2:
526
+ # Extend current range
527
+ end = min(len(edits), idx + context_chars + 1)
528
+ else:
529
+ # Save current range and start new one
530
+ ranges_to_show.append((start, end))
531
+ start = max(0, idx - context_chars)
532
+ end = min(len(edits), idx + context_chars + 1)
533
+
534
+ ranges_to_show.append((start, end))
535
+
536
+ # Generate HTML for ranges
537
+ old_parts = []
538
+ new_parts = []
539
+
540
+ for i, (start, end) in enumerate(ranges_to_show):
541
+ if i > 0:
542
+ separator = (
543
+ '<div style="color:#999;text-align:center;margin:10px 0;">...</div>'
544
+ )
545
+ old_parts.append(separator)
546
+ new_parts.append(separator)
547
+
548
+ for j in range(start, end):
549
+ edit = edits[j]
550
+ if edit.operation == Operation.DELETE:
551
+ escaped = escape_html(edit.old_text)
552
+ old_parts.append(f'<span style="{DEL_STYLE}">{escaped}</span>')
553
+ elif edit.operation == Operation.INSERT:
554
+ escaped = escape_html(edit.new_text)
555
+ new_parts.append(f'<span style="{INS_STYLE}">{escaped}</span>')
556
+ else: # EQUAL
557
+ escaped = escape_html(edit.old_text)
558
+ old_parts.append(f'<span style="{EQUAL_STYLE}">{escaped}</span>')
559
+ new_parts.append(f'<span style="{EQUAL_STYLE}">{escaped}</span>')
560
+
561
+ return f'''
562
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
563
+ <div>
564
+ <h4 style="margin: 0 0 10px 0;">Original</h4>
565
+ <div style="{CONTAINER_STYLE}">{"".join(old_parts)}</div>
566
+ </div>
567
+ <div>
568
+ <h4 style="margin: 0 0 10px 0;">Processed</h4>
569
+ <div style="{CONTAINER_STYLE}">{"".join(new_parts)}</div>
570
+ </div>
571
+ </div>
572
+ '''
573
+
574
+ def operation_to_past(op: Operation) -> str:
575
+ if op == Operation.INSERT:
576
+ return "inserted"
577
+ else:
578
+ return str(op) + "d"
579
+
580
+ def format_diff_summary(
581
+ edits: List[Edit],
582
+ classifications: Dict[Operation, Dict[str, int]],
583
+ metrics: Dict[str, Any],
584
+ ) -> str:
585
+ """Create a human-readable summary of the diff."""
586
+ lines = ["## Diff Summary\n"]
587
+
588
+ # Overall statistics
589
+ lines.append("### Overall Statistics")
590
+ lines.append(
591
+ f"- **Original text**: {metrics['total_original_chars']:,} characters"
592
+ )
593
+
594
+ # Format deletions
595
+ del_pct = format_percentage(metrics["deletion_percentage"])
596
+ lines.append(
597
+ f"- **Deletions**: {metrics['total_deleted_chars']:,} characters ({del_pct})"
598
+ )
599
+
600
+ # Format insertions
601
+ ins_pct = format_percentage(metrics["insertion_percentage"])
602
+ lines.append(
603
+ f"- **Insertions**: {metrics['total_inserted_chars']:,} characters ({ins_pct})"
604
+ )
605
+
606
+ # Format net change
607
+ net_pct = metrics["net_change_percentage"]
608
+ if abs(net_pct) < 0.01:
609
+ net_pct_str = f"{net_pct:+.3f}%"
610
+ else:
611
+ net_pct_str = f"{net_pct:+.1f}%"
612
+
613
+ lines.append(
614
+ f"- **Net change**: {net_pct_str} "
615
+ f"({'increase' if metrics['net_change_percentage'] > 0 else 'decrease' if metrics['net_change_percentage'] < 0 else 'no change'})"
616
+ )
617
+
618
+ # Character classifications
619
+ if classifications:
620
+ lines.append("\n### Character Classifications")
621
+
622
+ # Show changes by character class
623
+ for op in [Operation.DELETE, Operation.INSERT]:
624
+ if op in classifications and classifications[op]:
625
+ lines.append(f"\n**{operation_to_past(op).title()} Characters:**")
626
+ for char_class, count in sorted(
627
+ classifications[op].items(), key=lambda x: -x[1]
628
+ ):
629
+ lines.append(
630
+ f"- {char_class.replace('_', ' ').title()}: {count}"
631
+ )
632
+
633
+ # Show percentage changes by character class
634
+ lines.append("\n### Change Percentages by Character Class")
635
+
636
+ # Sort by most changed (highest deletion or insertion percentage)
637
+ sorted_classes = sorted(
638
+ metrics["char_class_metrics"].items(),
639
+ key=lambda x: max(
640
+ x[1]["deletion_percentage"],
641
+ 0
642
+ if x[1]["insertion_percentage"] == float("inf")
643
+ else x[1]["insertion_percentage"],
644
+ ),
645
+ reverse=True,
646
+ )
647
+
648
+ for char_class, class_metrics in sorted_classes:
649
+ if (
650
+ class_metrics["deleted_count"] > 0
651
+ or class_metrics["inserted_count"] > 0
652
+ ):
653
+ class_name = char_class.replace("_", " ").title()
654
+
655
+ # Format the line
656
+ line_parts = [f"- **{class_name}**:"]
657
+
658
+ if class_metrics["original_count"] > 0:
659
+ line_parts.append(
660
+ f"Original: {class_metrics['original_count']}"
661
+ )
662
+
663
+ if class_metrics["deleted_count"] > 0:
664
+ line_parts.append(
665
+ f"Deleted: {class_metrics['deleted_count']} "
666
+ f"({class_metrics['deletion_percentage']:.1f}%)"
667
+ )
668
+
669
+ if class_metrics["inserted_count"] > 0:
670
+ if class_metrics["insertion_percentage"] == float("inf"):
671
+ line_parts.append(
672
+ f"Inserted: {class_metrics['inserted_count']} (new)"
673
+ )
674
+ else:
675
+ line_parts.append(
676
+ f"Inserted: {class_metrics['inserted_count']} "
677
+ f"({class_metrics['insertion_percentage']:.1f}%)"
678
+ )
679
+
680
+ lines.append(" | ".join(line_parts))
681
+
682
+ return "\n".join(lines)
683
+
684
+ def format_percentage(value: float, min_decimals: int = 1) -> str:
685
+ """Format percentage with adaptive decimal places."""
686
+ if value == 0:
687
+ return "0%"
688
+ elif value < 0.01:
689
+ return f"{value:.3f}%" # Show 3 decimals for very small values
690
+ elif value < 0.1:
691
+ return f"{value:.2f}%" # Show 2 decimals for small values
692
+ elif value < 1:
693
+ return f"{value:.1f}%" # Show 1 decimal for values < 1%
694
+ else:
695
+ return f"{value:.0f}%" # No decimals for values >= 1%
696
+
697
+ def classify_edits_with_chars(
698
+ edits: List[Edit],
699
+ ) -> Dict[Operation, Dict[str, Dict[str, int]]]:
700
+ """
701
+ Classify edit operations by character class and track character frequencies.
702
+ Returns: {operation: {char_class: {char: count}}}
703
+ """
704
+ from collections import defaultdict, Counter
705
+
706
+ # Filter out EQUAL operations
707
+ change_edits = [e for e in edits if e.operation != Operation.EQUAL]
708
+
709
+ # Track characters by operation and classification
710
+ result = defaultdict(lambda: defaultdict(Counter))
711
+
712
+ for edit in change_edits:
713
+ text = (
714
+ edit.old_text if edit.operation == Operation.DELETE else edit.new_text
715
+ )
716
+
717
+ for char in text:
718
+ char_class = classify_char(char)
719
+ result[edit.operation][char_class][char] += 1
720
+
721
+ return dict(result)
722
+
723
+ def get_top_chars(char_counter: Dict[str, int], n: int = 5) -> str:
724
+ """Get top n characters by frequency, formatted for display."""
725
+ if not char_counter:
726
+ return "-"
727
+
728
+ # Sort by frequency and take top n
729
+ top_chars = sorted(char_counter.items(), key=lambda x: -x[1])[:n]
730
+
731
+ # Format characters for display
732
+ formatted_chars = []
733
+ for char, _ in top_chars:
734
+ if char == " ":
735
+ formatted_chars.append("·") # Middle dot for space
736
+ elif char == "\n":
737
+ formatted_chars.append("¶") # Pilcrow for newline
738
+ elif char == "\t":
739
+ formatted_chars.append("→") # Arrow for tab
740
+ elif ord(char) < 32 or ord(char) == 127:
741
+ formatted_chars.append(f"\\x{ord(char):02x}") # Hex for control chars
742
+ else:
743
+ formatted_chars.append(char)
744
+
745
+ return " ".join(formatted_chars)
746
+
747
+ def create_summary_tables(
748
+ edits: List[Edit],
749
+ classifications: Dict[Operation, Dict[str, int]],
750
+ metrics: Dict[str, Any],
751
+ ) -> Dict[str, GT]:
752
+ """Create great_tables tables for the diff summary."""
753
+
754
+ # Get detailed character data
755
+ detailed_classifications = classify_edits_with_chars(edits)
756
+
757
+ # Table 1: Overall Statistics (unchanged)
758
+ overall_data = pd.DataFrame(
759
+ {
760
+ "Metric": [
761
+ "Original Length",
762
+ "Characters Deleted",
763
+ "Characters Inserted",
764
+ "Net Change",
765
+ ],
766
+ "Count": [
767
+ metrics["total_original_chars"],
768
+ metrics["total_deleted_chars"],
769
+ metrics["total_inserted_chars"],
770
+ metrics["total_inserted_chars"] - metrics["total_deleted_chars"],
771
+ ],
772
+ "Percentage": [
773
+ "-",
774
+ format_percentage(metrics["deletion_percentage"]),
775
+ format_percentage(metrics["insertion_percentage"]),
776
+ f"{metrics['net_change_percentage']:+.3f}%"
777
+ if abs(metrics["net_change_percentage"]) < 0.01
778
+ else f"{metrics['net_change_percentage']:+.1f}%",
779
+ ],
780
+ }
781
+ )
782
+
783
+ overall_table = (
784
+ GT(overall_data)
785
+ .tab_header(
786
+ title="Text Change Summary",
787
+ subtitle=f"Total edits: {len([e for e in edits if e.operation != Operation.EQUAL])}",
788
+ )
789
+ .fmt_number(columns="Count", decimals=0, use_seps=True)
790
+ .tab_style(
791
+ style=[style.fill(color="#f0f0f0"), style.text(weight="bold")],
792
+ locations=loc.body(rows=[3]),
793
+ )
794
+ .cols_align(align="center", columns=["Count", "Percentage"])
795
+ .opt_stylize(style=1, color="blue")
796
+ )
797
+
798
+ # Table 2: Character Class Changes with top characters
799
+ char_class_data = []
800
+
801
+ # Get all character classes
802
+ all_classes = set()
803
+ for op_classes in classifications.values():
804
+ all_classes.update(op_classes.keys())
805
+ all_classes.update(metrics["char_class_metrics"].keys())
806
+
807
+ # Build rows
808
+ for char_class in sorted(all_classes):
809
+ class_metrics = metrics["char_class_metrics"].get(char_class, {})
810
+
811
+ # Get top characters for this class
812
+ del_chars = detailed_classifications.get(Operation.DELETE, {}).get(
813
+ char_class, {}
814
+ )
815
+ ins_chars = detailed_classifications.get(Operation.INSERT, {}).get(
816
+ char_class, {}
817
+ )
818
+
819
+ row = {
820
+ "Character Class": char_class.replace("_", " ").title(),
821
+ "Original": class_metrics.get("original_count", 0),
822
+ "Deleted": class_metrics.get("deleted_count", 0),
823
+ "Top Deleted": get_top_chars(del_chars, 5),
824
+ "Inserted": class_metrics.get("inserted_count", 0),
825
+ "Top Inserted": get_top_chars(ins_chars, 5),
826
+ "Del %": format_percentage(class_metrics.get("deletion_percentage", 0))
827
+ if class_metrics.get("deletion_percentage", 0) > 0
828
+ else "-",
829
+ "Ins %": (
830
+ "new"
831
+ if class_metrics.get("insertion_percentage", 0) == float("inf")
832
+ else format_percentage(class_metrics.get("insertion_percentage", 0))
833
+ if class_metrics.get("insertion_percentage", 0) > 0
834
+ else "-"
835
+ ),
836
+ }
837
+
838
+ # Only include rows with changes
839
+ if row["Deleted"] > 0 or row["Inserted"] > 0:
840
+ char_class_data.append(row)
841
+
842
+ if char_class_data:
843
+ char_class_df = pd.DataFrame(char_class_data)
844
+ char_class_table = (
845
+ GT(char_class_df)
846
+ .tab_header(title="Changes by Character Classification")
847
+ .fmt_number(
848
+ columns=["Original", "Deleted", "Inserted"],
849
+ decimals=0,
850
+ use_seps=True,
851
+ )
852
+ .tab_style(
853
+ style=style.fill(color="#ffcccc"),
854
+ locations=loc.body(columns=["Deleted", "Top Deleted"]),
855
+ )
856
+ .tab_style(
857
+ style=style.fill(color="#ccffcc"),
858
+ locations=loc.body(columns=["Inserted", "Top Inserted"]),
859
+ )
860
+ .tab_style(
861
+ style=style.text(font="monospace"),
862
+ locations=loc.body(columns=["Top Deleted", "Top Inserted"]),
863
+ )
864
+ .cols_align(
865
+ align="center",
866
+ columns=["Original", "Deleted", "Inserted", "Del %", "Ins %"],
867
+ )
868
+ .cols_align(align="left", columns=["Top Deleted", "Top Inserted"])
869
+ .tab_spanner(
870
+ label="Counts", columns=["Original", "Deleted", "Inserted"]
871
+ )
872
+ .tab_spanner(
873
+ label="Characters", columns=["Top Deleted", "Top Inserted"]
874
+ )
875
+ .tab_spanner(label="Percentages", columns=["Del %", "Ins %"])
876
+ .cols_width(
877
+ {
878
+ "Character Class": "20%",
879
+ "Original": "10%",
880
+ "Deleted": "10%",
881
+ "Top Deleted": "15%",
882
+ "Inserted": "10%",
883
+ "Top Inserted": "15%",
884
+ "Del %": "10%",
885
+ "Ins %": "10%",
886
+ }
887
+ )
888
+ .opt_stylize(style=1, color="blue")
889
+ )
890
+ else:
891
+ char_class_table = None
892
+
893
+ # Table 3: Compact Combined View (unchanged except for percentage formatting)
894
+ compact_data = []
895
+
896
+ # Add summary row
897
+ compact_data.append(
898
+ {
899
+ "Type": "Total",
900
+ "Deleted": metrics["total_deleted_chars"],
901
+ "Inserted": metrics["total_inserted_chars"],
902
+ "Net": metrics["total_inserted_chars"] - metrics["total_deleted_chars"],
903
+ "Change": f"{metrics['net_change_percentage']:+.3f}%"
904
+ if abs(metrics["net_change_percentage"]) < 0.01
905
+ else f"{metrics['net_change_percentage']:+.0f}%",
906
+ }
907
+ )
908
+
909
+ # Add top character classes (sorted by total change)
910
+ class_changes = []
911
+ for char_class, class_metrics in metrics["char_class_metrics"].items():
912
+ if (
913
+ class_metrics["deleted_count"] > 0
914
+ or class_metrics["inserted_count"] > 0
915
+ ):
916
+ class_changes.append(
917
+ {
918
+ "Type": char_class.replace("_", " ").title(),
919
+ "Deleted": class_metrics["deleted_count"],
920
+ "Inserted": class_metrics["inserted_count"],
921
+ "Net": class_metrics["inserted_count"]
922
+ - class_metrics["deleted_count"],
923
+ "Change": class_metrics["deleted_count"]
924
+ + class_metrics["inserted_count"],
925
+ }
926
+ )
927
+
928
+ # Sort by total change and take top 5
929
+ class_changes.sort(key=lambda x: x["Change"], reverse=True)
930
+ for item in class_changes[:5]:
931
+ item["Change"] = f"{item['Net']:+d}" if item["Net"] != 0 else "±0"
932
+ compact_data.append(item)
933
+
934
+ compact_df = pd.DataFrame(compact_data)
935
+ compact_table = (
936
+ GT(compact_df)
937
+ .tab_header(title="Edit Summary - Compact View")
938
+ .fmt_number(
939
+ columns=["Deleted", "Inserted", "Net"], decimals=0, use_seps=True
940
+ )
941
+ .tab_style(
942
+ style=[
943
+ style.fill(color="#e8e8e8"),
944
+ style.text(weight="bold"),
945
+ style.borders(sides=["top", "bottom"], color="#666", weight="2px"),
946
+ ],
947
+ locations=loc.body(rows=[0]),
948
+ )
949
+ .tab_style(
950
+ style=style.text(color="#880000"),
951
+ locations=loc.body(columns=["Deleted"]),
952
+ )
953
+ .tab_style(
954
+ style=style.text(color="#008800"),
955
+ locations=loc.body(columns=["Inserted"]),
956
+ )
957
+ .cols_align(
958
+ align="center", columns=["Deleted", "Inserted", "Net", "Change"]
959
+ )
960
+ .cols_width(
961
+ {
962
+ "Type": "40%",
963
+ "Deleted": "15%",
964
+ "Inserted": "15%",
965
+ "Net": "15%",
966
+ "Change": "15%",
967
+ }
968
+ )
969
+ .opt_stylize(style=1, color="cyan")
970
+ )
971
+
972
+ return {
973
+ "overall": overall_table,
974
+ "char_class": char_class_table,
975
+ "compact": compact_table,
976
+ }
977
+
978
+ def create_operation_matrix_table(
979
+ edits: List[Edit], classifications: Dict[Operation, Dict[str, int]]
980
+ ) -> GT:
981
+ """Create a matrix view of operations by character class."""
982
+
983
+ # Get all character classes
984
+ all_classes = set()
985
+ for op_classes in classifications.values():
986
+ all_classes.update(op_classes.keys())
987
+
988
+ # Build matrix data
989
+ matrix_data = []
990
+ for char_class in sorted(all_classes):
991
+ row = {
992
+ "Character Type": char_class.replace("_", " ").title(),
993
+ "Deletions": classifications.get(Operation.DELETE, {}).get(
994
+ char_class, 0
995
+ ),
996
+ "Insertions": classifications.get(Operation.INSERT, {}).get(
997
+ char_class, 0
998
+ ),
999
+ "Balance": (
1000
+ classifications.get(Operation.INSERT, {}).get(char_class, 0)
1001
+ - classifications.get(Operation.DELETE, {}).get(char_class, 0)
1002
+ ),
1003
+ }
1004
+ matrix_data.append(row)
1005
+
1006
+ # Sort by total changes
1007
+ matrix_data.sort(key=lambda x: x["Deletions"] + x["Insertions"], reverse=True)
1008
+
1009
+ # Convert to DataFrame
1010
+ matrix_df = pd.DataFrame(matrix_data)
1011
+
1012
+ # Calculate max values for domains
1013
+ max_del = max((r["Deletions"] for r in matrix_data), default=1)
1014
+ max_ins = max((r["Insertions"] for r in matrix_data), default=1)
1015
+ max_balance = max((abs(r["Balance"]) for r in matrix_data), default=1)
1016
+
1017
+ matrix_table = (
1018
+ GT(matrix_df)
1019
+ .tab_header(title="Operation Matrix by Character Type")
1020
+ .fmt_number(columns=["Deletions", "Insertions", "Balance"], decimals=0)
1021
+ .data_color(
1022
+ columns=["Deletions"],
1023
+ palette=["white", "#ffcccc"],
1024
+ domain=[0, max_del],
1025
+ )
1026
+ .data_color(
1027
+ columns=["Insertions"],
1028
+ palette=["white", "#ccffcc"],
1029
+ domain=[0, max_ins],
1030
+ )
1031
+ .data_color(
1032
+ columns=["Balance"],
1033
+ palette=["#ffcccc", "white", "#ccffcc"],
1034
+ domain=[-max_balance, max_balance],
1035
+ )
1036
+ .cols_align(align="center", columns=["Deletions", "Insertions", "Balance"])
1037
+ .opt_stylize(style=2, color="gray")
1038
+ )
1039
+
1040
+ return matrix_table
1041
+
1042
+ def is_long_diff(edits: List[Edit], original: str) -> bool:
1043
+ """Determine if a diff should use fast rendering."""
1044
+ return len(edits) > 1000 or len(original) > 10000
1045
+
1046
+ def analyze_text_changes(
1047
+ original: str,
1048
+ processed: str,
1049
+ ) -> Dict[str, Any]:
1050
+ """
1051
+ Main function to analyze changes between two texts.
1052
+ """
1053
+ edits = myers_diff(original, processed)
1054
+ classifications = classify_edits(edits)
1055
+ metrics = calculate_change_metrics(original, edits, classifications)
1056
+ summary = format_diff_summary(edits, classifications, metrics)
1057
+
1058
+ result = {
1059
+ "edits": edits,
1060
+ "classifications": classifications,
1061
+ "metrics": metrics,
1062
+ "summary": summary,
1063
+ "tables": create_summary_tables(edits, classifications, metrics),
1064
+ "matrix_table": create_operation_matrix_table(edits, classifications),
1065
+ }
1066
+
1067
+ return result
1068
+
1069
+ def render_html_diff(
1070
+ edits: List[Edit],
1071
+ original: str,
1072
+ context_chars: int = 5,
1073
+ side_by_side: bool = False,
1074
+ use_fast_html: bool | None = None,
1075
+ ) -> str:
1076
+ """
1077
+ Unified function to render HTML diffs with automatic optimization.
1078
+
1079
+ Args:
1080
+ edits: List of Edit operations
1081
+ original: Original text (for length checking)
1082
+ context_chars: Number of context lines to show in fast mode
1083
+ side_by_side: Whether to use side-by-side view
1084
+ use_fast_html: Force fast mode (None for auto-detect)
1085
+
1086
+ Returns:
1087
+ HTML string of the diff
1088
+ """
1089
+ if use_fast_html is None:
1090
+ use_fast_html = is_long_diff(edits, original)
1091
+
1092
+ if use_fast_html:
1093
+ if side_by_side:
1094
+ return generate_side_by_side_html_fast(
1095
+ edits, context_chars=context_chars
1096
+ )
1097
+ else:
1098
+ return generate_html_diff_fast(edits, context_chars=context_chars)
1099
+ else:
1100
+ if side_by_side:
1101
+ # For non-fast mode, still use length-based optimization
1102
+ if len(edits) > 500:
1103
+ return generate_side_by_side_html_fast(edits, max_length=50000)
1104
+ else:
1105
+ return generate_side_by_side_html(edits)
1106
+ else:
1107
+ return generate_html_diff(edits, show_equal=True, max_equal_length=200)
1108
+
1109
+ return analyze_text_changes, render_html_diff
1110
+
1111
+
1112
+ @app.cell
1113
+ def _(mo):
1114
+ o_file_upload = mo.ui.file(label="Original text", kind="area")
1115
+ p_file_upload = mo.ui.file(label="Preprocessed text", kind="area")
1116
+
1117
+ file_stack = mo.hstack([o_file_upload, p_file_upload], widths="equal")
1118
+ return file_stack, o_file_upload, p_file_upload
1119
+
1120
+
1121
+ @app.cell
1122
+ def _(mo):
1123
+ o_textbox = mo.ui.text_area(label="Original text", full_width=True)
1124
+ p_textbox = mo.ui.text_area(label="Preprocessed text", full_width=True)
1125
+
1126
+ text_stack = mo.hstack([o_textbox, p_textbox], widths="equal")
1127
+ return o_textbox, p_textbox, text_stack
1128
+
1129
+
1130
+ @app.cell
1131
+ def _(file_stack, mo, text_stack):
1132
+ mo.ui.tabs({"Text": text_stack, "File": file_stack})
1133
+ return
1134
+
1135
+
1136
+ @app.function
1137
+ def check_text_similarity(text1: str, text2: str, threshold: float = 0.6) -> bool:
1138
+ """Check if texts are similar enough based on length and character overlap."""
1139
+ if not text1 or not text2:
1140
+ return False
1141
+ return len(set(text1) & set(text2)) / len(
1142
+ set(text1) | set(text2)
1143
+ ) >= threshold and abs(len(text1) - len(text2)) / max(len(text1), len(text2)) <= (
1144
+ 1 - threshold
1145
+ )
1146
+
1147
+
1148
+ @app.cell
1149
+ def _(mo, o_file_upload, o_textbox, p_file_upload, p_textbox):
1150
+ from charset_normalizer import detect
1151
+
1152
+ def detect_encoding(b: bytes) -> str:
1153
+ result = detect(b)
1154
+ return result["encoding"]
1155
+
1156
+ o_text, p_text = (
1157
+ "Example text will be used if none provided!",
1158
+ "Example Text will be used, if none provided.",
1159
+ )
1160
+ try:
1161
+ if o_file_upload.contents():
1162
+ encoding = detect_encoding(o_file_upload.contents())
1163
+ try:
1164
+ o_text = o_file_upload.contents().decode(encoding)
1165
+ except UnicodeDecodeError:
1166
+ o_text = o_file_upload.contents().decode("utf-8")
1167
+ elif o_textbox.value:
1168
+ o_text = o_textbox.value
1169
+
1170
+ if p_file_upload.contents():
1171
+ encoding = detect_encoding(o_file_upload.contents())
1172
+ try:
1173
+ p_text = p_file_upload.contents().decode(encoding)
1174
+ except UnicodeDecodeError:
1175
+ p_text = p_file_upload.contents().decode("utf-8")
1176
+ elif p_textbox.value:
1177
+ p_text = p_textbox.value
1178
+ except UnicodeDecodeError:
1179
+ mo.stop(
1180
+ True,
1181
+ mo.md("Error decoding files. Please try UTF-8.").callout(kind="danger"),
1182
+ )
1183
+
1184
+ mo.stop(
1185
+ not check_text_similarity(o_text, p_text),
1186
+ mo.md(
1187
+ f"Texts are too dissimilar! Aborting comparison.\n\n{o_text[:50]}\n\n{p_text[:50]}"
1188
+ ).callout(kind="danger"),
1189
+ )
1190
+ return o_text, p_text
1191
+
1192
+
1193
+ @app.cell
1194
+ def _(analyze_text_changes, o_text, p_text):
1195
+ results = analyze_text_changes(o_text, p_text)
1196
+ return (results,)
1197
+
1198
+
1199
+ @app.cell
1200
+ def _(mo, results):
1201
+ results_tables = mo.vstack(
1202
+ [
1203
+ results["tables"]["overall"],
1204
+ results["tables"]["char_class"],
1205
+ results["tables"]["compact"],
1206
+ ]
1207
+ )
1208
+ return (results_tables,)
1209
+
1210
+
1211
+ @app.cell
1212
+ def _(mo, o_text, render_html_diff, results, results_tables):
1213
+ diff_view = mo.ui.tabs(
1214
+ {
1215
+ "Combined diff": mo.Html(
1216
+ render_html_diff(
1217
+ results["edits"],
1218
+ o_text,
1219
+ )
1220
+ ),
1221
+ "Side-by-side diff": mo.Html(
1222
+ render_html_diff(
1223
+ results["edits"],
1224
+ o_text,
1225
+ side_by_side=True,
1226
+ )
1227
+ ),
1228
+ }
1229
+ )
1230
+
1231
+ mo.md(f"""
1232
+ # Results
1233
+
1234
+ {results_tables}
1235
+
1236
+ {diff_view}
1237
+ """)
1238
+ return
1239
+
1240
+
1241
+ @app.cell
1242
+ def _():
1243
+ return
1244
+
1245
+
1246
+ if __name__ == "__main__":
1247
+ app.run()
development.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Development
2
+
3
+ ## Testing your Dockerfile locally
4
+
5
+ ```bash
6
+ docker build -t marimo-app .
7
+ docker run -it --rm -p 7860:7860 marimo-app
8
+ ```
pyproject.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "llm-text-preprocessing"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "charset-normalizer>=3.4.2",
9
+ "great-tables>=0.17.0",
10
+ "marimo>=0.14.6",
11
+ "pandas>=2.3.0",
12
+ ]
uv.lock ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.9.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "sniffio" },
12
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
13
+ ]
14
+ sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
15
+ wheels = [
16
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "babel"
21
+ version = "2.17.0"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "charset-normalizer"
30
+ version = "3.4.2"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
35
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
36
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
37
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
38
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
39
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
40
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
41
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
42
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
43
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
44
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
45
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
46
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
47
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
48
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
49
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
50
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
51
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
52
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
53
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
54
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
55
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
56
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
57
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
58
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
59
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
60
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
61
+ ]
62
+
63
+ [[package]]
64
+ name = "click"
65
+ version = "8.2.1"
66
+ source = { registry = "https://pypi.org/simple" }
67
+ dependencies = [
68
+ { name = "colorama", marker = "sys_platform == 'win32'" },
69
+ ]
70
+ sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
71
+ wheels = [
72
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
73
+ ]
74
+
75
+ [[package]]
76
+ name = "colorama"
77
+ version = "0.4.6"
78
+ source = { registry = "https://pypi.org/simple" }
79
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
80
+ wheels = [
81
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
82
+ ]
83
+
84
+ [[package]]
85
+ name = "commonmark"
86
+ version = "0.9.1"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ sdist = { url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", size = 95764, upload-time = "2019-10-04T15:37:39.817Z" }
89
+ wheels = [
90
+ { url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068, upload-time = "2019-10-04T15:37:37.674Z" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "docutils"
95
+ version = "0.21.2"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
98
+ wheels = [
99
+ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
100
+ ]
101
+
102
+ [[package]]
103
+ name = "faicons"
104
+ version = "0.2.2"
105
+ source = { registry = "https://pypi.org/simple" }
106
+ dependencies = [
107
+ { name = "htmltools" },
108
+ ]
109
+ sdist = { url = "https://files.pythonhosted.org/packages/06/e8/c12ef85c4444616ab1c1e96d5ecbadc8046d40b34797308846a2cfc06c80/faicons-0.2.2.tar.gz", hash = "sha256:6b7d7b19180179b6b83783f91bf6c9311c0f00ae0f97d41be1d24d9942361659", size = 604434, upload-time = "2024-01-16T23:27:21.634Z" }
110
+ wheels = [
111
+ { url = "https://files.pythonhosted.org/packages/65/3c/1db1b0f878319bb227f35a0fca7cad64e1f528b518bcab1a708da305c86d/faicons-0.2.2-py3-none-any.whl", hash = "sha256:d45d7c2635b53582a3375c67d7e975a28a91d2c16ef5f6b5033b5cdd507224b6", size = 607225, upload-time = "2024-01-16T23:27:19.475Z" },
112
+ ]
113
+
114
+ [[package]]
115
+ name = "great-tables"
116
+ version = "0.17.0"
117
+ source = { registry = "https://pypi.org/simple" }
118
+ dependencies = [
119
+ { name = "babel" },
120
+ { name = "commonmark" },
121
+ { name = "faicons" },
122
+ { name = "htmltools" },
123
+ { name = "importlib-metadata" },
124
+ { name = "importlib-resources" },
125
+ { name = "numpy" },
126
+ { name = "typing-extensions" },
127
+ ]
128
+ sdist = { url = "https://files.pythonhosted.org/packages/62/6f/85b80bec676dfb04e167224215475c9c353f72e52d8905c6eb1d2c93ee6f/great_tables-0.17.0.tar.gz", hash = "sha256:1bece693e3e72bb194a7d0aad99ed7af0ec0486576fc00dea7013fa7753e6f17", size = 10921120, upload-time = "2025-03-11T15:34:32.284Z" }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/a7/fa/4889fc5115e3300bf192958642166f2b9309fab84d367339ddc76a683e43/great_tables-0.17.0-py3-none-any.whl", hash = "sha256:36597209cf7709772207e9b969a3ca62de1b147cc4f4cb69f2b90ec7b935fb3e", size = 1378698, upload-time = "2025-03-11T15:34:30.831Z" },
131
+ ]
132
+
133
+ [[package]]
134
+ name = "h11"
135
+ version = "0.16.0"
136
+ source = { registry = "https://pypi.org/simple" }
137
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
138
+ wheels = [
139
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
140
+ ]
141
+
142
+ [[package]]
143
+ name = "htmltools"
144
+ version = "0.6.0"
145
+ source = { registry = "https://pypi.org/simple" }
146
+ dependencies = [
147
+ { name = "packaging" },
148
+ { name = "typing-extensions" },
149
+ ]
150
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/c568d17e9fb5ad5aa0ca3531c58d36fe69deb8eee4e53ff6425c1f99f210/htmltools-0.6.0.tar.gz", hash = "sha256:e8a3fb023d748935035db7ff17f620612ffc814a6a80b6ae388f7b7ab182adf7", size = 97152, upload-time = "2024-10-29T20:21:43.378Z" }
151
+ wheels = [
152
+ { url = "https://files.pythonhosted.org/packages/0a/ba/aa99706246f1938ca905eb6eeb7db832ac2e157aa4b805acb5cd4cd1791a/htmltools-0.6.0-py3-none-any.whl", hash = "sha256:072a274ff5e2851e0acce13fc5bb2bbdbbad8268dc8b123f881c05012ce7dce0", size = 84954, upload-time = "2024-10-29T20:21:42.067Z" },
153
+ ]
154
+
155
+ [[package]]
156
+ name = "idna"
157
+ version = "3.10"
158
+ source = { registry = "https://pypi.org/simple" }
159
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
160
+ wheels = [
161
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
162
+ ]
163
+
164
+ [[package]]
165
+ name = "importlib-metadata"
166
+ version = "8.7.0"
167
+ source = { registry = "https://pypi.org/simple" }
168
+ dependencies = [
169
+ { name = "zipp" },
170
+ ]
171
+ sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
174
+ ]
175
+
176
+ [[package]]
177
+ name = "importlib-resources"
178
+ version = "6.5.2"
179
+ source = { registry = "https://pypi.org/simple" }
180
+ sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
181
+ wheels = [
182
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
183
+ ]
184
+
185
+ [[package]]
186
+ name = "itsdangerous"
187
+ version = "2.2.0"
188
+ source = { registry = "https://pypi.org/simple" }
189
+ sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
190
+ wheels = [
191
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
192
+ ]
193
+
194
+ [[package]]
195
+ name = "jedi"
196
+ version = "0.19.2"
197
+ source = { registry = "https://pypi.org/simple" }
198
+ dependencies = [
199
+ { name = "parso" },
200
+ ]
201
+ sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
202
+ wheels = [
203
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
204
+ ]
205
+
206
+ [[package]]
207
+ name = "llm-text-preprocessing"
208
+ version = "0.1.0"
209
+ source = { virtual = "." }
210
+ dependencies = [
211
+ { name = "charset-normalizer" },
212
+ { name = "great-tables" },
213
+ { name = "marimo" },
214
+ { name = "pandas" },
215
+ ]
216
+
217
+ [package.metadata]
218
+ requires-dist = [
219
+ { name = "charset-normalizer", specifier = ">=3.4.2" },
220
+ { name = "great-tables", specifier = ">=0.17.0" },
221
+ { name = "marimo", specifier = ">=0.14.6" },
222
+ { name = "pandas", specifier = ">=2.3.0" },
223
+ ]
224
+
225
+ [[package]]
226
+ name = "loro"
227
+ version = "1.5.1"
228
+ source = { registry = "https://pypi.org/simple" }
229
+ sdist = { url = "https://files.pythonhosted.org/packages/42/d8/b90d66fb97a57c311f9d40fa48c5f997bec28c36faf2b720ece5c244aae0/loro-1.5.1.tar.gz", hash = "sha256:8376a14b23a11f934fcda8a02548a449ff4f5da816769c78a442a89a23cd9736", size = 60681, upload-time = "2025-05-15T00:24:36.215Z" }
230
+ wheels = [
231
+ { url = "https://files.pythonhosted.org/packages/ef/36/04291632421f74c00f219fecf353000c0e722773c41d1e57731187b96be0/loro-1.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3976d7cafa3dfd9e75f110e4cc8b1de4dba2709dbd42b99270f7139433bfa57e", size = 2952871, upload-time = "2025-05-15T00:22:36.556Z" },
232
+ { url = "https://files.pythonhosted.org/packages/8c/70/faf6cfda83a9f3dba377261876dc649cbf6ad256c267d126125f8701cba8/loro-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:362c8388b4a3948d70bc6cf060b5149e716bd41ffc2fa028a77ecbd1dff2fa50", size = 2747990, upload-time = "2025-05-15T00:22:24.284Z" },
233
+ { url = "https://files.pythonhosted.org/packages/86/5c/4f59d23293149b423af7a71f5a6320de48f2bdda64ea73e280d3a4394274/loro-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97395b6c5844398a2cfb2906631fd49352482617006608f55d0dcefd794626ee", size = 2965889, upload-time = "2025-05-15T00:19:48.576Z" },
234
+ { url = "https://files.pythonhosted.org/packages/9b/67/b317fd181f7a08aa4f5fb810dc8d40d69c7acab10c7cd0711e66281b0fa8/loro-1.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11674be191a382e3d7fd8d2e2c8abcba70f30f0e1e65c7718ff57dacb972aa85", size = 3046859, upload-time = "2025-05-15T00:20:18.609Z" },
235
+ { url = "https://files.pythonhosted.org/packages/17/a4/e3b0ab4071255dd9bb1ae8586b911586b7771a107dd50d6d6717814edbbb/loro-1.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06c90cd3fbe10be068063828966cec19d5b2fa5897a103dc39f5162f31af1c3d", size = 3279261, upload-time = "2025-05-15T00:20:45.917Z" },
236
+ { url = "https://files.pythonhosted.org/packages/9d/ce/19b13ac2b59c5c35dd5fc8c10c494296b65ae2101aaa5eaa1a0e590c60ae/loro-1.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52665162bdabdf5bb835e94920995e4704722cab6569b63bef13867f5b29c3bd", size = 3800927, upload-time = "2025-05-15T00:21:13.528Z" },
237
+ { url = "https://files.pythonhosted.org/packages/c2/84/15f9ce7e478cedf7739c349707ed090e2d55d463d8be646067f3656605c3/loro-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f6f86d4ba56ab08616e111da658a8395a7ff8266cfa1a2355e73fec3f3e0ca", size = 3105034, upload-time = "2025-05-15T00:22:02.755Z" },
238
+ { url = "https://files.pythonhosted.org/packages/25/c3/9eadd2a6c88cafa828b63a6423586d9ed732b0e817c311a9affae1509744/loro-1.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d4846f47eecc467a5a819d8352a7f5a3926126cb0fa4f29bae4d2013b716c9d3", size = 3364247, upload-time = "2025-05-15T00:21:41.163Z" },
239
+ { url = "https://files.pythonhosted.org/packages/92/59/f312a5d6d865d526ae11a2126c1da473bd45cfdae57d5bb68c4a3db9cdf3/loro-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dff6483163967b1096aefa035ad58e9164869bf02d63a6c8feb085755ebccff6", size = 3119271, upload-time = "2025-05-15T00:22:48.845Z" },
240
+ { url = "https://files.pythonhosted.org/packages/a1/71/704a30f6c0b1a3da792e1ee5f6096ca6e389157afabcb26be7f5dd54e3a3/loro-1.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ce2feac62a2a2a996a198c06874597129a7d4fbb1ced2e752e7c36cb7ee38e67", size = 3312152, upload-time = "2025-05-15T00:23:14.456Z" },
241
+ { url = "https://files.pythonhosted.org/packages/ca/5a/f2686fde16f41d7a2005cd0ad26b8df84fe51b1152e31100c59eb0580d78/loro-1.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be1cac46a3473d6462f79a8630bded844e6b17698c0745b9c9ef072878fa3df6", size = 3367555, upload-time = "2025-05-15T00:23:44.239Z" },
242
+ { url = "https://files.pythonhosted.org/packages/3f/e8/54fd01f24cf973d702f105cf23e3bd8ea79d5b0f022ab8ac74670a7ff70b/loro-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ecf70c9520c64e51e6fec24685c46398537fd2656b25b93875049956a304ef40", size = 3271211, upload-time = "2025-05-15T00:24:12.235Z" },
243
+ { url = "https://files.pythonhosted.org/packages/03/e1/5f89b15040c8f5e2f1261639ee407ad39cc2e98a0760c703e0b2b00eec20/loro-1.5.1-cp312-cp312-win32.whl", hash = "sha256:853e12b72c3c69cf9facbae59a835369174649417d38ca221f5f523f2069c2ff", size = 2466741, upload-time = "2025-05-15T00:24:55.82Z" },
244
+ { url = "https://files.pythonhosted.org/packages/7d/b2/cfa253e46326a1f3477cafa3c14a6a408c54d226abcbfc832b447e6f49ff/loro-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:772bb6afc979e9bd43b19967d45e1e177051a9b89212efbc2492d36b48e2e625", size = 2630378, upload-time = "2025-05-15T00:24:40.093Z" },
245
+ { url = "https://files.pythonhosted.org/packages/8d/cf/113776aaf5d4da883fbab2154c68d839b43d29cc61189f54af1b7044f521/loro-1.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4e54819ce83d464afb1bfcd85174b1086f8bb723d8e90b189eac101780da8db3", size = 2952496, upload-time = "2025-05-15T00:22:38.134Z" },
246
+ { url = "https://files.pythonhosted.org/packages/89/5b/f96b8e3f207bd1049ac10b2dff3c7f034463c4a4069a9568bd41e67f9364/loro-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1af8251ff5f3ea7bb0408e3cff61f9d26316c88c79c4264f351930569924d9c8", size = 2747958, upload-time = "2025-05-15T00:22:25.55Z" },
247
+ { url = "https://files.pythonhosted.org/packages/19/77/3cb0e14bf751a7c9a281141d34686c6d2e6926b7a002e9023fed7925f903/loro-1.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9c4bed00006ae19cc468b8f13b2f9639203d2425411949d6e372841d0e7ac2", size = 2965619, upload-time = "2025-05-15T00:19:49.949Z" },
248
+ { url = "https://files.pythonhosted.org/packages/08/af/d5e26c146996ddb9b7360f27b2570e1910aa0e37c7e5bd4fd238ac38428e/loro-1.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af4a0fd903523d7be9bf248b5eb572cff21b98cfd08eb87a145a891ad77616db", size = 3046490, upload-time = "2025-05-15T00:20:20.12Z" },
249
+ { url = "https://files.pythonhosted.org/packages/42/33/a723c978be8fa0005e3ccb0a96824bd4fe4874e9d03a08c2fb24f5c03f13/loro-1.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b80fe509a566388e04813bfa99baff9a8026da8f3fcb639500ee21c795dbcefd", size = 3278208, upload-time = "2025-05-15T00:20:47.2Z" },
250
+ { url = "https://files.pythonhosted.org/packages/49/ce/f2669e5af13524fbb9c89aad536d11446a339574b0598adf0191bd640aba/loro-1.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dd4373dc6e5727b7666e44c6c5b1c705bb2a0dbedaaccf4a81580fc1910ba17", size = 3799882, upload-time = "2025-05-15T00:21:15.274Z" },
251
+ { url = "https://files.pythonhosted.org/packages/8a/e5/7dbb63a7b53adf44e8b447c5f40e0116501035f587bdaf8feb9fc49b0bc3/loro-1.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff9be94c9704a0a7fd25f2ae00e4e37c26d4127ee12a3fe52bcc03d1e4584b67", size = 3104741, upload-time = "2025-05-15T00:22:04.141Z" },
252
+ { url = "https://files.pythonhosted.org/packages/0a/48/fc11057467f84f84414b081de62e45d31c1029ed00254d1b90d1399a5233/loro-1.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b273de2c99f5a9cab143b1a25dc6c5e922e569497d126a4729ff8be88c7ccdfc", size = 3364304, upload-time = "2025-05-15T00:21:42.439Z" },
253
+ { url = "https://files.pythonhosted.org/packages/9e/af/0edf2aad989b3d11585bc47289e22e4f0bfd7961ac4dbb121f8d54854f4d/loro-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a93ca575941c88c44a36e5b70079cfb300a4c9233cb94a2da73f385fbd1b442a", size = 3119348, upload-time = "2025-05-15T00:22:50.176Z" },
254
+ { url = "https://files.pythonhosted.org/packages/b1/99/17870634a89beca680c952fc6e4cf1866da7e54729044502f4d2e58086b3/loro-1.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:122cebb72c9e83ffa94623a2b8017d4e7c49da9e04b56c6acd008e9af88707d3", size = 3311880, upload-time = "2025-05-15T00:23:16.326Z" },
255
+ { url = "https://files.pythonhosted.org/packages/87/4b/55ec796fa81c2db75b15f7a61e44ce1ab4319e0b93fd77f6bbb3bd681c52/loro-1.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:758587fc262475afad8792add3f9c4b855bc44bcc79b2eeadb718ff68600c810", size = 3366918, upload-time = "2025-05-15T00:23:46.914Z" },
256
+ { url = "https://files.pythonhosted.org/packages/c3/a0/5a690fd20822522841ed4e314f3a5a00e4cde2c4b9989e11c4d0ace31333/loro-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e6e38d4143fd2e3e1ec299f9c764aa3786328b08c4c467a4cd10dcc626b38f2", size = 3270241, upload-time = "2025-05-15T00:24:13.818Z" },
257
+ { url = "https://files.pythonhosted.org/packages/6b/42/5097c347e72e3e9a2f8d4cd2dede9928e4271c56dbe8b9701275461c3234/loro-1.5.1-cp313-cp313-win32.whl", hash = "sha256:d4730cd9489040176eabcc2d2d5d6333b9db280c1b8f563b51f34c242863c010", size = 2466351, upload-time = "2025-05-15T00:24:57.113Z" },
258
+ { url = "https://files.pythonhosted.org/packages/5f/ec/3c0fce5a87b4e840ee26108129670b9335cac4fdbfd1b7b53bc7f7bd3b6a/loro-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3381acb0132e856bd0000623d63718fda0168287cff726e57dfd8074991d2d5", size = 2628456, upload-time = "2025-05-15T00:24:41.656Z" },
259
+ { url = "https://files.pythonhosted.org/packages/a9/88/643122473ec5ca39b62fc7583cd5b0b1100056435314bc454699b35069e7/loro-1.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7462bfadd8e51268d60429ca44717714e5f1430ef2be79adc87e84a5206158a3", size = 2965004, upload-time = "2025-05-15T00:19:51.604Z" },
260
+ { url = "https://files.pythonhosted.org/packages/cc/1c/163d50dbbabdcca1772f77c089c72e2ada6318ec28aa8a06f3334a26d319/loro-1.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:33897717216c44137dac67e00c5be1a57631c722aa0cd7b0c19831562e6a74fa", size = 3043720, upload-time = "2025-05-15T00:20:21.676Z" },
261
+ { url = "https://files.pythonhosted.org/packages/41/79/37ff3af1795bf84eb418878595ef3163d494d2fcb8272fd575e3a614266e/loro-1.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7ecf076f5ffcf2a69d6cb14c77cb8035e4c2c687e7934b3d192fbba8f4f15e", size = 3275171, upload-time = "2025-05-15T00:20:48.965Z" },
262
+ { url = "https://files.pythonhosted.org/packages/11/b7/47a84f4041306c31211a2e4fd266820fcd7091ff3451e6c381411c4b763a/loro-1.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3d35bdb2cb315f339d146b55a2daba6d892bb91bbb46eea8dcff4e633c3d3c2", size = 3792486, upload-time = "2025-05-15T00:21:16.642Z" },
263
+ { url = "https://files.pythonhosted.org/packages/0e/14/97cbdcae7e079617b71702d0d47c51624fa6a573fc2b3cd4e242ffd6f743/loro-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5b47bb658e8fde2e65d36c8fb03da2afe02e7db60e81548a2ccf4c7adf161e2", size = 3118535, upload-time = "2025-05-15T00:22:51.433Z" },
264
+ { url = "https://files.pythonhosted.org/packages/4b/37/e17d4a9f6307db3d3aa05450ac88b0bf29980dcf59477f7a0a6c8683e4ba/loro-1.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d8c497be06dd54c9520830bd1e8bb9b68c4f0ba0f735485a9a1281cb78d82d29", size = 3307450, upload-time = "2025-05-15T00:23:17.666Z" },
265
+ { url = "https://files.pythonhosted.org/packages/bc/5f/4597b1b12d4ea378eba10683d2e157bdcd917482a92a7321877aa1236683/loro-1.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:5eb4fbe5bef38379372ddc1874f8aec8ef885274de800f770aa60988010ce588", size = 3369861, upload-time = "2025-05-15T00:23:48.24Z" },
266
+ { url = "https://files.pythonhosted.org/packages/62/42/4a75638ed05156a185a89b705c01a76fefa01d2ca6690366b092ad5e93d9/loro-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9489cdcfa887fabfc18e5aeb0e89098d5c908ab41ccf4cdc51f434effd741b10", size = 3265428, upload-time = "2025-05-15T00:24:15.488Z" },
267
+ ]
268
+
269
+ [[package]]
270
+ name = "marimo"
271
+ version = "0.14.6"
272
+ source = { registry = "https://pypi.org/simple" }
273
+ dependencies = [
274
+ { name = "click" },
275
+ { name = "docutils" },
276
+ { name = "itsdangerous" },
277
+ { name = "jedi" },
278
+ { name = "loro" },
279
+ { name = "markdown" },
280
+ { name = "narwhals" },
281
+ { name = "packaging" },
282
+ { name = "psutil" },
283
+ { name = "pygments" },
284
+ { name = "pymdown-extensions" },
285
+ { name = "pyyaml" },
286
+ { name = "starlette" },
287
+ { name = "tomlkit" },
288
+ { name = "uvicorn" },
289
+ { name = "websockets" },
290
+ ]
291
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/41/8b4a32f7d6c0c1be046011870a843c81d18b7ef2a2f8791eb8aa2829f8bd/marimo-0.14.6.tar.gz", hash = "sha256:e4484af13924cd2c60811fd59d643e686fc78f5bbdae615876efac5bd9df94f8", size = 29111510, upload-time = "2025-06-21T22:34:11.439Z" }
292
+ wheels = [
293
+ { url = "https://files.pythonhosted.org/packages/02/26/9a8b012fc9e2ccf6c26a72cbd2de45a2077b98d2910d60ad9d25e84c5916/marimo-0.14.6-py3-none-any.whl", hash = "sha256:e1061458fa1ff387e80b674f251cac729d07260036726e0693f915710e79b115", size = 29585644, upload-time = "2025-06-21T22:34:15.391Z" },
294
+ ]
295
+
296
+ [[package]]
297
+ name = "markdown"
298
+ version = "3.8.2"
299
+ source = { registry = "https://pypi.org/simple" }
300
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
301
+ wheels = [
302
+ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
303
+ ]
304
+
305
+ [[package]]
306
+ name = "narwhals"
307
+ version = "1.43.1"
308
+ source = { registry = "https://pypi.org/simple" }
309
+ sdist = { url = "https://files.pythonhosted.org/packages/61/82/9f351a79260a6456db3f53d248268b4c3791f1e3228eec3c745e8816afd6/narwhals-1.43.1.tar.gz", hash = "sha256:6ff56d600da67a0a0980b83bd5577d076772fdba96474076ba4e76c920dbc1e5", size = 496655, upload-time = "2025-06-19T09:37:56.398Z" }
310
+ wheels = [
311
+ { url = "https://files.pythonhosted.org/packages/8f/1e/b741d4eabbde95b1790e7df3c33c6b19f9b48db98a1416c6a6f06572bc66/narwhals-1.43.1-py3-none-any.whl", hash = "sha256:1ee508fa4dc0e05aa5b88717ba11613d8d9ccf0dd1e48513d4a3afb237dba9f2", size = 362737, upload-time = "2025-06-19T09:37:54.415Z" },
312
+ ]
313
+
314
+ [[package]]
315
+ name = "numpy"
316
+ version = "2.3.1"
317
+ source = { registry = "https://pypi.org/simple" }
318
+ sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" }
319
+ wheels = [
320
+ { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" },
321
+ { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" },
322
+ { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" },
323
+ { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" },
324
+ { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" },
325
+ { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" },
326
+ { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" },
327
+ { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" },
328
+ { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" },
329
+ { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" },
330
+ { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" },
331
+ { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" },
332
+ { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" },
333
+ { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" },
334
+ { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" },
335
+ { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" },
336
+ { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" },
337
+ { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" },
338
+ { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" },
339
+ { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" },
340
+ { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" },
341
+ { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" },
342
+ { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" },
343
+ { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" },
344
+ { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" },
345
+ { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" },
346
+ { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" },
347
+ { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" },
348
+ { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" },
349
+ { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" },
350
+ { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" },
351
+ { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" },
352
+ { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" },
353
+ ]
354
+
355
+ [[package]]
356
+ name = "packaging"
357
+ version = "25.0"
358
+ source = { registry = "https://pypi.org/simple" }
359
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
360
+ wheels = [
361
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
362
+ ]
363
+
364
+ [[package]]
365
+ name = "pandas"
366
+ version = "2.3.0"
367
+ source = { registry = "https://pypi.org/simple" }
368
+ dependencies = [
369
+ { name = "numpy" },
370
+ { name = "python-dateutil" },
371
+ { name = "pytz" },
372
+ { name = "tzdata" },
373
+ ]
374
+ sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" }
375
+ wheels = [
376
+ { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" },
377
+ { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" },
378
+ { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" },
379
+ { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" },
380
+ { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" },
381
+ { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" },
382
+ { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" },
383
+ { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" },
384
+ { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" },
385
+ { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" },
386
+ { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" },
387
+ { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" },
388
+ { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" },
389
+ { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" },
390
+ { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" },
391
+ { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" },
392
+ { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" },
393
+ { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" },
394
+ { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" },
395
+ { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" },
396
+ ]
397
+
398
+ [[package]]
399
+ name = "parso"
400
+ version = "0.8.4"
401
+ source = { registry = "https://pypi.org/simple" }
402
+ sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" }
403
+ wheels = [
404
+ { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" },
405
+ ]
406
+
407
+ [[package]]
408
+ name = "psutil"
409
+ version = "7.0.0"
410
+ source = { registry = "https://pypi.org/simple" }
411
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
412
+ wheels = [
413
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
414
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
415
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
416
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
417
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
418
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
419
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
420
+ ]
421
+
422
+ [[package]]
423
+ name = "pygments"
424
+ version = "2.19.2"
425
+ source = { registry = "https://pypi.org/simple" }
426
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
427
+ wheels = [
428
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
429
+ ]
430
+
431
+ [[package]]
432
+ name = "pymdown-extensions"
433
+ version = "10.16"
434
+ source = { registry = "https://pypi.org/simple" }
435
+ dependencies = [
436
+ { name = "markdown" },
437
+ { name = "pyyaml" },
438
+ ]
439
+ sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" }
440
+ wheels = [
441
+ { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" },
442
+ ]
443
+
444
+ [[package]]
445
+ name = "python-dateutil"
446
+ version = "2.9.0.post0"
447
+ source = { registry = "https://pypi.org/simple" }
448
+ dependencies = [
449
+ { name = "six" },
450
+ ]
451
+ sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
452
+ wheels = [
453
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
454
+ ]
455
+
456
+ [[package]]
457
+ name = "pytz"
458
+ version = "2025.2"
459
+ source = { registry = "https://pypi.org/simple" }
460
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
461
+ wheels = [
462
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
463
+ ]
464
+
465
+ [[package]]
466
+ name = "pyyaml"
467
+ version = "6.0.2"
468
+ source = { registry = "https://pypi.org/simple" }
469
+ sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
470
+ wheels = [
471
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
472
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
473
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
474
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
475
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
476
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
477
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
478
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
479
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
480
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
481
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
482
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
483
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
484
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
485
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
486
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
487
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
488
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
489
+ ]
490
+
491
+ [[package]]
492
+ name = "six"
493
+ version = "1.17.0"
494
+ source = { registry = "https://pypi.org/simple" }
495
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
496
+ wheels = [
497
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
498
+ ]
499
+
500
+ [[package]]
501
+ name = "sniffio"
502
+ version = "1.3.1"
503
+ source = { registry = "https://pypi.org/simple" }
504
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
505
+ wheels = [
506
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
507
+ ]
508
+
509
+ [[package]]
510
+ name = "starlette"
511
+ version = "0.47.1"
512
+ source = { registry = "https://pypi.org/simple" }
513
+ dependencies = [
514
+ { name = "anyio" },
515
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
516
+ ]
517
+ sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
518
+ wheels = [
519
+ { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
520
+ ]
521
+
522
+ [[package]]
523
+ name = "tomlkit"
524
+ version = "0.13.3"
525
+ source = { registry = "https://pypi.org/simple" }
526
+ sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
527
+ wheels = [
528
+ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
529
+ ]
530
+
531
+ [[package]]
532
+ name = "typing-extensions"
533
+ version = "4.14.0"
534
+ source = { registry = "https://pypi.org/simple" }
535
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
536
+ wheels = [
537
+ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
538
+ ]
539
+
540
+ [[package]]
541
+ name = "tzdata"
542
+ version = "2025.2"
543
+ source = { registry = "https://pypi.org/simple" }
544
+ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
545
+ wheels = [
546
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
547
+ ]
548
+
549
+ [[package]]
550
+ name = "uvicorn"
551
+ version = "0.34.3"
552
+ source = { registry = "https://pypi.org/simple" }
553
+ dependencies = [
554
+ { name = "click" },
555
+ { name = "h11" },
556
+ ]
557
+ sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
558
+ wheels = [
559
+ { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
560
+ ]
561
+
562
+ [[package]]
563
+ name = "websockets"
564
+ version = "15.0.1"
565
+ source = { registry = "https://pypi.org/simple" }
566
+ sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
567
+ wheels = [
568
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
569
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
570
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
571
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
572
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
573
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
574
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
575
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
576
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
577
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
578
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
579
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
580
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
581
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
582
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
583
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
584
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
585
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
586
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
587
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
588
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
589
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
590
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
591
+ ]
592
+
593
+ [[package]]
594
+ name = "zipp"
595
+ version = "3.23.0"
596
+ source = { registry = "https://pypi.org/simple" }
597
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
598
+ wheels = [
599
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
600
+ ]