From de3304397797f0e2b422e0ca5a1ad0ff9d7ca041 Mon Sep 17 00:00:00 2001 From: Ara5429 Date: Mon, 30 Mar 2026 00:22:12 +0900 Subject: [PATCH 1/3] feat: Week 5 jobs parse, cover-letter draft API, dashboard, Chroma query alignment - POST /api/jobs/parse and POST /api/cover-letter/draft with Writer graph - Dashboard: job parse, draft, embedding demo copy - GitHub embedding metadata source=github; user_assets OpenAI query_embeddings - Docs, tests, issue/week5.md summary Made-with: Cursor --- .gitignore | 3 + docs/API_GitHub_Spec.md | 127 +-- docs/API_Service_Spec.md | 1 + issue/week5.md | 67 ++ poetry.lock | 891 +++++++++--------- pyproject.toml | 2 +- src/api/cover_letter.py | 175 ++++ src/api/jobs.py | 294 ++++++ src/app/main.py | 4 + src/graphs/writer_graph/edge.py | 10 +- src/graphs/writer_graph/node.py | 279 ++++-- src/graphs/writer_graph/prompts.py | 46 + src/service/github_embedding/pipeline.py | 2 + src/service/rag/__init__.py | 3 +- src/service/rag/passed_samples.py | 167 ++++ src/service/rag/user_assets.py | 41 + src/web/dashboard.py | 176 +++- tests/api/test_cover_letter.py | 321 +++++++ tests/api/test_jobs_parse.py | 97 ++ .../graphs/writer_graph/test_writer_nodes.py | 363 +++++++ tests/service/rag/test_passed_samples.py | 80 ++ tests/service/rag/test_user_assets.py | 47 + 22 files changed, 2601 insertions(+), 595 deletions(-) create mode 100644 issue/week5.md create mode 100644 src/api/cover_letter.py create mode 100644 src/api/jobs.py create mode 100644 src/graphs/writer_graph/prompts.py create mode 100644 src/service/rag/passed_samples.py create mode 100644 tests/api/test_cover_letter.py create mode 100644 tests/api/test_jobs_parse.py create mode 100644 tests/graphs/writer_graph/test_writer_nodes.py create mode 100644 tests/service/rag/test_passed_samples.py diff --git a/.gitignore b/.gitignore index 9a2654b..8a36527 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ __pycache__/ # 로컬 SQLite / Chroma persist (개발·데모용, 커밋 제외) data/autofolio.db data/chroma/ + +# 로컬 쿠키/세션 덤프 (실수 커밋 방지) +cookies.txt diff --git a/docs/API_GitHub_Spec.md b/docs/API_GitHub_Spec.md index 05cf2a5..07921cf 100644 --- a/docs/API_GitHub_Spec.md +++ b/docs/API_GitHub_Spec.md @@ -394,19 +394,23 @@ curl -X GET "https://example.com/api/github/repos/123/commits?author=Ara5429&per --- -## 5. 임베딩 API +## 5. 임베딩 API (구현 기준) + +> **구현 파일:** `src/api/github.py` — `GitHubEmbeddingRequestBody`, `run_github_repo_embedding_job`. +> 과거 초안이었던 `paths[]` / `strategy` / `force_refresh` 필드는 **없다**. 아래가 SSoT다. ### 5.1 POST `/api/github/repos/{repo_id}/embedding` -- **설명:** 지정된 레포/경로에 대해 임베딩을 생성하고 VectorDB/DB에 저장한 뒤, 요약 정보를 반환. +- **설명:** 선택된 레포(`owner/name` 형식, URL 인코딩 시 `owner%2Fname`)에 대해 **코드 문서 id** 목록으로 Chroma `user_assets_{user_id}` 적재 및 SQLite `asset_hierarchy`의 folder/project 행 동기화를 수행한다. +- **전제:** `repo_id`는 **PUT /api/user/selected-repos** 로 저장된 레포여야 한다. 아니면 **403 FORBIDDEN** (`REPO_NOT_SELECTED`). #### 1. Request Syntax ```bash -curl -X POST "https://example.com/api/github/repos/123/embedding" \ - -H "Authorization: Bearer " \ +curl -X POST "https://example.com/api/github/repos/owner%2Frepo-a/embedding" \ + -H "Cookie: session=..." \ -H "Content-Type: application/json" \ - -d '{"paths":["src/","README.md"],"branch":"main","strategy":"code_and_docs_v1","force_refresh":false}' + -d '{"code_document_ids":[],"ref":"main","include_summaries":false}' ``` #### 2. Request Header @@ -417,115 +421,48 @@ curl -X POST "https://example.com/api/github/repos/123/embedding" \ | Authorization | `Bearer ` | Cookie 또는 Authorization 중 하나 필수 | | Content-Type | `application/json` | Y | -#### 3. Request Element +#### 3. Request Body (JSON) | 파라미터 | 타입 | 필수 | 설명 | |----------|------|------|------| -| repo_id | integer \| string | Y | Path. GitHub 레포 ID 또는 `"owner/name"` | -| paths | array<string> | Y | 비어 있지 않은 배열 | -| branch | string | N | default=레포 default_branch | -| strategy | string | N | default=code_and_docs_v1 | -| force_refresh | boolean | N | default=false | - -### paths[] 선택 레벨 규칙 - -| 선택 레벨 | UI 동작 | paths[] 예시 | -|----------|---------|-------------| -| 레포 전체 | 레포 체크박스 선택 | `["/"]` | -| 폴더 선택 | 폴더 펼치고 체크 | `["src/", "docs/"]` | -| 파일 개별 선택 | 파일 체크박스 | `["src/main.py", "README.md"]` | -| 혼합 | 폴더 + 개별 파일 | `["src/auth/", "README.md"]` | +| code_document_ids | array<string> | N | 임베딩할 코드 문서 id. Chroma id 규칙: ``{owner/repo}/{repo내 상대경로}`` (예: `owner/repo/src/app.py`). **비어 있으면** SQLite `asset_hierarchy`에서 해당 레포의 `type=code` id를 읽어 전부 사용한다. | +| ref | string | N | 브랜치 또는 SHA (GitHub Contents API 조회에 사용) | +| include_summaries | boolean | N | `true`이면 응답에 요약 텍스트 배열 포함(응답 크기 증가) | -**백엔드 처리 규칙:** +**권장 흐름 (대시보드와 동일):** -- `"/"` → 레포 전체 트리 순회 (루트부터 모든 파일 포함) -- `"src/"` (끝이 `/`) → 해당 폴더 하위 전체 재귀 포함 -- `"src/main.py"` (끝이 파일명) → 해당 파일만 -- **중복 경로 자동 제거:** 폴더 + 그 안의 파일 동시 선택 시 폴더 기준 합산 +1. **POST /api/user/asset-hierarchy/sync-from-assets** — `selected_repo_assets`의 code 경로를 `asset_hierarchy`에 반영 +2. **POST /api/github/repos/{owner%2Frepo}/embedding** — `code_document_ids: []` 로 DB에 있는 code id만으로 임베딩 + 또는 Files Tree에서 고른 파일로 `owner/repo/경로` id를 채워 **명시 모드**로 호출 -**전제 조건:** - -- 임베딩은 반드시 **selected_repos**에 등록된 레포에 대해서만 수행 가능. selected_repos에 없는 repo_id로 호출 시 **403 FORBIDDEN** 반환. +`code_document_ids`가 비어 있고, `asset_hierarchy`에 해당 레포의 code 행이 없으면 **400 BAD_REQUEST** (`NO_CODE_ASSETS_IN_HIERARCHY`). #### 4. Response -**200 OK (임베딩 생성 및 저장 완료)** - -```json -{ - "repo_id": 123, - "branch": "main", - "paths": ["src/", "README.md"], - "strategy": "code_and_docs_v1", - "status": "completed", - "embedding": { - "chunks_indexed": 128, - "dimensions": 1536, - "total_tokens": 45231, - "storage": { - "type": "vectordb", - "index_name": "autofolio_github_repo_123_main", - "last_updated_at": "2026-03-04T10:23:45Z" - } - }, - "hierarchy_nodes_created": 243 -} -``` - -**200 OK (캐시 히트, 이미 최신 상태)** +**200 OK** — 본문 예: ```json { - "repo_id": 123, - "branch": "main", - "paths": ["src/"], - "strategy": "code_and_docs_v1", - "status": "completed", - "embedding": { - "chunks_indexed": 128, - "dimensions": 1536, - "total_tokens": 45231, - "storage": { - "type": "vectordb", - "index_name": "autofolio_github_repo_123_main", - "last_updated_at": "2026-03-01T09:00:00Z" - }, - "cache_hit": true - }, - "hierarchy_nodes_created": 0 + "status": "ok", + "embedded": 12, + "ids": ["owner/repo/src/main.py", "owner/repo/README.md"] } ``` -| 필드 | 타입 | 설명 | -|------|------|------| -| repo_id | integer | GitHub 레포 ID | -| branch | string | 브랜치명 | -| paths | array<string> | 요청한 경로 목록 | -| strategy | string | 사용된 임베딩 전략 | -| status | string | completed 등 | -| embedding | object | chunks_indexed, dimensions, total_tokens, storage 등 | -| hierarchy_nodes_created | integer | asset_hierarchy 테이블에 생성된 노드 수 (code+folder+project 합산). 캐시 히트 시 0 | - -#### asset_hierarchy 연동 - -임베딩 생성 시 **asset_hierarchy** 테이블이 함께 갱신된다. +| 필드 | 설명 | +|------|------| +| status | `"ok"` | +| embedded | Chroma에 적재한 문서 수(코드·폴더·프로젝트 노드 합산에 가깝게 카운트) | +| ids | 저장된 문서 id 목록 | +| summaries | `include_summaries=true`일 때만. `{ id, type, path, summary }` 형태 배열 | -| API 호출 | DB 동작 | -|----------|---------| -| PUT /api/user/selected-repos | selected_repos upsert | -| POST /api/github/repos/{id}/embedding | asset_hierarchy 전체 재생성 + ChromaDB upsert | - -- `asset_hierarchy.id` = ChromaDB `user_assets_{user_id}`의 document id와 동일. -- RAPTOR bottom-up 순서: **code → folder → project** -- 재임베딩 시 해당 **selected_repo_id**의 asset_hierarchy 행 전부 삭제 후 재생성. +파이프라인은 코드 파일 → 폴더(bottom-up) → 프로젝트 루트 순으로 요약·임베딩한다. `asset_hierarchy`의 folder/project 행은 임베딩 후 **선택된 code_document_ids** 기준으로 동기화된다. | 상태코드 | error | 발생조건 | |----------|-------|----------| -| 400 | BAD_REQUEST | paths가 비어 있거나 배열이 아님 | +| 400 | BAD_REQUEST | 잘못된 repo_id, 또는 임베딩할 code id 없음 | | 401 | UNAUTHORIZED | 로그인 필요 | -| 403 | FORBIDDEN | 해당 repo_id가 selected_repos에 등록되지 않음 | -| 404 | NOT_FOUND | 레포 없음 | -| 409 | EMBEDDING_IN_PROGRESS | 해당 레포/브랜치에 대한 임베딩 작업이 이미 진행 중 | -| 502 | EMBEDDING_FAILED | 임베딩 생성 중 오류 | +| 403 | FORBIDDEN | 레포가 selected_repos에 없음 | +| 500 | INTERNAL_SERVER_ERROR | `EMBEDDING_FAILED` 등 서버 처리 실패 | > 공통 에러(400/401/403/404/500/502)는 공통 규칙 참고. diff --git a/docs/API_Service_Spec.md b/docs/API_Service_Spec.md index 5592e20..fd63ea8 100644 --- a/docs/API_Service_Spec.md +++ b/docs/API_Service_Spec.md @@ -94,6 +94,7 @@ curl -X POST "https://example.com/api/user/documents" \ ### 1.1 POST `/api/jobs/parse` +- **구현:** `src/api/jobs.py` — `manual`은 즉시 DB 저장, `url`은 HTTP 수집 후 `OPENAI_API_KEY`가 있으면 JSON 구조화, 없으면 본문 스니펫을 `duties` 등에 휴리스틱 저장. - **설명:** 채용공고 입력을 두 가지 방식으로 받아 **담당 업무 / 자격 요건 / 우대 사항 / 기업명 / 기업 인재상 / 포지션명** 6개 항목을 확보한 뒤 **jobs 테이블에 저장**하고 `job_id`를 반환한다. - **source_type=url:** `url` 필수. 서버가 해당 URL을 크롤링 후 LLM으로 파싱. url 기준으로 신규 저장하며, 항상 jobs에 한 건을 남긴다. 크롤링 실패 시 400 CRAWL_FAILED. - **source_type=manual:** url 없음. 사용자가 6개 항목 직접 입력. position_title, company_name 필수, 나머지 선택. 항상 신규 저장(UUID id, 중복 체크 없음). diff --git a/issue/week5.md b/issue/week5.md new file mode 100644 index 0000000..bcdbef4 --- /dev/null +++ b/issue/week5.md @@ -0,0 +1,67 @@ +# Week 5 작업 정리 + +## 개요 + +자기소개서 Writer(코드 RAG + 채용공고 맥락), 채용공고 파싱 API, 대시보드 연동, GitHub 임베딩·RAG 검색 일관성 수정을 포함한 한 주차 작업입니다. + +## API·백엔드 + +### 채용공고 저장 (parse) + +- **`POST /api/jobs/parse`**: SQLite `jobs` 테이블에 공고 저장 후 `job_id` 반환. +- **manual**: 본문 직접 입력. +- **url**: 페이지를 가져와 파싱; `OPENAI_API_KEY`가 있으면 구조화 보조. + +### 자소서 Draft (Writer) + +- **`POST /api/cover-letter/draft`**: GitHub 로그인 세션 필수. 선택 `job_id`로 `jobs` + `job_parsed` 맥락 로드. +- LangGraph Writer: `retrieve_samples` → `load_assets` → `generate_draft` → `self_consistency` → `format_output`. +- 구현: `src/api/cover_letter.py`, `src/graphs/writer_graph/` (노드, 엣지, 프롬프트). + +### 앱 등록 + +- `src/app/main.py`에 jobs·cover letter 라우터 등록. + +## GitHub 코드 임베딩·RAG + +### 데모 흐름 (대시보드) + +- `asset_hierarchy` ↔ `selected_repo_assets` 동기화, GitHub Contents API, (옵션) OpenAI 요약·임베딩, Chroma `user_assets_{user_id}`. +- 문서: `docs/API_GitHub_Spec.md` (임베딩 바디 등 실제 코드와 맞춤). + +### 버그 수정 (핵심) + +1. **`source` 메타데이터** + GitHub 파이프라인 `_normalize_metadata`에 `source: github` 추가. Chroma `where` 필터와 스키마 정합. + +2. **`load_assets` 필터** + 과거 문서에 `source`가 없을 수 있어 `retrieve_user_assets(..., source_filter=None)`로 조회. + +3. **임베딩 공간 불일치 (Draft 빈 응답 원인)** + - 적재: OpenAI `text-embedding-3-small`. + - 조회: `query_texts`만 쓰면 컬렉션 기본 임베더(MiniLM 등)가 사용되어 차원·공간이 달라짐. + - **해결**: `OPENAI_API_KEY`가 있을 때 STAR 쿼리를 동일 `OpenAIEmbedder`로 임베딩한 뒤 `query_embeddings`로 검색 (`src/service/rag/user_assets.py`). + +## 프론트·대시보드 + +- `src/web/dashboard.py`: Job parse 패널(`job_id` 자동 채움), Cover letter Draft 패널, GitHub 임베딩 안내 문구 등. + +## 의존성 + +- `pyproject.toml` / `poetry.lock`: 예) `httpx` 등 공고 URL fetch용. + +## 테스트 + +- `tests/api/test_cover_letter.py`, `tests/api/test_jobs_parse.py` +- `tests/service/rag/test_user_assets.py` (OpenAI 쿼리 경로·기존 STAR 쿼리) +- `tests/graphs/writer_graph/` 등 Writer 노드 +- 기준: 전체 pytest 통과(주차 마감 시점 기준). + +## 문서 + +- `docs/API_GitHub_Spec.md`, `docs/API_Service_Spec.md` 갱신. + +## 참고 + +- Draft 응답에 `error`가 있어도 HTTP 200에서 본문만 비는 경우가 있었음 → API 레이어에서 그래프 `error`를 노출하는 개선은 후속 과제로 남길 수 있음. +- `used_assets.github_repos`는 응답 스키마 상 플레이스홀더에 가까움. diff --git a/poetry.lock b/poetry.lock index 5914c37..065f6f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,13 +39,13 @@ files = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.13.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, ] [package.dependencies] @@ -54,17 +54,17 @@ idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] +trio = ["trio (>=0.32.0)"] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] @@ -178,13 +178,13 @@ lxml = ["lxml"] [[package]] name = "build" -version = "1.4.0" +version = "1.4.2" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.9" files = [ - {file = "build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596"}, - {file = "build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936"}, + {file = "build-1.4.2-py3-none-any.whl", hash = "sha256:7a4d8651ea877cb2a89458b1b198f2e69f536c95e89129dbf5d448045d60db88"}, + {file = "build-1.4.2.tar.gz", hash = "sha256:35b14e1ee329c186d3f08466003521ed7685ec15ecffc07e68d706090bf161d1"}, ] [package.dependencies] @@ -200,135 +200,151 @@ virtualenv = ["virtualenv (>=20.11)", "virtualenv (>=20.17)", "virtualenv (>=20. [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.6" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, ] [[package]] @@ -444,18 +460,18 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.135.1" +version = "0.135.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" files = [ - {file = "fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e"}, - {file = "fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd"}, + {file = "fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5"}, + {file = "fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56"}, ] [package.dependencies] annotated-doc = ">=0.0.2" -pydantic = ">=2.7.0" +pydantic = ">=2.9.0" starlette = ">=0.46.0" typing-extensions = ">=4.8.0" typing-inspection = ">=0.4.2" @@ -488,13 +504,13 @@ files = [ [[package]] name = "fsspec" -version = "2026.2.0" +version = "2026.3.0" description = "File-system specification" optional = false python-versions = ">=3.10" files = [ - {file = "fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437"}, - {file = "fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff"}, + {file = "fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4"}, + {file = "fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41"}, ] [package.extras] @@ -527,81 +543,81 @@ tqdm = ["tqdm"] [[package]] name = "googleapis-common-protos" -version = "1.73.0" +version = "1.73.1" description = "Common protobufs used in Google APIs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8"}, - {file = "googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a"}, + {file = "googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8"}, + {file = "googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +protobuf = ">=4.25.8,<8.0.0" [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" -version = "3.3.1" +version = "3.3.2" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.10" files = [ - {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4"}, - {file = "greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8"}, - {file = "greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f"}, - {file = "greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b"}, - {file = "greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4"}, - {file = "greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149"}, - {file = "greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a"}, - {file = "greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1"}, - {file = "greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2"}, - {file = "greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946"}, - {file = "greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d"}, - {file = "greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1"}, - {file = "greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a"}, - {file = "greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79"}, - {file = "greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249"}, - {file = "greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451"}, - {file = "greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98"}, + {file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"}, + {file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"}, + {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, + {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, + {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, + {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, + {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, + {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, + {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, + {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, + {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, + {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, + {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, + {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, + {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, + {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, + {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, ] [package.extras] @@ -831,13 +847,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "1.7.1" +version = "1.8.0" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.9.0" files = [ - {file = "huggingface_hub-1.7.1-py3-none-any.whl", hash = "sha256:38c6cce7419bbde8caac26a45ed22b0cea24152a8961565d70ec21f88752bfaa"}, - {file = "huggingface_hub-1.7.1.tar.gz", hash = "sha256:be38fe66e9b03c027ad755cb9e4b87ff0303c98acf515b5d579690beb0bf3048"}, + {file = "huggingface_hub-1.8.0-py3-none-any.whl", hash = "sha256:d3eb5047bd4e33c987429de6020d4810d38a5bef95b3b40df9b17346b7f353f2"}, + {file = "huggingface_hub-1.8.0.tar.gz", hash = "sha256:c5627b2fd521e00caf8eff4ac965ba988ea75167fad7ee72e17f9b7183ec63f3"}, ] [package.dependencies] @@ -1068,13 +1084,13 @@ jsonpointer = ">=1.9" [[package]] name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" +version = "3.1.1" +description = "Identify specific nodes in a JSON document (RFC 6901) " optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, + {file = "jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca"}, + {file = "jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900"}, ] [[package]] @@ -1140,13 +1156,13 @@ google-auth = ["google-auth (>=1.0.1)"] [[package]] name = "langchain-core" -version = "1.2.19" +version = "1.2.23" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0.0,>=3.10.0" files = [ - {file = "langchain_core-1.2.19-py3-none-any.whl", hash = "sha256:6e74cb0fb443a8046ee298c05c99b67abe54cc57fcbc6d1cd3b0f2485ee47574"}, - {file = "langchain_core-1.2.19.tar.gz", hash = "sha256:87fa82c3eb4cc3d7a65f574cb447b5df09ec2131c8c2a0a02d4737ad02685438"}, + {file = "langchain_core-1.2.23-py3-none-any.whl", hash = "sha256:70866dfc5275b7840ce272ff70f0ff216c8666ab25dc1b41964a4ef58c02a3ff"}, + {file = "langchain_core-1.2.23.tar.gz", hash = "sha256:fdec64f90cfea25317e88d9803c44684af1f4e30dec4e58320dd7393bb0f0785"}, ] [package.dependencies] @@ -1161,29 +1177,29 @@ uuid-utils = ">=0.12.0,<1.0" [[package]] name = "langchain-openai" -version = "1.1.11" +version = "1.1.12" description = "An integration package connecting OpenAI and LangChain" optional = false python-versions = "<4.0.0,>=3.10.0" files = [ - {file = "langchain_openai-1.1.11-py3-none-any.whl", hash = "sha256:a03596221405d38d6852fb865467cb0d9ff9e79f335905eb6a576e8c4874ac71"}, - {file = "langchain_openai-1.1.11.tar.gz", hash = "sha256:44b003a2960d1f6699f23721196b3b97d0c420d2e04444950869213214b7a06a"}, + {file = "langchain_openai-1.1.12-py3-none-any.whl", hash = "sha256:da71ca3f2d18c16f7a2443cc306aa195ad2a07054335ac9b0626dcae02b6a0c5"}, + {file = "langchain_openai-1.1.12.tar.gz", hash = "sha256:ccf5ef02c896f6807b4d0e51aaf678a72ce81ae41201cae8d65e11eeff9ecb79"}, ] [package.dependencies] -langchain-core = ">=1.2.18,<2.0.0" +langchain-core = ">=1.2.21,<2.0.0" openai = ">=2.26.0,<3.0.0" tiktoken = ">=0.7.0,<1.0.0" [[package]] name = "langgraph" -version = "1.0.10" +version = "1.1.3" description = "Building stateful, multi-actor applications with LLMs" optional = false python-versions = ">=3.10" files = [ - {file = "langgraph-1.0.10-py3-none-any.whl", hash = "sha256:7c298bef4f6ea292fcf9824d6088fe41a6727e2904ad6066f240c4095af12247"}, - {file = "langgraph-1.0.10.tar.gz", hash = "sha256:73bd10ee14a8020f31ef07e9cd4c1a70c35cc07b9c2b9cd637509a10d9d51e29"}, + {file = "langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546"}, + {file = "langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370"}, ] [package.dependencies] @@ -1226,13 +1242,13 @@ langgraph-checkpoint = ">=2.1.0,<5.0.0" [[package]] name = "langgraph-sdk" -version = "0.3.9" +version = "0.3.12" description = "SDK for interacting with LangGraph API" optional = false python-versions = ">=3.10" files = [ - {file = "langgraph_sdk-0.3.9-py3-none-any.whl", hash = "sha256:94654294250c920789b6ed0d8a70c0117fed5736b61efc24ff647157359453c5"}, - {file = "langgraph_sdk-0.3.9.tar.gz", hash = "sha256:8be8958529b3f6d493ec248fdb46e539362efda75784654a42a7091d22504e0e"}, + {file = "langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327"}, + {file = "langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71"}, ] [package.dependencies] @@ -1241,13 +1257,13 @@ orjson = ">=3.11.5" [[package]] name = "langsmith" -version = "0.7.9" +version = "0.7.22" description = "Client library to connect to the LangSmith Observability and Evaluation Platform." optional = false python-versions = ">=3.10" files = [ - {file = "langsmith-0.7.9-py3-none-any.whl", hash = "sha256:e73478f4c4ae9b7407e0fcdced181f9f8b0e024c62a1552dbf0667ef6b19e82d"}, - {file = "langsmith-0.7.9.tar.gz", hash = "sha256:c6dfcc4cb8fea249714ac60a1963faa84cc59ded9cd1882794ffce8a8d1d1588"}, + {file = "langsmith-0.7.22-py3-none-any.whl", hash = "sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4"}, + {file = "langsmith-0.7.22.tar.gz", hash = "sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04"}, ] [package.dependencies] @@ -1568,13 +1584,13 @@ sympy = "*" [[package]] name = "openai" -version = "2.28.0" +version = "2.30.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.9" files = [ - {file = "openai-2.28.0-py3-none-any.whl", hash = "sha256:79aa5c45dba7fef84085701c235cf13ba88485e1ef4f8dfcedc44fc2a698fc1d"}, - {file = "openai-2.28.0.tar.gz", hash = "sha256:bb7fdff384d2a787fa82e8822d1dd3c02e8cf901d60f1df523b7da03cbb6d48d"}, + {file = "openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d"}, + {file = "openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885"}, ] [package.dependencies] @@ -1585,7 +1601,7 @@ jiter = ">=0.10.0,<1" pydantic = ">=1.9.0,<3" sniffio = "*" tqdm = ">4" -typing-extensions = ">=4.11,<5" +typing-extensions = ">=4.14,<5" [package.extras] aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"] @@ -1895,21 +1911,21 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "6.33.5" +version = "6.33.6" description = "" optional = false python-versions = ">=3.9" files = [ - {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, - {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, - {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"}, - {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"}, - {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"}, - {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"}, - {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, + {file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"}, + {file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"}, + {file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"}, + {file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"}, + {file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"}, + {file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"}, + {file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"}, ] [[package]] @@ -2332,13 +2348,13 @@ dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", " [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -2428,13 +2444,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, ] [package.extras] @@ -2540,147 +2556,148 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.2.28" +version = "2026.3.32" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" files = [ - {file = "regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d"}, - {file = "regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8"}, - {file = "regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c"}, - {file = "regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d"}, - {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318"}, - {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b"}, - {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e"}, - {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e"}, - {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451"}, - {file = "regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a"}, - {file = "regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5"}, - {file = "regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff"}, - {file = "regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9"}, - {file = "regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97"}, - {file = "regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703"}, - {file = "regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098"}, - {file = "regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2"}, - {file = "regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64"}, - {file = "regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022"}, - {file = "regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1"}, - {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a"}, - {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27"}, - {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae"}, - {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea"}, - {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b"}, - {file = "regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15"}, - {file = "regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61"}, - {file = "regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a"}, - {file = "regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7"}, - {file = "regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d"}, - {file = "regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d"}, - {file = "regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc"}, - {file = "regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8"}, - {file = "regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d"}, - {file = "regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4"}, - {file = "regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05"}, - {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5"}, - {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59"}, - {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf"}, - {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae"}, - {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b"}, - {file = "regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c"}, - {file = "regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4"}, - {file = "regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952"}, - {file = "regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784"}, - {file = "regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a"}, - {file = "regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d"}, - {file = "regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95"}, - {file = "regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472"}, - {file = "regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96"}, - {file = "regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92"}, - {file = "regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11"}, - {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881"}, - {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3"}, - {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215"}, - {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944"}, - {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768"}, - {file = "regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081"}, - {file = "regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff"}, - {file = "regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e"}, - {file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f"}, - {file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b"}, - {file = "regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8"}, - {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb"}, - {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1"}, - {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2"}, - {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a"}, - {file = "regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341"}, - {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25"}, - {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c"}, - {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b"}, - {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f"}, - {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550"}, - {file = "regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc"}, - {file = "regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8"}, - {file = "regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b"}, - {file = "regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc"}, - {file = "regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd"}, - {file = "regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff"}, - {file = "regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911"}, - {file = "regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33"}, - {file = "regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117"}, - {file = "regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d"}, - {file = "regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a"}, - {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf"}, - {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952"}, - {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8"}, - {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07"}, - {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6"}, - {file = "regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6"}, - {file = "regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7"}, - {file = "regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d"}, - {file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e"}, - {file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c"}, - {file = "regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7"}, - {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e"}, - {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc"}, - {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8"}, - {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0"}, - {file = "regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b"}, - {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b"}, - {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033"}, - {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43"}, - {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18"}, - {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a"}, - {file = "regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e"}, - {file = "regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9"}, - {file = "regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec"}, - {file = "regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2"}, + {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:462a041d2160090553572f6bb0be417ab9bb912a08de54cb692829c871ee88c1"}, + {file = "regex-2026.3.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c6f6b027d10f84bfe65049028892b5740878edd9eae5fea0d1710b09b1d257"}, + {file = "regex-2026.3.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:879ae91f2928a13f01a55cfa168acedd2b02b11b4cd8b5bb9223e8cde777ca52"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:887a9fa74418d74d645281ee0edcf60694053bd1bc2ebc49eb5e66bfffc6d107"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d571f0b2eec3513734ea31a16ce0f7840c0b85a98e7edfa0e328ed144f9ef78f"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ada7bd5bb6511d12177a7b00416ce55caee49fbf8c268f26b909497b534cacb"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:918db4e34a7ef3d0beee913fa54b34231cc3424676f1c19bdb85f01828d3cd37"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:69a847a6ffaa86e8af7b9e7037606e05a6f663deec516ad851e8e05d9908d16a"}, + {file = "regex-2026.3.32-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c8d402ea3dfe674288fe3962016affd33b5b27213d2b5db1823ffa4de524c57"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6b39a2cc5625bbc4fda18919a891eab9aab934eecf83660a90ce20c53621a9a"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f7cc00089b4c21847852c0ad76fb3680f9833b855a0d30bcec94211c435bff6b"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd03e38068faeef937cc6761a250a4aaa015564bd0d61481fefcf15586d31825"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e006ea703d5c0f3d112b51ba18af73b58209b954acfe3d8da42eacc9a00e4be6"}, + {file = "regex-2026.3.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6980ceb5c1049d4878632f08ba0bf7234c30e741b0dc9081da0f86eca13189d3"}, + {file = "regex-2026.3.32-cp310-cp310-win32.whl", hash = "sha256:6128dd0793a87287ea1d8bf16b4250dd96316c464ee15953d5b98875a284d41e"}, + {file = "regex-2026.3.32-cp310-cp310-win_amd64.whl", hash = "sha256:5aa78c857c1731bdd9863923ffadc816d823edf475c7db6d230c28b53b7bdb5e"}, + {file = "regex-2026.3.32-cp310-cp310-win_arm64.whl", hash = "sha256:34c905a721ddee0f84c99e3e3b59dd4a5564a6fe338222bc89dd4d4df166115c"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d7855f5e59fcf91d0c9f4a51dc5d8847813832a2230c3e8e35912ccf20baaa2"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18eb45f711e942c27dbed4109830bd070d8d618e008d0db39705f3f57070a4c6"}, + {file = "regex-2026.3.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed3b8281c5d0944d939c82db4ec2300409dd69ee087f7a75a94f2e301e855fb4"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad5c53f2e8fcae9144009435ebe3d9832003508cf8935c04542a1b3b8deefa15"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70c634e39c5cda0da05c93d6747fdc957599f7743543662b6dbabdd8d3ba8a96"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e0f6648fd48f4c73d801c55ab976cd602e2da87de99c07bff005b131f269c6a"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5e0fdb5744caf1036dec5510f543164f2144cb64932251f6dfd42fa872b7f9c"}, + {file = "regex-2026.3.32-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dab4178a0bc1ef13178832b12db7bc7f562e8f028b2b5be186e370090dc50652"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f95bd07f301135771559101c060f558e2cf896c7df00bec050ca7f93bf11585a"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcca2bceb823c9cc610e57b86a265d7ffc30e9fe98548c609eba8bd3c0c2488"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:567b57eb987547a23306444e4f6f85d4314f83e65c71d320d898aa7550550443"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b6acb765e7c1f2fa08ac9057a33595e26104d7d67046becae184a8f100932dd9"}, + {file = "regex-2026.3.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1ed17104d1be7f807fdec35ec99777168dd793a09510d753f8710590ba54cdd"}, + {file = "regex-2026.3.32-cp311-cp311-win32.whl", hash = "sha256:c60f1de066eb5a0fd8ee5974de4194bb1c2e7692941458807162ffbc39887303"}, + {file = "regex-2026.3.32-cp311-cp311-win_amd64.whl", hash = "sha256:8fe14e24124ef41220e5992a0f09432f890037df6f93fd3d6b7a0feff2db16b2"}, + {file = "regex-2026.3.32-cp311-cp311-win_arm64.whl", hash = "sha256:ded4fc0edf3de792850cb8b04bbf3c5bd725eeaf9df4c27aad510f6eed9c4e19"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad8d372587e659940568afd009afeb72be939c769c552c9b28773d0337251391"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f5747501b69299c6b0b047853771e4ed390510bada68cb16da9c9c2078343f7"}, + {file = "regex-2026.3.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db976be51375bca900e008941639448d148c655c9545071965d0571ecc04f5d0"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66a5083c3ffe5a5a95f8281ea47a88072d4f24001d562d1d9d28d4cdc005fec5"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e83ce8008b48762be296f1401f19afd9ea29f3d035d1974e0cecb74e9afbd1df"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3aa21bad31db904e0b9055e12c8282df62d43169c4a9d2929407060066ebc74"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f54840bea73541652f1170dc63402a5b776fc851ad36a842da9e5163c1f504a0"}, + {file = "regex-2026.3.32-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ffbadc647325dd4e3118269bda93ded1eb5f5b0c3b7ba79a3da9fbd04f248e9"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66d3126afe7eac41759cd5f0b3b246598086e88e70527c0d68c9e615b81771c4"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f785f44a44702dea89b28bce5bc82552490694ce4e144e21a4f0545e364d2150"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b7836aa13721dbdef658aebd11f60d00de633a95726521860fe1f6be75fa225a"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5336b1506142eb0f23c96fb4a34b37c4fefd4fed2a7042069f3c8058efe17855"}, + {file = "regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed"}, + {file = "regex-2026.3.32-cp312-cp312-win32.whl", hash = "sha256:d363660f9ef8c734495598d2f3e527fb41f745c73159dc0d743402f049fb6836"}, + {file = "regex-2026.3.32-cp312-cp312-win_amd64.whl", hash = "sha256:c9f261ad3cd97257dc1d9355bfbaa7dd703e06574bffa0fa8fe1e31da915ee38"}, + {file = "regex-2026.3.32-cp312-cp312-win_arm64.whl", hash = "sha256:89e50667e7e8c0e7903e4d644a2764fffe9a3a5d6578f72ab7a7b4205bf204b7"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c6d9c6e783b348f719b6118bb3f187b2e138e3112576c9679eb458cc8b2e164b"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f21ae18dfd15752cdd98d03cbd7a3640be826bfd58482a93f730dbd24d7b9fb"}, + {file = "regex-2026.3.32-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:844d88509c968dd44b30daeefac72b038b1bf31ac372d5106358ab01d393c48b"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fc918cd003ba0d066bf0003deb05a259baaaab4dc9bd4f1207bbbe64224857a"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbc458a292aee57d572075f22c035fa32969cdb7987d454e3e34d45a40a0a8b4"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:987cdfcfb97a249abc3601ad53c7de5c370529f1981e4c8c46793e4a1e1bfe8e"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d88fa37ba5e8a80ca8d956b9ea03805cfa460223ac94b7d4854ee5e30f3173"}, + {file = "regex-2026.3.32-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d082be64e51671dd5ee1c208c92da2ddda0f2f20d8ef387e57634f7e97b6aae"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1d7fa44aece1fa02b8927441614c96520253a5cad6a96994e3a81e060feed55"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d478a2ca902b6ef28ffc9521e5f0f728d036abe35c0b250ee8ae78cfe7c5e44e"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2820d2231885e97aff0fcf230a19ebd5d2b5b8a1ba338c20deb34f16db1c7897"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc8ced733d6cd9af5e412f256a32f7c61cd2d7371280a65c689939ac4572499f"}, + {file = "regex-2026.3.32-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:847087abe98b3c1ebf1eb49d6ef320dbba75a83ee4f83c94704580f1df007dd4"}, + {file = "regex-2026.3.32-cp313-cp313-win32.whl", hash = "sha256:d21a07edddb3e0ca12a8b8712abc8452481c3d3db19ae87fc94e9842d005964b"}, + {file = "regex-2026.3.32-cp313-cp313-win_amd64.whl", hash = "sha256:3c054e39a9f85a3d76c62a1d50c626c5e9306964eaa675c53f61ff7ec1204bbb"}, + {file = "regex-2026.3.32-cp313-cp313-win_arm64.whl", hash = "sha256:b2e9c2ea2e93223579308263f359eab8837dc340530b860cb59b713651889f14"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5d86e3fb08c94f084a625c8dc2132a79a3a111c8bf6e2bc59351fa61753c2f6e"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b6f366a5ef66a2df4d9e68035cfe9f0eb8473cdfb922c37fac1d169b468607b0"}, + {file = "regex-2026.3.32-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b8fca73e16c49dd972ce3a88278dfa5b93bf91ddef332a46e9443abe21ca2f7c"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b953d9d496d19786f4d46e6ba4b386c6e493e81e40f9c5392332458183b0599d"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b565f25171e04d4fad950d1fa837133e3af6ea6f509d96166eed745eb0cf63bc"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f28eac18a8733a124444643a66ac96fef2c0ad65f50034e0a043b90333dc677f"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cdd508664430dd51b8888deb6c5b416d8de046b2e11837254378d31febe4a98"}, + {file = "regex-2026.3.32-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c35d097f509cf7e40d20d5bee548d35d6049b36eb9965e8d43e4659923405b9"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:85c9b0c131427470a6423baa0a9330be6fd8c3630cc3ee6fdee03360724cbec5"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e50af656c15e2723eeb7279c0837e07accc594b95ec18b86821a4d44b51b24bf"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4bc32b4dbdb4f9f300cf9f38f8ea2ce9511a068ffaa45ac1373ee7a943f1d810"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3e5d1802cba785210a4a800e63fcee7a228649a880f3bf7f2aadccb151a834b"}, + {file = "regex-2026.3.32-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ef250a3f5e93182193f5c927c5e9575b2cb14b80d03e258bc0b89cc5de076b60"}, + {file = "regex-2026.3.32-cp313-cp313t-win32.whl", hash = "sha256:9cf7036dfa2370ccc8651521fcbb40391974841119e9982fa312b552929e6c85"}, + {file = "regex-2026.3.32-cp313-cp313t-win_amd64.whl", hash = "sha256:c940e00e8d3d10932c929d4b8657c2ea47d2560f31874c3e174c0d3488e8b865"}, + {file = "regex-2026.3.32-cp313-cp313t-win_arm64.whl", hash = "sha256:ace48c5e157c1e58b7de633c5e257285ce85e567ac500c833349c363b3df69d4"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a416ee898ecbc5d8b283223b4cf4d560f93244f6f7615c1bd67359744b00c166"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d76d62909bfb14521c3f7cfd5b94c0c75ec94b0a11f647d2f604998962ec7b6c"}, + {file = "regex-2026.3.32-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:631f7d95c83f42bccfe18946a38ad27ff6b6717fb4807e60cf24860b5eb277fc"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12917c6c6813ffcdfb11680a04e4d63c5532b88cf089f844721c5f41f41a63ad"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e221b615f83b15887636fcb90ed21f1a19541366f8b7ba14ba1ad8304f4ded4"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f9ae4755fa90f1dc2d0d393d572ebc134c0fe30fcfc0ab7e67c1db15f192041"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a094e9dcafedfb9d333db5cf880304946683f43a6582bb86688f123335122929"}, + {file = "regex-2026.3.32-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1cecea3e477af105f32ef2119b8d895f297492e41d317e60d474bc4bffd62ff"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f26262900edd16272b6360014495e8d68379c6c6e95983f9b7b322dc928a1194"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb22fa9ee6a0acb22fc9aecce5f9995fe4d2426ed849357d499d62608fbd7f9"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9b9118a78e031a2e4709cd2fcc3028432e89b718db70073a8da574c249b5b249"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b193ed199848aa96618cd5959c1582a0bf23cd698b0b900cb0ffe81b02c8659c"}, + {file = "regex-2026.3.32-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:10fb2aaae1aaadf7d43c9f3c2450404253697bf8b9ce360bd5418d1d16292298"}, + {file = "regex-2026.3.32-cp314-cp314-win32.whl", hash = "sha256:110ba4920721374d16c4c8ea7ce27b09546d43e16aea1d7f43681b5b8f80ba61"}, + {file = "regex-2026.3.32-cp314-cp314-win_amd64.whl", hash = "sha256:245667ad430745bae6a1e41081872d25819d86fbd9e0eec485ba00d9f78ad43d"}, + {file = "regex-2026.3.32-cp314-cp314-win_arm64.whl", hash = "sha256:1ca02ff0ef33e9d8276a1fcd6d90ff6ea055a32c9149c0050b5b67e26c6d2c51"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:51fb7e26f91f9091fd8ec6a946f99b15d3bc3667cb5ddc73dd6cb2222dd4a1cc"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:51a93452034d671b0e21b883d48ea66c5d6a05620ee16a9d3f229e828568f3f0"}, + {file = "regex-2026.3.32-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:03c2ebd15ff51e7b13bb3dc28dd5ac18cd39e59ebb40430b14ae1a19e833cff1"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bf2f3c2c5bd8360d335c7dcd4a9006cf1dabae063ee2558ee1b07bbc8a20d88"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a4a3189a99ecdd1c13f42513ab3fc7fa8311b38ba7596dd98537acb8cd9acc3"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c0bbfbd38506e1ea96a85da6782577f06239cb9fcf9696f1ea537c980c0680b"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aaf8ee8f34b677f90742ca089b9c83d64bdc410528767273c816a863ed57327"}, + {file = "regex-2026.3.32-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ea568832eca219c2be1721afa073c1c9eb8f98a9733fdedd0a9747639fc22a5"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e4c8fa46aad1a11ae2f8fcd1c90b9d55e18925829ac0d98c5bb107f93351745"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cec365d44835b043d7b3266487797639d07d621bec9dc0ea224b00775797cc1"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:09e26cad1544d856da85881ad292797289e4406338afe98163f3db9f7fac816c"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6062c4ef581a3e9e503dccf4e1b7f2d33fdc1c13ad510b287741ac73bc4c6b27"}, + {file = "regex-2026.3.32-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ebc0783907468f17fca3d7821b30f9c21865a721144eb498cb0ff99a67bcac"}, + {file = "regex-2026.3.32-cp314-cp314t-win32.whl", hash = "sha256:e480d3dac06c89bc2e0fd87524cc38c546ac8b4a38177650745e64acbbcfdeba"}, + {file = "regex-2026.3.32-cp314-cp314t-win_amd64.whl", hash = "sha256:67015a8162d413af9e3309d9a24e385816666fbf09e48e3ec43342c8536f7df6"}, + {file = "regex-2026.3.32-cp314-cp314t-win_arm64.whl", hash = "sha256:1a6ac1ed758902e664e0d95c1ee5991aa6fb355423f378ed184c6ec47a1ec0e9"}, + {file = "regex-2026.3.32.tar.gz", hash = "sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16"}, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.33.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, + {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-oauthlib" @@ -2858,29 +2875,29 @@ files = [ [[package]] name = "ruff" -version = "0.15.6" +version = "0.15.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, - {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, - {file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"}, - {file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"}, - {file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"}, - {file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"}, - {file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"}, + {file = "ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7"}, + {file = "ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570"}, + {file = "ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1"}, + {file = "ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec"}, + {file = "ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6"}, + {file = "ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb"}, + {file = "ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8"}, + {file = "ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49"}, + {file = "ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34"}, + {file = "ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89"}, + {file = "ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2"}, + {file = "ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e"}, ] [[package]] @@ -2929,13 +2946,13 @@ files = [ [[package]] name = "starlette" -version = "0.52.1" +version = "1.0.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" files = [ - {file = "starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74"}, - {file = "starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"}, + {file = "starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b"}, + {file = "starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149"}, ] [package.dependencies] @@ -3093,58 +3110,58 @@ testing = ["datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff", [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] @@ -3260,13 +3277,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.41.0" +version = "0.42.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" files = [ - {file = "uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187"}, - {file = "uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a"}, + {file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"}, + {file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"}, ] [package.dependencies] @@ -3836,4 +3853,4 @@ cffi = ["cffi (>=1.17,<2.0)", "cffi (>=2.0.0b)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "ae9058c57d83e27261bab8f4a12c003f7db9af7932ef463f7496ff7228e81b6d" +content-hash = "958693aad5ae55da891407b0d7f263e71d99d6b4059fa4aeea708061ffb86e63" diff --git a/pyproject.toml b/pyproject.toml index 25b2cb6..0d080b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ beautifulsoup4 = ">=4.11.0" playwright = ">=1.40.0" openai = ">=1.0.0" python-dotenv = "^1.2.1" +httpx = ">=0.28.0" langgraph = ">=0.2.0" aiosqlite = "^0.22.1" chromadb = "^1.5.5" @@ -26,7 +27,6 @@ itsdangerous = "^2.2.0" pytest = "^9.0.2" pytest-asyncio = "^1.3.0" ruff = "^0.15.6" -httpx = "^0.28.1" [build-system] requires = ["poetry-core"] diff --git a/src/api/cover_letter.py b/src/api/cover_letter.py new file mode 100644 index 0000000..a431e08 --- /dev/null +++ b/src/api/cover_letter.py @@ -0,0 +1,175 @@ +""" +자소서 Draft API (Writer 그래프). + +POST /api/cover-letter/draft — 문항별 초안 생성 및 drafts 테이블 저장. +""" + +from __future__ import annotations + +import uuid +from datetime import datetime, timezone +from typing import Any + +import aiosqlite +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, ConfigDict + +from src.db.sqlite.client import connect +from src.graphs.writer_graph import build_writer_graph + +router = APIRouter(prefix="/api/cover-letter", tags=["cover-letter"]) + + +def _error_response(status_code: int, error: str, message: str) -> JSONResponse: + return JSONResponse(status_code=status_code, content={"error": error, "message": message}) + + +class QuestionItem(BaseModel): + model_config = ConfigDict(extra="ignore") + + question_text: str + max_chars: int + + +class DraftRequest(BaseModel): + model_config = ConfigDict(extra="ignore") + + job_id: str | None = None + questions: list[QuestionItem] | None = None + + +async def fetch_job_context(job_id: str | None) -> tuple[dict[str, Any], str | None]: + """(Writer용 job_parsed, drafts FK용 job_id). jobs 행이 없으면 ({}, None).""" + if not job_id: + return {}, None + conn = await connect() + try: + cur = await conn.execute( + """ + SELECT id, company_name, company_values, position, duty, qualifications, preferred + FROM jobs + WHERE id = ? + """, + (job_id,), + ) + row = await cur.fetchone() + await cur.close() + if not row: + return {}, None + job_parsed = { + "company_name": row["company_name"], + "company_values": row["company_values"], + "position": row["position"], + "duty": row["duty"], + "qualifications": row["qualifications"], + "preferred": row["preferred"], + } + return job_parsed, row["id"] + finally: + await conn.close() + + +async def persist_draft_records(user_id: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + """drafts 테이블에 INSERT 하고 API 응답용 drafts 항목 목록을 반환한다.""" + conn = await connect() + try: + out: list[dict[str, Any]] = [] + for row in rows: + await conn.execute( + """ + INSERT INTO drafts ( + id, user_id, job_id, question_text, max_chars, answer, round, + created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + row["id"], + user_id, + row.get("job_id"), + row["question_text"], + row["max_chars"], + row["answer"], + row.get("round", 0), + row["created_at"], + row["updated_at"], + ), + ) + ans = row["answer"] or "" + out.append( + { + "draft_id": row["id"], + "question_text": row["question_text"], + "answer": ans, + "char_count": len(ans), + } + ) + await conn.commit() + return out + except aiosqlite.Error: + await conn.rollback() + raise + finally: + await conn.close() + + +@router.post("/draft", response_model=None) +async def create_cover_letter_draft(request: Request, body: DraftRequest) -> JSONResponse | dict[str, Any]: + user_id = request.session.get("user_id") + if not user_id: + return _error_response(401, "UNAUTHORIZED", "UNAUTHORIZED") + + if body.questions is None or len(body.questions) == 0: + return _error_response(400, "BAD_REQUEST", "QUESTIONS_REQUIRED") + + user_id = str(user_id) + req_job_id = body.job_id + job_parsed, fk_job_id = await fetch_job_context(req_job_id) + + graph = build_writer_graph() + answers: list[str] = [] + + for q in body.questions: + state: dict[str, Any] = { + "user_id": user_id, + "question": q.question_text, + "max_chars": q.max_chars, + "job_parsed": job_parsed, + "samples": [], + "assets": [], + } + result = await graph.ainvoke(state) + if result.get("error"): + answers.append("") + else: + draft_text = result.get("draft") + answers.append(draft_text if isinstance(draft_text, str) else "") + + now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + rows: list[dict[str, Any]] = [] + for q, ans in zip(body.questions, answers, strict=True): + rows.append( + { + "id": str(uuid.uuid4()), + "user_id": user_id, + "job_id": fk_job_id, + "question_text": q.question_text, + "max_chars": q.max_chars, + "answer": ans, + "round": 0, + "created_at": now, + "updated_at": now, + } + ) + + try: + drafts = await persist_draft_records(user_id, rows) + except aiosqlite.Error: + return _error_response(500, "INTERNAL_SERVER_ERROR", "DRAFT_PERSIST_FAILED") + + return { + "drafts": drafts, + "used_assets": {"github_repos": [], "accepted_essays_used": False}, + "created_at": now, + } diff --git a/src/api/jobs.py b/src/api/jobs.py new file mode 100644 index 0000000..be023e3 --- /dev/null +++ b/src/api/jobs.py @@ -0,0 +1,294 @@ +""" +채용공고 파싱 API — jobs 테이블 저장 후 job_id 반환. + +POST /api/jobs/parse — manual(직접 입력) 또는 url(크롤 + 선택적 LLM 구조화). +""" + +from __future__ import annotations + +import json +import os +import re +import uuid +from datetime import datetime, timezone +from typing import Any, Literal + +import aiosqlite +import httpx +from bs4 import BeautifulSoup +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse +from openai import AsyncOpenAI +from pydantic import BaseModel, ConfigDict, Field + +from src.db.sqlite.client import connect + +router = APIRouter(prefix="/api/jobs", tags=["jobs"]) + + +def _error_response(status_code: int, error: str, message: str) -> JSONResponse: + return JSONResponse(status_code=status_code, content={"error": error, "message": message}) + + +def _now_iso() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _join_lines(items: list[str] | None) -> str | None: + if not items: + return None + lines = [str(x).strip() for x in items if x and str(x).strip()] + return "\n".join(lines) if lines else None + + +class JobsParseBody(BaseModel): + model_config = ConfigDict(extra="ignore") + + source_type: Literal["url", "manual"] + url: str | None = None + position_title: str | None = None + company_name: str | None = None + company_persona: str | None = None + duties: list[str] = Field(default_factory=list) + requirements: list[str] = Field(default_factory=list) + preferences: list[str] = Field(default_factory=list) + + +async def _insert_job_row( + *, + job_id: str, + url: str, + duty: str | None, + qualifications: str | None, + preferred: str | None, + company_name: str | None, + company_values: str | None, + position: str | None, +) -> None: + conn = await connect() + try: + await conn.execute( + """ + INSERT INTO jobs ( + id, url, duty, qualifications, preferred, + company_name, company_values, position, created_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + job_id, + url, + duty, + qualifications, + preferred, + company_name, + company_values, + position, + _now_iso(), + ), + ) + await conn.commit() + except aiosqlite.Error: + await conn.rollback() + raise + finally: + await conn.close() + + +async def _fetch_url_text(raw_url: str) -> tuple[str, str]: + """(plain_text, page_title). 실패 시 예외.""" + url = raw_url.strip() + if not url.startswith(("http://", "https://")): + url = "https://" + url + async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client: + res = await client.get(url, headers={"User-Agent": "AutofolioJobBot/1.0"}) + res.raise_for_status() + html = res.text + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "noscript"]): + tag.decompose() + title = (soup.title.string or "").strip() if soup.title else "" + text = soup.get_text(separator="\n", strip=True) + text = re.sub(r"\n{3,}", "\n\n", text) + return text, title + + +def _heuristic_from_url_text(text: str, title: str) -> dict[str, Any]: + snippet = (text or "")[:6000] + return { + "company_name": (title[:200] if title else "(제목 없음)"), + "position_title": "채용공고", + "company_persona": "", + "duties": [snippet] if snippet else [], + "requirements": [], + "preferences": [], + } + + +async def _llm_structure_job(text: str, url: str, title: str) -> dict[str, Any] | None: + key = os.getenv("OPENAI_API_KEY") + if not key: + return None + client = AsyncOpenAI(api_key=key) + payload = (text or "")[:20000] + msg = ( + f"page_title: {title}\nurl: {url}\n\nbody_text:\n{payload}" + ) + resp = await client.chat.completions.create( + model=os.getenv("OPENAI_JOB_PARSE_MODEL", "gpt-4o-mini"), + response_format={"type": "json_object"}, + messages=[ + { + "role": "system", + "content": ( + "You extract job posting fields from Korean or English text. " + "Return a single JSON object with keys: " + "company_name (string), position_title (string), " + "company_persona (string), " + "duties (array of short strings), " + "requirements (array of strings), " + "preferences (array of strings). " + "Use empty string or [] if unknown." + ), + }, + {"role": "user", "content": msg}, + ], + temperature=0.2, + ) + raw = (resp.choices[0].message.content or "").strip() + data = json.loads(raw) + return { + "company_name": str(data.get("company_name") or "").strip() or "(미상)", + "position_title": str(data.get("position_title") or "").strip() or "채용공고", + "company_persona": str(data.get("company_persona") or "").strip(), + "duties": [str(x).strip() for x in (data.get("duties") or []) if str(x).strip()], + "requirements": [str(x).strip() for x in (data.get("requirements") or []) if str(x).strip()], + "preferences": [str(x).strip() for x in (data.get("preferences") or []) if str(x).strip()], + } + + +def _response_payload( + *, + job_id: str, + source_type: str, + position_title: str, + company_name: str, + company_persona: str, + duties: list[str], + requirements: list[str], + preferences: list[str], +) -> dict[str, Any]: + return { + "job_id": job_id, + "source_type": source_type, + "position_title": position_title, + "company_name": company_name, + "company_persona": company_persona, + "duties": duties, + "requirements": requirements, + "preferences": preferences, + "created_at": _now_iso(), + } + + +@router.post("/parse", response_model=None) +async def parse_job(request: Request, body: JobsParseBody) -> JSONResponse | dict[str, Any]: + uid = request.session.get("user_id") + if not uid: + return _error_response(401, "UNAUTHORIZED", "UNAUTHORIZED") + + if body.source_type == "manual": + if not body.position_title or not str(body.position_title).strip(): + return _error_response(400, "BAD_REQUEST", "position_title is required for manual") + if not body.company_name or not str(body.company_name).strip(): + return _error_response(400, "BAD_REQUEST", "company_name is required for manual") + + job_id = str(uuid.uuid4()) + url = f"manual://{job_id}" + duty = _join_lines(body.duties) + qual = _join_lines(body.requirements) + pref = _join_lines(body.preferences) + persona = (body.company_persona or "").strip() or None + + try: + await _insert_job_row( + job_id=job_id, + url=url, + duty=duty, + qualifications=qual, + preferred=pref, + company_name=body.company_name.strip(), + company_values=persona, + position=body.position_title.strip(), + ) + except aiosqlite.Error: + return _error_response(500, "INTERNAL_SERVER_ERROR", "JOB_PERSIST_FAILED") + + return _response_payload( + job_id=job_id, + source_type="manual", + position_title=body.position_title.strip(), + company_name=body.company_name.strip(), + company_persona=persona or "", + duties=list(body.duties or []), + requirements=list(body.requirements or []), + preferences=list(body.preferences or []), + ) + + # url + if not body.url or not str(body.url).strip(): + return _error_response(400, "BAD_REQUEST", "url is required for source_type=url") + + try: + text, title = await _fetch_url_text(body.url) + except httpx.HTTPError as exc: + return _error_response( + 400, + "CRAWL_FAILED", + f"HTTP fetch failed: {type(exc).__name__}: {exc}", + ) + except Exception as exc: + return _error_response( + 400, + "CRAWL_FAILED", + f"Failed to read job page: {type(exc).__name__}: {exc}", + ) + + structured: dict[str, Any] | None = None + try: + structured = await _llm_structure_job(text, body.url.strip(), title) + except Exception: + structured = None + + if structured is None: + structured = _heuristic_from_url_text(text, title) + + job_id = str(uuid.uuid4()) + duty = _join_lines(structured.get("duties") or []) + qual = _join_lines(structured.get("requirements") or []) + pref = _join_lines(structured.get("preferences") or []) + + try: + await _insert_job_row( + job_id=job_id, + url=body.url.strip(), + duty=duty, + qualifications=qual, + preferred=pref, + company_name=structured["company_name"], + company_values=(structured.get("company_persona") or None), + position=structured["position_title"], + ) + except aiosqlite.Error: + return _error_response(500, "INTERNAL_SERVER_ERROR", "JOB_PERSIST_FAILED") + + return _response_payload( + job_id=job_id, + source_type="url", + position_title=structured["position_title"], + company_name=structured["company_name"], + company_persona=structured.get("company_persona") or "", + duties=list(structured.get("duties") or []), + requirements=list(structured.get("requirements") or []), + preferences=list(structured.get("preferences") or []), + ) diff --git a/src/app/main.py b/src/app/main.py index 4953cc2..644c159 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -13,6 +13,8 @@ from starlette.middleware.sessions import SessionMiddleware from src.api import portfolio_router +from src.api.cover_letter import router as cover_letter_router +from src.api.jobs import router as jobs_router from src.api.auth import router as auth_router from src.api.github import router as github_router from src.api.user_assets import router as user_assets_router @@ -44,6 +46,8 @@ def create_app() -> FastAPI: app.include_router(auth_router) app.include_router(portfolio_router) + app.include_router(jobs_router) + app.include_router(cover_letter_router) app.include_router(github_router) app.include_router(user_assets_router) diff --git a/src/graphs/writer_graph/edge.py b/src/graphs/writer_graph/edge.py index 28349c4..1ff7810 100644 --- a/src/graphs/writer_graph/edge.py +++ b/src/graphs/writer_graph/edge.py @@ -33,12 +33,12 @@ def after_load_assets(state: WriterState) -> str: def after_self_consistency(state: WriterState) -> str: """self_consistency 이후 분기. - - 통과 → format_output - - 실패 & draft_retry_count < MAX → generate_draft (피드백 프롬프트 반영 재생성) - - 실패 & draft_retry_count >= MAX → format_output (재시도 상한, 그대로 진행) + - is_hallucination False(환각 없음) → format_output + - is_hallucination True & draft_retry_count < MAX → generate_draft (피드백 반영 재생성) + - is_hallucination True & draft_retry_count >= MAX → format_output (재시도 상한) """ - passed = state.get("is_hallucination") - if passed: + has_hallucination = state.get("is_hallucination") + if not has_hallucination: return "format_output" retry = state.get("draft_retry_count") or 0 if retry < MAX_DRAFT_RETRIES: diff --git a/src/graphs/writer_graph/node.py b/src/graphs/writer_graph/node.py index 55bacde..8b86cde 100644 --- a/src/graphs/writer_graph/node.py +++ b/src/graphs/writer_graph/node.py @@ -1,65 +1,234 @@ -"""2번 그래프(Writer) 노드. 내부 로직은 TODO 주석으로만 표시.""" - -from .state import WriterState - - -def retrieve_samples(state: WriterState) -> dict: - """진입 시 검증 + 합격 자소서 검색 모듈 호출. - - 입력: question, max_chars, job_parsed (assets 불필요 — load_assets에서 이후 조회) - 출력: (통과) samples / (검증 실패만) error → END - """ - # TODO: question, max_chars 필수 검증. 없으면 error 설정 후 반환 - # TODO: 검증 통과 시 retrieve_passed_cover_letters(question, company_name, ...) 호출 - # TODO: samples 못 찾아도(빈 리스트/검색 모듈 에러) error 설정 안 함 → load_assets로 진행 - return {"samples": state.get("samples") or []} +"""2번 그래프(Writer) 노드.""" +from __future__ import annotations -def load_assets(state: WriterState) -> dict: - """유저 DB에서 에셋 조회. retrieve_samples → (통과) load_assets → generate_draft. +import json +import os +import re +from typing import Any - 입력: user_id - 출력: (통과) assets / (실패) error → END - """ - # TODO: user_id로 P1 API 또는 DB에서 에셋 조회. 문항/공고 기반 관련 에셋 선별 - # TODO: 조회 실패 시 error 설정 후 반환 - return {"assets": state.get("assets") or []} +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_openai import ChatOpenAI +from src.service.rag import retrieve_passed_cover_letters, retrieve_user_assets -def generate_draft(state: WriterState) -> dict: - """LLM 초안 생성. 재진입 시 consistency_feedback 프롬프트 반영. - - 입력: assets, samples, question, job_parsed, (재진입) consistency_feedback - 출력: draft - """ - # TODO: assets + samples + question + job_parsed로 LLM 프롬프트 구성 - # TODO: 재진입 시 state.get("consistency_feedback")를 프롬프트에 포함 - # TODO: draft = llm.invoke(...) - return {"draft": state.get("draft") or "(초안 placeholder)"} - - -def self_consistency(state: WriterState) -> dict: - """환각 체크. 실패 시 consistency_feedback 기록, draft_retry_count 증가. +from .prompts import DRAFT_CONSISTENCY_SYSTEM_PROMPT, DRAFT_SYSTEM_PROMPT +from .state import WriterState - 입력: draft, assets, draft_retry_count - 출력: is_hallucination, (실패 시) consistency_feedback, draft_retry_count+1 - """ - # TODO: draft가 assets에 근거한지 검사. 없는 경험·지어낸 수치 → 실패 - # TODO: 실패 시 consistency_feedback에 문장/구간·근거 없는 내용 기록 - # TODO: draft_retry_count += 1 - return { - "is_hallucination": True, - "consistency_feedback": state.get("consistency_feedback") or {}, - "draft_retry_count": state.get("draft_retry_count") or 0, +DEFAULT_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") + + +def _build_chat_openai(temperature: float) -> ChatOpenAI: + """ChatOpenAI JSON mode (portfolio_graph/node.py와 동일 패턴).""" + return ChatOpenAI( + model=DEFAULT_MODEL, + temperature=temperature, + api_key=os.getenv("OPENAI_API_KEY"), + model_kwargs={"response_format": {"type": "json_object"}}, + ) + + +def _question_missing(state: WriterState) -> bool: + q = state.get("question") + if q is None: + return True + if isinstance(q, str) and not q.strip(): + return True + return False + + +def _max_chars_missing(state: WriterState) -> bool: + return state.get("max_chars") is None + + +def _job_parsed_meta(job_parsed: dict | None) -> tuple[str | None, str | None, str | None]: + if not isinstance(job_parsed, dict): + return None, None, None + company = job_parsed.get("company_name") + position = job_parsed.get("position") + year = job_parsed.get("year") + if year is not None and not isinstance(year, str): + year = str(year) + if isinstance(company, str) and not company.strip(): + company = None + if isinstance(position, str) and not position.strip(): + position = None + if isinstance(year, str) and not year.strip(): + year = None + return ( + str(company) if company is not None else None, + str(position) if position is not None else None, + year, + ) + + +def _summarize_assets(assets: list[Any]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in assets or []: + if not isinstance(item, dict): + continue + out.append( + { + "id": item.get("id"), + "summary": item.get("document"), + "metadata": item.get("metadata"), + } + ) + return out + + +async def retrieve_samples(state: WriterState) -> dict: + """진입 시 검증 + 합격 자소서 검색 모듈 호출.""" + if _question_missing(state): + return {"error": "question is required"} + if _max_chars_missing(state): + return {"error": "max_chars is required"} + + job = state.get("job_parsed") if isinstance(state.get("job_parsed"), dict) else {} + company_name, position, year = _job_parsed_meta(job) + question = str(state.get("question", "")).strip() + + try: + samples = await retrieve_passed_cover_letters( + question=question, + company_name=company_name, + position=position, + year=year, + top_k=5, + ) + except Exception: + samples = [] + + return {"samples": samples} + + +async def load_assets(state: WriterState) -> dict: + """유저 DB에서 에셋 조회.""" + user_id = state.get("user_id") + if not user_id: + return {"error": "user_id is required"} + + try: + # user_assets_{user_id} 는 현재 GitHub 임베딩만 적재. 과거 문서는 source 메타가 없을 수 있어 + # source_filter 를 쓰면 0건이 되어 no_github_assets 가 나므로 필터 없이 조회한다. + assets = await retrieve_user_assets( + user_id=str(user_id), + source_filter=None, + type_filter=None, + top_k=20, + ) + except Exception as exc: + return {"error": f"retrieve_user_assets_failed: {type(exc).__name__}", "assets": []} + + if not assets: + return {"error": "no_github_assets", "assets": []} + + return {"assets": assets} + + +async def generate_draft(state: WriterState) -> dict: + """LLM 초안 생성.""" + llm = _build_chat_openai(temperature=0.2) + + assets = _summarize_assets(state.get("assets") or []) + user_payload: dict[str, Any] = { + "question": state.get("question"), + "max_chars": state.get("max_chars"), + "job_parsed": state.get("job_parsed") or {}, + "assets": assets, + "samples": state.get("samples") or [], + } + human_text = json.dumps(user_payload, ensure_ascii=False) + feedback = state.get("consistency_feedback") + if feedback: + human_text += ( + "\n\n다음 지적사항을 반영해 수정해줘: " + + ( + json.dumps(feedback, ensure_ascii=False) + if isinstance(feedback, (dict, list)) + else str(feedback) + ) + ) + + try: + response = await llm.ainvoke( + [ + SystemMessage(content=DRAFT_SYSTEM_PROMPT), + HumanMessage(content=human_text), + ] + ) + content = response.content if isinstance(response.content, str) else "{}" + parsed = json.loads(content) + if isinstance(parsed, dict): + draft = parsed.get("draft") + if isinstance(draft, str): + return {"draft": draft} + except Exception: + pass + + return {"draft": ""} + + +async def self_consistency(state: WriterState) -> dict: + """환각 체크.""" + llm = _build_chat_openai(temperature=0.0) + draft = state.get("draft") or "" + assets = _summarize_assets(state.get("assets") or []) + retry = state.get("draft_retry_count") or 0 + + user_payload = { + "draft": draft, + "assets": assets, } + try: + response = await llm.ainvoke( + [ + SystemMessage(content=DRAFT_CONSISTENCY_SYSTEM_PROMPT), + HumanMessage(content=json.dumps(user_payload, ensure_ascii=False)), + ] + ) + content = response.content if isinstance(response.content, str) else "{}" + parsed = json.loads(content) + if not isinstance(parsed, dict): + return { + "is_hallucination": False, + "consistency_feedback": {}, + "draft_retry_count": retry, + } + + is_hallucination = bool(parsed.get("is_hallucination", False)) + raw_fb = parsed.get("consistency_feedback") + if not isinstance(raw_fb, dict): + raw_fb = {} + + if not is_hallucination: + return { + "is_hallucination": False, + "consistency_feedback": {}, + "draft_retry_count": retry, + } + + return { + "is_hallucination": True, + "consistency_feedback": raw_fb, + "draft_retry_count": retry + 1, + } + except Exception: + return { + "is_hallucination": False, + "consistency_feedback": {}, + "draft_retry_count": retry, + } def format_output(state: WriterState) -> dict: - """글자수·형식 정리. max_chars 필수. - - 입력: draft, max_chars - 출력: draft (최종) - """ - # TODO: max_chars로 글자수 제한 - # TODO: 줄바꿈·불필요 공백 정리 - return {"draft": state.get("draft") or ""} + """글자수·형식 정리.""" + draft = state.get("draft") or "" + if state.get("max_chars") is not None: + mc = int(state["max_chars"]) + draft = draft[:mc] + draft = re.sub(r"\n\n+", "\n", draft) + draft = draft.strip() + else: + draft = draft.strip() + return {"draft": draft} diff --git a/src/graphs/writer_graph/prompts.py b/src/graphs/writer_graph/prompts.py new file mode 100644 index 0000000..b076e5e --- /dev/null +++ b/src/graphs/writer_graph/prompts.py @@ -0,0 +1,46 @@ +"""Writer 그래프 노드에서 사용하는 프롬프트 상수.""" + +DRAFT_SYSTEM_PROMPT = ( + "You are an expert in evidence-based Korean cover-letter (자기소개서) writing for job applications.\n" + "You write drafts that hiring managers can trust because every substantive claim is grounded " + "in the candidate's own materials.\n\n" + "## Rules\n" + "1. **Evidence-only**: Use ONLY facts, experiences, metrics, and technologies that appear in " + "the provided `assets`. Do NOT invent employers, projects, numbers, dates, awards, or skills " + "that are not supported by the assets.\n" + "2. **Inputs to honor**: The answer must directly address `question`, respect `max_chars` " + "(character budget for the final draft body), and reflect `job_parsed` when given — especially " + "company name (기업명) and stated values / talent philosophy (인재상) — without fabricating " + "details not present in `job_parsed` or `assets`.\n" + "3. **Samples**: Treat `samples` (passed cover-letter examples) as **style and structure " + "reference only**. Do NOT copy sentences verbatim or import facts from samples that are not " + "also backed by this candidate's `assets`.\n" + "4. **Language**: Write the draft body in **Korean**.\n" + "5. **Revision pass**: If `consistency_feedback` is provided (e.g. from a prior verification " + "step), you **must** revise the draft to fix every issue described there while still obeying " + "rules 1–4.\n\n" + "## Output format\n" + "Return ONLY valid JSON with the following structure — no markdown fences, no commentary, " + "no extra keys:\n" + '{"draft": "..."}' +) + +DRAFT_CONSISTENCY_SYSTEM_PROMPT = ( + "You are a strict verifier specialized in detecting hallucinations in Korean job-application " + "cover letters (자기소개서).\n" + "Your job is to compare the draft against the candidate's `assets` only.\n\n" + "## What to check\n" + "1. **Grounding**: Flag any experience, metric, date, role, project, technology, certification, " + "or achievement in `draft` that is **not clearly supported** by the provided `assets`.\n" + "2. **Claims**: Treat unsupported generalizations or strong assertions as issues if they imply " + "facts that do not appear in `assets`.\n\n" + "## Boolean semantics\n" + '"is_hallucination": true → at least one unsupported experience, number, or claim exists.\n' + '"is_hallucination": false → the draft is fully grounded in `assets` for substantive content.\n\n' + "## Output format\n" + "Return ONLY valid JSON — no markdown, no extra keys:\n" + '{"is_hallucination": true|false, ' + '"consistency_feedback": {"issues": [{"sentence": "...", "reason": "..."}]}}\n' + "If there are no issues, return an empty list: " + '{"is_hallucination": false, "consistency_feedback": {"issues": []}}' +) diff --git a/src/service/github_embedding/pipeline.py b/src/service/github_embedding/pipeline.py index b494917..d818060 100644 --- a/src/service/github_embedding/pipeline.py +++ b/src/service/github_embedding/pipeline.py @@ -81,8 +81,10 @@ def _normalize_metadata( asset_type: str, path_value: str, ) -> dict[str, str]: + # retrieve_user_assets(source_filter=["github"]) 및 메타 스키마와 맞춘다. return { "user_id": user_id, + "source": "github", "repo": repo_full_name, "ref": ref or "", "type": asset_type, diff --git a/src/service/rag/__init__.py b/src/service/rag/__init__.py index 9d04599..1748b1b 100644 --- a/src/service/rag/__init__.py +++ b/src/service/rag/__init__.py @@ -5,6 +5,7 @@ 재사용 가능한 검색 함수 제공 """ +from .passed_samples import retrieve_passed_cover_letters from .user_assets import retrieve_user_assets -__all__ = ["retrieve_user_assets"] +__all__ = ["retrieve_passed_cover_letters", "retrieve_user_assets"] diff --git a/src/service/rag/passed_samples.py b/src/service/rag/passed_samples.py new file mode 100644 index 0000000..980664e --- /dev/null +++ b/src/service/rag/passed_samples.py @@ -0,0 +1,167 @@ +""" +합격 자소서(문항 단위) ChromaDB 조회. + +스키마: docs/AUTOFOLIO_합격자소서_벡터DB_스키마.md +컬렉션: `passed_cover_letters` (없거나 비어 있으면 빈 리스트). +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from src.db.vector.chroma import get_chroma_client + +PASSED_COVER_LETTERS_COLLECTION = "passed_cover_letters" + + +def _strip_or_none(value: str | None) -> str | None: + if value is None: + return None + s = value.strip() + return s if s else None + + +def _build_query_text( + question: str, + company_name: str | None, + position: str | None, + year: str | None, +) -> str: + """임베딩 쿼리 문자열. company/position/year가 모두 있을 때만 전체 포맷.""" + q = (question or "").strip() + c = _strip_or_none(company_name) + p = _strip_or_none(position) + y = _strip_or_none(year) + if c and p and y: + return f"{c}의 {y}년 {p} 공고의 자기소개서 문항 1 : {q}" + return f"자기소개서 문항 1 : {q}" + + +def _build_where_clause( + company_name: str | None, + position: str | None, + year: str | None, +) -> dict[str, Any] | None: + """제공된 메타 필드에 대해서만 $eq 조건을 쌓고, 2개 이상이면 $and.""" + conditions: list[dict[str, Any]] = [] + c = _strip_or_none(company_name) + if c is not None: + conditions.append({"company": {"$eq": c}}) + p = _strip_or_none(position) + if p is not None: + conditions.append({"position": {"$eq": p}}) + y = _strip_or_none(year) + if y is not None: + conditions.append({"year": {"$eq": y}}) + + if not conditions: + return None + if len(conditions) == 1: + return conditions[0] + return {"$and": conditions} + + +def _normalize_passed_results(raw: dict[str, Any]) -> list[dict[str, Any]]: + """Chroma query 결과를 Writer용 dict 리스트로 변환.""" + ids = (raw.get("ids") or [[]])[0] + metas = (raw.get("metadatas") or [[]])[0] + dists = (raw.get("distances") or [[]])[0] + + out: list[dict[str, Any]] = [] + for idx, _ in enumerate(ids): + meta = metas[idx] if idx < len(metas) else None + if not isinstance(meta, dict): + meta = {} + + def _meta_str(key: str) -> str: + v = meta.get(key) + if v is None: + return "" + return str(v) + + dist = dists[idx] if idx < len(dists) else None + out.append( + { + "question": _meta_str("question"), + "answer": _meta_str("answer"), + "company": _meta_str("company"), + "position": _meta_str("position"), + "year": _meta_str("year"), + "source": _meta_str("source"), + "distance": dist, + } + ) + return out + + +def _retrieve_passed_cover_letters_sync( + question: str, + company_name: str | None, + position: str | None, + year: str | None, + top_k: int, +) -> list[dict[str, Any]]: + client = get_chroma_client() + try: + collection = client.get_collection(name=PASSED_COVER_LETTERS_COLLECTION) + except Exception: + return [] + + try: + n = collection.count() + except Exception: + return [] + + if n == 0: + return [] + + query_text = _build_query_text(question, company_name, position, year) + where = _build_where_clause(company_name, position, year) + + n_results = min(top_k, n) + if n_results <= 0: + return [] + + kwargs: dict[str, Any] = { + "query_texts": [query_text], + "n_results": n_results, + } + if where is not None: + kwargs["where"] = where + + try: + raw = collection.query(**kwargs) + except Exception: + return [] + + return _normalize_passed_results(raw) + + +async def retrieve_passed_cover_letters( + question: str, + company_name: str | None = None, + position: str | None = None, + year: str | None = None, + top_k: int = 5, +) -> list[dict]: + """합격 자소서 문항 청크를 유사도 검색한다. + + - 쿼리 텍스트는 스키마 문서와 동일한 포맷(또는 메타 미완 시 축약 포맷)으로 구성한다. + - ``company`` / ``position`` / ``year`` 메타는 값이 넘겨진 필드에 한해 where 필터로 적용한다. + - 컬렉션이 없거나 비어 있으면 ``[]`` (예외 없음). + + Returns: + ``[{question, answer, company, position, year, source, distance}, ...]`` + """ + if top_k <= 0: + raise ValueError("top_k must be greater than 0") + + return await asyncio.to_thread( + _retrieve_passed_cover_letters_sync, + question, + company_name, + position, + year, + top_k, + ) diff --git a/src/service/rag/user_assets.py b/src/service/rag/user_assets.py index 4eb8dc4..b0ad7a8 100644 --- a/src/service/rag/user_assets.py +++ b/src/service/rag/user_assets.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio +import os from typing import Any from src.db.vector import get_user_asset_collection @@ -108,6 +109,24 @@ def _query_user_assets_sync( return _normalize_results(raw) +def _query_user_assets_sync_embedding( + user_id: str, + query_embedding: list[float], + where: dict[str, Any] | None, + top_k: int, +) -> list[dict]: + """GitHub 파이프라인이 OpenAI 임베딩으로 적재한 벡터와 동일 공간에서 검색한다.""" + collection = get_user_asset_collection(user_id) + kwargs: dict[str, Any] = { + "query_embeddings": [query_embedding], + "n_results": top_k, + } + if where is not None: + kwargs["where"] = where + raw = collection.query(**kwargs) + return _normalize_results(raw) + + def _merge_and_deduplicate(results_per_query: list[list[dict]], top_k: int) -> list[dict]: """다중 쿼리 결과를 id 기준으로 중복 제거 후 distance 오름차순으로 반환한다. @@ -153,6 +172,28 @@ async def retrieve_user_assets( where = _build_where_clause(source_filter, type_filter) + # GitHub 임베딩은 OpenAI API로 벡터를 넣는다. query_texts 는 컬렉션 기본 임베더(MiniLM 등)를 + # 쓰므로 차원·공간이 달라 검색이 비거나 실패할 수 있다. OPENAI_API_KEY 가 있으면 동일 모델로 + # 쿼리 벡터를 만든 뒤 query_embeddings 로 검색한다. + if os.getenv("OPENAI_API_KEY"): + from src.service.github_embedding.openai_embedder import OpenAIEmbedder + + embedder = OpenAIEmbedder() + embeddings = await embedder.embed(PORTFOLIO_STAR_QUERIES) + results_per_query = await asyncio.gather( + *[ + asyncio.to_thread( + _query_user_assets_sync_embedding, + user_id, + emb, + where, + top_k, + ) + for emb in embeddings + ] + ) + return _merge_and_deduplicate(list(results_per_query), top_k) + results_per_query = await asyncio.gather( *[ asyncio.to_thread(_query_user_assets_sync, user_id, query, where, top_k) diff --git a/src/web/dashboard.py b/src/web/dashboard.py index 8e4fbee..26e5562 100644 --- a/src/web/dashboard.py +++ b/src/web/dashboard.py @@ -39,7 +39,7 @@

Autofolio Dev Dashboard

-

OAuth · GitHub API · 선택 레포/assets · 코드 임베딩(Chroma) 개발용 화면입니다.

+

OAuth · GitHub API · 선택 레포/assets · 코드 임베딩(Chroma) · 채용공고 parse → job_id · 자소서 Draft 개발용 화면입니다.

@@ -165,6 +165,69 @@

     
 
+    
+

Jobs 채용공고 저장 (parse)

+

+ POST /api/jobs/parse로 SQLite jobs 테이블에 공고를 넣고 job_id를 받습니다. + 성공 시 아래 자소서 Draft의 job_id 입력칸에 자동으로 채웁니다. 이후 POST /api/cover-letter/draft에 같은 id를 넘기면 공고 맥락이 붙습니다. +

+
+ +    + +
+
+

+ +
+

+ +
+

+ +
+

+ +
+

+ +
+

+ +
+
+ + +

응답

+

+    
+ +
+

Writer 자소서 Draft

+

+ GitHub 로그인 필수. 위에서 공고 저장으로 받은 job_id가 있으면 맥락이 붙습니다(비우면 범용 생성). + 코드 RAG를 쓰려면 위 임베딩 단계를 먼저 완료하는 것이 좋습니다. +

+
+
+ +
+
+
+ +
+
+ + +
+ +

응답

+

+    
+ diff --git a/tests/api/test_cover_letter.py b/tests/api/test_cover_letter.py index 7320f6f..c9b0cab 100644 --- a/tests/api/test_cover_letter.py +++ b/tests/api/test_cover_letter.py @@ -68,6 +68,20 @@ async def setup() -> None: """, ("u1", "testlogin", "tok", "2026-03-01", "2026-03-01"), ) + await conn.execute( + """ + INSERT INTO selected_repos (user_id, repo_full_name, created_at) + VALUES (?, ?, ?) + """, + ("u1", "owner/repo-a", "2026-03-01"), + ) + await conn.execute( + """ + INSERT INTO selected_repos (user_id, repo_full_name, created_at) + VALUES (?, ?, ?) + """, + ("u1", "owner/repo-b", "2026-03-01"), + ) await conn.commit() await conn.close() @@ -78,6 +92,10 @@ async def setup() -> None: @pytest.fixture def draft_client(monkeypatch: pytest.MonkeyPatch, tmp_db) -> TestClient: """create_app()에 등록된 cover-letter 라우터로 TestClient.""" + monkeypatch.setattr( + "src.api.cover_letter.ensure_selected_repos_embedded", + AsyncMock(return_value=None), + ) return TestClient(create_app()) @@ -110,7 +128,22 @@ def test_draft_missing_questions_field(draft_client: TestClient) -> None: def test_draft_success_single_question(draft_client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: class FakeGraph: async def ainvoke(self, state: dict) -> dict: - return {"draft": "테스트 초안"} + return { + "draft": "테스트 초안", + "assets": [ + {"id": "a", "metadata": {"repo": "owner/repo-a"}}, + {"id": "b", "metadata": {"repo": "owner/repo-a"}}, + {"id": "c", "metadata": {"repo": "owner/repo-b"}}, + ], + "samples": [ + { + "id": "s1", + "company": "SK플래닛", + "position": "신입 앱개발자", + "question": "개발 역량을 보여줄 수 있는 프로젝트를 서술하세요.", + } + ], + } monkeypatch.setattr("src.api.cover_letter.build_writer_graph", lambda: FakeGraph()) monkeypatch.setattr( @@ -146,6 +179,17 @@ async def ainvoke(self, state: dict) -> dict: assert d0.get("answer") == "테스트 초안" assert d0.get("char_count") == len("테스트 초안") assert "used_assets" in body + ua = body["used_assets"] + assert ua.get("accepted_essays_used") is True + essays = ua.get("accepted_essays") or [] + assert len(essays) == 1 + assert essays[0].get("company") == "SK플래닛" + assert essays[0].get("position") == "신입 앱개발자" + assert "개발 역량" in (essays[0].get("question") or "") + gh = ua.get("github_repos") or [] + assert len(gh) == 2 + names = sorted(x.get("full_name") for x in gh if isinstance(x, dict)) + assert names == ["owner/repo-a", "owner/repo-b"] assert "created_at" in body @@ -195,7 +239,7 @@ async def setup_job() -> None: class FakeGraph: async def ainvoke(self, state: dict) -> dict: captured["job_parsed"] = state.get("job_parsed") - return {"draft": "x"} + return {"draft": "x", "assets": [], "samples": []} monkeypatch.setattr("src.api.cover_letter.build_writer_graph", lambda: FakeGraph()) monkeypatch.setattr( @@ -222,6 +266,10 @@ async def ainvoke(self, state: dict) -> dict: ) assert r.status_code == 200, r.text assert captured.get("job_parsed") == expected_job_parsed + ua0 = r.json()["used_assets"] + assert ua0["accepted_essays_used"] is False + assert ua0.get("accepted_essays") == [] + assert ua0["github_repos"] == [] captured.clear() @@ -253,12 +301,18 @@ async def ainvoke(self, state: dict) -> dict: def test_draft_success_multiple_questions(draft_client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: calls: list[dict] = [] + n = [0] class FakeGraph: async def ainvoke(self, state: dict) -> dict: calls.append(dict(state)) q = state.get("question", "") - return {"draft": f"답-{q[:4]}"} + n[0] += 1 + return { + "draft": f"답-{q[:4]}", + "assets": [{"metadata": {"repo": f"o/r-{n[0]}"}}], + "samples": [] if n[0] > 1 else [{"x": 1}], + } async def fake_persist(_user_id: str, rows: list[dict]) -> list[dict]: out: list[dict] = [] @@ -289,12 +343,22 @@ async def fake_persist(_user_id: str, rows: list[dict]) -> list[dict]: body = r.json() assert len(body["drafts"]) == 3 assert len(calls) == 3 + assert [c.get("primary_repo") for c in calls] == [ + "owner/repo-a", + "owner/repo-b", + "owner/repo-a", + ] + ua = body["used_assets"] + assert ua["accepted_essays_used"] is True + assert ua.get("accepted_essays") == [] + gh = sorted(x["full_name"] for x in ua["github_repos"]) + assert gh == ["o/r-1", "o/r-2", "o/r-3"] def test_draft_graph_error_returns_partial(draft_client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: class FakeGraph: async def ainvoke(self, state: dict) -> dict: - return {"error": "no_github_assets", "draft": ""} + return {"error": "no_github_assets", "draft": "", "assets": [], "samples": []} monkeypatch.setattr("src.api.cover_letter.build_writer_graph", lambda: FakeGraph()) monkeypatch.setattr( @@ -319,3 +383,27 @@ async def ainvoke(self, state: dict) -> dict: assert r.status_code == 200, r.text body = r.json() assert body["drafts"][0].get("answer") == "" + assert body["used_assets"]["github_repos"] == [] + assert body["used_assets"]["accepted_essays_used"] is False + assert body["used_assets"].get("accepted_essays") == [] + + +@pytest.mark.asyncio +async def test_ensure_selected_repos_embedded_invokes_job_when_chroma_empty( + monkeypatch: pytest.MonkeyPatch, tmp_db +) -> None: + monkeypatch.setattr("src.api.cover_letter._repo_has_embedding_docs", lambda _u, _r: False) + monkeypatch.setattr( + "src.api.cover_letter.fetch_code_document_ids_for_repo", + AsyncMock(return_value=["owner/repo-a/foo.py"]), + ) + mock_job = AsyncMock(return_value={"embedded": 1}) + monkeypatch.setattr("src.api.cover_letter.run_github_repo_embedding_job", mock_job) + + from src.api.cover_letter import ensure_selected_repos_embedded + + await ensure_selected_repos_embedded("u1", ["owner/repo-a"]) + mock_job.assert_awaited_once() + assert mock_job.await_args.kwargs["user_id"] == "u1" + assert mock_job.await_args.kwargs["repo_full_name"] == "owner/repo-a" + assert mock_job.await_args.kwargs["code_document_ids"] == ["owner/repo-a/foo.py"] diff --git a/tests/graphs/writer_graph/test_writer_nodes.py b/tests/graphs/writer_graph/test_writer_nodes.py index bb68389..e80f794 100644 --- a/tests/graphs/writer_graph/test_writer_nodes.py +++ b/tests/graphs/writer_graph/test_writer_nodes.py @@ -12,6 +12,7 @@ import inspect import json +from typing import Any from unittest.mock import AsyncMock, MagicMock import pytest @@ -128,6 +129,12 @@ async def test_retrieve_samples_empty_result_no_error(monkeypatch): @pytest.mark.asyncio async def test_load_assets_missing_user_id(monkeypatch): + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(side_effect=AssertionError("should not call get_selected_repos")), + raising=False, + ) monkeypatch.setattr( writer_node, "retrieve_user_assets", @@ -142,8 +149,36 @@ async def test_load_assets_missing_user_id(monkeypatch): assert "user_id" in str(result["error"]).lower() +@pytest.mark.asyncio +async def test_load_assets_no_selected_repos(monkeypatch): + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(return_value=[]), + raising=False, + ) + monkeypatch.setattr( + writer_node, + "retrieve_user_assets", + AsyncMock(side_effect=AssertionError("should not call retrieve_user_assets")), + raising=False, + ) + + state = {"user_id": "u1", "question": "Q", "max_chars": 100} + result = await _call_node(writer_node.load_assets, state) + + assert result.get("error") == "no_selected_repos" + assert result.get("assets") == [] + + @pytest.mark.asyncio async def test_load_assets_empty_result_sets_error(monkeypatch): + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(return_value=["owner/r1"]), + raising=False, + ) monkeypatch.setattr( writer_node, "retrieve_user_assets", @@ -168,10 +203,17 @@ async def test_load_assets_success(monkeypatch): assets_out = [ {"id": "a1", "document": "요약1", "metadata": {"type": "code"}}, ] + mock_retrieve = AsyncMock(return_value=assets_out) + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(return_value=["owner/repo"]), + raising=False, + ) monkeypatch.setattr( writer_node, "retrieve_user_assets", - AsyncMock(return_value=assets_out), + mock_retrieve, raising=False, ) @@ -185,6 +227,91 @@ async def test_load_assets_success(monkeypatch): assert not result.get("error") assert result.get("assets") == assets_out + mock_retrieve.assert_awaited_once_with( + user_id="u1", + source_filter=None, + type_filter=None, + repo_filter=["owner/repo"], + top_k=20, + ) + + +@pytest.mark.asyncio +async def test_load_assets_primary_repo_merges_primary_and_supplement(monkeypatch): + """primary_repo: 최대 15 + 다른 레포당 최대 5, id 중복 제거, 총 20.""" + calls: list[dict[str, Any]] = [] + + async def fake_retrieve(**kwargs: Any) -> list[dict[str, Any]]: + calls.append(kwargs) + rf = kwargs.get("repo_filter") or [] + top_k = kwargs.get("top_k", 0) + if rf == ["p/a"]: + return [{"id": f"a{i}", "document": f"p{i}", "metadata": {"repo": "p/a"}} for i in range(min(15, top_k))] + if rf == ["p/b"]: + return [{"id": f"b{i}", "document": f"s{i}", "metadata": {"repo": "p/b"}} for i in range(min(5, top_k))] + return [] + + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(return_value=["p/a", "p/b"]), + raising=False, + ) + monkeypatch.setattr( + writer_node, + "retrieve_user_assets", + fake_retrieve, + raising=False, + ) + + state = { + "user_id": "u1", + "primary_repo": "p/a", + "question": "Q", + "max_chars": 100, + } + result = await _call_node(writer_node.load_assets, state) + + assert not result.get("error") + assets = result.get("assets") or [] + assert len(assets) == 20 + assert calls[0]["repo_filter"] == ["p/a"] and calls[0]["top_k"] == 15 + assert calls[1]["repo_filter"] == ["p/b"] and calls[1]["top_k"] == 5 + + +@pytest.mark.asyncio +async def test_load_assets_primary_repo_empty_falls_back_to_all_repos(monkeypatch): + async def fake_retrieve(**kwargs: Any) -> list[dict[str, Any]]: + rf = kwargs.get("repo_filter") or [] + if rf == ["p/x"]: + return [] + if rf == ["p/x", "p/y"]: + return [{"id": "z1", "document": "d", "metadata": {"repo": "p/y"}}] + return [] + + monkeypatch.setattr( + writer_node, + "get_selected_repos", + AsyncMock(return_value=["p/x", "p/y"]), + raising=False, + ) + monkeypatch.setattr( + writer_node, + "retrieve_user_assets", + fake_retrieve, + raising=False, + ) + + state = { + "user_id": "u1", + "primary_repo": "p/x", + "question": "Q", + "max_chars": 100, + } + result = await _call_node(writer_node.load_assets, state) + + assert not result.get("error") + assert (result.get("assets") or []) == [{"id": "z1", "document": "d", "metadata": {"repo": "p/y"}}] # --------------------------------------------------------------------------- diff --git a/tests/service/rag/test_passed_samples.py b/tests/service/rag/test_passed_samples.py index 2d6eba7..05cb6d8 100644 --- a/tests/service/rag/test_passed_samples.py +++ b/tests/service/rag/test_passed_samples.py @@ -45,8 +45,10 @@ def test_build_where_none_when_all_missing() -> None: assert _build_where_clause(None, None, None) is None -def test_build_where_single_field() -> None: - assert _build_where_clause("ACME", None, None) == {"company": {"$eq": "ACME"}} +def test_build_where_none_when_only_one_or_two_fields() -> None: + assert _build_where_clause("ACME", None, None) is None + assert _build_where_clause("ACME", "백엔드", None) is None + assert _build_where_clause(None, "백엔드", "2024") is None def test_build_where_and_three_fields() -> None: diff --git a/tests/service/rag/test_user_assets.py b/tests/service/rag/test_user_assets.py index d4f8508..8285d1f 100644 --- a/tests/service/rag/test_user_assets.py +++ b/tests/service/rag/test_user_assets.py @@ -183,6 +183,44 @@ async def test_retrieve_user_assets_builds_where_clause(monkeypatch): } +@pytest.mark.asyncio +async def test_retrieve_user_assets_builds_where_clause_with_repo_filter(monkeypatch): + fake = _make_fake_query_sync([]) + monkeypatch.setattr("src.service.rag.user_assets._query_user_assets_sync", fake) + + await retrieve_user_assets( + user_id="u1", + source_filter=["github"], + type_filter=None, + repo_filter=["a/b", "c/d"], + top_k=3, + ) + + for call in fake.calls: + assert call["where"] == { + "$and": [ + {"source": {"$in": ["github"]}}, + {"repo": {"$in": ["a/b", "c/d"]}}, + ] + } + + +@pytest.mark.asyncio +async def test_retrieve_user_assets_empty_repo_filter_returns_empty(monkeypatch): + called = [] + + def fake_sync(*args, **kwargs): + called.append(1) + return [] + + monkeypatch.setattr("src.service.rag.user_assets._query_user_assets_sync", fake_sync) + + result = await retrieve_user_assets(user_id="u1", repo_filter=[], top_k=5) + + assert result == [] + assert called == [] + + def test_portfolio_star_query_backward_compat(): """PORTFOLIO_STAR_QUERY 상수가 삭제되지 않고 유지되는지 확인한다.""" assert isinstance(PORTFOLIO_STAR_QUERY, str) diff --git a/week-issues/week5.md b/week-issues/week5.md new file mode 100644 index 0000000..8c9c05a --- /dev/null +++ b/week-issues/week5.md @@ -0,0 +1,155 @@ +# Week 5 작업 정리 + +> PR 본문에 그대로 붙여 넣을 수 있도록 요약·이슈·파일 목록까지 한 파일에 모았습니다. + +--- + +## PR 제목 (예시) + +``` +Week 5: Jobs parse, Cover letter Draft, RAG/Chroma 정합, 대시보드·GitHub 임베딩 UX +``` + +--- + +## Summary (PR 본문용) + +- **채용공고 저장**: `POST /api/jobs/parse` — SQLite `jobs`에 저장 후 `job_id` 반환 (manual / url). +- **자소서 Draft (Writer)**: `POST /api/cover-letter/draft` — 세션 필수, 선택 `job_id`로 공고 맥락 로드, LangGraph Writer 파이프라인. +- **RAG / Chroma 정합**: GitHub 임베딩 메타 `source: github`, `load_assets`에서 구 문서 호환, **임베딩 공간 불일치** 해결(OpenAI 임베딩으로 `query_embeddings` 검색). +- **대시보드 (`src/web/dashboard.py`)**: Job parse UI, Writer Draft UI, **레포 카드 탭(파일 선택 / 파일 보기 / 커밋)**, 카드 단위 히스토리 동기화·임베딩, **경로 인코딩 이슈** 대응, **페이지 로드 시 Chroma 임베딩 뱃지 동기화**. +- **백엔드**: `GET /api/user/embedding-status` — 선택 레포별 Chroma `user_assets_{user_id}`에 `repo` 메타 존재 여부로 embedded 여부 반환. + +--- + +## API·백엔드 + +### 채용공고 저장 (parse) + +- **`POST /api/jobs/parse`**: SQLite `jobs` 테이블에 공고 저장 후 `job_id` 반환. +- **manual**: 본문 직접 입력. +- **url**: 페이지를 가져와 파싱; `OPENAI_API_KEY`가 있으면 구조화 보조. + +### 자소서 Draft (Writer) + +- **`POST /api/cover-letter/draft`**: GitHub 로그인 세션 필수. 선택 `job_id`로 `jobs` + `job_parsed` 맥락 로드. +- LangGraph Writer: `retrieve_samples` → `load_assets` → `generate_draft` → `self_consistency` → `format_output`. +- 구현: `src/api/cover_letter.py`, `src/graphs/writer_graph/` (노드, 엣지, 프롬프트). + +### 앱 등록 + +- `src/app/main.py`에 jobs·cover letter 라우터 등록. + +### GitHub 임베딩 상태 (신규) + +- **`GET /api/user/embedding-status`** (`src/api/github.py`) + - 인증: 세션 `user_id` 필수. + - `selected_repos` 목록의 각 `full_name`에 대해 Chroma 컬렉션 `user_assets_{user_id}`에서 + `collection.get(where={"repo": {"$eq": full_name}}, limit=1)` — `ids`가 있으면 embedded. + - 응답: `{ "status": { "": true | false, ... } }`. + +--- + +## GitHub 코드 임베딩·RAG + +### 데모 흐름 (대시보드) + +- `asset_hierarchy` ↔ `selected_repo_assets` 동기화, GitHub Contents API, (옵션) OpenAI 요약·임베딩, Chroma `user_assets_{user_id}`. +- 문서: `docs/API_GitHub_Spec.md` (임베딩 바디 등 실제 코드와 맞춤). + +### 버그 수정 (핵심) + +1. **`source` 메타데이터** + GitHub 파이프라인 `_normalize_metadata`에 `source: github` 추가. Chroma `where` 필터와 스키마 정합. + +2. **`load_assets` 필터** + 과거 문서에 `source`가 없을 수 있어 `retrieve_user_assets(..., source_filter=None)`로 조회. + +3. **임베딩 공간 불일치 (Draft 빈 응답 원인)** + - 적재: OpenAI `text-embedding-3-small`. + - 조회: `query_texts`만 쓰면 컬렉션 기본 임베더(MiniLM 등)가 사용되어 차원·공간이 달라짐. + - **해결**: `OPENAI_API_KEY`가 있을 때 STAR 쿼리를 동일 `OpenAIEmbedder`로 임베딩한 뒤 `query_embeddings`로 검색 (`src/service/rag/user_assets.py`). + +--- + +## 프론트·대시보드 (`src/web/dashboard.py`) + +### 공통 + +- Job parse 패널 (`job_id` 자동 채움), Cover letter Draft 패널, GitHub 임베딩 안내 문구. + +### 레포 카드 (임베딩 데모 그리드) — 상세 + +- **탭 3개**: `[파일 선택] [파일 보기] [커밋]` — `repoCardMap.activeTab` (기본 `'files'`). +- **파일 선택**: 파일 트리 체크박스, 파일 선택 저장, **히스토리 → SQLite** (`sync-from-assets`), **임베딩** (카드 단건: PUT assets → sync → POST embedding; 상단 `embedRef` / `include_summaries`와 동일 규칙). +- **파일 보기**: `selectedAssetKeys` 중 `code:` 경로만 드롭다운 — 선택 시 `GET .../contents?path=...&encoding=raw`, 카드 내 `
` 표시. 카드별 `ref`는 비우면 쿼리 미전송(기본 브랜치).
+- **커밋**: `author`(비우면 서버에서 로그인 유저), `per_page`(기본 10), 커밋 조회 — 상위 10건을 `sha(7) · message · date · files_changed` 형태로 표시.
+- **에러**: fetch 실패 시 `alert` 대신 카드 내 에러 영역에 `접근 실패: ...` 형식.
+
+### 경로 인코딩 (FastAPI `{repo_id:path}` 호환)
+
+- **문제**: `encodeURIComponent(full_name)` 시 `owner/repo` → `owner%2Frepo`가 되어 FastAPI가 path로 인식하지 못해 404 / fetch 실패.
+- **해결**: 임베딩·파일·내용·커밋 요청 URL의 경로 구간에는 `encodeURIComponent(full_name)` 대신  
+  `full_name.replace(/ /g, '%20')`만 적용(슬래시 유지).  
+  Query(`path`, `ref`, `author` 등)는 기존처럼 `encodeURIComponent` 유지.
+
+### 임베딩 뱃지 동기화 (페이지 새로고침)
+
+- `loadMe()` → `loadSelectedReposUI()` 완료 후 **`GET /api/user/embedding-status`** 호출.
+- 응답 `status`로 `embeddingStatusCache` 갱신 후 **`renderAllRepoCards()`** 재호출 → ✅/⬜ 뱃지가 Chroma 실제 상태와 일치.
+
+### 기타 (raw string)
+
+- `dashboard.py`는 Python raw string이라 JS 문자열 이스케이프 주의(줄바꿈 등).
+
+---
+
+## 구현·이슈 해결 요약 (타임라인식)
+
+| 이슈 | 원인 | 해결 |
+|------|------|------|
+| `%2F`로 인코딩된 레포 경로 | `full_name`을 `encodeURIComponent` → 슬래시가 `%2F` | 경로에는 슬래시 유지, 공백만 `%20` |
+| 탭/기능 분산 | 단일 패널에 기능 몰림 | 탭 3개 + `activeTab` 상태 |
+| 뱃지와 실제 Chroma 불일치 | 세션 내 캐시만 갱신, 새로고침 시 초기화 | `GET /api/user/embedding-status` + 캐시 + 재렌더 |
+| raw string 내 JS `\n` | `\\n` vs `\n` 혼동 | 커밋 목록 등 실제 줄바꿈에 맞게 `.join('\n')` 정리 |
+
+---
+
+## 의존성
+
+- `pyproject.toml` / `poetry.lock`: 예) `httpx` 등 공고 URL fetch용.
+
+---
+
+## 테스트
+
+- `tests/api/test_cover_letter.py`, `tests/api/test_jobs_parse.py`
+- `tests/service/rag/test_user_assets.py` (OpenAI 쿼리 경로·기존 STAR 쿼리)
+- `tests/graphs/writer_graph/` 등 Writer 노드
+- 기준: 전체 pytest 통과(주차 마감 시점 기준).
+
+---
+
+## 문서
+
+- `docs/API_GitHub_Spec.md`, `docs/API_Service_Spec.md` 갱신.
+
+---
+
+## 참고·후속
+
+- Draft 응답에 `error`가 있어도 HTTP 200에서 본문만 비는 경우가 있었음 → API 레이어에서 그래프 `error`를 노출하는 개선은 후속 과제로 남길 수 있음.
+- `used_assets.github_repos`는 응답 스키마 상 플레이스홀더에 가까움.
+- `GET /api/user/embedding-status`에 대한 단위/API 테스트 추가는 선택 과제.
+
+---
+
+## 변경 파일 참고 (PR 리뷰용 체크리스트)
+
+- **API / 서비스**: `src/api/github.py`, `src/api/jobs.py`, `src/api/cover_letter.py`, `src/service/rag/user_assets.py`, `src/service/rag/passed_samples.py`, …
+- **Writer 그래프**: `src/graphs/writer_graph/node.py`, `prompts.py`, `state.py`, …
+- **대시보드**: `src/web/dashboard.py`
+- **테스트**: `tests/api/`, `tests/service/rag/`, `tests/graphs/writer_graph/`, …
+- **스크립트(선택)**: `scripts/load_passed_cover_letters.py`
+
+(정확한 diff는 `git diff main...week5` 또는 PR Files changed 탭 참고.)

From ff1874890adf17351688a5b5764a6ce6f1cadc2e Mon Sep 17 00:00:00 2001
From: Ara5429 
Date: Wed, 1 Apr 2026 11:18:23 +0900
Subject: [PATCH 3/3] fix(ci): noqa E402 for path bootstrap imports in
 load_passed_cover_letters script

Made-with: Cursor
---
 scripts/load_passed_cover_letters.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/scripts/load_passed_cover_letters.py b/scripts/load_passed_cover_letters.py
index df541c3..d236cb2 100644
--- a/scripts/load_passed_cover_letters.py
+++ b/scripts/load_passed_cover_letters.py
@@ -20,10 +20,10 @@
 if str(ROOT) not in sys.path:
     sys.path.insert(0, str(ROOT))
 
-from dotenv import load_dotenv
+from dotenv import load_dotenv  # noqa: E402
 
-from src.db.vector.chroma import get_chroma_client
-from src.service.rag.passed_samples import PASSED_COVER_LETTERS_COLLECTION
+from src.db.vector.chroma import get_chroma_client  # noqa: E402
+from src.service.rag.passed_samples import PASSED_COVER_LETTERS_COLLECTION  # noqa: E402
 
 load_dotenv(ROOT / ".env")