diff --git a/usecase/solar-knowledge-management/.env.example b/usecase/solar-knowledge-management/.env.example new file mode 100644 index 0000000..ea786ed --- /dev/null +++ b/usecase/solar-knowledge-management/.env.example @@ -0,0 +1,32 @@ +# .env.example -> .env + +# =============================================== +# API Key 설정 (공통 요소) +# =============================================== +UPSTAGE_API_KEY=your_api_key_here +TAVILY_API_KEY=your_api_key_here + + + + +# =============================================== +# 노트 분할 관련 요소 +# =============================================== +UPSTAGE_API_BASE=https://api.upstage.ai/v1/solar +MODEL_NAME=solar-pro2 + +# Application Settings +DEFAULT_WORKSPACE=./workspace +PROMPTS_DIR=./prompts + +# Atomic Note Generation +DEFAULT_ATOM_DIRECTION= this is for Zettelkasten like atomic note. + +# LLM Settings +DEFAULT_TEMPERATURE=0.7 +DEFAULT_MAX_TOKENS=8192 +HTTP_TIMEOUT_ASYNC=120.0 +HTTP_TIMEOUT_SYNC=120.0 + +# File Settings +ATOMIC_NOTES_SUFFIX=-atoms diff --git a/usecase/solar-knowledge-management/.gitignore b/usecase/solar-knowledge-management/.gitignore new file mode 100644 index 0000000..8160ada --- /dev/null +++ b/usecase/solar-knowledge-management/.gitignore @@ -0,0 +1,334 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode + +notebooks/ +archive/ +backend/related_note/vector_store/ +backend/related_note/embedded_markers.txt + +### dotenv ### +.env + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### JupyterNotebooks ### +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### venv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode \ No newline at end of file diff --git a/usecase/solar-knowledge-management/.python-version b/usecase/solar-knowledge-management/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/usecase/solar-knowledge-management/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/usecase/solar-knowledge-management/.streamlit/config.toml b/usecase/solar-knowledge-management/.streamlit/config.toml new file mode 100644 index 0000000..32df682 --- /dev/null +++ b/usecase/solar-knowledge-management/.streamlit/config.toml @@ -0,0 +1,2 @@ +[theme] +base="light" diff --git a/usecase/solar-knowledge-management/LICENSE b/usecase/solar-knowledge-management/LICENSE new file mode 100644 index 0000000..0d8ed05 --- /dev/null +++ b/usecase/solar-knowledge-management/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Jaemin Hong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/usecase/solar-knowledge-management/README-KO.md b/usecase/solar-knowledge-management/README-KO.md new file mode 100644 index 0000000..70ac09e --- /dev/null +++ b/usecase/solar-knowledge-management/README-KO.md @@ -0,0 +1,276 @@ +# UpThink + +**[Upstage AI Ambassador]** Personal Knowledge Management with Upstage Solar Pro 2 ✨ + +## Overview + +UpThink는 개인 지식 관리 환경(Obsidian)에서 발생하는 반복적인 수작업 비용을 최소화하는 서비스입니다. \ +지식을 정리하는 과정에서 필연적으로 발생하는 다음의 병목 현상들을 해결합니다. + +| 문제 | 설명 | +|------|------| +| **이미지 데이터 처리** | 시각 정보를 텍스트로 변환하는 수동 작업 | +| **태그 관리** | 태그 컨벤션 유지 및 스타일링 고민 | +| **지식 연결성 부재** | 연관된 과거 노트를 찾기 위한 탐색 비용 | +| **비구조화된 문서** | 방대한 노트 분할의 필요성 | + +UpThink는 **Upstage Solar Pro 2**의 강력한 언어 이해 능력을 기반으로 이러한 과정을 자동화합니다. \ +사용자는 단순 반복 작업에서 벗어나, 가장 중요한 사고 활동에만 몰입할 수 있습니다. + +### Table of Contents + +- [Overview](#overview) +- [Key Features](#key-features) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) + - [Flow Chart](#flow-chart) +- [Installation](#installation) +- [Usage](#usage) + - [시연 영상 보러가기](#-시연-영상-보러가기-youtube-) +- [Project Structure](#project-structure) +- [Members & Roles](#members--roles) +- [Acknowledgements](#acknowledgements) + +## Key Features + +### 1️⃣ 이미지 대체 텍스트 생성 + +노트 내 이미지에서 텍스트를 추출하고, 이미지 내용을 설명하는 대체 텍스트를 자동으로 생성합니다. + +- **Upstage Document Parse**로 이미지에서 OCR 및 문서 구조 추출 +- **Solar Pro 2**로 추출된 텍스트를 바탕으로 50단어 내외의 대체 텍스트 생성 +- 마크다운 파일 내 모든 이미지를 일괄 처리 +- 이미지 링크 `![[image.png]]` 형식의 바로 아래에 자동 삽입 + +### 2️⃣ 태그 추천 + +노트 내용을 분석하여 적절한 태그를 추천하고, 기존 Vault의 태그 컨벤션과 일관성을 유지합니다. + +- Vault 내 기존 태그 자동 수집 (해시태그 `#tag` 및 YAML frontmatter 지원) +- 사용자 정의 태그 가이드라인 설정 (언어, 대소문자, 구분자, 태그 개수) +- **Solar Pro 2**로 노트 내용 기반 태그 생성 +- **Qwen Embedding** 모델로 기존 태그와 유사도 비교 및 매칭 +- YAML frontmatter 형식으로 태그 자동 삽입 + +### 3️⃣ 연관 노트 추천 + +현재 노트와 의미적으로 유사한 노트를 찾아 자동으로 연결합니다. + +- **Upstage Embedding Model**과 **Chroma DB**로 Vault 내 노트 벡터화 +- 임베딩되지 않은 노트 자동 식별 및 일괄 처리 +- 긴 노트도 청킹하여 처리 가능 +- 유사도 검색으로 Top 3 연관 노트 추천 +- 백링크 `[[note]]` 형식으로 `## Related Notes` 섹션에 자동 삽입 + +### 4️⃣ 노트 분할 + +방대한 노트를 주제별로 분리하여 원자화하고 상호 연결된 지식 체계를 구축합니다. + +- **Solar Pro 2**로 노트 내 주제(Topic) 자동 추출 +- 템플릿 기반의 유연한 분할 전략 지원 +- 추출된 주제 편집, 삭제, 추가 가능 +- 분할된 원자 노트 자동 생성 및 저장 +- 원본 노트에 백링크 및 `## Generated Atomic Notes` 섹션 자동 삽입 + +## Tech Stack + +| 분류 | 기술 | +|------|------| +| **Language** | Python 3.13 | +| **Frontend** | Streamlit | +| **LLM** | Upstage Solar Pro 2 | +| **Document AI** | Upstage Document Parse | +| **Embedding** | Upstage Embedding, Qwen3-Embedding-0.6B | +| **Vector DB** | Chroma DB | +| **Framework** | LangChain | +| **Package Manager** | uv | + +## Architecture + +Image +
+
+ +| 레이어 | 구성 요소 | 설명 | +|--------|-----------|------| +| **Frontend** | Streamlit | 웹 기반 사용자 인터페이스 | +| **Backend** | Python 모듈 | 4가지 핵심 기능 구현 | +| **Upstage API** | Solar Pro 2, Document Parse, Embedding Model | LLM, OCR, 벡터 임베딩 | +| **Local** | Qwen Embedding, Chroma DB | 태그 유사도 비교, 노트 벡터 저장 | + +### Flow Chart + +#### 이미지 대체 텍스트 생성 + +Image +
+
+ +| Step | Flow | Backend 주요 모듈 | +|:----:|------|-------------------| +| 1 | 이미지 링크 추출 및 대체 텍스트 존재 여부 확인 | `MarkdownImageProcessor._collect_images_to_process()` | +| 1 | Vault 내 이미지 파일 경로 탐색 | `MarkdownImageProcessor._find_image_in_vault()` | +| 2 | 이미지에서 텍스트 추출 | `OCRProcessor.extract_text()` | +| 2 | 대체 텍스트 생성 | `AltTextGenerator.generate_alt_text()` | +| 3 | 이미지 링크 하단에 대체 텍스트 삽입 | `MarkdownImageProcessor.process_images()` | + +#### 태그 추천 + +Image +
+
+ +| Step | Flow | Backend 주요 모듈 | +|:----:|------|-------------------| +| 1 | 기존 태그 수집 및 확인 | `TagExtractor.get_unique_tags()`, `TagExtractor.count_tags()` | +| 2 | 태그 가이드라인 설정 및 신규 태그 생성 | `GuidelineGenerator()`, `TagGenerator.generate_tags()` | +| 3 | 기존 태그와 신규 태그 비교 | `TagComparator.compare_tags()` | +| 3 | 최종 태그 제안 | `TagComparator.get_final_tags()` | +| 4 | YAML Frontmatter 삽입 | `add_yaml_frontmatter()` | + +#### 연관 노트 추천 + +Image +
+
+ +| Step | Flow | Backend 주요 모듈 | +|:----:|------|-------------------| +| 1 | 임베딩되지 않은 노트 확인 | `Related_Note.get_unembedded_notes()` | +| 2 | 전처리 및 청킹 | `Related_Note.clean_text()`, `Related_Note.chunk_text()` | +| 2 | 노트 임베딩 및 DB 저장 | `Related_Note.index_unembedded_notes()` | +| 3 | 연관 노트 검색 | `Related_Note.find_related_notes()` | +| 4 | 백링크 삽입 | `Related_Note.append_related_links()` | + +#### 노트 분할 + +Image +
+
+ +| Step | Flow | Backend 주요 모듈 | +|:----:|------|-------------------| +| 1 | 프롬프트 템플릿 로드 | `PromptLoader.load_template()` | +| 1 | Topic 추출 | `UpstageClient.generate_with_template_sync()` | +| 2 | Topic 목록 파싱 | `ResponseParser.parse_topics_from_json()` | +| 3 | 원자 노트 생성 | `FileHandler.create_atomic_note()` | +| 3 | 백링크 삽입 | `FileHandler.insert_backlinks()` | + +## Installation + +### 지원 환경 +- macOS +- Windows (PowerShell, CMD) + +### uv 설치 + +- https://docs.astral.sh/uv/getting-started/installation/ + +#### Homebrew + +``` +brew install uv +``` + +#### Windows + +``` +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +### 프로젝트 설정 + +``` +# git clone +git clone https://github.com/geminii01/product-usecase-knowledge-management-upthink.git +cd product-usecase-knowledge-management-upthink +``` +``` +# 환경 변수 설정 (필수!) +cp .env.example .env + +# .env 파일을 열어서 API 키 입력 +# UPSTAGE_API_KEY=your_api_key_here +# TAVILY_API_KEY=your_api_key_here +``` +``` +# Python 3.13과 의존성 자동 설치 +uv sync +``` + +### 실행 + +``` +streamlit run frontend/app.py + +# 아래 Local URL로 접속! +# http://localhost:8501 +``` + +## Usage + +### 🎬 시연 영상 보러가기: [YouTube](https://www.youtube.com/watch?v=8bjLew7KTW4) 🎬 + +### 기본 사용법 + +1. 사이드바에서 **Vault 경로**를 입력합니다. (Obsidian Vault의 절대 경로) +2. 처리할 **Markdown 파일**을 업로드합니다. +3. 원하는 기능 페이지로 이동하여 실행합니다. + +## Project Structure + +``` +upthink/ +├── frontend/ # Streamlit 프론트엔드 +│ ├── app.py # 메인 앱 (라우팅, 공통 사이드바) +│ ├── home.py # 홈 페이지 +│ ├── image_ocr.py # 이미지 대체 텍스트 생성 UI +│ ├── tag_suggest.py # 태그 추천 UI +│ ├── related_note.py # 연관 노트 추천 UI +│ ├── note_split.py # 노트 분할 UI +│ └── note_freshness.py # 최신 정보 확인 UI +│ +├── backend/ # 백엔드 로직 +│ ├── image_ocr/ # 이미지 OCR 및 대체 텍스트 생성 +│ │ ├── ocr_processor.py # Document Parse API 연동 +│ │ ├── alt_text_generator.py # Solar Pro 2 대체 텍스트 생성 +│ │ └── markdown_processor.py # 마크다운 이미지 처리 +│ │ +│ ├── tag_suggest/ # 태그 추천 +│ │ ├── tag_extractor.py # 태그 패턴 2가지 추출 +│ │ ├── tag_guidelines.py # 가이드라인 생성 +│ │ ├── tag_generator.py # Solar Pro 2 태그 생성 +│ │ ├── tag_comparator.py # Qwen Embedding 유사도 비교 +│ │ └── markdown_processor.py # YAML frontmatter 처리 +│ │ +│ ├── related_note/ # 연관 노트 추천 +│ │ └── related_note.py # Chroma DB 기반 유사도 검색 +│ │ +│ ├── note_split/ # 노트 분할 +│ │ ├── config.py # 설정 +│ │ ├── models.py # 데이터 모델 +│ │ ├── core/ # 상태 관리, 파일 처리 +│ │ ├── llm/ # LLM 클라이언트, 프롬프트 로더 +│ │ └── ui/ # UI 컴포넌트 +│ │ +│ └── note_freshness/ # 최신성 검증 +│ ├── api/ # Tavily, Wikipedia API +│ ├── core/ # 상태 관리 +│ └── llm/ # LLM 연동 +│ +├── prompts/ # 프롬프트 템플릿 (YAML) +├── pyproject.toml # 프로젝트 설정 및 의존성 +└── .env.example # 환경 변수 예시 +``` + +## Members & Roles + +| 김수연 | 오주영 | 윤이지 | 홍재민 | +|:------:|:------:|:------:|:------:| +|
|
|
|
| +| ▪︎ 이미지 대체 텍스트 생성 기능 개발 | ▪︎ 노트 분할 기능 개발
▪︎ 최신성 검증 통합 | ▪︎ PM
▪︎ 연관 노트 추천 기능 개발 | ▪︎ 태그 추천 기능 개발
▪︎ GitHub 관리 & 팀 코드 통합 | + +## Acknowledgements + +이 프로젝트는 **Upstage AI Ambassador** 활동의 일환으로 진행되었습니다. \ +프로젝트를 진행할 수 있도록 Credit을 지원해 주신 **[Upstage](https://www.upstage.ai/)** 에 감사드립니다. diff --git a/usecase/solar-knowledge-management/README.md b/usecase/solar-knowledge-management/README.md new file mode 100644 index 0000000..0747989 --- /dev/null +++ b/usecase/solar-knowledge-management/README.md @@ -0,0 +1,276 @@ +# UpThink + +**[Upstage AI Ambassador]** Personal Knowledge Management with Upstage Solar Pro 2 ✨ + +## Overview + +UpThink is a service designed to minimize the repetitive manual effort in Personal Knowledge Management environments (specifically Obsidian). +It addresses the following bottlenecks that inevitably arise during the knowledge organization process. + +| Problem | Description | +|------|------| +| **Image Data Processing** | Manual conversion of visual information into text | +| **Tag Management** | Maintaining tag conventions and styling concerns | +| **Lack of Knowledge Connectivity** | Search costs for finding relevant past notes | +| **Unstructured Documents** | Need for splitting massive notes | + +UpThink automates these processes based on the powerful language understanding capabilities of **Upstage Solar Pro 2**. +Users can break free from simple repetitive tasks and focus on what matters most—thinking. + +### Table of Contents + +- [Overview](#overview) +- [Key Features](#key-features) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) + - [Flow Chart](#flow-chart) +- [Installation](#installation) +- [Usage](#usage) + - [Watch Demo Video](#-watch-demo-video-youtube-) +- [Project Structure](#project-structure) +- [Members & Roles](#members--roles) +- [Acknowledgements](#acknowledgements) + +## Key Features + +### 1️⃣ Image Alt Text Generation + +Extracts text from images within notes and automatically generates alt text describing the image content. + +- Perform OCR and extract document structure from images using **Upstage Document Parse** +- Generate alt text of around 50 words based on extracted text using **Solar Pro 2** +- Batch process all images within markdown files +- Automatically insert alt text below image links in `![[image.png]]` format + +### 2️⃣ Tag Recommendation + +Analyzes note content to recommend appropriate tags and maintains consistency with existing Vault tag conventions. + +- Automatically collect existing tags in Vault (supports hashtags `#tag` and YAML frontmatter) +- Set user-defined tag guidelines (language, case, separators, number of tags) +- Generate tags based on note content using **Solar Pro 2** +- Compare and match similarity with existing tags using **Qwen Embedding** model +- Automatically insert tags in YAML frontmatter format + +### 3️⃣ Related Note Recommendation + +Finds notes semantically similar to the current note and automatically connects them. + +- Vectorize notes in Vault using **Upstage Embedding Model** and **Chroma DB** +- Automatically identify and batch process unembedded notes +- Process long notes through chunking +- Recommend Top 3 related notes via similarity search +- Automatically append backlinks to the `## Related Notes` section using the `[[note]]` format + +### 4️⃣ Note Splitting + +Splits massive notes by topic to atomize them and build an interconnected knowledge system. + +- Automatically extract topics within notes using **Solar Pro 2** +- Support flexible splitting strategies based on templates +- Edit, delete, or add extracted topics +- Automatically generate and save split atomic notes +- Automatically insert backlinks and `## Generated Atomic Notes` section in original note + +## Tech Stack + +| Category | Technology | +|------|------| +| **Language** | Python 3.13 | +| **Frontend** | Streamlit | +| **LLM** | Upstage Solar Pro 2 | +| **Document AI** | Upstage Document Parse | +| **Embedding** | Upstage Embedding, Qwen3-Embedding-0.6B | +| **Vector DB** | Chroma DB | +| **Framework** | LangChain | +| **Package Manager** | uv | + +## Architecture + +Image +
+
+ +| Layer | Component | Description | +|--------|-----------|------| +| **Frontend** | Streamlit | Web-based User Interface | +| **Backend** | Python Modules | Implementation of 4 core features | +| **Upstage API** | Solar Pro 2, Document Parse, Embedding Model | LLM, OCR, Vector Embedding | +| **Local** | Qwen Embedding, Chroma DB | Tag similarity comparison, Note vector storage | + +### Flow Chart + +#### Image Alt Text Generation + +Image +
+
+ +| Step | Flow | Key Backend Modules | +|:----:|------|-------------------| +| 1 | Extract image links and check alt text existence | `MarkdownImageProcessor._collect_images_to_process()` | +| 1 | Search image file paths in Vault | `MarkdownImageProcessor._find_image_in_vault()` | +| 2 | Extract text from image | `OCRProcessor.extract_text()` | +| 2 | Generate alt text | `AltTextGenerator.generate_alt_text()` | +| 3 | Insert alt text below image link | `MarkdownImageProcessor.process_images()` | + +#### Tag Recommendation + +Image +
+
+ +| Step | Flow | Key Backend Modules | +|:----:|------|-------------------| +| 1 | Collect and check existing tags | `TagExtractor.get_unique_tags()`, `TagExtractor.count_tags()` | +| 2 | Set tag guidelines and generate new tags | `GuidelineGenerator()`, `TagGenerator.generate_tags()` | +| 3 | Compare existing and new tags | `TagComparator.compare_tags()` | +| 3 | Suggest final tags | `TagComparator.get_final_tags()` | +| 4 | Insert YAML Frontmatter | `add_yaml_frontmatter()` | + +#### Related Note Recommendation + +Image +
+
+ +| Step | Flow | Key Backend Modules | +|:----:|------|-------------------| +| 1 | Identify unembedded notes | `Related_Note.get_unembedded_notes()` | +| 2 | Preprocessing and chunking | `Related_Note.clean_text()`, `Related_Note.chunk_text()` | +| 2 | Embed notes and save to DB | `Related_Note.index_unembedded_notes()` | +| 3 | Search related notes | `Related_Note.find_related_notes()` | +| 4 | Insert backlinks | `Related_Note.append_related_links()` | + +#### Note Splitting + +Image +
+
+ +| Step | Flow | Key Backend Modules | +|:----:|------|-------------------| +| 1 | Load prompt template | `PromptLoader.load_template()` | +| 1 | Extract Topic | `UpstageClient.generate_with_template_sync()` | +| 2 | Parse Topic list | `ResponseParser.parse_topics_from_json()` | +| 3 | Generate atomic notes | `FileHandler.create_atomic_note()` | +| 3 | Insert backlinks | `FileHandler.insert_backlinks()` | + +## Installation + +### Supported Environments +- macOS +- Windows (PowerShell, CMD) + +### Install uv + +- https://docs.astral.sh/uv/getting-started/installation/ + +#### Homebrew + +``` +brew install uv +``` + +#### Windows + +``` +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +### Project Setup + +``` +# git clone +git clone https://github.com/geminii01/product-usecase-knowledge-management-upthink.git +cd product-usecase-knowledge-management-upthink +``` +``` +# Environment Variable Setup (Required!) +cp .env.example .env + +# Open .env file and enter API keys +# UPSTAGE_API_KEY=your_api_key_here +# TAVILY_API_KEY=your_api_key_here +``` +``` +# Install Python 3.13 and dependencies automatically +uv sync +``` + +### Run + +``` +streamlit run frontend/app.py + +# Access via Local URL below! +# http://localhost:8501 +``` + +## Usage + +### 🎬 Watch Demo Video: [YouTube](https://www.youtube.com/watch?v=8bjLew7KTW4) 🎬 + +### Basic Usage + +1. Enter **Vault Path** in the sidebar. (Absolute path to Obsidian Vault) +2. Upload **Markdown file** to process. +3. Go to the desired feature page and execute. + +## Project Structure + +``` +upthink/ +├── frontend/ # Streamlit Frontend +│ ├── app.py # Main App (Routing, Common Sidebar) +│ ├── home.py # Home Page +│ ├── image_ocr.py # Image Alt Text Generation UI +│ ├── tag_suggest.py # Tag Recommendation UI +│ ├── related_note.py # Related Note Recommendation UI +│ ├── note_split.py # Note Splitting UI +│ └── note_freshness.py # Freshness Check UI +│ +├── backend/ # Backend Logic +│ ├── image_ocr/ # Image OCR & Alt Text Generation +│ │ ├── ocr_processor.py # Document Parse API Integration +│ │ ├── alt_text_generator.py # Solar Pro 2 Alt Text Generation +│ │ └── markdown_processor.py # Markdown Image Processing +│ │ +│ ├── tag_suggest/ # Tag Suggestion +│ │ ├── tag_extractor.py # Extract 2 Tag Patterns +│ │ ├── tag_guidelines.py # Guideline Generation +│ │ ├── tag_generator.py # Solar Pro 2 Tag Generation +│ │ ├── tag_comparator.py # Qwen Embedding Similarity Comparison +│ │ └── markdown_processor.py # YAML frontmatter Processing +│ │ +│ ├── related_note/ # Related Note Recommendation +│ │ └── related_note.py # Chroma DB based Similarity Search +│ │ +│ ├── note_split/ # Note Splitting +│ │ ├── config.py # Config +│ │ ├── models.py # Data Models +│ │ ├── core/ # State Management, File Handling +│ │ ├── llm/ # LLM Client, Prompt Loader +│ │ └── ui/ # UI Components +│ │ +│ └── note_freshness/ # Freshness Verification +│ ├── api/ # Tavily, Wikipedia API +│ ├── core/ # State Management +│ └── llm/ # LLM Integration +│ +├── prompts/ # Prompt Templates (YAML) +├── pyproject.toml # Project Configuration & Dependencies +└── .env.example # Environment Variable Example +``` + +## Members & Roles + +| Kim Su-yeon | Oh Ju-yeong | Yoon I-ji | Hong Jae-min | +|:------:|:------:|:------:|:------:| +|

|
|
|
| +| ▪︎ Developed Image Alt Text Generation Feature | ▪︎ Developed Note Splitting Feature
▪︎ Integrated Freshness Verification | ▪︎ PM
▪︎ Developed Related Note Recommendation Feature | ▪︎ Developed Tag Recommendation Feature
▪︎ GitHub Management & Team Code Integration | + +## Acknowledgements + +This project was conducted as part of the **Upstage AI Ambassador** program. \ +We thank **[Upstage](https://www.upstage.ai/)** for providing credits to support this project. diff --git a/usecase/solar-knowledge-management/backend/__init__.py b/usecase/solar-knowledge-management/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usecase/solar-knowledge-management/backend/image_ocr/__init__.py b/usecase/solar-knowledge-management/backend/image_ocr/__init__.py new file mode 100644 index 0000000..7769530 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/image_ocr/__init__.py @@ -0,0 +1,9 @@ +from .ocr_processor import OCRProcessor +from .alt_text_generator import AltTextGenerator +from .markdown_processor import MarkdownImageProcessor + +__all__ = [ + "OCRProcessor", + "AltTextGenerator", + "MarkdownImageProcessor", +] diff --git a/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py b/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py new file mode 100644 index 0000000..1dde161 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py @@ -0,0 +1,105 @@ +""" +LLM을 사용하여 OCR 텍스트로부터 대체 텍스트 생성 +""" + +import os +from openai import OpenAI +from openai import APIError as OpenAIAPIError +from dotenv import load_dotenv + +load_dotenv() + + +class AltTextGenerator: + """OCR 추출 텍스트를 solar-pro2 LLM에 입력하여 대체 텍스트를 생성""" + + def __init__( + self, + model: str = "solar-pro2", + max_tokens: int = 150, + ): + """ + AltTextGenerator 초기화 + + Args: + model: 사용할 LLM 모델 (기본값: "solar-pro2") + max_tokens: 최대 토큰 수 (기본값: 150) + + Raises: + ValueError: UPSTAGE_API_KEY 환경 변수가 설정되지 않은 경우 + """ + api_key = os.getenv("UPSTAGE_API_KEY") + if not api_key: + raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다") + + self.client = OpenAI( + api_key=api_key, + base_url="https://api.upstage.ai/v1", + ) + self.model = model + self.max_tokens = max_tokens + + def generate_alt_text(self, extracted_text: str) -> str: + """ + 추출된 텍스트를 solar-pro2 LLM에 입력하여 대체 텍스트를 생성합니다. + + Args: + extracted_text: OCR로 추출된 텍스트 + + Returns: + 생성된 대체 텍스트 (오류 시 ERROR 메시지) + + Raises: + OpenAIAPIError: LLM API 호출 오류 + Exception: 기타 처리 오류 + """ + if not extracted_text: + error_msg = "ERROR: OCR에서 추출된 텍스트가 비어있습니다. 이미지에 텍스트가 없거나 OCR이 실패했을 수 있습니다." + print(f"[WARNING] {error_msg}") + return error_msg + + if extracted_text.startswith("ERROR"): + return extracted_text + + try: + # LLM 프롬프트 + prompt_text: str = ( + f"다음은 이미지에서 추출된 텍스트와 문서 구조 분석 결과입니다.\n\n---\n{extracted_text}\n---\n\n" + f"이 내용을 바탕으로 이미지의 내용을 상세히 설명하고, 이 설명을 시각 장애인을 위한 **대체 텍스트**로 " + f"50단어 내외의 한국어로 생성해주세요. 오직 생성된 대체 텍스트만 출력하세요." + ) + + # solar-pro2 LLM 호출 (텍스트 전용) + response = self.client.chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": prompt_text}], + max_tokens=self.max_tokens, + ) + + return response.choices[0].message.content.strip() + + except OpenAIAPIError as e: + error_msg = f"ERROR: LLM 추론 API 오류 (HTTP {e.response.status_code})" + print(f"[ERROR] {error_msg}") + print(f"[ERROR] 응답 상세: {e.response.text}") + return f"{error_msg}: {e.response.text[:100]}" + except Exception as e: + error_msg = f"ERROR: LLM Processing Failed - {str(e)}" + print(f"[ERROR] {error_msg}") + return error_msg + + +if __name__ == "__main__": + # 테스트 코드 + generator = AltTextGenerator() + + # 테스트용 OCR 추출 텍스트 + test_extracted_text = """ + Machine Learning 개요 + - 지도 학습: 레이블이 있는 데이터로 학습 + - 비지도 학습: 레이블이 없는 데이터로 패턴 발견 + - 강화 학습: 보상을 통한 학습 + """ + + result = generator.generate_alt_text(test_extracted_text) + print(f"[INFO] 생성된 대체 텍스트: {result}") diff --git a/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py b/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py new file mode 100644 index 0000000..f475122 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py @@ -0,0 +1,215 @@ +""" +마크다운 파일 내 이미지 처리 모듈 +""" + +import re +from pathlib import Path +from typing import List, Dict, Callable, Optional + +from .ocr_processor import OCRProcessor +from .alt_text_generator import AltTextGenerator + + +class MarkdownImageProcessor: + """마크다운 파일에서 이미지를 찾아 대체 텍스트를 생성하고 업데이트 (Obsidian 전용)""" + + # Obsidian 위키링크 형식: ![[image.png]] + OBSIDIAN_IMAGE_PATTERN = re.compile( + r"!\[\[(?P[^\]]+\.(png|jpg|jpeg))\]\]", re.IGNORECASE + ) + + def __init__(self): + """MarkdownImageProcessor 초기화""" + self.ocr_processor = OCRProcessor() + self.alt_text_generator = AltTextGenerator() + self._image_cache = {} # 이미지 파일명 -> 경로 캐시 + + def _build_image_cache(self, vault_root: Path) -> None: + """ + Vault 전체에서 이미지 파일을 재귀적으로 검색하여 캐시 생성 + + Args: + vault_root: Vault 루트 경로 + """ + self._image_cache = {} + + # 지원하는 이미지 확장자 + image_extensions = {".png", ".jpg", ".jpeg"} + + # Vault 전체를 재귀적으로 검색 + for image_path in vault_root.rglob("*"): + if image_path.is_file() and image_path.suffix.lower() in image_extensions: + filename = image_path.name + # 같은 파일명이 여러 개 있을 경우, 첫 번째 것을 사용 + if filename not in self._image_cache: + self._image_cache[filename] = image_path + + print( + f"[INFO] Vault에서 {len(self._image_cache)}개의 이미지 파일을 찾았습니다." + ) + + def _find_image_in_vault(self, filename: str, vault_root: Path) -> Optional[Path]: + """ + Vault 전체에서 파일명으로 이미지 찾기 + + Args: + filename: 이미지 파일명 + vault_root: Vault 루트 경로 + + Returns: + 이미지 파일 경로 (없으면 None) + """ + # 캐시가 비어있으면 캐시 생성 + if not self._image_cache: + self._build_image_cache(vault_root) + + return self._image_cache.get(filename) + + def process_images( + self, + md_content: str, + vault_root: Path, + progress_callback: Optional[Callable[[int, int, str], None]] = None, + ) -> tuple[str, List[Dict]]: + """ + 마크다운 내용에서 이미지를 찾아 대체 텍스트를 생성하고 업데이트 + + Args: + md_content: 원본 마크다운 내용 + vault_root: Vault 루트 경로 (이미지 파일을 찾을 기준 폴더) + progress_callback: 진행 상황 콜백 함수 (현재 인덱스, 전체 개수, 이미지 경로) + + Returns: + (업데이트된 마크다운 내용, 처리된 이미지 정보 리스트) + """ + # 1. 대체 텍스트가 없는 이미지 수집 + images_to_process = self._collect_images_to_process(md_content, vault_root) + + if not images_to_process: + return md_content, [] + + # 2. 각 이미지에 대해 OCR + LLM 대체 텍스트 생성 + new_content = md_content + offset = 0 + processed_images = [] + + for i, img_data in enumerate(images_to_process): + # 진행 상황 콜백 호출 + if progress_callback: + progress_callback(i + 1, len(images_to_process), img_data["src"]) + + # 1단계: OCR/Parse 텍스트 추출 + extracted_text = self.ocr_processor.extract_text(img_data["path"]) + + # 2단계: LLM 추론으로 대체 텍스트 생성 + if "ERROR" in extracted_text: + new_alt_text = extracted_text + else: + new_alt_text = self.alt_text_generator.generate_alt_text(extracted_text) + + # 텍스트 삽입 로직: 이미지 링크 아래에 대체 텍스트 추가 + match = img_data["match_object"] + original_string = match.group(0) + + # Obsidian 위키링크 형식 유지 + 아래에 코드 블록으로 대체 텍스트 추가 + replacement_string = f"{original_string}\n```\n(대체 텍스트 by Upstage)\n{new_alt_text}\n```\n" + + start = match.start() + offset + end = match.end() + offset + + new_content = new_content[:start] + replacement_string + new_content[end:] + offset += len(replacement_string) - len(original_string) + + # 처리된 이미지 정보 저장 + processed_images.append( + { + "src": img_data["src"], + "new_alt_text": new_alt_text, + } + ) + + return new_content, processed_images + + def _collect_images_to_process( + self, md_content: str, vault_root: Path + ) -> List[Dict]: + """ + 마크다운 내용에서 Obsidian 위키링크 형식의 이미지를 수집 + + Args: + md_content: 마크다운 내용 + vault_root: Vault 루트 경로 + + Returns: + 처리할 이미지 정보 리스트 + """ + images_to_process = [] + + # Obsidian 위키링크 형식 매칭: ![[image.png]] + for match in self.OBSIDIAN_IMAGE_PATTERN.finditer(md_content): + filename = match.group("filename").strip() + + # 다음 줄에 코드 블록 "```\n(대체 텍스트 by Upstage)"가 있으면 이미 처리된 것으로 간주 + match_end = match.end() + remaining_content = md_content[match_end:] + + if remaining_content.startswith("\n```\n(대체 텍스트 by Upstage)"): + print(f"[INFO] '{filename}'은 이미 대체 텍스트가 있습니다. 건너뜁니다.") + continue + + # Vault 전체에서 이미지 파일 찾기 + image_full_path = self._find_image_in_vault(filename, vault_root) + + if image_full_path: + images_to_process.append( + { + "src": filename, + "path": image_full_path, + "match_object": match, + } + ) + else: + print( + f"[WARNING] Vault 경로에서 이미지 파일 '{filename}'을 찾을 수 없습니다." + ) + + return images_to_process + + +if __name__ == "__main__": + # 테스트 코드 + from dotenv import load_dotenv + + load_dotenv() + + processor = MarkdownImageProcessor() + + # 테스트용 마크다운 내용 (Obsidian 위키링크 형식) + test_md_content = """ +# 테스트 노트 + +이것은 테스트 이미지입니다. + +![[test.png]] +사용자가 작성한 노트 내용... + +다른 이미지: + +![[diagram.jpg]] +이미 작성된 내용이 있어도 대체 텍스트는 코드 블록으로 구분됩니다. +""" + + test_vault_path = Path("/path/to/vault") + + def test_progress_callback(current: int, total: int, img_src: str): + print(f"[PROGRESS] {current}/{total} - {img_src}") + + # 처리 실행 + if test_vault_path.exists(): + updated_content, processed = processor.process_images( + test_md_content, test_vault_path, test_progress_callback + ) + print(f"\n[INFO] 업데이트된 내용:\n{updated_content}") + print(f"\n[INFO] 처리된 이미지: {len(processed)}개") + else: + print(f"[WARNING] 테스트 Vault 경로를 찾을 수 없습니다: {test_vault_path}") diff --git a/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py b/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py new file mode 100644 index 0000000..fdcb09d --- /dev/null +++ b/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py @@ -0,0 +1,128 @@ +""" +OCR 및 문서 구조 추출 API 호출 모듈 +""" + +import os +import requests +from pathlib import Path +from typing import Dict, Optional +from dotenv import load_dotenv + +load_dotenv() + + +class OCRProcessor: + """Upstage Document Parse API를 사용하여 이미지에서 OCR 텍스트 및 구조를 추출""" + + def __init__(self): + """ + OCRProcessor 초기화 + + Raises: + ValueError: UPSTAGE_API_KEY 환경 변수가 설정되지 않은 경우 + """ + self.api_key: Optional[str] = os.getenv("UPSTAGE_API_KEY") + if not self.api_key: + raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다") + + self.ocr_endpoint: str = "https://api.upstage.ai/v1/document-digitization" + + def extract_text(self, image_path: Path) -> str: + """ + Upstage Document Parse API를 사용하여 이미지에서 OCR 텍스트 및 구조를 추출합니다. + + Args: + image_path: 추출할 이미지 파일 경로 + + Returns: + 추출된 텍스트 (오류 시 ERROR 메시지) + + Raises: + requests.exceptions.HTTPError: HTTP 요청 실패 시 + Exception: 기타 처리 오류 시 + """ + try: + # 요청 헤더 구성 + headers: Dict[str, str] = {"Authorization": f"Bearer {self.api_key}"} + mime_type = "image/" + image_path.suffix.lstrip(".") + + # 파일을 with 문으로 안전하게 열기 + with open(image_path, "rb") as image_file: + files = { + "document": ( + image_path.name, + image_file, + mime_type, + ) + } + + # 공식 데이터 파라미터 + data_payload: Dict = { + "model": "document-parse", + "ocr": "force", # OCR 강제 실행 + "output_formats": "['markdown', 'text']", # 출력 형식 명시 + "base64_encoding": "['table', 'figure']", # 테이블과 이미지 포함 + } + + response = requests.post( + self.ocr_endpoint, + headers=headers, + files=files, + data=data_payload, + timeout=120, + ) + + response.raise_for_status() + response_json: Dict = response.json() + + # Document Parse 결과에서 추출된 텍스트를 반환 + # 1. 최상위 content 객체에서 text 추출 (우선순위: text > markdown) + content = response_json.get("content", {}) + extracted_text = content.get("text", "").strip() or content.get("markdown", "").strip() + + # 2. content가 없으면 elements 배열에서 추출 + if not extracted_text and "elements" in response_json: + elements = response_json.get("elements", []) + text_parts = [] + + for element in elements: + element_content = element.get("content", {}) + # 각 element의 텍스트 추출 (우선순위: text > markdown) + element_text = element_content.get("text", "").strip() or element_content.get("markdown", "").strip() + + if element_text: + text_parts.append(element_text) + + extracted_text = "\n\n".join(text_parts) + + # 디버깅: 추출된 텍스트 길이 출력 + print(f"[INFO] OCR 결과: {len(extracted_text)}자 추출 (파일: {image_path.name})") + if not extracted_text: + print(f"[WARNING] OCR이 빈 텍스트를 반환했습니다.") + print(f"[DEBUG] API 응답 구조: content={bool(content)}, elements={len(response_json.get('elements', []))}개") + + return extracted_text + + except requests.exceptions.HTTPError as e: + error_msg = ( + f"ERROR: OCR/Parse API 요청 실패 (HTTP {e.response.status_code})" + ) + print(f"[ERROR] {error_msg}") + print(f"[ERROR] 응답 상세: {e.response.text}") + return f"{error_msg}: {e.response.text[:100]}" + except Exception as e: + error_msg = f"ERROR: OCR Processing Failed - {str(e)}" + print(f"[ERROR] {error_msg}") + return error_msg + + +if __name__ == "__main__": + # 테스트 코드 + processor = OCRProcessor() + test_image_path = Path("test_image.png") + + if test_image_path.exists(): + result = processor.extract_text(test_image_path) + print(f"[INFO] OCR 결과: {result[:200]}...") + else: + print(f"[WARNING] 테스트 이미지 파일을 찾을 수 없습니다: {test_image_path}") diff --git a/usecase/solar-knowledge-management/backend/note_freshness/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/__init__.py new file mode 100644 index 0000000..fb0f408 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/__init__.py @@ -0,0 +1 @@ +"""Note freshness check module for checking and updating note recency.""" diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py new file mode 100644 index 0000000..17de443 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py @@ -0,0 +1,6 @@ +"""External API clients for note freshness module.""" + +from .wikipedia import WikipediaClient +from .tavily import TavilyClient + +__all__ = ["WikipediaClient", "TavilyClient"] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py b/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py new file mode 100644 index 0000000..7398ef0 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py @@ -0,0 +1,105 @@ +"""Tavily Search API client.""" + +import httpx +from typing import Optional, Dict, Any, List +from datetime import datetime +from ..config import Config + + +class TavilyClient: + """Client for Tavily Search API.""" + + BASE_URL = "https://api.tavily.com/search" + + def __init__(self, api_key: Optional[str] = None): + """Initialize Tavily client. + + Args: + api_key: Tavily API key (defaults to Config.TAVILY_API_KEY) + """ + self.api_key = api_key or Config.TAVILY_API_KEY + if not self.api_key: + raise ValueError("TAVILY_API_KEY is required") + + def search( + self, + query: str, + search_depth: str = "basic", + max_results: int = 5, + include_answer: bool = False, + include_raw_content: bool = False, + ) -> Optional[Dict[str, Any]]: + """Search using Tavily API. + + Args: + query: Search query + search_depth: "basic" or "advanced" + max_results: Maximum number of results + include_answer: Include AI-generated answer + include_raw_content: Include raw page content + + Returns: + Search results dictionary or None on error + """ + payload = { + "api_key": self.api_key, + "query": query, + "search_depth": search_depth, + "max_results": max_results, + "include_answer": include_answer, + "include_raw_content": include_raw_content, + } + + try: + with httpx.Client(timeout=60.0) as client: + response = client.post(self.BASE_URL, json=payload) + response.raise_for_status() + data = response.json() + + return { + "query": query, + "results": data.get("results", []), + "answer": data.get("answer", ""), + "searched_at": datetime.now().isoformat(), + } + except httpx.HTTPStatusError as e: + print(f"HTTP error during Tavily search: {e}") + return None + except Exception as e: + print(f"Error during Tavily search: {e}") + return None + + def search_and_parse( + self, query: str, max_results: int = 3 + ) -> Optional[Dict[str, Any]]: + """Search and parse results for freshness check. + + Args: + query: Search query + max_results: Maximum results to return + + Returns: + Parsed search results with query, results, and searched_at + """ + result = self.search(query, max_results=max_results) + if not result: + return None + + # Parse and limit results + parsed_results = [] + for item in result.get("results", [])[:max_results]: + parsed_results.append( + { + "title": item.get("title", ""), + "url": item.get("url", ""), + "content": item.get("content", ""), + "score": item.get("score", 0), + "published_date": item.get("published_date", ""), + } + ) + + return { + "query": query, + "results": parsed_results, + "searched_at": result["searched_at"], + } diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py b/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py new file mode 100644 index 0000000..726a849 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py @@ -0,0 +1,86 @@ +"""Wikipedia API client for searching and retrieving articles.""" + +import wikipedia +from typing import Optional, Dict, Any +from datetime import datetime + + +class WikipediaClient: + """Client for Wikipedia API using wikipedia package.""" + + def __init__(self, language: str = "ko"): + """Initialize Wikipedia client. + + Args: + language: Wikipedia language code (default: ko for Korean) + """ + self.language = language + wikipedia.set_lang(language) + + def search_and_get_summary(self, keyword: str) -> Optional[Dict[str, Any]]: + """Search for a keyword and get the summary of the top result. + + Args: + keyword: Search keyword + + Returns: + Dictionary with keyword, title, summary, url, and searched_at + """ + result = { + "keyword": keyword, + "wiki_exists": False, + "title": "", + "summary": "Wikipedia에서 해당 키워드에 대한 문서 요약 정보를 찾지 못함", + "url": "", + "searched_at": datetime.now().isoformat(), + } + + try: + # Search for keyword (get top 1 result) + page_titles = wikipedia.search(keyword, results=1) + + if not page_titles: + print(f"문서 미존재: '{keyword}' 검색 결과가 없습니다.") + return result + + page_title = page_titles[0] + + # Get page summary + page_summary = wikipedia.summary( + page_title, sentences=3, auto_suggest=False + ) + + # Get page for URL + try: + page = wikipedia.page(page_title, auto_suggest=False) + url = page.url + except Exception: + url = f"https://{self.language}.wikipedia.org/wiki/{page_title.replace(' ', '_')}" + + # Update result + result["wiki_exists"] = True + result["title"] = page_title + result["summary"] = page_summary + result["url"] = url + + print(f"문서 존재: '{page_title}'") + return result + + except wikipedia.exceptions.PageError: + print(f"문서 미존재: '{keyword}' - 제목이 정확히 일치하는 문서가 없습니다.") + return result + except wikipedia.exceptions.DisambiguationError as e: + print(f"모호성: '{keyword}' - 여러 문서가 검색됨. 옵션: {e.options[:3]}") + result["wiki_exists"] = True + result["title"] = keyword + result["summary"] = ( + f"모호성 해소 필요: 다음 중 하나를 선택해야 합니다: {e.options[:3]}" + ) + result["url"] = ( + f"https://{self.language}.wikipedia.org/wiki/{keyword.replace(' ', '_')}" + ) + return result + except Exception as e: + print(f"Wikipedia API 오류 발생: {e}") + result["summary"] = f"Wikipedia API 처리 중 오류 발생: {e}" + return result diff --git a/usecase/solar-knowledge-management/backend/note_freshness/config.py b/usecase/solar-knowledge-management/backend/note_freshness/config.py new file mode 100644 index 0000000..20806cb --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/config.py @@ -0,0 +1,81 @@ +"""Configuration management for note freshness module.""" + +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + + +def _get_project_root() -> Path: + """Get the project root directory.""" + current = Path(__file__).resolve() + for level in range(4): + candidate = current.parents[level] + if (candidate / "pyproject.toml").exists(): + return candidate + return current.parent.parent.parent.parent + + +PROJECT_ROOT = _get_project_root() + + +class Config: + """Application configuration manager for note freshness.""" + + # Upstage API settings + UPSTAGE_API_KEY: str = os.getenv("UPSTAGE_API_KEY", "") + UPSTAGE_API_BASE: str = os.getenv( + "UPSTAGE_API_BASE", "https://api.upstage.ai/v1/solar" + ) + MODEL_NAME: str = os.getenv("MODEL_NAME", "solar-pro2") + + # Information Extraction API + UPSTAGE_IE_API_BASE: str = "https://api.upstage.ai/v1/information-extraction" + + # Tavily API + TAVILY_API_KEY: str = os.getenv("TAVILY_API_KEY", "") + + # Application settings + PROMPTS_DIR: Path = Path(os.getenv("PROMPTS_DIR", str(PROJECT_ROOT / "prompts"))) + DATA_DIR: Path = PROJECT_ROOT / "data" + + # LLM settings + DEFAULT_TEMPERATURE: float = float(os.getenv("DEFAULT_TEMPERATURE", "0.7")) + DEFAULT_MAX_TOKENS: int = int(os.getenv("DEFAULT_MAX_TOKENS", "8192")) + + # HTTP client timeout settings + HTTP_TIMEOUT_ASYNC: float = float(os.getenv("HTTP_TIMEOUT_ASYNC", "120.0")) + HTTP_TIMEOUT_SYNC: float = float(os.getenv("HTTP_TIMEOUT_SYNC", "120.0")) + + @classmethod + def validate(cls) -> bool: + """Validate that required configuration is present.""" + if not cls.UPSTAGE_API_KEY: + return False + return True + + @classmethod + def validate_tavily(cls) -> bool: + """Validate Tavily API key.""" + return bool(cls.TAVILY_API_KEY) + + @classmethod + def get_freshness_folder(cls, raw_note_path: Path) -> Path: + """Generate the folder path for freshness data. + + Args: + raw_note_path: Path to the raw note file + + Returns: + Path to the freshness data folder + """ + parent = raw_note_path.parent + stem = raw_note_path.stem + return parent / stem + + @classmethod + def ensure_directories(cls): + """Ensure required directories exist.""" + cls.PROMPTS_DIR.mkdir(parents=True, exist_ok=True) + cls.DATA_DIR.mkdir(parents=True, exist_ok=True) diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py new file mode 100644 index 0000000..59c506c --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py @@ -0,0 +1,7 @@ +"""Core utilities for note freshness module.""" + +from .path_utils import resolve_path, format_path_for_display +from .state_manager import StateManager +from .file_handler import FileHandler + +__all__ = ["resolve_path", "format_path_for_display", "StateManager", "FileHandler"] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py b/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py new file mode 100644 index 0000000..87e0d8a --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py @@ -0,0 +1,233 @@ +"""File handling operations for note freshness.""" + +import re +import yaml +from pathlib import Path +from typing import Optional, Tuple, Union +from datetime import datetime +from ..models import FreshnessMetadata +from .path_utils import resolve_path + + +class FileHandler: + """Handler for file operations related to note freshness.""" + + @staticmethod + def _resolve_path(path: Union[str, Path]) -> Path: + return resolve_path(path) + + @staticmethod + def read_note( + note_path: Union[str, Path], + ) -> Tuple[Optional[str], Optional[FreshnessMetadata]]: + """Read a markdown note file and extract metadata. + + Args: + note_path: Path to the note file + + Returns: + Tuple of (full content as string, FreshnessMetadata) or (None, None) on error + """ + try: + resolved_path = FileHandler._resolve_path(note_path) + with open(resolved_path, "r", encoding="utf-8") as f: + content = f.read() + + # Extract YAML front matter + metadata = FileHandler._extract_yaml_metadata(content) + return content, metadata + except FileNotFoundError: + print(f"File not found: {note_path}") + return None, None + except Exception as e: + print(f"Error reading file {note_path}: {e}") + return None, None + + @staticmethod + def _extract_yaml_metadata(content: str) -> FreshnessMetadata: + """Extract freshness-related metadata from YAML front matter.""" + metadata = FreshnessMetadata() + + # Match YAML front matter + yaml_match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL) + if yaml_match: + try: + yaml_content = yaml.safe_load(yaml_match.group(1)) + if yaml_content: + # Extract info_keyword + info_keyword = yaml_content.get("info_keyword", []) + if isinstance(info_keyword, str): + info_keyword = [info_keyword] + metadata.info_keyword = info_keyword + + # Extract info_query + info_query = yaml_content.get("info_query", []) + if isinstance(info_query, str): + info_query = [info_query] + metadata.info_query = info_query + + # Extract search timestamps + metadata.wiki_searched_at = yaml_content.get("wiki_searched_at") + metadata.tavily_searched_at = yaml_content.get("tavily_searched_at") + except yaml.YAMLError as e: + print(f"Error parsing YAML metadata: {e}") + + return metadata + + @staticmethod + def update_note_metadata( + note_path: Union[str, Path], + info_keyword: list = None, + info_query: list = None, + wiki_searched_at: str = None, + tavily_searched_at: str = None, + ) -> bool: + """Update the YAML front matter of a note with freshness metadata. + + Args: + note_path: Path to the note file + info_keyword: List of keywords for Wikipedia search + info_query: List of queries for Tavily search + wiki_searched_at: Timestamp of Wikipedia search + tavily_searched_at: Timestamp of Tavily search + + Returns: + True if successful, False otherwise + """ + try: + resolved_path = FileHandler._resolve_path(note_path) + with open(resolved_path, "r", encoding="utf-8") as f: + content = f.read() + + # Check if YAML front matter exists + yaml_match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL) + + if yaml_match: + # Parse existing YAML + try: + yaml_content = yaml.safe_load(yaml_match.group(1)) or {} + except yaml.YAMLError: + yaml_content = {} + + # Update metadata + if info_keyword is not None: + yaml_content["info_keyword"] = info_keyword + if info_query is not None: + yaml_content["info_query"] = info_query + if wiki_searched_at is not None: + yaml_content["wiki_searched_at"] = wiki_searched_at + if tavily_searched_at is not None: + yaml_content["tavily_searched_at"] = tavily_searched_at + + # Reconstruct content with updated YAML + new_yaml = yaml.dump( + yaml_content, + allow_unicode=True, + default_flow_style=False, + sort_keys=False, + ) + new_content = f"---\n{new_yaml}---{content[yaml_match.end():]}" + else: + # Create new YAML front matter + yaml_content = {} + if info_keyword is not None: + yaml_content["info_keyword"] = info_keyword + if info_query is not None: + yaml_content["info_query"] = info_query + if wiki_searched_at is not None: + yaml_content["wiki_searched_at"] = wiki_searched_at + if tavily_searched_at is not None: + yaml_content["tavily_searched_at"] = tavily_searched_at + + new_yaml = yaml.dump( + yaml_content, + allow_unicode=True, + default_flow_style=False, + sort_keys=False, + ) + new_content = f"---\n{new_yaml}---\n\n{content}" + + # Write back to file + with open(resolved_path, "w", encoding="utf-8") as f: + f.write(new_content) + return True + except Exception as e: + print(f"Error updating note metadata: {e}") + return False + + @staticmethod + def save_search_result( + save_folder: Union[str, Path], filename: str, content: str + ) -> bool: + """Save search result to a markdown file. + + Args: + save_folder: Folder to save the result + filename: Name of the file (without extension) + content: Markdown content to save + + Returns: + True if successful, False otherwise + """ + try: + resolved_folder = FileHandler._resolve_path(save_folder) + resolved_folder.mkdir(parents=True, exist_ok=True) + + file_path = resolved_folder / f"{filename}.md" + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + return True + except Exception as e: + print(f"Error saving search result: {e}") + return False + + @staticmethod + def insert_freshness_guide( + note_path: Union[str, Path], guide_summary: str, full_guide_path: str + ) -> bool: + """Insert freshness guide summary at the top of the note (after YAML). + + Args: + note_path: Path to the note file + guide_summary: Summary of the freshness guide + full_guide_path: Relative path to the full guide + + Returns: + True if successful, False otherwise + """ + try: + resolved_path = FileHandler._resolve_path(note_path) + with open(resolved_path, "r", encoding="utf-8") as f: + content = f.read() + + # Find end of YAML front matter + yaml_match = re.match(r"^---\s*\n.*?\n---\s*\n?", content, re.DOTALL) + + # Create the guide block + guide_block = f""" +> [!info] 최신성 검토 가이드 +> {guide_summary} +> +> [[{full_guide_path}|전체 가이드 보기]] + +""" + + if yaml_match: + # Insert after YAML + insert_pos = yaml_match.end() + new_content = content[:insert_pos] + guide_block + content[insert_pos:] + else: + # Insert at the beginning + new_content = guide_block + content + + with open(resolved_path, "w", encoding="utf-8") as f: + f.write(new_content) + return True + except Exception as e: + print(f"Error inserting freshness guide: {e}") + return False + + @staticmethod + def get_current_timestamp() -> str: + """Get current timestamp in ISO format.""" + return datetime.now().isoformat() diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py b/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py new file mode 100644 index 0000000..1ae68fb --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py @@ -0,0 +1,9 @@ +"""Path utilities - re-export from note_split.""" + +from backend.note_split.core.path_utils import ( + resolve_path, + format_path_for_display, + normalize_path_for_wsl, +) + +__all__ = ["resolve_path", "format_path_for_display", "normalize_path_for_wsl"] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py b/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py new file mode 100644 index 0000000..e1de3a9 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py @@ -0,0 +1,182 @@ +"""State management for Streamlit session state.""" + +from typing import List, Optional +import streamlit as st +from pathlib import Path +from ..models import FreshnessMetadata, DescriptionTemplate +from .path_utils import resolve_path + + +class StateManager: + """Manager for Streamlit session state for note freshness.""" + + # State keys + KEY_RAW_NOTE_PATH = "freshness_raw_note_path" + KEY_SAVE_FOLDER_PATH = "freshness_save_folder_path" + KEY_RAW_NOTE_CONTENT = "freshness_raw_note_content" + KEY_METADATA = "freshness_metadata" + KEY_INFO_KEYWORD = "freshness_info_keyword" + KEY_INFO_QUERY = "freshness_info_query" + KEY_DESCRIPTION_TEMPLATE = "freshness_desc_template" + KEY_WIKI_RESULTS = "freshness_wiki_results" + KEY_TAVILY_RESULTS = "freshness_tavily_results" + KEY_STEP = "freshness_current_step" + + # Step identifiers + STEP_INIT = "init" + STEP_NOTE_VALIDATED = "note_validated" + STEP_TEMPLATE_SELECT = "template_select" + STEP_EXTRACTION_DONE = "extraction_done" + STEP_METADATA_CONFIRMED = "metadata_confirmed" + STEP_SEARCH_DONE = "search_done" + STEP_GUIDE_GENERATED = "guide_generated" + + @staticmethod + def initialize(): + """Initialize session state with default values.""" + if StateManager.KEY_STEP not in st.session_state: + st.session_state[StateManager.KEY_STEP] = StateManager.STEP_INIT + + if StateManager.KEY_RAW_NOTE_PATH not in st.session_state: + st.session_state[StateManager.KEY_RAW_NOTE_PATH] = None + + if StateManager.KEY_SAVE_FOLDER_PATH not in st.session_state: + st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = None + + if StateManager.KEY_RAW_NOTE_CONTENT not in st.session_state: + st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = None + + if StateManager.KEY_METADATA not in st.session_state: + st.session_state[StateManager.KEY_METADATA] = None + + if StateManager.KEY_INFO_KEYWORD not in st.session_state: + st.session_state[StateManager.KEY_INFO_KEYWORD] = [] + + if StateManager.KEY_INFO_QUERY not in st.session_state: + st.session_state[StateManager.KEY_INFO_QUERY] = [] + + if StateManager.KEY_DESCRIPTION_TEMPLATE not in st.session_state: + st.session_state[StateManager.KEY_DESCRIPTION_TEMPLATE] = None + + if StateManager.KEY_WIKI_RESULTS not in st.session_state: + st.session_state[StateManager.KEY_WIKI_RESULTS] = [] + + if StateManager.KEY_TAVILY_RESULTS not in st.session_state: + st.session_state[StateManager.KEY_TAVILY_RESULTS] = [] + + @staticmethod + def get_current_step() -> str: + return st.session_state.get(StateManager.KEY_STEP, StateManager.STEP_INIT) + + @staticmethod + def set_step(step: str): + st.session_state[StateManager.KEY_STEP] = step + + @staticmethod + def get_raw_note_path() -> Optional[Path]: + path = st.session_state.get(StateManager.KEY_RAW_NOTE_PATH) + if path: + return resolve_path(path) + return None + + @staticmethod + def set_raw_note_path(path: Path): + st.session_state[StateManager.KEY_RAW_NOTE_PATH] = str(path) + + @staticmethod + def get_save_folder_path() -> Optional[Path]: + path = st.session_state.get(StateManager.KEY_SAVE_FOLDER_PATH) + if path: + return resolve_path(path) + return None + + @staticmethod + def set_save_folder_path(path: Path): + st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = str(path) + + @staticmethod + def get_raw_note_content() -> Optional[str]: + return st.session_state.get(StateManager.KEY_RAW_NOTE_CONTENT) + + @staticmethod + def set_raw_note_content(content: str): + st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = content + + @staticmethod + def get_metadata() -> Optional[FreshnessMetadata]: + data = st.session_state.get(StateManager.KEY_METADATA) + if data: + return FreshnessMetadata( + info_keyword=data.get("info_keyword", []), + info_query=data.get("info_query", []), + wiki_searched_at=data.get("wiki_searched_at"), + tavily_searched_at=data.get("tavily_searched_at"), + ) + return None + + @staticmethod + def set_metadata(metadata: FreshnessMetadata): + st.session_state[StateManager.KEY_METADATA] = metadata.to_yaml_dict() + + @staticmethod + def get_info_keyword() -> List[str]: + return st.session_state.get(StateManager.KEY_INFO_KEYWORD, []) + + @staticmethod + def set_info_keyword(keywords: List[str]): + st.session_state[StateManager.KEY_INFO_KEYWORD] = keywords + + @staticmethod + def get_info_query() -> List[str]: + return st.session_state.get(StateManager.KEY_INFO_QUERY, []) + + @staticmethod + def set_info_query(queries: List[str]): + st.session_state[StateManager.KEY_INFO_QUERY] = queries + + @staticmethod + def get_description_template() -> Optional[DescriptionTemplate]: + data = st.session_state.get(StateManager.KEY_DESCRIPTION_TEMPLATE) + if data: + return DescriptionTemplate.from_dict(data) + return None + + @staticmethod + def set_description_template(template: DescriptionTemplate): + st.session_state[StateManager.KEY_DESCRIPTION_TEMPLATE] = template.to_dict() + + @staticmethod + def get_wiki_results() -> List[dict]: + return st.session_state.get(StateManager.KEY_WIKI_RESULTS, []) + + @staticmethod + def set_wiki_results(results: List[dict]): + st.session_state[StateManager.KEY_WIKI_RESULTS] = results + + @staticmethod + def get_tavily_results() -> List[dict]: + return st.session_state.get(StateManager.KEY_TAVILY_RESULTS, []) + + @staticmethod + def set_tavily_results(results: List[dict]): + st.session_state[StateManager.KEY_TAVILY_RESULTS] = results + + @staticmethod + def reset(): + """Reset all state to initial values.""" + keys_to_reset = [ + StateManager.KEY_STEP, + StateManager.KEY_RAW_NOTE_PATH, + StateManager.KEY_SAVE_FOLDER_PATH, + StateManager.KEY_RAW_NOTE_CONTENT, + StateManager.KEY_METADATA, + StateManager.KEY_INFO_KEYWORD, + StateManager.KEY_INFO_QUERY, + StateManager.KEY_DESCRIPTION_TEMPLATE, + StateManager.KEY_WIKI_RESULTS, + StateManager.KEY_TAVILY_RESULTS, + ] + for key in keys_to_reset: + if key in st.session_state: + del st.session_state[key] + StateManager.initialize() diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py new file mode 100644 index 0000000..a4bc794 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py @@ -0,0 +1,7 @@ +"""LLM utilities for note freshness module.""" + +from .client import UpstageClient +from .parsers import ResponseParser +from .prompt_loader import PromptLoader + +__all__ = ["UpstageClient", "ResponseParser", "PromptLoader"] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py new file mode 100644 index 0000000..53e4971 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py @@ -0,0 +1,147 @@ +"""LLM client for Upstage API interaction including Information Extraction.""" + +import base64 +import json +import httpx +from openai import OpenAI +from typing import List, Optional, Dict, Any +from pathlib import Path +from ..config import Config + + +class UpstageClient: + """Client for interacting with Upstage Solar API.""" + + def __init__( + self, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + model: Optional[str] = None, + ): + self.api_key = api_key or Config.UPSTAGE_API_KEY + self.api_base = api_base or Config.UPSTAGE_API_BASE + self.model = model or Config.MODEL_NAME + + if not self.api_key: + raise ValueError("UPSTAGE_API_KEY is required") + + def _get_headers(self) -> Dict[str, str]: + return { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + def _encode_file_to_base64(self, file_path: Path) -> str: + """Encode file to base64 string.""" + with open(file_path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + + def make_request_sync( + self, + messages: List[Dict[str, str]], + temperature: float = None, + max_tokens: int = None, + ) -> Optional[str]: + """Make a synchronous request to the Upstage API.""" + temperature = temperature or Config.DEFAULT_TEMPERATURE + max_tokens = max_tokens or Config.DEFAULT_MAX_TOKENS + + url = f"{self.api_base}/chat/completions" + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + } + + try: + with httpx.Client(timeout=Config.HTTP_TIMEOUT_SYNC) as client: + response = client.post(url, headers=self._get_headers(), json=payload) + response.raise_for_status() + data = response.json() + return data["choices"][0]["message"]["content"] + except httpx.HTTPStatusError as e: + print(f"HTTP error occurred: {e}") + return None + except Exception as e: + print(f"Error making request: {e}") + return None + + def extract_information( + self, document_path: Path, schema: str + ) -> Optional[Dict[str, Any]]: + """Extract information from a document using Upstage Information Extraction API. + + Args: + document_path: Path to the document file (docx) + schema: JSON schema string defining what to extract + + Returns: + Extracted information as dictionary or None on error + """ + try: + # Encode document to base64 + base64_data = self._encode_file_to_base64(document_path) + + # Create OpenAI client for Information Extraction + client = OpenAI( + api_key=self.api_key, + base_url="https://api.upstage.ai/v1/information-extraction", + ) + + # Parse schema string to dict + try: + schema_dict = json.loads(schema) + except json.JSONDecodeError: + print("Error: Invalid JSON schema") + return None + + # Make extraction request + extraction_response = client.chat.completions.create( + model="information-extract", + messages=[ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": f"data:application/octet-stream;base64,{base64_data}" + }, + } + ], + } + ], + response_format={ + "type": "json_schema", + "json_schema": {"name": "document_schema", "schema": schema_dict}, + }, + ) + + # Parse result + json_str = extraction_response.choices[0].message.content + result = json.loads(json_str) + return result + + except Exception as e: + print(f"Error during information extraction: {e}") + return None + + def generate_freshness_guide( + self, system_prompt: str, user_prompt: str, temperature: float = 0.3 + ) -> Optional[str]: + """Generate freshness guide using Solar model. + + Args: + system_prompt: System prompt + user_prompt: User prompt with context + temperature: Lower temperature for more focused output + + Returns: + Generated guide content or None on error + """ + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.make_request_sync(messages, temperature=temperature) diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py new file mode 100644 index 0000000..ac66c96 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py @@ -0,0 +1,96 @@ +"""Parsers for LLM responses.""" + +import json +import re +from typing import List, Optional, Dict, Any + + +class ResponseParser: + """Parser for LLM responses.""" + + @staticmethod + def parse_extraction_result(result: Dict[str, Any]) -> tuple[List[str], List[str]]: + """Parse information extraction result to get keywords and queries. + + Args: + result: Raw extraction result from API + + Returns: + Tuple of (info_keyword list, info_query list) + """ + info_keyword = [] + info_query = [] + + if not result: + return info_keyword, info_query + + # Try to extract from various possible formats + if isinstance(result, dict): + # Direct extraction + if "info_keyword" in result: + kw = result["info_keyword"] + info_keyword = kw if isinstance(kw, list) else [kw] + + if "info_query" in result: + q = result["info_query"] + info_query = q if isinstance(q, list) else [q] + + # Try nested structure + if "extraction" in result: + return ResponseParser.parse_extraction_result(result["extraction"]) + + return info_keyword, info_query + + @staticmethod + def parse_wiki_content(content: str) -> str: + """Parse Wikipedia content for markdown format.""" + # Clean up content + content = re.sub(r"\[\[([^\]|]+)\|?[^\]]*\]\]", r"\1", content) + return content.strip() + + @staticmethod + def parse_tavily_results(results: List[dict]) -> List[dict]: + """Parse Tavily search results. + + Args: + results: Raw Tavily results + + Returns: + List of parsed result dictionaries + """ + parsed = [] + for result in results[:3]: # Limit to top 3 results + parsed.append( + { + "title": result.get("title", ""), + "url": result.get("url", ""), + "content": result.get("content", ""), + "score": result.get("score", 0), + } + ) + return parsed + + @staticmethod + def extract_json_from_response(response: str) -> Optional[Dict[str, Any]]: + """Extract and parse JSON from a response.""" + # Try to find JSON in code blocks first + json_match = re.search(r"```json\s*(.*?)\s*```", response, re.DOTALL) + if json_match: + try: + return json.loads(json_match.group(1)) + except json.JSONDecodeError: + pass + + # Try to find JSON object + json_match = re.search(r"\{.*\}", response, re.DOTALL) + if json_match: + try: + return json.loads(json_match.group(0)) + except json.JSONDecodeError: + pass + + # Try parsing entire response + try: + return json.loads(response) + except json.JSONDecodeError: + return None diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py new file mode 100644 index 0000000..7b2d82f --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py @@ -0,0 +1,91 @@ +"""Prompt template loading functionality for note freshness.""" + +import yaml +from pathlib import Path +from typing import Dict, List, Optional +from backend.note_split.models import PromptTemplate +from ..config import Config + + +class PromptLoader: + """Loader for prompt templates from YAML files.""" + + def __init__(self, prompts_dir: Optional[Path] = None): + """Initialize the prompt loader. + + Args: + prompts_dir: Directory containing prompt template files + """ + self.prompts_dir = prompts_dir or Config.PROMPTS_DIR + + def load_template(self, template_name: str) -> Optional[PromptTemplate]: + """Load a specific prompt template by name. + + Args: + template_name: Name of the template file (without .yml extension) + + Returns: + PromptTemplate object or None if not found + """ + template_path = self.prompts_dir / f"{template_name}.yml" + + if not template_path.exists(): + return None + + try: + with open(template_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + if data is None: + print(f"Warning: Template file '{template_name}.yml' is empty.") + return None + + if not isinstance(data, dict): + print(f"Error: Template {template_name} must be a dictionary.") + return None + + return PromptTemplate( + name=data.get("name", template_name), + description=data.get("description", ""), + system_prompt=data.get("system_prompt", ""), + user_prompt_template=data.get("user_prompt_template", ""), + ) + except yaml.YAMLError as e: + print(f"Error parsing YAML in template {template_name}: {e}") + return None + except Exception as e: + print(f"Error loading template {template_name}: {e}") + return None + + def load_schema(self, schema_name: str) -> Optional[str]: + """Load a JSON schema from a YAML file. + + Args: + schema_name: Name of the schema file (without .yml extension) + + Returns: + Schema string or None if not found + """ + schema_path = self.prompts_dir / f"{schema_name}.yml" + + if not schema_path.exists(): + return None + + try: + with open(schema_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + if data is None or "schema" not in data: + return None + + return data["schema"].strip() + except Exception as e: + print(f"Error loading schema {schema_name}: {e}") + return None + + def list_templates(self) -> List[str]: + """List all available template names.""" + if not self.prompts_dir.exists(): + return [] + + return [p.stem for p in self.prompts_dir.glob("*.yml")] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/models.py b/usecase/solar-knowledge-management/backend/note_freshness/models.py new file mode 100644 index 0000000..0a36f8a --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/models.py @@ -0,0 +1,99 @@ +"""Data models for the note freshness check module.""" + +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime + + +@dataclass +class FreshnessMetadata: + """Represents metadata for freshness checking. + + Attributes: + info_keyword: Keywords for Wikipedia search + info_query: Query strings for Tavily search + wiki_searched_at: Timestamp of last Wikipedia search + tavily_searched_at: Timestamp of last Tavily search + """ + + info_keyword: List[str] = field(default_factory=list) + info_query: List[str] = field(default_factory=list) + wiki_searched_at: Optional[str] = None + tavily_searched_at: Optional[str] = None + + def to_yaml_dict(self) -> dict: + """Convert to dictionary for YAML serialization.""" + result = {} + if self.info_keyword: + result["info_keyword"] = self.info_keyword + if self.info_query: + result["info_query"] = self.info_query + if self.wiki_searched_at: + result["wiki_searched_at"] = self.wiki_searched_at + if self.tavily_searched_at: + result["tavily_searched_at"] = self.tavily_searched_at + return result + + +@dataclass +class WikiSearchResult: + """Wikipedia search result. + + Attributes: + keyword: Search keyword used + title: Article title + summary: Article summary + url: Article URL + searched_at: Search timestamp + """ + + keyword: str + title: str + summary: str + url: str + searched_at: str + + +@dataclass +class TavilySearchResult: + """Tavily search result. + + Attributes: + query: Search query used + results: List of search result items + searched_at: Search timestamp + """ + + query: str + results: List[dict] + searched_at: str + + +@dataclass +class DescriptionTemplate: + """Template for information extraction description. + + Attributes: + name: Template name + description: Template description for UI + content: The actual description content for API + """ + + name: str + description: str + content: str + + def to_dict(self) -> dict: + return { + "name": self.name, + "description": self.description, + "content": self.content, + } + + @classmethod + def from_dict(cls, data: dict) -> "DescriptionTemplate": + return cls( + name=data.get("name", ""), + description=data.get("description", ""), + content=data.get("content", ""), + ) diff --git a/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py new file mode 100644 index 0000000..a7d5a26 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py @@ -0,0 +1,21 @@ +"""UI components for note freshness module.""" + +from .components import ( + render_file_input_section, + render_template_selection_section, + render_metadata_review_section, + render_search_results_section, + render_error, + render_success, + render_info, +) + +__all__ = [ + "render_file_input_section", + "render_template_selection_section", + "render_metadata_review_section", + "render_search_results_section", + "render_error", + "render_success", + "render_info", +] diff --git a/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py b/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py new file mode 100644 index 0000000..5b128f0 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py @@ -0,0 +1,124 @@ +"""Reusable UI components for Streamlit application.""" + +import streamlit as st +from typing import List, Optional, Dict + + +def render_file_input_section(): + """Render the file input section.""" + st.markdown("## 1. 노트 검증") + + note_path = st.text_input( + "노트 경로", + placeholder="/path/to/your/note.md", + help="최신성을 검토할 마크다운 노트 파일의 경로", + ) + + save_folder = st.text_input( + "저장 폴더 경로 (선택사항)", + placeholder="미입력시 위에서 입력한 노트가 있는 경로에 저장됩니다", + help="검색 결과와 가이드를 저장할 폴더", + ) + + return note_path, save_folder + + +def render_template_selection_section(default_template: str = ""): + """Render the template selection and editing section.""" + st.markdown("## 2. 추출 템플릿 설정") + + st.markdown( + """ + 아래 템플릿은 노트에서 최신성 검토를 위한 키워드와 쿼리를 추출하는 데 사용됩니다. + 필요에 따라 수정할 수 있습니다. + """ + ) + + template_content = st.text_area( + "추출 설명 템플릿", + value=default_template, + height=300, + help="Upstage Information Extraction API에 전달할 설명", + ) + + return template_content + + +def render_metadata_review_section(keywords: List[str], queries: List[str]): + """Render the metadata review and editing section.""" + st.markdown("## 3. 추출 결과 검토") + + st.markdown("추출된 키워드와 쿼리를 검토하고 필요시 수정하세요.") + + # Keywords editing + st.markdown("### 검색 키워드 (Wikipedia)") + keywords_text = st.text_area( + "키워드 (한 줄에 하나씩)", + value="\n".join(keywords), + height=150, + help="Wikipedia 검색에 사용할 키워드", + ) + edited_keywords = [kw.strip() for kw in keywords_text.split("\n") if kw.strip()] + + # Queries editing + st.markdown("### 검색 쿼리 (Tavily)") + queries_text = st.text_area( + "쿼리 (한 줄에 하나씩)", + value="\n".join(queries), + height=150, + help="Tavily 검색에 사용할 쿼리", + ) + edited_queries = [q.strip() for q in queries_text.split("\n") if q.strip()] + + return edited_keywords, edited_queries + + +def render_search_results_section(wiki_results: List[dict], tavily_results: List[dict]): + """Render the search results section.""" + st.markdown("## 4. 검색 결과") + + # Wikipedia results + if wiki_results: + st.markdown("### Wikipedia 검색 결과") + for result in wiki_results: + with st.expander( + f"📖 {result.get('title', 'Unknown')} ({result.get('keyword', '')})" + ): + st.markdown(f"**요약:** {result.get('summary', 'N/A')[:500]}...") + if result.get("url"): + st.markdown(f"[Wikipedia 링크]({result['url']})") + + # Tavily results + if tavily_results: + st.markdown("### Tavily 검색 결과") + for result in tavily_results: + query = result.get("query", "") + st.markdown(f"#### 쿼리: {query}") + for item in result.get("results", []): + with st.expander(f"🔍 {item.get('title', 'Unknown')}"): + st.markdown(f"**내용:** {item.get('content', 'N/A')[:500]}...") + if item.get("url"): + st.markdown(f"[원본 링크]({item['url']})") + + +def render_guide_preview(guide_content: str): + """Render the generated guide preview.""" + st.markdown("## 5. 최신성 검토 가이드") + + with st.expander("가이드 전체 보기", expanded=True): + st.markdown(guide_content) + + +def render_error(message: str): + """Render an error message.""" + st.error(f"❌ {message}") + + +def render_success(message: str): + """Render a success message.""" + st.success(f"✅ {message}") + + +def render_info(message: str): + """Render an info message.""" + st.info(f"ℹ️ {message}") diff --git a/usecase/solar-knowledge-management/backend/note_split/__init__.py b/usecase/solar-knowledge-management/backend/note_split/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usecase/solar-knowledge-management/backend/note_split/config.py b/usecase/solar-knowledge-management/backend/note_split/config.py new file mode 100644 index 0000000..a7fe8d7 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/config.py @@ -0,0 +1,90 @@ +"""Configuration management for the application.""" +import os +from pathlib import Path +from typing import Optional +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +def _get_project_root() -> Path: + """Get the project root directory. + + This function finds the project root by looking for pyproject.toml + starting from the current file's directory and going up. + + Returns: + Path to the project root directory + """ + current = Path(__file__).resolve() + # Start from backend/note_split/config.py, go up to project root + # Try going up 3 levels first (most common case) + for level in range(4): + candidate = current.parents[level] + if (candidate / 'pyproject.toml').exists(): + return candidate + # Fallback: assume project root is 3 levels up from config.py + return current.parent.parent.parent.parent + + +PROJECT_ROOT = _get_project_root() + + +class Config: + """Application configuration manager.""" + + # Upstage API settings + UPSTAGE_API_KEY: str = os.getenv('UPSTAGE_API_KEY', '') + UPSTAGE_API_BASE: str = os.getenv('UPSTAGE_API_BASE', 'https://api.upstage.ai/v1/solar') + MODEL_NAME: str = os.getenv('MODEL_NAME', 'solar-pro') + + # Application settings + DEFAULT_WORKSPACE: Path = Path(os.getenv('DEFAULT_WORKSPACE', './workspace')) + PROMPTS_DIR: Path = Path(os.getenv('PROMPTS_DIR', str(PROJECT_ROOT / 'prompts'))) + + # LLM settings + DEFAULT_TEMPERATURE: float = float(os.getenv('DEFAULT_TEMPERATURE', '0.7')) + DEFAULT_MAX_TOKENS: int = int(os.getenv('DEFAULT_MAX_TOKENS', '4096')) + + # HTTP client timeout settings (in seconds) + HTTP_TIMEOUT_ASYNC: float = float(os.getenv('HTTP_TIMEOUT_ASYNC', '60.0')) + HTTP_TIMEOUT_SYNC: float = float(os.getenv('HTTP_TIMEOUT_SYNC', '120.0')) + + # File settings + ATOMIC_NOTES_SUFFIX: str = os.getenv('ATOMIC_NOTES_SUFFIX', '-atoms') + + # Atomic note generation settings + DEFAULT_ATOM_DIRECTION: str = os.getenv('DEFAULT_ATOM_DIRECTION', '') + + @classmethod + def validate(cls) -> bool: + """Validate that required configuration is present. + + Returns: + True if configuration is valid, False otherwise + """ + if not cls.UPSTAGE_API_KEY: + return False + return True + + @classmethod + def get_atomic_notes_folder(cls, raw_note_path: Path) -> Path: + """Generate the folder path for atomic notes based on raw note path. + + Args: + raw_note_path: Path to the raw note file + + Returns: + Path to the atomic notes folder + """ + parent = raw_note_path.parent + stem = raw_note_path.stem + return parent / f"{stem}{cls.ATOMIC_NOTES_SUFFIX}" + + @classmethod + def ensure_directories(cls): + """Ensure required directories exist.""" + cls.DEFAULT_WORKSPACE.mkdir(parents=True, exist_ok=True) + cls.PROMPTS_DIR.mkdir(parents=True, exist_ok=True) + diff --git a/usecase/solar-knowledge-management/backend/note_split/core/__init__.py b/usecase/solar-knowledge-management/backend/note_split/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py b/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py new file mode 100644 index 0000000..68971c4 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py @@ -0,0 +1,278 @@ +"""File handling operations for raw notes and atomic notes.""" +from pathlib import Path +from typing import List, Optional, Tuple, Union +from ..models import Topic +from ..config import Config +from .path_utils import resolve_path + + +class FileHandler: + """Handler for file operations related to notes.""" + + @staticmethod + def _resolve_path(path: Union[str, Path]) -> Path: + """Resolve and normalize a path for the current environment. + + Args: + path: Path string or Path object + + Returns: + Resolved and normalized Path object + """ + return resolve_path(path) + + @staticmethod + def read_note(note_path: Union[str, Path]) -> Tuple[Optional[str], Optional[List[str]]]: + """Read a markdown note file. + + Args: + note_path: Path to the note file (string or Path) + + Returns: + Tuple of (full content as string, list of lines) or (None, None) on error + """ + try: + resolved_path = FileHandler._resolve_path(note_path) + with open(resolved_path, 'r', encoding='utf-8') as f: + content = f.read() + lines = content.split('\n') + return content, lines + except FileNotFoundError: + print(f"File not found: {note_path}") + return None, None + except Exception as e: + print(f"Error reading file {note_path}: {e}") + return None, None + + @staticmethod + def get_lines_content(lines: List[str], line_numbers: List[int]) -> str: + """Extract content from specific line numbers. + + Args: + lines: List of all lines in the document + line_numbers: List of line numbers to extract (1-indexed) + + Returns: + Concatenated content of specified lines + """ + content_lines = [] + for line_num in sorted(line_numbers): + # Convert 1-indexed to 0-indexed + idx = line_num - 1 + if 0 <= idx < len(lines): + content_lines.append(lines[idx]) + + return '\n'.join(content_lines) + + @staticmethod + def insert_backlinks( + lines: List[str], + topics: List[Topic] + ) -> List[str]: + """Insert backlinks to atomic notes in the raw note. + + Backlinks are inserted at the last line of consecutive line ranges + to improve readability. + + Args: + lines: List of lines from the raw note + topics: List of Topic objects with line numbers + + Returns: + Modified list of lines with backlinks inserted + """ + # Create a mapping of line numbers to topics + line_to_backlinks = {} + + for topic in topics: + if not topic.line_numbers: + continue + + # Group consecutive line numbers + sorted_lines = sorted(topic.line_numbers) + ranges = [] + current_range = [sorted_lines[0]] + + for line_num in sorted_lines[1:]: + if line_num == current_range[-1] + 1: + current_range.append(line_num) + else: + ranges.append(current_range) + current_range = [line_num] + ranges.append(current_range) + + # Add backlink to the last line of each range + for line_range in ranges: + last_line = line_range[-1] + if last_line not in line_to_backlinks: + line_to_backlinks[last_line] = [] + line_to_backlinks[last_line].append(f"[[{topic.topic}|{topics.index(topic) + 1}]]") + + # Insert backlinks into lines + modified_lines = [] + for i, line in enumerate(lines, 1): + modified_lines.append(line) + if i in line_to_backlinks: + # Add backlinks at the end of the line + backlinks_str = ' '.join(line_to_backlinks[i]) + modified_lines[-1] = f"{line} {backlinks_str}" + + return modified_lines + + @staticmethod + def append_topic_list( + lines: List[str], + topics: List[Topic] + ) -> List[str]: + """Append list of generated atomic notes to the end of raw note. + + Args: + lines: List of lines from the raw note + topics: List of Topic objects + + Returns: + Modified list of lines with topic list appended + """ + # Add separator + lines.append('') + lines.append('---') + lines.append('&&&') + lines.append('') + lines.append('## Generated Atomic Notes') + lines.append('') + + # Add topic links + for topic in topics: + lines.append(f"{topics.index(topic) + 1}. [[{topic.topic}]]") + + return lines + + @staticmethod + def create_atomic_note( + topic: Topic, + related_content: str, + generated_content: Optional[str] = None + ) -> str: + """Create content for an atomic note. + + Args: + topic: Topic object with metadata + related_content: Content extracted from the raw note + generated_content: Optional LLM-generated content for the note + + Returns: + Complete markdown content for the atomic note + """ + parts = [] + + # Add properties + parts.append(topic.get_properties_markdown()) + parts.append('') + + # Add title + parts.append(f"# {topic.topic}") + parts.append('') + + # Add overview + parts.append("## Overview") + parts.append(topic.coverage) + parts.append('') + + # Add related content from raw note + if related_content: + parts.append("## Related Content from Raw Note") + parts.append('') + parts.append(related_content) + parts.append('') + + # Add generated content if available + if generated_content: + parts.append("## Generated Content") + parts.append('') + parts.append(generated_content) + parts.append('') + + # Add keywords section + if topic.keywords: + parts.append("## Keywords") + parts.append('') + parts.append(', '.join(f"`{kw}`" for kw in topic.keywords)) + parts.append('') + + return '\n'.join(parts) + + @staticmethod + def save_atomic_note( + save_folder: Union[str, Path], + topic: Topic, + content: str + ) -> bool: + """Save an atomic note to a file. + + Args: + save_folder: Folder to save the atomic note (string or Path) + topic: Topic object + content: Markdown content for the note + + Returns: + True if successful, False otherwise + """ + try: + resolved_folder = FileHandler._resolve_path(save_folder) + # Ensure save folder exists + resolved_folder.mkdir(parents=True, exist_ok=True) + + # Sanitize filename + safe_filename = FileHandler._sanitize_filename(topic.topic) + file_path = resolved_folder / f"{safe_filename}.md" + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + return True + except Exception as e: + print(f"Error saving atomic note: {e}") + return False + + @staticmethod + def save_raw_note(note_path: Union[str, Path], lines: List[str]) -> bool: + """Save the modified raw note. + + Args: + note_path: Path to the raw note file (string or Path) + lines: List of lines to write + + Returns: + True if successful, False otherwise + """ + try: + resolved_path = FileHandler._resolve_path(note_path) + content = '\n'.join(lines) + with open(resolved_path, 'w', encoding='utf-8') as f: + f.write(content) + return True + except Exception as e: + print(f"Error saving raw note {note_path}: {e}") + return False + + @staticmethod + def _sanitize_filename(filename: str) -> str: + """Sanitize a string to be used as a filename. + + Args: + filename: String to sanitize + + Returns: + Sanitized filename + """ + # Replace invalid characters with underscore + invalid_chars = '<>:"/\\|?*' + for char in invalid_chars: + filename = filename.replace(char, '_') + + # Limit length + max_length = 200 + if len(filename) > max_length: + filename = filename[:max_length] + + return filename.strip() + diff --git a/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py b/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py new file mode 100644 index 0000000..62c85d5 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py @@ -0,0 +1,110 @@ +"""Path utilities for cross-platform path handling, including WSL support.""" +import os +import platform +import re +from pathlib import Path +from typing import Union + +# Pattern to match Windows paths like C:\Users\... or C:/Users/... +_WINDOWS_PATH_PATTERN = re.compile(r'^([A-Za-z]):[/\\](.*)$') + + +def _is_wsl_environment() -> bool: + """Detect if running in WSL environment. + + Returns: + True if running in WSL, False otherwise + """ + if platform.system() != "Linux": + return False + + # Check /proc/version for WSL indicators + if os.path.exists("/proc/version"): + try: + with open("/proc/version", "r", encoding="utf-8") as f: + version_info = f.read().lower() + if "microsoft" in version_info or "wsl" in version_info: + return True + except (OSError, IOError): + pass + + # Check for /mnt/c mount point + if os.path.exists("/mnt/c"): + return True + + return False + + +def normalize_path_for_wsl(path: Union[str, Path]) -> Path: + """Normalize Windows paths to WSL paths when running in WSL environment. + + Args: + path: Path string or Path object (can be Windows or WSL format) + + Returns: + Normalized Path object suitable for the current environment + """ + path_str = str(path).strip() + + # Strip surrounding quotes (both single and double) + if (path_str.startswith('"') and path_str.endswith('"')) or \ + (path_str.startswith("'") and path_str.endswith("'")): + path_str = path_str[1:-1].strip() + + if not _is_wsl_environment(): + return Path(path_str) + + # Match Windows path pattern (C:\Users\... or C:/Users/...) + match = _WINDOWS_PATH_PATTERN.match(path_str) + if match: + drive_letter = match.group(1).lower() + rest_of_path = match.group(2).replace("\\", "/") + wsl_path = Path(f"/mnt/{drive_letter}/{rest_of_path}") + return wsl_path + + return Path(path_str) + + +def resolve_path(path: Union[str, Path]) -> Path: + """Resolve and normalize a path for the current environment. + + This function: + - Converts Windows paths to WSL paths if in WSL + - Expands user home directory (~) + - Resolves to absolute path + + Args: + path: Path string or Path object + + Returns: + Resolved and normalized Path object + """ + normalized = normalize_path_for_wsl(path) + return normalized.expanduser().resolve() + + +def format_path_for_display(path: Union[str, Path], *, prefer_windows_format: bool = False) -> str: + r"""Format a path for display, optionally converting WSL paths to Windows format. + + Args: + path: Path string or Path object + prefer_windows_format: If True and in WSL, convert /mnt/... to C:\... format + + Returns: + Formatted path string + """ + path_obj = Path(path) + + if prefer_windows_format and _is_wsl_environment(): + path_str = str(path_obj) + if path_str.startswith("/mnt/"): + parts = path_str.split("/", 3) + if len(parts) >= 4: + drive_letter = parts[2].upper() + rest_path = parts[3].replace("/", "\\") + return f"{drive_letter}:\\{rest_path}" + + return str(path_obj) + + + diff --git a/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py b/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py new file mode 100644 index 0000000..04a071a --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py @@ -0,0 +1,206 @@ +"""State management for Streamlit session state.""" +from typing import List, Optional, Dict, Any +import streamlit as st +from pathlib import Path +from ..models import Topic +from .path_utils import resolve_path + + +class StateManager: + """Manager for Streamlit session state.""" + + # State keys + KEY_RAW_NOTE_PATH = 'raw_note_path' + KEY_SAVE_FOLDER_PATH = 'save_folder_path' + KEY_RAW_NOTE_CONTENT = 'raw_note_content' + KEY_RAW_NOTE_LINES = 'raw_note_lines' + KEY_TOPICS = 'topics' + KEY_SELECTED_TEMPLATE = 'selected_template' + KEY_ANALYSIS_INSTRUCTIONS = 'analysis_instructions' + KEY_STEP = 'current_step' + + # Step identifiers + STEP_INIT = 'init' + STEP_TEMPLATE_SELECT = 'template_select' + STEP_TOPICS_EXTRACTED = 'topics_extracted' + STEP_NOTES_GENERATED = 'notes_generated' + + @staticmethod + def initialize(): + """Initialize session state with default values.""" + if StateManager.KEY_STEP not in st.session_state: + st.session_state[StateManager.KEY_STEP] = StateManager.STEP_INIT + + if StateManager.KEY_RAW_NOTE_PATH not in st.session_state: + st.session_state[StateManager.KEY_RAW_NOTE_PATH] = None + + if StateManager.KEY_SAVE_FOLDER_PATH not in st.session_state: + st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = None + + if StateManager.KEY_RAW_NOTE_CONTENT not in st.session_state: + st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = None + + if StateManager.KEY_RAW_NOTE_LINES not in st.session_state: + st.session_state[StateManager.KEY_RAW_NOTE_LINES] = None + + if StateManager.KEY_TOPICS not in st.session_state: + st.session_state[StateManager.KEY_TOPICS] = [] + + if StateManager.KEY_SELECTED_TEMPLATE not in st.session_state: + st.session_state[StateManager.KEY_SELECTED_TEMPLATE] = None + + if StateManager.KEY_ANALYSIS_INSTRUCTIONS not in st.session_state: + st.session_state[StateManager.KEY_ANALYSIS_INSTRUCTIONS] = '' + + @staticmethod + def get_current_step() -> str: + """Get the current step in the workflow.""" + return st.session_state.get(StateManager.KEY_STEP, StateManager.STEP_INIT) + + @staticmethod + def set_step(step: str): + """Set the current workflow step.""" + st.session_state[StateManager.KEY_STEP] = step + + @staticmethod + def get_raw_note_path() -> Optional[Path]: + """Get the raw note path.""" + path = st.session_state.get(StateManager.KEY_RAW_NOTE_PATH) + if path: + return resolve_path(path) + return None + + @staticmethod + def set_raw_note_path(path: Path): + """Set the raw note path.""" + st.session_state[StateManager.KEY_RAW_NOTE_PATH] = str(path) + + @staticmethod + def get_save_folder_path() -> Optional[Path]: + """Get the save folder path.""" + path = st.session_state.get(StateManager.KEY_SAVE_FOLDER_PATH) + if path: + return resolve_path(path) + return None + + @staticmethod + def set_save_folder_path(path: Path): + """Set the save folder path.""" + st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = str(path) + + @staticmethod + def get_raw_note_content() -> Optional[str]: + """Get the raw note content.""" + return st.session_state.get(StateManager.KEY_RAW_NOTE_CONTENT) + + @staticmethod + def set_raw_note_content(content: str, lines: List[str]): + """Set the raw note content and lines.""" + st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = content + st.session_state[StateManager.KEY_RAW_NOTE_LINES] = lines + + @staticmethod + def get_raw_note_lines() -> Optional[List[str]]: + """Get the raw note lines.""" + return st.session_state.get(StateManager.KEY_RAW_NOTE_LINES) + + @staticmethod + def get_topics() -> List[Topic]: + """Get the list of topics.""" + topics_data = st.session_state.get(StateManager.KEY_TOPICS, []) + return [Topic.from_dict(t) if isinstance(t, dict) else t for t in topics_data] + + @staticmethod + def set_topics(topics: List[Topic]): + """Set the list of topics.""" + st.session_state[StateManager.KEY_TOPICS] = [ + t.to_dict() if isinstance(t, Topic) else t for t in topics + ] + + @staticmethod + def add_topic(topic: Topic): + """Add a new topic to the list.""" + topics = StateManager.get_topics() + topics.append(topic) + StateManager.set_topics(topics) + + @staticmethod + def update_topic(index: int, topic: Topic): + """Update a topic at a specific index.""" + topics = StateManager.get_topics() + if 0 <= index < len(topics): + topics[index] = topic + StateManager.set_topics(topics) + + @staticmethod + def delete_topic(index: int): + """Delete a topic at a specific index.""" + topics = StateManager.get_topics() + if 0 <= index < len(topics): + topics.pop(index) + StateManager.set_topics(topics) + + @staticmethod + def delete_topics(indices: List[int]): + """Delete multiple topics by indices.""" + topics = StateManager.get_topics() + # Sort indices in reverse to avoid index shifting issues + for index in sorted(indices, reverse=True): + if 0 <= index < len(topics): + topics.pop(index) + StateManager.set_topics(topics) + + @staticmethod + def get_selected_topics() -> List[Topic]: + """Get list of topics that are selected.""" + return [t for t in StateManager.get_topics() if t.selected] + + @staticmethod + def get_selected_topic_indices() -> List[int]: + """Get indices of selected topics.""" + topics = StateManager.get_topics() + return [i for i, t in enumerate(topics) if t.selected] + + @staticmethod + def select_all_topics(): + """Select all topics.""" + topics = StateManager.get_topics() + for topic in topics: + topic.selected = True + StateManager.set_topics(topics) + + @staticmethod + def deselect_all_topics(): + """Deselect all topics.""" + topics = StateManager.get_topics() + for topic in topics: + topic.selected = False + StateManager.set_topics(topics) + + @staticmethod + def get_selected_template() -> Optional[str]: + """Get the selected prompt template name.""" + return st.session_state.get(StateManager.KEY_SELECTED_TEMPLATE) + + @staticmethod + def set_selected_template(template_name: str): + """Set the selected prompt template.""" + st.session_state[StateManager.KEY_SELECTED_TEMPLATE] = template_name + + @staticmethod + def get_analysis_instructions() -> str: + """Get the analysis instructions.""" + return st.session_state.get(StateManager.KEY_ANALYSIS_INSTRUCTIONS, '') + + @staticmethod + def set_analysis_instructions(instructions: str): + """Set the analysis instructions.""" + st.session_state[StateManager.KEY_ANALYSIS_INSTRUCTIONS] = instructions + + @staticmethod + def reset(): + """Reset all state to initial values.""" + for key in list(st.session_state.keys()): + del st.session_state[key] + StateManager.initialize() + diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/__init__.py b/usecase/solar-knowledge-management/backend/note_split/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/client.py b/usecase/solar-knowledge-management/backend/note_split/llm/client.py new file mode 100644 index 0000000..e79f6e4 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/llm/client.py @@ -0,0 +1,198 @@ +"""LLM client for Upstage API interaction.""" +import asyncio +from typing import List, Optional, Dict, Any +import httpx +from ..config import Config +from ..models import PromptTemplate + + +class UpstageClient: + """Client for interacting with Upstage Solar API.""" + + def __init__( + self, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + model: Optional[str] = None + ): + """Initialize the Upstage client. + + Args: + api_key: Upstage API key (defaults to Config.UPSTAGE_API_KEY) + api_base: API base URL (defaults to Config.UPSTAGE_API_BASE) + model: Model name (defaults to Config.MODEL_NAME) + """ + self.api_key = api_key or Config.UPSTAGE_API_KEY + self.api_base = api_base or Config.UPSTAGE_API_BASE + self.model = model or Config.MODEL_NAME + + if not self.api_key: + raise ValueError("UPSTAGE_API_KEY is required") + + def _get_headers(self) -> Dict[str, str]: + """Get request headers with API key.""" + return { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + async def _make_request( + self, + messages: List[Dict[str, str]], + temperature: float = Config.DEFAULT_TEMPERATURE, + max_tokens: int = Config.DEFAULT_MAX_TOKENS + ) -> Optional[str]: + """Make an async request to the Upstage API. + + Args: + messages: List of message dictionaries with 'role' and 'content' + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + + Returns: + Generated text or None on error + """ + url = f"{self.api_base}/chat/completions" + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens + } + + try: + async with httpx.AsyncClient(timeout=Config.HTTP_TIMEOUT_ASYNC) as client: + response = await client.post( + url, + headers=self._get_headers(), + json=payload + ) + response.raise_for_status() + + data = response.json() + return data['choices'][0]['message']['content'] + except httpx.HTTPStatusError as e: + print(f"HTTP error occurred: {e}") + return None + except Exception as e: + print(f"Error making request: {e}") + return None + + def make_request_sync( + self, + messages: List[Dict[str, str]], + temperature: float = Config.DEFAULT_TEMPERATURE, + max_tokens: int = Config.DEFAULT_MAX_TOKENS + ) -> Optional[str]: + """Make a synchronous request to the Upstage API. + + Args: + messages: List of message dictionaries with 'role' and 'content' + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + + Returns: + Generated text or None on error + """ + url = f"{self.api_base}/chat/completions" + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens + } + + try: + with httpx.Client(timeout=Config.HTTP_TIMEOUT_SYNC) as client: + response = client.post( + url, + headers=self._get_headers(), + json=payload + ) + response.raise_for_status() + + data = response.json() + return data['choices'][0]['message']['content'] + except httpx.HTTPStatusError as e: + print(f"HTTP error occurred: {e}") + return None + except Exception as e: + print(f"Error making request: {e}") + return None + + async def generate_with_template( + self, + template: PromptTemplate, + user_vars: Dict[str, Any], + temperature: float = Config.DEFAULT_TEMPERATURE, + max_tokens: int = Config.DEFAULT_MAX_TOKENS + ) -> Optional[str]: + """Generate text using a prompt template. + + Args: + template: PromptTemplate object + user_vars: Variables to format the user prompt + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + + Returns: + Generated text or None on error + """ + messages = [ + {"role": "system", "content": template.system_prompt}, + {"role": "user", "content": template.format_user_prompt(**user_vars)} + ] + + return await self._make_request(messages, temperature, max_tokens) + + def generate_with_template_sync( + self, + template: PromptTemplate, + user_vars: Dict[str, Any], + temperature: float = Config.DEFAULT_TEMPERATURE, + max_tokens: int = Config.DEFAULT_MAX_TOKENS + ) -> Optional[str]: + """Generate text using a prompt template (synchronous). + + Args: + template: PromptTemplate object + user_vars: Variables to format the user prompt + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + + Returns: + Generated text or None on error + """ + messages = [ + {"role": "system", "content": template.system_prompt}, + {"role": "user", "content": template.format_user_prompt(**user_vars)} + ] + + return self.make_request_sync(messages, temperature, max_tokens) + + async def generate_batch( + self, + requests: List[Dict[str, Any]] + ) -> List[Optional[str]]: + """Generate multiple responses in parallel. + + Args: + requests: List of request dictionaries, each containing: + - messages: List of message dicts + - temperature (optional): Sampling temperature + - max_tokens (optional): Maximum tokens to generate + + Returns: + List of generated texts (or None for failed requests) + """ + tasks = [] + for req in requests: + messages = req['messages'] + temperature = req.get('temperature', Config.DEFAULT_TEMPERATURE) + max_tokens = req.get('max_tokens', Config.DEFAULT_MAX_TOKENS) + + task = self._make_request(messages, temperature, max_tokens) + tasks.append(task) + + return await asyncio.gather(*tasks) + diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py b/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py new file mode 100644 index 0000000..ae326f7 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py @@ -0,0 +1,143 @@ +"""Parsers for LLM responses.""" +import json +import re +from typing import List, Optional, Dict, Any +from ..models import Topic + + +class ResponseParser: + """Parser for LLM responses.""" + + @staticmethod + def parse_topics_from_json(response: str) -> List[Topic]: + """Parse topics from JSON response. + + Expected JSON format: + { + "topics": [ + { + "topic": "Topic Name", + "coverage": "Brief overview", + "line_numbers": [1, 2, 3], + "keywords": ["keyword1", "keyword2"] + } + ] + } + + Args: + response: JSON string response from LLM + + Returns: + List of Topic objects + """ + try: + # Try to extract JSON from markdown code blocks if present + json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL) + if json_match: + response = json_match.group(1) + + data = json.loads(response) + topics_data = data.get('topics', []) + + topics = [] + for topic_data in topics_data: + topic = Topic( + topic=topic_data.get('topic', ''), + coverage=topic_data.get('coverage', ''), + line_numbers=topic_data.get('line_numbers', []), + keywords=topic_data.get('keywords', []) + ) + topics.append(topic) + + return topics + except json.JSONDecodeError as e: + print(f"Error parsing JSON response: {e}") + return [] + except Exception as e: + print(f"Error processing topics: {e}") + return [] + + @staticmethod + def parse_line_numbers_from_json(response: str) -> List[int]: + """Parse line numbers from JSON response. + + Expected JSON format: + { + "line_numbers": [1, 2, 3, 5, 7] + } + + Args: + response: JSON string response from LLM + + Returns: + List of line numbers + """ + try: + # Try to extract JSON from markdown code blocks if present + json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL) + if json_match: + response = json_match.group(1) + + data = json.loads(response) + return data.get('line_numbers', []) + except json.JSONDecodeError as e: + print(f"Error parsing JSON response: {e}") + return [] + except Exception as e: + print(f"Error processing line numbers: {e}") + return [] + + @staticmethod + def parse_atomic_note_content(response: str) -> str: + """Parse atomic note content from LLM response. + + The response should contain markdown content for the atomic note. + + Args: + response: Markdown content from LLM + + Returns: + Cleaned atomic note content + """ + # Remove any markdown code blocks if the entire response is wrapped + if response.strip().startswith('```') and response.strip().endswith('```'): + # Remove the first and last line (markdown fences) + lines = response.strip().split('\n') + if len(lines) > 2: + # Remove first line and last line + response = '\n'.join(lines[1:-1]) + + return response.strip() + + @staticmethod + def extract_json_from_response(response: str) -> Optional[Dict[str, Any]]: + """Extract and parse JSON from a response that may contain other text. + + Args: + response: Response text that may contain JSON + + Returns: + Parsed JSON dictionary or None if parsing fails + """ + # Try to find JSON in code blocks first + json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL) + if json_match: + try: + return json.loads(json_match.group(1)) + except json.JSONDecodeError: + pass + + # Try to find JSON object in the text + json_match = re.search(r'\{.*\}', response, re.DOTALL) + if json_match: + try: + return json.loads(json_match.group(0)) + except json.JSONDecodeError: + pass + + # Try parsing the entire response + try: + return json.loads(response) + except json.JSONDecodeError: + return None + diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py b/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py new file mode 100644 index 0000000..6bb8abf --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py @@ -0,0 +1,86 @@ +"""Prompt template loading functionality.""" +import yaml +from pathlib import Path +from typing import Dict, List, Optional +from ..models import PromptTemplate +from ..config import Config + + +class PromptLoader: + """Loader for prompt templates from YAML files.""" + + def __init__(self, prompts_dir: Optional[Path] = None): + """Initialize the prompt loader. + + Args: + prompts_dir: Directory containing prompt template files + """ + self.prompts_dir = prompts_dir or Config.PROMPTS_DIR + + def load_template(self, template_name: str) -> Optional[PromptTemplate]: + """Load a specific prompt template by name. + + Args: + template_name: Name of the template file (without .yml extension) + + Returns: + PromptTemplate object or None if not found + """ + template_path = self.prompts_dir / f"{template_name}.yml" + + if not template_path.exists(): + return None + + try: + with open(template_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + # 빈 파일이나 None인 경우 명시적으로 처리 + if data is None: + print(f"Warning: Template file '{template_name}.yml' is empty or contains no valid YAML content.") + return None + + # data가 딕셔너리가 아닌 경우 처리 + if not isinstance(data, dict): + print(f"Error loading template {template_name}: YAML content must be a dictionary, got {type(data).__name__}") + return None + + return PromptTemplate( + name=data.get('name', template_name), + description=data.get('description', ''), + system_prompt=data.get('system_prompt', ''), + user_prompt_template=data.get('user_prompt_template', '') + ) + except yaml.YAMLError as e: + print(f"Error parsing YAML in template {template_name}: {e}") + return None + except Exception as e: + print(f"Error loading template {template_name}: {e}") + return None + + def list_templates(self) -> List[str]: + """List all available template names. + + Returns: + List of template names (without .yml extension) + """ + if not self.prompts_dir.exists(): + return [] + + return [ + p.stem for p in self.prompts_dir.glob('*.yml') + ] + + def get_templates_info(self) -> Dict[str, str]: + """Get information about all available templates. + + Returns: + Dictionary mapping template names to their descriptions + """ + templates_info = {} + for template_name in self.list_templates(): + template = self.load_template(template_name) + if template: + templates_info[template_name] = template.description + return templates_info + diff --git a/usecase/solar-knowledge-management/backend/note_split/models.py b/usecase/solar-knowledge-management/backend/note_split/models.py new file mode 100644 index 0000000..fbe2408 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/models.py @@ -0,0 +1,85 @@ +"""Data models for the atomic notes application.""" +from dataclasses import dataclass, field +from typing import List, Optional + + +@dataclass +class Topic: + """Represents a topic extracted from a raw note. + + Attributes: + topic: The name/title of the topic + coverage: Brief summary/overview of the topic + line_numbers: List of line numbers in the raw note related to this topic + keywords: List of key terms related to this topic + user_direction: Optional user instructions for generating the atomic note + use_llm: Whether to use LLM for generating atomic note content + selected: Whether this topic is selected for batch operations + """ + topic: str + coverage: str + line_numbers: List[int] = field(default_factory=list) + keywords: List[str] = field(default_factory=list) + user_direction: Optional[str] = None + use_llm: bool = False + selected: bool = False + + def to_dict(self) -> dict: + """Convert Topic to dictionary.""" + return { + 'topic': self.topic, + 'coverage': self.coverage, + 'line_numbers': self.line_numbers, + 'keywords': self.keywords, + 'user_direction': self.user_direction, + 'use_llm': self.use_llm, + 'selected': self.selected + } + + @classmethod + def from_dict(cls, data: dict) -> 'Topic': + """Create Topic from dictionary.""" + return cls( + topic=data.get('topic', ''), + coverage=data.get('coverage', ''), + line_numbers=data.get('line_numbers', []), + keywords=data.get('keywords', []), + user_direction=data.get('user_direction'), + use_llm=data.get('use_llm', False), + selected=data.get('selected', False) + ) + + def get_properties_markdown(self) -> str: + """Generate markdown properties section for the atomic note.""" + props = [ + "---", + f"topic: {self.topic}", + f"coverage: {self.coverage}", + f"line_numbers: {self.line_numbers}", + f"keywords: {', '.join(self.keywords)}", + ] + if self.user_direction: + props.append(f"user_direction: {self.user_direction}") + props.append("---") + return '\n'.join(props) + + +@dataclass +class PromptTemplate: + """Represents a prompt template for LLM interaction. + + Attributes: + name: Template identifier + description: Brief description of the template's purpose + system_prompt: System-level instructions for the LLM + user_prompt_template: Template for user prompt with placeholders + """ + name: str + description: str + system_prompt: str + user_prompt_template: str + + def format_user_prompt(self, **kwargs) -> str: + """Format the user prompt with given variables.""" + return self.user_prompt_template.format(**kwargs) + diff --git a/usecase/solar-knowledge-management/backend/note_split/ui/__init__.py b/usecase/solar-knowledge-management/backend/note_split/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usecase/solar-knowledge-management/backend/note_split/ui/components.py b/usecase/solar-knowledge-management/backend/note_split/ui/components.py new file mode 100644 index 0000000..f07b586 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/note_split/ui/components.py @@ -0,0 +1,382 @@ +"""Reusable UI components for Streamlit application.""" +import streamlit as st +from typing import List, Optional, Callable +from ..models import Topic +from ..core.state_manager import StateManager + + +def render_topic_card( + topic: Topic, + index: int, + on_update: Optional[Callable] = None, + on_delete: Optional[Callable] = None, + on_select: Optional[Callable] = None +): + """Render a topic card with information and actions. + + Args: + topic: Topic object to display + index: Index of the topic in the list + on_update: Callback for update action + on_delete: Callback for delete action + on_select: Callback for selection toggle + """ + with st.container(): + # Create columns for layout + col_check, col_llm, col_content, col_actions = st.columns([0.5, 0.5, 7.5, 1.5]) + + # Checkbox for selection + with col_check: + if on_select: + st.markdown( + '', + unsafe_allow_html=True + ) + selected = st.checkbox( + "선택", + value=topic.selected, + key=f"select_{index}", + label_visibility="collapsed", + help="원자 노트 생성을 위해 이 토픽을 선택합니다" + ) + if selected != topic.selected: + on_select(index, selected) + + # Checkbox for LLM generation + with col_llm: + st.markdown( + '🤖', + unsafe_allow_html=True + ) + use_llm = st.checkbox( + "LLM 생성", + value=topic.use_llm, + key=f"use_llm_{index}", + label_visibility="collapsed", + help="LLM을 사용하여 원자 노트 초안과 작성 가이드라인을 생성합니다" + ) + if use_llm != topic.use_llm: + # Update topic's use_llm field + updated_topic = Topic( + topic=topic.topic, + coverage=topic.coverage, + line_numbers=topic.line_numbers, + keywords=topic.keywords, + user_direction=topic.user_direction, + use_llm=use_llm, + selected=topic.selected + ) + if on_update: + on_update(index, updated_topic) + + # Main content + with col_content: + st.markdown(f"### {topic.topic}") + st.markdown(f"**Overview:** {topic.coverage}") + + if topic.keywords: + keywords_str = ', '.join(f'`{kw}`' for kw in topic.keywords) + st.markdown(f"**Keywords:** {keywords_str}") + + if topic.line_numbers: + lines_str = ', '.join(str(ln) for ln in sorted(topic.line_numbers)) + st.markdown(f"**Related Lines:** {lines_str}") + + # Action buttons + with col_actions: + col_edit, col_del = st.columns(2) + + with col_edit: + if st.button("✏️", key=f"edit_{index}", help="Edit topic"): + st.session_state[f'editing_{index}'] = True + + with col_del: + if st.button("🗑️", key=f"delete_{index}", help="Delete topic"): + if on_delete: + on_delete(index) + + # Edit modal (using expander as a simple modal) + if st.session_state.get(f'editing_{index}', False): + with st.expander("Edit Topic", expanded=True): + render_topic_edit_form(topic, index, on_update) + + st.divider() + + +def render_topic_edit_form( + topic: Topic, + index: int, + on_save: Optional[Callable] = None +): + """Render an edit form for a topic. + + Args: + topic: Topic object to edit + index: Index of the topic + on_save: Callback when save is clicked + """ + new_topic = st.text_input( + "Topic Name", + value=topic.topic, + key=f"edit_topic_{index}" + ) + + new_coverage = st.text_area( + "Coverage", + value=topic.coverage, + key=f"edit_coverage_{index}", + height=100 + ) + + new_keywords = st.text_input( + "Keywords (comma-separated)", + value=', '.join(topic.keywords), + key=f"edit_keywords_{index}" + ) + + new_user_direction = st.text_area( + "User Direction (optional)", + value=topic.user_direction or '', + key=f"edit_direction_{index}", + height=80, + help="Additional instructions for generating the atomic note" + ) + + new_use_llm = st.checkbox( + "Use LLM for content generation", + value=topic.use_llm, + key=f"edit_use_llm_{index}", + help="Enable LLM to generate draft content and writing guidelines" + ) + + col_save, col_cancel = st.columns(2) + + with col_save: + if st.button("Save", key=f"save_{index}"): + updated_topic = Topic( + topic=new_topic, + coverage=new_coverage, + line_numbers=topic.line_numbers, + keywords=[kw.strip() for kw in new_keywords.split(',') if kw.strip()], + user_direction=new_user_direction if new_user_direction else None, + use_llm=new_use_llm, + selected=topic.selected + ) + + if on_save: + on_save(index, updated_topic) + + st.session_state[f'editing_{index}'] = False + st.rerun() + + with col_cancel: + if st.button("Cancel", key=f"cancel_{index}"): + st.session_state[f'editing_{index}'] = False + st.rerun() + + +def render_topics_list( + topics: List[Topic], + on_update: Optional[Callable] = None, + on_delete: Optional[Callable] = None, + on_select: Optional[Callable] = None +): + """Render a list of topic cards. + + Args: + topics: List of Topic objects + on_update: Callback for topic updates + on_delete: Callback for topic deletion + on_select: Callback for topic selection + """ + if not topics: + st.info("No topics extracted yet. Please run topic extraction first.") + return + + st.markdown(f"### Extracted Topics ({len(topics)})") + st.markdown("---") + + for i, topic in enumerate(topics): + render_topic_card(topic, i, on_update, on_delete, on_select) + + +def render_batch_actions(): + """Render batch action buttons.""" + col1, col2, col3 = st.columns([1, 1, 2]) + + with col1: + if st.button("Select All", use_container_width=True): + StateManager.select_all_topics() + st.rerun() + + with col2: + if st.button("Deselect All", use_container_width=True): + StateManager.deselect_all_topics() + st.rerun() + + with col3: + selected_indices = StateManager.get_selected_topic_indices() + if selected_indices: + if st.button( + f"Delete Selected ({len(selected_indices)})", + use_container_width=True, + type="primary" + ): + st.session_state['confirm_batch_delete'] = True + + # Confirmation dialog + if st.session_state.get('confirm_batch_delete', False): + st.warning("Are you sure you want to delete the selected topics?") + col_yes, col_no = st.columns(2) + + with col_yes: + if st.button("Yes, Delete", type="primary"): + selected_indices = StateManager.get_selected_topic_indices() + StateManager.delete_topics(selected_indices) + st.session_state['confirm_batch_delete'] = False + st.success(f"Deleted {len(selected_indices)} topic(s)") + st.rerun() + + with col_no: + if st.button("Cancel"): + st.session_state['confirm_batch_delete'] = False + st.rerun() + + +def render_add_topics_form(on_add: Optional[Callable] = None): + """Render form for adding new topics. + + Args: + on_add: Callback when new topics should be added + """ + with st.expander("Add New Topics"): + st.markdown("### Request Additional Topics") + + guidance = st.text_area( + "Topic Generation Guidance", + placeholder="E.g., 'Focus on technical implementation details' or 'Extract business logic concepts'", + help="Provide guidance for what kind of topics to extract" + ) + + col1, col2 = st.columns(2) + + with col1: + num_topics = st.number_input( + "Number of Topics", + min_value=1, + max_value=10, + value=3, + help="How many additional topics to generate" + ) + + with col2: + template = st.selectbox( + "Prompt Template", + options=["topic_extract_amend_default", "topic_extract_diverse"], + help="Select the prompt template for topic extraction" + ) + + if st.button("Generate New Topics", type="primary"): + if on_add: + on_add(guidance, num_topics, template) + + +def render_progress_indicator(message: str, progress: Optional[float] = None): + """Render a progress indicator. + + Args: + message: Message to display + progress: Optional progress value (0.0 to 1.0) + """ + if progress is not None: + st.progress(progress, text=message) + else: + with st.spinner(message): + pass + + +def render_file_input_section(): + """Render the file input section.""" + st.markdown("## 1. Initial Setup") + + note_path = st.text_input( + "Raw Note Path", + placeholder="/path/to/your/note.md", + help="Path to the markdown note file you want to analyze" + ) + + save_folder = st.text_input( + "Save Folder Path (optional)", + placeholder="Leave empty to use default location", + help="Folder where atomic notes will be saved. If empty, defaults to [note-name]-atoms/" + ) + + analysis_instructions = st.text_area( + "Analysis Instructions (optional)", + placeholder="Additional instructions for topic extraction...", + help="Optional guidance for the LLM when extracting topics", + height=100 + ) + + return note_path, save_folder, analysis_instructions + + +def render_template_selection_section(templates: dict): + """Render the template selection section. + + Args: + templates: Dictionary of template names to descriptions + + Returns: + Selected template name or None + """ + st.markdown("## 2. Select Prompt Template") + + if not templates: + st.warning("No prompt templates found. Please add template files to the prompts/ folder.") + return None + + # Filter templates: only include topic_extract_* templates that are NOT topic_extract_amend_* + filtered_templates = { + name: desc for name, desc in templates.items() + if name.startswith('topic_extract_') and not name.startswith('topic_extract_amend_') + } + + if not filtered_templates: + st.warning("No valid topic extraction templates found.") + return None + + # Create options with descriptions + template_options = { + name: f"{name} - {desc}" for name, desc in filtered_templates.items() + } + + # Set default to topic_extract_default if it exists, otherwise use first template + default_index = 0 + if 'topic_extract_default' in filtered_templates: + default_index = list(filtered_templates.keys()).index('topic_extract_default') + + selected = st.selectbox( + "Choose a template", + options=list(template_options.keys()), + index=default_index, + format_func=lambda x: template_options[x] + ) + + return selected + + +def render_error(message: str): + """Render an error message.""" + st.error(f"❌ {message}") + + +def render_success(message: str): + """Render a success message.""" + st.success(f"✅ {message}") + + +def render_info(message: str): + """Render an info message.""" + st.info(f"ℹ️ {message}") + diff --git a/usecase/solar-knowledge-management/backend/related_note/__init__.py b/usecase/solar-knowledge-management/backend/related_note/__init__.py new file mode 100644 index 0000000..6462262 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/related_note/__init__.py @@ -0,0 +1 @@ +from .related_note import Related_Note \ No newline at end of file diff --git a/usecase/solar-knowledge-management/backend/related_note/related_note.py b/usecase/solar-knowledge-management/backend/related_note/related_note.py new file mode 100644 index 0000000..62d493f --- /dev/null +++ b/usecase/solar-knowledge-management/backend/related_note/related_note.py @@ -0,0 +1,346 @@ +from dotenv import load_dotenv +from langchain_chroma import Chroma +from langchain_upstage import UpstageEmbeddings +from pathlib import Path +import tiktoken +import math +from uuid import uuid4 +import os +import re + +load_dotenv() + + +class Related_Note: + """ + Obsidian vault 안의 md 파일들을 임베딩하고, + 특정 노트에 대한 연관 노트 3개를 찾아 [[링크]]로 추가하는 클래스. + + - UpstageEmbeddings + Chroma DB 사용 + - embedded_markers.txt 로 "이미 임베딩된 파일"을 추적 + """ + + def __init__(self, vault_path: str) -> None: + """ + - vault_path / vector_store 경로 / embedded marker 경로 설정 + - Upstage 임베딩 객체 생성 + - Chroma 벡터스토어 로드 또는 새로 생성 + - embedded_markers.txt 파일이 없으면 빈 파일 생성 + * embedded_markers는 이미 임베딩된 노트들 기록해둠으로서 추후 중복 임베딩 및 저장 피하는 용도 + + Args: + vault_path (str): Obsidian Vault 디렉토리의 절대 경로 + """ + self.vault_path = Path(vault_path).resolve() + base_path = Path(__file__).resolve() + self.embedding_path = base_path.parent + self.store_dir = self.embedding_path / "vector_store" + self.marker_root = self.embedding_path / "embedded_markers.txt" + + # 임베딩 / 토크나이저 설정 + self.embeddings = UpstageEmbeddings(model="embedding-query") + # 업스테이지 임베딩 최대 가능 인풋인 4000토큰 측정 + self.enc = tiktoken.encoding_for_model("text-embedding-3-small") + + # Chroma 벡터스토어 초기화 + self.vector_store = self._init_vector_store() + + # 마커 파일 보장 + self._ensure_marker_file() + + def _init_vector_store(self) -> Chroma: + """ + Chroma 벡터스토어를 초기화합니다. + + Returns: + Chroma: persist_directory를 가지는 Chroma 인스턴스. + """ + if self.store_dir.exists(): + return Chroma( + persist_directory=str(self.store_dir), + embedding_function=self.embeddings, + ) + else: + # Chroma.from_texts 를 쓰기 위해 dummy 데이터 한 번 넣었다가 바로 삭제 + random_id = str(uuid4()) + vs = Chroma.from_texts( + texts=["dummy_data"], + ids=[random_id], + embedding=self.embeddings, + persist_directory=str(self.store_dir), + ) + vs.delete(ids=[random_id]) + return vs + + def _ensure_marker_file(self) -> None: + """ + embedded_markers.txt 파일이 없으면 빈 파일로 생성합니다. + Args: + None + Returns: + None + """ + if not self.marker_root.exists(): + self.marker_root.write_text("", encoding="utf-8") + + # ────────────────────────────────────────────────────────── + # 마커(이미 임베딩된 노트) 관련 + # ────────────────────────────────────────────────────────── + def load_embedded_notes(self) -> set[str]: + """ + 이미 임베딩된 노트들의 상대 경로 집합을 읽어옵니다. + Args: + None + Returns: + set[str]: vault 기준 경로 집합. + """ + if not self.marker_root.exists(): + return set() + + lines = self.marker_root.read_text(encoding="utf-8").splitlines() + return {line.strip() for line in lines if line.strip()} + + def save_embedded_notes(self, new_notes: list[str]) -> None: + """ + 새로 임베딩된 노트들의 상대 경로를 마커 파일에 추가합니다. + Args: + new_notes (list[str]): vault 기준 상대 경로 리스트. + Returns: + None + """ + with self.marker_root.open("a", encoding="utf-8") as f: + for rel in new_notes: + f.write(rel + "\n") + + # ────────────────────────────────────────────────────────── + # 임베딩 대상 선택 / 청킹 / 전처리 + # ────────────────────────────────────────────────────────── + def get_unembedded_notes(self) -> list[str]: + """ + 아직 임베딩되지 않은 md 파일 경로들을 찾습니다. + - embedded_markers.txt에 없는 파일만 대상 + - 'upthink' 디렉토리 하위는 제외 + - 경로는 vault 기준 상대 경로로 반환 + + Args: + None + Returns: + list[str]: 아직 임베딩되지 않은 md 파일들의 상대 경로 리스트. + + * frontend의 경우, to_embed 리스트 내 파일을 임베딩하겠다는 메시지를 보여주면 될 것 같습니다. + """ + embedded = self.load_embedded_notes() + to_embed: list[str] = [] + + for md_file in self.vault_path.rglob("*.md"): + rel = md_file.relative_to(self.vault_path).as_posix() + if ".venv" in rel.split("/"): + continue + if rel not in embedded: + to_embed.append(rel) + + return to_embed + + def chunk_text(self, text: str) -> list[str]: + """ + 텍스트를 최대 약 4000 토큰 단위로 청킹합니다. + Args: + text (str): 원본 텍스트. + Returns: + list[str]: 청크 텍스트 리스트. (4000 토큰 이하이면 길이 1 리스트) + """ + token_list = self.enc.encode(text) + total_tokens = len(token_list) + chunks: list[str] = [] + + if total_tokens > 4000: + n_chunks = math.ceil(total_tokens / 4000) + chunk_size = math.ceil(total_tokens / n_chunks) + for i in range(0, total_tokens, chunk_size): + chunk_tokens = token_list[i : i + chunk_size] + chunk_text = self.enc.decode(chunk_tokens) + chunks.append(chunk_text) + else: + chunks.append(text) + + return chunks + + def clean_text(self, text: str) -> str: + """ + md 파일 내용을 간단히 전처리합니다. + - Windows/Unix 줄바꿈 정규화 (Windows 호환성) + - 특수 공백 문자 제거 (non-breaking space 등) + - 굵게(**) 마크다운 제거 + - 연속 개행을 하나로 축소 + - 앞뒤 공백 제거 + + Args: + text (str): 원본 텍스트. + Returns: + str: 전처리된 텍스트. + """ + # Windows 줄바꿈(\r\n)을 Unix 스타일(\n)로 정규화 + x = text.replace("\r\n", "\n").replace("\r", "\n") + x = re.sub(r"[\xa0\u200b]", "", x) + x = re.sub(r"\*\*", "", x) + x = re.sub(r"\n+", "\n", x) + x = x.strip() + return x + + # ────────────────────────────────────────────────────────── + # 임베딩 실행 + # ────────────────────────────────────────────────────────── + def index_unembedded_notes(self) -> None: + """ + 아직 임베딩되지 않은 md 파일들을 찾아 모두 임베딩합니다. + - get_unembedded_notes()로 대상 탐색 + - clean_text() + chunk_text()로 전처리/청킹 + - Chroma.add_texts()로 벡터스토어에 추가 + - embedded_markers.txt에 기록 + + Args: + None + Returns: + None + """ + to_embed = self.get_unembedded_notes() + + if not to_embed: + return + + for note_rel in to_embed: + note_path = self.vault_path / note_rel + raw_text = note_path.read_text(encoding="utf-8") + cleaned = self.clean_text(raw_text) + + # 빈 텍스트는 임베딩 건너뛰기 (Windows 호환성) + if not cleaned or not cleaned.strip(): + # 빈 파일도 마커에 기록하여 다음에 다시 시도하지 않음 + self.save_embedded_notes([note_rel]) + continue + + chunks = self.chunk_text(cleaned) + + if len(chunks) > 1: + for i, chunk in enumerate(chunks, start=1): + # 빈 청크는 건너뛰기 + if not chunk or not chunk.strip(): + continue + self.vector_store.add_texts( + ids=[str(uuid4())], + metadatas=[ + { + "title": f"{Path(note_rel).stem}_{i}", + "path": note_rel, + } + ], + texts=[chunk], + ) + else: + self.vector_store.add_texts( + ids=[str(uuid4())], + metadatas=[ + { + "title": Path(note_rel).stem, + "path": note_rel, + } + ], + texts=[cleaned], + ) + + self.save_embedded_notes([note_rel]) + + # ────────────────────────────────────────────────────────── + # 연관 노트 찾기 & 링크 삽입 + # ────────────────────────────────────────────────────────── + def find_related_notes(self, MY_VAULT_PATH: str, k: int = 3) -> list[str]: + """ + 주어진 노트와 의미적으로 유사한 md 파일 경로를 찾습니다. + Args: + MY_VAULT_PATH (str): + vault 기준 상대 경로, 사용자가 input으로 넣을 경로 + 예) "upthink/data/HCI 2025 학회 강의세션들.md" + k (int, optional): + 최대 몇 개의 연관 노트를 반환할지. 기본값 3. + Returns: + list[str]: + 연관 노트들의 vault 기준 상대 경로 리스트. + (자기 자신은 포함하지 않으며, 중복 제거됨) + * frontend에 해당 노트들이 추천되었다는 문구가 간단하게 보여졌으면 좋겠습니다. + """ + + norm_query = Path(MY_VAULT_PATH).as_posix() + query_note_path = self.vault_path / norm_query + + raw_text = query_note_path.read_text(encoding="utf-8") + cleaned = self.clean_text(raw_text) + query_chunks = self.chunk_text(cleaned) + + # 첫 번째 청크 기준으로 유사도 검색 + hits = self.vector_store.similarity_search(query_chunks[0], k=k + 4) + related: list[str] = [] + for d in hits: + raw_path = d.metadata.get("path", "") + if not raw_path: + continue + norm_path = Path(raw_path).as_posix() + + # 자기 자신은 제외 + if norm_path == norm_query: + continue + + # 중복 제외 + if norm_path in related: + continue + + related.append(norm_path) + + if len(related) >= k: + break + return related + + def append_related_links(self, MY_VAULT_PATH: str, k: int = 3): + """ + 주어진 노트 파일의 끝에 "Related Notes" 섹션을 추가하고 + [[연관노트]] 링크를 최대 k개까지 삽입합니다. + Args: + MY_VAULT_PATH (str): + vault 기준 상대 경로. + 예) r"upthink\\data\\HCI 2025 학회 강의세션들.md" + k (int, optional): + 삽입할 링크 개수 (최대). 기본값 3. + Returns: + None + """ + print(MY_VAULT_PATH) + related = self.find_related_notes(MY_VAULT_PATH, k=k) + if not related: + return + + norm_query = Path(MY_VAULT_PATH).as_posix() + target_path = self.vault_path / norm_query + + with target_path.open("a", encoding="utf-8") as f: + list_ = [] + f.write("\n\n## 🔗 Related Notes\n") + for path_rel in related[:k]: + # Obsidian 링크에서는 확장자(.md)를 떼기 위해 [:-3] + f.write(f"[[{path_rel[:-3]}]]\n") + list_.append(path_rel[:-3]) + return list_ + + +# ────────────────────────────────────────────────────────── +# 단독 실행용 예시 +# ────────────────────────────────────────────────────────── +if __name__ == "__main__": + # Vault 경로를 지정해야 합니다 + MY_VAULT_PATH = "YOUR_VAULT_PATH_HERE" # 예: "/Users/username/Documents/MyVault" + + engine = Related_Note(vault_path=MY_VAULT_PATH) + + # 1) 아직 임베딩 안 된 노트들 임베딩 + engine.index_unembedded_notes() + + # 2) 특정 노트에 대해 연관 노트 3개 링크 삽입 + engine.append_related_links(r"upthink\data\HCI 2025 학회 강의세션들.md", k=3) diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py b/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py new file mode 100644 index 0000000..b2a4df7 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py @@ -0,0 +1,15 @@ +from .tag_extractor import TagExtractor +from .tag_guidelines import GuidelineGenerator, ChecklistType +from .tag_generator import TagGenerator +from .tag_comparator import TagComparator, TagMatch +from .markdown_processor import add_yaml_frontmatter + +__all__ = [ + "TagExtractor", + "GuidelineGenerator", + "ChecklistType", + "TagGenerator", + "TagComparator", + "TagMatch", + "add_yaml_frontmatter", +] diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py b/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py new file mode 100644 index 0000000..da03362 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py @@ -0,0 +1,43 @@ +""" +마크다운 파일 처리 유틸리티 + +YAML frontmatter 추가 등 마크다운 파일 관련 기능 제공 +""" + +from datetime import datetime +from typing import List + + +def add_yaml_frontmatter(md_content: str, tags: List[str]) -> str: + """ + 마크다운 파일 최상단에 YAML frontmatter를 추가 + + Args: + md_content: 원본 마크다운 내용 + tags: 추가할 태그 리스트 + + Returns: + YAML frontmatter가 추가된 마크다운 내용 + + Examples: + >>> content = "# Hello World" + >>> result = add_yaml_frontmatter(content, ["python", "tutorial"]) + >>> "tags:" in result + True + """ + today = datetime.now().strftime("%Y-%m-%d") + + # 태그 리스트를 YAML 형식으로 변환 + tags_yaml = "\n".join([f" - {tag}" for tag in tags]) + + # YAML frontmatter 생성 + frontmatter = f"""--- +created: {today} +modified: {today} +tags: +{tags_yaml} +--- + +""" + + return frontmatter + md_content diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py new file mode 100644 index 0000000..7d77534 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py @@ -0,0 +1,313 @@ +""" +신규 생성된 태그와 기존 태그를 비교하여 매칭 및 추천 + +Qwen Embedding 모델을 사용하여 의미적 유사도 계산 +""" + +import numpy as np +from typing import List, Optional +from dataclasses import dataclass + +from sentence_transformers import SentenceTransformer + +from .tag_extractor import TagExtractor + + +@dataclass +class TagMatch: + """태그 매칭 결과""" + + new_tag: str # 신규 생성된 태그 + matched_tag: Optional[str] # 매칭된 기존 태그 (없으면 None) + similarity: float # 유사도 점수 (0.0 ~ 1.0) + is_new: bool # 완전히 새로운 태그인지 여부 + + +class TagComparator: + """신규 태그와 기존 태그를 비교하여 매칭 및 추천""" + + def __init__( + self, + similarity_threshold: float = 0.85, + model_name: str = "Qwen/Qwen3-Embedding-0.6B", + ): + """ + TagComparator 초기화 + + Args: + similarity_threshold: 유사도 임계값 (이상이면 같은 태그로 간주) + model_name: 사용할 임베딩 모델명 + """ + print(f"[INFO] Embedding 모델 로딩 중: {model_name}") + self.model = SentenceTransformer(model_name) + self.similarity_threshold = similarity_threshold + self.tag_extractor = TagExtractor() + + def _get_embeddings(self, texts: List[str]) -> np.ndarray: + """ + 텍스트 리스트를 embedding 벡터로 변환 + + Args: + texts: 변환할 텍스트 리스트 + + Returns: + embedding 벡터 배열 (shape: [len(texts), embedding_dim]) + + Raises: + ValueError: 빈 텍스트가 포함된 경우 + """ + if not texts: + return np.array([]) + + # 빈 문자열 필터링 및 검증 + valid_texts = [text.strip() for text in texts if text and text.strip()] + if len(valid_texts) != len(texts): + print( + f"[WARNING] {len(texts) - len(valid_texts)}개의 빈 태그가 제거되었습니다" + ) + + if not valid_texts: + raise ValueError("유효한 텍스트가 없습니다") + + # SentenceTransformer로 embedding 생성 (기존 태그/신규 태그 모두 document로 처리) + embeddings = self.model.encode(valid_texts) + + return embeddings + + def compare_tags( + self, new_tags: List[str], existing_tags: List[str] + ) -> List[TagMatch]: + """ + 신규 태그와 기존 태그를 비교하여 매칭 + + Args: + new_tags: 신규 생성된 태그 리스트 + existing_tags: 기존 태그 리스트 + + Returns: + 각 신규 태그에 대한 매칭 결과 리스트 + + Raises: + ValueError: 유효하지 않은 태그가 포함된 경우 + Exception: Embedding 생성 실패 시 + """ + if not new_tags: + return [] + + # 빈 문자열 필터링 + valid_new_tags = [tag.strip() for tag in new_tags if tag and tag.strip()] + if len(valid_new_tags) != len(new_tags): + print( + f"[WARNING] {len(new_tags) - len(valid_new_tags)}개의 빈 신규 태그가 제거되었습니다" + ) + + if not valid_new_tags: + raise ValueError("유효한 신규 태그가 없습니다") + + if not existing_tags: + # 기존 태그가 없으면 모두 새로운 태그 + return [ + TagMatch(new_tag=tag, matched_tag=None, similarity=0.0, is_new=True) + for tag in valid_new_tags + ] + + valid_existing_tags = [ + tag.strip() for tag in existing_tags if tag and tag.strip() + ] + if not valid_existing_tags: + print("[WARNING] 유효한 기존 태그가 없어 모든 태그를 신규로 처리합니다") + return [ + TagMatch(new_tag=tag, matched_tag=None, similarity=0.0, is_new=True) + for tag in valid_new_tags + ] + + try: + # Embedding 생성 (신규 태그와 기존 태그 모두 같은 공간에서 처리) + new_tag_embeddings = self._get_embeddings(valid_new_tags) + existing_tag_embeddings = self._get_embeddings(valid_existing_tags) + + # 모든 신규 태그와 기존 태그 간의 유사도를 한번에 계산 + all_similarities = self.model.similarity( + new_tag_embeddings, existing_tag_embeddings + ) + + results = [] + + # 각 신규 태그에 대해 가장 유사한 기존 태그 찾기 + for i, new_tag in enumerate(valid_new_tags): + # i번째 신규 태그의 모든 유사도 + similarities = all_similarities[i] + + # 가장 유사한 태그 찾기 + max_similarity_idx = np.argmax(similarities) + max_similarity = float(similarities[max_similarity_idx]) + + # 임계값 이상이면 매칭, 아니면 새로운 태그 + if max_similarity >= self.similarity_threshold: + matched_tag = valid_existing_tags[max_similarity_idx] + is_new = False + else: + matched_tag = None + is_new = True + + results.append( + TagMatch( + new_tag=new_tag, + matched_tag=matched_tag, + similarity=max_similarity, + is_new=is_new, + ) + ) + + return results + + except Exception as e: + raise Exception(f"태그 비교 중 오류 발생: {str(e)}") from e + + def get_recommended_tags( + self, new_tags: List[str], vault_path: str + ) -> List[TagMatch]: + """ + Vault에서 기존 태그를 추출하고 신규 태그와 비교 + + Args: + new_tags: 신규 생성된 태그 리스트 + vault_path: Vault 디렉토리 경로 + + Returns: + 각 신규 태그에 대한 매칭 결과 리스트 + """ + # Vault에서 기존 태그 추출 + existing_tags = list(self.tag_extractor.get_unique_tags(vault_path)) + + # 비교 및 매칭 + return self.compare_tags(new_tags, existing_tags) + + @staticmethod + def get_final_tags(matches: List[TagMatch]) -> List[str]: + """ + 매칭 결과를 기반으로 최종 추천 태그 리스트 생성 + + Args: + matches: 태그 매칭 결과 리스트 + + Returns: + 최종 추천 태그 리스트 (기존 태그로 대체되거나 새로운 태그, 중복 제거됨) + """ + final_tags = [] + seen_tags = set() + + for match in matches: + if match.is_new: + # 새로운 태그 + tag = match.new_tag + else: + # 기존 태그로 대체 + tag = match.matched_tag + + # 중복 체크 후 추가 + if tag not in seen_tags: + final_tags.append(tag) + seen_tags.add(tag) + + return final_tags + + +if __name__ == "__main__": + import pathlib + import traceback + from .tag_guidelines import GuidelineGenerator, ChecklistType + from .tag_generator import TagGenerator + + # 설정 + VAULT_PATH = "YOUR_PATH_HERE" + TEST_FILE = "YOUR_FILE_HERE" + + print("=" * 60) + print("전체 태그 추천 파이프라인 테스트") + print("=" * 60) + + try: + # 1. Vault에서 기존 태그 추출 + print("\n[1단계] Vault에서 기존 태그 추출 중...") + comparator = TagComparator() + + if not pathlib.Path(VAULT_PATH).exists(): + print(f"[WARNING] Vault 경로를 찾을 수 없습니다: {VAULT_PATH}") + print("[INFO] 기존 태그 없이 테스트를 진행합니다") + existing_tags = [] + else: + existing_tags = list(comparator.tag_extractor.get_unique_tags(VAULT_PATH)) + print(f"✓ 기존 태그 {len(existing_tags)}개 발견") + print(f" 예시: {sorted(existing_tags)[:10]}") + if len(existing_tags) > 10: + print(f" ... 외 {len(existing_tags) - 10}개") + + # 2. 신규 태그 생성 + print(f"\n[2단계] '{TEST_FILE}'에서 신규 태그 생성 중...") + + # 체크리스트 설정 + checklist: ChecklistType = { + "language": "en", + "case_style": "lowercase", + "separator": "hyphen", + "tag_count_range": {"min": 2, "max": 3}, + } + + # 가이드라인 생성기 초기화 + guideline_gen = GuidelineGenerator(checklist) + + # 태그 생성기 초기화 + tag_gen = TagGenerator() + + # 마크다운 파일 읽기 + data_path = pathlib.Path(__file__).parent.parent.parent / "data" / TEST_FILE + + if not data_path.exists(): + print(f"[ERROR] 파일을 찾을 수 없습니다: {data_path}") + exit(1) + + with open(data_path, "r", encoding="utf-8") as f: + md_content = f.read() + + # 태그 생성 + new_tags = tag_gen.generate_tags(guideline_gen, md_content, TEST_FILE) + print(f"✓ 신규 태그 생성 완료: {new_tags}") + print(f" 태그 개수: {len(new_tags)}") + + # 3. 태그 비교 및 매칭 + print(f"\n[3단계] 신규 태그와 기존 태그 비교 중...") + matches = comparator.compare_tags(new_tags, existing_tags) + + print("\n[매칭 결과]") + print("-" * 60) + for match in matches: + status = "✓ 매칭됨" if not match.is_new else "✗ 신규" + matched_info = ( + f"→ {match.matched_tag}" if match.matched_tag else "→ 추가 필요" + ) + print( + f"{status} | {match.new_tag:30s} {matched_info:25s} (유사도: {match.similarity:.3f})" + ) + + # 4. 최종 추천 태그 + final_tags = TagComparator.get_final_tags(matches) + print(f"\n[최종 추천 태그]") + print("-" * 60) + print(final_tags) + print(f" 최종 태그 개수: {len(final_tags)}") + + # 통계 + new_count = sum(1 for m in matches if m.is_new) + matched_count = len(matches) - new_count + print(f"\n[통계]") + print(f" 신규 태그: {new_count}개") + print(f" 매칭된 태그: {matched_count}개") + + print("\n" + "=" * 60) + print("테스트 완료!") + + except Exception as e: + print(f"\n[ERROR] 테스트 실패: {e}") + traceback.print_exc() + exit(1) diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py new file mode 100644 index 0000000..bebbb15 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py @@ -0,0 +1,185 @@ +""" +사용자의 Vault 폴더 경로를 입력 받아서, 정규표현식으로 태그를 추출하게 됨 + +태그 형식은 2가지임 +1. 해시태그 + #upstage #solar-pro2 +2. YAML (노트 최상단 frontmatter) + --- + tags: + - upstage + - solar-pro2 + --- +""" + +import re +import yaml +from pathlib import Path +from typing import List, Set, Dict +from collections import Counter + + +class TagExtractor: + """Vault 내 노트들에서 태그를 추출하는 클래스""" + + def __init__(self) -> None: + # 해시태그 패턴 + self.HASHTAG_PATTERN = r"(?:^|\s)#([a-zA-Z0-9가-힣_-]+)" + # YAML 패턴 + self.YAML_PATTERN = r"\A---\s*\n(.*?)\n---" + # YAML tag key + self.YAML_TAG_KEY = "tags" + + def find_hash_tags(self, content: str) -> List[str]: + """ + 해시태그 패턴의 태그 찾기 (e.g. #upstage) + + Args: + content: 노트 내용 + + Returns: + 추출된 태그 리스트 + """ + hash_tags = re.findall(self.HASHTAG_PATTERN, content) + return hash_tags + + def find_yaml_tags(self, content: str) -> List[str]: + """ + YAML 패턴에서 태그 찾기 + + Args: + content: 노트 내용 + + Returns: + 추출된 태그 리스트 + """ + yaml_tags = [] + match = re.search(self.YAML_PATTERN, content, re.DOTALL) + if match: + try: + frontmatter = yaml.safe_load(match.group(1)) + if isinstance(frontmatter, dict): + for key, value in frontmatter.items(): + if key.lower() in self.YAML_TAG_KEY: + if isinstance(value, list): + # if tag가 None이거나 빈 문자열("")이 아니면 리스트에 포함 + yaml_tags.extend([str(tag) for tag in value if tag]) + elif value: + yaml_tags.append(str(value)) + except yaml.YAMLError: + # 빈 리스트 반환 + pass + + return yaml_tags + + def extract_tags_from_note(self, content: str) -> Set[str]: + """ + 노트에서 모든 태그를 추출 + + Args: + content: 노트 내용 + + Returns: + 추출된 태그의 집합 + """ + tags = set() + tags.update(self.find_hash_tags(content)) + tags.update(self.find_yaml_tags(content)) + + return tags + + def extract_tags_from_vault(self, vault_path: str) -> Dict[str, List[str]]: + """ + Vault 내 모든 노트에서 태그를 추출 + + Args: + vault_path: Vault 디렉토리 경로 + + Returns: + 파일 경로는 key, 태그 리스트를 value로 하는 딕셔너리 + """ + vault_dir = Path(vault_path) + + if not vault_dir.exists() or not vault_dir.is_dir(): + raise ValueError(f"유효하지 않은 Vault 경로: {vault_path}") + + tags_by_file = {} + + # 모든 노트 순회 + for md_file in vault_dir.rglob("*.md"): + try: + content = md_file.read_text(encoding="utf-8") + tags = self.extract_tags_from_note(content) + + if tags: + tags_by_file[str(md_file)] = sorted(list(tags)) + except Exception as e: + print(f"파일 읽기 실패 {md_file}: {e}") + continue + + return tags_by_file + + def get_unique_tags(self, vault_path: str) -> Set[str]: + """ + 유니크한 태그만 있도록 + + Args: + vault_path: Vault 디렉토리 경로 + + Returns: + 유니크한 태그의 집합 + """ + tags_by_file = self.extract_tags_from_vault(vault_path) + unique_tags = set() + for tags in tags_by_file.values(): + unique_tags.update(tags) + + return unique_tags + + def count_tags(self, vault_path: str) -> Dict[str, int]: + """Vault 내 태그 빈도 계산""" + tags_by_file = self.extract_tags_from_vault(vault_path) + tag_counts = Counter() + for tags in tags_by_file.values(): + tag_counts.update(tags) + + return dict(tag_counts.most_common()) + + +if __name__ == "__main__": + tag_extractor = TagExtractor() + + MY_VAULT_PATH = "YOUR_PATH_HERE" # 절대 경로로 입력 + + print(f"'{MY_VAULT_PATH}' 경로에서 태그 추출 시작") + print("-" * 40) + + try: + # 태그 빈도수 계산 + print("\n태그 빈도수 (가장 많이 사용된 상위 20개)") + tag_counts = tag_extractor.count_tags(MY_VAULT_PATH) + + if not tag_counts: + print(" -> 태그를 찾을 수 없습니다.") + else: + # 상위 20개만 출력 (너무 많을 경우 대비) + for tag, count in list(tag_counts.items())[:20]: + print(f" - {tag}: {count}회") + if len(tag_counts) > 20: + print(f" ... (외 {len(tag_counts) - 20}개 태그)") + + # 전체 고유 태그 목록 + print("\n전체 고유 태그 목록 (알파벳 순)") + unique_tags = tag_extractor.get_unique_tags(MY_VAULT_PATH) + + if not unique_tags: + print(" -> 태그를 찾을 수 없습니다.") + else: + print(f" -> 총 {len(unique_tags)}개의 고유 태그 발견") + print(sorted(list(unique_tags))) + + except ValueError as e: + print(f"❌ 경로 오류가 발생했습니다: {e}") + print("MY_VAULT_PATH 변수에 올바른 Vault 폴더 경로를 입력했는지 확인하세요.") + except Exception as e: + print(f"❌ 예상치 못한 오류가 발생했습니다: {e}") diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py new file mode 100644 index 0000000..a029782 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py @@ -0,0 +1,222 @@ +""" +가이드라인과 markdown 파일을 받아, 신규 태그를 생성 +""" + +import os +import json + +from openai import OpenAI +from dotenv import load_dotenv + +from .tag_guidelines import GuidelineGenerator + +load_dotenv() + + +class TagGenerator: + """LLM을 사용하여 마크다운 내용을 기반으로 태그 생성""" + + def __init__( + self, + model: str = "solar-pro2", + temperature: float = 0.3, + max_tokens: int = 1000, + reasoning_effort: str = "high", + timeout: int = 300, + ): + """ + TagGenerator 초기화 + + Args: + model: 사용할 LLM 모델 (기본값: "solar-pro2") + temperature: 생성 다양성 (0.0-1.0, 기본값: 0.3) + max_tokens: 최대 토큰 수 (기본값: 1000) + reasoning_effort: reasoning 강도 (기본값: "high") + timeout: API 타임아웃 (초 단위, 기본값: 300 = 5분) + """ + api_key = os.getenv("UPSTAGE_API_KEY") + if not api_key: + raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다") + + self.client = OpenAI( + api_key=api_key, + base_url="https://api.upstage.ai/v1", + timeout=timeout, + ) + self.model = model + self.temperature = temperature + self.max_tokens = max_tokens + self.reasoning_effort = reasoning_effort + + def _parse_llm_response(self, response: str) -> list[str]: + """ + LLM 응답을 파싱하여 태그 리스트 추출 + + Args: + response: LLM의 원본 응답 + + Returns: + 파싱된 태그 리스트 + + Raises: + ValueError: JSON 파싱 실패 시 + """ + if not response: + raise ValueError("LLM 응답이 비어있습니다") + + # 다양한 형식의 코드 블록 제거 + cleaned_response = response.strip() + + if cleaned_response.startswith("```"): + # 첫 번째 줄 제거 (```json, ```JSON, ``` 등) + lines = cleaned_response.split("\n", 1) + if len(lines) > 1: + cleaned_response = lines[1] + else: + cleaned_response = cleaned_response[3:] + + # 마지막 ``` 제거 + if cleaned_response.endswith("```"): + cleaned_response = cleaned_response[:-3] + + cleaned_response = cleaned_response.strip() + + # JSON 파싱 + try: + parsed = json.loads(cleaned_response) + tags = parsed.get("tags", []) + if not isinstance(tags, list): + raise ValueError("tags가 리스트 형식이 아닙니다") + return tags + except json.JSONDecodeError as e: + raise ValueError( + f"LLM 응답을 JSON으로 파싱할 수 없습니다: {cleaned_response[:200]}" + ) from e + + def generate_tags( + self, + guideline_generator: GuidelineGenerator, + md_content: str, + filename: str, + max_retries: int = 5, + ) -> list[str]: + """ + 가이드라인과 마크다운 내용을 기반으로 태그 생성 + + Args: + guideline_generator: 태그 생성 가이드라인 생성기 + md_content: 마크다운 파일 내용 + filename: 파일명 (LLM 컨텍스트 제공용) + max_retries: 최대 재시도 횟수 (기본값: 5) + + Returns: + 생성된 태그 리스트 + + Raises: + ValueError: LLM 응답이 올바른 JSON 형식이 아닌 경우 + Exception: LLM API 호출 실패 시 + """ + # 가이드라인 생성 + system_prompt = guideline_generator.generate_guideline() + + # 사용자 메시지 구성 + user_message = f"markdown 파일: {filename}\n\n{md_content}" + + last_error = None + + for attempt in range(max_retries): + try: + # LLM API 호출 + print(f"[DEBUG] 태그 생성 시작 (시도 {attempt + 1}/{max_retries})") + print(f"[DEBUG] 모델: {self.model}, reasoning_effort: {self.reasoning_effort}") + print(f"[DEBUG] API 호출 중... (최대 {self.client.timeout}초 대기)") + + stream = self.client.chat.completions.create( + model=self.model, + messages=[ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": user_message, + }, + ], + max_tokens=self.max_tokens, + temperature=self.temperature, + reasoning_effort=self.reasoning_effort, + stream=False, + ) + + print(f"[DEBUG] API 응답 수신 완료") + + # 응답 추출 + if not stream.choices: + raise ValueError("LLM 응답에 choices가 없습니다") + + response = stream.choices[0].message.content + print(f"[DEBUG] 응답 파싱 시작") + + # 응답 파싱 + tags = self._parse_llm_response(response) + print(f"[DEBUG] 태그 생성 완료: {len(tags)}개") + return tags + + except ValueError as e: + last_error = e + if attempt < max_retries - 1: + print( + f"[WARNING] 태그 생성 실패 (시도 {attempt + 1}/{max_retries}): {e}" + ) + # temperature를 약간 조정하여 재시도 + self.temperature = min(self.temperature + 0.1, 1.0) + continue + except Exception as e: + raise Exception(f"태그 생성 중 오류 발생: {str(e)}") from e + + # 모든 재시도 실패 + raise Exception( + f"태그 생성 실패 ({max_retries}회 시도): {str(last_error)}" + ) from last_error + + +if __name__ == "__main__": + import pathlib + from backend.tag_suggest import ChecklistType, GuidelineGenerator + + # 테스트용 체크리스트 + checklist: ChecklistType = { + "language": "en", + "case_style": "lowercase", + "separator": "hyphen", + "tag_count_range": {"min": 3, "max": 5}, + } + + # 가이드라인 생성기 초기화 + guideline_gen = GuidelineGenerator(checklist) + tag_gen = TagGenerator() + + # 테스트용 마크다운 파일 + FILENAME = "YOUR_FILE_HERE" + DATA_PATH = pathlib.Path(__file__).parent.parent.parent / "data" / FILENAME + + if not DATA_PATH.exists(): + print(f"[ERROR] 파일을 찾을 수 없습니다: {DATA_PATH}") + exit(1) + + print(f"[INFO] 파일 경로: {DATA_PATH}") + + with open(DATA_PATH, "r", encoding="utf-8") as f: + md_content = f.read() + + try: + print(f"[INFO] 태그 생성 시작") + tags = tag_gen.generate_tags(guideline_gen, md_content, FILENAME) + print(f"\n[SUCCESS] 생성된 태그: {tags}") + print(f"[INFO] 태그 개수: {len(tags)}") + except Exception as e: + print(f"\n[ERROR] 에러 발생: {e}") + import traceback + + traceback.print_exc() diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py new file mode 100644 index 0000000..48c7ee1 --- /dev/null +++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py @@ -0,0 +1,190 @@ +""" +태그 작성을 위한 가이드라인 생성 + +사용자의 체크리스트를 받아서 LLM에게 태그 작성 가이드라인을 전달 + +--- +✅ 체크리스트 +주로 사용하는 언어 +영어 사용 시 대소문자 규칙 +단어의 구분자 +태그 개수 +""" + +from typing import Dict, Any, TypedDict + + +class ChecklistType(TypedDict, total=False): + """태그 작성 패턴 체크리스트 타입""" + + language: str # ko, en + case_style: str # lowercase, uppercase (영어 사용 시) + separator: str # hyphen, underscore + tag_count_range: Dict[str, int] # min, max + + +class GuidelineGenerator: + """태그 작성 가이드라인 생성 클래스""" + + def __init__(self, checklist: ChecklistType) -> None: + self.checklist = checklist + self._validate_checklist() + + def _validate_checklist(self) -> None: + """체크리스트 유효성 검사""" + required_fields = [ + "language", + "separator", + "tag_count_range", + ] + + for field in required_fields: + if field not in self.checklist: + raise ValueError(f"필수 항목이 누락되었습니다: {field}") + + # 영어 사용 시 case_style 필수 + if self.checklist["language"] in ["en"]: + if "case_style" not in self.checklist: + raise ValueError("영어 사용 시 대소문자 설정은 필수입니다") + + if self.checklist["case_style"] not in ["lowercase", "uppercase"]: + raise ValueError("영어 사용 시 대소문자 설정은 필수입니다") + + # tag_count_range 검증 + tag_range = self.checklist.get("tag_count_range", {}) + + min_val = tag_range["min"] + max_val = tag_range["max"] + + if "min" not in tag_range or "max" not in tag_range: + raise ValueError("태그의 최소, 최대 개수 설정이 필요합니다") + + if min_val > max_val: + raise ValueError(f"'{min_val}'은 '{max_val}'보다 작거나 같아야 합니다") + + if min_val < 2 or max_val > 10: + raise ValueError("태그 개수는 최소 2개, 최대 10개로 제한됩니다") + + def generate_guideline(self) -> str: + """ + 체크리스트를 바탕으로 태그 작성 가이드라인 생성 + + Returns: + LLM 프롬프트에 사용할 가이드라인 문자열 + """ + guideline_parts = [ + "# 태그 작성 가이드라인\n", + "다음 규칙을 **반드시** 준수하여 태그를 생성해 주세요.\n", + ] + + # 주로 사용하는 언어 + guideline_parts.append(self._generate_language_rule()) + + # 영어 사용 시 대소문자 규칙 + if self.checklist["language"] in ["en"]: + guideline_parts.append(self._generate_case_rule()) + + # 단어의 구분이 필요할 경우 + guideline_parts.append(self._generate_separator_rule()) + + # 선호하는 태그 개수 + guideline_parts.append(self._generate_count_rule()) + + # Output 형식 + guideline_parts.append(self._generate_output_format()) + + return "\n".join(guideline_parts) + + def _generate_language_rule(self) -> str: + """언어 규칙 생성""" + language = self.checklist["language"] + + language_map = { + "ko": "**한국어**만 사용하세요.", + "en": "**영어**만 사용하세요.", + } + + rule = language_map[language] + return f"## 주로 사용하는 언어\n{rule}\n" + + def _generate_case_rule(self) -> str: + """대소문자 규칙 생성 (영어 사용 시)""" + case_style = self.checklist.get("case_style", "lowercase") + + case_map = { + "lowercase": f"**소문자**만 사용하세요.", + "uppercase": f"**대문자**만 사용하세요.", + } + + rule = case_map[case_style] + return f"## 대소문자 규칙\n{rule}\n" + + def _generate_separator_rule(self) -> str: + """단어 구분자 규칙 생성""" + separator = self.checklist["separator"] + + separator_map = { + "hyphen": "단어의 사이를 **하이픈(-)**으로 구분하세요.", + "underscore": "단어의 사이를 **언더스코어(_)**로 구분하세요.", + } + + rule = separator_map[separator] + return f"## 단어에서 구분자가 필요한 경우\n{rule}\n" + + def _generate_count_rule(self) -> str: + """태그 개수 규칙 생성""" + tag_range = self.checklist["tag_count_range"] + min_count = tag_range["min"] + max_count = tag_range["max"] + + return ( + f"## 태그 개수 (CRITICAL)\n" + f"**반드시 {min_count}개 이상 {max_count}개 이하의 태그만 생성하세요.**\n" + f"- {min_count}개보다 적으면 안 됩니다.\n" + f"- {max_count}개보다 많으면 안 됩니다.\n" + f"- 이 규칙을 위반하면 응답이 거부됩니다.\n" + ) + + def _generate_output_format(self) -> str: + """Output 형식 규칙 생성""" + return ( + "# Output Format\n" + "Return ONLY the JSON object:\n" + "```json\n" + '{"tags": ["...", "...", "...", ..., "..."]}\n' + "```" + ) + + def get_summary(self) -> Dict[str, Any]: + """ + 체크리스트 요약 정보 반환 + + Returns: + 체크리스트의 주요 설정을 요약한 딕셔너리 + """ + summary = { + "언어": self.checklist["language"], + "대소문자": self.checklist.get("case_style", "N/A"), + "구분자": self.checklist["separator"], + "태그 개수": f"{self.checklist['tag_count_range']['min']}-{self.checklist['tag_count_range']['max']}개", + } + + return summary + + +if __name__ == "__main__": + test_checklist: ChecklistType = { + "language": "en", + "case_style": "lowercase", + "separator": "hyphen", + "tag_count_range": {"min": 3, "max": 5}, + } + + guidelines_generator = GuidelineGenerator(test_checklist) + summary = guidelines_generator.get_summary() + print("\n체크리스트 요약:") + for key, value in summary.items(): + print(f" - {key}: {value}") + + print("\n생성된 가이드라인:") + print(guidelines_generator.generate_guideline()) diff --git a/usecase/solar-knowledge-management/frontend/app.py b/usecase/solar-knowledge-management/frontend/app.py new file mode 100644 index 0000000..a32c09b --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/app.py @@ -0,0 +1,88 @@ +""" +UpThink 메인 앱 +""" + +import os +import streamlit as st + +from dotenv import load_dotenv + +load_dotenv() + +st.set_page_config(page_title="UpThink", page_icon="💭", layout="wide") + +# API Key 설정 +UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY") + + +# 공통 사이드바 설정 +def render_common_sidebar(): + """모든 페이지에서 공통으로 사용하는 사이드바""" + with st.sidebar: + # Vault 경로 입력 + st.text_input( + "Vault 경로", + placeholder="Obsidian Vault의 경로를 입력하세요", + help="Obsidian Vault 디렉토리의 절대 경로를 입력하세요", + key="vault_path", + ) + + # 파일 업로드 + st.file_uploader( + "Markdown 파일 업로드", + type=["md"], + help="처리할 마크다운 파일을 업로드하세요", + key="uploaded_file", + ) + + +# 공통 사이드바 렌더링 +render_common_sidebar() + + +home = st.Page( + "home.py", + title="Intro", + icon=":material/home:", + default=True, +) + +image_ocr = st.Page( + "image_ocr.py", + title="이미지 대체 텍스트 생성", + icon=":material/image_search:", +) +tag_suggest = st.Page( + "tag_suggest.py", + title="태그 추천", + icon=":material/new_label:", +) +related_note = st.Page( + "related_note.py", + title="연관 노트 추천", + icon=":material/note_stack:", +) +note_split = st.Page( + "note_split.py", + title="노트 분할", + icon=":material/split_scene:", +) +note_freshness = st.Page( + "note_freshness.py", + title="최신 정보 확인", + icon=":material/update:", +) + +pg = st.navigation( + { + "홈": [home], + "노트 정리": [ + image_ocr, + tag_suggest, + related_note, + note_split, + ], + "최신성 검증": [note_freshness], + } +) +pg.run() diff --git a/usecase/solar-knowledge-management/frontend/home.py b/usecase/solar-knowledge-management/frontend/home.py new file mode 100644 index 0000000..d3d368b --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/home.py @@ -0,0 +1,78 @@ +""" +서비스 설명 기재 (최초로 진입하는 페이지) +""" + +import streamlit as st + +st.title("💭 UpThink") +st.caption("""Think + Upstage ✨\\ +지식을 정리하는 사고에만 몰입해 보세요!""") + +st.markdown( + """### 개요 +개인 지식 관리 환경(Obsidian)에서 노트에 지식을 정리할 때, 가장 중요한 사고의 흐름이 끊긴 적 있으신가요? + +▪︎ㅤ노트 내 이미지의 정보를 직접 옮겨 적거나 \\ +▪︎ㅤ태그의 대소문자나 구분자 등 스타일링 규칙을 고민하거나 \\ +▪︎ㅤ작성 중인 내용과 연관된 과거 노트를 찾기 위해 탐색하거나 \\ +▪︎ㅤ내용이 너무 많아진 노트를 어떻게 분할할지 막막하거나 + +UpThink는 Upstage Solar Pro 2의 강력한 언어 이해 능력을 활용하여 이러한 지식 관리의 병목 구간을 해결합니다. \\ +이미지 분석부터 태그 정리, 연관 지식 탐색, 노트 분할까지! 번거로운 정리 작업은 AI에게 맡기고, 가장 중요한 사고 활동에만 몰입해 보세요. +""" +) + +st.divider() + +st.markdown("### 시연 영상") +st.markdown("👇 사용 방법은 시연 영상을 참고해 주세요!") +st.video("https://www.youtube.com/watch?v=8bjLew7KTW4", width=900) + +st.divider() + +st.markdown("### 주요 서비스 기능") +st.markdown( + """##### 1️⃣ 이미지 대체 텍스트 생성 +노트 내 이미지를 탐색하여 Upstage Document Parse로 텍스트를 추출한 후, Solar Pro 2를 사용하여 이미지를 설명하는 대체 텍스트를 생성합니다. \\ +생성된 대체 텍스트는 `(대체 텍스트 by Upstage)` 코드 블록으로 이미지 링크 아래에 추가되어, 수정된 Markdown 파일을 다운로드할 수 있습니다.""" +) +st.image( + "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155856-9d7a9c48-0e53-45f3-88d9-1cb0c6ea3981.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163232Z&X-Amz-Expires=300&X-Amz-Signature=a295d1d084ff3ae49e59d83cacb69588851300012ae4a143711a8e4116d72fb1&X-Amz-SignedHeaders=host", + width=800, +) +st.markdown( + """##### 2️⃣ 태그 추천 +Obsidian Vault 경로에 있는 모든 Markdown 파일에서 2가지 태그 패턴을 추출합니다. \\ +사용자가 업로드한 파일 내용과 직접 설정한 가이드라인(언어, 포맷 등)을 기반으로 태그를 생성하고, 기존 태그와의 유사도를 비교해 최종 태그를 선별합니다. \\ +최종 선정된 태그 목록은 YAML Frontmatter 형식으로 노트 최상단에 자동으로 추가되어, 사용자가 수정된 Markdown 파일을 다운로드할 수 있습니다.""" +) +st.image( + "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155896-4b950ff7-6a1b-4df9-ac76-afc5f1defac9.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163318Z&X-Amz-Expires=300&X-Amz-Signature=24911f26b551015c43a0503775a4281b4d3b5468912e2ec1076b209a82bf900e&X-Amz-SignedHeaders=host", + width=800, +) +st.markdown( + """##### 3️⃣ 연관 노트 추천 +Vault 내 노트를 Upstage Embedding Model로 벡터화하여 Chroma DB에 저장합니다. \\ +업로드한 노트와 유사도가 높은 Top 3 노트를 검색하여 추천합니다. \\ +추천된 노트는 `## Related Notes` 섹션에 백링크 형식으로 자동 삽입됩니다.""" +) +st.image( + "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155923-1ee795f1-9bcc-4916-9bbc-190dff0ee82e.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163334Z&X-Amz-Expires=300&X-Amz-Signature=5ac8983cf767cf81996427e2e5046d481fa04d2bde1d5ec5ba3a4fca7baeae02&X-Amz-SignedHeaders=host", + width=800, +) +st.markdown( + """##### 4️⃣ 노트 분할 +Solar Pro 2로 노트에서 주제(Topic)를 자동 추출하고, 사용자가 편집/삭제/추가할 수 있습니다. \\ +각 주제별로 원자 노트를 생성하여 지정된 폴더에 저장합니다. \\ +원본 노트에는 백링크와 `## Generated Atomic Notes` 섹션이 자동으로 추가됩니다.""" +) +st.image( + "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155946-f834e4a6-7227-4dcd-81e3-5087cf5f218c.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163350Z&X-Amz-Expires=300&X-Amz-Signature=34e0ed69aa99677f81b7cef22958ef47cd54d01c434ea000cf7e2d9aef2c626a&X-Amz-SignedHeaders=host", + width=800, +) + +st.divider() + +st.markdown("""**Acknowledgements** \\ +이 프로젝트는 **Upstage AI Ambassador** 활동의 일환으로 진행되었습니다. \\ +프로젝트를 진행할 수 있도록 Credit을 지원해 주신 **[Upstage](https://www.upstage.ai/)** 에 감사드립니다.""") \ No newline at end of file diff --git a/usecase/solar-knowledge-management/frontend/image_ocr.py b/usecase/solar-knowledge-management/frontend/image_ocr.py new file mode 100644 index 0000000..21a2039 --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/image_ocr.py @@ -0,0 +1,179 @@ +""" +노트 내 이미지에서 텍스트 추출 -> 대체 텍스트 생성 +""" + +import sys +from pathlib import Path + +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +import os +import streamlit as st +from typing import Optional + +from backend.image_ocr import MarkdownImageProcessor + + +def init_session_state(): + """세션 상태 초기화 (이미지 OCR 전용)""" + # vault_path와 uploaded_file은 공통 요소여서, frontend/app.py 에서 관리 + if "image_ocr_step" not in st.session_state: + st.session_state.image_ocr_step = 1 + + +def main(): + """메인 함수""" + init_session_state() + + # 메인 헤더 + st.title("🖼️ 이미지 대체 텍스트 생성") + st.caption("노트 내 이미지가 어떤 정보를 가지고 있는지 쉽게 제공받을 수 있습니다!") + st.text("") + + # API 키 확인 + UPSTAGE_API_KEY: Optional[str] = os.getenv("UPSTAGE_API_KEY") + if not UPSTAGE_API_KEY: + st.error( + "⚠️ **UPSTAGE_API_KEY** 환경 변수가 설정되지 않았습니다. " + "AI 기능을 사용하려면 터미널에 `export UPSTAGE_API_KEY='YOUR_KEY'` 명령을 실행하고 앱을 재시작하세요." + ) + return + + # Step 1: Vault 경로 및 파일 확인 + if st.session_state.image_ocr_step == 1: + vault_path_str = st.session_state.get("vault_path", "") + uploaded_file = st.session_state.get("uploaded_file") + + if vault_path_str and uploaded_file: + vault_root = Path(vault_path_str.strip()) + if not vault_root.is_dir(): + st.error( + f"오류: 입력된 경로 ({vault_path_str})는 유효한 폴더가 아닙니다." + ) + return + + st.info( + f"- Vault 경로:ㅤ{vault_path_str}\n" + f"- Markdown 파일:ㅤ{uploaded_file.name}\n\n" + f"**💡 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.**" + ) + if st.button("이미지 대체 텍스트 생성 시작", type="primary"): + st.session_state.image_ocr_step = 2 + st.rerun() + else: + st.warning( + "👈ㅤ왼쪽 사이드바에서 ***Vault 경로*** 와 ***Markdown 파일 업로드*** 설정을 완료해 주세요." + ) + + # Step 2: 이미지 처리 (자동 실행) + elif st.session_state.image_ocr_step == 2: + vault_path_str = st.session_state.get("vault_path", "") + uploaded_file = st.session_state.get("uploaded_file") + vault_root = Path(vault_path_str.strip()) + + try: + # 마크다운 내용 읽기 + md_content = uploaded_file.getvalue().decode("utf-8") + + # 프로세서 초기화 + processor = MarkdownImageProcessor() + + # 진행 상황 표시 + progress_container = st.container() + with progress_container: + st.divider() + st.subheader("🔍 OCR 분석 및 LLM 추론") + progress_bar = st.progress(0, text="초기화 중...") + status_text = st.empty() + + # 진행 상황 콜백 함수 + def progress_callback(current: int, total: int, img_src: str): + progress = current / total + progress_bar.progress(progress) + status_text.caption(f"[{current}/{total}] '{img_src}' 처리 중...") + + # 이미지 처리 실행 + processed_md, processed_images = processor.process_images( + md_content, vault_root, progress_callback + ) + + # 진행 상황 표시 완료 + progress_bar.empty() + status_text.empty() + + # 결과를 세션에 저장 + st.session_state.processed_md = processed_md + st.session_state.processed_images = processed_images + + # Step 3로 이동 + st.session_state.image_ocr_step = 3 + st.rerun() + + except Exception as e: + st.error(f"❌ 이미지 처리 실패: {e}") + with st.expander("상세 오류 정보"): + import traceback + + st.code(traceback.format_exc()) + + # Step 3: 처리 결과 표시 + elif st.session_state.image_ocr_step == 3: + processed_md = st.session_state.get("processed_md", "") + processed_images = st.session_state.get("processed_images", []) + uploaded_file = st.session_state.get("uploaded_file") + + # 결과 확인 + if not processed_images: + st.info( + "🔍 대체 텍스트 생성이 필요한 이미지가 없거나 이미지가 포함되지 않았습니다." + ) + return + + # 처리된 이미지 목록 표시 + with st.expander("📊 처리된 이미지 목록", expanded=False): + for img_info in processed_images: + st.caption( + f"'{img_info['src']}' 텍스트 생성 완료: *{img_info['new_alt_text'][:50]}...*" + ) + + st.success( + f"✅ 이미지 처리 완료. {len(processed_images)}개 이미지가 업데이트되었습니다." + ) + + # 결과 표시 + st.divider() + st.subheader("✅ 처리 결과 확인") + + col_download, _, col_reset = st.columns([1, 2, 1]) + + with col_reset: + if st.button( + "🔄ㅤ새로고침", + use_container_width=True, + type="secondary", + help="처음 단계로 돌아갑니다", + ): + # 세션 데이터 정리 + if "processed_md" in st.session_state: + del st.session_state.processed_md + if "processed_images" in st.session_state: + del st.session_state.processed_images + st.session_state.image_ocr_step = 1 + st.rerun() + + with col_download: + st.download_button( + label="⬇️ㅤ다운로드", + data=processed_md, + file_name=f"processed_{uploaded_file.name}", + type="primary", + mime="text/markdown", + use_container_width=True, + ) + + st.code(processed_md, language="markdown") + + +if __name__ == "__main__": + main() diff --git a/usecase/solar-knowledge-management/frontend/note_freshness.py b/usecase/solar-knowledge-management/frontend/note_freshness.py new file mode 100644 index 0000000..e996cce --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/note_freshness.py @@ -0,0 +1,543 @@ +"""Main Streamlit application for Note Freshness Check.""" + +import streamlit as st +import sys +import tempfile +import pypandoc +from pathlib import Path +from typing import Optional + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +# Define prompts directory +PROMPTS_DIR = project_root / "prompts" + +from backend.note_freshness.config import Config +from backend.note_freshness.core.state_manager import StateManager +from backend.note_freshness.core.file_handler import FileHandler +from backend.note_freshness.core.path_utils import resolve_path, format_path_for_display +from backend.note_freshness.llm.client import UpstageClient +from backend.note_freshness.llm.parsers import ResponseParser +from backend.note_freshness.llm.prompt_loader import PromptLoader +from backend.note_freshness.api.wikipedia import WikipediaClient +from backend.note_freshness.api.tavily import TavilyClient +from backend.note_freshness.ui.components import ( + render_file_input_section, + render_template_selection_section, + render_metadata_review_section, + render_search_results_section, + render_guide_preview, + render_error, + render_success, + render_info, +) + + +def ensure_pandoc_installed() -> bool: + """Ensure pandoc is installed, download if necessary. + + Returns: + bool: True if pandoc is available, False otherwise. + """ + try: + # Try to get pandoc path to check if it's installed + pypandoc.get_pandoc_path() + return True + except (OSError, RuntimeError): + # Pandoc is not installed, download it + try: + with st.spinner("Pandoc을 다운로드하는 중..."): + pypandoc.download_pandoc() + st.success("✅ Pandoc이 성공적으로 설치되었습니다.") + return True + except Exception as e: + st.error(f"⚠️ Pandoc 다운로드에 실패했습니다: {str(e)}") + st.info( + "수동으로 Pandoc을 설치해주세요: https://pandoc.org/installing.html" + ) + return False + + +def initialize_app(): + """Initialize the application.""" + Config.ensure_directories() + StateManager.initialize() + + +def validate_api_key() -> bool: + """Validate that API key is configured.""" + if not Config.validate(): + st.error( + "⚠️ Upstage API key not found. Please set UPSTAGE_API_KEY in your .env file." + ) + return False + return True + + +def get_default_schema() -> str: + """Load default extraction schema from file.""" + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + schema = loader.load_schema("info_extract_schema") + if schema: + return schema + # Fallback default + return """{ + "type": "object", + "properties": { + "info_keyword": { + "type": "string", + "description": "The most important keyword derived from the document." + }, + "info_query": { + "type": "string", + "description": "A Korean search query for retrieving up-to-date information." + } + }, + "required": ["info_keyword", "info_query"] +}""" + + +def handle_note_validation(note_path: str, save_folder: str): + """Handle the note validation step.""" + path = resolve_path(note_path) + + if not path.exists(): + render_error(f"파일을 찾을 수 없습니다: {note_path}") + return + + if not path.suffix == ".md": + render_error("마크다운 (.md) 파일을 입력해주세요.") + return + + # Read the note and check for existing metadata + content, metadata = FileHandler.read_note(path) + if content is None: + render_error("노트 파일을 읽는 데 실패했습니다.") + return + + # Set paths in state + StateManager.set_raw_note_path(path) + StateManager.set_raw_note_content(content) + + # Set save folder + if save_folder: + resolved_save_folder = resolve_path(save_folder) + StateManager.set_save_folder_path(resolved_save_folder) + else: + default_folder = Config.get_freshness_folder(path) + StateManager.set_save_folder_path(default_folder) + + # Check for existing metadata + if metadata and metadata.info_keyword and metadata.info_query: + StateManager.set_metadata(metadata) + StateManager.set_info_keyword(metadata.info_keyword) + StateManager.set_info_query(metadata.info_query) + StateManager.set_step(StateManager.STEP_METADATA_CONFIRMED) + render_success("기존 메타데이터를 발견했습니다. 검색 단계로 진행합니다.") + else: + StateManager.set_step(StateManager.STEP_NOTE_VALIDATED) + render_success(f"노트가 확인되었습니다: {path.name}") + + st.rerun() + + +def handle_extraction(schema_content: str): + """Handle information extraction from the note.""" + note_path = StateManager.get_raw_note_path() + if not note_path: + render_error("노트 경로를 찾을 수 없습니다.") + return + + # Ensure pandoc is installed before using it + if not ensure_pandoc_installed(): + render_error("Pandoc이 필요합니다. 설치 후 다시 시도해주세요.") + return + + with st.spinner("노트에서 키워드와 쿼리를 추출 중..."): + try: + # Convert markdown to docx using pypandoc + with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_file: + tmp_path = Path(tmp_file.name) + + pypandoc.convert_file(str(note_path), "docx", outputfile=str(tmp_path)) + + # Call Upstage Information Extraction API + client = UpstageClient() + result = client.extract_information(tmp_path, schema_content) + + # Clean up temp file + tmp_path.unlink() + + if not result: + render_error("정보 추출에 실패했습니다.") + return + + # Parse results + info_keyword, info_query = ResponseParser.parse_extraction_result(result) + + if not info_keyword and not info_query: + render_error( + "키워드와 쿼리를 추출하지 못했습니다. 템플릿을 확인해주세요." + ) + return + + # Save to state + StateManager.set_info_keyword(info_keyword) + StateManager.set_info_query(info_query) + StateManager.set_step(StateManager.STEP_EXTRACTION_DONE) + + render_success( + f"추출 완료: {len(info_keyword)}개 키워드, {len(info_query)}개 쿼리" + ) + st.rerun() + + except Exception as e: + render_error(f"추출 중 오류가 발생했습니다: {str(e)}") + + +def handle_metadata_confirmation(keywords: list, queries: list): + """Handle confirmation of extracted metadata.""" + note_path = StateManager.get_raw_note_path() + + # Update note with metadata + success = FileHandler.update_note_metadata( + note_path, info_keyword=keywords, info_query=queries + ) + + if success: + StateManager.set_info_keyword(keywords) + StateManager.set_info_query(queries) + StateManager.set_step(StateManager.STEP_METADATA_CONFIRMED) + render_success("메타데이터가 노트에 저장되었습니다.") + st.rerun() + else: + render_error("메타데이터 저장에 실패했습니다.") + + +def handle_search(): + """Handle Wikipedia and Tavily searches.""" + keywords = StateManager.get_info_keyword() + queries = StateManager.get_info_query() + save_folder = StateManager.get_save_folder_path() + note_path = StateManager.get_raw_note_path() + + wiki_results = [] + tavily_results = [] + + # Wikipedia search + if keywords: + with st.spinner("Wikipedia 검색 중..."): + wiki_client = WikipediaClient(language="ko") + for keyword in keywords: + result = wiki_client.search_and_get_summary(keyword) + if result and result.get("wiki_exists", False): + wiki_results.append(result) + print( + f"Wikipedia 결과: {keyword} -> wiki_exists={result.get('wiki_exists')}" + ) + + # Save wiki results + if wiki_results: + wiki_content = "# Wikipedia 검색 결과\n\n" + for r in wiki_results: + wiki_content += f"## {r['title']} ({r['keyword']})\n\n" + wiki_content += f"{r['summary']}\n\n" + wiki_content += f"[Wikipedia 링크]({r['url']})\n\n---\n\n" + + print(f"save_folder: {save_folder}") + FileHandler.save_search_result(save_folder, "wiki_search", wiki_content) + + # Update note with search timestamp + timestamp = ( + wiki_results[0]["searched_at"] + if wiki_results + else FileHandler.get_current_timestamp() + ) + FileHandler.update_note_metadata(note_path, wiki_searched_at=timestamp) + + # Tavily search + if queries and Config.validate_tavily(): + with st.spinner("Tavily 검색 중..."): + try: + tavily_client = TavilyClient() + for query in queries: + result = tavily_client.search_and_parse(query) + if result: + tavily_results.append(result) + + # Save tavily results + if tavily_results: + tavily_content = "# Tavily 검색 결과\n\n" + for r in tavily_results: + tavily_content += f"## 쿼리: {r['query']}\n\n" + for item in r["results"]: + tavily_content += f"### {item['title']}\n\n" + tavily_content += f"{item['content']}\n\n" + tavily_content += f"[원본 링크]({item['url']})\n\n" + tavily_content += "---\n\n" + + FileHandler.save_search_result( + save_folder, "tavily_search", tavily_content + ) + + # Update note with search timestamp + timestamp = ( + tavily_results[0]["searched_at"] + if tavily_results + else FileHandler.get_current_timestamp() + ) + FileHandler.update_note_metadata( + note_path, tavily_searched_at=timestamp + ) + except ValueError as e: + render_info(f"Tavily 검색을 건너뜁니다: {str(e)}") + elif queries: + render_info("Tavily API 키가 설정되지 않아 검색을 건너뜁니다.") + + # Save results to state + StateManager.set_wiki_results(wiki_results) + StateManager.set_tavily_results(tavily_results) + StateManager.set_step(StateManager.STEP_SEARCH_DONE) + + render_success("검색이 완료되었습니다.") + st.rerun() + + +def handle_guide_generation(): + """Handle freshness guide generation.""" + wiki_results = StateManager.get_wiki_results() + tavily_results = StateManager.get_tavily_results() + note_content = StateManager.get_raw_note_content() + save_folder = StateManager.get_save_folder_path() + note_path = StateManager.get_raw_note_path() + + full_guide = "# 최신성 검토 가이드\n\n" + + client = UpstageClient() + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + + # Generate guide from Wikipedia results + if wiki_results: + with st.spinner("Wikipedia 기반 가이드 생성 중..."): + full_guide += "## Wikipedia 기반 검토\n\n" + + # Load wiki template + wiki_template = loader.load_template("ck_recentness_wiki") + + for result in wiki_results: + if wiki_template: + user_vars = { + "keyword": result["keyword"], + "wiki_title": result["title"], + "wiki_summary": result["summary"], + "note_content": note_content[:3000], + } + user_prompt = wiki_template.format_user_prompt(**user_vars) + guide = client.generate_freshness_guide( + wiki_template.system_prompt, user_prompt + ) + else: + # Fallback if template not found + guide = None + + if guide: + full_guide += f"### {result['keyword']}\n\n{guide}\n\n---\n\n" + + # Generate guide from Tavily results + if tavily_results: + with st.spinner("Tavily 기반 가이드 생성 중..."): + full_guide += "## 웹 검색 기반 검토\n\n" + + # Load tavily template + tavily_template = loader.load_template("ck_recentness_tavily") + + for result in tavily_results: + search_results_text = "" + for item in result["results"]: + search_results_text += f"### {item['title']}\n{item['content']}\n\n" + + if tavily_template: + user_vars = { + "query": result["query"], + "search_results": search_results_text, + "note_content": note_content[:3000], + } + user_prompt = tavily_template.format_user_prompt(**user_vars) + guide = client.generate_freshness_guide( + tavily_template.system_prompt, user_prompt + ) + else: + guide = None + + if guide: + full_guide += f"### {result['query']}\n\n{guide}\n\n---\n\n" + + # Save full guide + FileHandler.save_search_result(save_folder, "rcnt-guide-full", full_guide) + + # Generate summary + with st.spinner("요약 생성 중..."): + summary_template = loader.load_template("ck_recentness_summary") + + if summary_template: + user_vars = {"full_guide": full_guide[:2000]} + summary_prompt = summary_template.format_user_prompt(**user_vars) + summary = client.generate_freshness_guide( + summary_template.system_prompt, summary_prompt + ) + else: + summary = None + + if summary: + # Get relative path for backlink + note_stem = note_path.stem + guide_path = f"{note_stem}/rcnt-guide-full" + + # Insert guide summary into note + FileHandler.insert_freshness_guide(note_path, summary, guide_path) + + StateManager.set_step(StateManager.STEP_GUIDE_GENERATED) + render_success("최신성 검토 가이드가 생성되었습니다!") + st.rerun() + + +def main(): + """Main application entry point.""" + initialize_app() + + # Title + st.title("🔄 최신 정보 확인") + st.caption( + "노트의 정보가 최신인지 확인하고, 최신성 검토 가이드를 노트에 추가합니다!" + ) + + # Check API key + if not validate_api_key(): + return + + # Check pandoc installation + try: + pypandoc.get_pandoc_path() + pandoc_available = True + except (OSError, RuntimeError): + pandoc_available = False + + if not pandoc_available: + st.warning("⚠️ Pandoc이 설치되지 않았습니다. 최신성 검토를 위해 필요합니다.") + + st.markdown("### 설치 방법") + + col1, col2 = st.columns(2) + + with col1: + st.markdown("##### macOS") + st.markdown("**방법 1:** Homebrew 사용 (추천)") + st.code("brew install pandoc") + st.markdown( + "**방법 2:** [공식 인스톨러 다운로드](https://github.com/jgm/pandoc/releases/latest)" + ) + + with col2: + st.markdown("##### Windows") + st.markdown("**방법 1:** winget 사용 (추천)") + st.code("winget install --source winget --exact --id JohnMacFarlane.Pandoc") + st.markdown( + "**방법 2:** [공식 인스톨러 다운로드](https://github.com/jgm/pandoc/releases/latest)" + ) + + st.info("💡 설치 후 새로고침을 해주세요.") + st.stop() + + # Main content based on current step + current_step = StateManager.get_current_step() + + # Step 1: Initial Setup & Validation + if current_step == StateManager.STEP_INIT: + note_path, save_folder = render_file_input_section() + + if st.button("노트 검증", type="primary"): + if note_path: + handle_note_validation(note_path, save_folder) + else: + render_error("노트 경로를 입력해주세요.") + + # Step 2: Template Selection & Extraction + elif current_step == StateManager.STEP_NOTE_VALIDATED: + st.markdown("---") + default_schema = get_default_schema() + schema_content = render_template_selection_section(default_schema) + + if st.button("템플릿 선택 완료", type="primary"): + handle_extraction(schema_content) + + # Step 3: Metadata Review + elif current_step == StateManager.STEP_EXTRACTION_DONE: + st.markdown("---") + keywords = StateManager.get_info_keyword() + queries = StateManager.get_info_query() + + edited_keywords, edited_queries = render_metadata_review_section( + keywords, queries + ) + + if st.button("최신성 메타데이터 확정", type="primary"): + handle_metadata_confirmation(edited_keywords, edited_queries) + + # Step 4: Search + elif current_step == StateManager.STEP_METADATA_CONFIRMED: + st.markdown("---") + st.markdown("## 4. 검색 실행") + + keywords = StateManager.get_info_keyword() + queries = StateManager.get_info_query() + + st.markdown(f"**검색할 키워드:** {', '.join(keywords)}") + st.markdown(f"**검색할 쿼리:** {', '.join(queries)}") + + if st.button("검색 시작", type="primary"): + handle_search() + + # Step 5: Guide Generation + elif current_step == StateManager.STEP_SEARCH_DONE: + st.markdown("---") + wiki_results = StateManager.get_wiki_results() + tavily_results = StateManager.get_tavily_results() + + render_search_results_section(wiki_results, tavily_results) + + st.markdown("---") + if st.button("최신성 가이드 생성", type="primary"): + handle_guide_generation() + + # Step 6: Completion + elif current_step == StateManager.STEP_GUIDE_GENERATED: + st.markdown("---") + st.success("✅ 최신성 검토가 완료되었습니다!") + + save_folder = StateManager.get_save_folder_path() + note_path = StateManager.get_raw_note_path() + + save_folder_display = format_path_for_display( + save_folder, prefer_windows_format=True + ) + note_display = format_path_for_display(note_path, prefer_windows_format=True) + + st.markdown(f"**검색 결과 저장 위치:** `{save_folder_display}`") + st.markdown(f"**업데이트된 노트:** `{note_display}`") + + st.markdown("---") + st.markdown("### 생성된 파일") + st.markdown("- `wiki_search.md`: Wikipedia 검색 결과") + st.markdown("- `tavily_search.md`: Tavily 검색 결과") + st.markdown("- `rcnt-guide-full.md`: 전체 최신성 검토 가이드") + + st.markdown("---") + if st.button("🔄 초기화", type="primary"): + StateManager.reset() + st.rerun() + + +if __name__ == "__main__": + main() diff --git a/usecase/solar-knowledge-management/frontend/note_split.py b/usecase/solar-knowledge-management/frontend/note_split.py new file mode 100644 index 0000000..a7fd92e --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/note_split.py @@ -0,0 +1,499 @@ +"""Main Streamlit application for Atomic Note Weaver.""" +import streamlit as st +import asyncio +import sys +from pathlib import Path +from typing import List, Optional + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +# Define prompts directory +PROMPTS_DIR = project_root / 'prompts' + +from backend.note_split.config import Config +from backend.note_split.models import Topic +from backend.note_split.core.state_manager import StateManager +from backend.note_split.core.file_handler import FileHandler +from backend.note_split.core.path_utils import resolve_path, format_path_for_display +from backend.note_split.llm.client import UpstageClient +from backend.note_split.llm.parsers import ResponseParser +from backend.note_split.llm.prompt_loader import PromptLoader +from backend.note_split.ui.components import ( + render_file_input_section, + render_template_selection_section, + render_topics_list, + render_batch_actions, + render_add_topics_form, + render_error, + render_success, + render_info +) + + +def initialize_app(): + """Initialize the application.""" + Config.ensure_directories() + StateManager.initialize() + + +def validate_api_key() -> bool: + """Validate that API key is configured.""" + if not Config.validate(): + st.error( + "⚠️ Upstage API key not found. Please set UPSTAGE_API_KEY in your .env file." + ) + st.code("UPSTAGE_API_KEY=your_api_key_here") + return False + return True + + +def handle_note_analysis(note_path: str, save_folder: str, instructions: str): + """Handle the note analysis step.""" + # Normalize path for WSL if needed + path = resolve_path(note_path) + + if not path.exists(): + render_error(f"File not found: {note_path}") + return + + if not path.suffix == '.md': + render_error("Please provide a markdown (.md) file.") + return + + # Read the note + content, lines = FileHandler.read_note(path) + if content is None: + render_error("Failed to read the note file.") + return + + # Set paths in state + StateManager.set_raw_note_path(path) + StateManager.set_raw_note_content(content, lines) + StateManager.set_analysis_instructions(instructions) + + # Set save folder + if save_folder: + resolved_save_folder = resolve_path(save_folder) + StateManager.set_save_folder_path(resolved_save_folder) + else: + default_folder = Config.get_atomic_notes_folder(path) + StateManager.set_save_folder_path(default_folder) + + # Move to next step + StateManager.set_step(StateManager.STEP_TEMPLATE_SELECT) + render_success(f"Note loaded successfully: {path.name}") + st.rerun() + + +def handle_template_selection(template_name: str): + """Handle template selection.""" + StateManager.set_selected_template(template_name) + render_success(f"Template selected: {template_name}") + + +def handle_topic_extraction(): + """Handle topic extraction from the note.""" + template_name = StateManager.get_selected_template() + if not template_name: + render_error("Please select a template first.") + return + + # Load template + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + template = loader.load_template(template_name) + if not template: + # 파일이 존재하는지 확인하여 더 구체적인 메시지 제공 + template_path = loader.prompts_dir / f"{template_name}.yml" + if template_path.exists(): + render_error(f"Failed to load template '{template_name}': The YAML file is empty or invalid. Please check the file format.") + else: + render_error(f"Template '{template_name}' not found. Please check the template name.") + return + + # Get note content + content = StateManager.get_raw_note_content() + instructions = StateManager.get_analysis_instructions() + + # Prepare variables for prompt + user_vars = { + 'note_content': content, + 'additional_instructions': instructions if instructions else 'None' + } + + # Call LLM + with st.spinner("Extracting topics from your note..."): + try: + client = UpstageClient() + response = client.generate_with_template_sync(template, user_vars) + + if not response: + render_error("Failed to get response from LLM.") + return + + # Parse topics + topics = ResponseParser.parse_topics_from_json(response) + + if not topics: + render_error("No topics were extracted. Please try again with different instructions.") + return + + # Save topics to state + StateManager.set_topics(topics) + StateManager.set_step(StateManager.STEP_TOPICS_EXTRACTED) + render_success(f"Successfully extracted {len(topics)} topic(s)!") + st.rerun() + + except Exception as e: + render_error(f"Error during topic extraction: {str(e)}") + + +def handle_topic_update(index: int, updated_topic: Topic): + """Handle topic update.""" + # Check if we need to update line numbers + original_topic = StateManager.get_topics()[index] + + needs_line_update = ( + updated_topic.topic != original_topic.topic or + updated_topic.coverage != original_topic.coverage + ) + + # If only use_llm changed, skip line number update and success message + only_llm_changed = ( + updated_topic.topic == original_topic.topic and + updated_topic.coverage == original_topic.coverage and + updated_topic.use_llm != original_topic.use_llm + ) + + if needs_line_update: + # Load line number update template + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + template = loader.load_template('line_number_update') + + if template: + content = StateManager.get_raw_note_content() + user_vars = { + 'note_content': content, + 'topic': updated_topic.topic, + 'coverage': updated_topic.coverage + } + + with st.spinner("Updating line numbers..."): + try: + client = UpstageClient() + response = client.generate_with_template_sync(template, user_vars) + + if response: + new_line_numbers = ResponseParser.parse_line_numbers_from_json(response) + updated_topic.line_numbers = new_line_numbers + + except Exception as e: + st.warning(f"Could not update line numbers automatically: {str(e)}") + + # Update the topic + StateManager.update_topic(index, updated_topic) + + if not only_llm_changed: + render_success("Topic updated successfully!") + else: + st.rerun() + + +def handle_topic_delete(index: int): + """Handle topic deletion.""" + StateManager.delete_topic(index) + render_success("Topic deleted successfully!") + st.rerun() + + +def handle_topic_selection(index: int, selected: bool): + """Handle topic selection toggle.""" + topics = StateManager.get_topics() + topics[index].selected = selected + StateManager.set_topics(topics) + + +def handle_add_topics(guidance: str, num_topics: int, template_name: str): + """Handle adding new topics.""" + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + template = loader.load_template(template_name) + + if not template: + # 파일이 존재하는지 확인하여 더 구체적인 메시지 제공 + template_path = loader.prompts_dir / f"{template_name}.yml" + if template_path.exists(): + render_error(f"Failed to load template '{template_name}': The YAML file is empty or invalid. Please check the file format.") + else: + render_error(f"Template '{template_name}' not found. Please check the template name.") + return + + content = StateManager.get_raw_note_content() + existing_topics = StateManager.get_topics() + existing_topics_str = '\n'.join([f"- {t.topic}: {t.coverage}" for t in existing_topics]) + + user_vars = { + 'note_content': content, + 'existing_topics': existing_topics_str, + 'guidance': guidance if guidance else 'Extract additional relevant topics', + 'num_topics': num_topics + } + + with st.spinner(f"Generating {num_topics} new topic(s)..."): + try: + client = UpstageClient() + response = client.generate_with_template_sync(template, user_vars) + + if not response: + render_error("Failed to get response from LLM.") + return + + new_topics = ResponseParser.parse_topics_from_json(response) + + if not new_topics: + render_error("No new topics were generated.") + return + + # Add new topics to state + for topic in new_topics: + StateManager.add_topic(topic) + + render_success(f"Added {len(new_topics)} new topic(s)!") + st.rerun() + + except Exception as e: + render_error(f"Error generating new topics: {str(e)}") + + +async def generate_atomic_note( + topic: Topic, + lines: List[str], + save_folder: Path +) -> bool: + """Generate and save a single atomic note. + + Args: + topic: Topic object + lines: Lines from the raw note + save_folder: Folder to save the atomic note + + Returns: + True if successful, False otherwise + """ + # Get related content + related_content = FileHandler.get_lines_content(lines, topic.line_numbers) + + # Generate content with LLM if use_llm is enabled + generated_content = None + if topic.use_llm: + try: + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + template = loader.load_template('atomic_note_generate') + + if template: + # Use user_direction if provided, otherwise use DEFAULT_ATOM_DIRECTION + direction = topic.user_direction or Config.DEFAULT_ATOM_DIRECTION + + # If no direction is provided at all, skip LLM generation + if not direction: + print(f"Warning: No direction provided for {topic.topic}. Skipping LLM generation.") + else: + user_vars = { + 'topic': topic.topic, + 'coverage': topic.coverage, + 'related_content': related_content, + 'keywords': ', '.join(topic.keywords), + 'user_direction': direction + } + + client = UpstageClient() + response = await client.generate_with_template(template, user_vars) + + if response: + generated_content = ResponseParser.parse_atomic_note_content(response) + + except Exception as e: + print(f"Warning: Could not generate content for {topic.topic}: {e}") + + # Create atomic note content + note_content = FileHandler.create_atomic_note( + topic, + related_content, + generated_content + ) + + # Save the note + return FileHandler.save_atomic_note(save_folder, topic, note_content) + + +async def handle_generate_atomic_notes_async(topics: List[Topic]): + """Handle atomic note generation (async).""" + lines = StateManager.get_raw_note_lines() + save_folder = StateManager.get_save_folder_path() + raw_note_path = StateManager.get_raw_note_path() + + if not lines or not save_folder or not raw_note_path: + render_error("Missing required data. Please start over.") + return + + # Generate all atomic notes in parallel + progress_bar = st.progress(0, text="Generating atomic notes...") + + tasks = [generate_atomic_note(topic, lines, save_folder) for topic in topics] + results = [] + + for i, task in enumerate(asyncio.as_completed(tasks)): + result = await task + results.append(result) + progress = (i + 1) / len(tasks) + progress_bar.progress(progress, text=f"Generated {i + 1}/{len(tasks)} atomic notes...") + + success_count = sum(results) + + if success_count == 0: + render_error("Failed to generate any atomic notes.") + return + + # Insert backlinks in raw note + modified_lines = FileHandler.insert_backlinks(lines, topics) + + # Append topic list to raw note + modified_lines = FileHandler.append_topic_list(modified_lines, topics) + + # Save modified raw note + if FileHandler.save_raw_note(raw_note_path, modified_lines): + render_success( + f"Successfully generated {success_count}/{len(topics)} atomic note(s)!\n\n" + f"Saved to: {save_folder}" + ) + StateManager.set_step(StateManager.STEP_NOTES_GENERATED) + else: + render_error("Failed to update the raw note with backlinks.") + + +def handle_generate_atomic_notes(): + """Handle atomic note generation (sync wrapper).""" + selected_topics = StateManager.get_selected_topics() + + if not selected_topics: + render_error("Please select at least one topic to generate atomic notes.") + return + + # Run async function + asyncio.run(handle_generate_atomic_notes_async(selected_topics)) + + +def main(): + """Main application entry point.""" + initialize_app() + + # Title + st.title("📝 노트 분할") + st.caption("원시 노트를 원자적이고 상호 연결된 지식으로 변환합니다.") + + # Check API key + if not validate_api_key(): + return + + # Sidebar + with st.sidebar: + st.markdown("## Navigation") + current_step = StateManager.get_current_step() + + st.markdown(f"**Current Step:** {current_step}") + + if st.button("🔄 Reset Application"): + StateManager.reset() + st.rerun() + + st.markdown("---") + st.markdown("### About") + st.markdown( + "이 애플리케이션은 AI 기반 토픽 추출을 사용하여 " + "큰 노트를 원자 노트로 분해하는 데 도움을 줍니다." + ) + + # Main content + current_step = StateManager.get_current_step() + + # Step 1: Initial Setup + if current_step == StateManager.STEP_INIT: + note_path, save_folder, instructions = render_file_input_section() + + if st.button("Start Analysis", type="primary"): + if note_path: + handle_note_analysis(note_path, save_folder, instructions) + else: + render_error("Please provide a note path.") + + # Step 2: Template Selection + elif current_step == StateManager.STEP_TEMPLATE_SELECT: + st.markdown("---") + loader = PromptLoader(prompts_dir=PROMPTS_DIR) + templates = loader.get_templates_info() + + selected_template = render_template_selection_section(templates) + + if selected_template: + handle_template_selection(selected_template) + + if st.button("Extract Topics", type="primary"): + handle_topic_extraction() + + # Step 3: Topic Management + elif current_step in [StateManager.STEP_TOPICS_EXTRACTED, StateManager.STEP_NOTES_GENERATED]: + st.markdown("---") + st.markdown("## 3. Review and Manage Topics") + + # Batch actions + render_batch_actions() + + st.markdown("---") + + # Topics list + topics = StateManager.get_topics() + render_topics_list( + topics, + on_update=handle_topic_update, + on_delete=handle_topic_delete, + on_select=handle_topic_selection + ) + + st.markdown("---") + + # Add topics form + render_add_topics_form(on_add=handle_add_topics) + + st.markdown("---") + + # Generate atomic notes button + st.markdown("## 4. Generate Atomic Notes") + + selected_topics = StateManager.get_selected_topics() + + if selected_topics: + st.info(f"Ready to generate {len(selected_topics)} atomic note(s).") + + if st.button("Generate Atomic Notes", type="primary", use_container_width=True): + handle_generate_atomic_notes() + else: + st.warning("Please select at least one topic to generate atomic notes.") + + # Show results if notes were generated + if current_step == StateManager.STEP_NOTES_GENERATED: + st.markdown("---") + st.success("✅ Atomic notes have been generated!") + + save_folder = StateManager.get_save_folder_path() + raw_note_path = StateManager.get_raw_note_path() + + # Format paths for display (Windows format if in WSL) + save_folder_display = format_path_for_display(save_folder, prefer_windows_format=True) if save_folder else None + raw_note_display = format_path_for_display(raw_note_path, prefer_windows_format=True) if raw_note_path else None + + st.markdown(f"**Atomic notes saved to:** `{save_folder_display}`") + st.markdown(f"**Raw note updated:** `{raw_note_display}`") + + +if __name__ == "__main__": + main() diff --git a/usecase/solar-knowledge-management/frontend/related_note.py b/usecase/solar-knowledge-management/frontend/related_note.py new file mode 100644 index 0000000..33db646 --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/related_note.py @@ -0,0 +1,156 @@ +""" +연관 노트 추천 Streamlit 앱 +""" + +import sys +from pathlib import Path +import streamlit as st + +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from backend.related_note import Related_Note + + +@st.cache_resource +def get_engine(vault_path: str): + """엔진 인스턴스를 캐싱하여 재사용 (Chroma DB 연결 충돌 방지)""" + return Related_Note(vault_path=vault_path) + + +def init_session_state(): + """세션 상태 초기화""" + if "show_input" not in st.session_state: + st.session_state.show_input = False + + +def render_embedding_section(engine): + """임베딩 섹션 렌더링""" + notes_to_embed = engine.get_unembedded_notes() + + st.warning("🌀 아직 임베딩되지 않은 노트가 있습니다.") + st.write(f"총 {len(notes_to_embed)}개 노트가 임베딩 대상입니다:") + + with st.expander("📄 임베딩 대상 노트 목록 보기"): + for note in notes_to_embed: + st.text(f"- {note}") + + if st.button("임베딩 시작하기 🚀"): + with st.spinner("노트 임베딩 중입니다... 시간이 조금 걸릴 수 있습니다."): + engine.index_unembedded_notes() + + st.success("✅ 임베딩이 완료되었습니다!") + st.balloons() + st.rerun() + + +def render_recommendation_section(engine): + """추천 섹션 렌더링""" + st.success("🎉 모든 노트가 이미 임베딩되었습니다!") + st.write("바로 추천 노트를 생성할 수 있습니다.") + + # 단계별 UI + if not st.session_state.show_input: + # STEP 1: 노트 경로 입력 버튼 + if st.button("노트 경로 입력", type="primary"): + st.session_state.show_input = True + st.rerun() + else: + # STEP 2: 텍스트 입력 및 추천 결과 + target_note = st.text_input( + "추천을 받을 노트 경로를 입력 후 Enter를 눌러주세요.", + key="target_note_input", + value=st.session_state.get("last_target_note", ""), + ) + + if target_note: + # 추천 결과가 세션에 없으면 새로 생성 + if ( + "related_results" not in st.session_state + or st.session_state.get("last_target_note") != target_note + ): + with st.spinner("연관 노트를 찾는 중입니다..."): + related = engine.append_related_links(target_note, k=3) + st.session_state.related_results = related + st.session_state.last_target_note = target_note + + # 추천 결과가 있으면 표시 (입력 여부와 무관) + if "related_results" in st.session_state and st.session_state.related_results: + related = st.session_state.related_results + + st.subheader("🔗 추천 노트 3개") + for r in related: + st.markdown(r) + + # 새로고침 버튼 + st.text("") + *_, reset_btn = st.columns([5, 1]) + with reset_btn: + if st.button( + "🔄ㅤ새로고침", + use_container_width=True, + help="처음 단계로 돌아갑니다", + ): + # 연관 노트 페이지 관련 키 초기화 + st.session_state.show_input = False + keys_to_delete = [ + "target_note_input", + "related_results", + "last_target_note", + ] + for key in keys_to_delete: + if key in st.session_state: + del st.session_state[key] + st.rerun() + elif target_note: + st.info("연관된 노트를 찾지 못했습니다.") + + +def main(): + """메인 함수""" + # 세션 상태 초기화 + init_session_state() + + # 메인 헤더 + st.title("📝 연관 노트 추천") + st.caption("업로드한 노트와 관련성 높은 내용을 가진 노트들을 추천받아 보세요!") + st.text("") + + # Vault 경로 확인 + vault_path = st.session_state.get("vault_path", "") + + if not vault_path: + st.warning("👈 왼쪽 사이드바에서 ***Vault 경로*** 를 입력해주세요.") + st.stop() + + # 경로 유효성 검사 + vault_dir = Path(vault_path) + if not vault_dir.exists() or not vault_dir.is_dir(): + st.error(f"❌ 유효하지 않은 경로입니다: {vault_path}") + st.stop() + + # 엔진 초기화 (캐싱됨) + try: + engine = get_engine(vault_path=vault_path) + st.success( + f"""✅ Vault 연결 완료: {vault_path} + +(Vault 경로의 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.)""" + ) + except Exception as e: + st.error(f"❌ 엔진 초기화 실패: {e}") + st.stop() + + # 임베딩 안 된 노트 확인 + notes_to_embed = engine.get_unembedded_notes() + + if not notes_to_embed: + # 모든 노트가 임베딩된 경우: 추천 섹션 + render_recommendation_section(engine) + else: + # 임베딩 안 된 노트가 있는 경우: 임베딩 섹션 + render_embedding_section(engine) + + +if __name__ == "__main__": + main() diff --git a/usecase/solar-knowledge-management/frontend/tag_suggest.py b/usecase/solar-knowledge-management/frontend/tag_suggest.py new file mode 100644 index 0000000..1b015b4 --- /dev/null +++ b/usecase/solar-knowledge-management/frontend/tag_suggest.py @@ -0,0 +1,456 @@ +""" +노트 태그 추천 Streamlit 앱 +""" + +import sys +from pathlib import Path + +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +import re +import time +import traceback +import streamlit as st + +from backend.tag_suggest import ( + TagExtractor, + GuidelineGenerator, + ChecklistType, + TagGenerator, + TagComparator, + TagMatch, + add_yaml_frontmatter, +) + + +def init_session_state(): + """세션 상태 초기화 (태그 추천 전용)""" + # vault_path와 uploaded_file은 공통 요소여서, frontend/app.py 에서 관리 + + # 체크리스트 (태그 생성 가이드라인) + if "checklist" not in st.session_state: + st.session_state.checklist = None + # 기존 태그 + if "existing_tags" not in st.session_state: + st.session_state.existing_tags = [] + # 신규 태그 (생성된 태그) + if "new_tags" not in st.session_state: + st.session_state.new_tags = [] + # 태그 비교 + if "matches" not in st.session_state: + st.session_state.matches = [] + + if "step" not in st.session_state: + st.session_state.step = 1 + + +def render_existing_tags_preview(): + """기존 태그 수집 결과""" + vault_path_str = st.session_state.get("vault_path", "") + if not vault_path_str: + return + + vault_path = Path(vault_path_str.strip()) + if not vault_path.exists(): + st.warning(f"⚠️ Vault 경로를 찾을 수 없습니다: {vault_path_str}") + return + + # 기존 태그가 아직 로드되지 않았다면 로드 + if not st.session_state.existing_tags: + with st.spinner("기존 태그를 수집하는 중..."): + try: + extractor = TagExtractor() + existing_tags = list(extractor.get_unique_tags(str(vault_path))) + st.session_state.existing_tags = existing_tags + except Exception as e: + st.error(f"❌ 태그 수집 실패: {e}") + return + + existing_tags = st.session_state.existing_tags + + # 결과 표시 + with st.expander("📊 기존 태그 미리보기", expanded=False): + if existing_tags: + st.info(f"✓ 총 **{len(existing_tags)}개**의 고유 태그 발견") + + # 빈도순으로 상위 10개 태그 표시 + try: + extractor = TagExtractor() + tag_counts = extractor.count_tags(str(vault_path)) + + # 상위 10개 추출 + top_10_tags = list(tag_counts.items())[:10] + + st.markdown("**상위 10개 태그 (빈도순):**") + st.code(", ".join([tag for tag, _ in top_10_tags])) + + if len(existing_tags) > 10: + st.caption(f"... 외 {len(existing_tags) - 10}개") + except Exception as e: + # 빈도 계산 실패 시 기존 방식으로 폴백 + st.markdown("**태그 목록 (일부):**") + st.code(", ".join(sorted(existing_tags)[:10])) + if len(existing_tags) > 10: + st.caption(f"... 외 {len(existing_tags) - 10}개") + else: + st.warning("⚠️ 기존 태그가 없습니다. 모든 태그가 새로운 태그로 추가됩니다.") + + +def render_checklist_form(): + """체크리스트 설문 폼 렌더링""" + with st.container(border=True): + st.markdown("#### 📝 태그 작성 가이드라인") + col_lang, col_case = st.columns(2) + col_sep, col_num = st.columns(2) + + # 주로 사용하는 언어 + with col_lang: + st.markdown("**1/ 주로 사용하는 언어**") + language = st.radio( + "언어", + options=["en", "ko"], + format_func=lambda x: { + "en": "영어", + "ko": "한국어", + }[x], + label_visibility="collapsed", + key="language_radio", + ) + + # 대소문자 규칙 (영어 사용 시) + with col_case: + st.markdown("**2/ 영어 대소문자 규칙**") + case_style = None + if language in ["en"]: + case_style = st.radio( + "대소문자", + options=["lowercase", "uppercase"], + format_func=lambda x: { + "lowercase": "소문자 (e.g., `upstage`)", + "uppercase": "대문자 (e.g., `UPSTAGE`)", + }[x], + label_visibility="collapsed", + key="case_style_radio", + ) + + # 단어 구분자 + with col_sep: + st.markdown("**3/ 단어 구분자**") + separator = st.radio( + "구분자", + options=["hyphen", "underscore"], + format_func=lambda x: { + "hyphen": "하이픈ㅤㅤ (e.g., `deep-learning`)", + "underscore": "언더스코어 (e.g., `deep_learning`)", + }[x], + label_visibility="collapsed", + key="separator_radio", + ) + + # 태그 개수 + with col_num: + st.markdown("**4/ 태그 개수 범위** (최소 2개, 최대 10개)") + col_min, col_max = st.columns(2) + with col_min: + min_count = st.number_input( + "최소", + min_value=2, + max_value=10, + value=2, + key="min_count_input", + ) + with col_max: + max_count = st.number_input( + "최대", + min_value=2, + max_value=10, + value=5, + key="max_count_input", + ) + + # 최소값이 최대값보다 크면 경고 + if min_count > max_count: + warning_min = st.warning("⚠️ 최소값이 최대값보다 클 수 없습니다.") + time.sleep(1) + warning_min.empty() + # 최대값이 최소값보다 작으면 경고 + elif max_count < min_count: + warning_max = st.warning("⚠️ 최대값이 최소값보다 작을 수 없습니다.") + time.sleep(1) + warning_max.empty() + + + # 체크리스트 생성 (버튼 클릭 -> 과정 실행) + _, guide_ok = st.columns(2) + with guide_ok: + st.markdown("") + # step 3 이상이면 비활성화 (이미 태그가 생성됨) + is_disabled = st.session_state.step >= 3 + if st.button( + "✅ㅤ태그 생성", + use_container_width=True, + type="primary", + disabled=is_disabled, + ): + # 최소/최대 검증 + if min_count > max_count: + error_min = st.error("❌ 최소값이 최대값보다 클 수 없습니다.") + time.sleep(1) + error_min.empty() + elif max_count < min_count: + error_max = st.error("❌ 최대값이 최소값보다 작을 수 없습니다.") + time.sleep(1) + error_max.empty() + + checklist: ChecklistType = { + "language": language, + "separator": separator, + "tag_count_range": {"min": int(min_count), "max": int(max_count)}, + } + + if case_style: + checklist["case_style"] = case_style + + try: + # 유효성 검사 + guideline_gen = GuidelineGenerator(checklist) + st.session_state.checklist = checklist + + # 업로드된 파일 확인 + if not st.session_state.get("uploaded_file"): + st.error("⚠️ 마크다운 파일을 업로드해주세요.") + return + + # 태그 생성 프로세스 시작 + progress_bar = st.progress(0) + status_text = st.empty() + + # 파일 내용 읽기 + uploaded_file = st.session_state.uploaded_file + md_content = uploaded_file.getvalue().decode("utf-8") + filename = uploaded_file.name + + # 1. 태그 생성 + status_text.caption("[1/2] 신규 태그 생성 중 ...") + progress_bar.progress(30) + + tag_gen = TagGenerator() + + new_tags = tag_gen.generate_tags( + guideline_gen, md_content, filename + ) + + st.session_state.new_tags = new_tags + progress_bar.progress(60) + + # 2. 기존 태그와 비교 + status_text.caption("[2/2] 기존 태그와 비교 중 ...") + comparator = TagComparator() + + matches = comparator.compare_tags( + new_tags, st.session_state.existing_tags + ) + st.session_state.matches = matches + + progress_bar.progress(100) + status_text.empty() + progress_bar.empty() + + st.session_state.step = 3 + st.rerun() + + except ValueError as e: + error_valueerror = st.error(f"❌ 오류: {e}") + time.sleep(1) + error_valueerror.empty() + except Exception as e: + st.error(f"❌ 태그 생성 실패: {e}") + with st.expander("상세 오류 정보"): + st.code(traceback.format_exc()) + + +def render_compare_tags(): + """기존, 신규 태그 결과 시각화""" + with st.container(border=True): + st.markdown("#### 📊 태그 비교 결과") + + if not st.session_state.matches: + return + matches: list[TagMatch] = st.session_state.matches + + # 통계 + new_count = sum(1 for m in matches if m.is_new) + matched_count = len(matches) - new_count + + col_new_tags, col_match_tags, col_existing_tags = st.columns(3) + with col_new_tags: + st.metric("신규 태그", f"{new_count}개") + with col_match_tags: + st.metric("매칭된 태그", f"{matched_count}개") + with col_existing_tags: + st.metric("총 태그", f"{len(matches)}개") + + # 상세 결과 + for match in matches: + if match.is_new: + st.success(f"신규 : `{match.new_tag}` (유사도: {match.similarity:.2f})") + else: + st.info( + f"매칭 : `{match.new_tag}`ㅤ→ㅤ`{match.matched_tag}` (유사도: {match.similarity:.2f})" + ) + + # 최종 태그 확인 버튼 + st.text("") + _, col_final_btn = st.columns(2) + with col_final_btn: + if st.button( + "✨ㅤ최종 태그 제안", type="primary", use_container_width=True + ): + st.session_state.step = 4 + st.rerun() + + return matches + + +def render_final_offer(matches): + """최종 태그 제안""" + # 저장 상태 메시지 표시 및 새로고침 버튼 + save_msg_col, *_, reset_btn = st.columns([20, 1, 1, 3]) + + with save_msg_col: + # 저장 결과 메시지 표시 + if st.session_state.get("save_success_msg"): + st.success(st.session_state.save_success_msg) + st.session_state.save_success_msg = None + elif st.session_state.get("save_error_msg"): + st.error(st.session_state.save_error_msg) + st.session_state.save_error_msg = None + + with reset_btn: + if st.button( + "🔄ㅤ새로고침", + use_container_width=True, + help="기존 태그를 수집하는 단계로 돌아갑니다", + ): + # 태그 추천 페이지 관련 키들만 삭제 + keys_to_delete = [ + "step", + "checklist", + "existing_tags", + "new_tags", + "matches", + "save_success_msg", + "save_error_msg", + ] + + for key in keys_to_delete: + if key in st.session_state: + del st.session_state[key] + + init_session_state() + st.rerun() + + with st.container(border=True): + st.markdown("#### ✨ 최종 태그 제안") + + # 정적 메서드로 호출 (인스턴스 생성 불필요) + final_tags = TagComparator.get_final_tags(matches) + + # YAML frontmatter가 추가된 파일 생성 + uploaded_file = st.session_state.uploaded_file + original_content = uploaded_file.getvalue().decode("utf-8") + updated_content = add_yaml_frontmatter(original_content, final_tags) + + # YAML frontmatter 미리보기 (첫 번째 --- 부터 두 번째 --- 까지) + yaml_match = re.match(r"(---\n.*?\n---)", updated_content, re.DOTALL) + if yaml_match: + yaml_preview = yaml_match.group(1) + st.code(yaml_preview, language="yaml") + else: + st.code(updated_content[:200], language="yaml") # fallback + + # 저장 및 다운로드 버튼 + st.text("") + *_, download_btn = st.columns(4) + + with download_btn: + # Vault에 저장 버튼 + vault_path = st.session_state.get("vault_path") + if vault_path and Path(vault_path).exists(): + if st.button( + "💾ㅤVault에 저장", use_container_width=True, type="primary" + ): + try: + save_path = Path(vault_path) / uploaded_file.name + save_path.write_text(updated_content, encoding="utf-8") + st.session_state.save_success_msg = f"✅ 저장 완료: {save_path}" + st.rerun() + except Exception as e: + st.session_state.save_error_msg = f"❌ 저장 실패: {e}" + st.rerun() + else: + # Vault 경로가 없으면 다운로드 버튼 + st.download_button( + label="⬇️ㅤ다운로드", + data=updated_content.encode("utf-8"), + file_name=uploaded_file.name, + mime="text/markdown", + use_container_width=True, + type="primary", + ) + st.caption("💡 YAML frontmatter가 추가된 파일을 저장하세요") + + +def main(): + """메인 함수""" + # 세션 상태 초기화 + init_session_state() + + # 메인 헤더 + st.title("🏷️ 태그 추천") + st.caption("노트에 적합한 태그를 Upstage Solar Pro 2로 추천받아 보세요!") + st.text("") + + # 단계별 렌더링 + # Step 1: 기존 태그 미리보기 + if st.session_state.step == 1: + vault_path = st.session_state.get("vault_path") + uploaded_file = st.session_state.get("uploaded_file") + + if vault_path and uploaded_file: + st.info( + f"- Vault 경로:ㅤ{vault_path}\n" + f"- Markdown 파일:ㅤ{uploaded_file.name}\n\n" + f"**💡 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.**" + ) + if st.button("기존 태그 분석 시작", type="primary"): + st.session_state.step = 2 + st.rerun() + else: + st.warning( + "👈ㅤ왼쪽 사이드바에서 ***Vault 경로*** 와 ***Markdown 파일 업로드*** 설정을 완료해 주세요." + ) + + # Step 2-3: 기존 태그 미리보기 + 태그 작성 가이드라인 + 태그 비교 결과 + if st.session_state.step >= 2 and st.session_state.step < 4: + render_existing_tags_preview() + + col1, col2 = st.columns(2) + + with col1: + render_checklist_form() + + with col2: + # Step 3: 태그 비교 결과 + if st.session_state.step >= 3: + matches = render_compare_tags() + + # Step 4: 최종 추천 태그만 표시 + if st.session_state.step == 4: + matches = st.session_state.matches + render_final_offer(matches) + + +if __name__ == "__main__": + main() diff --git a/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml b/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml new file mode 100644 index 0000000..be1c8b6 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml @@ -0,0 +1,62 @@ +name: atomic_note_generate +description: Generate draft content and writing guidelines for an atomic note based on topic and user direction + +system_prompt: | + You are an expert at creating atomic notes - concise, focused notes on a single topic. + + Your task is to generate TWO distinct sections for an atomic note: + 1. **Draft Content (초안)**: A well-structured draft of the atomic note content + 2. **Writing Guidelines (작성 가이드라인)**: Specific guidelines for refining and expanding the draft + + Base your generation on: + 1. The topic name and coverage + 2. Related content from the original note (related_contexts) + 3. Relevant keywords + 4. User's specific directions (user_direction) + + For the Draft Content: + - Be clear, concise, and focused on the single topic + - Expand on the related content with additional context or explanations + - Connect related concepts and ideas + - Be written in markdown format + - Include relevant examples if appropriate + - Synthesize and expand on the related content rather than simply copying it + + For the Writing Guidelines: + - Provide specific, actionable guidance for improving the draft + - Suggest areas that need expansion or clarification + - Recommend connections to other topics or concepts + - Include formatting or structural suggestions if relevant + - Consider the user's directions when providing guidelines + + DO NOT include: + - The topic name as a heading (this will be added automatically) + - Property metadata (this is handled separately) + - Repetition of the exact content from related_content (synthesize and expand instead) + + Output format (use these exact section headers): + ## Draft Content + [Your draft content here] + + ## Writing Guidelines + [Your writing guidelines here] + +user_prompt_template: | + Topic: {topic} + + Coverage: {coverage} + + Related content from original note: + {related_content} + + Keywords: {keywords} + + User's directions: + {user_direction} + + Please generate: + 1. A draft content (초안) for this atomic note that synthesizes and expands on the related content + 2. Writing guidelines (작성 가이드라인) for refining and improving the draft + + Follow the user's directions and ensure the content is focused on the single topic. + diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml new file mode 100644 index 0000000..c1954eb --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml @@ -0,0 +1,16 @@ +name: ck_recentness_summary +description: Prompt for summarizing the freshness check guide + +system_prompt: | + 당신은 문서 요약 전문가입니다. + 주어진 최신성 검토 가이드라인을 간결하게 요약하여 + 노트 상단에 삽입할 수 있는 형태로 작성합니다. + + 요약은 2-3문장으로 핵심만 담아야 합니다. + +user_prompt_template: | + ## 전체 최신성 검토 가이드 + {full_guide} + + 위 가이드를 2-3문장으로 요약해주세요. + 가장 중요한 업데이트 필요 사항만 간결하게 언급해주세요. diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml new file mode 100644 index 0000000..60c6044 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml @@ -0,0 +1,31 @@ +name: ck_recentness_tavily +description: Prompt for checking note freshness against Tavily search results + +system_prompt: | + 당신은 문서의 최신성을 검토하는 전문가입니다. + 주어진 노트 내용과 웹 검색 결과를 비교하여 노트의 정보가 최신인지 확인하고, + 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 제공합니다. + + 응답은 마크다운 형식으로 작성하며, 다음 구조를 따릅니다: + 1. 검토 요약 + 2. 최신성 상태 (최신/업데이트 필요/확인 필요) + 3. 구체적인 업데이트 권장사항 + 4. 참고할 만한 최신 자료 + +user_prompt_template: | + ## 검색 쿼리 + {query} + + ## 웹 검색 결과 + {search_results} + + ## 원본 노트 내용 + {note_content} + + 위 정보를 바탕으로 원본 노트의 최신성을 검토하고, + 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 작성해주세요. + 특히 다음 사항에 주목해주세요: + - 최근 발표된 새로운 정보 + - 변경된 사항이나 업데이트 + - 노트에 추가하면 좋을 최신 내용 + - 참고할 만한 최신 자료 링크 diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml new file mode 100644 index 0000000..fb998cf --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml @@ -0,0 +1,31 @@ +name: ck_recentness_wiki +description: Prompt for checking note freshness against Wikipedia content + +system_prompt: | + 당신은 문서의 최신성을 검토하는 전문가입니다. + 주어진 노트 내용과 Wikipedia 정보를 비교하여 노트의 정보가 최신인지 확인하고, + 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 제공합니다. + + 응답은 마크다운 형식으로 작성하며, 다음 구조를 따릅니다: + 1. 검토 요약 + 2. 최신성 상태 (최신/업데이트 필요/확인 필요) + 3. 구체적인 업데이트 권장사항 + +user_prompt_template: | + ## 검토할 키워드 + {keyword} + + ## Wikipedia 정보 + **제목:** {wiki_title} + **요약:** {wiki_summary} + + ## 원본 노트 내용 + {note_content} + + 위 정보를 바탕으로 원본 노트의 최신성을 검토하고, + 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 작성해주세요. + 특히 다음 사항에 주목해주세요: + - 날짜나 버전 정보의 변경 + - 새로운 기능이나 업데이트 + - 더 이상 유효하지 않은 정보 + - 추가해야 할 최신 내용 diff --git a/usecase/solar-knowledge-management/prompts/info_extract_schema.yml b/usecase/solar-knowledge-management/prompts/info_extract_schema.yml new file mode 100644 index 0000000..eb48c10 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/info_extract_schema.yml @@ -0,0 +1,18 @@ +name: info_extract_schema +description: Schema for extracting freshness check keywords and queries from a note + +schema: | + { + "type": "object", + "properties": { + "info_keyword": { + "type": "string", + "description": "The most important keyword derived from the document. The keyword must be something that exists as a Wikipedia article." + }, + "info_query": { + "type": "string", + "description": "A Korean search query that should be used with an Internet search API to retrieve the 'most up-to-date' information related to this document. Do not use Year or Month" + } + }, + "required": ["info_keyword", "info_query"] + } diff --git a/usecase/solar-knowledge-management/prompts/info_extract_template.yml b/usecase/solar-knowledge-management/prompts/info_extract_template.yml new file mode 100644 index 0000000..3497ca2 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/info_extract_template.yml @@ -0,0 +1,20 @@ +name: info_extract_template +description: Template for extracting freshness check keywords and queries from a note + +schema: | + { + "type": "object", + "properties": { + "info_keyword": { + "type": "array", + "items": {"type": "string"}, + "description": "Wikipedia 검색에 사용할 핵심 개념 키워드 목록. 문서의 주요 주제, 기술, 개념을 대표하는 단어를 추출합니다. 예: 'Machine Learning', 'Transformer', 'Python'" + }, + "info_query": { + "type": "array", + "items": {"type": "string"}, + "description": "Tavily 웹 검색에 사용할 구체적인 검색 쿼리 목록. 최신 정보, 업데이트, 변경사항을 확인하기 위한 검색어를 작성합니다. 예: 'GPT-4 latest updates 2024', 'Python 3.12 new features'" + } + }, + "required": ["info_keyword", "info_query"] + } diff --git a/usecase/solar-knowledge-management/prompts/line_number_update.yml b/usecase/solar-knowledge-management/prompts/line_number_update.yml new file mode 100644 index 0000000..3075268 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/line_number_update.yml @@ -0,0 +1,30 @@ +name: line_number_update +description: Update line numbers for a topic based on modified topic name or coverage + +system_prompt: | + You are tasked with identifying the relevant line numbers in a note for a given topic. + Analyze the note content and determine which lines discuss or relate to the specified topic. + + Consider: + - Direct mentions of the topic + - Related concepts and context + - Examples and explanations + - Code snippets or data related to the topic + + Output your response as JSON with only the line numbers: + ```json + { + "line_numbers": [1, 2, 3, 5, 7, 10] + } + ``` + +user_prompt_template: | + Note content: + {note_content} + + Topic: {topic} + Coverage: {coverage} + + Please identify all line numbers (1-indexed) in the note that relate to this topic. + Return only the line numbers as a JSON array. + diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml b/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml new file mode 100644 index 0000000..3c954d5 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml @@ -0,0 +1,53 @@ +name: topic_extract_amend_default +description: Extract additional topics that complement existing ones without duplication + +system_prompt: | + You are an expert at analyzing markdown notes and extracting additional topics + that haven't been covered yet. + + You will be provided with: + 1. The original note content + 2. A list of topics that have already been extracted + 3. Guidance on what kind of additional topics to extract + 4. The number of new topics to generate + + Your task is to identify NEW topics that: + - Are distinct from existing topics + - Complement the existing analysis + - Follow the provided guidance + - Represent meaningful concepts from the note + + For each new topic, provide: + 1. A clear, unique topic name (different from existing topics) + 2. A brief overview (coverage) explaining the scope + 3. Line numbers where this topic appears in the note + 4. Relevant keywords + + Output your response as JSON in the following format: + ```json + { + "topics": [ + { + "topic": "New Topic Name", + "coverage": "Brief overview of this new topic", + "line_numbers": [5, 6, 12, 13], + "keywords": ["keyword1", "keyword2"] + } + ] + } + ``` + +user_prompt_template: | + Note content: + {note_content} + + Existing topics (DO NOT duplicate these): + {existing_topics} + + Guidance for new topics: + {guidance} + + Please extract {num_topics} new topic(s) that complement the existing analysis. + Ensure the new topics are distinct and don't overlap with existing ones. + Provide line numbers (1-indexed) for each new topic. + diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_default.yml b/usecase/solar-knowledge-management/prompts/topic_extract_default.yml new file mode 100644 index 0000000..967de2c --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/topic_extract_default.yml @@ -0,0 +1,37 @@ +name: topic_extract_default +description: Default prompt for extracting topics from a raw note + +system_prompt: | + You are an expert at analyzing markdown notes and extracting key topics. + Your task is to identify distinct, meaningful topics from the provided note content. + + For each topic, you should: + 1. Provide a clear, concise topic name + 2. Write a brief overview (coverage) of what the topic encompasses + 3. Identify the line numbers in the original note where this topic is discussed + 4. Extract relevant keywords + + Output your response as JSON in the following format: + ```json + { + "topics": [ + { + "topic": "Topic Name", + "coverage": "Brief overview of what this topic covers", + "line_numbers": [1, 2, 3, 10, 11], + "keywords": ["keyword1", "keyword2", "keyword3"] + } + ] + } + ``` + +user_prompt_template: | + Please analyze the following note and extract key topics: + + {note_content} + + Additional Instructions: {additional_instructions} + + Extract all significant topics from this note. Each topic should represent a distinct concept or theme. + Provide line numbers (1-indexed) where each topic is discussed in the original note. + diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml b/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml new file mode 100644 index 0000000..27d2876 --- /dev/null +++ b/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml @@ -0,0 +1,45 @@ +name: topic_extract_diverse +description: Extract topics with emphasis on diverse perspectives and granular concepts + +system_prompt: | + You are an expert at analyzing markdown notes with a focus on extracting diverse, + granular topics from multiple perspectives. Your task is to identify topics that cover: + + - Technical concepts and implementation details + - Theoretical foundations and principles + - Practical applications and use cases + - Related problems and solutions + - Historical context and evolution + - Future directions and implications + + For each topic, you should: + 1. Provide a clear, descriptive topic name + 2. Write a comprehensive overview explaining the topic's scope + 3. Identify all line numbers where the topic is mentioned or implied + 4. Extract diverse keywords including technical terms, concepts, and related ideas + + Output your response as JSON in the following format: + ```json + { + "topics": [ + { + "topic": "Detailed Topic Name", + "coverage": "Comprehensive overview covering multiple aspects", + "line_numbers": [1, 2, 3, 10, 11, 15], + "keywords": ["technical_term", "concept", "application", "principle"] + } + ] + } + ``` + +user_prompt_template: | + Please analyze the following note and extract topics from multiple perspectives: + + {note_content} + + Additional Instructions: {additional_instructions} + + Extract topics covering different aspects: technical details, concepts, applications, + problems, solutions, and implications. Break down complex ideas into atomic topics. + Provide comprehensive line numbers for each topic. + diff --git a/usecase/solar-knowledge-management/pyproject.toml b/usecase/solar-knowledge-management/pyproject.toml new file mode 100644 index 0000000..8d9e8b3 --- /dev/null +++ b/usecase/solar-knowledge-management/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "upthink" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "langchain-chroma<1.0.0", + "langchain-core>=0.2.43", + "langchain-upstage>=0.1.7", + "openai>=1.109.1", + "python-dotenv>=1.2.1", + "transformers>=4.57.1", + "sentence-transformers>=5.1.2", + "pyyaml>=6.0.3", + "streamlit>=1.50.0", + "httpx>=0.28.1", + "markdown>=3.10", + "tavily-python>=0.7.13", + "pypandoc>=1.16.2", + "wikipedia>=1.4.0", +] diff --git a/usecase/solar-knowledge-management/requirements.txt b/usecase/solar-knowledge-management/requirements.txt new file mode 100644 index 0000000..cedccc7 --- /dev/null +++ b/usecase/solar-knowledge-management/requirements.txt @@ -0,0 +1,14 @@ +langchain-chroma==0.2.2 +langchain-core==0.2.43 +langchain-upstage==0.1.7 +openai==1.109.1 +python-dotenv==1.2.1 +transformers==4.57.1 +sentence-transformers==5.1.2 +pyyaml==6.0.3 +streamlit==1.51.0 +httpx==0.28.1 +markdown==3.10 +tavily-python==0.7.13 +pypandoc==1.16.2 +wikipedia==1.4.0 diff --git a/usecase/solar-knowledge-management/uv.lock b/usecase/solar-knowledge-management/uv.lock new file mode 100644 index 0000000..aca80cb --- /dev/null +++ b/usecase/solar-knowledge-management/uv.lock @@ -0,0 +1,2709 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "altair" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 }, +] + +[[package]] +name = "asgiref" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806 }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626 }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853 }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793 }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930 }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194 }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381 }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750 }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757 }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740 }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197 }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974 }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498 }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853 }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626 }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862 }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544 }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787 }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753 }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587 }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178 }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295 }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700 }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034 }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766 }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449 }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310 }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761 }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553 }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009 }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029 }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907 }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500 }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412 }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486 }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940 }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776 }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922 }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367 }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187 }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752 }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881 }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931 }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313 }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253 }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185 }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656 }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662 }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240 }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152 }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284 }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643 }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698 }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725 }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912 }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "build" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382 }, +] + +[[package]] +name = "cachetools" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280 }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "chroma-hnswlib" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256 } + +[[package]] +name = "chromadb" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "chroma-hnswlib" }, + { name = "fastapi" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "posthog" }, + { name = "pydantic" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/cd/f0f2de3f466ff514fb6b58271c14f6d22198402bb5b71b8d890231265946/chromadb-0.6.3.tar.gz", hash = "sha256:c8f34c0b704b9108b04491480a36d42e894a960429f87c6516027b5481d59ed3", size = 29297929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/8e/5c186c77bf749b6fe0528385e507e463f1667543328d76fd00a49e1a4e6a/chromadb-0.6.3-py3-none-any.whl", hash = "sha256:4851258489a3612b558488d98d09ae0fe0a28d5cad6bd1ba64b96fdc419dc0e5", size = 611129 }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 }, +] + +[[package]] +name = "fastapi" +version = "0.121.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441", size = 342523 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc", size = 109192 }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, +] + +[[package]] +name = "flatbuffers" +version = "25.9.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869 }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 }, +] + +[[package]] +name = "google-auth" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861 }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699 }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885 }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550 }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010 }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264 }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071 }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094 }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } +wheels = [ + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396 }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + +[[package]] +name = "kubernetes" +version = "34.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380 }, +] + +[[package]] +name = "langchain-chroma" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chromadb" }, + { name = "langchain-core" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/be/bf74dc6b721d71b29134d15703bdc167cd26e17667b68ef55a0feb701e7a/langchain_chroma-0.2.2.tar.gz", hash = "sha256:11225ca6077b2bf919b84d74e4d343121e077c0fa3274db1929a270fef9d1002", size = 15890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/ad/02b43c9c6243d430a32fda148236ba02e49289326a58aeef57723f3a92fe/langchain_chroma-0.2.2-py3-none-any.whl", hash = "sha256:7766335f16975c2059bb6e8ea75a59a4082c52e6c9d66827681d1bce2c2756a2", size = 11364 }, +] + +[[package]] +name = "langchain-core" +version = "0.2.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/25/a14a1287e5d7e00eaf8cb3ee3c31d029127f14b945de6fc8e2f0e28e2b12/langchain_core-0.2.43.tar.gz", hash = "sha256:42c2ef6adedb911f4254068b6adc9eb4c4075f6c8cb3d83590d3539a815695f5", size = 316915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/9b/b26405992d807a592ab3e7792f0eb2c2f71fe69111c972caf7786ba99199/langchain_core-0.2.43-py3-none-any.whl", hash = "sha256:619601235113298ebf8252a349754b7c28d3cf7166c7c922da24944b78a9363a", size = 397066 }, +] + +[[package]] +name = "langchain-openai" +version = "0.1.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/cb/98fe365f2e5eee39d0130279959a84182ab414879b666ffc2b9d69b95633/langchain_openai-0.1.25.tar.gz", hash = "sha256:eb116f744f820247a72f54313fb7c01524fba0927120d4e899e5e4ab41ad3928", size = 45224 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/2e/a4430cad7a98e29e9612648f8b12d7449ab635a742c19bf1d62f8713ecaa/langchain_openai-0.1.25-py3-none-any.whl", hash = "sha256:f0b34a233d0d9cb8fce6006c903e57085c493c4f0e32862b99063b96eaedb109", size = 51550 }, +] + +[[package]] +name = "langchain-upstage" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langchain-openai" }, + { name = "pypdf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/a4/7fdf663304546dee43741a6b8fedc2414409a347322e6194d1ff9b2ab755/langchain_upstage-0.1.7.tar.gz", hash = "sha256:521c861596ff9de83efa88f3181e3d60218a74754d230c995dcdad888519d2fc", size = 13130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/ee/373ca56f77795f237ca2a1bfd0c6a801b89ff2c83c3708bd394566f681b7/langchain_upstage-0.1.7-py3-none-any.whl", hash = "sha256:9a7f2c8622c51bb825e2d7731ab373216bde44a9ee0ff51ab3c0d379859ce1ec", size = 16208 }, +] + +[[package]] +name = "langsmith" +version = "0.1.147" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/56/201dd94d492ae47c1bf9b50cacc1985113dc2288d8f15857e1f4a6818376/langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a", size = 300453 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812 }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678 }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874 }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012 }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197 }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840 }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644 }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153 }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684 }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057 }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344 }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325 }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240 }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060 }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781 }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174 }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734 }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493 }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089 }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571 }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806 }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600 }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349 }, + { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209 }, + { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843 }, + { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648 }, + { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164 }, + { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068 }, + { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367 }, + { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306 }, + { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312 }, + { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775 }, + { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178 }, + { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738 }, + { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510 }, + { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053 }, + { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546 }, + { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422 }, + { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135 }, + { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879 }, + { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696 }, + { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421 }, + { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853 }, + { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438 }, + { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409 }, + { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909 }, + { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331 }, + { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085 }, + { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195 }, + { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919 }, + { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160 }, + { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206 }, + { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970 }, + { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063 }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "narwhals" +version = "2.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/dc/8db74daf8c2690ec696c1d772a33cc01511559ee8a9e92d7ed85a18e3c22/narwhals-2.10.2.tar.gz", hash = "sha256:ff738a08bc993cbb792266bec15346c1d85cc68fdfe82a23283c3713f78bd354", size = 584954 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/a9/9e02fa97e421a355fc5e818e9c488080fce04a8e0eebb3ed75a84f041c4a/narwhals-2.10.2-py3-none-any.whl", hash = "sha256:059cd5c6751161b97baedcaf17a514c972af6a70f36a89af17de1a0caf519c43", size = 419573 }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695 }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466 }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836 }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337 }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691 }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898 }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518 }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276 }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610 }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184 }, +] + +[[package]] +name = "openai" +version = "1.109.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020 }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797 }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492 }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648 }, +] + +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525 }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871 }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055 }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061 }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541 }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535 }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703 }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131 }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164 }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859 }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926 }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007 }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314 }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152 }, + { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501 }, + { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862 }, + { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047 }, + { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073 }, + { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597 }, + { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515 }, + { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703 }, + { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311 }, + { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127 }, + { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201 }, + { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872 }, + { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931 }, + { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065 }, + { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310 }, + { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151 }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, +] + +[[package]] +name = "packaging" +version = "23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011 }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, +] + +[[package]] +name = "posthog" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364 }, +] + +[[package]] +name = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593 }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882 }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521 }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445 }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159 }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172 }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477 }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, +] + +[[package]] +name = "pydeck" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pypandoc" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/18/9f5f70567b97758625335209b98d5cb857e19aa1a9306e9749567a240634/pypandoc-1.16.2.tar.gz", hash = "sha256:7a72a9fbf4a5dc700465e384c3bb333d22220efc4e972cb98cf6fc723cdca86b", size = 31477 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/e9/b145683854189bba84437ea569bfa786f408c8dc5bc16d8eb0753f5583bf/pypandoc-1.16.2-py3-none-any.whl", hash = "sha256:c200c1139c8e3247baf38d1e9279e85d9f162499d1999c6aa8418596558fe79b", size = 19451 }, +] + +[[package]] +name = "pypdf" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/65/2ed7c9e1d31d860f096061b3dd2d665f501e09faaa0409a3f0d719d2a16d/pypdf-4.3.1.tar.gz", hash = "sha256:b2f37fe9a3030aa97ca86067a56ba3f9d3565f9a791b305c7355d8392c30d91b", size = 293266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/60/eccdd92dd4af3e4bea6d6a342f7588c618a15b9bec4b968af581e498bcc4/pypdf-4.3.1-py3-none-any.whl", hash = "sha256:64b31da97eda0771ef22edb1bfecd5deee4b72c3d1736b7df2689805076d6418", size = 295825 }, +] + +[[package]] +name = "pypika" +version = "0.48.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259 } + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 }, +] + +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235 }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241 }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079 }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151 }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520 }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699 }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720 }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096 }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465 }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230 }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268 }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100 }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759 }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326 }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736 }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677 }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847 }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800 }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827 }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471 }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578 }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447 }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385 }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642 }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507 }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376 }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907 }, + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830 }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819 }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127 }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767 }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585 }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828 }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509 }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014 }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410 }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593 }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925 }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444 }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968 }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876 }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506 }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433 }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601 }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039 }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407 }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172 }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020 }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451 }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355 }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146 }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656 }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782 }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671 }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749 }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233 }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "safetensors" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797 }, + { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206 }, + { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261 }, + { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117 }, + { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154 }, + { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713 }, + { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835 }, + { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503 }, + { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256 }, + { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281 }, + { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286 }, + { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957 }, + { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926 }, + { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192 }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382 }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042 }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180 }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660 }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057 }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731 }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852 }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094 }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436 }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749 }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906 }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836 }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236 }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593 }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007 }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856 }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306 }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371 }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877 }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103 }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756 }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566 }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877 }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366 }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931 }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081 }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244 }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753 }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912 }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371 }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477 }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678 }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178 }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246 }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469 }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043 }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952 }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512 }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639 }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729 }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251 }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681 }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423 }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027 }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379 }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052 }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183 }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174 }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852 }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595 }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269 }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779 }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128 }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127 }, +] + +[[package]] +name = "sentence-transformers" +version = "5.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pillow" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/96/f3f3409179d14dbfdbea8622e2e9eaa3c8836ddcaecd2cd5ff0a11731d20/sentence_transformers-5.1.2.tar.gz", hash = "sha256:0f6c8bd916a78dc65b366feb8d22fd885efdb37432e7630020d113233af2b856", size = 375185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009 }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679 }, +] + +[[package]] +name = "starlette" +version = "0.49.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340 }, +] + +[[package]] +name = "streamlit" +version = "1.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/6d/327ddd5fc35fcf2aeecb4040668337f5565a1c6c95b1e892b8bfd4bb9031/streamlit-1.51.0.tar.gz", hash = "sha256:1e742a9c0b698f466c6f5bf58d333beda5a1fbe8de660743976791b5c1446ef6", size = 9742904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/60/868371b6482ccd9ef423c6f62650066cf8271fdb2ee84f192695ad6b7a96/streamlit-1.51.0-py3-none-any.whl", hash = "sha256:4008b029f71401ce54946bb09a6a3e36f4f7652cbb48db701224557738cfda38", size = 10171702 }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, +] + +[[package]] +name = "tavily-python" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/fb/b6d6327a78b9107681701d56b3a2dda48dc5420a6ee8b33a147d87bfba60/tavily_python-0.7.13.tar.gz", hash = "sha256:347f92402331d071557f6dd6680f813a7d484b4ba7240905cc397cd192d1355c", size = 17237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/4d/e5e4c65cd66144ac3d0d5a6a2bbfba22eb6a63e6e450beba10ee8413b86d/tavily_python-0.7.13-py3-none-any.whl", hash = "sha256:911825467f2bb19b8162b4766d3e81081160a7c0fb8a15c7c716b2bef73e6296", size = 15484 }, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 }, +] + +[[package]] +name = "tokenizers" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318 }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478 }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994 }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141 }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049 }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730 }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560 }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221 }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569 }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599 }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862 }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250 }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003 }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743 }, + { url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493 }, + { url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162 }, + { url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751 }, + { url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929 }, + { url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978 }, + { url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995 }, + { url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347 }, + { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245 }, + { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804 }, + { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132 }, + { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845 }, + { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558 }, + { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788 }, + { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500 }, + { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659 }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "transformers" +version = "4.57.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925 }, +] + +[[package]] +name = "triton" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410 }, + { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924 }, + { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488 }, + { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192 }, +] + +[[package]] +name = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "upthink" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "langchain-chroma" }, + { name = "langchain-core" }, + { name = "langchain-upstage" }, + { name = "markdown" }, + { name = "openai" }, + { name = "pypandoc" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "sentence-transformers" }, + { name = "streamlit" }, + { name = "tavily-python" }, + { name = "transformers" }, + { name = "wikipedia" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "langchain-chroma", specifier = "<1.0.0" }, + { name = "langchain-core", specifier = ">=0.2.43" }, + { name = "langchain-upstage", specifier = ">=0.1.7" }, + { name = "markdown", specifier = ">=3.10" }, + { name = "openai", specifier = ">=1.109.1" }, + { name = "pypandoc", specifier = ">=1.16.2" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "sentence-transformers", specifier = ">=5.1.2" }, + { name = "streamlit", specifier = ">=1.50.0" }, + { name = "tavily-python", specifier = ">=0.7.13" }, + { name = "transformers", specifier = ">=4.57.1" }, + { name = "wikipedia", specifier = ">=1.4.0" }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "wikipedia" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/35/25e68fbc99e672127cc6fbb14b8ec1ba3dfef035bf1e4c90f78f24a80b7d/wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2", size = 27748 } + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +]