From 5e41d01ea0bcef728349d1fb5532387d7db03275 Mon Sep 17 00:00:00 2001 From: Rico Furtado Date: Fri, 22 May 2026 02:02:38 -0400 Subject: [PATCH 01/14] fix: Ensure SUCCESS status requires fetchable result in DoclingPollingService --- src/services/docling_polling_service.py | 26 ++++++++++++++++++++-- tests/unit/test_docling_polling_service.py | 23 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/services/docling_polling_service.py b/src/services/docling_polling_service.py index a8cc83a07..3ecc2d19a 100644 --- a/src/services/docling_polling_service.py +++ b/src/services/docling_polling_service.py @@ -53,7 +53,12 @@ async def poll_until_ready( transient_retry_budget: int = 5, ) -> DoclingPollResult: """Loop on Docling status until terminal or until max_seconds elapses. - + + A SUCCESS status is treated as ready only after the result endpoint + returns a payload with usable ``document.json_content``. This prevents + handing Langflow a task that Docling accepted but failed to convert + into a consumable document. + Transient errors (network, 5xx, NOT_FOUND seen briefly before the task is registered server-side) are absorbed up to ``transient_retry_budget`` before being surfaced as failures. The interval grows by @@ -81,8 +86,25 @@ async def poll_until_ready( elapsed = time.monotonic() - start if snapshot.state == DoclingTaskState.SUCCESS: + try: + await self.docling_service.fetch_task_result(task_id) + except Exception as e: + detail = f"Docling result unavailable after SUCCESS status: {str(e)}" + logger.warning( + "Docling task reached SUCCESS but result fetch failed", + task_id=task_id, + detail=detail, + elapsed_seconds=round(elapsed, 2), + ) + return DoclingPollResult( + outcome=PollOutcome.FAILED, + detail=detail, + last_snapshot=snapshot, + elapsed_seconds=elapsed, + ) + logger.debug( - "Docling task reached SUCCESS", + "Docling task reached SUCCESS and result is available", task_id=task_id, elapsed_seconds=round(elapsed, 2), ) diff --git a/tests/unit/test_docling_polling_service.py b/tests/unit/test_docling_polling_service.py index 803e01bb8..81b65d37f 100644 --- a/tests/unit/test_docling_polling_service.py +++ b/tests/unit/test_docling_polling_service.py @@ -42,14 +42,15 @@ def no_sleep(): @pytest.mark.asyncio async def test_returns_success_immediately_when_already_done(polling_service, mock_docling_service): mock_docling_service.check_task_status.return_value = _snap(DoclingTaskState.SUCCESS) - + mock_docling_service.fetch_task_result.return_value = {"body": "ok"} + result = await polling_service.poll_until_ready( task_id="t1", poll_interval=1.0, max_seconds=10.0 ) assert result.outcome == PollOutcome.SUCCESS assert mock_docling_service.check_task_status.call_count == 1 - + mock_docling_service.fetch_task_result.assert_awaited_once_with("t1") @pytest.mark.asyncio async def test_loops_through_processing_then_success( @@ -62,6 +63,8 @@ async def test_loops_through_processing_then_success( _snap(DoclingTaskState.SUCCESS), ] + mock_docling_service.fetch_task_result.return_value = {"body": "ok"} + result = await polling_service.poll_until_ready( task_id="t1", poll_interval=1.0, max_seconds=60.0 ) @@ -69,7 +72,23 @@ async def test_loops_through_processing_then_success( assert result.outcome == PollOutcome.SUCCESS assert mock_docling_service.check_task_status.call_count == 4 assert no_sleep.call_count == 3 + mock_docling_service.fetch_task_result.assert_awaited_once_with("t1") + +@pytest.mark.asyncio +async def test_success_status_requires_fetchable_result(polling_service, mock_docling_service): + mock_docling_service.check_task_status.return_value = _snap(DoclingTaskState.SUCCESS) + mock_docling_service.fetch_task_result.side_effect = RuntimeError( + "missing document.json_content" + ) + + result = await polling_service.poll_until_ready( + task_id="t1", poll_interval=1.0, max_seconds=10.0 + ) + + assert result.outcome == PollOutcome.FAILED + assert "missing document.json_content" in (result.detail or "") + mock_docling_service.fetch_task_result.assert_awaited_once_with("t1") @pytest.mark.asyncio async def test_returns_failed_on_docling_failure(polling_service, mock_docling_service): From d96c810b9681ec28c013874b118da017b6c18fc4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 06:11:18 +0000 Subject: [PATCH 02/14] style: ruff autofix (auto) --- src/services/docling_polling_service.py | 12 ++++++------ tests/unit/test_docling_polling_service.py | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/services/docling_polling_service.py b/src/services/docling_polling_service.py index 3ecc2d19a..3cc6f52da 100644 --- a/src/services/docling_polling_service.py +++ b/src/services/docling_polling_service.py @@ -32,8 +32,8 @@ class PollOutcome(str, Enum): @dataclass class DoclingPollResult: outcome: PollOutcome - detail: Optional[str] = None - last_snapshot: Optional[DoclingStatusSnapshot] = None + detail: str | None = None + last_snapshot: DoclingStatusSnapshot | None = None elapsed_seconds: float = 0.0 @@ -53,12 +53,12 @@ async def poll_until_ready( transient_retry_budget: int = 5, ) -> DoclingPollResult: """Loop on Docling status until terminal or until max_seconds elapses. - + A SUCCESS status is treated as ready only after the result endpoint returns a payload with usable ``document.json_content``. This prevents handing Langflow a task that Docling accepted but failed to convert into a consumable document. - + Transient errors (network, 5xx, NOT_FOUND seen briefly before the task is registered server-side) are absorbed up to ``transient_retry_budget`` before being surfaced as failures. The interval grows by @@ -74,7 +74,7 @@ async def poll_until_ready( deadline = start + max_seconds interval = poll_interval consecutive_not_found = 0 - last_snapshot: Optional[DoclingStatusSnapshot] = None + last_snapshot: DoclingStatusSnapshot | None = None logger.debug("Starting Docling polling", task_id=task_id) @@ -102,7 +102,7 @@ async def poll_until_ready( last_snapshot=snapshot, elapsed_seconds=elapsed, ) - + logger.debug( "Docling task reached SUCCESS and result is available", task_id=task_id, diff --git a/tests/unit/test_docling_polling_service.py b/tests/unit/test_docling_polling_service.py index 81b65d37f..db714c9c1 100644 --- a/tests/unit/test_docling_polling_service.py +++ b/tests/unit/test_docling_polling_service.py @@ -6,9 +6,10 @@ slots are reserved for chunking / embedding / indexing only. """ -import pytest from unittest.mock import AsyncMock, patch +import pytest + from services.docling_polling_service import ( DoclingPollingService, PollOutcome, @@ -43,7 +44,7 @@ def no_sleep(): async def test_returns_success_immediately_when_already_done(polling_service, mock_docling_service): mock_docling_service.check_task_status.return_value = _snap(DoclingTaskState.SUCCESS) mock_docling_service.fetch_task_result.return_value = {"body": "ok"} - + result = await polling_service.poll_until_ready( task_id="t1", poll_interval=1.0, max_seconds=10.0 ) @@ -52,6 +53,7 @@ async def test_returns_success_immediately_when_already_done(polling_service, mo assert mock_docling_service.check_task_status.call_count == 1 mock_docling_service.fetch_task_result.assert_awaited_once_with("t1") + @pytest.mark.asyncio async def test_loops_through_processing_then_success( polling_service, mock_docling_service, no_sleep @@ -90,6 +92,7 @@ async def test_success_status_requires_fetchable_result(polling_service, mock_do assert "missing document.json_content" in (result.detail or "") mock_docling_service.fetch_task_result.assert_awaited_once_with("t1") + @pytest.mark.asyncio async def test_returns_failed_on_docling_failure(polling_service, mock_docling_service): mock_docling_service.check_task_status.return_value = _snap( From 1863a81a14538b95256ba7ad2a19cf6ae9ddb018 Mon Sep 17 00:00:00 2001 From: Rico Furtado Date: Sat, 23 May 2026 15:45:53 -0400 Subject: [PATCH 03/14] fix: Catch specific DoclingServeError when fetching task result after SUCCESS status --- src/services/docling_polling_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/docling_polling_service.py b/src/services/docling_polling_service.py index 3cc6f52da..d2cea36f6 100644 --- a/src/services/docling_polling_service.py +++ b/src/services/docling_polling_service.py @@ -13,6 +13,7 @@ from typing import Optional from services.docling_service import ( + DoclingServeError, DoclingService, DoclingStatusSnapshot, DoclingTaskState, @@ -88,7 +89,7 @@ async def poll_until_ready( if snapshot.state == DoclingTaskState.SUCCESS: try: await self.docling_service.fetch_task_result(task_id) - except Exception as e: + except DoclingServeError as e: detail = f"Docling result unavailable after SUCCESS status: {str(e)}" logger.warning( "Docling task reached SUCCESS but result fetch failed", From 6da88adf48fd959683e20433954da084c1f9377f Mon Sep 17 00:00:00 2001 From: Wallgau <46035189+Wallgau@users.noreply.github.com> Date: Fri, 22 May 2026 11:30:24 -0400 Subject: [PATCH 04/14] feat: update style for oss of the failed task in the task panel (#1647) * update style for oss of the failed task in the task panel * keep logic on click, remove unecessary useeffect * fix padding * wip implementing Saas style * utils to reshape error until backend provide info we need * utils to reshape error until backend provide info we need * utils to reshape error until backend provide info we need and fixinf fallbacks of isTotalFailure * utils to reshape error until backend provide into * have Saas style for failed and complete labelstatus and width and border * few style adjustment to follow codebase pattern * adjust succeed and partially succeed case * adding comment for TODO implementation or more clarity * remove carbon icon package and replace carbon icon * add incident-reporter-icon --------- Co-authored-by: Olfa Maslah --- frontend/app/globals.css | 17 ++ .../icons/incident-reporter-icon.tsx | 24 ++ .../components/task-collapsible-section.tsx | 6 +- frontend/components/task-error-content.tsx | 264 ++++++++++++------ .../components/task-notification-menu.tsx | 205 +++++++++----- frontend/components/tasks_details.tsx | 16 +- frontend/contexts/task-context.tsx | 77 +++-- frontend/lib/task-error-display.ts | 93 ++++++ frontend/lib/task-utils.ts | 21 ++ frontend/tailwind.config.ts | 19 ++ .../tests/core/tasks-unified-panel.spec.ts | 49 ++-- 11 files changed, 578 insertions(+), 213 deletions(-) create mode 100644 frontend/components/icons/incident-reporter-icon.tsx create mode 100644 frontend/lib/task-error-display.ts diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 9fb849a30..fad853f2f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -63,6 +63,7 @@ --badge-foreground: 0 0% 100%; /* #FFFFFF */ --radius: 0.5rem; + --radius-mmd: 13px; --sidebar-background: 0 0% 98%; @@ -88,6 +89,7 @@ --failure-message: hsl(var(--foreground)); --failure-scroll: #ff6464; --failure-muted: #a66262; + --failure-component-cause: 0 0% 32%; /* #525252 */ } .dark { @@ -156,6 +158,7 @@ --failure-message: #ffc9c9cc; --failure-scroll: #ff6464; --failure-muted: #a66262; + --failure-component-cause: 0 0% 32%; /* #525252 */ } /* IBM Light */ @@ -238,6 +241,13 @@ --badge: 0 0% 22.4%; /* #393939 */ --badge-foreground: 0 0% 96%; /* #F4F4F4 */ + + --task-status-failed-bg: 357 73% 37%; /* #a2191f */ + --task-status-failed-fg: 357 100% 93%; /* #ffd7d9 */ + --task-status-partial-bg: 26 90% 37%; /* #8a3800 */ + --task-status-partial-fg: 46 100% 90%; /* #fff1c8 */ + --task-status-complete-bg: 137 73% 22%; /* #0e6027 */ + --task-status-complete-fg: 135 59% 80%; /* #a7f0ba */ } /* IBM: match OSS switch colors — black track when checked, white thumb always */ @@ -329,6 +339,13 @@ --badge: 0 0% 32%; /* #525252 */ --badge-foreground: 0 0% 96%; /* #F4F4F4 */ + + --task-status-failed-bg: 357 73% 37%; /* #a2191f */ + --task-status-failed-fg: 357 100% 93%; /* #ffd7d9 */ + --task-status-partial-bg: 22 78% 32%; /* warm orange */ + --task-status-partial-fg: 46 97% 65%; /* #f1c21b */ + --task-status-complete-bg: 137 73% 22%; /* #0e6027 */ + --task-status-complete-fg: 135 59% 80%; /* #a7f0ba */ } /* IBM Settings: section titles (productive heading-04); beats CardTitle utilities */ diff --git a/frontend/components/icons/incident-reporter-icon.tsx b/frontend/components/icons/incident-reporter-icon.tsx new file mode 100644 index 000000000..757170118 --- /dev/null +++ b/frontend/components/icons/incident-reporter-icon.tsx @@ -0,0 +1,24 @@ +import { cn } from "@/lib/utils"; + +/** + * Carbon "incident-reporter" glyph (clipboard + alert badge). + * Inline SVG avoids `@carbon/icons-react` for a single icon. + * @see https://github.com/carbon-design-system/carbon/tree/main/packages/icons + */ +export function IncidentReporterIcon({ + className, + ...props +}: React.SVGProps) { + return ( + + + + ); +} diff --git a/frontend/components/task-collapsible-section.tsx b/frontend/components/task-collapsible-section.tsx index e7c28068c..fc0f081f1 100644 --- a/frontend/components/task-collapsible-section.tsx +++ b/frontend/components/task-collapsible-section.tsx @@ -33,14 +33,14 @@ export function TaskCollapsibleSection({ >

{title} - + {items.length}

{isOpen ? ( - + ) : ( - + )} diff --git a/frontend/components/task-error-content.tsx b/frontend/components/task-error-content.tsx index 34266bea2..fbd291403 100644 --- a/frontend/components/task-error-content.tsx +++ b/frontend/components/task-error-content.tsx @@ -1,16 +1,26 @@ "use client"; -import { ArrowDown, ArrowUp, ChevronDown, XCircle } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { AlertCircle, ChevronDown, Flag, XCircle } from "lucide-react"; +import { useMemo, useState } from "react"; +import { IncidentReporterIcon } from "@/components/icons/incident-reporter-icon"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { useIsCloudBrand } from "@/contexts/brand-context"; import { type Task } from "@/contexts/task-context"; -import { getFailedFileEntries } from "@/lib/task-utils"; +import { displayFileTaskError } from "@/lib/task-error-display"; +import { + getFailedFileCount, + getFailedFileEntries, + getSuccessfulFileCount, + isCompletedTotalFailure, + isTerminalFailedTask, +} from "@/lib/task-utils"; import { formatTaskTimestamp, parseTimestamp } from "@/lib/time-utils"; +import { cn } from "@/lib/utils"; interface TaskErrorContentProps { task: Task; @@ -18,7 +28,6 @@ interface TaskErrorContentProps { nowMs?: number; showHeader?: boolean; defaultExpanded?: boolean; - expandTrigger?: number; } export function TaskErrorContent({ @@ -27,126 +36,199 @@ export function TaskErrorContent({ nowMs = Date.now(), showHeader = true, defaultExpanded = false, - expandTrigger = 0, }: TaskErrorContentProps) { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); - useEffect(() => { - if (defaultExpanded) { - setIsExpanded(true); - } - }, [defaultExpanded, expandTrigger]); + const isCloudBrand = useIsCloudBrand(); + const [accordionValue, setAccordionValue] = useState( + defaultExpanded ? "failed-files" : "", + ); + const isExpanded = accordionValue === "failed-files"; + const failedEntries = useMemo(() => getFailedFileEntries(task), [task]); - const failedCount = task.failed_files ?? failedEntries.length; - const successCount = task.successful_files ?? 0; + const failedCount = getFailedFileCount(task); + const successCount = getSuccessfulFileCount(task); const timestamp = parseTimestamp(task.created_at) ?? parseTimestamp(task.updated_at); - const statusLabel = "INCOMPLETE"; - const statusPillClassName = - "text-destructive border-failure-pill bg-failure-soft"; + const isFailedStatus = + isTerminalFailedTask(task) || isCompletedTotalFailure(task); + const statusLabel = isFailedStatus ? "Failed" : "Complete"; + // Pill colors: failed (red) vs partial success (amber/orange), each with IBM tokens or OSS borders. + const statusPillClassName = cn( + "shrink-0 rounded-full px-2 py-1 text-xs", + isFailedStatus + ? isCloudBrand + ? "border-0 bg-task-status-failed text-task-status-failed-foreground" + : "border border-failure-pill bg-failure-soft text-destructive" + : isCloudBrand + ? "border-0 bg-task-status-partial text-task-status-partial-foreground" + : "border border-brand-amber-30 bg-brand-amber-10 text-brand-amber", + ); if (failedCount <= 0 && failedEntries.length === 0) { return null; } + const ossIconColumn = showHeader && !isCloudBrand; + + const accordionTrigger = ( +
+
+ + {successCount} success · {failedCount} failed + + +
+ +
+ ); + return (
- {showHeader && ( - <> -
-
- -

- Task {task.task_id.slice(0, 8)}... +

+ {showHeader && ( +
+ {ossIconColumn && + (isFailedStatus ? ( + + ) : ( + + ))} +
+
+

+ Task {task.task_id.slice(0, 8)}... +

+ {!isExpanded && ( +

{statusLabel}

+ )} +
+

+ {formatTaskTimestamp(timestamp, mode, nowMs)}

- {!isExpanded && ( -

- {statusLabel} -

- )}
+ )} -
-

- {formatTaskTimestamp(timestamp, mode, nowMs)} -

-
- - )} - - setIsExpanded(Boolean(value))} - > - - -
- - {successCount} success, - - - {failedCount} failed - - -
-
- -
-

- Failure Log{" "} - - ({failedCount} of {failedCount} pending) - -

-
+ + setAccordionValue(value === "failed-files" ? "failed-files" : "") + } + > + + + {ossIconColumn ? ( +
+
+
{accordionTrigger}
+
+ ) : ( + accordionTrigger + )} + + +
{failedEntries.map(([filePath, fileInfo], index) => { const fileName = fileInfo.filename || filePath.split("/").pop() || filePath; - const message = + const rawError = typeof fileInfo.error === "string" && fileInfo.error.trim() ? fileInfo.error.trim() - : task.error || "Unknown error"; + : task.error; + const { line, componentCause } = + displayFileTaskError(rawError); return (
-

- {">"} {fileName} +

+ {fileName}

-

- {message} +

+ {line}

+ {componentCause ? ( +
+ + + {componentCause} + +
+ ) : null}
); })}
- {failedCount > 1 && ( -
-
- - -
- scroll · {failedCount} errors -
- )} -
- -
-
+ + + +
); } diff --git a/frontend/components/task-notification-menu.tsx b/frontend/components/task-notification-menu.tsx index d88005bdb..f2a2ea4fc 100644 --- a/frontend/components/task-notification-menu.tsx +++ b/frontend/components/task-notification-menu.tsx @@ -22,11 +22,18 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { useIsCloudBrand } from "@/contexts/brand-context"; import { Task, useTask } from "@/contexts/task-context"; -import { hasFailedFileEntries, isTerminalFailedTask } from "@/lib/task-utils"; +import { + hasFailedFileEntries, + isCompletedTotalFailure, + isTerminalFailedTask, +} from "@/lib/task-utils"; import { parseTimestampMs } from "@/lib/time-utils"; +import { cn } from "@/lib/utils"; export function TaskNotificationMenu() { + const isCloudBrand = useIsCloudBrand(); const { tasks, isFetching, @@ -81,11 +88,6 @@ export function TaskNotificationMenu() { }), [tasks], ); - const mostRecentFailureTaskId = - terminalTasks.find( - (task) => isTerminalFailedTask(task) || hasFailedFileEntries(task), - )?.task_id ?? null; - // Ensure selected task is visible in the past tasks section. useEffect(() => { if (!selectedTaskId) return; @@ -103,16 +105,23 @@ export function TaskNotificationMenu() { // Don't render if menu is closed if (!isMenuOpen) return null; - const getTaskIcon = (status: Task["status"], hasFailedFiles = false) => { + const getTaskIcon = ( + status: Task["status"], + hasFailedFiles = false, + isTotalFailure = false, + ) => { switch (status) { case "completed": if (hasFailedFiles) { + if (isTotalFailure) { + return ; + } return ; } return ; case "failed": case "error": - return ; + return ; case "pending": return ; case "running": @@ -123,25 +132,61 @@ export function TaskNotificationMenu() { } }; - const getStatusBadge = (status: Task["status"], hasFailedFiles = false) => { + const pastTaskRowClass = cn( + "w-full py-mmd px-4 transition-colors hover:bg-muted/60", + isCloudBrand ? "border-t border-muted" : "rounded-mmd border border-muted", + ); + + const statusBadgeBase = "shrink-0 rounded-full px-2 py-1 text-xs font-normal"; + + const getStatusBadge = ( + status: Task["status"], + hasFailedFiles = false, + isTotalFailure = false, + ) => { switch (status) { case "completed": + if (hasFailedFiles && isTotalFailure) { + return ( + + FAILED + + ); + } if (hasFailedFiles) { return ( - COMPLETED + Complete ); } return ( - COMPLETED + Complete ); case "failed": @@ -149,16 +194,24 @@ export function TaskNotificationMenu() { return ( - INCOMPLETE + FAILED ); case "pending": return ( Pending @@ -168,7 +221,10 @@ export function TaskNotificationMenu() { return ( Processing @@ -177,7 +233,10 @@ export function TaskNotificationMenu() { return ( Unknown @@ -266,7 +325,9 @@ export function TaskNotificationMenu() { }; return ( -
+
{/* Active Tasks */} {activeTasks.length > 0 && ( -
+

Active Tasks

{activeTasks.map((task) => { const progress = formatTaskProgress(task); + const hasFailedFiles = hasFailedFileEntries(task); const showCancel = task.status === "pending" || task.status === "running" || task.status === "processing"; + const showTaskIcon = + !isCloudBrand || + task.status !== "completed" || + hasFailedFiles; return ( - +
- {getTaskIcon(task.status)} + {showTaskIcon && + getTaskIcon( + task.status, + hasFailedFiles, + isCompletedTotalFailure(task), + )} Task {task.task_id.substring(0, 8)}...
@@ -311,7 +382,7 @@ export function TaskNotificationMenu() {
{(progress || showCancel) && ( - + {progress && (
@@ -363,18 +434,18 @@ export function TaskNotificationMenu() {
)} - {hasFailedFileEntries(task) && ( + {hasFailedFiles && (
)} @@ -395,35 +466,42 @@ export function TaskNotificationMenu() { onToggle={() => setIsPastOpen((prev) => !prev)} emptyText="No past tasks." containerClassName="" - contentClassName="transition-all duration-200" + contentClassName={cn( + "flex flex-col transition-all duration-200", + isCloudBrand + ? "p-0 [&>*:last-child]:border-b [&>*:last-child]:border-muted" + : "gap-2 p-4 pt-2", + )} renderItem={(task) => { const progress = formatTaskProgress(task); const hasFailedFiles = hasFailedFileEntries(task); - const shouldExpandDetails = - selectedTaskId === task.task_id || - (!selectedTaskId && task.task_id === mostRecentFailureTaskId); + const isTotalFailure = isCompletedTotalFailure(task); + const shouldExpandDetails = selectedTaskId === task.task_id; - if (isTerminalFailedTask(task)) { + // Same full card as total failure; partial only differs inside (Complete pill / amber icon). + if ( + isTerminalFailedTask(task) || + isTotalFailure || + hasFailedFiles + ) { return ( ); } return ( -
+
- {getTaskIcon(task.status, hasFailedFiles)} + {!isCloudBrand && getTaskIcon(task.status)}
Task {task.task_id.substring(0, 8)}... @@ -436,37 +514,20 @@ export function TaskNotificationMenu() { )}
- {task.status === "completed" && - progress?.detailed && - !hasFailedFiles && ( -
- {progress.detailed.successful} success,{" "} - {progress.detailed.failed} failed - {(progress.detailed.running || 0) > 0 && ( - - , {progress.detailed.running} running - - )} -
- )} + {task.status === "completed" && progress?.detailed && ( +
+ {progress.detailed.successful} success,{" "} + {progress.detailed.failed} failed + {(progress.detailed.running || 0) > 0 && ( + , {progress.detailed.running} running + )} +
+ )}
- {getStatusBadge(task.status, hasFailedFiles)} + {getStatusBadge(task.status)}
- {hasFailedFiles && ( -
- -
- )}
); }} diff --git a/frontend/components/tasks_details.tsx b/frontend/components/tasks_details.tsx index 8a9d2f3ae..f6f923453 100644 --- a/frontend/components/tasks_details.tsx +++ b/frontend/components/tasks_details.tsx @@ -2,15 +2,18 @@ import { useEffect, useMemo, useState } from "react"; import { TaskCollapsibleSection } from "@/components/task-collapsible-section"; import { TaskErrorContent } from "@/components/task-error-content"; import { TaskPanelHeader } from "@/components/task-panel-header"; +import { useIsCloudBrand } from "@/contexts/brand-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { type Task } from "@/contexts/task-context"; import { parseTimestampMs } from "@/lib/time-utils"; +import { cn } from "@/lib/utils"; interface FailedTasksInfoProps { failedTasks: Task[]; } export const FailedTasksInfo = ({ failedTasks }: FailedTasksInfoProps) => { + const isCloudBrand = useIsCloudBrand(); const [openSections, setOpenSections] = useState< Record<"recent" | "past", boolean> >({ @@ -76,7 +79,12 @@ export const FailedTasksInfo = ({ failedTasks }: FailedTasksInfoProps) => { ); return ( -
+
{failedTasks.length === 0 ? ( @@ -98,6 +106,12 @@ export const FailedTasksInfo = ({ failedTasks }: FailedTasksInfoProps) => { })) } emptyText={section.emptyText} + contentClassName={cn( + "flex flex-col", + isCloudBrand + ? "p-0 [&>*:last-child]:border-b [&>*:last-child]:border-muted" + : "gap-2 p-4", + )} renderItem={(task) => ( 0 && successfulFiles === 0; + + if (isTotalFailure) { + trackProcessFailure({ + processType: "Ingestion", + process: "Document Upload", + category: "Knowledge", + task_id: currentTask.task_id, + total_files: currentTask.total_files, + failed_files: failedFiles, + duration_seconds: currentTask.duration_seconds, + }); + } else { + trackProcessSuccess({ + processType: "Ingestion", + process: "Document Upload", + category: "Knowledge", + task_id: currentTask.task_id, + total_files: currentTask.total_files, + successful_files: successfulFiles, + failed_files: failedFiles, + duration_seconds: currentTask.duration_seconds, + }); + } let description = ""; if (failedFiles > 0) { @@ -356,17 +373,27 @@ export function TaskProvider({ children }: { children: React.ReactNode }) { } uploaded successfully`; } if (!isOnboardingActive) { - toast.success("Task completed", { - description, - action: { - label: "View", - onClick: () => { - selectTask(currentTask.task_id); - setIsMenuOpen(true); - setIsRecentTasksExpanded(true); - }, + const toastAction = { + label: "View", + onClick: () => { + selectTask(currentTask.task_id); + setIsMenuOpen(true); + setIsRecentTasksExpanded(true); }, - }); + }; + if (isTotalFailure) { + toast.error("Task failed", { + description: `${failedFiles} file${ + failedFiles !== 1 ? "s" : "" + } failed`, + action: toastAction, + }); + } else { + toast.success("Task completed", { + description, + action: toastAction, + }); + } } const completedHasFailures = hasFailedFileEntries(currentTask); diff --git a/frontend/lib/task-error-display.ts b/frontend/lib/task-error-display.ts new file mode 100644 index 000000000..782b40502 --- /dev/null +++ b/frontend/lib/task-error-display.ts @@ -0,0 +1,93 @@ +// Will update when backend is ready (error_summary, failing_step, component_cause). + +export const FILE_ERROR_MAX_LINE_LENGTH = 80; + +export type TaskErrorComponentCause = "OpenSearch" | "Docling" | "Langflow"; + +const COMPONENT_CAUSES: ReadonlyArray<{ + keyword: string; + label: TaskErrorComponentCause; +}> = [ + { keyword: "opensearch", label: "OpenSearch" }, + { keyword: "docling", label: "Docling" }, + { keyword: "langflow", label: "Langflow" }, +]; + +export interface FileTaskErrorDisplay { + line: string; + componentCause?: TaskErrorComponentCause; +} + +function normalizeErrorText(raw: string): string { + return raw.replace(/\s+/g, " ").trim(); +} + +function stripNoisePrefixes(text: string): string { + return text + .replace(/^Error running graph:\s*/i, "") + .replace(/^Error building Component [^:]+:\s*/i, "") + .trim(); +} + +function truncateLine( + text: string, + maxLength = FILE_ERROR_MAX_LINE_LENGTH, +): string { + if (text.length <= maxLength) { + return text; + } + return `${text.slice(0, maxLength - 1).trimEnd()}…`; +} + +/** Prefer a short clause from long, nested error strings. */ +function extractReadableLine(text: string): string { + const beforeCausedBy = text.split(/\s+caused by:/i)[0]?.trim() ?? text; + + if (beforeCausedBy.length <= FILE_ERROR_MAX_LINE_LENGTH) { + return beforeCausedBy; + } + + const colonParts = beforeCausedBy.split(":"); + const lastClause = colonParts[colonParts.length - 1]?.trim(); + if ( + lastClause && + lastClause.length >= 10 && + lastClause.length <= FILE_ERROR_MAX_LINE_LENGTH + ) { + return lastClause; + } + + return beforeCausedBy; +} + +export function detectComponentCause( + raw: string, +): TaskErrorComponentCause | undefined { + const lower = raw.toLowerCase(); + for (const { keyword, label } of COMPONENT_CAUSES) { + if (lower.includes(keyword)) { + return label; + } + } + return undefined; +} + +export function displayFileTaskError( + raw: string | undefined | null, +): FileTaskErrorDisplay { + if (!raw?.trim()) { + return { line: "Unknown error" }; + } + + const normalized = normalizeErrorText(raw); + const componentCause = detectComponentCause(normalized); + + let line = stripNoisePrefixes(normalized); + line = truncateLine(extractReadableLine(line)); + + if (!line) { + line = "Unknown error"; + } + + return componentCause ? { line, componentCause } : { line }; +} diff --git a/frontend/lib/task-utils.ts b/frontend/lib/task-utils.ts index 94b441fa9..8f22468d5 100644 --- a/frontend/lib/task-utils.ts +++ b/frontend/lib/task-utils.ts @@ -24,6 +24,27 @@ export function isCompletedWithFailures(task: Task): boolean { return task.status === "completed" && hasFailedFileEntries(task); } +export function getSuccessfulFileCount(task: Task): number { + if (typeof task.successful_files === "number") { + return task.successful_files; + } + return Object.values(task.files || {}).filter( + (fileInfo) => fileInfo?.status === "completed", + ).length; +} + +export function getFailedFileCount(task: Task): number { + if (typeof task.failed_files === "number") { + return task.failed_files; + } + return getFailedFileEntries(task).length; +} + +/** Completed task with failures and no successful files — treat as failed, not partial success. */ +export function isCompletedTotalFailure(task: Task): boolean { + return isCompletedWithFailures(task) && getSuccessfulFileCount(task) === 0; +} + export function isFailureLikeTask(task: Task): boolean { return isTerminalFailedTask(task) || isCompletedWithFailures(task); } diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index a74c8c8ab..cbed098bc 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -192,12 +192,31 @@ const config = { message: "var(--failure-message)", scroll: "var(--failure-scroll)", muted: "var(--failure-muted)", + "component-cause": "hsl(var(--failure-component-cause))", }, + "task-status": { + failed: { + DEFAULT: "hsl(var(--task-status-failed-bg))", + foreground: "hsl(var(--task-status-failed-fg))", + }, + partial: { + DEFAULT: "hsl(var(--task-status-partial-bg))", + foreground: "hsl(var(--task-status-partial-fg))", + }, + complete: { + DEFAULT: "hsl(var(--task-status-complete-bg))", + foreground: "hsl(var(--task-status-complete-fg))", + }, + }, + }, + spacing: { + mmd: "13px", }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", + mmd: "var(--radius-mmd)", }, fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], diff --git a/frontend/tests/core/tasks-unified-panel.spec.ts b/frontend/tests/core/tasks-unified-panel.spec.ts index 73f711a54..7bb57146a 100644 --- a/frontend/tests/core/tasks-unified-panel.spec.ts +++ b/frontend/tests/core/tasks-unified-panel.spec.ts @@ -77,13 +77,21 @@ const wireTasksState = async (page: Page, initialTasks: MockTask[]) => { }; }; -const expandFirstFailureAccordion = async (page: Page) => { - const failureLog = page.getByText("Failure Log").first(); - if (await failureLog.isVisible()) { +/** Matches accordion summary, e.g. "1 success · 2 failed". */ +const FAILURE_SUMMARY_BUTTON = /\d+\s*success\s*[·,.]\s*\d+\s*failed/i; + +const expandFirstFailureAccordion = async ( + page: Page, + expectedError?: string, +) => { + if ( + expectedError && + (await page.getByText(expectedError).first().isVisible()) + ) { return; } await page - .getByRole("button", { name: /\d+\s*success,\s*\d+\s*failed/i }) + .getByRole("button", { name: FAILURE_SUMMARY_BUTTON }) .first() .click(); }; @@ -110,18 +118,17 @@ const openTasksPanel = async (page: Page) => { }; const openPastTasksSection = async (page: Page) => { - const failureAccordionTrigger = page.getByRole("button", { - name: /\d+\s*success,\s*\d+\s*failed/i, - }); - if (await failureAccordionTrigger.count()) { - return; - } - const pastTasksToggle = page.getByRole("button", { name: /Past Tasks/i }); - if (await pastTasksToggle.count()) { + await expect(pastTasksToggle.first()).toBeVisible({ timeout: 15000 }); + + const failureSummary = page.getByRole("button", { + name: FAILURE_SUMMARY_BUTTON, + }); + if ((await failureSummary.count()) === 0) { await pastTasksToggle.first().click(); } - await expect(failureAccordionTrigger.first()).toBeVisible({ timeout: 15000 }); + + await expect(failureSummary.first()).toBeVisible({ timeout: 15000 }); }; test("completed task with failures keeps failure log in Tasks panel", async ({ @@ -178,8 +185,10 @@ test("completed task with failures keeps failure log in Tasks panel", async ({ ); await openTasksPanel(page); await openPastTasksSection(page); - await expandFirstFailureAccordion(page); - await expect(page.getByText("Failure Log")).toBeVisible(); + await expandFirstFailureAccordion( + page, + "Synthetic ingestion failure for test", + ); await expect( page.getByText("Synthetic ingestion failure for test"), ).toBeVisible(); @@ -228,8 +237,7 @@ test("completed task with failures requires View click to open tasks panel", asy ); await openTasksPanel(page); await openPastTasksSection(page); - await expandFirstFailureAccordion(page); - await expect(page.getByText("Failure Log")).toBeVisible(); + await expandFirstFailureAccordion(page, "Auto-open on partial success"); await expect(page.getByText("Auto-open on partial success")).toBeVisible(); }); @@ -274,8 +282,7 @@ test("new failed task auto-opens tasks panel", async ({ page }) => { ); await openTasksPanel(page); await openPastTasksSection(page); - await expandFirstFailureAccordion(page); - await expect(page.getByText("Failure Log")).toBeVisible(); + await expandFirstFailureAccordion(page, "Auto-open on failed task"); await expect(page.getByText("Auto-open on failed task")).toBeVisible(); }); @@ -325,6 +332,6 @@ test("unified panel shows all completed tasks in a single past tasks section", a await openTasksPanel(page); await expect(page.getByText("Task task-new...")).toBeVisible(); await expect(page.getByText("Task task-old...")).toBeVisible(); - // The most recent failure task auto-expands, hiding its INCOMPLETE pill; the older one stays collapsed. - await expect(page.getByText("INCOMPLETE")).toHaveCount(1); + // Failure summaries stay collapsed by default; both tasks show the Failed pill. + await expect(page.getByText("Failed", { exact: true })).toHaveCount(2); }); From 1a44ec299973959411040a6e238b2bd704020c59 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 22 May 2026 11:39:56 -0500 Subject: [PATCH 05/14] fix: Encode IBM API key as Basic auth header (#1664) * Encode IBM API key as Basic auth header Add base64 encoding for the IBM auth path: import base64, construct a Basic auth token from X-Username and X-Api-Key (username:apikey), and store it in user.jwt_token and user.opensearch_credentials. Also set request.state.user before attaching the DB user ID so downstream code can access the created user object. * style: ruff autofix (auto) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/dependencies.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dependencies.py b/src/dependencies.py index 5a12c0f09..bb3c0e776 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -626,6 +626,9 @@ async def get_api_key_user_async( Raises HTTP 401 if no valid credentials are provided. """ + import base64 + + # IBM auth path: X-Username + X-Api-Key forwarded by the MCP via the SDK from config.settings import IBM_AUTH_ENABLED # IBM auth path: X-Username + X-Api-Key forwarded by the MCP via the SDK @@ -633,16 +636,21 @@ async def get_api_key_user_async( ibm_username = request.headers.get("X-Username") ibm_api_key = request.headers.get("X-Api-Key") if ibm_username and ibm_api_key: + # check if ibm api key is base 64 encoded + userpass = f"{ibm_username}:{ibm_api_key}" + ibm_api_key_b64 = base64.b64encode(userpass.encode("utf-8")).decode("utf-8") + user = User( user_id=ibm_username, email=ibm_username, name=ibm_username, picture=None, provider="ibm_ams", - jwt_token=None, + jwt_token=f"Basic {ibm_api_key_b64}", opensearch_username=ibm_username, - opensearch_credentials=ibm_api_key, + opensearch_credentials=ibm_api_key_b64, ) + request.state.user = user return await _attach_db_user_id(request, user) # API key path From 3bd3fe30aa8cbc9de2e9242c4e3ec8bbca21f730 Mon Sep 17 00:00:00 2001 From: ming Date: Fri, 22 May 2026 14:07:06 -0400 Subject: [PATCH 06/14] fix: restart deployment if env changes (#1665) * restart deployment if env changes * unit test * lint --- .../internal/controller/openrag_controller.go | 44 +++-- .../controller/openrag_controller_test.go | 186 ++++++++++++++++++ 2 files changed, 217 insertions(+), 13 deletions(-) diff --git a/kubernetes/operator/internal/controller/openrag_controller.go b/kubernetes/operator/internal/controller/openrag_controller.go index be6381029..1e4113174 100644 --- a/kubernetes/operator/internal/controller/openrag_controller.go +++ b/kubernetes/operator/internal/controller/openrag_controller.go @@ -119,7 +119,9 @@ func (r *OpenRAGReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.reconcileServiceAccounts(ctx, instance, targetNS); err != nil { return r.updateStatusError(ctx, instance, "service accounts", err) } - if err := r.reconcileEnvSecrets(ctx, instance, targetNS); err != nil { + // Reconcile .env secrets and get their hashes to trigger pod restarts when secrets change + backendEnvHash, langflowEnvHash, err := r.reconcileEnvSecrets(ctx, instance, targetNS) + if err != nil { return r.updateStatusError(ctx, instance, "env secrets", err) } if err := r.reconcilePVCs(ctx, instance, targetNS); err != nil { @@ -128,7 +130,7 @@ func (r *OpenRAGReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.reconcileServices(ctx, instance, targetNS); err != nil { return r.updateStatusError(ctx, instance, "services", err) } - if err := r.reconcileDeployments(ctx, instance, targetNS); err != nil { + if err := r.reconcileDeployments(ctx, instance, targetNS, backendEnvHash, langflowEnvHash); err != nil { return r.updateStatusError(ctx, instance, "deployments", err) } if err := r.reconcileDoclingComponents(ctx, instance, targetNS); err != nil { @@ -207,19 +209,24 @@ func parseEnvValue(envContent, key string) string { // reconcileEnvSecrets creates / updates the backend and Langflow .env Secrets // from CR fields and fixed runtime defaults. // All sensitive values (whether user-provided or generated) are consolidated into .env files. -func (r *OpenRAGReconciler) reconcileEnvSecrets(ctx context.Context, o *openragv1alpha1.OpenRAG, targetNS string) error { +// Returns SHA256 hashes of the backend and langflow .env content to use as pod annotations. +func (r *OpenRAGReconciler) reconcileEnvSecrets(ctx context.Context, o *openragv1alpha1.OpenRAG, targetNS string) (backendHash, langflowHash string, err error) { // Build backend .env content with all secrets consolidated backendEnvContent, err := r.buildBackendEnv(ctx, o, targetNS) if err != nil { - return fmt.Errorf("failed to build backend env: %w", err) + return "", "", fmt.Errorf("failed to build backend env: %w", err) } // Build langflow .env content with all secrets consolidated langflowEnvContent, err := r.buildLangflowEnv(ctx, o, targetNS) if err != nil { - return fmt.Errorf("failed to build langflow env: %w", err) + return "", "", fmt.Errorf("failed to build langflow env: %w", err) } + // Calculate SHA256 hashes of .env content for pod restart triggering + backendHash = calculateHash(backendEnvContent) + langflowHash = calculateHash(langflowEnvContent) + type envDef struct { name string content string @@ -242,13 +249,13 @@ func (r *OpenRAGReconciler) reconcileEnvSecrets(ctx context.Context, o *openragv StringData: map[string]string{".env": d.content}, } if err := r.setOwnerOrLabel(o, secret, targetNS); err != nil { - return err + return "", "", err } if err := r.createOrUpdate(ctx, secret); err != nil { - return err + return "", "", err } } - return nil + return backendHash, langflowHash, nil } func (r *OpenRAGReconciler) buildBackendEnv(ctx context.Context, o *openragv1alpha1.OpenRAG, targetNS string) (string, error) { @@ -610,11 +617,11 @@ func (r *OpenRAGReconciler) reconcileServices(ctx context.Context, o *openragv1a return nil } -func (r *OpenRAGReconciler) reconcileDeployments(ctx context.Context, o *openragv1alpha1.OpenRAG, targetNS string) error { +func (r *OpenRAGReconciler) reconcileDeployments(ctx context.Context, o *openragv1alpha1.OpenRAG, targetNS string, backendEnvHash, langflowEnvHash string) error { deploys := []client.Object{ r.frontendDeployment(o, targetNS), - r.backendDeployment(o, targetNS), - r.langflowDeployment(o, targetNS), + r.backendDeployment(o, targetNS, backendEnvHash), + r.langflowDeployment(o, targetNS, langflowEnvHash), } for _, d := range deploys { if err := r.setOwnerOrLabel(o, d, targetNS); err != nil { @@ -679,7 +686,7 @@ func (r *OpenRAGReconciler) frontendDeployment(o *openragv1alpha1.OpenRAG, targe } } -func (r *OpenRAGReconciler) backendDeployment(o *openragv1alpha1.OpenRAG, targetNS string) *appsv1.Deployment { +func (r *OpenRAGReconciler) backendDeployment(o *openragv1alpha1.OpenRAG, targetNS string, envHash string) *appsv1.Deployment { spec := o.Spec.Backend replicas := replicasOrDefault(spec.Replicas) @@ -723,6 +730,8 @@ func (r *OpenRAGReconciler) backendDeployment(o *openragv1alpha1.OpenRAG, target deploymentAnnotations := mergeDeploymentAnnotations(spec.Annotations) podLabels := mergePodLabels(baseLabels, spec.PodLabels) podAnnotations := mergePodAnnotations(spec.PodAnnotations) + // Add .env secret hash to trigger pod restart when secret changes + podAnnotations["openr.ag/backend-env-hash"] = envHash return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName("be"), @@ -767,7 +776,7 @@ func (r *OpenRAGReconciler) backendDeployment(o *openragv1alpha1.OpenRAG, target } } -func (r *OpenRAGReconciler) langflowDeployment(o *openragv1alpha1.OpenRAG, targetNS string) *appsv1.Deployment { +func (r *OpenRAGReconciler) langflowDeployment(o *openragv1alpha1.OpenRAG, targetNS string, envHash string) *appsv1.Deployment { spec := o.Spec.Langflow replicas := replicasOrDefault(spec.Replicas) @@ -843,6 +852,8 @@ func (r *OpenRAGReconciler) langflowDeployment(o *openragv1alpha1.OpenRAG, targe deploymentAnnotations := mergeDeploymentAnnotations(spec.Annotations) podLabels := mergePodLabels(baseLabels, spec.PodLabels) podAnnotations := mergePodAnnotations(spec.PodAnnotations) + // Add .env secret hash to trigger pod restart when secret changes + podAnnotations["openr.ag/langflow-env-hash"] = envHash return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName("lf"), @@ -2632,3 +2643,10 @@ func (r *OpenRAGReconciler) getOrGenerateSecret(ctx context.Context, o *openragv return newSecret, nil } + +// calculateHash calculates the SHA256 hash of a string and returns the hex-encoded result. +// This is used to create annotations on pod templates that trigger rolling restarts when secrets change. +func calculateHash(content string) string { + hash := sha256.Sum256([]byte(content)) + return hex.EncodeToString(hash[:]) +} diff --git a/kubernetes/operator/internal/controller/openrag_controller_test.go b/kubernetes/operator/internal/controller/openrag_controller_test.go index 54ccc25c3..746617656 100644 --- a/kubernetes/operator/internal/controller/openrag_controller_test.go +++ b/kubernetes/operator/internal/controller/openrag_controller_test.go @@ -1273,3 +1273,189 @@ func TestReconcile_AllComponentsWithCustomNames_OperatorCreates(t *testing.T) { assert.True(t, errors.IsNotFound(err), "Default Service for %s should not be created", role) } } + +// --------------------------------------------------------------------------- +// .env Secret Hash and Pod Restart Tests +// --------------------------------------------------------------------------- + +func TestCalculateHash_Deterministic(t *testing.T) { + // Same content should always produce the same hash + content := "VAR1=value1\nVAR2=value2\nVAR3=value3\n" + + hash1 := calculateHash(content) + hash2 := calculateHash(content) + + assert.Equal(t, hash1, hash2, "Same content should produce same hash") + assert.NotEmpty(t, hash1, "Hash should not be empty") + assert.Len(t, hash1, 64, "SHA256 hash should be 64 hex characters") +} + +func TestCalculateHash_DifferentContent(t *testing.T) { + // Different content should produce different hashes + content1 := "VAR1=value1\nVAR2=value2\n" + content2 := "VAR1=value1\nVAR2=different\n" + + hash1 := calculateHash(content1) + hash2 := calculateHash(content2) + + assert.NotEqual(t, hash1, hash2, "Different content should produce different hash") +} + +func TestEnvHash_StableAcrossReconciles(t *testing.T) { + // Test that identical env produces identical hash even across multiple reconcile loops + s := newScheme(t) + cr := minimalCR("test-openrag", "test-ns") + cr.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "custom_value"}, + } + + r, _ := reconciler(s, cr) + + // Reconcile multiple times + var hashes []string + for i := 0; i < 5; i++ { + backendEnvContent, err := r.buildBackendEnv(context.Background(), cr, "test-ns") + require.NoError(t, err) + hash := calculateHash(backendEnvContent) + hashes = append(hashes, hash) + } + + // All hashes should be identical + for i := 1; i < len(hashes); i++ { + assert.Equal(t, hashes[0], hashes[i], "Hash should be stable across reconciles (iteration %d)", i) + } +} + +func TestEnvHash_ChangesWhenEnvChanges(t *testing.T) { + // Test that changing env vars produces different hash + s := newScheme(t) + cr := minimalCR("test-openrag", "test-ns") + cr.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "original_value"}, + } + + r, _ := reconciler(s, cr) + + // Get hash with original env + backendEnvContent1, err := r.buildBackendEnv(context.Background(), cr, "test-ns") + require.NoError(t, err) + hash1 := calculateHash(backendEnvContent1) + + // Change env var value + cr.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "changed_value"}, + } + + // Get hash with changed env + backendEnvContent2, err := r.buildBackendEnv(context.Background(), cr, "test-ns") + require.NoError(t, err) + hash2 := calculateHash(backendEnvContent2) + + assert.NotEqual(t, hash1, hash2, "Hash should change when env vars change") +} + +func TestDeployment_ContainsEnvHashAnnotation(t *testing.T) { + // Test that backend deployment has env hash annotation + s := newScheme(t) + cr := minimalCR("test-openrag", "test-ns") + r, c := reconciler(s, cr) + + reconcileOnce(t, r, cr) + + // Get backend deployment + backendDeploy := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("be"), Namespace: "test-ns"}, backendDeploy)) + + // Check for hash annotation + annotations := backendDeploy.Spec.Template.Annotations + require.NotNil(t, annotations, "Pod template should have annotations") + assert.Contains(t, annotations, "openr.ag/backend-env-hash", "Backend pod should have env hash annotation") + assert.NotEmpty(t, annotations["openr.ag/backend-env-hash"], "Hash annotation should not be empty") + assert.Len(t, annotations["openr.ag/backend-env-hash"], 64, "Hash should be 64 hex characters") + + // Get langflow deployment + langflowDeploy := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("lf"), Namespace: "test-ns"}, langflowDeploy)) + + // Check for hash annotation + lfAnnotations := langflowDeploy.Spec.Template.Annotations + require.NotNil(t, lfAnnotations, "Langflow pod template should have annotations") + assert.Contains(t, lfAnnotations, "openr.ag/langflow-env-hash", "Langflow pod should have env hash annotation") + assert.NotEmpty(t, lfAnnotations["openr.ag/langflow-env-hash"], "Hash annotation should not be empty") + assert.Len(t, lfAnnotations["openr.ag/langflow-env-hash"], 64, "Hash should be 64 hex characters") +} + +func TestDeployment_HashChangeTriggersUpdate(t *testing.T) { + // Test that changing env causes hash annotation to change, triggering pod restart + s := newScheme(t) + cr := minimalCR("test-openrag", "test-ns") + cr.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "original"}, + } + r, c := reconciler(s, cr) + + // Initial reconcile + reconcileOnce(t, r, cr) + + // Get initial hash + backendDeploy1 := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("be"), Namespace: "test-ns"}, backendDeploy1)) + hash1 := backendDeploy1.Spec.Template.Annotations["openr.ag/backend-env-hash"] + require.NotEmpty(t, hash1, "Initial hash should exist") + + // Update CR with different env value + updatedCR := &openragv1alpha1.OpenRAG{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: "test-openrag", Namespace: "test-ns"}, updatedCR)) + updatedCR.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "changed"}, + } + require.NoError(t, c.Update(context.Background(), updatedCR)) + + // Reconcile again with updated CR + reconcileOnce(t, r, updatedCR) + + // Get updated hash + backendDeploy2 := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("be"), Namespace: "test-ns"}, backendDeploy2)) + hash2 := backendDeploy2.Spec.Template.Annotations["openr.ag/backend-env-hash"] + require.NotEmpty(t, hash2, "Updated hash should exist") + + // Hash should have changed + assert.NotEqual(t, hash1, hash2, "Hash should change when env changes, triggering pod restart") +} + +func TestDeployment_NoHashChangeWhenEnvUnchanged(t *testing.T) { + // Test that reconciling without env changes keeps the same hash + s := newScheme(t) + cr := minimalCR("test-openrag", "test-ns") + cr.Spec.Backend.Env = []corev1.EnvVar{ + {Name: "CUSTOM_VAR", Value: "constant"}, + } + r, c := reconciler(s, cr) + + // Initial reconcile + reconcileOnce(t, r, cr) + + // Get initial hash + backendDeploy1 := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("be"), Namespace: "test-ns"}, backendDeploy1)) + hash1 := backendDeploy1.Spec.Template.Annotations["openr.ag/backend-env-hash"] + + // Reconcile again without changing env + reconcileOnce(t, r, cr) + + // Get hash after second reconcile + backendDeploy2 := &appsv1.Deployment{} + require.NoError(t, c.Get(context.Background(), + types.NamespacedName{Name: resourceName("be"), Namespace: "test-ns"}, backendDeploy2)) + hash2 := backendDeploy2.Spec.Template.Annotations["openr.ag/backend-env-hash"] + + // Hash should be identical (no unnecessary pod restart) + assert.Equal(t, hash1, hash2, "Hash should remain same when env unchanged (avoids unnecessary restarts)") +} From c441e96a4751151eeee5d881c2d4fa17d364714a Mon Sep 17 00:00:00 2001 From: rodageve <78763007+rodageve@users.noreply.github.com> Date: Fri, 22 May 2026 14:57:15 -0400 Subject: [PATCH 07/14] fix: Ensure Langflow .env variable definitions from LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT (#1667) * Ensure we dynamically update the list of Langflow .env environment variables with default values when the comma separated list defined in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT changes * fix tests * fix additional linting errors --------- Co-authored-by: rodageve --- .../operator/internal/controller/env.go | 36 ++++ .../internal/controller/env_example_test.go | 2 +- .../operator/internal/controller/env_test.go | 170 ++++++++++++++++++ .../internal/controller/openrag_controller.go | 15 +- 4 files changed, 221 insertions(+), 2 deletions(-) diff --git a/kubernetes/operator/internal/controller/env.go b/kubernetes/operator/internal/controller/env.go index d229e3479..e08b02a12 100644 --- a/kubernetes/operator/internal/controller/env.go +++ b/kubernetes/operator/internal/controller/env.go @@ -69,6 +69,14 @@ func NewEnvVarManager() *EnvVarManager { "FILESIZE": "0", "SELECTED_EMBEDDING_MODEL": "", + // OpenSearch defaults (for variables in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT) + "OPENSEARCH_PASSWORD": "None", + "OPENSEARCH_URL": "None", + "OPENSEARCH_INDEX_NAME": "None", + + // Docling defaults (for variables in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT) + "DOCLING_SERVE_URL": "None", + // Provider API keys (defaults to None, overridden by CR spec) "OPENAI_API_KEY": "None", "ANTHROPIC_API_KEY": "None", @@ -216,6 +224,34 @@ func (m *EnvVarManager) BuildEnvFileContent(envVars map[string]string) string { return b.String() } +// EnsureRequiredEnvVars ensures all variables listed in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT +// exist in the envVars map with at least a "None" value. This is critical because Langflow components +// expect these variables to be present in the environment, and the list can be customized via CR spec, +// operator env vars (OPTLF_LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT), or defaults. +func (m *EnvVarManager) EnsureRequiredEnvVars(envVars map[string]string) { + // Get the list of required variables + requiredVarsStr, exists := envVars["LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT"] + if !exists || requiredVarsStr == "" { + return + } + + // Parse comma-separated list + requiredVars := strings.Split(requiredVarsStr, ",") + + // Ensure each variable exists with at least "None" value + for _, varName := range requiredVars { + varName = strings.TrimSpace(varName) + if varName == "" { + continue + } + + // Only add if not already present + if _, exists := envVars[varName]; !exists { + envVars[varName] = "None" + } + } +} + // Generates a base64-encoded string of exactly 32 bytes for Fernet func generateBase64SecretKey() (string, error) { randomBytes := make([]byte, 32) diff --git a/kubernetes/operator/internal/controller/env_example_test.go b/kubernetes/operator/internal/controller/env_example_test.go index a7c5d5c8b..85bf4bff2 100644 --- a/kubernetes/operator/internal/controller/env_example_test.go +++ b/kubernetes/operator/internal/controller/env_example_test.go @@ -43,7 +43,7 @@ func Example_envVarPriority() { // LANGFLOW_WORKERS: 8 (from operator env) // LANGFLOW_LOG_LEVEL: ERROR (from CR spec) // - // .env file would contain 1380 bytes + // .env file would contain 1475 bytes } // Example showing how different components use different prefixes diff --git a/kubernetes/operator/internal/controller/env_test.go b/kubernetes/operator/internal/controller/env_test.go index 9d033397b..51b2a9150 100644 --- a/kubernetes/operator/internal/controller/env_test.go +++ b/kubernetes/operator/internal/controller/env_test.go @@ -218,3 +218,173 @@ func TestEnvVarManager_NewEnvVarManagerDefaults(t *testing.T) { // Verify Frontend defaults (empty for now) assert.NotNil(t, manager.DefaultOpenRagFEEnvVars) } + +func TestEnvVarManager_EnsureRequiredEnvVars(t *testing.T) { + tests := []struct { + name string + inputEnvVars map[string]string + expectedResult map[string]string + description string + }{ + { + name: "adds missing variables with None", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,VAR2,VAR3", + "VAR1": "existing_value", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,VAR2,VAR3", + "VAR1": "existing_value", + "VAR2": "None", + "VAR3": "None", + }, + description: "Should add VAR2 and VAR3 with 'None' value, preserve VAR1", + }, + { + name: "handles whitespace in variable list", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1, VAR2 , VAR3", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1, VAR2 , VAR3", + "VAR1": "None", + "VAR2": "None", + "VAR3": "None", + }, + description: "Should trim whitespace from variable names", + }, + { + name: "skips empty variable names", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,,VAR2, ,VAR3", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,,VAR2, ,VAR3", + "VAR1": "None", + "VAR2": "None", + "VAR3": "None", + }, + description: "Should skip empty strings and whitespace-only entries", + }, + { + name: "does nothing when LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT is missing", + inputEnvVars: map[string]string{ + "VAR1": "value1", + }, + expectedResult: map[string]string{ + "VAR1": "value1", + }, + description: "Should not modify envVars when the list variable is missing", + }, + { + name: "does nothing when LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT is empty", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "", + "VAR1": "value1", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "", + "VAR1": "value1", + }, + description: "Should not modify envVars when the list is empty", + }, + { + name: "preserves existing values including empty strings", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,VAR2,VAR3", + "VAR1": "", + "VAR2": "0", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "VAR1,VAR2,VAR3", + "VAR1": "", + "VAR2": "0", + "VAR3": "None", + }, + description: "Should preserve empty string and '0' values, only add missing VAR3", + }, + { + name: "handles real-world variable list", + inputEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "JWT,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL", + "JWT": "token123", + "OPENSEARCH_PASSWORD": "secret", + }, + expectedResult: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "JWT,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL", + "JWT": "token123", + "OPENSEARCH_PASSWORD": "secret", + "OPENSEARCH_URL": "None", + "DOCLING_SERVE_URL": "None", + }, + description: "Should add missing OpenSearch and Docling variables", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := &EnvVarManager{} + + // Call the function + manager.EnsureRequiredEnvVars(tt.inputEnvVars) + + // Verify the result + assert.Equal(t, tt.expectedResult, tt.inputEnvVars, tt.description) + }) + } +} + +func TestEnvVarManager_EnsureRequiredEnvVars_Integration(t *testing.T) { + // Test with the actual default configuration + manager := NewEnvVarManager() + + // Get the default Langflow env vars + envVars := manager.GetLangflowEnvVars(nil) + + // Verify LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT exists + requiredVarsStr, exists := envVars["LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT"] + assert.True(t, exists, "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT should exist in defaults") + assert.NotEmpty(t, requiredVarsStr, "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT should not be empty") + + // Call EnsureRequiredEnvVars + manager.EnsureRequiredEnvVars(envVars) + + // Parse the required variables list + requiredVars := []string{"JWT", "OPENRAG_QUERY_FILTER", "OPENSEARCH_PASSWORD", "OPENSEARCH_URL", + "OPENSEARCH_INDEX_NAME", "DOCLING_SERVE_URL", "DOCLING_TASK_ID", "OWNER", "OWNER_NAME", + "OWNER_EMAIL", "CONNECTOR_TYPE", "DOCUMENT_ID", "SOURCE_URL", "ALLOWED_USERS", + "ALLOWED_GROUPS", "FILENAME", "MIMETYPE", "FILESIZE", "SELECTED_EMBEDDING_MODEL", + "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "WATSONX_API_KEY", "WATSONX_ENDPOINT", + "WATSONX_PROJECT_ID", "OLLAMA_BASE_URL"} + + // Verify all required variables exist in the envVars map + for _, varName := range requiredVars { + _, exists := envVars[varName] + assert.True(t, exists, "Variable %s should exist in envVars", varName) + // Note: Some variables may have empty string as their default value (e.g., DOCUMENT_ID, SOURCE_URL, SELECTED_EMBEDDING_MODEL) + // The important thing is that they exist in the map + } + + // Verify the newly added defaults are present + assert.Equal(t, "None", envVars["OPENSEARCH_PASSWORD"], "OPENSEARCH_PASSWORD should have default 'None'") + assert.Equal(t, "None", envVars["OPENSEARCH_URL"], "OPENSEARCH_URL should have default 'None'") + assert.Equal(t, "None", envVars["OPENSEARCH_INDEX_NAME"], "OPENSEARCH_INDEX_NAME should have default 'None'") + assert.Equal(t, "None", envVars["DOCLING_SERVE_URL"], "DOCLING_SERVE_URL should have default 'None'") +} + +func TestEnvVarManager_EnsureRequiredEnvVars_CustomList(t *testing.T) { + // Test with a custom LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT value + manager := &EnvVarManager{ + DefaultLangflowEnvVars: map[string]string{ + "LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT": "CUSTOM_VAR1,CUSTOM_VAR2", + "CUSTOM_VAR1": "value1", + }, + } + + envVars := manager.GetLangflowEnvVars(nil) + manager.EnsureRequiredEnvVars(envVars) + + // Verify custom variables are handled + assert.Equal(t, "value1", envVars["CUSTOM_VAR1"], "CUSTOM_VAR1 should preserve existing value") + assert.Equal(t, "None", envVars["CUSTOM_VAR2"], "CUSTOM_VAR2 should be added with 'None'") +} diff --git a/kubernetes/operator/internal/controller/openrag_controller.go b/kubernetes/operator/internal/controller/openrag_controller.go index 1e4113174..bc32d08f4 100644 --- a/kubernetes/operator/internal/controller/openrag_controller.go +++ b/kubernetes/operator/internal/controller/openrag_controller.go @@ -508,7 +508,16 @@ func (r *OpenRAGReconciler) buildLangflowEnv(ctx context.Context, o *openragv1al } // Docling configuration from CR spec - if d := o.Spec.Docling; d != nil { + // Priority: DoclingComponents (operator-managed) > Docling (external) + if dc := o.Spec.DoclingComponents; dc != nil && dc.Enabled && dc.Serve != nil { + // Use operator-managed docling-serve + port := int32(5001) + if dc.Serve.Port > 0 { + port = dc.Serve.Port + } + envVars["DOCLING_SERVE_URL"] = fmt.Sprintf("http://%s:%d", getServiceName(o, "ds"), port) + } else if d := o.Spec.Docling; d != nil { + // Use external docling service scheme := d.Scheme if scheme == "" { scheme = "http" @@ -520,6 +529,10 @@ func (r *OpenRAGReconciler) buildLangflowEnv(ctx context.Context, o *openragv1al envVars["DOCLING_SERVE_URL"] = fmt.Sprintf("%s://%s:%d", scheme, d.Host, port) } + // Ensure all variables in LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT exist with at least "None" value + // This is critical because the list can be customized via CR spec, operator env vars, or defaults + r.EnvVarManager.EnsureRequiredEnvVars(envVars) + // Convert map to .env file format return r.EnvVarManager.BuildEnvFileContent(envVars), nil } From cc8d8064c51b3ceaf9dbe583ee01e8dbaa9e4f4c Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 22 May 2026 14:31:53 -0500 Subject: [PATCH 08/14] chore: Retire openrag-mcp; switch docs to streamable HTTP (#1668) * Retire openrag-mcp; switch docs to streamable HTTP Remove the stdio-based MCP server and all in-repo MCP tooling, and update README to mark the package as retired. Deleted module files include the MCP entrypoint, server, config, registry and individual tools (chat, search, documents, settings). The README was rewritten to announce that openrag-mcp is retired, explain migration to the built-in streamable-HTTP /mcp endpoint, update Cursor/Claude examples to use URL+headers auth, list the new v1 API tools, and note that the last PyPI release is final. This change consolidates MCP functionality into the OpenRAG core and removes the subprocess/stdio implementation and its source code. * Mark MCP SDK retired and clean package metadata Update package metadata to reflect retirement and integration into the OpenRAG backend. Bump version to 0.3.0 and replace the project description with a retirement/migration note. Set Development Status to Inactive, remove explicit Python version classifiers, and clear runtime dependencies and the CLI script entrypoint. Also remove the hatch env pip-args setting; build-system and wheel package target remain unchanged. * chore: update uv.lock files after version bump * Update uv.lock --------- Co-authored-by: github-actions[bot] --- sdks/mcp/README.md | 238 ++---- sdks/mcp/pyproject.toml | 22 +- sdks/mcp/src/openrag_mcp/__main__.py | 7 - sdks/mcp/src/openrag_mcp/config.py | 122 ---- sdks/mcp/src/openrag_mcp/server.py | 81 --- sdks/mcp/src/openrag_mcp/tools/__init__.py | 15 - sdks/mcp/src/openrag_mcp/tools/chat.py | 116 --- sdks/mcp/src/openrag_mcp/tools/documents.py | 367 ---------- sdks/mcp/src/openrag_mcp/tools/registry.py | 37 - sdks/mcp/src/openrag_mcp/tools/search.py | 139 ---- sdks/mcp/src/openrag_mcp/tools/settings.py | 266 ------- sdks/mcp/uv.lock | 761 +------------------- 12 files changed, 74 insertions(+), 2097 deletions(-) delete mode 100644 sdks/mcp/src/openrag_mcp/__main__.py delete mode 100644 sdks/mcp/src/openrag_mcp/config.py delete mode 100644 sdks/mcp/src/openrag_mcp/server.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/__init__.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/chat.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/documents.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/registry.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/search.py delete mode 100644 sdks/mcp/src/openrag_mcp/tools/settings.py diff --git a/sdks/mcp/README.md b/sdks/mcp/README.md index 8034b85f9..a2ab963da 100644 --- a/sdks/mcp/README.md +++ b/sdks/mcp/README.md @@ -1,237 +1,137 @@ -# OpenRAG MCP Server +# OpenRAG MCP — Final Release Notice -An [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes your OpenRAG knowledge base to AI assistants. It lets MCP-compatible apps like Cursor, Claude Desktop, and IBM Watson Orchestrate use OpenRAG’s RAG capabilities (chat, search, settings) over a standard protocol—no custom integrations per platform. +> **This package (`openrag-mcp`) has been retired.** +> +> OpenRAG now ships a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server over **streamable HTTP** — no subprocess, no separate install, no API key in a subprocess env block. Connect your MCP client directly to your running OpenRAG instance. +> +> The last published version of `openrag-mcp` on PyPI is the final release. No further updates will be made to this package. --- -## What is OpenRAG MCP? +## Migrating to streamable HTTP -OpenRAG MCP is a **connectivity layer** between your OpenRAG instance and AI applications. The host app (e.g. Cursor or Claude Desktop) runs the MCP server as a subprocess and talks to it over stdio using JSON-RPC. The server then calls your OpenRAG API with your API key. Your knowledge base stays the single source of truth; all connected apps get the same RAG-backed chat and search. - ---- - -## Quick Start - -Run the server with **uvx** (no local install required; requires Python 3.10+ and [uv](https://docs.astral.sh/uv/)): - -```bash -uvx openrag-mcp -``` - -Set required environment variables first (or pass them via your MCP client config): - -```bash -export OPENRAG_URL="https://your-openrag-instance.com" -export OPENRAG_API_KEY="orag_your_api_key" -uvx openrag-mcp -``` - -To pin a version: - -```bash -uvx --from openrag-mcp==0.2.1 openrag-mcp -``` +The OpenRAG backend exposes an MCP endpoint at `/mcp` using the streamable-HTTP transport. Any MCP client that supports `"url"`-based server configs (Cursor, Claude Desktop, and the MCP SDK) can connect to it directly. ### Prerequisites -- Python 3.10+ -- A running OpenRAG instance -- An OpenRAG API key (create one in **Settings → API Keys** in OpenRAG) -- `uv` installed (for `uvx`) - ---- - -## Available Tools - -These tools are currently exposed by the server: - -| Tool | Description | -|:-----|:------------| -| `openrag_chat` | Send a message and get a RAG-enhanced response. Optional: `chat_id`, `filter_id`, `limit`, `score_threshold`. | -| `openrag_search` | Semantic search over the knowledge base. Optional: `limit`, `score_threshold`, `filter_id`, `data_sources`, `document_types`. | -| `openrag_get_settings` | Get current OpenRAG configuration (LLM, embeddings, chunk settings, system prompt, etc.). | -| `openrag_update_settings` | Update OpenRAG configuration (LLM model, embedding model, chunk size/overlap, system prompt, table structure, OCR, picture descriptions). | -| `openrag_list_models` | List available language and embedding models for a provider (`openai`, `anthropic`, `ollama`, `watsonx`). | - -### Coming later (document tools) - -Document ingestion and management tools (`openrag_ingest_file`, `openrag_ingest_url`, `openrag_delete_document`, `openrag_get_task_status`, `openrag_wait_for_task`) are implemented but not yet registered in this server; they will be enabled in a future release. +- A running OpenRAG instance (v0.3.0 or later) +- An OpenRAG API key — create one in **Settings → API Keys** --- -## Environment Variables - -| Variable | Description | Required | Default | -|:---------|:------------|:--------:|:--------| -| `OPENRAG_API_KEY` | Your OpenRAG API key | Yes | — | -| `OPENRAG_URL` | Base URL of your OpenRAG instance | No | `http://localhost:3000` | - -**MCP HTTP client (optional):** - -| Variable | Description | Required | Default | -|:---------|:------------|:--------:|:--------| -| `OPENRAG_MCP_TIMEOUT` | Request timeout in seconds | No | `60.0` | -| `OPENRAG_MCP_MAX_CONNECTIONS` | Maximum concurrent connections | No | `100` | -| `OPENRAG_MCP_MAX_KEEPALIVE_CONNECTIONS` | Maximum keepalive connections | No | `20` | -| `OPENRAG_MCP_MAX_RETRIES` | Maximum retry attempts for failed requests | No | `3` | -| `OPENRAG_MCP_FOLLOW_REDIRECTS` | Whether to follow HTTP redirects | No | `true` | - -These must be set in the environment when the MCP server runs (e.g. in the `env` block of your MCP client config). - ---- - -## How to Use - -### Cursor +## Cursor **Config file:** `~/.cursor/mcp.json` +**Standard API key:** + ```json { "mcpServers": { "openrag": { - "command": "uvx", - "args": ["openrag-mcp"], - "env": { - "OPENRAG_URL": "https://your-openrag-instance.com", - "OPENRAG_API_KEY": "orag_your_api_key_here" + "url": "https://your-openrag-instance.com/mcp", + "headers": { + "X-API-Key": "orag_your_api_key_here" } } } } ``` -Restart Cursor after changing the config. - -### Claude Desktop - -**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` -**Windows:** `%APPDATA%\Claude\claude_desktop_config.json` +**IBM auth (when `IBM_AUTH_ENABLED=true` on the server):** ```json { "mcpServers": { "openrag": { - "command": "uvx", - "args": ["openrag-mcp"], - "env": { - "OPENRAG_URL": "https://your-openrag-instance.com", - "OPENRAG_API_KEY": "orag_your_api_key_here" + "url": "https://your-openrag-instance.com/mcp", + "headers": { + "X-Username": "your_ibm_username", + "X-Api-Key": "your_ibm_api_key" } } } } ``` -Restart Claude Desktop after editing the file. +Restart Cursor after saving the config. --- -## Run from source (development) - -To use the **latest MCP code** from the repo (including settings and models tools), run from source. Do **not** install the package if you want local edits to apply. - -### Steps - -| Step | What | Command | Required for | -|------|------|---------|---------------| -| 1 | OpenRAG backend | Run your OpenRAG app (e.g. frontend + API) | All tools | -| 2 | MCP from source | `cd sdks/mcp && uv sync` | All tools; no wheel needed | -| 3 | (Optional) SDK from repo | `cd sdks/python && uv pip install -e .` | Only if you need unreleased chat/search SDK changes | - -Settings and models tools (`openrag_get_settings`, `openrag_update_settings`, `openrag_list_models`) use direct HTTP. Chat and search use the OpenRAG SDK (PyPI version is fine unless you need unreleased SDK changes). +## Claude Desktop -### Run the MCP from source - -```bash -cd sdks/mcp -uv sync -export OPENRAG_URL="http://localhost:3000" -export OPENRAG_API_KEY="orag_your_api_key" -uv run openrag-mcp -``` - -### Cursor: use repo path so it runs your code +**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows:** `%APPDATA%\Claude\claude_desktop_config.json` -In `~/.cursor/mcp.json`, set `--directory` to your **actual repo path** so Cursor runs the MCP from source: +**Standard API key:** ```json { "mcpServers": { "openrag": { - "command": "uv", - "args": [ - "run", - "--directory", - "/path/to/openrag/sdks/mcp", - "openrag-mcp" - ], - "env": { - "OPENRAG_URL": "https://your-openrag-instance.com", - "OPENRAG_API_KEY": "orag_your_api_key_here" + "url": "https://your-openrag-instance.com/mcp", + "headers": { + "X-API-Key": "orag_your_api_key_here" } } } } ``` -Replace `/path/to/openrag` with your real path (e.g. `/Users/edwin.jose/Documents/openrag`). - -If you previously installed the MCP (`pip install openrag-mcp` or a wheel), uninstall it so Cursor uses the repo: +**IBM auth:** -```bash -uv pip uninstall openrag-mcp +```json +{ + "mcpServers": { + "openrag": { + "url": "https://your-openrag-instance.com/mcp", + "headers": { + "X-Username": "your_ibm_username", + "X-Api-Key": "your_ibm_api_key" + } + } + } +} ``` -Then restart Cursor. - ---- - -## Use cases and benefits - -- **One integration, many apps** – Same MCP server works with Cursor, Claude Desktop, Watson Orchestrate, and any MCP client. -- **RAG in the loop** – Chat and search are grounded in your OpenRAG knowledge base, with optional filters and scoring. -- **Agent-friendly** – Agents can call OpenRAG for answers, list models, and read/update settings without custom APIs. -- **Lightweight** – No extra service to deploy; the host app spawns the server as a subprocess and talks over stdio. -- **Secure** – Only clients that have your `OPENRAG_API_KEY` (via env) can use the server to access OpenRAG. - -**Example scenarios:** Query internal docs and runbooks from your IDE; power support bots with your product docs; search and summarize across ingested documents; automate workflows that need RAG (when document tools are enabled). +Restart Claude Desktop after editing the file. --- +## Available tools -## Example prompts +All tools are auto-exposed from the `/v1/` API and are available immediately after connecting: -Once the server is configured, you can ask the AI to: - -- *"Search my knowledge base for authentication best practices"* -- *"Chat with OpenRAG about the Q4 roadmap"* -- *"What are the current OpenRAG settings?"* -- *"List available models for the openai provider"* -- *"Update OpenRAG to use chunk size 512"* +| Tool | Description | +|:-----|:------------| +| `openrag_chat` | Send a message and get a RAG-enhanced response. Supports `chat_id` and `filter_id`. | +| `openrag_list_chats` | List all chat conversations. | +| `openrag_get_chat` | Get a specific chat conversation by ID. | +| `openrag_delete_chat` | Delete a chat conversation by ID. | +| `openrag_search` | Semantic search over the knowledge base. Supports filters, score threshold, data sources. | +| `openrag_ingest` | Ingest documents (files, URLs, text) into the knowledge base. Returns a `task_id`. | +| `openrag_get_task_status` | Check the status of an ingestion task by `task_id`. | +| `openrag_delete_document` | Delete a document from the knowledge base by filename. | +| `openrag_get_settings` | Get current OpenRAG configuration (LLM, embeddings, chunk settings, system prompt). | +| `openrag_update_settings` | Update OpenRAG configuration. All fields are optional. | +| `openrag_list_models` | List available models for a provider (`openai`, `anthropic`, `ollama`, `watsonx`). | +| `openrag_create_knowledge_filter` | Create a knowledge filter to scope searches and chats. | +| `openrag_search_knowledge_filters` | Search knowledge filters by name or criteria. | +| `openrag_get_knowledge_filter` | Get a knowledge filter by ID. | +| `openrag_update_knowledge_filter` | Update an existing knowledge filter. | +| `openrag_delete_knowledge_filter` | Delete a knowledge filter by ID. | --- -## Troubleshooting - -### "OPENRAG_API_KEY environment variable is required" - -Set `OPENRAG_API_KEY` in the `env` section of your MCP config (Cursor or Claude Desktop). The server reads it at startup. - -### "Connection refused" or network errors - -1. Confirm your OpenRAG instance is running and reachable. -2. Check `OPENRAG_URL` (no trailing slash; include `https://` if applicable). -3. Ensure no firewall or proxy is blocking the client machine from reaching OpenRAG. - -### Tools not appearing +## Why streamable HTTP? -1. Restart the host app (Cursor or Claude Desktop) after changing the MCP config. -2. Check the app’s MCP/log output for errors (e.g. wrong `command`/`args` or missing `uv`/`uvx`). -3. If using "run from source", ensure `args` includes `--directory` and the correct path to `sdks/mcp`. +- **No subprocess** — your MCP client connects over HTTP; nothing to install or spawn locally. +- **Full tool surface** — document ingestion, task tracking, and knowledge filters are available from day one (previously listed as "coming later" in the stdio package). +- **One auth model** — the same API key or IBM credentials you use for the REST API work for MCP. +- **Self-hosted and secure** — the `/mcp` endpoint is part of your OpenRAG deployment; nothing leaves your network. --- ## License -Apache 2.0 - See [LICENSE](../../LICENSE) for details. +Apache 2.0 — see [LICENSE](../../LICENSE) for details. diff --git a/sdks/mcp/pyproject.toml b/sdks/mcp/pyproject.toml index d7648d862..3284b588a 100644 --- a/sdks/mcp/pyproject.toml +++ b/sdks/mcp/pyproject.toml @@ -1,41 +1,27 @@ [project] name = "openrag-mcp" -version = "0.3.0rc4" -description = "MCP server for OpenRAG" +version = "0.3.0" +description = "RETIRED — OpenRAG MCP is now built into the OpenRAG backend as a streamable-HTTP endpoint at /mcp. See README for migration." readme = "README.md" requires-python = ">=3.10" license = { text = "Apache-2.0" } authors = [{ name = "OpenRAG Team" }] keywords = ["mcp", "openrag", "rag", "ai", "llm"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 7 - Inactive", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = [ - "mcp>=1.0.0", - "httpx>=0.28.0", - "openrag-sdk==0.3.0rc1", -] - -[project.scripts] -openrag-mcp = "openrag_mcp:main" +dependencies = [] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" -[tool.hatch.envs.default] -pip-args = ["--pre"] - [tool.hatch.build.targets.wheel] packages = ["src/openrag_mcp"] diff --git a/sdks/mcp/src/openrag_mcp/__main__.py b/sdks/mcp/src/openrag_mcp/__main__.py deleted file mode 100644 index 39abbfc10..000000000 --- a/sdks/mcp/src/openrag_mcp/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Entry point for running as a module: python -m openrag_mcp""" - -from openrag_mcp.server import main - -if __name__ == "__main__": - main() - diff --git a/sdks/mcp/src/openrag_mcp/config.py b/sdks/mcp/src/openrag_mcp/config.py deleted file mode 100644 index 2ef0d88d6..000000000 --- a/sdks/mcp/src/openrag_mcp/config.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Configuration for OpenRAG MCP server.""" - -import os - -from openrag_sdk import OpenRAGClient - - -def _parse_float(key: str, default: float) -> float: - """Parse a positive float from environment.""" - raw = os.environ.get(key) - if raw is None: - return default - try: - value = float(raw) - return value if value > 0 else default - except ValueError: - return default - - -def _parse_int(key: str, default: int) -> int: - """Parse a positive int from environment.""" - raw = os.environ.get(key) - if raw is None: - return default - try: - value = int(raw) - return value if value > 0 else default - except ValueError: - return default - - -def _parse_bool(key: str, default: bool) -> bool: - """Parse a boolean from environment (true/false, 1/0).""" - raw = os.environ.get(key) - if raw is None: - return default - return raw.strip().lower() in ("true", "1", "yes") - - -class Config: - """Configuration loaded from environment variables.""" - - def __init__(self): - self.openrag_url = os.environ.get("OPENRAG_URL", "http://localhost:3000") - self.api_key = os.environ.get("OPENRAG_API_KEY") - - # Platform auth mode: user provides PLATFORM_USERNAME + IBM_API_KEY instead of OPENRAG_API_KEY. - # The MCP forwards these as headers on every SDK request to OpenRAG. - platform_username = os.environ.get("PLATFORM_USERNAME") - ibm_api_key = os.environ.get("IBM_API_KEY") - self.ibm_extra_headers: dict[str, str] = {} - if platform_username: - self.ibm_extra_headers["X-Username"] = platform_username - if ibm_api_key: - self.ibm_extra_headers["X-Api-Key"] = ibm_api_key - - if not self.api_key and not self.ibm_extra_headers: - raise ValueError( - "OPENRAG_API_KEY environment variable is required. " - "Create an API key in OpenRAG Settings > API Keys." - ) - - # MCP httpx client configuration (OPENRAG_MCP_*) - self.mcp_timeout = _parse_float("OPENRAG_MCP_TIMEOUT", 60.0) - self.mcp_max_connections = _parse_int("OPENRAG_MCP_MAX_CONNECTIONS", 100) - self.mcp_max_keepalive_connections = _parse_int("OPENRAG_MCP_MAX_KEEPALIVE_CONNECTIONS", 20) - self.mcp_max_retries = _parse_int("OPENRAG_MCP_MAX_RETRIES", 3) - self.mcp_follow_redirects = _parse_bool("OPENRAG_MCP_FOLLOW_REDIRECTS", True) - - @property - def headers(self) -> dict[str, str]: - """Get HTTP headers for API requests.""" - headers: dict[str, str] = {"Content-Type": "application/json"} - if self.api_key: - headers["X-API-Key"] = self.api_key - return headers - - -_config: Config | None = None -_openrag_client: OpenRAGClient | None = None - - -def get_config() -> Config: - """Get singleton config instance.""" - global _config - if _config is None: - _config = Config() - return _config - - -def get_openrag_client() -> OpenRAGClient: - """Get singleton OpenRAGClient instance.""" - global _openrag_client - if _openrag_client is None: - config = get_config() - _openrag_client = OpenRAGClient( - api_key=config.api_key, - extra_headers=config.ibm_extra_headers or None, - ) - return _openrag_client - - -def get_client(): - """Get an httpx async client configured for OpenRAG. - - This is kept for backward compatibility with operations - not yet supported by the SDK (list_documents, ingest_url). - """ - import httpx - - config = get_config() - return httpx.AsyncClient( - base_url=config.openrag_url, - headers=config.headers, - timeout=config.mcp_timeout, - limits=httpx.Limits( - max_connections=config.mcp_max_connections, - max_keepalive_connections=config.mcp_max_keepalive_connections, - ), - transport=httpx.AsyncHTTPTransport(retries=config.mcp_max_retries), - follow_redirects=config.mcp_follow_redirects, - ) diff --git a/sdks/mcp/src/openrag_mcp/server.py b/sdks/mcp/src/openrag_mcp/server.py deleted file mode 100644 index 7c8e25519..000000000 --- a/sdks/mcp/src/openrag_mcp/server.py +++ /dev/null @@ -1,81 +0,0 @@ -"""OpenRAG MCP Server - Main server setup and entry point.""" -# Note: The MCP server is currently configured explicitly rather than being driven -# directly by the OpenRAG SDK, so changes to SDK parameters must be reflected here manually. - -import asyncio -import logging - -from mcp.server import Server -from mcp.server.stdio import stdio_server -from mcp.types import TextContent, Tool - -from openrag_mcp.config import get_config - -# Import tools module to trigger registration, then get registry functions -from openrag_mcp.tools import get_all_tools, get_handler - -# Configure logging to stderr (stdout is used for MCP protocol) -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[logging.StreamHandler()], -) -logger = logging.getLogger("openrag-mcp") - - -def create_server() -> Server: - """Create and configure the MCP server with all tools registered.""" - # Validate configuration early - config = get_config() - logger.info(f"Connecting to OpenRAG at {config.openrag_url}") - - # Create server instance - server = Server("openrag-mcp") - - @server.list_tools() - async def list_all_tools() -> list[Tool]: - """List all available tools.""" - return get_all_tools() - - @server.call_tool() - async def call_tool(name: str, arguments: dict) -> list[TextContent]: - """Handle all tool calls by dispatching to the appropriate handler.""" - handler = get_handler(name) - if handler: - return await handler(arguments) - return [TextContent(type="text", text=f"Unknown tool: {name}")] - - logger.info("OpenRAG MCP server initialized with all tools") - return server - - -async def run_server(): - """Run the MCP server with stdio transport.""" - server = create_server() - - async with stdio_server() as (read_stream, write_stream): - logger.info("Starting OpenRAG MCP server with stdio transport") - await server.run( - read_stream, - write_stream, - server.create_initialization_options(), - ) - - -def main(): - """Entry point for the MCP server.""" - try: - asyncio.run(run_server()) - except KeyboardInterrupt: - logger.info("Server stopped by user") - except ValueError as e: - # Configuration errors - logger.error(f"Configuration error: {e}") - raise SystemExit(1) - except Exception as e: - logger.error(f"Server error: {e}") - raise SystemExit(1) - - -if __name__ == "__main__": - main() diff --git a/sdks/mcp/src/openrag_mcp/tools/__init__.py b/sdks/mcp/src/openrag_mcp/tools/__init__.py deleted file mode 100644 index fce9de1da..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""OpenRAG MCP tools. - -Import this module to register all tools with the registry. -""" - -# Import tools to trigger registration -from openrag_mcp.tools import chat # noqa: F401 -from openrag_mcp.tools import search # noqa: F401 -from openrag_mcp.tools import documents # noqa: F401 -from openrag_mcp.tools import settings # noqa: F401 - -# Re-export registry functions for convenience -from openrag_mcp.tools.registry import get_all_tools, get_handler - -__all__ = ["get_all_tools", "get_handler"] diff --git a/sdks/mcp/src/openrag_mcp/tools/chat.py b/sdks/mcp/src/openrag_mcp/tools/chat.py deleted file mode 100644 index 13199460c..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/chat.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Chat tool for OpenRAG MCP server.""" - -import logging - -from mcp.types import TextContent, Tool - -from openrag_sdk import ( - AuthenticationError, - OpenRAGError, - RateLimitError, - ServerError, - ValidationError, -) - -from openrag_mcp.config import get_openrag_client -from openrag_mcp.tools.registry import register_tool - -logger = logging.getLogger("openrag-mcp.chat") - - -# Tool definition -CHAT_TOOL = Tool( - name="openrag_chat", - description=( - "Send a message to OpenRAG and get a RAG-enhanced response. " - "The response is informed by documents in your knowledge base. " - "Use chat_id to continue a previous conversation, or filter_id " - "to apply a knowledge filter." - ), - inputSchema={ - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Your question or message to send to OpenRAG", - }, - "chat_id": { - "type": "string", - "description": "Optional conversation ID to continue a previous chat", - }, - "filter_id": { - "type": "string", - "description": "Optional knowledge filter ID to apply", - }, - "limit": { - "type": "integer", - "description": "Maximum number of sources to retrieve (default: 10)", - "default": 10, - }, - "score_threshold": { - "type": "number", - "description": "Minimum relevance score threshold (default: 0)", - "default": 0, - }, - }, - "required": ["message"], - }, -) - - -async def handle_chat(arguments: dict) -> list[TextContent]: - """Handle openrag_chat tool calls.""" - message = arguments.get("message", "") - chat_id = arguments.get("chat_id") - filter_id = arguments.get("filter_id") - limit = arguments.get("limit", 10) - score_threshold = arguments.get("score_threshold", 0) - - if not message: - return [TextContent(type="text", text="Error: message is required")] - - try: - client = get_openrag_client() - response = await client.chat.create( - message=message, - chat_id=chat_id, - filter_id=filter_id, - limit=limit, - score_threshold=score_threshold, - ) - - # Build formatted response - output_parts = [response.response] - - if response.sources: - output_parts.append("\n\n---\n**Sources:**") - for i, source in enumerate(response.sources, 1): - output_parts.append(f"\n{i}. {source.filename} (relevance: {source.score:.2f})") - - if response.chat_id: - output_parts.append(f"\n\n_Chat ID: {response.chat_id}_") - - return [TextContent(type="text", text="".join(output_parts))] - - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except ValidationError as e: - logger.error(f"Validation error: {e.message}") - return [TextContent(type="text", text=f"Invalid request: {e.message}")] - except RateLimitError as e: - logger.error(f"Rate limit error: {e.message}") - return [TextContent(type="text", text=f"Rate limited: {e.message}")] - except ServerError as e: - logger.error(f"Server error: {e.message}") - return [TextContent(type="text", text=f"Server error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Chat error: {e}") - return [TextContent(type="text", text=f"Error: {str(e)}")] - - -# Register the tool -register_tool(CHAT_TOOL, handle_chat) diff --git a/sdks/mcp/src/openrag_mcp/tools/documents.py b/sdks/mcp/src/openrag_mcp/tools/documents.py deleted file mode 100644 index a1817aae7..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/documents.py +++ /dev/null @@ -1,367 +0,0 @@ -"""Document tools for OpenRAG MCP server.""" - -import logging -from pathlib import Path - -from mcp.types import TextContent, Tool - -from openrag_sdk import ( - AuthenticationError, - NotFoundError, - OpenRAGError, - RateLimitError, - ServerError, - ValidationError, -) - -from openrag_mcp.config import get_openrag_client -from openrag_mcp.tools.registry import register_tool - -logger = logging.getLogger("openrag-mcp.documents") - - -# ============================================================================ -# Tool: openrag_ingest_file -# ============================================================================ - -INGEST_FILE_TOOL = Tool( - name="openrag_ingest_file", - description=( - "Ingest a local file into the OpenRAG knowledge base. " - "Supported formats: PDF, DOCX, TXT, MD, HTML, and more. " - "By default waits for ingestion to complete. Set wait=false to return immediately." - ), - inputSchema={ - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Absolute path to the file to ingest", - }, - "wait": { - "type": "boolean", - "description": "Wait for ingestion to complete (default: true). Set to false to return immediately with task_id.", - "default": True, - }, - }, - "required": ["file_path"], - }, -) - - -async def handle_ingest_file(arguments: dict) -> list[TextContent]: - """Handle openrag_ingest_file tool calls.""" - file_path = arguments.get("file_path", "") - wait = arguments.get("wait", True) - - if not file_path: - return [TextContent(type="text", text="Error: file_path is required")] - - path = Path(file_path) - - if not path.exists(): - return [TextContent(type="text", text=f"Error: File not found: {file_path}")] - - if not path.is_file(): - return [TextContent(type="text", text=f"Error: Path is not a file: {file_path}")] - - try: - client = get_openrag_client() - response = await client.documents.ingest(file_path=path, wait=wait) - - if wait: - status = response.status - successful = response.successful_files - failed = response.failed_files - - if status == "completed": - result = f"Successfully ingested '{path.name}'." - result += f"\nStatus: {status}" - result += f"\nSuccessful files: {successful}" - if failed > 0: - result += f"\nFailed files: {failed}" - else: - result = f"Ingestion finished with status: {status}" - result += f"\nSuccessful files: {successful}" - result += f"\nFailed files: {failed}" - else: - result = f"Successfully queued '{response.filename or path.name}' for ingestion." - if response.task_id: - result += f"\nTask ID: {response.task_id}" - result += "\n\nUse openrag_get_task_status or openrag_wait_for_task to check progress." - - return [TextContent(type="text", text=result)] - - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except ValidationError as e: - logger.error(f"Validation error: {e.message}") - return [TextContent(type="text", text=f"Invalid request: {e.message}")] - except RateLimitError as e: - logger.error(f"Rate limit error: {e.message}") - return [TextContent(type="text", text=f"Rate limited: {e.message}")] - except ServerError as e: - logger.error(f"Server error: {e.message}") - return [TextContent(type="text", text=f"Server error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except TimeoutError as e: - logger.error(f"Timeout error: {e}") - return [TextContent(type="text", text=f"Ingestion timed out: {str(e)}")] - except Exception as e: - logger.error(f"Ingest file error: {e}") - return [TextContent(type="text", text=f"Error ingesting file: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_ingest_url -# ============================================================================ - -INGEST_URL_TOOL = Tool( - name="openrag_ingest_url", - description=( - "Ingest content from a URL into the OpenRAG knowledge base. " - "The URL content will be fetched, processed, and stored." - ), - inputSchema={ - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch and ingest", - }, - }, - "required": ["url"], - }, -) - - -async def handle_ingest_url(arguments: dict) -> list[TextContent]: - """Handle openrag_ingest_url tool calls.""" - url = arguments.get("url", "") - - if not url: - return [TextContent(type="text", text="Error: url is required")] - - if not url.startswith(("http://", "https://")): - return [TextContent(type="text", text="Error: url must start with http:// or https://")] - - try: - client = get_openrag_client() - response = await client.chat.create( - message=f"Please ingest the content from this URL into the knowledge base: {url}", - ) - - return [TextContent(type="text", text=f"URL ingestion requested.\n\n{response.response}")] - - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except ServerError as e: - logger.error(f"Server error: {e.message}") - return [TextContent(type="text", text=f"Server error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Ingest URL error: {e}") - return [TextContent(type="text", text=f"Error ingesting URL: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_get_task_status -# ============================================================================ - -GET_TASK_STATUS_TOOL = Tool( - name="openrag_get_task_status", - description=( - "Check the status of an ingestion task. " - "Use the task_id returned from openrag_ingest_file when wait=false." - ), - inputSchema={ - "type": "object", - "properties": { - "task_id": { - "type": "string", - "description": "The task ID to check status for", - }, - }, - "required": ["task_id"], - }, -) - - -async def handle_get_task_status(arguments: dict) -> list[TextContent]: - """Handle openrag_get_task_status tool calls.""" - task_id = arguments.get("task_id", "") - - if not task_id: - return [TextContent(type="text", text="Error: task_id is required")] - - try: - client = get_openrag_client() - status = await client.documents.get_task_status(task_id) - - output_parts = [f"**Task Status: {status.status}**"] - output_parts.append(f"\nTask ID: {status.task_id}") - output_parts.append(f"\nTotal files: {status.total_files}") - output_parts.append(f"\nProcessed: {status.processed_files}") - output_parts.append(f"\nSuccessful: {status.successful_files}") - output_parts.append(f"\nFailed: {status.failed_files}") - - if status.files: - output_parts.append("\n\n**File Details:**") - for filename, file_status in status.files.items(): - output_parts.append(f"\n- {filename}: {file_status}") - - return [TextContent(type="text", text="".join(output_parts))] - - except NotFoundError as e: - logger.error(f"Task not found: {e.message}") - return [TextContent(type="text", text=f"Task not found: {e.message}")] - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Get task status error: {e}") - return [TextContent(type="text", text=f"Error getting task status: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_wait_for_task -# ============================================================================ - -WAIT_FOR_TASK_TOOL = Tool( - name="openrag_wait_for_task", - description=( - "Wait for an ingestion task to complete. " - "Polls the task status until it completes or fails." - ), - inputSchema={ - "type": "object", - "properties": { - "task_id": { - "type": "string", - "description": "The task ID to wait for", - }, - "timeout": { - "type": "number", - "description": "Maximum seconds to wait (default: 300)", - "default": 300, - }, - }, - "required": ["task_id"], - }, -) - - -async def handle_wait_for_task(arguments: dict) -> list[TextContent]: - """Handle openrag_wait_for_task tool calls.""" - task_id = arguments.get("task_id", "") - timeout = arguments.get("timeout", 300) - - if not task_id: - return [TextContent(type="text", text="Error: task_id is required")] - - try: - client = get_openrag_client() - status = await client.documents.wait_for_task(task_id, timeout=timeout) - - output_parts = [f"**Task Completed: {status.status}**"] - output_parts.append(f"\nTask ID: {status.task_id}") - output_parts.append(f"\nTotal files: {status.total_files}") - output_parts.append(f"\nSuccessful: {status.successful_files}") - output_parts.append(f"\nFailed: {status.failed_files}") - - if status.files: - output_parts.append("\n\n**File Details:**") - for filename, file_status in status.files.items(): - output_parts.append(f"\n- {filename}: {file_status}") - - return [TextContent(type="text", text="".join(output_parts))] - - except TimeoutError as e: - logger.error(f"Wait for task timeout: {e}") - return [TextContent(type="text", text=f"Task did not complete within {timeout} seconds.")] - except NotFoundError as e: - logger.error(f"Task not found: {e.message}") - return [TextContent(type="text", text=f"Task not found: {e.message}")] - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Wait for task error: {e}") - return [TextContent(type="text", text=f"Error waiting for task: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_delete_document -# ============================================================================ - -DELETE_DOCUMENT_TOOL = Tool( - name="openrag_delete_document", - description="Delete a document from the OpenRAG knowledge base.", - inputSchema={ - "type": "object", - "properties": { - "filename": { - "type": "string", - "description": "Name of the file to delete", - }, - }, - "required": ["filename"], - }, -) - - -async def handle_delete_document(arguments: dict) -> list[TextContent]: - """Handle openrag_delete_document tool calls.""" - filename = arguments.get("filename", "") - - if not filename: - return [TextContent(type="text", text="Error: filename is required")] - - try: - client = get_openrag_client() - response = await client.documents.delete(filename) - - if response.success: - return [TextContent( - type="text", - text=f"Successfully deleted '{filename}' ({response.deleted_chunks} chunks removed).", - )] - else: - return [TextContent( - type="text", - text=f"Failed to delete '{filename}'.", - )] - - except NotFoundError as e: - logger.error(f"Document not found: {e.message}") - return [TextContent(type="text", text=f"Document not found: {e.message}")] - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except ServerError as e: - logger.error(f"Server error: {e.message}") - return [TextContent(type="text", text=f"Server error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Delete document error: {e}") - return [TextContent(type="text", text=f"Error deleting document: {str(e)}")] - - -# ============================================================================ -# Register all tools -# ============================================================================ -#NOTE: Ingest Tools are disabled in OpenRAGMCP currently. \ No newline at end of file diff --git a/sdks/mcp/src/openrag_mcp/tools/registry.py b/sdks/mcp/src/openrag_mcp/tools/registry.py deleted file mode 100644 index 851496abc..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/registry.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Tool registry for OpenRAG MCP server.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass - -from mcp.types import TextContent, Tool - -# Type alias for tool handlers -ToolHandler = Callable[[dict], Awaitable[list[TextContent]]] - - -@dataclass -class ToolEntry: - """A tool definition with its handler.""" - tool: Tool - handler: ToolHandler - - -# Global registry: tool_name -> ToolEntry -_registry: dict[str, ToolEntry] = {} - - -def register_tool(tool: Tool, handler: ToolHandler) -> None: - """Register a tool with its handler.""" - _registry[tool.name] = ToolEntry(tool=tool, handler=handler) - - -def get_all_tools() -> list[Tool]: - """Get all registered tools.""" - return [entry.tool for entry in _registry.values()] - - -def get_handler(name: str) -> ToolHandler | None: - """Get the handler for a tool by name.""" - entry = _registry.get(name) - return entry.handler if entry else None - diff --git a/sdks/mcp/src/openrag_mcp/tools/search.py b/sdks/mcp/src/openrag_mcp/tools/search.py deleted file mode 100644 index 2f87c8ef0..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/search.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Search tool for OpenRAG MCP server.""" - -import logging - -from mcp.types import TextContent, Tool - -from openrag_sdk import ( - AuthenticationError, - OpenRAGError, - RateLimitError, - SearchFilters, - ServerError, - ValidationError, -) - -from openrag_mcp.config import get_openrag_client -from openrag_mcp.tools.registry import register_tool - -logger = logging.getLogger("openrag-mcp.search") - - -# Tool definition -SEARCH_TOOL = Tool( - name="openrag_search", - description=( - "Search the OpenRAG knowledge base using semantic search. " - "Returns matching document chunks with relevance scores. " - "Optionally filter by data sources or document types." - ), - inputSchema={ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query", - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - }, - "score_threshold": { - "type": "number", - "description": "Minimum relevance score threshold (default: 0)", - "default": 0, - }, - "filter_id": { - "type": "string", - "description": "Optional knowledge filter ID to apply", - }, - "data_sources": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of filenames to filter by", - }, - "document_types": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of MIME types to filter by (e.g., 'application/pdf')", - }, - }, - "required": ["query"], - }, -) - - -async def handle_search(arguments: dict) -> list[TextContent]: - """Handle openrag_search tool calls.""" - query = arguments.get("query", "") - limit = arguments.get("limit", 10) - score_threshold = arguments.get("score_threshold", 0) - filter_id = arguments.get("filter_id") - data_sources = arguments.get("data_sources") - document_types = arguments.get("document_types") - - if not query: - return [TextContent(type="text", text="Error: query is required")] - - try: - client = get_openrag_client() - - # Build filters if provided - filters = None - if data_sources or document_types: - filters = SearchFilters( - data_sources=data_sources, - document_types=document_types, - ) - - response = await client.search.query( - query=query, - limit=limit, - score_threshold=score_threshold, - filter_id=filter_id, - filters=filters, - ) - - if not response.results: - return [TextContent(type="text", text="No results found.")] - - # Format results - output_parts = [f"Found {len(response.results)} result(s):\n"] - - for i, result in enumerate(response.results, 1): - output_parts.append(f"\n---\n**{i}. {result.filename}**") - if result.page: - output_parts.append(f" (page {result.page})") - output_parts.append(f"\nRelevance: {result.score:.2f}\n") - - # Truncate long content - content = result.text - if len(content) > 500: - content = content[:500] + "..." - output_parts.append(f"\n{content}\n") - - return [TextContent(type="text", text="".join(output_parts))] - - except AuthenticationError as e: - logger.error(f"Authentication error: {e.message}") - return [TextContent(type="text", text=f"Authentication error: {e.message}")] - except ValidationError as e: - logger.error(f"Validation error: {e.message}") - return [TextContent(type="text", text=f"Invalid request: {e.message}")] - except RateLimitError as e: - logger.error(f"Rate limit error: {e.message}") - return [TextContent(type="text", text=f"Rate limited: {e.message}")] - except ServerError as e: - logger.error(f"Server error: {e.message}") - return [TextContent(type="text", text=f"Server error: {e.message}")] - except OpenRAGError as e: - logger.error(f"OpenRAG error: {e.message}") - return [TextContent(type="text", text=f"Error: {e.message}")] - except Exception as e: - logger.error(f"Search error: {e}") - return [TextContent(type="text", text=f"Error: {str(e)}")] - - -# Register the tool -register_tool(SEARCH_TOOL, handle_search) diff --git a/sdks/mcp/src/openrag_mcp/tools/settings.py b/sdks/mcp/src/openrag_mcp/tools/settings.py deleted file mode 100644 index d82cc226a..000000000 --- a/sdks/mcp/src/openrag_mcp/tools/settings.py +++ /dev/null @@ -1,266 +0,0 @@ -"""Settings and models tools for OpenRAG MCP server. - -Uses direct HTTP calls so these tools work with any OpenRAG backend, -without depending on SDK version (models client or settings model fields). -""" - -import logging - -from mcp.types import TextContent, Tool - -from openrag_mcp.config import get_client -from openrag_mcp.tools.registry import register_tool - -logger = logging.getLogger("openrag-mcp.settings") - -VALID_PROVIDERS = ("openai", "anthropic", "ollama", "watsonx") - -def _format_http_error(response) -> str: - """Extract error message from HTTP response.""" - try: - data = response.json() - return data.get("error", response.text) or f"HTTP {response.status_code}" - except Exception: - return response.text or f"HTTP {response.status_code}" - - -async def _request_get(path_with_api: str, path_without_api: str): - """GET request; try /api/v1/... first, then /v1/... on 404 (backend-only).""" - http = get_client() - response = await http.get(path_with_api) - if response.status_code == 404 and path_with_api.startswith("/api"): - response = await http.get(path_without_api) - return response - - -async def _request_post(path_with_api: str, path_without_api: str, json_body: dict): - """POST request; try /api/v1/... first, then /v1/... on 404.""" - http = get_client() - response = await http.post(path_with_api, json=json_body) - if response.status_code == 404 and path_with_api.startswith("/api"): - response = await http.post(path_without_api, json=json_body) - return response - - -# ============================================================================ -# Tool: openrag_get_settings -# ============================================================================ - -GET_SETTINGS_TOOL = Tool( - name="openrag_get_settings", - description=( - "Get the current OpenRAG configuration. Returns LLM provider and model, " - "embedding provider and model, chunk settings, document processing options " - "(table structure, OCR, picture descriptions), and system prompt." - ), - inputSchema={ - "type": "object", - "properties": {}, - }, -) - - -async def handle_get_settings(arguments: dict) -> list[TextContent]: - """Handle openrag_get_settings tool calls.""" - try: - response = await _request_get("/api/v1/settings", "/v1/settings") - if response.status_code != 200: - return [TextContent(type="text", text=_format_http_error(response))] - - data = response.json() - agent = data.get("agent") or {} - knowledge = data.get("knowledge") or {} - - parts = ["**Current OpenRAG settings**\n"] - parts.append("\n**Agent (LLM)**") - parts.append(f"\n- Provider: {agent.get('llm_provider') or '—'}") - parts.append(f"\n- Model: {agent.get('llm_model') or '—'}") - system_prompt = agent.get("system_prompt") - if system_prompt: - prompt_preview = ( - system_prompt[:200] + "..." if len(system_prompt) > 200 else system_prompt - ) - parts.append(f"\n- System prompt: {prompt_preview}") - parts.append("\n**Knowledge (embeddings & ingestion)**") - parts.append(f"\n- Embedding provider: {knowledge.get('embedding_provider') or '—'}") - parts.append(f"\n- Embedding model: {knowledge.get('embedding_model') or '—'}") - parts.append(f"\n- Chunk size: {knowledge.get('chunk_size', '—')}") - parts.append(f"\n- Chunk overlap: {knowledge.get('chunk_overlap', '—')}") - if "table_structure" in knowledge: - parts.append(f"\n- Table structure: {knowledge['table_structure']}") - if "ocr" in knowledge: - parts.append(f"\n- OCR: {knowledge['ocr']}") - if "picture_descriptions" in knowledge: - parts.append(f"\n- Picture descriptions: {knowledge['picture_descriptions']}") - - return [TextContent(type="text", text="".join(parts))] - - except Exception as e: - logger.error("Get settings error: %s", e) - return [TextContent(type="text", text=f"Error getting settings: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_update_settings -# ============================================================================ - -UPDATE_SETTINGS_TOOL = Tool( - name="openrag_update_settings", - description=( - "Update OpenRAG configuration. All parameters are optional; only provided " - "fields are changed. Use this to set LLM model, embedding model, chunk size/overlap, " - "system prompt, and document processing options (table structure, OCR, picture descriptions)." - ), - inputSchema={ - "type": "object", - "properties": { - "llm_provider": { - "type": "string", - "description": "LLM provider: openai, anthropic, watsonx, or ollama", - }, - "llm_model": { - "type": "string", - "description": "Language model name (e.g. gpt-4o, claude-sonnet-4)", - }, - "embedding_provider": { - "type": "string", - "description": "Embedding provider: openai, watsonx, or ollama", - }, - "embedding_model": { - "type": "string", - "description": "Embedding model name", - }, - "chunk_size": { - "type": "integer", - "description": "Chunk size for document ingestion", - }, - "chunk_overlap": { - "type": "integer", - "description": "Chunk overlap for document ingestion", - }, - "system_prompt": { - "type": "string", - "description": "System prompt for the agent", - }, - "table_structure": { - "type": "boolean", - "description": "Enable table structure extraction in documents", - }, - "ocr": { - "type": "boolean", - "description": "Enable OCR for images in documents", - }, - "picture_descriptions": { - "type": "boolean", - "description": "Enable picture/image descriptions", - }, - }, - }, -) - - -async def handle_update_settings(arguments: dict) -> list[TextContent]: - """Handle openrag_update_settings tool calls.""" - options = {k: v for k, v in arguments.items() if v is not None} - if not options: - return [TextContent(type="text", text="No settings to update. Provide at least one option.")] - - try: - response = await _request_post("/api/v1/settings", "/v1/settings", options) - if response.status_code != 200: - return [TextContent(type="text", text=_format_http_error(response))] - data = response.json() - message = data.get("message", "Configuration updated successfully") - return [TextContent(type="text", text=message)] - except Exception as e: - logger.error("Update settings error: %s", e) - return [TextContent(type="text", text=f"Error updating settings: {str(e)}")] - - -# ============================================================================ -# Tool: openrag_list_models -# ============================================================================ - -LIST_MODELS_TOOL = Tool( - name="openrag_list_models", - description=( - "List available language models and embedding models for a provider. " - "Use this before updating settings to see which model values are valid. " - "Provider must be configured in OpenRAG (API key or endpoint set in Settings)." - ), - inputSchema={ - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Provider to list models for: openai, anthropic, ollama, or watsonx", - "enum": list(VALID_PROVIDERS), - }, - }, - "required": ["provider"], - }, -) - - -async def handle_list_models(arguments: dict) -> list[TextContent]: - """Handle openrag_list_models tool calls.""" - provider = (arguments.get("provider") or "").lower() - if not provider: - return [TextContent(type="text", text="Error: provider is required")] - if provider not in VALID_PROVIDERS: - return [ - TextContent( - type="text", - text=f"Error: provider must be one of {', '.join(VALID_PROVIDERS)}", - ) - ] - - try: - response = await _request_get(f"/api/v1/models/{provider}", f"/v1/models/{provider}") - if response.status_code != 200: - return [TextContent(type="text", text=_format_http_error(response))] - - data = response.json() - language_models = data.get("language_models") or [] - embedding_models = data.get("embedding_models") or [] - - parts = [f"**Available models for {provider}**\n"] - parts.append("\n**Language models**") - if language_models: - for m in language_models: - default = " (default)" if m.get("default") else "" - parts.append(f"\n- {m.get('value', m.get('label', ''))}{default}") - else: - parts.append("\n- None") - parts.append("\n**Embedding models**") - if embedding_models: - for m in embedding_models: - default = " (default)" if m.get("default") else "" - parts.append(f"\n- {m.get('value', m.get('label', ''))}{default}") - else: - parts.append("\n- None") - - return [TextContent(type="text", text="".join(parts))] - - except AttributeError as e: - if "models" in str(e) and "OpenRAGClient" in str(e): - msg = ( - "You're running an old build of openrag-mcp. To fix:\n" - "1. Uninstall: uv pip uninstall openrag-mcp\n" - "2. In Cursor MCP config (~/.cursor/mcp.json) use: \"args\": [\"run\", \"--directory\", \"/Users/edwin.jose/Documents/openrag/sdks/mcp\", \"openrag-mcp\"]\n" - "3. Restart Cursor." - ) - return [TextContent(type="text", text=msg)] - raise - except Exception as e: - logger.error("List models error: %s", e) - return [TextContent(type="text", text=f"Error listing models: {str(e)}")] - - -# ============================================================================ -# Register all tools -# ============================================================================ - -register_tool(GET_SETTINGS_TOOL, handle_get_settings) -register_tool(UPDATE_SETTINGS_TOOL, handle_update_settings) -register_tool(LIST_MODELS_TOOL, handle_list_models) diff --git a/sdks/mcp/uv.lock b/sdks/mcp/uv.lock index 2878b5fc3..c223fd89b 100644 --- a/sdks/mcp/uv.lock +++ b/sdks/mcp/uv.lock @@ -2,766 +2,7 @@ version = 1 revision = 3 requires-python = ">=3.10" -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "mcp" -version = "1.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, -] - [[package]] name = "openrag-mcp" -version = "0.3.0rc4" +version = "0.3.0" source = { editable = "." } -dependencies = [ - { name = "mcp" }, - { name = "openrag-sdk" }, -] - -[package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.28.0" }, - { name = "mcp", specifier = ">=1.0.0" }, - { name = "openrag-sdk", specifier = "==0.3.0rc1" }, -] - -[[package]] -name = "openrag-sdk" -version = "0.3.0rc1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/17/4f5b489616d8a91e8782c725bd40944a6ed904384b10d63c58d0261909d4/openrag_sdk-0.3.0rc1.tar.gz", hash = "sha256:91e824ce9accbf2fb818d33b92f3743d72e59659be1ffe5938c5a83cfa8cc799", size = 15442, upload-time = "2026-04-09T18:50:15.94Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/5a/95add758ec61894089b94387896b41d53262ec846625dbc74e7c583aa764/openrag_sdk-0.3.0rc1-py3-none-any.whl", hash = "sha256:0905994e48aa279636013536087ab69d3f1c1a9895bf22602c4ddcd85a90d562", size = 16624, upload-time = "2026-04-09T18:50:14.861Z" }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/8b/54651ad49bce99a50fd61a7f19c2b6a79fbb072e693101fbb1194c362054/sse_starlette-3.0.4.tar.gz", hash = "sha256:5e34286862e96ead0eb70f5ddd0bd21ab1f6473a8f44419dd267f431611383dd", size = 22576, upload-time = "2025-12-14T16:22:52.493Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/22/8ab1066358601163e1ac732837adba3672f703818f693e179b24e0d3b65c/sse_starlette-3.0.4-py3-none-any.whl", hash = "sha256:32c80ef0d04506ced4b0b6ab8fe300925edc37d26f666afb1874c754895f5dc3", size = 11764, upload-time = "2025-12-14T16:22:51.453Z" }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] From 67eb292ef54a9ae42e4fe770dda734a17892b130 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 22 May 2026 15:26:29 -0500 Subject: [PATCH 09/14] fix: connector sync and override feature (#1663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add filename-based duplicate handling for connectors Add end-to-end support for filename-based duplicate handling on connector ingests. Frontend: send a new replace_duplicates flag with connector sync requests, perform a pre-sync duplicate check, and show a DuplicateHandlingDialog that lets users overwrite or skip duplicates when uploading from provider UI. Backend: propagate replace_duplicates through connector_router, request models, and connector services into the file processors. ConnectorFileProcessor and LangflowConnectorFileProcessor now check whether a filename already exists in the index and either fail the file task or delete the existing document before ingesting when replace_duplicates is true. Utilities/tests: clean_connector_filename now preserves original spacing/slashes and only enforces MIME-mapped extensions; get_filename_aliases adds underscore/sanitized variants so lookups match connector-indexed names. Add unit tests covering filename dedupe logic and filename alias behavior. * Use duplicateNames list and display names Replace numeric duplicateCount with a duplicateNames string[] across upload and dropdown flows so the UI can show the actual file names that would be overwritten. The duplicate-handling dialog now accepts duplicateNames, derives an effective count, and lists up to 5 duplicate filenames with an "… and N more" indicator; message labels and button text use the effective count. Toast messages and pending state in upload/[provider]/page.tsx and knowledge-dropdown.tsx were updated to pass and consume duplicateNames and to use duplicateNames.length for counts. * Update page.tsx * style: ruff autofix (auto) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../app/api/mutations/useSyncConnector.ts | 2 + frontend/app/upload/[provider]/page.tsx | 127 ++++++++- .../components/duplicate-handling-dialog.tsx | 39 ++- frontend/components/knowledge-dropdown.tsx | 23 +- src/api/connector_router.py | 2 + src/api/connectors.py | 5 + src/connectors/langflow_connector_service.py | 2 + src/connectors/service.py | 2 + src/models/processors.py | 36 ++- src/utils/file_utils.py | 29 +- ...est_connector_processor_filename_dedupe.py | 250 ++++++++++++++++++ .../unit/test_file_utils_filename_aliases.py | 81 ++++++ 12 files changed, 551 insertions(+), 47 deletions(-) create mode 100644 tests/unit/test_connector_processor_filename_dedupe.py create mode 100644 tests/unit/test_file_utils_filename_aliases.py diff --git a/frontend/app/api/mutations/useSyncConnector.ts b/frontend/app/api/mutations/useSyncConnector.ts index e40508211..214133429 100644 --- a/frontend/app/api/mutations/useSyncConnector.ts +++ b/frontend/app/api/mutations/useSyncConnector.ts @@ -72,6 +72,8 @@ const syncConnector = async ({ sync_all?: boolean; /** Restrict ingest to these bucket names (IBM COS). */ bucket_filter?: string[]; + /** When true, replace any indexed document with the same filename. */ + replace_duplicates?: boolean; }; }): Promise => { const response = await fetch(`/api/connectors/${connectorType}/sync`, { diff --git a/frontend/app/upload/[provider]/page.tsx b/frontend/app/upload/[provider]/page.tsx index eae4389b5..a4112d86c 100644 --- a/frontend/app/upload/[provider]/page.tsx +++ b/frontend/app/upload/[provider]/page.tsx @@ -9,7 +9,7 @@ import { RefreshCw, } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { toast } from "sonner"; import { useSyncConnector } from "@/app/api/mutations/useSyncConnector"; import { useGetConnectorsQuery } from "@/app/api/queries/useGetConnectorsQuery"; @@ -19,6 +19,7 @@ import { useS3BucketStatusQuery } from "@/app/api/queries/useS3BucketStatusQuery import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker"; import { IngestSettings } from "@/components/cloud-picker/ingest-settings"; import { getIngestChunkSettingsError } from "@/components/cloud-picker/types"; +import { DuplicateHandlingDialog } from "@/components/duplicate-handling-dialog"; import { FileBrowserDialog } from "@/components/file-browser-dialog"; import { Button } from "@/components/ui/button"; import { @@ -28,6 +29,7 @@ import { } from "@/components/ui/tooltip"; import { useTask } from "@/contexts/task-context"; import { useSessionIngestSettings } from "@/hooks/useSessionIngestSettings"; +import { duplicateCheck } from "@/lib/upload-utils"; // Connectors that sync entire buckets/repositories without a file picker const DIRECT_SYNC_PROVIDERS = ["ibm_cos", "aws_s3"]; @@ -410,6 +412,15 @@ export default function UploadProviderPage() { const [selectedFiles, setSelectedFiles] = useState([]); const [ingestSettings, setIngestSettings] = useSessionIngestSettings(); + const [isCheckingDuplicates, setIsCheckingDuplicates] = useState(false); + const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false); + const [pendingSync, setPendingSync] = useState<{ + connector: { connectionId?: string; type: string }; + allFiles: CloudFile[]; + nonDuplicateFiles: CloudFile[]; + duplicateNames: string[]; + } | null>(null); + const isOverwriteConfirmedRef = useRef(false); const accessToken = tokenData?.access_token || null; const isLoading = @@ -427,21 +438,17 @@ export default function UploadProviderPage() { setSelectedFiles(files); }; - const handleSync = (connector: { connectionId?: string; type: string }) => { - if (!connector.connectionId || selectedFiles.length === 0) return; - - const chunkErr = getIngestChunkSettingsError(ingestSettings); - if (chunkErr) { - toast.error("Could not start ingest", { description: chunkErr }); - return; - } - + const submitSync = ( + connector: { connectionId?: string; type: string }, + files: CloudFile[], + replaceDuplicates: boolean, + ) => { syncMutation.mutate( { connectorType: connector.type, body: { connection_id: connector.connectionId, - selected_files: selectedFiles.map((file) => ({ + selected_files: files.map((file) => ({ id: file.id, name: file.name, mimeType: file.mimeType, @@ -450,6 +457,7 @@ export default function UploadProviderPage() { isFolder: file.isFolder, })), settings: ingestSettings, + replace_duplicates: replaceDuplicates, }, }, { @@ -467,6 +475,89 @@ export default function UploadProviderPage() { ); }; + const handleSync = async (connector: { + connectionId?: string; + type: string; + }) => { + if (!connector.connectionId || selectedFiles.length === 0) return; + + const chunkErr = getIngestChunkSettingsError(ingestSettings); + if (chunkErr) { + toast.error("Could not start ingest", { description: chunkErr }); + return; + } + + setIsCheckingDuplicates(true); + try { + const results = await Promise.all( + selectedFiles.map(async (file) => { + if (file.isFolder) return { file, isDuplicate: false }; + try { + const fakeFile = new File([], file.name); + const { exists } = await duplicateCheck(fakeFile); + return { file, isDuplicate: exists }; + } catch (err) { + console.error( + `[Connector Sync] Duplicate check failed for ${file.name}:`, + err, + ); + return { file, isDuplicate: false }; + } + }), + ); + + const duplicates = results.filter((r) => r.isDuplicate); + const nonDuplicates = results + .filter((r) => !r.isDuplicate) + .map((r) => r.file); + + if (duplicates.length === 0) { + submitSync(connector, selectedFiles, false); + return; + } + + setPendingSync({ + connector, + allFiles: selectedFiles, + nonDuplicateFiles: nonDuplicates, + duplicateNames: duplicates.map((r) => r.file.name), + }); + setDuplicateDialogOpen(true); + } finally { + setIsCheckingDuplicates(false); + } + }; + + const handleOverwriteDuplicates = () => { + if (!pendingSync) return; + isOverwriteConfirmedRef.current = true; + const { connector, allFiles } = pendingSync; + submitSync(connector, allFiles, true); + setPendingSync(null); + }; + + const handleDuplicateDialogOpenChange = (open: boolean) => { + if (!open && pendingSync) { + if (isOverwriteConfirmedRef.current) { + // Overwrite already submitted in handleOverwriteDuplicates; this close + // event fires immediately after and would otherwise re-enter the + // "skip duplicates" branch. + isOverwriteConfirmedRef.current = false; + } else { + const { connector, nonDuplicateFiles, duplicateNames } = pendingSync; + if (nonDuplicateFiles.length > 0) { + submitSync(connector, nonDuplicateFiles, false); + } else { + toast.info( + `All ${duplicateNames.length} selected file(s) already exist. Nothing was synced.`, + ); + } + } + setPendingSync(null); + } + setDuplicateDialogOpen(open); + }; + const getProviderDisplayName = () => { const nameMap: { [key: string]: string } = { google_drive: "Google Drive", @@ -652,8 +743,10 @@ export default function UploadProviderPage() { className="bg-foreground text-background hover:bg-foreground/90 font-semibold" variant={!hasSelectedFiles ? "secondary" : undefined} onClick={() => handleSync(connector)} - loading={isIngesting} - disabled={!hasSelectedFiles || isIngesting} + loading={isIngesting || isCheckingDuplicates} + disabled={ + !hasSelectedFiles || isIngesting || isCheckingDuplicates + } > {hasSelectedFiles ? ( <> @@ -673,6 +766,14 @@ export default function UploadProviderPage() {
+ + ); } diff --git a/frontend/components/duplicate-handling-dialog.tsx b/frontend/components/duplicate-handling-dialog.tsx index a19faabc4..76b5a84c9 100644 --- a/frontend/components/duplicate-handling-dialog.tsx +++ b/frontend/components/duplicate-handling-dialog.tsx @@ -19,8 +19,11 @@ interface DuplicateHandlingDialogProps { isLoading?: boolean; duplicateLabel?: string; duplicateCount?: number; + duplicateNames?: string[]; } +const MAX_LISTED_DUPLICATES = 5; + export const DuplicateHandlingDialog: React.FC< DuplicateHandlingDialogProps > = ({ @@ -30,27 +33,40 @@ export const DuplicateHandlingDialog: React.FC< isLoading = false, duplicateLabel, duplicateCount, + duplicateNames, }) => { const handleOverwrite = async () => { await onOverwrite(); onOpenChange(false); }; + const namesProvided = duplicateNames && duplicateNames.length > 0; + const effectiveCount = namesProvided + ? duplicateNames!.length + : duplicateCount; + const description = - typeof duplicateCount === "number" - ? duplicateCount === 1 + typeof effectiveCount === "number" + ? effectiveCount === 1 ? "1 duplicate document already exists. Overwriting will replace the existing document version. This can't be undone." - : `${duplicateCount} duplicate documents already exist. Overwriting will replace the existing document versions. This can't be undone.` + : `${effectiveCount} duplicate documents already exist. Overwriting will replace the existing document versions. This can't be undone.` : duplicateLabel ? `A document named "${duplicateLabel}" already exists. Overwriting will replace the existing document version. This can't be undone.` : "Overwriting will replace the existing document with another version. This can't be undone."; const overwriteLabel = - typeof duplicateCount === "number" ? "Overwrite duplicates" : "Overwrite"; + typeof effectiveCount === "number" ? "Overwrite duplicates" : "Overwrite"; const cancelLabel = - typeof duplicateCount === "number" + typeof effectiveCount === "number" ? "Skip duplicates & continue" : "Cancel"; + const visibleNames = namesProvided + ? duplicateNames!.slice(0, MAX_LISTED_DUPLICATES) + : []; + const remainingCount = namesProvided + ? duplicateNames!.length - visibleNames.length + : 0; + return ( @@ -61,6 +77,19 @@ export const DuplicateHandlingDialog: React.FC< + {namesProvided && ( +
    + {visibleNames.map((name) => ( +
  • + {name} +
  • + ))} + {remainingCount > 0 && ( +
  • … and {remainingCount} more
  • + )} +
+ )} +