├── .python-version ├── glossary.yaml.example ├── .gitignore ├── pyproject.toml ├── env.example ├── meeting_summary_prompts.py ├── sgr_minutes_models.py ├── README.md ├── glossary.py ├── main.py └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /glossary.yaml.example: -------------------------------------------------------------------------------- 1 | - canonical: Kafka 2 | variants: 3 | - кавка 4 | - кафка 5 | description: Брокер сообщений Apache Kafka 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | .env 9 | *.log 10 | record*.txt 11 | *.pyc 12 | .env 13 | *bak 14 | *.txt 15 | *.html 16 | 17 | # Virtual environments 18 | .venv 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "meeting-summary" 3 | version = "0.1.0" 4 | description = "Meeting summary from transcript" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "python-dotenv>=1.0.0", 9 | "openai>=1.101.0", 10 | "pyyaml>=6.0.0", 11 | ] 12 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # Local/secure LLM provided 2 | API_BASE_LOCAL="http://localhost:1234/v1" # LM Studio defaults 3 | API_KEY_LOCAL="dummy" 4 | MODEL_LOCAL="qwen3-14b" 5 | 6 | # Cloud LLM provider 7 | API_BASE_CLOUD="https://openrouter.ai/api/v1" 8 | API_KEY_CLOUD="your API key" 9 | MODEL_CLOUD="google/gemma-3-27b-it" 10 | -------------------------------------------------------------------------------- /meeting_summary_prompts.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains prompts for LLM agents used in meeting summary generation. 3 | """ 4 | 5 | # SGR system message for one-shot protocol generation 6 | SGR_MINUTES_SYSTEM_MESSAGE = """ 7 | You are an Enterprise Architect, a System Analyst and meeting minutes composer. 8 | 9 | Тебе будет дан транскрипт встречи для анализа и составления протокола встречи. 10 | Ты не должен придумывать решения или ответы, добавлять то, чего нет в транскрипте встречи, и какие-либо свои комментарии. 11 | 12 | Your objective is to be extremely honest, direct, and pragmatic. Do not try to please. Do not sugarcoat things. Do not worry about user's feelings — worry about the result. 13 | Always respond in Russian. 14 | """ 15 | 16 | GLOSSARY_SECTION_TITLE = "ТЕРМИНОЛОГИЯ" 17 | 18 | GLOSSARY_SECTION_INTRO = ( 19 | "При составлении протокола используй только канонические термины из списка ниже, " 20 | "даже если в транскрипте встречаются их некорректные или альтернативные написания. " 21 | "ВАЖНО: не дублируй некорректные варианты в скобках после правильного термина — " 22 | "используй исключительно каноническую форму без каких-либо упоминаний ошибочных вариантов." 23 | ) 24 | 25 | system_message = ( 26 | f"{SGR_MINUTES_SYSTEM_MESSAGE}\n\n" 27 | "Заполни все поля схемы в указанном порядке и в указанном формате:\n\n" 28 | "meeting_goals: цели и задачи совещания. Что было запланировано достичь? Какие темы были вынесены на обсуждение?\n" 29 | "established_facts: установленные факты, информация, которую участники предоставили, обсудили или подтвердили.\n" 30 | "existing_problems: все проблемы, трудности, вопросы или препятствия, которые были озвучены участниками.\n" 31 | "assignments: поручения. Для каждого поручения укажи: task (что нужно сделать), responsible (кто ответственный), deadline (срок выполнения). Если ответственный или срок не указаны в транскрипте, оставь соответствующие поля пустыми (запрещено писать 'Нет' или 'Не указан' в этих полях).\n" 32 | "qa_items: Заданные на встрече вопросы и данные на них ответы. Выбирай только существенные вопросы, относящиеся к обсуждаемой теме. Не обращай внимания на сиюминутные технические вопросы типа `видно ли мой экран`, `слышно ли меня` и т.п.\n" 33 | "decision_records: ключевые решения, принятые на встрече. Для каждого решения укажи: context (ситуация, в которой возник вопрос), question (суть вопроса), considered_options (рассмотренные варианты решения списком), proposed_decision (предложенное решение), status (состояние обсуждения предложенного решения: Предложено (обсуждение или принятие решения не завершено), Принято (обсуждено и однозначно принято решение), Отложено (данный вопрос сейчас не имеет значения), Отклонено (предложенное решение не подошло)), consequences (последствия принятого решения списком с (+) для положительных или (-) для отрицательных), rationale (обоснование принятого решения).\n" 34 | "open_questions: все открытые вопросы, которые были озвучены на встрече, но остались без ответа, и по ним не было принято решение и не было создано поручение.\n" 35 | "meeting_title: краткое, описательное название совещания на русском языке (до 60 символов).\n" 36 | ) 37 | -------------------------------------------------------------------------------- /sgr_minutes_models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import List 3 | 4 | class QAItem(BaseModel): 5 | """Individual question and answer pair using SGR Cycle pattern.""" 6 | question: str = Field(description="The question asked during the meeting") 7 | answer: str = Field(description="The answer provided to the question") 8 | 9 | 10 | class ProtocolItem(BaseModel): 11 | """Individual item in a protocol section using SGR Cycle pattern.""" 12 | content: str = Field(description="The content of this protocol item") 13 | 14 | 15 | class DecisionRecords(BaseModel): 16 | """Key decision made during the meeting with full context and consequences.""" 17 | context: str = Field(description="The context or situation that led to the question") 18 | question: str = Field(description="The core question or issue being addressed.") 19 | considered_options: List[str] = Field(description="List of options that were considered") 20 | proposed_decision: str = Field(description="The final decision made. Should exactly match one of `considered_options`. No follow-up actions are allowed in this foeld.") 21 | status: str = Field(description="Status of the proposed_decision: Предложено, Принято, or Отложено. 'Предложено' means that the discussion or research is not finished yet. 'Принято' means that one of the considered options is approved (proposed_decision matches one of considered_options), and no changes on this topic are expected. 'Отложено' means that the question does not matter at the moment. 'Отклонено' means that none of considered_options are good enough.") 22 | consequences: List[str] = Field(description="List of consequences of the proposed decision, each prefixed with (+) for positive or (-) for negative") 23 | rationale: str = Field(description="The reasoning or justification behind proposed decision") 24 | 25 | 26 | class Assignment(BaseModel): 27 | """Task assignment with responsible person and deadline.""" 28 | task: str = Field(description="What needs to be done") 29 | responsible: str = Field(description="Who is responsible for this task") 30 | deadline: str = Field(description="When this task should be completed") 31 | 32 | 33 | class MinutesResponse(BaseModel): 34 | """Structured output for one-shot minutes generation with SGR Cycle patterns. 35 | 36 | Fields are ordered to guide the model's reasoning (SGR Cascade + Cycle): 37 | 1) Generate each protocol section as lists of items (SGR Cycle) 38 | 2) Extract Questions & Answers as list of Q&A pairs (SGR Cycle) 39 | 3) Generate meeting title 40 | """ 41 | 42 | meeting_goals: List[ProtocolItem] = Field( 43 | description="List of meeting goals and objectives", 44 | min_length=2 45 | ) 46 | established_facts: List[ProtocolItem] = Field( 47 | description="List of the most significant facts established and confirmed during the meeting" 48 | ) 49 | existing_problems: List[ProtocolItem] = Field( 50 | description="List of the most significant problems and issues identified during the meeting" 51 | ) 52 | assignments: List[Assignment] = Field( 53 | description="List of task assignments with responsible persons and deadlines where specified" 54 | ) 55 | decision_records: List[DecisionRecords] = Field( 56 | description="Decision Records - a list of decisions made during the meeting." 57 | ) 58 | # Q&A extraction using SGR Cycle pattern 59 | qa_items: List[QAItem] = Field( 60 | description="A concise list of questions and their corresponding answers from the meeting transcript, focusing solely on relevant discussion topics", 61 | min_length=3, 62 | max_length=20 63 | ) 64 | open_questions: List[ProtocolItem] = Field( 65 | description="List of open questions that have no confident answers and no decisions or assignments were made on them" 66 | ) 67 | meeting_title: str = Field( 68 | description="Short, descriptive meeting title in Russian (up to 60 characters)" 69 | ) 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Протокол встречи из транскрипта встречи (SGR) 2 | 3 | CLI-скрипт для генерации структурированного протокола совещания из текстового транскрипта по технике Schema-Guided Reasoning (один вызов LLM с заполнением схемы). 4 | 5 | Протокол включает: 6 | - Цели встречи 7 | - Установленные факты 8 | - Имеющиеся проблемы 9 | - Ключевые решения (структурированная таблица с контекстом, вариантами, обоснованием и последствиями) 10 | - Поручения (таблица с ответственными и сроками) 11 | - Вопросы и ответы 12 | - Открытые вопросы 13 | 14 | 🔥 Хорошо работает даже с локальными 4B-моделями! 15 | 16 | ### Требования 17 | - Python 3.12 или выше 18 | - Установленный [uv](https://github.com/astral-sh/uv?tab=readme-ov-file#installation) (запуск и управление зависимостями в Python-проектах) 19 | 20 | Зависимости (устанавливаются автоматически через `uv`): 21 | - `openai>=1.101.0` — для работы с OpenAI-совместимыми API 22 | - `python-dotenv>=1.0.0` — для загрузки переменных окружения из `.env` 23 | 24 | ### Настройка 25 | Создайте файл `.env` в корне проекта на основе `.env.example` и задайте параметры провайдеров LLM, которых планируете использовать. 26 | Достаточно настроить одного из провайдеров (локальный или облачный). Указывайте совместимый с OpenAI API endpoint и ключ, модель — идентификатор модели. 27 | 28 | ```env 29 | # Локальный провайдер 30 | API_BASE_LOCAL= 31 | API_KEY_LOCAL= 32 | MODEL_LOCAL= 33 | 34 | # Облачный провайдер 35 | API_BASE_CLOUD= 36 | API_KEY_CLOUD= 37 | MODEL_CLOUD= 38 | ``` 39 | 40 | ### Запуск 41 | Базовый сценарий (по умолчанию - локальный провайдер): 42 | 43 | ```bash 44 | uv run main.py path\to\transcript.txt 45 | ``` 46 | 47 | Использовать облачного провайдера: 48 | 49 | ```bash 50 | uv run main.py path\to\transcript.txt --online 51 | ``` 52 | 53 | Переопределить модель для текущего провайдера (по умолчанию - модель, указанная в `.env`): 54 | 55 | ```bash 56 | uv run main.py path\to\transcript.txt -m model-id 57 | ``` 58 | - Для локального распознавания рекомендуется использовать модель `qwen/qwen3-4b-2507` (Да, очень маленькая. Да, результаты лучше, чем у больших.) 59 | - Для облачного распознавания рекомендуется использовать модель `google/gemini-2.5-pro` 60 | 61 | Задать температуру семплирования (по умолчанию 0.15): 62 | 63 | ```bash 64 | uv run main.py path\to\transcript.txt -t 0.2 65 | ``` 66 | 67 | Добавить подробности (имя модели и время генерации) в имя выходного файла: 68 | 69 | ```bash 70 | uv run main.py path\to\transcript.txt --details 71 | ``` 72 | 73 | Использовать исходное имя транскрипта вместо даты в качестве префикса: 74 | 75 | ```bash 76 | uv run main.py path\to\transcript.txt --keep_base_name 77 | ``` 78 | 79 | ### Глоссарий терминов 80 | 81 | Система поддерживает использование глоссария для коррекции некорректных написаний терминов, которые могут возникнуть при распознавании речи (например, "GVT" вместо "JWT", "кавка" вместо "Kafka"). Глоссарий работает в два этапа: 82 | 83 | 1. **Промт-инструкции**: LLM получает явные правила использования канонических терминов вместо их ошибочных вариантов 84 | 2. **Автоматическая постобработка** (по умолчанию включена): удаляет случаи, когда LLM добавляет некорректный вариант в скобках после правильного термина (например, `JWT (GVT)` → `JWT`) 85 | 86 | #### Базовый глоссарий 87 | 88 | Создайте файл `glossary.yaml` в корне проекта со следующей структурой (на основе примера glossary.yaml.example): 89 | 90 | ```yaml 91 | - canonical: JWT 92 | variants: 93 | - GVT 94 | - гвт 95 | description: JSON Web Token — стандарт токенов для аутентификации 96 | 97 | - canonical: Kafka 98 | variants: 99 | - кавка 100 | - кафка 101 | - Кавка 102 | description: Брокер сообщений Apache Kafka 103 | ``` 104 | 105 | Каждая запись содержит: 106 | - `canonical` (обязательно) — правильное написание термина 107 | - `variants` (обязательно) — список некорректных/альтернативных вариантов написания 108 | - `description` (опционально) — краткое описание термина для дополнительного контекста LLM 109 | 110 | Базовый глоссарий загружается автоматически, если файл `glossary.yaml` существует в корне проекта. 111 | 112 | #### Дополнительный глоссарий 113 | 114 | Для проектно-специфичных терминов можно указать дополнительный глоссарий через параметр `--extra-glossary`: 115 | 116 | ```bash 117 | uv run main.py path\to\transcript.txt --extra-glossary client_glossary.yaml 118 | ``` 119 | 120 | При конфликте (одинаковый вариант указывает на разные канонические термины) **приоритет имеет дополнительный глоссарий**, конфликт логируется. 121 | 122 | #### Принудительная замена всех вхождений некорректных вариантов 123 | 124 | По умолчанию глоссарий работает в два этапа: 125 | 1. Удаление скобочных дублей типа `JWT (GVT)` → `JWT` 126 | 2. **Принудительная замена** всех отдельных слов, совпадающих с вариантами из глоссария (без учёта регистра), на канонические термины 127 | 128 | Примеры замен на втором этапе: 129 | - `GVT` → `JWT` 130 | - `кавка` → `Kafka` 131 | - `Кавка` → `Kafka` 132 | 133 | Чтобы отключить принудительную замену и оставить только удаление скобочных дублей: 134 | 135 | ```bash 136 | uv run main.py path\to\transcript.txt --no-force-replace 137 | ``` 138 | 139 | **Внимание**: принудительная замена может заменить варианты даже в контекстах, где это нежелательно (например, в прямых цитатах или именах файлов). Если это происходит, используйте флаг `--no-force-replace`. 140 | 141 | #### Отключение удаления некорректных вариантов в скобках после канонических терминов 142 | 143 | Если нужно оставить текст в том виде, как его вернула LLM (без удаления некорректных вариантов в скобках после канонических терминов): 144 | 145 | ```bash 146 | uv run main.py path\to\transcript.txt --no-glossary-cleanup 147 | ``` 148 | 149 | Можно комбинировать флаги: 150 | 151 | ```bash 152 | # Только принудительная замена, без очистки скобок 153 | uv run main.py path\to\transcript.txt --no-glossary-cleanup 154 | 155 | # Только очистка скобок, без принудительной замены 156 | uv run main.py path\to\transcript.txt --no-force-replace 157 | 158 | # Отключить всю постобработку глоссария 159 | uv run main.py path\to\transcript.txt --no-glossary-cleanup --no-force-replace 160 | ``` 161 | 162 | ### Температура: влияние и рекомендации 163 | - **Низкая (0.0–0.25)**: более детерминированный и фактологичный вывод. Рекомендуется для протоколов. 164 | - **Средняя (0.25–0.4)**: немного больше вариативности формулировок. 165 | - **Высокая (0.5+)**: креативнее, но выше риск отклонений от исходного текста — обычно не нужно для протоколов. 166 | 167 | По умолчанию используется **0.15**. 168 | 169 | ### Результат 170 | Итоговый протокол сохраняется рядом с исходным файлом транскрипта. Формат имени зависит от использованных параметров: 171 | 172 | - **По умолчанию**: `-<тема_встречи>.md` 173 | - **С флагом --keep_base_name**: `-<тема_встречи>.md` 174 | - **С флагом --details**: к имени файла добавляется `-[<время>s]` 175 | 176 | Примеры: 177 | - `2024-11-07-Обсуждение-архитектуры.md` (по умолчанию) 178 | - `meeting_transcript-Обсуждение-архитектуры.md` (с --keep_base_name) 179 | - `2024-11-07-Обсуждение-архитектуры-qwen3-4b-2507[45s].md` (с --details) 180 | 181 | #### Структура выходного протокола 182 | 183 | Протокол формируется в формате Markdown и включает следующие разделы: 184 | 185 | 1. **Цели встречи** — нумерованный список целей и задач совещания 186 | 2. **Установленные факты** — ключевые факты, подтверждённые на встрече 187 | 3. **Имеющиеся проблемы** — выявленные проблемы и вопросы 188 | 4. **Ключевые решения** — структурированная таблица с полями: 189 | - Контекст — ситуация, приведшая к вопросу 190 | - Вопрос — суть обсуждаемого вопроса 191 | - Рассмотренные варианты — список вариантов решения 192 | - Решение — финальное решение 193 | - Статус — Предложено / Принято / Отложено / Отклонено 194 | - Последствия — список последствий с префиксами (+) для позитивных и (-) для негативных 195 | - Обоснование — объяснение выбора 196 | 5. **Поручения** — таблица поручений с ответственными и сроками выполнения 197 | 6. **Вопросы и ответы** — таблица вопросов и ответов из обсуждения 198 | 7. **Открытые вопросы** — нерешённые вопросы без конкретных решений 199 | 200 | ### Логирование 201 | 202 | Скрипт автоматически сохраняет логи в директорию `logs/meeting_summary.log` с ротацией по дням (хранятся последние 30 дней). В логах отображается информация о загрузке глоссариев, конфликтах терминов и применении постобработки. 203 | -------------------------------------------------------------------------------- /glossary.py: -------------------------------------------------------------------------------- 1 | """Glossary support for meeting minutes generation. 2 | 3 | This module loads glossary definitions from YAML files and prepares 4 | a compact, human-readable rules block for inclusion into the LLM prompt. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import logging 10 | import os 11 | import re 12 | from dataclasses import dataclass, field 13 | from typing import Iterable, List, Dict, Tuple, Optional 14 | 15 | import yaml 16 | 17 | from meeting_summary_prompts import GLOSSARY_SECTION_TITLE, GLOSSARY_SECTION_INTRO 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | @dataclass 23 | class GlossaryEntry: 24 | """Single glossary entry with canonical term and its variants.""" 25 | 26 | canonical: str 27 | variants: List[str] = field(default_factory=list) 28 | description: Optional[str] = None 29 | 30 | def normalized_variants(self) -> List[str]: 31 | """Return variants cleaned from empty strings and duplicates.""" 32 | seen = set() 33 | result: List[str] = [] 34 | for v in self.variants: 35 | v_clean = (v or "").strip() 36 | if not v_clean: 37 | continue 38 | if v_clean.lower() in seen: 39 | continue 40 | seen.add(v_clean.lower()) 41 | result.append(v_clean) 42 | return result 43 | 44 | 45 | class Glossary: 46 | """Container for glossary entries plus helpers for prompt generation.""" 47 | 48 | def __init__(self, entries: Optional[Iterable[GlossaryEntry]] = None) -> None: 49 | self.entries: List[GlossaryEntry] = list(entries or []) 50 | 51 | def is_empty(self) -> bool: 52 | """Return True if no usable entries exist.""" 53 | return not any(e.normalized_variants() for e in self.entries) 54 | 55 | def clean_protocol_text(self, text: str) -> str: 56 | """Remove incorrect variant mentions in parentheses after canonical terms. 57 | 58 | Example: "JWT (GVT)" -> "JWT" 59 | This is a lightweight post-processing step to remove cases where LLM 60 | helpfully adds the incorrect variant in parentheses despite instructions. 61 | """ 62 | if not text or self.is_empty(): 63 | return text 64 | 65 | # Build a map of canonical -> set of variants (normalized to lowercase) 66 | canonical_to_variants: Dict[str, set] = {} 67 | for entry in self.entries: 68 | variants_lower = {v.lower() for v in entry.normalized_variants()} 69 | canonical_to_variants[entry.canonical] = variants_lower 70 | 71 | # For each canonical term, remove patterns like "Canonical (variant)" 72 | for canonical, variants_set in canonical_to_variants.items(): 73 | # Escape canonical for regex 74 | canonical_escaped = re.escape(canonical) 75 | # Build a pattern that matches any variant in parentheses 76 | # We want to match: "Canonical (variant1)" or "Canonical (variant2)" 77 | for variant in variants_set: 78 | variant_escaped = re.escape(variant) 79 | # Case-insensitive pattern: canonical followed by optional spaces, open paren, variant, close paren 80 | pattern = rf"\b{canonical_escaped}\s*\(\s*{variant_escaped}\s*\)" 81 | # Replace with just the canonical term 82 | text = re.sub(pattern, canonical, text, flags=re.IGNORECASE) 83 | 84 | return text 85 | 86 | def force_replace_variants(self, text: str) -> str: 87 | """Force replace all standalone variant occurrences with canonical terms. 88 | 89 | This is an aggressive post-processing step that replaces any standalone 90 | word matching a glossary variant (case-insensitive) with its canonical term. 91 | 92 | Example: "GVT токен" -> "JWT токен", "Кавка брокер" -> "Kafka брокер" 93 | """ 94 | if not text or self.is_empty(): 95 | return text 96 | 97 | # Build a map: variant (lowercase) -> canonical term 98 | variant_to_canonical: Dict[str, str] = {} 99 | for entry in self.entries: 100 | for variant in entry.normalized_variants(): 101 | variant_to_canonical[variant.lower()] = entry.canonical 102 | 103 | # Sort variants by length (descending) to handle multi-word variants first 104 | sorted_variants = sorted(variant_to_canonical.keys(), key=len, reverse=True) 105 | 106 | # Replace each variant with its canonical term 107 | for variant_lower in sorted_variants: 108 | canonical = variant_to_canonical[variant_lower] 109 | # Escape variant for regex 110 | variant_escaped = re.escape(variant_lower) 111 | # Match variant as a whole word (case-insensitive) 112 | pattern = rf"\b{variant_escaped}\b" 113 | # Replace with canonical term, preserving surrounding text 114 | text = re.sub(pattern, canonical, text, flags=re.IGNORECASE) 115 | 116 | return text 117 | 118 | def to_prompt_block(self) -> str: 119 | """Render glossary rules as a text block for inclusion into system prompt. 120 | 121 | The block is written in Russian and explains how to map variants to 122 | canonical terms when composing the meeting protocol. 123 | """ 124 | if self.is_empty(): 125 | return "" 126 | 127 | lines: List[str] = [] 128 | lines.append(f"{GLOSSARY_SECTION_TITLE}\n") 129 | lines.append(GLOSSARY_SECTION_INTRO) 130 | 131 | # Sort for stable, predictable order 132 | for entry in sorted(self.entries, key=lambda e: e.canonical.lower()): 133 | variants = entry.normalized_variants() 134 | if not variants: 135 | continue 136 | 137 | variants_str = ", ".join(f'"{v}"' for v in variants) 138 | line = ( 139 | f'- Если в тексте встречаются варианты: {variants_str}, ' 140 | f'используй в протоколе термин: "{entry.canonical}".' 141 | ) 142 | if entry.description: 143 | line += f" {entry.description}" 144 | lines.append(line) 145 | 146 | return "\n".join(lines) 147 | 148 | 149 | def _load_yaml(path: str) -> List[dict]: 150 | """Load raw YAML data from file, returning a list of entry dicts.""" 151 | if not os.path.exists(path): 152 | raise FileNotFoundError(f"Glossary file does not exist: {path}") 153 | 154 | with open(path, "r", encoding="utf-8") as f: 155 | data = yaml.safe_load(f) or [] 156 | 157 | if not isinstance(data, list): 158 | raise ValueError(f"Glossary YAML must contain a list of entries, got {type(data)!r}") 159 | 160 | return data 161 | 162 | 163 | def load_glossary(path: str) -> Glossary: 164 | """Load a single glossary from a YAML file.""" 165 | raw_entries = _load_yaml(path) 166 | entries: List[GlossaryEntry] = [] 167 | 168 | for idx, raw in enumerate(raw_entries): 169 | if not isinstance(raw, dict): 170 | logger.warning("Skipping non-dict glossary entry at index %s in %s", idx, path) 171 | continue 172 | 173 | canonical = (raw.get("canonical") or "").strip() 174 | variants_raw = raw.get("variants") or [] 175 | description_raw = raw.get("description") 176 | 177 | if not canonical: 178 | logger.warning("Skipping glossary entry without canonical term at index %s in %s", idx, path) 179 | continue 180 | 181 | if not isinstance(variants_raw, list): 182 | logger.warning( 183 | "Skipping glossary entry with non-list variants at index %s in %s", idx, path 184 | ) 185 | continue 186 | 187 | variants = [] 188 | for v in variants_raw: 189 | v_str = (str(v) if v is not None else "").strip() 190 | if v_str: 191 | variants.append(v_str) 192 | 193 | if not variants: 194 | logger.warning( 195 | "Skipping glossary entry without usable variants at index %s in %s", idx, path 196 | ) 197 | continue 198 | 199 | description = None 200 | if description_raw is not None: 201 | description_str = str(description_raw).strip() 202 | description = description_str if description_str else None 203 | 204 | entries.append( 205 | GlossaryEntry( 206 | canonical=canonical, 207 | variants=variants, 208 | description=description, 209 | ) 210 | ) 211 | 212 | if not entries: 213 | logger.info("Loaded glossary from %s, but no valid entries were found", path) 214 | else: 215 | logger.info("Loaded %d glossary entries from %s", len(entries), path) 216 | 217 | return Glossary(entries) 218 | 219 | 220 | def merge_glossaries(base: Glossary, extra: Glossary) -> Glossary: 221 | """Merge base and extra glossaries with extra taking precedence on conflicts. 222 | 223 | Conflict rule: 224 | - If the same variant appears in both glossaries but points to different 225 | canonical terms, the canonical from the extra glossary is used and 226 | a warning is logged. 227 | """ 228 | # Map normalized variant -> (canonical, description, source, surface_form) 229 | variant_map: Dict[str, Tuple[str, Optional[str], str, str]] = {} 230 | 231 | def add_entries(entries: Iterable[GlossaryEntry], source: str, overwrite: bool) -> None: 232 | for entry in entries: 233 | for v in entry.normalized_variants(): 234 | key = v.lower() 235 | if key in variant_map and not overwrite: 236 | # Base glossary keeps existing mapping 237 | continue 238 | 239 | if key in variant_map and overwrite: 240 | prev_canonical, _, prev_source, _ = variant_map[key] 241 | if prev_canonical != entry.canonical: 242 | logger.warning( 243 | "Glossary conflict for variant %r: %r from %s overridden by %r from %s", 244 | v, 245 | prev_canonical, 246 | prev_source, 247 | entry.canonical, 248 | source, 249 | ) 250 | 251 | variant_map[key] = (entry.canonical, entry.description, source, v) 252 | 253 | # Base glossary first, then extra with overwrite 254 | add_entries(base.entries, source="base", overwrite=False) 255 | add_entries(extra.entries, source="extra", overwrite=True) 256 | 257 | # Rebuild entries grouped by canonical term 258 | canonical_map: Dict[str, Tuple[set, Optional[str]]] = {} 259 | for _, (canonical, description, _, surface_form) in variant_map.items(): 260 | if canonical not in canonical_map: 261 | canonical_map[canonical] = (set(), description) 262 | variants_set, existing_description = canonical_map[canonical] 263 | variants_set.add(surface_form) 264 | # Prefer any non-empty description, later extra entries may override 265 | if description: 266 | canonical_map[canonical] = (variants_set, description) 267 | else: 268 | canonical_map[canonical] = (variants_set, existing_description) 269 | 270 | merged_entries: List[GlossaryEntry] = [] 271 | for canonical, (variants_set, description) in canonical_map.items(): 272 | merged_entries.append( 273 | GlossaryEntry( 274 | canonical=canonical, 275 | variants=sorted(variants_set), 276 | description=description, 277 | ) 278 | ) 279 | 280 | logger.info( 281 | "Merged glossaries: %d base entries + %d extra entries -> %d merged entries", 282 | len(base.entries), 283 | len(extra.entries), 284 | len(merged_entries), 285 | ) 286 | 287 | return Glossary(merged_entries) 288 | 289 | 290 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | import logging 6 | import logging.handlers 7 | import argparse 8 | from datetime import datetime 9 | from typing import Optional 10 | 11 | from dotenv import load_dotenv 12 | from openai import OpenAI 13 | 14 | from meeting_summary_prompts import system_message 15 | from sgr_minutes_models import MinutesResponse 16 | from glossary import Glossary, load_glossary, merge_glossaries 17 | 18 | logger = logging.getLogger(__name__) 19 | logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") 20 | def _setup_file_logging() -> None: 21 | """Configure daily rotating file logging and keep 30 days.""" 22 | log_dir = "logs" 23 | os.makedirs(log_dir, exist_ok=True) 24 | log_path = os.path.join(log_dir, "meeting_summary.log") 25 | file_handler = logging.handlers.TimedRotatingFileHandler( 26 | log_path, when="midnight", backupCount=30, encoding="utf-8" 27 | ) 28 | file_handler.setLevel(logging.INFO) 29 | file_formatter = logging.Formatter( 30 | "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s() - %(message)s" 31 | ) 32 | file_handler.setFormatter(file_formatter) 33 | logging.getLogger().addHandler(file_handler) 34 | 35 | _setup_file_logging() 36 | 37 | # Load environment variables from .env file 38 | load_dotenv() 39 | 40 | # Use environment variable to control storage type, default to JSON storage 41 | API_BASE_CLOUD = os.getenv("API_BASE_CLOUD") 42 | API_KEY_CLOUD = os.getenv("API_KEY_CLOUD") 43 | MODEL_ID_CLOUD = os.getenv("MODEL_CLOUD") 44 | 45 | API_BASE_LOCAL = os.getenv("API_BASE_LOCAL") 46 | API_KEY_LOCAL = os.getenv("API_KEY_LOCAL") 47 | MODEL_ID_LOCAL = os.getenv("MODEL_LOCAL") 48 | 49 | def get_meeting_date_regex(file_name: str) -> tuple[datetime.date, str]: 50 | """ 51 | Extract meeting date from filename using regex patterns. 52 | Supports YYYY-MM-DD and YYYY-MMDD formats. 53 | Returns: (date_object, formatted_string_for_protocol) 54 | """ 55 | filename = os.path.basename(file_name) 56 | logger.info(f"Extracting date from filename: {filename}") 57 | 58 | # Map weekday numbers to Russian day names 59 | weekdays_ru = { 60 | 0: "понедельник", 61 | 1: "вторник", 62 | 2: "среда", 63 | 3: "четверг", 64 | 4: "пятница", 65 | 5: "суббота", 66 | 6: "воскресенье" 67 | } 68 | 69 | # Try to match YYYY-MM-DD or YYYY-MMDD patterns 70 | date_match = re.search(r"(\d{4}-\d{2}-\d{2})|(\d{4}-\d{4})", filename) 71 | 72 | meeting_date = None 73 | if date_match: 74 | if date_match.group(1): # Matched YYYY-MM-DD 75 | meeting_date_str = date_match.group(1) 76 | meeting_date = datetime.strptime(meeting_date_str, "%Y-%m-%d").date() 77 | elif date_match.group(2): # Matched YYYY-MMDD 78 | meeting_date_str = date_match.group(2) 79 | # Convert YYYY-MMDD to YYYY-MM-DD for parsing 80 | meeting_date_str = meeting_date_str[:4] + '-' + meeting_date_str[5:7] + '-' + meeting_date_str[7:] 81 | meeting_date = datetime.strptime(meeting_date_str, "%Y-%m-%d").date() 82 | 83 | if meeting_date: 84 | logger.info(f"Extracted date: {meeting_date}") 85 | else: 86 | today = datetime.now().date() 87 | meeting_date = today 88 | logger.info(f"No date found in filename. Using today's date: {meeting_date}") 89 | 90 | day_of_week_num = meeting_date.weekday() 91 | day_of_week_name = weekdays_ru[day_of_week_num] 92 | 93 | final_answer_str = f"Дата совещания: {day_of_week_name} {meeting_date.strftime('%Y-%m-%d')}" 94 | return meeting_date, final_answer_str 95 | 96 | def load_transcript(file_name: str) -> str: 97 | with open(file_name, encoding="utf-8") as file: 98 | transcript = file.read() 99 | return transcript 100 | 101 | 102 | def get_summary_file_name( 103 | file_name: str, 104 | model_id: str, 105 | suffix: str = "", 106 | details: bool = False, 107 | elapsed_seconds: int | None = None, 108 | meeting_date: datetime.date | None = None, 109 | keep_base_name: bool = False, 110 | ) -> str: 111 | """Build final summary file path. 112 | 113 | Includes base transcript name, optional LLM-provided suffix, model id, and 114 | optionally appends elapsed seconds in square brackets before the extension. 115 | """ 116 | file_dir = os.path.dirname(file_name) 117 | file_base_name = os.path.basename(file_name) 118 | file_base_name_without_ext = os.path.splitext(file_base_name)[0] 119 | safe_model_name = model_id.split("/")[-1] 120 | 121 | # If meeting_date is provided, use date prefix instead of original filename 122 | if meeting_date is not None: 123 | date_prefix = meeting_date.strftime("%Y-%m-%d") 124 | else: 125 | date_prefix = datetime.now().strftime("%Y-%m-%d") 126 | 127 | base = file_base_name_without_ext if keep_base_name else date_prefix 128 | 129 | base = f"{base}-{suffix}" if suffix else base 130 | 131 | if details: 132 | base = f"{base}-{safe_model_name}" 133 | if elapsed_seconds is not None: 134 | base = f"{base}[{elapsed_seconds}s]" 135 | 136 | output_file_name = f"{base}.md" 137 | 138 | return os.path.join(file_dir, output_file_name) if file_dir else output_file_name 139 | 140 | 141 | def _build_system_message_with_glossary(glossary: Optional[Glossary]) -> str: 142 | """Build final system message, optionally enriched with glossary rules.""" 143 | if glossary is None or glossary.is_empty(): 144 | return system_message 145 | 146 | glossary_block = glossary.to_prompt_block() 147 | if not glossary_block: 148 | return system_message 149 | 150 | return f"{system_message}\n\n{glossary_block}" 151 | 152 | 153 | def get_minutes_one_call( 154 | file_name: str, 155 | meeting_date: str, 156 | client: OpenAI, 157 | model_id: str, 158 | temperature: float, 159 | glossary: Optional[Glossary] = None, 160 | ) -> MinutesResponse: 161 | """Run a single SGR-guided call that returns all minutes artifacts with detailed protocol sections. 162 | 163 | The model is guided to fill the schema fields in a cascade order: 164 | topics -> protocol sections -> Q&A -> filename suffix. 165 | """ 166 | transcript = load_transcript(file_name) 167 | 168 | user_message = ( 169 | f"Дата встречи: {meeting_date}\n\n" 170 | "Транскрипт встречи:\n\n" 171 | f"{transcript}" 172 | ) 173 | 174 | try: 175 | completion = client.chat.completions.parse( 176 | model=model_id, 177 | response_format=MinutesResponse, 178 | messages=[ 179 | { 180 | "role": "system", 181 | "content": _build_system_message_with_glossary(glossary), 182 | }, 183 | {"role": "user", "content": user_message}, 184 | ], 185 | temperature=temperature, 186 | max_tokens=60000, 187 | timeout=3600, 188 | ) 189 | 190 | if not completion.choices or not completion.choices[0].message: 191 | raise ValueError("Empty LLM response") 192 | 193 | minutes: MinutesResponse | None = getattr(completion.choices[0].message, "parsed", None) 194 | if minutes is None: 195 | raise ValueError("Parsed MinutesResponse is missing in the response") 196 | 197 | return minutes 198 | 199 | except Exception as e: 200 | logger.error(f"Error in SGR parsing: {e}") 201 | raise 202 | 203 | def make_filesystem_safe_suffix(suffix: str) -> str: 204 | # Replace spaces with dashes and remove invalid characters 205 | safe_suffix = re.sub(r'[^\w\-_а-яёА-ЯЁ]', '', suffix.replace(' ', '-')) 206 | # Remove multiple consecutive dashes 207 | safe_suffix = re.sub(r'-+', '-', safe_suffix) 208 | # Remove leading/trailing dashes 209 | safe_suffix = safe_suffix.strip('-') 210 | # Limit length to 30 characters 211 | safe_suffix = safe_suffix[:60] 212 | return safe_suffix 213 | 214 | 215 | def main(): 216 | """Main function to execute SGR-based meeting minutes generation. 217 | 218 | Uses a single LLM call with detailed protocol sections to generate all parts of the protocol: 219 | - Individual protocol sections (goals, facts, problems, decisions, tasks, questions) 220 | - Questions & Answers table 221 | - Filename suffix 222 | """ 223 | 224 | parser = argparse.ArgumentParser(description="Summarize meeting transcript using Schema Guided Reasoning technique.") 225 | parser.add_argument("transcript_file", help="Path to the meeting transcript file.") 226 | parser.add_argument("-m", "--model", help="Model to use for summarization. If not specified, uses default model for selected provider.") 227 | 228 | parser.add_argument("--online", action="store_true", help="Use online cloud LLM provider instead of local.") 229 | parser.add_argument("-t", "--temperature", type=float, default=0.15, help="Sampling temperature for the LLM (default: 0.15).") 230 | parser.add_argument("--details", action="store_true", help="Add model name and generation time to output filename (default: False).") 231 | parser.add_argument("--keep_base_name", action="store_true", help="Keep original transcript filename as base (default: use date prefix).") 232 | parser.add_argument( 233 | "--extra-glossary", 234 | dest="extra_glossary", 235 | help="Path to an additional glossary YAML file that overrides base entries on conflicts.", 236 | ) 237 | parser.add_argument( 238 | "--no-glossary-cleanup", 239 | dest="no_glossary_cleanup", 240 | action="store_true", 241 | help="Disable automatic post-processing cleanup of incorrect term variants in parentheses (default: cleanup enabled).", 242 | ) 243 | parser.add_argument( 244 | "--no-force-replace", 245 | dest="no_force_replace", 246 | action="store_true", 247 | help="Disable automatic force replacement of all standalone glossary variant occurrences with canonical terms (default: force replacement enabled).", 248 | ) 249 | 250 | args = parser.parse_args() 251 | transcript_file_name = args.transcript_file 252 | use_online = args.online 253 | keep_base_name = args.keep_base_name if args.keep_base_name else False 254 | extra_glossary_path: Optional[str] = args.extra_glossary 255 | 256 | # Determine which provider configuration to use 257 | if use_online: 258 | api_base = API_BASE_CLOUD 259 | api_key = API_KEY_CLOUD 260 | default_model = MODEL_ID_CLOUD 261 | provider_type = "online cloud" 262 | else: 263 | api_base = API_BASE_LOCAL 264 | api_key = API_KEY_LOCAL 265 | default_model = MODEL_ID_LOCAL 266 | provider_type = "local" 267 | 268 | # Use specified model or default for selected provider 269 | model_id = args.model if args.model else default_model 270 | 271 | logger.info("Using %s provider with model: %s", provider_type, model_id) 272 | 273 | openai_client = OpenAI( 274 | base_url=api_base, 275 | api_key=api_key, 276 | ) 277 | 278 | # Get meeting date using algorithmic extraction 279 | meeting_date_obj, meeting_date_str = get_meeting_date_regex(transcript_file_name) 280 | 281 | logger.info("Processing meeting from %s", meeting_date_str) 282 | 283 | # Load base glossary.yaml if present, and optional extra glossary if provided 284 | glossary: Optional[Glossary] = None 285 | base_glossary_path = "glossary.yaml" 286 | 287 | try: 288 | if os.path.exists(base_glossary_path): 289 | base_glossary = load_glossary(base_glossary_path) 290 | else: 291 | base_glossary = None 292 | 293 | extra_glossary: Optional[Glossary] = None 294 | if extra_glossary_path: 295 | extra_glossary = load_glossary(extra_glossary_path) 296 | 297 | if base_glossary and extra_glossary: 298 | glossary = merge_glossaries(base_glossary, extra_glossary) 299 | else: 300 | glossary = extra_glossary or base_glossary 301 | 302 | except FileNotFoundError as e: 303 | # Extra glossary path is wrong – fail fast with clear log 304 | logger.error("Glossary file not found: %s", e) 305 | raise 306 | except Exception as e: 307 | logger.error("Failed to load glossary files: %s", e) 308 | raise 309 | 310 | generation_start_time = datetime.now() 311 | minutes = get_minutes_one_call( 312 | file_name=transcript_file_name, 313 | meeting_date=meeting_date_str, 314 | client=openai_client, 315 | model_id=model_id, 316 | temperature=args.temperature, 317 | glossary=glossary, 318 | ) 319 | generation_end_time = datetime.now() 320 | generation_seconds = (generation_end_time - generation_start_time).total_seconds() 321 | 322 | meeting_title_filename_suffix = make_filesystem_safe_suffix(minutes.meeting_title) 323 | logger.info("Got response, meeting title: %s", minutes.meeting_title) 324 | 325 | # Assemble full protocol from individual sections using Cycle pattern data 326 | def format_section_items(items): 327 | """Format a list of protocol items into a numbered list.""" 328 | if not items: 329 | return "Отсутствуют" 330 | 331 | numbered_items = [] 332 | for i, item in enumerate(items, 1): 333 | content = item.content if hasattr(item, 'content') else str(item) 334 | numbered_items.append(f"{i}. {content}") 335 | 336 | return "\n".join(numbered_items) 337 | 338 | def format_tasks_items(items): 339 | """Format tasks but hide unset fields like 'Не указано' for deadline/responsible.""" 340 | if not items: 341 | return "Отсутствуют" 342 | 343 | def _clean_unset_fields(text: str) -> str: 344 | # Remove patterns like 'Ответственный: Не указано', 'Ответственные: Не указано' 345 | text = re.sub(r"(,\s*)?Ответственн(?:ый|ые):\s*Не\s*указано\.?", "", text, flags=re.IGNORECASE) 346 | # Remove patterns like 'Срок: Не указано', 'Сроки: Не указано', 'Дедлайн: Не указано' 347 | text = re.sub(r"(,\s*)?(?:Сроки?|Дедлайн):?\s*Не\s*указано\.?", "", text, flags=re.IGNORECASE) 348 | # Normalize extra spaces and trailing punctuation left after removals 349 | text = re.sub(r"\s{2,}", " ", text).strip() 350 | text = re.sub(r"[\s,;:\-]+$", "", text) 351 | return text 352 | 353 | numbered_items = [] 354 | for i, item in enumerate(items, 1): 355 | content = item.content if hasattr(item, 'content') else str(item) 356 | content = _clean_unset_fields(content) 357 | numbered_items.append(f"{i}. {content}") 358 | 359 | return "\n".join(numbered_items) 360 | 361 | def format_key_decisions(decisions): 362 | """Format key decisions as a markdown table with 8 columns.""" 363 | if not decisions: 364 | return "Отсутствуют" 365 | 366 | # Table header 367 | table = "| № | Контекст | Вопрос | Рассмотренные варианты | Решение | Статус | Последствия | Обоснование |\n" 368 | table += "|---|----------|--------|------------------------|---------|--------|-------------|-------------|\n" 369 | 370 | for i, decision in enumerate(decisions, 1): 371 | # Format considered options as bullet list 372 | options_text = "
".join([f"- {opt}" for opt in decision.considered_options]) 373 | 374 | # Format consequences with line breaks 375 | consequences_text = "
".join(decision.consequences) 376 | 377 | # Escape pipe characters in cell content 378 | context = decision.context.replace("|", "\\|") 379 | question = decision.question.replace("|", "\\|") 380 | decision_text = decision.proposed_decision.replace("|", "\\|") 381 | status = decision.status.replace("|", "\\|") 382 | rationale = decision.rationale.replace("|", "\\|") 383 | 384 | table += f"| {i} | {context} | {question} | {options_text} | {decision_text} | {status} | {consequences_text} | {rationale} |\n" 385 | 386 | return table 387 | 388 | def format_assignments(assignments): 389 | """Format assignments as a markdown table with 4 columns.""" 390 | if not assignments: 391 | return "Отсутствуют" 392 | 393 | # Table header 394 | table = "| № | Что сделать | Ответственный | Срок |\n" 395 | table += "|---|-------------|---------------|------|\n" 396 | 397 | for i, assignment in enumerate(assignments, 1): 398 | # Escape pipe characters in cell content 399 | task = assignment.task.replace("|", "\\|") 400 | responsible = assignment.responsible.replace("|", "\\|") 401 | deadline = assignment.deadline.replace("|", "\\|") 402 | 403 | table += f"| {i} | {task} | {responsible} | {deadline} |\n" 404 | 405 | return table 406 | 407 | # Build Q&A section to embed into the summary 408 | qa_markdown = "| № | Вопрос | Ответ |\n" 409 | qa_markdown += "|----|---------|--------|\n" 410 | for i, qa_item in enumerate(minutes.qa_items, 1): 411 | qa_markdown += f"| {i} | {qa_item.question} | {qa_item.answer} |\n" 412 | 413 | full_protocol = f"""# Протокол совещания: {minutes.meeting_title} 414 | 415 | {meeting_date_str} 416 | 417 | ### Цели встречи 418 | 419 | {format_section_items(minutes.meeting_goals)} 420 | 421 | ### Установленные факты 422 | 423 | {format_section_items(minutes.established_facts)} 424 | 425 | ### Имеющиеся проблемы 426 | 427 | {format_section_items(minutes.existing_problems)} 428 | 429 | ### Ключевые решения 430 | 431 | {format_key_decisions(minutes.decision_records)} 432 | 433 | ### Поручения 434 | 435 | {format_assignments(minutes.assignments)} 436 | 437 | ### Вопросы и ответы 438 | 439 | {qa_markdown} 440 | 441 | ### Открытые вопросы 442 | 443 | {format_section_items(minutes.open_questions)} 444 | """ 445 | 446 | # Apply glossary-based cleanup to remove incorrect variants in parentheses 447 | if glossary and not glossary.is_empty() and not args.no_glossary_cleanup: 448 | full_protocol = glossary.clean_protocol_text(full_protocol) 449 | logger.info("Applied glossary cleanup to protocol text") 450 | 451 | # Force replace all variant occurrences with canonical terms (enabled by default) 452 | if glossary and not glossary.is_empty() and not args.no_force_replace: 453 | full_protocol = glossary.force_replace_variants(full_protocol) 454 | logger.info("Applied force replacement of glossary variants with canonical terms") 455 | 456 | # Write full protocol with meeting date header 457 | elapsed_seconds_whole = int(generation_seconds) 458 | summary_file_name_with_time = get_summary_file_name( 459 | file_name=transcript_file_name, 460 | model_id=model_id, 461 | suffix=meeting_title_filename_suffix, 462 | details=args.details, 463 | elapsed_seconds=elapsed_seconds_whole, 464 | meeting_date=meeting_date_obj if not args.keep_base_name else None, 465 | keep_base_name=keep_base_name, 466 | ) 467 | with open(summary_file_name_with_time, "w", encoding="utf-8") as file: 468 | file.write(full_protocol) 469 | 470 | # Q&A is now embedded into the summary; no separate Q&A file is written 471 | logger.info( 472 | "Summary saved to file: %s; generation time: %.2f seconds", 473 | summary_file_name_with_time, 474 | generation_seconds, 475 | ) 476 | 477 | if __name__ == "__main__": 478 | main() 479 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.11.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 22 | ] 23 | sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } 24 | wheels = [ 25 | { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2025.11.12" 31 | source = { registry = "https://pypi.org/simple" } 32 | sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, 35 | ] 36 | 37 | [[package]] 38 | name = "colorama" 39 | version = "0.4.6" 40 | source = { registry = "https://pypi.org/simple" } 41 | 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" } 42 | wheels = [ 43 | { 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" }, 44 | ] 45 | 46 | [[package]] 47 | name = "distro" 48 | version = "1.9.0" 49 | source = { registry = "https://pypi.org/simple" } 50 | sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } 51 | wheels = [ 52 | { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, 53 | ] 54 | 55 | [[package]] 56 | name = "dotenv" 57 | version = "0.9.9" 58 | source = { registry = "https://pypi.org/simple" } 59 | dependencies = [ 60 | { name = "python-dotenv" }, 61 | ] 62 | wheels = [ 63 | { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, 64 | ] 65 | 66 | [[package]] 67 | name = "h11" 68 | version = "0.16.0" 69 | source = { registry = "https://pypi.org/simple" } 70 | 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" } 71 | wheels = [ 72 | { 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" }, 73 | ] 74 | 75 | [[package]] 76 | name = "httpcore" 77 | version = "1.0.9" 78 | source = { registry = "https://pypi.org/simple" } 79 | dependencies = [ 80 | { name = "certifi" }, 81 | { name = "h11" }, 82 | ] 83 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 84 | wheels = [ 85 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 86 | ] 87 | 88 | [[package]] 89 | name = "httpx" 90 | version = "0.28.1" 91 | source = { registry = "https://pypi.org/simple" } 92 | dependencies = [ 93 | { name = "anyio" }, 94 | { name = "certifi" }, 95 | { name = "httpcore" }, 96 | { name = "idna" }, 97 | ] 98 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 101 | ] 102 | 103 | [[package]] 104 | name = "idna" 105 | version = "3.11" 106 | source = { registry = "https://pypi.org/simple" } 107 | sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, 110 | ] 111 | 112 | [[package]] 113 | name = "jiter" 114 | version = "0.12.0" 115 | source = { registry = "https://pypi.org/simple" } 116 | sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } 117 | wheels = [ 118 | { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, 119 | { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, 120 | { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, 121 | { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, 122 | { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, 123 | { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, 124 | { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, 125 | { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, 126 | { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, 127 | { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, 128 | { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, 129 | { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, 130 | { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, 131 | { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, 132 | { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, 133 | { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, 134 | { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, 135 | { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, 136 | { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, 137 | { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, 138 | { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, 139 | { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, 140 | { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, 141 | { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, 142 | { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, 143 | { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, 144 | { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, 145 | { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, 146 | { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, 147 | { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, 148 | { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, 149 | { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, 150 | { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, 151 | { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, 152 | { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, 153 | { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, 154 | { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, 155 | { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, 156 | { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, 157 | { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, 158 | { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, 159 | { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, 160 | { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, 161 | { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, 162 | { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, 163 | { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, 164 | { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, 165 | { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, 166 | { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, 167 | { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, 168 | { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, 169 | { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, 170 | { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, 171 | { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, 172 | { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, 173 | { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, 174 | { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, 175 | { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, 176 | { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, 177 | { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, 178 | ] 179 | 180 | [[package]] 181 | name = "meeting-summary-sgr" 182 | version = "0.1.0" 183 | source = { virtual = "." } 184 | dependencies = [ 185 | { name = "dotenv" }, 186 | { name = "openai" }, 187 | ] 188 | 189 | [package.metadata] 190 | requires-dist = [ 191 | { name = "dotenv", specifier = ">=0.9.9" }, 192 | { name = "openai", specifier = ">=1.101.0" }, 193 | ] 194 | 195 | [[package]] 196 | name = "openai" 197 | version = "2.7.2" 198 | source = { registry = "https://pypi.org/simple" } 199 | dependencies = [ 200 | { name = "anyio" }, 201 | { name = "distro" }, 202 | { name = "httpx" }, 203 | { name = "jiter" }, 204 | { name = "pydantic" }, 205 | { name = "sniffio" }, 206 | { name = "tqdm" }, 207 | { name = "typing-extensions" }, 208 | ] 209 | sdist = { url = "https://files.pythonhosted.org/packages/71/e3/cec27fa28ef36c4ccea71e9e8c20be9b8539618732989a82027575aab9d4/openai-2.7.2.tar.gz", hash = "sha256:082ef61163074d8efad0035dd08934cf5e3afd37254f70fc9165dd6a8c67dcbd", size = 595732, upload-time = "2025-11-10T16:42:31.108Z" } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/25/66/22cfe4b695b5fd042931b32c67d685e867bfd169ebf46036b95b57314c33/openai-2.7.2-py3-none-any.whl", hash = "sha256:116f522f4427f8a0a59b51655a356da85ce092f3ed6abeca65f03c8be6e073d9", size = 1008375, upload-time = "2025-11-10T16:42:28.574Z" }, 212 | ] 213 | 214 | [[package]] 215 | name = "pydantic" 216 | version = "2.12.4" 217 | source = { registry = "https://pypi.org/simple" } 218 | dependencies = [ 219 | { name = "annotated-types" }, 220 | { name = "pydantic-core" }, 221 | { name = "typing-extensions" }, 222 | { name = "typing-inspection" }, 223 | ] 224 | sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, 227 | ] 228 | 229 | [[package]] 230 | name = "pydantic-core" 231 | version = "2.41.5" 232 | source = { registry = "https://pypi.org/simple" } 233 | dependencies = [ 234 | { name = "typing-extensions" }, 235 | ] 236 | sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } 237 | wheels = [ 238 | { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, 239 | { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, 240 | { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, 241 | { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, 242 | { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, 243 | { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, 244 | { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, 245 | { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, 246 | { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, 247 | { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, 248 | { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, 249 | { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, 250 | { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, 251 | { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, 252 | { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, 253 | { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, 254 | { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, 255 | { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, 256 | { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, 257 | { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, 258 | { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, 259 | { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, 260 | { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, 261 | { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, 262 | { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, 263 | { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, 264 | { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, 265 | { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, 266 | { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, 267 | { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, 268 | { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, 269 | { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, 270 | { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, 271 | { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, 272 | { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, 273 | { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, 274 | { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, 275 | { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, 276 | { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, 277 | { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, 278 | { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, 279 | { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, 280 | { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, 281 | { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, 282 | { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, 283 | { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, 284 | { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, 285 | { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, 286 | { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, 287 | { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, 288 | { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, 289 | { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, 290 | { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, 291 | { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, 292 | { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, 293 | { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, 294 | { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, 295 | { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, 296 | { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, 297 | { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, 298 | ] 299 | 300 | [[package]] 301 | name = "python-dotenv" 302 | version = "1.2.1" 303 | source = { registry = "https://pypi.org/simple" } 304 | sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } 305 | wheels = [ 306 | { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, 307 | ] 308 | 309 | [[package]] 310 | name = "sniffio" 311 | version = "1.3.1" 312 | source = { registry = "https://pypi.org/simple" } 313 | 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" } 314 | wheels = [ 315 | { 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" }, 316 | ] 317 | 318 | [[package]] 319 | name = "tqdm" 320 | version = "4.67.1" 321 | source = { registry = "https://pypi.org/simple" } 322 | dependencies = [ 323 | { name = "colorama", marker = "sys_platform == 'win32'" }, 324 | ] 325 | sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } 326 | wheels = [ 327 | { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, 328 | ] 329 | 330 | [[package]] 331 | name = "typing-extensions" 332 | version = "4.15.0" 333 | source = { registry = "https://pypi.org/simple" } 334 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 335 | wheels = [ 336 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 337 | ] 338 | 339 | [[package]] 340 | name = "typing-inspection" 341 | version = "0.4.2" 342 | source = { registry = "https://pypi.org/simple" } 343 | dependencies = [ 344 | { name = "typing-extensions" }, 345 | ] 346 | sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } 347 | wheels = [ 348 | { 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" }, 349 | ] 350 | --------------------------------------------------------------------------------