From 14b86c19a2ac5c92d9c21b9075ba95e8bc7d65c0 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Fri, 3 Apr 2026 22:47:16 +0600 Subject: [PATCH 1/5] refactor: reorganize imports and update media handling structure --- agentflow_cli/media/_compat.py | 23 ------------------ .../src/app/core/config/media_settings.py | 4 ++-- agentflow_cli/src/app/core/config/settings.py | 4 ++-- .../src/app/core/exceptions/handle_errors.py | 2 +- agentflow_cli/src/app/loader.py | 6 ++--- agentflow_cli/src/app/main.py | 2 +- .../src/app/routers/checkpointer/router.py | 2 +- .../schemas/checkpointer_schemas.py | 2 +- .../services/checkpointer_service.py | 4 ++-- agentflow_cli/src/app/routers/graph/router.py | 2 +- .../routers/graph/schemas/graph_schemas.py | 2 +- .../routers/graph/services/graph_service.py | 6 ++--- .../graph/services/multimodal_preprocessor.py | 4 ++-- .../src/app/routers/media/__init__.py | 20 +++++++--------- .../routers/store/schemas/store_schemas.py | 4 ++-- .../routers/store/services/store_service.py | 4 ++-- .../{ => src/app/utils}/media/__init__.py | 4 ---- .../{ => src/app/utils}/media/extractor.py | 10 ++------ .../{ => src/app/utils}/media/pipeline.py | 7 +++--- examples/currency_agent/executor.py | 19 +++++++++++---- examples/currency_agent/graph.py | 14 +++++------ graph/react.py | 8 +++---- tests/integration_tests/store/conftest.py | 2 +- tests/test_multimodal_sprint2_extraction.py | 11 ++++----- tests/test_sprint4_media_api.py | 24 +++++++++---------- tests/unit_tests/store/conftest.py | 4 ++-- tests/unit_tests/store/test_store_schemas.py | 4 ++-- tests/unit_tests/store/test_store_service.py | 2 +- tests/unit_tests/test_checkpointer_service.py | 4 ++-- tests/unit_tests/test_fix_graph.py | 4 ++-- 30 files changed, 89 insertions(+), 119 deletions(-) delete mode 100644 agentflow_cli/media/_compat.py rename agentflow_cli/{ => src/app/utils}/media/__init__.py (80%) rename agentflow_cli/{ => src/app/utils}/media/extractor.py (87%) rename agentflow_cli/{ => src/app/utils}/media/pipeline.py (92%) diff --git a/agentflow_cli/media/_compat.py b/agentflow_cli/media/_compat.py deleted file mode 100644 index fd11ed9..0000000 --- a/agentflow_cli/media/_compat.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Compatibility helpers for upstream AgentFlow media enums.""" - -from __future__ import annotations - -from agentflow.media.config import DocumentHandling - - -def ensure_document_handling_aliases() -> None: - """Expose stable aliases across AgentFlow enum renames.""" - try: - _ = DocumentHandling.PASS_RAW - except AttributeError: - DocumentHandling.PASS_RAW = DocumentHandling.FORWARD_RAW - - try: - _ = DocumentHandling.FORWARD_RAW - except AttributeError: - DocumentHandling.FORWARD_RAW = DocumentHandling.PASS_RAW - - -ensure_document_handling_aliases() - -DOCUMENT_PASS_RAW = DocumentHandling.PASS_RAW diff --git a/agentflow_cli/src/app/core/config/media_settings.py b/agentflow_cli/src/app/core/config/media_settings.py index d3590a2..8ba51c9 100644 --- a/agentflow_cli/src/app/core/config/media_settings.py +++ b/agentflow_cli/src/app/core/config/media_settings.py @@ -6,6 +6,7 @@ from enum import Enum from functools import lru_cache +from pydantic import ConfigDict from pydantic_settings import BaseSettings @@ -41,8 +42,7 @@ class MediaSettings(BaseSettings): MEDIA_SIGNED_URL_TTL_SECONDS: int = 3600 MEDIA_SIGNED_URL_REFRESH_BUFFER_SECONDS: int = 60 - class Config: - extra = "allow" + model_config = ConfigDict(extra="allow") @lru_cache diff --git a/agentflow_cli/src/app/core/config/settings.py b/agentflow_cli/src/app/core/config/settings.py index a278460..040d797 100644 --- a/agentflow_cli/src/app/core/config/settings.py +++ b/agentflow_cli/src/app/core/config/settings.py @@ -2,6 +2,7 @@ import os from functools import lru_cache +from pydantic import ConfigDict from pydantic import field_validator, model_validator from pydantic_settings import BaseSettings @@ -149,8 +150,7 @@ def check_production_security(self): return self - class Config: - extra = "allow" + model_config = ConfigDict(extra="allow") @lru_cache diff --git a/agentflow_cli/src/app/core/exceptions/handle_errors.py b/agentflow_cli/src/app/core/exceptions/handle_errors.py index d330b25..a9269a5 100644 --- a/agentflow_cli/src/app/core/exceptions/handle_errors.py +++ b/agentflow_cli/src/app/core/exceptions/handle_errors.py @@ -1,5 +1,5 @@ # Handle all exceptions of agentflow here -from agentflow.exceptions import ( +from agentflow.core.exceptions import ( GraphError, GraphRecursionError, MetricsError, diff --git a/agentflow_cli/src/app/loader.py b/agentflow_cli/src/app/loader.py index fbf888d..d310ead 100644 --- a/agentflow_cli/src/app/loader.py +++ b/agentflow_cli/src/app/loader.py @@ -4,9 +4,9 @@ import os from pathlib import Path -from agentflow.checkpointer import BaseCheckpointer -from agentflow.graph import CompiledGraph -from agentflow.store import BaseStore +from agentflow.core import CompiledGraph +from agentflow.storage.checkpointer import BaseCheckpointer +from agentflow.storage.store import BaseStore from injectq import InjectQ from agentflow_cli import BaseAuth diff --git a/agentflow_cli/src/app/main.py b/agentflow_cli/src/app/main.py index 83a72cb..c421abc 100644 --- a/agentflow_cli/src/app/main.py +++ b/agentflow_cli/src/app/main.py @@ -1,6 +1,6 @@ import os -from agentflow.graph import CompiledGraph +from agentflow.core.graph import CompiledGraph from fastapi import FastAPI from fastapi.concurrency import asynccontextmanager from fastapi.responses import ORJSONResponse diff --git a/agentflow_cli/src/app/routers/checkpointer/router.py b/agentflow_cli/src/app/routers/checkpointer/router.py index c6f5b46..5aa609a 100644 --- a/agentflow_cli/src/app/routers/checkpointer/router.py +++ b/agentflow_cli/src/app/routers/checkpointer/router.py @@ -2,7 +2,7 @@ from typing import Any -from agentflow.state import Message +from agentflow.core.state import Message from fastapi import APIRouter, Depends, HTTPException, Request, status from injectq.integrations import InjectAPI diff --git a/agentflow_cli/src/app/routers/checkpointer/schemas/checkpointer_schemas.py b/agentflow_cli/src/app/routers/checkpointer/schemas/checkpointer_schemas.py index 7dafb8a..8e3eb83 100644 --- a/agentflow_cli/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +++ b/agentflow_cli/src/app/routers/checkpointer/schemas/checkpointer_schemas.py @@ -2,7 +2,7 @@ from typing import Any -from agentflow.state import Message +from agentflow.core.state import Message from pydantic import BaseModel, Field diff --git a/agentflow_cli/src/app/routers/checkpointer/services/checkpointer_service.py b/agentflow_cli/src/app/routers/checkpointer/services/checkpointer_service.py index 88335d5..f68ef98 100644 --- a/agentflow_cli/src/app/routers/checkpointer/services/checkpointer_service.py +++ b/agentflow_cli/src/app/routers/checkpointer/services/checkpointer_service.py @@ -1,7 +1,7 @@ from typing import Any -from agentflow.checkpointer import BaseCheckpointer -from agentflow.state import AgentState, Message +from agentflow.core.state import AgentState, Message +from agentflow.storage.checkpointer import BaseCheckpointer from fastapi import HTTPException from injectq import inject, singleton diff --git a/agentflow_cli/src/app/routers/graph/router.py b/agentflow_cli/src/app/routers/graph/router.py index ccdcd79..8367136 100644 --- a/agentflow_cli/src/app/routers/graph/router.py +++ b/agentflow_cli/src/app/routers/graph/router.py @@ -1,6 +1,6 @@ from typing import Any -from agentflow.state import StreamChunk +from agentflow.core.state import StreamChunk from fastapi import APIRouter, Depends, Request from fastapi.logger import logger from fastapi.responses import StreamingResponse diff --git a/agentflow_cli/src/app/routers/graph/schemas/graph_schemas.py b/agentflow_cli/src/app/routers/graph/schemas/graph_schemas.py index 2f07170..0eb6ec8 100644 --- a/agentflow_cli/src/app/routers/graph/schemas/graph_schemas.py +++ b/agentflow_cli/src/app/routers/graph/schemas/graph_schemas.py @@ -1,6 +1,6 @@ from typing import Any -from agentflow.state import Message +from agentflow.core.state import Message from agentflow.utils import ResponseGranularity from pydantic import BaseModel, Field, field_validator diff --git a/agentflow_cli/src/app/routers/graph/services/graph_service.py b/agentflow_cli/src/app/routers/graph/services/graph_service.py index 08405d0..482e930 100644 --- a/agentflow_cli/src/app/routers/graph/services/graph_service.py +++ b/agentflow_cli/src/app/routers/graph/services/graph_service.py @@ -3,9 +3,9 @@ from typing import Any from uuid import uuid4 -from agentflow.checkpointer import BaseCheckpointer -from agentflow.graph import CompiledGraph -from agentflow.state import AgentState, Message, StreamChunk, StreamEvent +from agentflow.core.graph import CompiledGraph +from agentflow.core.state import AgentState, Message, StreamChunk, StreamEvent +from agentflow.storage.checkpointer import BaseCheckpointer from agentflow.utils.thread_info import ThreadInfo from fastapi import HTTPException from injectq import InjectQ, inject, singleton diff --git a/agentflow_cli/src/app/routers/graph/services/multimodal_preprocessor.py b/agentflow_cli/src/app/routers/graph/services/multimodal_preprocessor.py index 079e4ed..16fac22 100644 --- a/agentflow_cli/src/app/routers/graph/services/multimodal_preprocessor.py +++ b/agentflow_cli/src/app/routers/graph/services/multimodal_preprocessor.py @@ -6,8 +6,8 @@ import logging from typing import TYPE_CHECKING -from agentflow.state import Message -from agentflow.state.message_block import DocumentBlock, TextBlock +from agentflow.core.state import Message +from agentflow.core.state.message_block import DocumentBlock, TextBlock if TYPE_CHECKING: diff --git a/agentflow_cli/src/app/routers/media/__init__.py b/agentflow_cli/src/app/routers/media/__init__.py index 917c473..789e250 100644 --- a/agentflow_cli/src/app/routers/media/__init__.py +++ b/agentflow_cli/src/app/routers/media/__init__.py @@ -8,20 +8,18 @@ import time from typing import Any -from agentflow.checkpointer import BaseCheckpointer -from agentflow.media.config import DocumentHandling -from agentflow.media.storage.base import BaseMediaStore +from agentflow.storage.checkpointer import BaseCheckpointer +from agentflow.storage.media.config import DocumentHandling +from agentflow.storage.media.storage.base import BaseMediaStore from injectq import InjectQ, inject, singleton -from agentflow_cli.media._compat import DOCUMENT_PASS_RAW, ensure_document_handling_aliases -from agentflow_cli.media.extractor import DocumentExtractor -from agentflow_cli.media.pipeline import DocumentPipeline from agentflow_cli.src.app.core.config.media_settings import MediaSettings, MediaStorageType +from agentflow_cli.src.app.utils.media.extractor import DocumentExtractor +from agentflow_cli.src.app.utils.media.pipeline import DocumentPipeline logger = logging.getLogger("agentflow-cli.media") -ensure_document_handling_aliases() _SIGNED_URL_NAMESPACE = "media:signed-url" _EXTRACTION_NAMESPACE = "media:extraction" @@ -41,17 +39,17 @@ def _create_media_store(settings: MediaSettings) -> BaseMediaStore: stype = settings.MEDIA_STORAGE_TYPE if stype == MediaStorageType.MEMORY: - from agentflow.media.storage.memory_store import InMemoryMediaStore + from agentflow.storage.media.storage.memory_store import InMemoryMediaStore return InMemoryMediaStore() if stype == MediaStorageType.LOCAL: - from agentflow.media.storage.local_store import LocalFileMediaStore + from agentflow.storage.media.storage.local_store import LocalFileMediaStore return LocalFileMediaStore(base_dir=settings.MEDIA_STORAGE_PATH) if stype == MediaStorageType.CLOUD: - from agentflow.media.storage.cloud_store import CloudMediaStore + from agentflow.storage.media.storage.cloud_store import CloudMediaStore from cloud_storage_manager import ( AwsConfig, CloudStorageFactory, @@ -117,7 +115,7 @@ def _create_media_store(settings: MediaSettings) -> BaseMediaStore: def _create_document_pipeline(settings: MediaSettings) -> DocumentPipeline: handling_map = { "extract_text": DocumentHandling.EXTRACT_TEXT, - "pass_raw": DOCUMENT_PASS_RAW, + "pass_raw": DocumentHandling.FORWARD_RAW, "skip": DocumentHandling.SKIP, } handling = handling_map.get(settings.DOCUMENT_HANDLING, DocumentHandling.EXTRACT_TEXT) diff --git a/agentflow_cli/src/app/routers/store/schemas/store_schemas.py b/agentflow_cli/src/app/routers/store/schemas/store_schemas.py index 38c9d1b..b5e1570 100644 --- a/agentflow_cli/src/app/routers/store/schemas/store_schemas.py +++ b/agentflow_cli/src/app/routers/store/schemas/store_schemas.py @@ -4,8 +4,8 @@ from typing import Any -from agentflow.state import Message -from agentflow.store.store_schema import ( +from agentflow.core.state import Message +from agentflow.storage.store.store_schema import ( DistanceMetric, MemoryRecord, MemorySearchResult, diff --git a/agentflow_cli/src/app/routers/store/services/store_service.py b/agentflow_cli/src/app/routers/store/services/store_service.py index 2925f4b..ffd1320 100644 --- a/agentflow_cli/src/app/routers/store/services/store_service.py +++ b/agentflow_cli/src/app/routers/store/services/store_service.py @@ -2,8 +2,8 @@ from typing import Any -from agentflow.state import Message -from agentflow.store import BaseStore +from agentflow.core.state import Message +from agentflow.storage.store import BaseStore from fastapi import HTTPException from injectq import inject, singleton diff --git a/agentflow_cli/media/__init__.py b/agentflow_cli/src/app/utils/media/__init__.py similarity index 80% rename from agentflow_cli/media/__init__.py rename to agentflow_cli/src/app/utils/media/__init__.py index 72f099f..f710a7a 100644 --- a/agentflow_cli/media/__init__.py +++ b/agentflow_cli/src/app/utils/media/__init__.py @@ -5,14 +5,10 @@ The API platform auto-extracts using textxtract. """ -from ._compat import ensure_document_handling_aliases from .extractor import DocumentExtractor from .pipeline import DocumentPipeline -ensure_document_handling_aliases() - - __all__ = [ "DocumentExtractor", "DocumentPipeline", diff --git a/agentflow_cli/media/extractor.py b/agentflow_cli/src/app/utils/media/extractor.py similarity index 87% rename from agentflow_cli/media/extractor.py rename to agentflow_cli/src/app/utils/media/extractor.py index 69dbcc2..8aec11d 100644 --- a/agentflow_cli/media/extractor.py +++ b/agentflow_cli/src/app/utils/media/extractor.py @@ -19,12 +19,6 @@ except ImportError: # pragma: no cover AsyncTextExtractor = None # type: ignore[assignment] - class FileTypeNotSupportedError(Exception): # type: ignore[no-redef] - """Fallback exception when textxtract is unavailable.""" - - class ExtractionError(Exception): # type: ignore[no-redef] - """Fallback exception when textxtract is unavailable.""" - class DocumentExtractor: """Wraps textxtract AsyncTextExtractor for API-side document extraction. @@ -71,9 +65,9 @@ async def extract(self, data: bytes | str, filename: str | None = None) -> str | return await self.extractor.extract(data, filename) return await self.extractor.extract(data) - except FileTypeNotSupportedError: + except FileTypeNotSupportedError: # type: ignore logger.warning("Document type not supported for extraction: %s", filename) return None - except ExtractionError as exc: + except ExtractionError as exc: # type: ignore logger.exception("Document extraction failed for %s", filename) raise ValueError("Failed to extract text from document") from exc diff --git a/agentflow_cli/media/pipeline.py b/agentflow_cli/src/app/utils/media/pipeline.py similarity index 92% rename from agentflow_cli/media/pipeline.py rename to agentflow_cli/src/app/utils/media/pipeline.py index 1328127..061e0fd 100644 --- a/agentflow_cli/media/pipeline.py +++ b/agentflow_cli/src/app/utils/media/pipeline.py @@ -10,10 +10,9 @@ from base64 import b64decode from typing import Any -from agentflow.media.config import DocumentHandling -from agentflow.state.message_block import DocumentBlock, TextBlock +from agentflow.core.state.message_block import DocumentBlock, TextBlock +from agentflow.storage.media.config import DocumentHandling -from ._compat import DOCUMENT_PASS_RAW from .extractor import DocumentExtractor @@ -57,7 +56,7 @@ async def process_document(self, document_block: Any) -> Any | None: if self.handling == DocumentHandling.SKIP: return None - if self.handling == DOCUMENT_PASS_RAW: + if self.handling == DocumentHandling.FORWARD_RAW: return document_block # EXTRACT_TEXT path diff --git a/examples/currency_agent/executor.py b/examples/currency_agent/executor.py index 1c4781e..fdc47a0 100644 --- a/examples/currency_agent/executor.py +++ b/examples/currency_agent/executor.py @@ -18,7 +18,7 @@ from a2a.types import TaskState, TextPart from agentflow.a2a_integration.executor import AgentFlowExecutor -from agentflow.state import Message as AFMessage +from agentflow.core.state import Message as AFMessage from agentflow.utils.constants import ResponseGranularity logger = logging.getLogger(__name__) @@ -28,10 +28,18 @@ # --------------------------------------------------------------------------- # _ASKING_PHRASES = [ - "could you", "please provide", "please specify", - "what amount", "which currency", "what currency", - "let me know", "can you tell", "i need", - "please tell", "what is the", "what date", + "could you", + "please provide", + "please specify", + "what amount", + "which currency", + "what currency", + "let me know", + "can you tell", + "i need", + "please tell", + "what is the", + "what date", ] @@ -44,6 +52,7 @@ def _is_asking_for_input(text: str) -> bool: # Executor # # --------------------------------------------------------------------------- # + class CurrencyAgentExecutor(AgentFlowExecutor): """Runs the currency graph; emits INPUT_REQUIRED for vague queries.""" diff --git a/examples/currency_agent/graph.py b/examples/currency_agent/graph.py index 7085cc0..8c0af6f 100644 --- a/examples/currency_agent/graph.py +++ b/examples/currency_agent/graph.py @@ -12,9 +12,9 @@ from dotenv import load_dotenv from litellm import acompletion -from agentflow.adapters.llm.model_response_converter import ModelResponseConverter -from agentflow.graph import StateGraph, ToolNode -from agentflow.state import AgentState +from agentflow.runtime.adapters.llm.model_response_converter import ModelResponseConverter +from agentflow.core.graph import StateGraph, ToolNode +from agentflow.core.state import AgentState from agentflow.utils.constants import END from agentflow.utils.converter import convert_messages @@ -24,6 +24,7 @@ # Tool — Frankfurter API # # --------------------------------------------------------------------------- # + async def get_exchange_rate( currency_from: str, currency_to: str, @@ -90,17 +91,14 @@ async def llm_node(state: AgentState): # Routing # # --------------------------------------------------------------------------- # + def should_use_tools(state: AgentState) -> str: if not state.context: return END last = state.context[-1] - if ( - hasattr(last, "tools_calls") - and last.tools_calls - and last.role == "assistant" - ): + if hasattr(last, "tools_calls") and last.tools_calls and last.role == "assistant": return "TOOL" if last.role == "tool": diff --git a/graph/react.py b/graph/react.py index 5e45e52..a6bbd70 100644 --- a/graph/react.py +++ b/graph/react.py @@ -1,7 +1,7 @@ -from agentflow.checkpointer import InMemoryCheckpointer -from agentflow.graph import StateGraph, ToolNode -from agentflow.graph.agent import Agent -from agentflow.state import AgentState +from agentflow.storage.checkpointer import InMemoryCheckpointer +from agentflow.core.graph import StateGraph, ToolNode +from agentflow.core.graph.agent import Agent +from agentflow.core.state import AgentState from agentflow.utils.constants import END from dotenv import load_dotenv diff --git a/tests/integration_tests/store/conftest.py b/tests/integration_tests/store/conftest.py index bad47ff..f08c61a 100644 --- a/tests/integration_tests/store/conftest.py +++ b/tests/integration_tests/store/conftest.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from agentflow.store import BaseStore, MemorySearchResult, MemoryType +from agentflow.storage.store import BaseStore, MemorySearchResult, MemoryType from fastapi import FastAPI from fastapi.testclient import TestClient diff --git a/tests/test_multimodal_sprint2_extraction.py b/tests/test_multimodal_sprint2_extraction.py index 6fb702f..dc2654d 100644 --- a/tests/test_multimodal_sprint2_extraction.py +++ b/tests/test_multimodal_sprint2_extraction.py @@ -5,16 +5,15 @@ """ import pytest +from agentflow.storage.media.config import DocumentHandling +from agentflow.core.state.message_block import DocumentBlock, MediaRef, TextBlock -from agentflow.media.config import DocumentHandling -from agentflow.state.message_block import DocumentBlock, MediaRef, TextBlock - -from agentflow_cli.media.extractor import ( +from agentflow_cli.src.app.utils.media.extractor import ( DocumentExtractor, ExtractionError, FileTypeNotSupportedError, ) -from agentflow_cli.media.pipeline import DocumentPipeline +from agentflow_cli.src.app.utils.media.pipeline import DocumentPipeline # --------------------------------------------------------------------------- @@ -73,7 +72,7 @@ async def test_extraction_error_raises_value_error(self): def test_no_textxtract_raises_import_error(self, monkeypatch): """If textxtract is not installed, instantiation raises ImportError.""" - import agentflow_cli.media.extractor as mod + import agentflow_cli.src.app.utils.media.extractor as mod monkeypatch.setattr(mod, "AsyncTextExtractor", None) with pytest.raises(ImportError, match="textxtract is required"): diff --git a/tests/test_sprint4_media_api.py b/tests/test_sprint4_media_api.py index 1db44f1..ef48a49 100644 --- a/tests/test_sprint4_media_api.py +++ b/tests/test_sprint4_media_api.py @@ -14,7 +14,7 @@ # --------------------------------------------------------------------------- # MediaService unit tests # --------------------------------------------------------------------------- -from agentflow.checkpointer import InMemoryCheckpointer +from agentflow.storage.checkpointer import InMemoryCheckpointer from agentflow_cli.src.app.core.config.media_settings import MediaSettings, MediaStorageType @@ -165,7 +165,7 @@ class TestMultimodalPreprocessor: @pytest.mark.asyncio async def test_noop_when_no_media_service(self): - from agentflow.state import Message + from agentflow.core.state import Message from agentflow_cli.src.app.routers.graph.services.multimodal_preprocessor import ( preprocess_multimodal_messages, @@ -177,8 +177,8 @@ async def test_noop_when_no_media_service(self): @pytest.mark.asyncio async def test_document_file_id_resolved_to_text(self): - from agentflow.state import Message - from agentflow.state.message_block import DocumentBlock, MediaRef, TextBlock + from agentflow.core.state import Message + from agentflow.core.state.message_block import DocumentBlock, MediaRef, TextBlock from agentflow_cli.src.app.routers.graph.services.multimodal_preprocessor import ( preprocess_multimodal_messages, @@ -202,8 +202,8 @@ async def test_document_file_id_resolved_to_text(self): @pytest.mark.asyncio async def test_image_file_id_to_agentflow_url(self): - from agentflow.state import Message - from agentflow.state.message_block import ImageBlock, MediaRef + from agentflow.core.state import Message + from agentflow.core.state.message_block import ImageBlock, MediaRef from agentflow_cli.src.app.routers.graph.services.multimodal_preprocessor import ( preprocess_multimodal_messages, @@ -224,8 +224,8 @@ async def test_image_file_id_to_agentflow_url(self): @pytest.mark.asyncio async def test_text_only_message_unchanged(self): - from agentflow.state import Message - from agentflow.state.message_block import TextBlock + from agentflow.core.state import Message + from agentflow.core.state.message_block import TextBlock from agentflow_cli.src.app.routers.graph.services.multimodal_preprocessor import ( preprocess_multimodal_messages, @@ -239,8 +239,8 @@ async def test_text_only_message_unchanged(self): @pytest.mark.asyncio async def test_document_file_id_without_cached_text(self): - from agentflow.state import Message - from agentflow.state.message_block import DocumentBlock, MediaRef + from agentflow.core.state import Message + from agentflow.core.state.message_block import DocumentBlock, MediaRef from agentflow_cli.src.app.routers.graph.services.multimodal_preprocessor import ( preprocess_multimodal_messages, @@ -285,7 +285,7 @@ def test_memory_type(self): class TestMediaStoreFactory: def test_memory_store(self): - from agentflow.media.storage.memory_store import InMemoryMediaStore + from agentflow.storage.media.storage.memory_store import InMemoryMediaStore from agentflow_cli.src.app.routers.media import _create_media_store s = _make_settings(MEDIA_STORAGE_TYPE=MediaStorageType.MEMORY) @@ -293,7 +293,7 @@ def test_memory_store(self): assert isinstance(store, InMemoryMediaStore) def test_local_store(self): - from agentflow.media.storage.local_store import LocalFileMediaStore + from agentflow.storage.media.storage.local_store import LocalFileMediaStore from agentflow_cli.src.app.routers.media import _create_media_store s = _make_settings(MEDIA_STORAGE_TYPE=MediaStorageType.LOCAL) diff --git a/tests/unit_tests/store/conftest.py b/tests/unit_tests/store/conftest.py index 203c1fb..0059bc2 100644 --- a/tests/unit_tests/store/conftest.py +++ b/tests/unit_tests/store/conftest.py @@ -4,8 +4,8 @@ from uuid import uuid4 import pytest -from agentflow.state import Message -from agentflow.store import BaseStore, MemorySearchResult, MemoryType +from agentflow.core.state import Message +from agentflow.storage.store import BaseStore, MemorySearchResult, MemoryType from agentflow_cli.src.app.routers.store.services.store_service import StoreService diff --git a/tests/unit_tests/store/test_store_schemas.py b/tests/unit_tests/store/test_store_schemas.py index c3e1bb2..44f253c 100644 --- a/tests/unit_tests/store/test_store_schemas.py +++ b/tests/unit_tests/store/test_store_schemas.py @@ -1,8 +1,8 @@ """Unit tests for store schemas.""" import pytest -from agentflow.state import Message -from agentflow.store.store_schema import DistanceMetric, MemoryType, RetrievalStrategy +from agentflow.core.state import Message +from agentflow.storage.store.store_schema import DistanceMetric, MemoryType, RetrievalStrategy from pydantic import ValidationError from agentflow_cli.src.app.routers.store.schemas.store_schemas import ( diff --git a/tests/unit_tests/store/test_store_service.py b/tests/unit_tests/store/test_store_service.py index 4ba6724..8dd8ce3 100644 --- a/tests/unit_tests/store/test_store_service.py +++ b/tests/unit_tests/store/test_store_service.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from agentflow.store.store_schema import DistanceMetric, MemoryType, RetrievalStrategy +from agentflow.storage.store.store_schema import DistanceMetric, MemoryType, RetrievalStrategy from agentflow_cli.src.app.routers.store.schemas.store_schemas import ( DeleteMemorySchema, diff --git a/tests/unit_tests/test_checkpointer_service.py b/tests/unit_tests/test_checkpointer_service.py index 220f57b..9d56741 100644 --- a/tests/unit_tests/test_checkpointer_service.py +++ b/tests/unit_tests/test_checkpointer_service.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agentflow.checkpointer import BaseCheckpointer -from agentflow.state import AgentState, Message +from agentflow.storage.checkpointer import BaseCheckpointer +from agentflow.core.state import AgentState, Message from agentflow_cli.src.app.routers.checkpointer.schemas.checkpointer_schemas import ( MessagesListResponseSchema, diff --git a/tests/unit_tests/test_fix_graph.py b/tests/unit_tests/test_fix_graph.py index 92ff43b..73b76bf 100644 --- a/tests/unit_tests/test_fix_graph.py +++ b/tests/unit_tests/test_fix_graph.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agentflow.checkpointer import BaseCheckpointer -from agentflow.state import AgentState, Message, TextBlock +from agentflow.storage.checkpointer import BaseCheckpointer +from agentflow.core.state import AgentState, Message, TextBlock from fastapi import HTTPException from agentflow_cli.src.app.routers.graph.services.graph_service import GraphService From 1aa8305643c5e66ba49ef8a522f25e07d0345bb0 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Fri, 3 Apr 2026 22:47:37 +0600 Subject: [PATCH 2/5] refactor: consolidate import statements in settings and react modules --- agentflow_cli/src/app/core/config/settings.py | 3 +-- graph/react.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/agentflow_cli/src/app/core/config/settings.py b/agentflow_cli/src/app/core/config/settings.py index 040d797..a61fa55 100644 --- a/agentflow_cli/src/app/core/config/settings.py +++ b/agentflow_cli/src/app/core/config/settings.py @@ -2,8 +2,7 @@ import os from functools import lru_cache -from pydantic import ConfigDict -from pydantic import field_validator, model_validator +from pydantic import ConfigDict, field_validator, model_validator from pydantic_settings import BaseSettings diff --git a/graph/react.py b/graph/react.py index a6bbd70..f20b5d9 100644 --- a/graph/react.py +++ b/graph/react.py @@ -1,7 +1,7 @@ -from agentflow.storage.checkpointer import InMemoryCheckpointer from agentflow.core.graph import StateGraph, ToolNode from agentflow.core.graph.agent import Agent from agentflow.core.state import AgentState +from agentflow.storage.checkpointer import InMemoryCheckpointer from agentflow.utils.constants import END from dotenv import load_dotenv From e4ace05f2a1f9b82004bcaa448024fe95f200460 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Fri, 3 Apr 2026 22:57:11 +0600 Subject: [PATCH 3/5] refactor: update DocumentHandling usage in DocumentPipeline tests --- tests/test_multimodal_sprint2_extraction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_multimodal_sprint2_extraction.py b/tests/test_multimodal_sprint2_extraction.py index dc2654d..19fb9b6 100644 --- a/tests/test_multimodal_sprint2_extraction.py +++ b/tests/test_multimodal_sprint2_extraction.py @@ -5,8 +5,8 @@ """ import pytest -from agentflow.storage.media.config import DocumentHandling from agentflow.core.state.message_block import DocumentBlock, MediaRef, TextBlock +from agentflow.storage.media.config import DocumentHandling from agentflow_cli.src.app.utils.media.extractor import ( DocumentExtractor, @@ -104,7 +104,7 @@ async def test_skip_returns_none(self): async def test_pass_raw_returns_original(self): pipeline = DocumentPipeline( document_extractor=DocumentExtractor(extractor=FakeExtractor()), - handling=DocumentHandling.PASS_RAW, + handling=DocumentHandling.FORWARD_RAW, ) block = DocumentBlock(media=MediaRef(kind="url", url="https://example.com/doc.pdf")) result = await pipeline.process_document(block) From edef90e8844d9eeb21bc565f228a79886520f865 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 6 Apr 2026 20:30:55 +0600 Subject: [PATCH 4/5] Refactor code structure for improved readability and maintainability --- agentflow_cli/src/app/core/auth/jwt_auth.py | 15 +- agentflow_cli/src/app/core/config/settings.py | 6 + agentflow_cli/src/app/main.py | 28 ++ .../routers/graph/services/graph_service.py | 4 + pyproject.toml | 4 +- tests/unit_tests/test_checkpointer_service.py | 2 +- uv.lock | 246 +++++++++++++++++- 7 files changed, 286 insertions(+), 19 deletions(-) diff --git a/agentflow_cli/src/app/core/auth/jwt_auth.py b/agentflow_cli/src/app/core/auth/jwt_auth.py index 7937666..a49face 100644 --- a/agentflow_cli/src/app/core/auth/jwt_auth.py +++ b/agentflow_cli/src/app/core/auth/jwt_auth.py @@ -1,4 +1,3 @@ -import os from typing import Any import jwt @@ -7,6 +6,7 @@ from agentflow_cli.src.app.core import logger from agentflow_cli.src.app.core.auth.base_auth import BaseAuth +from agentflow_cli.src.app.core.config.settings import get_settings from agentflow_cli.src.app.core.exceptions import UserAccountError @@ -44,13 +44,12 @@ def authenticate( message="Invalid token, please login again", error_code="REVOKED_TOKEN", ) - jwt_secret_key = os.environ.get("JWT_SECRET_KEY", None) - jwt_algorithm = os.environ.get("JWT_ALGORITHM", None) - # check bearer token then remove barer prefix + settings = get_settings() + jwt_secret_key = settings.JWT_SECRET_KEY + jwt_algorithm = settings.JWT_ALGORITHM + token = credential.credentials - if token.lower().startswith("bearer "): - token = token[7:] if jwt_secret_key is None or jwt_algorithm is None: raise UserAccountError( @@ -61,8 +60,8 @@ def authenticate( try: decoded_token = jwt.decode( token, - jwt_secret_key, # type: ignore - algorithms=[jwt_algorithm], # type: ignore + jwt_secret_key, + algorithms=[jwt_algorithm], ) except jwt.ExpiredSignatureError: raise UserAccountError( diff --git a/agentflow_cli/src/app/core/config/settings.py b/agentflow_cli/src/app/core/config/settings.py index a61fa55..9fbc6bc 100644 --- a/agentflow_cli/src/app/core/config/settings.py +++ b/agentflow_cli/src/app/core/config/settings.py @@ -102,6 +102,12 @@ class Settings(BaseSettings): SNOWFLAKE_NODE_BITS: int = 5 SNOWFLAKE_WORKER_BITS: int = 8 + ################################# + ###### JWT Config ############### + ################################# + JWT_SECRET_KEY: str | None = None + JWT_ALGORITHM: str = "HS256" + @field_validator("MODE", mode="before") @classmethod def normalize_mode(cls, v: str | None) -> str: diff --git a/agentflow_cli/src/app/main.py b/agentflow_cli/src/app/main.py index c421abc..aef1173 100644 --- a/agentflow_cli/src/app/main.py +++ b/agentflow_cli/src/app/main.py @@ -1,3 +1,4 @@ +import logging import os from agentflow.core.graph import CompiledGraph @@ -21,6 +22,8 @@ from agentflow_cli.src.app.routers import init_routes +logger = logging.getLogger("agentflow_api") + settings = get_settings() # redis_client = Redis( # host=settings.REDIS_HOST, @@ -36,6 +39,28 @@ container.bind_instance(GraphConfig, graph_config) +async def _cleanup_temp_media_cache() -> None: + """Run best-effort cleanup of expired temporary media cache entries.""" + try: + from agentflow.storage.media.temp_cache import TemporaryMediaCache + + checkpointer = container.try_get("checkpointer") or container.try_get("BaseCheckpointer") + media_store = container.try_get("media_store") or container.try_get("BaseMediaStore") + + if checkpointer is None: + logger.debug("No checkpointer available, skipping temp media cache cleanup") + return + + cache = TemporaryMediaCache() + cleaned = await cache.cleanup(checkpointer, media_store) + if cleaned: + logger.info("Cleaned up %d expired temporary media cache entries on startup", cleaned) + else: + logger.debug("No expired temporary media cache entries to clean up") + except Exception as e: + logger.warning("Failed to clean up temporary media cache on startup: %s", e) + + @asynccontextmanager async def lifespan(app: FastAPI): # Load the cache @@ -46,6 +71,9 @@ async def lifespan(app: FastAPI): container=container, ) + # Clean up expired temporary media cache on startup + await _cleanup_temp_media_cache() + # load Store # store = load_store(graph_config.store_path) # injector.binder.bind(BaseStore, store) diff --git a/agentflow_cli/src/app/routers/graph/services/graph_service.py b/agentflow_cli/src/app/routers/graph/services/graph_service.py index 482e930..f553df1 100644 --- a/agentflow_cli/src/app/routers/graph/services/graph_service.py +++ b/agentflow_cli/src/app/routers/graph/services/graph_service.py @@ -3,6 +3,7 @@ from typing import Any from uuid import uuid4 +from agentflow.core.exceptions.media_exceptions import UnsupportedMediaInputError from agentflow.core.graph import CompiledGraph from agentflow.core.state import AgentState, Message, StreamChunk, StreamEvent from agentflow.storage.checkpointer import BaseCheckpointer @@ -284,6 +285,9 @@ async def invoke_graph( meta=meta, ) + except UnsupportedMediaInputError as e: + logger.warning("Unsupported media input: %s", e.message) + raise HTTPException(status_code=422, detail=e.message) except ValueError as e: logger.warning(f"Graph input validation failed: {e}") raise HTTPException(status_code=422, detail=str(e)) diff --git a/pyproject.toml b/pyproject.toml index 1b5538d..7a53d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "10xscale-agentflow-cli" -version = "0.2.9" +version = "0.3.0.1" description = "CLI and API for 10xscale AgentFlow" readme = "README.md" license = {text = "MIT"} @@ -34,7 +34,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP :: HTTP Servers", ] dependencies = [ - "10xscale-agentflow>=0.5.0", + "10xscale-agentflow>=0.7.0", "fastapi", "gunicorn", "orjson", diff --git a/tests/unit_tests/test_checkpointer_service.py b/tests/unit_tests/test_checkpointer_service.py index 9d56741..d25e17c 100644 --- a/tests/unit_tests/test_checkpointer_service.py +++ b/tests/unit_tests/test_checkpointer_service.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agentflow.storage.checkpointer import BaseCheckpointer from agentflow.core.state import AgentState, Message +from agentflow.storage.checkpointer import BaseCheckpointer from agentflow_cli.src.app.routers.checkpointer.schemas.checkpointer_schemas import ( MessagesListResponseSchema, diff --git a/uv.lock b/uv.lock index 82143f0..866f826 100644 --- a/uv.lock +++ b/uv.lock @@ -8,21 +8,22 @@ resolution-markers = [ [[package]] name = "10xscale-agentflow" -version = "0.5.2" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "injectq" }, + { name = "pillow" }, { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/e5/38e25c34c08e4fde4e79d4b9e0547dcafa4df4b1b3b0d4ae39e1d07f4db5/10xscale_agentflow-0.5.2.tar.gz", hash = "sha256:418a70662d742427ff8a6dd86d2ea4b294701a9d5210315427f7e054a015c2a8", size = 181272, upload-time = "2025-11-20T16:02:25.668Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/87/c68ad65b41dcbee73be2ceddea1b8c8ff0b37e02b4fc0ca217a18271a6df/10xscale_agentflow-0.7.0.tar.gz", hash = "sha256:caf79b75d4d7451bb6123e5cf28251936924a8e6d3f3e0923d8c38b60bb0b3a2", size = 388433, upload-time = "2026-04-05T17:16:49.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/56/6b9a0ce4b3288ec078157942d74ffab83aab7e4d39a391b742bebf52d122/10xscale_agentflow-0.5.2-py3-none-any.whl", hash = "sha256:9e6d30e3022857e429fac5b48fb5c8f0d78f892dd73bf16d56999775d2f6cdfb", size = 196284, upload-time = "2025-11-20T16:02:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/46/01/f31656c7ea2561d50050ed6e571345c2c3b8af79fce01e3c48050608f0dd/10xscale_agentflow-0.7.0-py3-none-any.whl", hash = "sha256:093b51c2de521e1c7208a1b0a8fb9a4b02df6c98ada94ca2c43cfba2e4a3939e", size = 423797, upload-time = "2026-04-05T17:16:47.432Z" }, ] [[package]] name = "10xscale-agentflow-cli" -version = "0.2.9" +version = "0.3.0.1" source = { editable = "." } dependencies = [ { name = "10xscale-agentflow" }, @@ -34,6 +35,7 @@ dependencies = [ { name = "pyjwt" }, { name = "python-dotenv" }, { name = "python-multipart" }, + { name = "textxtract", extra = ["docx", "html", "md", "pdf", "xml"] }, { name = "typer" }, { name = "uvicorn" }, ] @@ -77,7 +79,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "10xscale-agentflow", specifier = ">=0.5.0" }, + { name = "10xscale-agentflow", specifier = ">=0.7.0" }, { name = "fastapi" }, { name = "firebase-admin", marker = "extra == 'firebase'", specifier = ">=6.5.0" }, { name = "google-cloud-logging", marker = "extra == 'gcloud'" }, @@ -92,6 +94,7 @@ requires-dist = [ { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.7" }, { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.10.0" }, { name = "snowflakekit", marker = "extra == 'snowflakekit'" }, + { name = "textxtract", extras = ["pdf", "docx", "html", "xml", "md"], specifier = ">=0.2.0" }, { name = "typer" }, { name = "uvicorn" }, ] @@ -139,6 +142,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "cachecontrol" version = "0.14.3" @@ -845,14 +861,14 @@ wheels = [ [[package]] name = "injectq" -version = "0.3.2" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/48/27f54089093be0c99c6d76120fd8abc39c32ca8e7d0e38c0faca4e0bbca3/injectq-0.3.2.tar.gz", hash = "sha256:b8499c4080816ed11a469df645a0d8811f15cd627f4b265115e31db2b23d29d0", size = 454498, upload-time = "2025-09-15T02:31:17.7Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/78/6c7b66d0c9ff30a61baa196a3a4d2621d890c9bbe1b081781ff0797f13d8/injectq-0.4.1.tar.gz", hash = "sha256:de383debdb1591d4be9e2164e5ba1686af4ea0686885b3d085c3126b3b42bc31", size = 3218348, upload-time = "2026-03-17T09:33:51.48Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/3d/90948d0d04740bd6e3117894d7d85948ce5dbd321fd2a5d474f4742bb0a5/injectq-0.3.2-py3-none-any.whl", hash = "sha256:f569aa24dfdf16021bbc2f6e094871b92797d6a70a344f0469f4aac4908e0c43", size = 66521, upload-time = "2025-09-15T02:31:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/a6/cbfd571bd34671f66e88da411abb4a6384fcbd60831a28909f5028513bf5/injectq-0.4.1-py3-none-any.whl", hash = "sha256:6fd6dc56e424ba1a4d5f2595c4cd71c33846a7c13d04ac18e5de4f5bfb45c79c", size = 74023, upload-time = "2026-03-17T09:33:47.608Z" }, ] [[package]] @@ -876,6 +892,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/4d/a1a283987b6e478f5e1c89f1721b54b546a7375c76b77578e7d770861ea3/lib-4.0.0-py3-none-any.whl", hash = "sha256:d0a9e03b27d2b94c403f43c1af39366500032d587dcd45ffdf707131cbdf58c6", size = 4047, upload-time = "2021-12-14T03:13:28.763Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + [[package]] name = "markdown" version = "3.9" @@ -1178,6 +1274,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.4.0" @@ -1366,6 +1531,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] +[[package]] +name = "pymupdf" +version = "1.27.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/32/f6b645c51d79a188a4844140c5dabca7b487ad56c4be69c4bc782d0d11a9/pymupdf-1.27.2.2.tar.gz", hash = "sha256:ea8fdc3ab6671ca98f629d5ec3032d662c8cf1796b146996b7ad306ac7ed3335", size = 85354380, upload-time = "2026-03-20T09:47:58.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/88/d01992a50165e22dec057a1129826846c547feb4ba07f42720ac030ce438/pymupdf-1.27.2.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:800f43e60a6f01f644343c2213b8613db02eaf4f4ba235b417b3351fa99e01c0", size = 23987563, upload-time = "2026-03-19T12:35:42.989Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0e/9f526bc1d49d8082eff0d1547a69d541a0c5a052e71da625559efaba46a6/pymupdf-1.27.2.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e2e4299ef1ac0c9dff9be096cbd22783699673abecfa7c3f73173ae06421d73", size = 23263089, upload-time = "2026-03-20T09:44:16.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/be/984f0d6343935b5dd30afaed6be04fc753146bf55709e63ef28bf9ef7497/pymupdf-1.27.2.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5e3d54922db1c7da844f1208ac1db05704770988752311f81dd36694ae0a07b", size = 24318817, upload-time = "2026-03-20T09:44:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/22/8e/85e9d9f11dbf34036eb1df283805ef6b885f2005a56d6533bb58ab0b8a11/pymupdf-1.27.2.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:892698c9768457eb0991c102c96a856c0a7062539371df5e6bee0816f3ef498e", size = 24948135, upload-time = "2026-03-20T09:44:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/db/e6/386edb017e5b93f1ab0bf6653ae32f3dd8dfc834ed770212e10ca62f4af9/pymupdf-1.27.2.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b4bbfa6ef347fade678771a93f6364971c51a2cdc44cd2400dc4eeed1ddb4e6", size = 25169585, upload-time = "2026-03-20T09:45:05.393Z" }, + { url = "https://files.pythonhosted.org/packages/ba/fd/f1ebe24fcd31aaea8b85b3a7ac4c3fc96e20388be5466ace27c9a3c546d9/pymupdf-1.27.2.2-cp310-abi3-win32.whl", hash = "sha256:0b8e924433b7e0bd46be820899300259235997d5a747638471fb2762baa8ee30", size = 18008861, upload-time = "2026-03-20T09:45:21.353Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b6/2a9a8556000199bbf80a5915dcd15d550d1e5288894316445c54726aaf53/pymupdf-1.27.2.2-cp310-abi3-win_amd64.whl", hash = "sha256:09bb53f9486ccb5297030cbc2dbdae845ba1c3c5126e96eb2d16c4f118de0b5b", size = 19238032, upload-time = "2026-03-20T09:45:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c6/e3e11c42f09b9c34ec332c0f37b817671b59ef4001895b854f0494092105/pymupdf-1.27.2.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6cebfbbdfd219ebdebf4d8e3914624b2e3d3a844c43f4f76935822dd9b13cc12", size = 24985299, upload-time = "2026-03-20T09:45:53.26Z" }, +] + [[package]] name = "pyparsing" version = "3.2.4" @@ -1455,6 +1636,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -1634,6 +1828,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/78/3663b2342816564735190815cfaf705fb383c9b390b345ce8c99b68a99c8/snowflakekit-0.1.1-py3-none-any.whl", hash = "sha256:3352b9944943d700d8bea7850af7758a138ea36fae2fab0cef5fd3f158e210a9", size = 5039, upload-time = "2025-05-27T05:30:46.407Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + [[package]] name = "starlette" version = "0.40.0" @@ -1646,6 +1849,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/0f/64baf7a06492e8c12f5c4b49db286787a7255195df496fc21f5fd9eecffa/starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4", size = 73303, upload-time = "2024-10-15T06:52:32.486Z" }, ] +[[package]] +name = "textxtract" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/2d/f20dc77ab412680c8ff3b363bf2de72a977c6ff93c05b3e35ccc66f9050b/textxtract-0.2.3.tar.gz", hash = "sha256:8b9c5d763ce8f0178108c4bcd585b735162f61ebb6b1f95fbe85017cfa97a287", size = 23094, upload-time = "2025-10-30T07:31:46.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/59/30587fed9a72c8c2c842b29f3b74685c54d0ea9553a2d3cb3b4dbc6bc5ee/textxtract-0.2.3-py3-none-any.whl", hash = "sha256:862ccc5560cbfc1f58bdc7e7bad65b0fe950aff90462845db99173519ce81491", size = 28377, upload-time = "2025-10-30T07:31:45.013Z" }, +] + +[package.optional-dependencies] +docx = [ + { name = "python-docx" }, +] +html = [ + { name = "beautifulsoup4" }, + { name = "lxml" }, +] +md = [ + { name = "markdown" }, +] +pdf = [ + { name = "pymupdf" }, +] +xml = [ + { name = "lxml" }, +] + [[package]] name = "typer" version = "0.17.4" From e778684e1284906b7fc3ddc657b672532584108f Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 6 Apr 2026 22:55:13 +0600 Subject: [PATCH 5/5] refactor: update coverage configuration and test paths for consistency --- .coveragerc | 28 ++++++++------- pyproject.toml | 13 ++++--- tests/unit_tests/auth/test_jwt_auth.py | 50 +++++++++++++++----------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3f8dd99..700cff0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,23 +1,27 @@ [run] branch = True -source = src +source = agentflow_cli omit = - src/tests/* + */__init__.py + tests/* + */tests/* graph/* migrations/* - src/app/worker.py - src/app/main.py - src/app/loader.py - src/app/routers/checkpointer/* - src/app/routers/graph/* - src/app/routers/*/services/* # services often depend on external libs; still test parts - src/app/core/auth/* - src/app/core/config/setup_logs.py - src/app/core/config/sentry_config.py - src/app/core/config/worker_middleware.py + scripts/* + venv/* + .venv/* [report] exclude_lines = pragma: no cover if TYPE_CHECKING: if __name__ == .__main__. + @abc.abstractmethod + @abstractmethod + raise NotImplementedError +show_missing = True + +[paths] +source = + agentflow_cli + */site-packages/agentflow_cli diff --git a/pyproject.toml b/pyproject.toml index 7a53d07..95f827d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,7 +177,7 @@ env = [ "ENVIRONMENT=pytest", ] testpaths = [ - "agentflow-cli/src/tests", + "tests", ] pythonpath = [ ".", @@ -186,8 +186,8 @@ filterwarnings = [ "ignore::DeprecationWarning" ] addopts = [ - # Limit coverage collection to the local project package only - "--cov=agentflow-cli", + # Limit coverage collection to the local project package only + "--cov=agentflow_cli", "--cov-report=html", "--cov-report=term-missing", "--cov-report=xml", @@ -198,7 +198,7 @@ addopts = [ [tool.coverage.run] # Only measure the first-party project package -source = ["agentflow-cli"] +source = ["agentflow_cli"] branch = true omit = [ "*/__init__.py", # often trivial @@ -221,8 +221,8 @@ show_missing = true [tool.coverage.paths] source = [ - "agentflow-cli", - "*/site-packages/agentflow-cli", + "agentflow_cli", + "*/site-packages/agentflow_cli", ] [tool.pytest-env] @@ -246,4 +246,3 @@ dev = [ "markdown-it-py==3.0.0", "requests==2.32.3", ] - diff --git a/tests/unit_tests/auth/test_jwt_auth.py b/tests/unit_tests/auth/test_jwt_auth.py index e48a384..23987af 100644 --- a/tests/unit_tests/auth/test_jwt_auth.py +++ b/tests/unit_tests/auth/test_jwt_auth.py @@ -8,7 +8,7 @@ - Invalid/malformed tokens - Valid tokens without user_id - Valid tokens with user_id (successful auth) -- Bearer prefix handling +- Bearer scheme handling - WWW-Authenticate header setting """ @@ -22,6 +22,7 @@ from fastapi.security import HTTPAuthorizationCredentials from agentflow_cli.src.app.core.auth.jwt_auth import JwtAuth +from agentflow_cli.src.app.core.config.settings import get_settings from agentflow_cli.src.app.core.exceptions.user_exception import UserAccountError @@ -30,6 +31,14 @@ TEST_ALGORITHM = "HS256" +@pytest.fixture(autouse=True) +def clear_settings_cache(): + """Keep env-patched settings isolated between JWT auth tests.""" + get_settings.cache_clear() + yield + get_settings.cache_clear() + + class TestJwtAuth: """Test suite for JwtAuth.authenticate method.""" @@ -74,9 +83,13 @@ def create_token( """Helper method to create a JWT token.""" return jwt.encode(payload, secret, algorithm=algorithm) - def create_credentials(self, token: str) -> HTTPAuthorizationCredentials: + def create_credentials( + self, + token: str, + scheme: str = "Bearer", + ) -> HTTPAuthorizationCredentials: """Helper method to create HTTPAuthorizationCredentials.""" - return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token) + return HTTPAuthorizationCredentials(scheme=scheme, credentials=token) # ========================================================================= # Test: Null credentials @@ -116,12 +129,12 @@ def test_authenticate_missing_jwt_secret_key_raises_error( assert exc_info.value.error_code == "JWT_SETTINGS_NOT_CONFIGURED" assert "JWT settings are not configured" in exc_info.value.message - def test_authenticate_missing_jwt_algorithm_raises_error( + def test_authenticate_uses_default_jwt_algorithm_when_env_var_missing( self, jwt_auth: JwtAuth, mock_response: Response, ): - """Test that missing JWT_ALGORITHM raises UserAccountError.""" + """Test that missing JWT_ALGORITHM falls back to the Settings default.""" with patch.dict( os.environ, {"JWT_SECRET_KEY": TEST_SECRET_KEY}, @@ -132,8 +145,8 @@ def test_authenticate_missing_jwt_algorithm_raises_error( with pytest.raises(UserAccountError) as exc_info: jwt_auth.authenticate(None, mock_response, credentials) - assert exc_info.value.error_code == "JWT_SETTINGS_NOT_CONFIGURED" - assert "JWT settings are not configured" in exc_info.value.message + assert exc_info.value.error_code == "INVALID_TOKEN" + assert "Invalid token" in exc_info.value.message def test_authenticate_missing_both_jwt_settings_raises_error( self, @@ -322,53 +335,50 @@ def test_authenticate_returns_all_custom_claims( assert result["organization_id"] == "org-789" # ========================================================================= - # Test: Bearer prefix handling + # Test: Bearer scheme handling # ========================================================================= - def test_authenticate_strips_bearer_prefix_lowercase( + def test_authenticate_accepts_lowercase_bearer_scheme( self, jwt_auth: JwtAuth, mock_response: Response, jwt_env_vars, valid_token_payload: dict, ): - """Test that 'bearer ' prefix (lowercase) is stripped from token.""" + """Test that lowercase bearer scheme works when token is separate.""" actual_token = self.create_token(valid_token_payload) - token_with_prefix = f"bearer {actual_token}" - credentials = self.create_credentials(token_with_prefix) + credentials = self.create_credentials(actual_token, scheme="bearer") result = jwt_auth.authenticate(None, mock_response, credentials) assert result is not None assert result["user_id"] == "user-123" - def test_authenticate_strips_bearer_prefix_uppercase( + def test_authenticate_accepts_titlecase_bearer_scheme( self, jwt_auth: JwtAuth, mock_response: Response, jwt_env_vars, valid_token_payload: dict, ): - """Test that 'Bearer ' prefix (capitalized) is stripped from token.""" + """Test that titlecase Bearer scheme works when token is separate.""" actual_token = self.create_token(valid_token_payload) - token_with_prefix = f"Bearer {actual_token}" - credentials = self.create_credentials(token_with_prefix) + credentials = self.create_credentials(actual_token, scheme="Bearer") result = jwt_auth.authenticate(None, mock_response, credentials) assert result is not None assert result["user_id"] == "user-123" - def test_authenticate_strips_bearer_prefix_mixed_case( + def test_authenticate_accepts_uppercase_bearer_scheme( self, jwt_auth: JwtAuth, mock_response: Response, jwt_env_vars, valid_token_payload: dict, ): - """Test that 'BEARER ' prefix (mixed case) is stripped from token.""" + """Test that uppercase bearer scheme works when token is separate.""" actual_token = self.create_token(valid_token_payload) - token_with_prefix = f"BEARER {actual_token}" - credentials = self.create_credentials(token_with_prefix) + credentials = self.create_credentials(actual_token, scheme="BEARER") result = jwt_auth.authenticate(None, mock_response, credentials)