From 8cc2bc9087b9f49203127970071baba06ab66986 Mon Sep 17 00:00:00 2001 From: Luigi Maiorano Date: Sun, 7 Dec 2025 21:37:42 +0100 Subject: [PATCH] taguette pre-process --- Stage1_Theme_Discovery.py | 226 ++++++++++++++++++++++++++++++++++++++ Taguette-Preprocess.py | 120 ++++++++++++++++++++ pyproject.toml | 1 + utils.py | 16 ++- uv.lock | 88 +++++++++++++++ 5 files changed, 445 insertions(+), 6 deletions(-) create mode 100644 Stage1_Theme_Discovery.py create mode 100644 Taguette-Preprocess.py diff --git a/Stage1_Theme_Discovery.py b/Stage1_Theme_Discovery.py new file mode 100644 index 0000000..34ef6ca --- /dev/null +++ b/Stage1_Theme_Discovery.py @@ -0,0 +1,226 @@ +import marimo + +__generated_with = "0.18.1" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import marimo as mo + import json + import pandas as pd + import re + from pathlib import Path + from utils import connect_qumo_ollama, load_srt + + # Configuration + VM_NAME = 'hiperf-gpu' + MODEL = 'llama3.3:70b' + TRANSCRIPT_DIR = Path("data/transcripts") + OUTPUT_FILE = Path("master_codebook.json") + + client = connect_qumo_ollama(VM_NAME) + return ( + MODEL, + OUTPUT_FILE, + TRANSCRIPT_DIR, + client, + json, + load_srt, + mo, + pd, + re, + ) + + +@app.cell +def _(mo): + mo.md(r""" + # Stage 1: Theme Discovery + + **Goal:** Identify recurring themes across a sample of interviews. + + 1. **Select Transcripts:** Choose 4-5 representative interviews. + 2. **Extract Topics:** The AI will analyze each transcript to find key topics. + 3. **Synthesize Themes:** Topics are grouped into a Master Codebook. + 4. **Refine & Save:** Edit the definitions and save the `master_codebook.json`. + """) + return + + +@app.cell +def _(TRANSCRIPT_DIR, mo): + # File Selection + srt_files = list(TRANSCRIPT_DIR.glob("*.srt")) + file_options = {f.name: str(f) for f in srt_files} + + file_selector = mo.ui.multiselect( + options=file_options, + label="Select Transcripts (Recommended: 4-5)", + full_width=True + ) + file_selector + return (file_selector,) + + +@app.cell +def _(file_selector, mo): + mo.md(f"**Selected:** {len(file_selector.value)} files") + return + + +@app.cell +def _(mo): + start_discovery_btn = mo.ui.run_button(label="Start Discovery Process") + start_discovery_btn + return (start_discovery_btn,) + + +@app.cell +def _( + MODEL, + client, + file_selector, + json, + load_srt, + mo, + re, + start_discovery_btn, +): + # Map Phase: Extract Topics per Transcript + extracted_topics = [] + status_callout = mo.md("") + + if start_discovery_btn.value and file_selector.value: + with mo.status.spinner("Analyzing transcripts...") as _spinner: + for filepath in file_selector.value: + _transcript = load_srt(filepath) + + # Truncate for discovery if too long (optional, but good for speed) + # Using first 15k chars usually gives enough context for high-level themes + _context = _transcript[:15000] + + _prompt = f""" + Analyze this interview transcript and list the top 5-7 key topics or themes discussed. + Focus on: Brand voice, Customer experience, Design systems, and AI. + + Return ONLY a JSON list of strings. Example: ["Inconsistent Tone", "Mobile Latency", "AI Trust"] + + Transcript: + {_context}... + """ + + try: + _response = client.generate(model=MODEL, prompt=_prompt) + # Find JSON list in response + _match = re.search(r'\[.*\]', _response.response, re.DOTALL) + if _match: + _topics = json.loads(_match.group(0)) + extracted_topics.extend(_topics) + except Exception as e: + print(f"Error processing {filepath}: {e}") + + status_callout = mo.callout( + f"✅ Extracted {len(extracted_topics)} raw topics from {len(file_selector.value)} files.", + kind="success" + ) + elif start_discovery_btn.value: + status_callout = mo.callout("Please select at least one file.", kind="warn") + + status_callout + return (extracted_topics,) + + +@app.cell +def _(MODEL, client, extracted_topics, json, mo, re, start_discovery_btn): + # Reduce Phase: Synthesize Themes + suggested_themes = [] + + if start_discovery_btn.value and extracted_topics: + with mo.status.spinner("Synthesizing Master Codebook...") as _spinner: + _topics_str = ", ".join(extracted_topics) + + _synthesis_prompt = f""" + You are a qualitative data architect. + + I have a list of raw topics extracted from multiple interviews: + [{_topics_str}] + + Task: + 1. Group these into 5-8 distinct, high-level Themes. + 2. Create a definition for each theme. + 3. Assign a hex color code to each. + 4. ALWAYS include a theme named "Other" for miscellaneous insights. + + Return a JSON object with this structure: + [ + {{"Theme": "Theme Name", "Definition": "Description...", "Color": "#HEXCODE"}}, + ... + ] + """ + + _response = client.generate(model=MODEL, prompt=_synthesis_prompt) + + _match = re.search(r'\[.*\]', _response.response, re.DOTALL) + if _match: + try: + suggested_themes = json.loads(_match.group(0)) + except: + suggested_themes = [{"Theme": "Error parsing JSON", "Definition": _response.response, "Color": "#000000"}] + + return (suggested_themes,) + + +@app.cell +def _(mo, pd, suggested_themes): + # Interactive Editor + + # Default empty structure if nothing generated yet + _initial_data = suggested_themes if suggested_themes else [ + {"Theme": "Example Theme", "Definition": "Description here...", "Color": "#CCCCCC"} + ] + + df_themes = pd.DataFrame(_initial_data) + + theme_editor = mo.ui.data_editor( + df_themes, + label="Master Codebook Editor", + column_config={ + "Color": mo.ui.column.color_picker(label="Color") + }, + num_rows="dynamic" # Allow adding/removing rows + ) + + mo.vstack([ + mo.md("### Review & Refine Codebook"), + mo.md("Edit the themes below. You can add rows, change colors, or refine definitions."), + theme_editor + ]) + return (theme_editor,) + + +@app.cell +def _(OUTPUT_FILE, json, mo, theme_editor): + save_btn = mo.ui.run_button(label="Save Master Codebook") + + save_message = mo.md("") + + if save_btn.value: + _final_df = theme_editor.value + # Convert to list of dicts + _codebook = _final_df.to_dict(orient="records") + + with open(OUTPUT_FILE, "w") as f: + json.dump(_codebook, f, indent=2) + + save_message = mo.callout(f"✅ Saved {len(_codebook)} themes to `{OUTPUT_FILE}`", kind="success") + + mo.vstack([ + save_btn, + save_message + ]) + return + + +if __name__ == "__main__": + app.run() diff --git a/Taguette-Preprocess.py b/Taguette-Preprocess.py new file mode 100644 index 0000000..bda2dff --- /dev/null +++ b/Taguette-Preprocess.py @@ -0,0 +1,120 @@ +import marimo + +__generated_with = "0.18.0" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import marimo as mo + import pandas as pd + from pathlib import Path + return Path, mo, pd + + +@app.cell +def _(Path): + INPUT_DIR = Path("data/transcripts/raw") + OUTPUT_DIR = Path("data/transcripts/clean") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + return INPUT_DIR, OUTPUT_DIR + + +@app.cell +def _(INPUT_DIR, mo): + csv_files = list(INPUT_DIR.glob("*.csv")) + file_options = {f.stem: str(f) for f in csv_files} + + file_dropdown = mo.ui.dropdown( + options=file_options, + label="Select CSV Transcript", + full_width=True + ) + file_dropdown + return (file_dropdown,) + + +@app.cell +def _(file_dropdown, mo, pd): + def csv_to_markdown(df): + """Convert transcript DataFrame to markdown, merging consecutive same-speaker turns.""" + lines = [f"# Interview Transcript\n"] + + # Track previous speaker to detect when speaker changes + prev_speaker = None + # Accumulate text from consecutive turns by same speaker + merged_text = [] + + for _, row in df.iterrows(): + speaker = row["Speaker"] + text = str(row["Transcript"]).strip() + + if speaker == prev_speaker: + # Same speaker continues — append text to current block + merged_text.append(text) + else: + # New speaker detected — flush previous speaker's block + if prev_speaker is not None: + # Format: **Speaker**: text-part-1\ntext-part-2 + blank line + lines.append(f"**{prev_speaker}**: {'\n'.join(merged_text)}\n\n") + + # Start new block for current speaker + prev_speaker = speaker + merged_text = [text] + + # Flush final speaker's block + if prev_speaker is not None: + lines.append(f"**{prev_speaker}**: {'\n'.join(merged_text)}\n\n") + + return "\n".join(lines) + + # Preview + preview = mo.md("") + if file_dropdown.value: + df = pd.read_csv(file_dropdown.value) + md_content = csv_to_markdown(df) + preview = mo.md(md_content) + + preview + return (csv_to_markdown,) + + +@app.cell +def _(mo): + convert_btn = mo.ui.run_button(label="Convert to Markdown") + convert_btn + return (convert_btn,) + + +@app.cell +def _(OUTPUT_DIR, Path, convert_btn, csv_to_markdown, file_dropdown, mo, pd): + result = mo.md("") + + if convert_btn.value and file_dropdown.value: + _df = pd.read_csv(file_dropdown.value) + _md = csv_to_markdown(_df) + _out_path = OUTPUT_DIR / (Path(file_dropdown.value).stem + ".md") + _out_path.write_text(_md) + result = mo.callout(f"✅ Saved to `{_out_path}`", kind="success") + + result + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Taguette + + Upload and process using taguette: http://taguette.tail44fa00.ts.net/ + """) + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() diff --git a/pyproject.toml b/pyproject.toml index 636ec25..b354067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "marimo>=0.18.0", "numpy>=2.3.5", "ollama>=0.6.1", + "pandas>=2.3.3", "pyzmq>=27.1.0", "requests>=2.32.5", ] diff --git a/utils.py b/utils.py index 789c269..830a9ec 100644 --- a/utils.py +++ b/utils.py @@ -76,11 +76,15 @@ def connect_qumo_ollama(vm_name: str ='ollama-lite') -> Client: client = Client( host=QUMO_OLLAMA_URL ) + + print(f"Connection succesful. WebUI available at: http://{vm_name}.tail44fa00.ts.net:3000\nAvailable models:") + for m in client.list().models: + print(f" - '{m.model}' ") + return client + except requests.ConnectionError: - print(f"Failed to reach {QUMO_OLLAMA_URL}. Check that the VM is running and Tailscale is up") - - print(f"Connection succesful. WebUI available at: http://{vm_name}.tail44fa00.ts.net:3000\nAvailable models:") - for m in client.list().models: - print(f" - '{m.model}' ") - return client + pass + + print(f"Failed to reach {QUMO_OLLAMA_URL}. Check that the VM is running and Tailscale is up") + return None diff --git a/uv.lock b/uv.lock index aa65c07..67e0611 100644 --- a/uv.lock +++ b/uv.lock @@ -232,6 +232,7 @@ dependencies = [ { name = "marimo" }, { name = "numpy" }, { name = "ollama" }, + { name = "pandas" }, { name = "pyzmq" }, { name = "requests" }, ] @@ -241,6 +242,7 @@ requires-dist = [ { name = "marimo", specifier = ">=0.18.0" }, { name = "numpy", specifier = ">=2.3.5" }, { name = "ollama", specifier = ">=0.6.1" }, + { name = "pandas", specifier = ">=2.3.3" }, { name = "pyzmq", specifier = ">=27.1.0" }, { name = "requests", specifier = ">=2.32.5" }, ] @@ -483,6 +485,53 @@ wheels = [ { 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" }, ] +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + [[package]] name = "parso" version = "0.8.5" @@ -635,6 +684,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/40/b2d7b9fdccc63e48ae4dbd363b6b89eb7ac346ea49ed667bb71f92af3021/pymdown_extensions-10.17.1-py3-none-any.whl", hash = "sha256:1f160209c82eecbb5d8a0d8f89a4d9bd6bdcbde9a8537761844cfc57ad5cd8a6", size = 266310, upload-time = "2025-11-11T21:44:56.809Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -739,6 +809,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -791,6 +870,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + [[package]] name = "urllib3" version = "2.5.0"