From 7cfc762c049684333a1ff96b474b6cdb052a6709 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 04:51:11 +0900 Subject: [PATCH 01/12] =?UTF-8?q?remove:=20=E3=82=AB=E3=83=AC=E3=83=B3?= =?UTF-8?q?=E3=83=80=E3=83=BC=E6=A9=9F=E8=83=BD=E3=82=92=E5=85=A8=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 右側カレンダーパネルとその設定・i18n・react-day-picker依存を取り除く。 期日(dueDate)の入力・表示・パース・通知・並び替えは従来どおり。 --- package.json | 1 - pnpm-lock.yaml | 26 --- src/App.tsx | 14 +- src/components/CalendarPanel.tsx | 271 ------------------------------- src/components/SettingsModal.tsx | 3 +- src/components/TaskInput.tsx | 15 -- src/components/TaskItem.tsx | 3 +- src/context/UiContext.tsx | 10 -- src/i18n.ts | 12 -- src/main.tsx | 1 - src/styles.css | 19 --- src/types.ts | 2 - 12 files changed, 5 insertions(+), 372 deletions(-) delete mode 100644 src/components/CalendarPanel.tsx diff --git a/package.json b/package.json index 2186004..eb1bb4c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@tauri-apps/plugin-opener": "^2", "date-fns": "^4.4.0", "react": "^19.1.0", - "react-day-picker": "^10.0.1", "react-dom": "^19.1.0", "uuid": "^14.0.0", "zustand": "^5.0.14" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8db176..f749d0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,9 +38,6 @@ importers: react: specifier: ^19.1.0 version: 19.2.6 - react-day-picker: - specifier: ^10.0.1 - version: 10.0.1(@types/react@19.2.15)(react@19.2.6) react-dom: specifier: ^19.1.0 version: 19.2.6(react@19.2.6) @@ -171,9 +168,6 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@date-fns/tz@1.5.0': - resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} - '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -929,16 +923,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-day-picker@10.0.1: - resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==} - engines: {node: '>=18'} - peerDependencies: - '@types/react': '>=16.8.0' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: @@ -1217,8 +1201,6 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@date-fns/tz@1.5.0': {} - '@dnd-kit/accessibility@3.1.1(react@19.2.6)': dependencies: react: 19.2.6 @@ -1797,14 +1779,6 @@ snapshots: queue-microtask@1.2.3: {} - react-day-picker@10.0.1(@types/react@19.2.15)(react@19.2.6): - dependencies: - '@date-fns/tz': 1.5.0 - date-fns: 4.4.0 - react: 19.2.6 - optionalDependencies: - '@types/react': 19.2.15 - react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 diff --git a/src/App.tsx b/src/App.tsx index f7d9134..7ea1321 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import { t } from "./i18n"; import { buildDfsOrder, getDepth, hasUndoneDescendants } from "./utils/taskTree"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { LogicalSize } from "@tauri-apps/api/dpi"; -import { CalendarPanel } from "./components/CalendarPanel"; import { listen } from "@tauri-apps/api/event"; import { openUrl } from "@tauri-apps/plugin-opener"; import { CloseConfirmModal } from "./components/CloseConfirmModal"; @@ -26,7 +25,6 @@ function AppInner() { const loaded = useStore((s) => s.loaded); const grouping = useStore((s) => s.settings.groupingEnabled); const tagsEnabled = useStore((s) => s.settings.tagsEnabled ?? false); - const calendarEnabled = useStore((s) => s.settings.calendarEnabled ?? false); const tags = useStore((s) => s.tags); const lang = useStore((s) => s.settings.language); const lastFocusMinutes = useStore((s) => s.settings.lastFocusMinutes); @@ -182,12 +180,11 @@ function AppInner() { const scaleFactor = await win.scaleFactor(); const logical = (await win.innerSize()).toLogical(scaleFactor); const DEFAULT_WIDTH = 480; - const targetWidth = calendarEnabled ? Math.round(DEFAULT_WIDTH * 2.5) : DEFAULT_WIDTH; - if (Math.round(logical.width) !== targetWidth) { - await win.setSize(new LogicalSize(targetWidth, logical.height)); + if (Math.round(logical.width) !== DEFAULT_WIDTH) { + await win.setSize(new LogicalSize(DEFAULT_WIDTH, logical.height)); } })(); - }, [calendarEnabled]); + }, []); useEffect(() => { if (!loaded) return; @@ -777,11 +774,6 @@ function AppInner() { {!focusedTaskId && } - {calendarEnabled && ( -
- -
- )} setShowSettings(false)} /> {showShortcuts && setShowShortcuts(false)} />} diff --git a/src/components/CalendarPanel.tsx b/src/components/CalendarPanel.tsx deleted file mode 100644 index 5675043..0000000 --- a/src/components/CalendarPanel.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { useStore } from "../store"; -import { useUiContext } from "../context/UiContext"; -import { t } from "../i18n"; -import { dateToIso, DAY_NAMES_JA, DAY_NAMES_EN_SHORT } from "../utils/parseDate"; - -type ViewMode = "month" | "week"; - -function startOfWeek(date: Date): Date { - const d = new Date(date); - d.setHours(0, 0, 0, 0); - d.setDate(d.getDate() - d.getDay()); - return d; -} - -function addDays(date: Date, days: number): Date { - const d = new Date(date); - d.setDate(d.getDate() + days); - return d; -} - -function addMonths(date: Date, months: number): Date { - const d = new Date(date); - d.setMonth(d.getMonth() + months); - return d; -} - -function startOfMonth(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth(), 1); -} - -function endOfMonth(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth() + 1, 0); -} - -function isSameDay(a: Date, b: Date): boolean { - return ( - a.getFullYear() === b.getFullYear() && - a.getMonth() === b.getMonth() && - a.getDate() === b.getDate() - ); -} - -export function CalendarPanel() { - const tasks = useStore((s) => s.tasks); - const toggleTask = useStore((s) => s.toggleTask); - const lang = useStore((s) => s.settings.language); - const { setPendingMainInput, selectedTaskId, calendarJumpIso, setCalendarJumpIso } = useUiContext(); - - const [anchorDate, setAnchorDate] = useState(() => new Date()); - const [viewMode, setViewMode] = useState("month"); - const [weekdaysOnly, setWeekdaysOnly] = useState(false); - const today = new Date(); - - // タスク追加/更新で確定した期限にカレンダーの表示月をジャンプさせる - useEffect(() => { - if (!calendarJumpIso) return; - setAnchorDate(new Date(calendarJumpIso + "T00:00:00")); - setCalendarJumpIso(null); - }, [calendarJumpIso, setCalendarJumpIso]); - - const tasksByDate = useMemo(() => { - const map = new Map(); - for (const task of tasks) { - if (!task.dueDate) continue; - const list = map.get(task.dueDate) ?? []; - list.push(task); - map.set(task.dueDate, list); - } - return map; - }, [tasks]); - - const dayNames = - lang === "ja" - ? DAY_NAMES_JA - : DAY_NAMES_EN_SHORT.map((d) => d[0].toUpperCase() + d.slice(1)); - const visibleDayIndexes = weekdaysOnly ? [1, 2, 3, 4, 5] : [0, 1, 2, 3, 4, 5, 6]; - - const weeks = useMemo(() => { - if (viewMode === "week") { - return [Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(anchorDate), i))]; - } - const monthStart = startOfMonth(anchorDate); - const monthEnd = endOfMonth(anchorDate); - const gridStart = startOfWeek(monthStart); - const gridEnd = addDays(startOfWeek(monthEnd), 6); - const days: Date[] = []; - for (let cur = gridStart; cur <= gridEnd; cur = addDays(cur, 1)) { - days.push(cur); - } - const rows: Date[][] = []; - for (let i = 0; i < days.length; i += 7) rows.push(days.slice(i, i + 7)); - return rows; - }, [viewMode, anchorDate]); - - function goPrev() { - setAnchorDate((d) => (viewMode === "month" ? addMonths(d, -1) : addDays(d, -7))); - } - function goNext() { - setAnchorDate((d) => (viewMode === "month" ? addMonths(d, 1) : addDays(d, 7))); - } - function goToday() { - setAnchorDate(new Date()); - } - - function openMainInputFor(day: Date) { - setPendingMainInput(` ${day.getMonth() + 1}/${day.getDate()}`); - } - - const headerLabel = - viewMode === "month" - ? lang === "ja" - ? `${anchorDate.getFullYear()}年${anchorDate.getMonth() + 1}月` - : anchorDate.toLocaleDateString("en-US", { month: "long", year: "numeric" }) - : (() => { - const s = startOfWeek(anchorDate); - const e = addDays(s, 6); - return `${s.getMonth() + 1}/${s.getDate()} - ${e.getMonth() + 1}/${e.getDate()}`; - })(); - - return ( -
-
-
-
{headerLabel}
-
- - - -
-
-
-
- {(["month", "week"] as ViewMode[]).map((m) => ( - - ))} -
- -
-
- -
- {visibleDayIndexes.map((i) => ( -
- {dayNames[i]} -
- ))} -
- -
- {weeks.map((week, wi) => ( -
- {visibleDayIndexes.map((di) => { - const day = week[di]; - const iso = dateToIso(day); - const isCurrentMonth = viewMode === "week" || day.getMonth() === anchorDate.getMonth(); - const isToday = isSameDay(day, today); - const dayTasks = isToday - ? [ - ...(tasksByDate.get(iso) ?? []), - ...tasks.filter((tk) => tk.isMinimum && tk.dueDate !== iso), - ] - : tasksByDate.get(iso) ?? []; - const visibleTasks = viewMode === "week" ? dayTasks : dayTasks.slice(0, 3); - const overflow = viewMode === "week" ? 0 : dayTasks.length - visibleTasks.length; - - return ( -
openMainInputFor(day)} - className={`${viewMode === "week" ? "h-full flex flex-col" : "min-h-[64px]"} rounded-md p-1 text-left cursor-text transition-colors ${ - isToday - ? "bg-black/[0.04] hover:bg-black/[0.06] dark:bg-white/[0.05] dark:hover:bg-white/[0.07]" - : "hover:bg-black/[0.03] dark:hover:bg-white/[0.04]" - } ${isCurrentMonth ? "" : "opacity-40"}`} - > -
{day.getDate()}
-
- {visibleTasks.map((task) => { - const label = - task.projectName && task.title.startsWith(`:${task.projectName} `) - ? task.title.slice(task.projectName.length + 2) - : task.title; - return ( -
e.stopPropagation()} - className={`flex items-center gap-1 rounded px-0.5 -mx-0.5 ${ - selectedTaskId === task.id ? "bg-black/10 dark:bg-white/15" : "" - }`} - > - - - {label} - -
- ); - })} - {overflow > 0 &&
+{overflow}
} -
-
- ); - })} -
- ))} -
-
- ); -} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index c0e19a2..f8a268d 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -10,7 +10,7 @@ interface Props { onClose: () => void; } -type ToggleKey = "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate" | "tagsEnabled" | "calendarEnabled" | "urlEnabled"; +type ToggleKey = "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate" | "tagsEnabled" | "urlEnabled"; const TOGGLES: Array<{ key: ToggleKey; label: TKey; desc: TKey }> = [ { key: "urlEnabled", label: "setting_urlEnabled", desc: "setting_urlEnabled_desc" }, @@ -18,7 +18,6 @@ const TOGGLES: Array<{ key: ToggleKey; label: TKey; desc: TKey }> = [ { key: "autoDeleteOldCompleted", label: "setting_autoDeleteOldCompleted", desc: "setting_autoDeleteOldCompleted_desc" }, { key: "showDueDate", label: "setting_showDueDate", desc: "setting_showDueDate_desc" }, { key: "tagsEnabled", label: "setting_tagsEnabled", desc: "setting_tagsEnabled_desc" }, - { key: "calendarEnabled", label: "setting_calendarEnabled", desc: "setting_calendarEnabled_desc" }, ]; function Toggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) { diff --git a/src/components/TaskInput.tsx b/src/components/TaskInput.tsx index e5d6b75..a9ba528 100644 --- a/src/components/TaskInput.tsx +++ b/src/components/TaskInput.tsx @@ -32,22 +32,8 @@ export function TaskInput() { searchQuery, setSearchMode, setSearchQuery, - pendingMainInput, - setPendingMainInput, - setCalendarJumpIso, } = useUiContext(); - // カレンダーの日付セルクリックで日付付きテキストを差し込み、日付の手前にカーソルを置く - useEffect(() => { - if (pendingMainInput === null) return; - setValue(pendingMainInput); - setPendingMainInput(null); - requestAnimationFrame(() => { - inputRef.current?.focus(); - inputRef.current?.setSelectionRange(0, 0); - }); - }, [pendingMainInput, setPendingMainInput]); - // mainInputRefとinputRefを同期 useEffect(() => { mainInputRef.current = inputRef.current; @@ -362,7 +348,6 @@ export function TaskInput() { const p = pendingSubmit; setPendingSubmit(null); finalizeSubmit(p.parts, p.iso, p.firstProject); - if (p.iso) setCalendarJumpIso(p.iso); }} /> )} diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index e7b3258..ac71586 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -34,7 +34,7 @@ export function TaskItem({ task, draggable = true }: Props) { const depth = useMemo(() => getDepth(task, allTasks), [task, allTasks]); const isLocked = useMemo(() => hasUndoneDescendants(task.id, allTasks), [task.id, allTasks]); - const { selectedTaskId, focusedTaskId, isReorderMode, bulkSelected, setSelectedTaskId, vibratingTaskId, setCalendarJumpIso, completingTaskIds } = useUiContext(); + const { selectedTaskId, focusedTaskId, isReorderMode, bulkSelected, setSelectedTaskId, vibratingTaskId, completingTaskIds } = useUiContext(); const isSelected = selectedTaskId === task.id; const isFocused = focusedTaskId === task.id; const isBulkSelected = bulkSelected.has(task.id); @@ -418,7 +418,6 @@ export function TaskItem({ task, draggable = true }: Props) { updateTask(task.id, pendingUpdate); setPendingUpdate(null); setEditing(false); - if (pendingUpdate.dueDate) setCalendarJumpIso(pendingUpdate.dueDate); }} /> )} diff --git a/src/context/UiContext.tsx b/src/context/UiContext.tsx index 76ef67d..8fdf95b 100644 --- a/src/context/UiContext.tsx +++ b/src/context/UiContext.tsx @@ -23,10 +23,6 @@ interface UiContextValue { mainInputRef: React.RefObject; vibratingTaskId: string | null; triggerVibration: (id: string) => void; - pendingMainInput: string | null; - setPendingMainInput: (text: string | null) => void; - calendarJumpIso: string | null; - setCalendarJumpIso: (iso: string | null) => void; completingTaskIds: Set; triggerCompleting: (ids: string[], onDone: () => void) => void; } @@ -45,8 +41,6 @@ export function UiProvider({ children }: { children: React.ReactNode }) { const mainInputRef = useRef(null); const [vibratingTaskId, setVibratingTaskId] = useState(null); const vibrateTimerRef = useRef | null>(null); - const [pendingMainInput, setPendingMainInput] = useState(null); - const [calendarJumpIso, setCalendarJumpIso] = useState(null); const [completingTaskIds, setCompletingTaskIds] = useState>(new Set()); function triggerVibration(id: string) { @@ -100,10 +94,6 @@ export function UiProvider({ children }: { children: React.ReactNode }) { mainInputRef, vibratingTaskId, triggerVibration, - pendingMainInput, - setPendingMainInput, - calendarJumpIso, - setCalendarJumpIso, completingTaskIds, triggerCompleting, }} diff --git a/src/i18n.ts b/src/i18n.ts index 8435a90..1a76a02 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -41,12 +41,6 @@ const dict = { setting_showDueDate_desc: "タスクに期日を設定・表示する", setting_tagsEnabled: "タグ機能", setting_tagsEnabled_desc: "タグの作成・タスクへの付与を有効にする", - setting_calendarEnabled: "カレンダー機能", - setting_calendarEnabled_desc: "右側にカレンダーパネルを表示する", - calendarViewMonth: "月", - calendarViewWeek: "週", - calendarWeekdaysOnly: "平日のみ", - calendarToday: "今日", tagFilter: "タグ絞込", theme: "テーマ", themeLight: "ライト", @@ -131,12 +125,6 @@ const dict = { setting_showDueDate_desc: "Show due date fields on tasks", setting_tagsEnabled: "Tags feature", setting_tagsEnabled_desc: "Enable tag creation and assignment", - setting_calendarEnabled: "Calendar feature", - setting_calendarEnabled_desc: "Show a calendar panel on the right", - calendarViewMonth: "Month", - calendarViewWeek: "Week", - calendarWeekdaysOnly: "Weekdays only", - calendarToday: "Today", tagFilter: "Tag filter", theme: "Theme", themeLight: "Light", diff --git a/src/main.tsx b/src/main.tsx index 9f95271..81da346 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./styles.css"; -import "react-day-picker/style.css"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/styles.css b/src/styles.css index f8b04b4..cae1874 100644 --- a/src/styles.css +++ b/src/styles.css @@ -78,10 +78,6 @@ html.dark .section-header { color: #98989D; } -html.dark .rdp { - color: #F5F5F7; -} - html.dark .scrollbar-thin::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); } @@ -163,21 +159,6 @@ html.dark .task-bulk-bg { pointer-events: none; } -/* react-day-picker minimal restyle */ -.rdp { - --rdp-accent-color: #1D1D1F; - --rdp-accent-background-color: rgba(0,0,0,0.08); - --rdp-background-color: rgba(0,0,0,0.08); - font-size: 13px; - margin: 0; -} - -html.dark .rdp { - --rdp-accent-color: #F5F5F7; - --rdp-accent-background-color: rgba(255,255,255,0.12); - --rdp-background-color: rgba(255,255,255,0.12); -} - @keyframes fade-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } diff --git a/src/types.ts b/src/types.ts index d44d65f..74f57dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,6 @@ export interface Settings { copyGroupingEnabled: boolean; showDueDate: boolean; tagsEnabled: boolean; - calendarEnabled: boolean; urlEnabled: boolean; focusShortcut: string; lastFocusMinutes: number; @@ -55,7 +54,6 @@ export const defaultSettings: Settings = { copyGroupingEnabled: true, showDueDate: false, tagsEnabled: false, - calendarEnabled: false, urlEnabled: true, focusShortcut: "", lastFocusMinutes: 25, From 9636739449f90c623d7525f8003112f273d1ad65 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 04:51:41 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20AI=E3=82=A8=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=83=B3=E3=83=88=E9=80=A3=E6=90=BA=E7=94=A8=E3=81=AE?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=ABAPI=E3=81=A8MCP=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=82=92=E5=85=AC=E9=96=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 127.0.0.1にバインドし、Bearerトークン必須のHTTP APIを追加。 GET/POST/PATCH/DELETE /tasks でタスクを操作でき、変更は external-data-changed イベントでフロントに反映される。 依存ゼロのMCP stdioブリッジを mcp/ に同梱。 --- mcp/README.md | 70 +++++++++ mcp/server.js | 135 ++++++++++++++++++ mcp/smoke.sh | 26 ++++ src-tauri/Cargo.lock | 61 ++++++-- src-tauri/Cargo.toml | 3 + src-tauri/src/api.rs | 327 +++++++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 4 + src/App.tsx | 14 ++ 8 files changed, 627 insertions(+), 13 deletions(-) create mode 100644 mcp/README.md create mode 100644 mcp/server.js create mode 100755 mcp/smoke.sh create mode 100644 src-tauri/src/api.rs diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..26acaa3 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,70 @@ +# FastTask MCP サーバ + +FastTask が起動時に立てるローカル HTTP API (127.0.0.1 のみ) を叩く、依存ゼロの MCP stdio ブリッジ。 + +必要条件: Node 18+ / FastTask アプリが起動していること。 + +## 仕組み + +FastTask は起動時に `app_data_dir/api.json` へ実ポートと認証トークンを書き出す。 + +- macOS: `~/Library/Application Support/com.fasttask.desktop/api.json` +- Linux: `~/.local/share/com.fasttask.desktop/api.json` +- Windows: `%APPDATA%\com.fasttask.desktop\api.json` + +MCP サーバはこのファイルを毎リクエスト読み直すので、設定は不要。環境変数 +`FASTTASK_PORT` / `FASTTASK_TOKEN` / `FASTTASK_APP_DATA_DIR` で上書きできる。 + +## 登録 + +Claude Code: + +```sh +claude mcp add fasttask -- node /absolute/path/to/FastTask/mcp/server.js +``` + +Claude Desktop (`claude_desktop_config.json`) / 手動設定: + +```json +{ + "mcpServers": { + "fasttask": { + "command": "node", + "args": ["/absolute/path/to/FastTask/mcp/server.js"] + } + } +} +``` + +## tools + +| tool | 引数 | +| --- | --- | +| `list_tasks` | `status?` (`todo` \| `done`) | +| `create_task` | `title` (必須), `dueDate?`, `url?`, `tags?` | +| `update_task` | `id` (必須), `title?`, `status?`, `dueDate?`, `url?`, `tags?` | +| `delete_task` | `id` (必須) — 子タスクも一緒に削除 | + +## HTTP API を直接叩く + +全リクエストに `Authorization: Bearer ` が必須 (無いと 401)。 + +```sh +BASE=http://127.0.0.1:$(sed -n 's/.*"port": *\([0-9]*\).*/\1/p' ~/Library/Application\ Support/com.fasttask.desktop/api.json) +TOKEN=$(sed -n 's/.*"token": *"\([^"]*\)".*/\1/p' ~/Library/Application\ Support/com.fasttask.desktop/api.json) + +curl "$BASE/tasks?status=todo" -H "Authorization: Bearer $TOKEN" +curl -X POST "$BASE/tasks" -H "Authorization: Bearer $TOKEN" -d '{"title":"資料を書く","dueDate":"2026-11-10"}' +curl -X PATCH "$BASE/tasks/" -H "Authorization: Bearer $TOKEN" -d '{"status":"done"}' +curl -X DELETE "$BASE/tasks/" -H "Authorization: Bearer $TOKEN" +``` + +## 動作確認 + +FastTask を起動した状態で: + +```sh +sh mcp/smoke.sh +``` + +トークン無し → 401、作成 → 一覧に出る、を確認して後片付けする。 diff --git a/mcp/server.js b/mcp/server.js new file mode 100644 index 0000000..91e810c --- /dev/null +++ b/mcp/server.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node +// FastTask MCP stdio bridge -> ローカルHTTP API。依存なし (Node 18+ の fetch を使用)。 +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { createInterface } from "node:readline"; + +const APP_DATA_DIR = + process.env.FASTTASK_APP_DATA_DIR || + (process.platform === "darwin" + ? join(homedir(), "Library", "Application Support", "com.fasttask.desktop") + : process.platform === "win32" + ? join(process.env.APPDATA || "", "com.fasttask.desktop") + : join(homedir(), ".local", "share", "com.fasttask.desktop")); + +function config() { + let port = process.env.FASTTASK_PORT; + let token = process.env.FASTTASK_TOKEN; + if (!port || !token) { + const f = JSON.parse(readFileSync(join(APP_DATA_DIR, "api.json"), "utf8")); + port ||= f.port; + token ||= f.token; + } + return { base: `http://127.0.0.1:${port}`, token }; +} + +async function api(method, path, body) { + const { base, token } = config(); + const res = await fetch(base + path, { + method, + headers: { + Authorization: `Bearer ${token}`, + ...(body ? { "Content-Type": "application/json" } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + if (!res.ok) throw new Error(`${res.status}: ${text}`); + return text; +} + +const TOOLS = [ + { + name: "list_tasks", + description: "FastTask のタスク一覧を取得する", + inputSchema: { + type: "object", + properties: { status: { type: "string", enum: ["todo", "done"] } }, + }, + run: (a) => api("GET", `/tasks${a.status ? `?status=${encodeURIComponent(a.status)}` : ""}`), + }, + { + name: "create_task", + description: "FastTask にタスクを作成する", + inputSchema: { + type: "object", + properties: { + title: { type: "string" }, + dueDate: { type: "string", description: "ISO 日付 (YYYY-MM-DD)" }, + url: { type: "string" }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["title"], + }, + run: (a) => api("POST", "/tasks", a), + }, + { + name: "update_task", + description: "タスクを部分更新する (title / status / dueDate / url / tags)", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + title: { type: "string" }, + status: { type: "string", enum: ["todo", "done"] }, + dueDate: { type: "string" }, + url: { type: "string" }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["id"], + }, + run: ({ id, ...patch }) => api("PATCH", `/tasks/${encodeURIComponent(id)}`, patch), + }, + { + name: "delete_task", + description: "タスクを削除する (子タスクも削除される)", + inputSchema: { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + }, + run: ({ id }) => api("DELETE", `/tasks/${encodeURIComponent(id)}`), + }, +]; + +function send(msg) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n"); +} + +async function handle(req) { + switch (req.method) { + case "initialize": + return { + protocolVersion: req.params?.protocolVersion || "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "fasttask", version: "0.1.0" }, + }; + case "tools/list": + return { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) }; + case "tools/call": { + const tool = TOOLS.find((t) => t.name === req.params?.name); + if (!tool) throw new Error(`unknown tool: ${req.params?.name}`); + const text = await tool.run(req.params.arguments || {}); + return { content: [{ type: "text", text }] }; + } + default: + throw new Error(`unknown method: ${req.method}`); + } +} + +createInterface({ input: process.stdin }).on("line", async (line) => { + if (!line.trim()) return; + let req; + try { + req = JSON.parse(line); + } catch { + return; + } + try { + const result = await handle(req); + if (req.id !== undefined) send({ id: req.id, result }); + } catch (e) { + if (req.id !== undefined) send({ id: req.id, error: { code: -32000, message: String(e.message || e) } }); + } +}); diff --git a/mcp/smoke.sh b/mcp/smoke.sh new file mode 100755 index 0000000..53e6f12 --- /dev/null +++ b/mcp/smoke.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# FastTask ローカルAPI の最小動作確認。FastTask を起動した状態で実行する。 +# sh mcp/smoke.sh +set -e +DIR="${FASTTASK_APP_DATA_DIR:-$HOME/Library/Application Support/com.fasttask.desktop}" +PORT=$(sed -n 's/.*"port": *\([0-9]*\).*/\1/p' "$DIR/api.json") +TOKEN=$(sed -n 's/.*"token": *"\([^"]*\)".*/\1/p' "$DIR/api.json") +BASE="http://127.0.0.1:$PORT" + +code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/tasks") +[ "$code" = "401" ] || { echo "FAIL: トークン無しが $code (401 期待)"; exit 1; } +echo "ok: トークン無し -> 401" + +TITLE="smoke-$(date +%s)" +ID=$(curl -s -X POST "$BASE/tasks" -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' -d "{\"title\":\"$TITLE\"}" \ + | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') +[ -n "$ID" ] || { echo "FAIL: 作成できなかった"; exit 1; } +echo "ok: 作成 id=$ID" + +curl -s "$BASE/tasks?status=todo" -H "Authorization: Bearer $TOKEN" | grep -q "$TITLE" \ + || { echo "FAIL: 一覧に出ない"; exit 1; } +echo "ok: 一覧に出る" + +curl -s -X DELETE "$BASE/tasks/$ID" -H "Authorization: Bearer $TOKEN" >/dev/null +echo "ok: 後片付け完了 — PASS" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b5d38f4..dd6e387 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "FastTask" version = "1.2.1" dependencies = [ + "chrono", "serde", "serde_json", "tauri", @@ -15,6 +16,8 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-notification", "tauri-plugin-opener", + "tiny_http", + "uuid", ] [[package]] @@ -83,6 +86,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -519,11 +528,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -794,7 +811,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -986,7 +1003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1633,6 +1650,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.10.1" @@ -1688,7 +1711,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.56.0", + "windows-core 0.61.2", ] [[package]] @@ -2200,7 +2223,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2299,7 +2322,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -2543,7 +2566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -3098,7 +3121,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3429,7 +3452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3986,10 +4009,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4087,6 +4110,18 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -4354,7 +4389,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4394,7 +4429,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4882,7 +4917,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 24625dc..ebee205 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,3 +21,6 @@ tauri-plugin-global-shortcut = "2" tauri-plugin-notification = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +tiny_http = "0.12" +uuid = { version = "1", features = ["v4"] } +chrono = "0.4" diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs new file mode 100644 index 0000000..ca790b0 --- /dev/null +++ b/src-tauri/src/api.rs @@ -0,0 +1,327 @@ +// AIエージェント連携用のローカルHTTP API (127.0.0.1 のみ)。 +use std::fs; +use std::io::Read; +use std::path::PathBuf; +use std::sync::Mutex; + +use serde_json::{json, Value}; +use tauri::{Emitter, Manager}; +use tiny_http::{Header, Request, Response, Server}; + +const MAX_BODY: usize = 1 << 20; // 1MiB +const PORT_MIN: u16 = 48730; +const PORT_MAX: u16 = 48760; + +// ponytail: プロセス内 Mutex で API 側の読み書きだけを直列化している。 +// 既知の上限: アプリ本体 (save_data) も同じ data.json を書くため、 +// フロントの保存と API の書き込みが重なると後勝ちで片方が失われうる。 +// 改善案: save_data とこの API を同一の共有 Mutex + ファイルロック(flock)経由にし、 +// 書き込み前に mtime を比較して競合を検出する。 +static DATA_LOCK: Mutex<()> = Mutex::new(()); + +struct Ctx { + app: tauri::AppHandle, + dir: PathBuf, + token: String, +} + +pub fn start(app: tauri::AppHandle) { + let dir = match app.path().app_data_dir() { + Ok(d) => d, + Err(e) => { + eprintln!("api: app_data_dir error: {e}"); + return; + } + }; + if let Err(e) = fs::create_dir_all(&dir) { + eprintln!("api: mkdir error: {e}"); + return; + } + + let (server, port) = match (PORT_MIN..PORT_MAX).find_map(|p| { + Server::http(("127.0.0.1", p)) + .ok() + .map(|s| (s, p)) + }) { + Some(v) => v, + None => { + eprintln!("api: no free port in {PORT_MIN}..{PORT_MAX}"); + return; + } + }; + + let token = load_or_create_token(&dir); + let info = dir.join("api.json"); + if let Err(e) = fs::write( + &info, + serde_json::to_string_pretty(&json!({ "port": port, "token": token })).unwrap(), + ) { + eprintln!("api: write api.json error: {e}"); + return; + } + + let ctx = Ctx { app, dir, token }; + std::thread::spawn(move || { + for req in server.incoming_requests() { + if let Err(e) = handle(&ctx, req) { + eprintln!("api: {e}"); + } + } + }); +} + +fn load_or_create_token(dir: &PathBuf) -> String { + let path = dir.join("api.json"); + if let Ok(s) = fs::read_to_string(&path) { + if let Ok(v) = serde_json::from_str::(&s) { + if let Some(t) = v.get("token").and_then(Value::as_str) { + if !t.is_empty() { + return t.to_string(); + } + } + } + } + format!( + "{}{}", + uuid::Uuid::new_v4().simple(), + uuid::Uuid::new_v4().simple() + ) +} + +fn respond(req: Request, status: u16, body: Value) -> std::io::Result<()> { + let header = Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(); + req.respond( + Response::from_string(body.to_string()) + .with_status_code(status) + .with_header(header), + ) +} + +fn handle(ctx: &Ctx, mut req: Request) -> std::io::Result<()> { + let authed = req + .headers() + .iter() + .any(|h| { + h.field.equiv("Authorization") + && h.value.as_str().strip_prefix("Bearer ") == Some(ctx.token.as_str()) + }); + if !authed { + return respond(req, 401, json!({ "error": "unauthorized" })); + } + + let (path, query) = { + let url = req.url().to_string(); + match url.split_once('?') { + Some((p, q)) => (p.to_string(), q.to_string()), + None => (url, String::new()), + } + }; + let method = req.method().as_str().to_string(); + + // ボディ読み込み (サイズ上限つき) + let mut body = String::new(); + if matches!(method.as_str(), "POST" | "PATCH") { + let len = req.body_length().unwrap_or(0); + if len > MAX_BODY { + return respond(req, 413, json!({ "error": "body too large" })); + } + if let Err(e) = req.as_reader().take(MAX_BODY as u64 + 1).read_to_string(&mut body) { + return respond(req, 400, json!({ "error": format!("read error: {e}") })); + } + if body.len() > MAX_BODY { + return respond(req, 413, json!({ "error": "body too large" })); + } + } + + let segs: Vec<&str> = path.trim_matches('/').split('/').collect(); + let result = match (method.as_str(), segs.as_slice()) { + ("GET", ["tasks"]) => list_tasks(ctx, &query), + ("POST", ["tasks"]) => create_task(ctx, &body), + ("PATCH", ["tasks", id]) => update_task(ctx, id, &body), + ("DELETE", ["tasks", id]) => delete_task(ctx, id), + _ => Err((404, "not found".to_string())), + }; + + match result { + Ok(v) => respond(req, 200, v), + Err((code, msg)) => respond(req, code, json!({ "error": msg })), + } +} + +type ApiResult = Result; + +fn data_path(ctx: &Ctx) -> PathBuf { + ctx.dir.join("data.json") +} + +fn read_data(ctx: &Ctx) -> Result { + let path = data_path(ctx); + if !path.exists() { + return Ok(json!({ "tasks": [], "tags": [], "version": "1" })); + } + let s = fs::read_to_string(&path).map_err(|e| (500, format!("read error: {e}")))?; + serde_json::from_str(&s).map_err(|e| (500, format!("parse error: {e}"))) +} + +fn write_data(ctx: &Ctx, data: &Value) -> Result<(), (u16, String)> { + let tmp = ctx.dir.join("data.json.tmp"); + let s = serde_json::to_string_pretty(data).map_err(|e| (500, format!("serialize: {e}")))?; + fs::write(&tmp, s).map_err(|e| (500, format!("write error: {e}")))?; + fs::rename(&tmp, data_path(ctx)).map_err(|e| (500, format!("rename error: {e}")))?; + let _ = ctx.app.emit("external-data-changed", ()); + Ok(()) +} + +fn tasks_of(data: &Value) -> Vec { + data.get("tasks") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default() +} + +fn list_tasks(ctx: &Ctx, query: &str) -> ApiResult { + let _g = DATA_LOCK.lock().unwrap(); + let data = read_data(ctx)?; + let status = query.split('&').find_map(|kv| kv.strip_prefix("status=")); + let mut tasks = tasks_of(&data); + if let Some(s) = status { + if s != "todo" && s != "done" { + return Err((400, "status must be todo or done".into())); + } + tasks.retain(|t| t.get("status").and_then(Value::as_str) == Some(s)); + } + Ok(json!({ "tasks": tasks })) +} + +fn parse_body(body: &str) -> Result { + let v: Value = serde_json::from_str(body).map_err(|e| (400, format!("invalid json: {e}")))?; + if !v.is_object() { + return Err((400, "body must be a JSON object".into())); + } + Ok(v) +} + +fn create_task(ctx: &Ctx, body: &str) -> ApiResult { + let b = parse_body(body)?; + let title = b + .get("title") + .and_then(Value::as_str) + .map(str::trim) + .filter(|s| !s.is_empty()) + .ok_or((400, "title is required".to_string()))?; + if title.chars().count() > 1000 { + return Err((400, "title too long".into())); + } + let tags = match b.get("tags") { + None | Some(Value::Null) => vec![], + Some(Value::Array(a)) if a.iter().all(Value::is_string) => a.clone(), + Some(_) => return Err((400, "tags must be an array of strings".into())), + }; + + let _g = DATA_LOCK.lock().unwrap(); + let mut data = read_data(ctx)?; + let mut tasks = tasks_of(&data); + let next_order = tasks + .iter() + .filter(|t| t.get("status").and_then(Value::as_str) == Some("todo")) + .filter_map(|t| t.get("order").and_then(Value::as_i64)) + .max() + .map_or(0, |m| m + 1); + + let mut task = json!({ + "id": uuid::Uuid::new_v4().to_string(), + "title": title, + "status": "todo", + "tags": tags, + "createdAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + "order": next_order, + }); + for key in ["dueDate", "url"] { + match b.get(key) { + None | Some(Value::Null) => {} + Some(Value::String(s)) => { + task[key] = json!(s); + } + Some(_) => return Err((400, format!("{key} must be a string"))), + } + } + + tasks.push(task.clone()); + data["tasks"] = Value::Array(tasks); + write_data(ctx, &data)?; + Ok(task) +} + +fn update_task(ctx: &Ctx, id: &str, body: &str) -> ApiResult { + let b = parse_body(body)?; + let _g = DATA_LOCK.lock().unwrap(); + let mut data = read_data(ctx)?; + let mut tasks = tasks_of(&data); + let task = tasks + .iter_mut() + .find(|t| t.get("id").and_then(Value::as_str) == Some(id)) + .ok_or((404, "task not found".to_string()))?; + + if let Some(v) = b.get("title") { + let s = v + .as_str() + .map(str::trim) + .filter(|s| !s.is_empty() && s.chars().count() <= 1000) + .ok_or((400, "title must be a non-empty string".to_string()))?; + task["title"] = json!(s); + } + if let Some(v) = b.get("status") { + let s = v.as_str().unwrap_or(""); + if s != "todo" && s != "done" { + return Err((400, "status must be todo or done".into())); + } + task["status"] = json!(s); + if s == "done" { + task["completedAt"] = + json!(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)); + } else { + task["completedAt"] = Value::Null; + } + } + for key in ["dueDate", "url"] { + match b.get(key) { + None => {} + Some(Value::Null) => { + task[key] = Value::Null; + } + Some(Value::String(s)) => { + task[key] = json!(s); + } + Some(_) => return Err((400, format!("{key} must be a string or null"))), + } + } + if let Some(v) = b.get("tags") { + match v { + Value::Array(a) if a.iter().all(Value::is_string) => task["tags"] = v.clone(), + _ => return Err((400, "tags must be an array of strings".into())), + } + } + + let updated = task.clone(); + data["tasks"] = Value::Array(tasks); + write_data(ctx, &data)?; + Ok(updated) +} + +fn delete_task(ctx: &Ctx, id: &str) -> ApiResult { + let _g = DATA_LOCK.lock().unwrap(); + let mut data = read_data(ctx)?; + let mut tasks = tasks_of(&data); + let before = tasks.len(); + // 子タスクも一緒に消す (parentId でぶら下がる孤児を残さない) + tasks.retain(|t| { + t.get("id").and_then(Value::as_str) != Some(id) + && t.get("parentId").and_then(Value::as_str) != Some(id) + }); + if tasks.len() == before { + return Err((404, "task not found".into())); + } + data["tasks"] = Value::Array(tasks); + write_data(ctx, &data)?; + Ok(json!({ "deleted": id })) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a144be9..613c8ac 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +mod api; + use std::fs; use tauri::{Emitter, Manager}; use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}; @@ -108,6 +110,8 @@ pub fn run() { } }); + api::start(app.handle().clone()); + Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src/App.tsx b/src/App.tsx index 7ea1321..86e14ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { buildDfsOrder, getDepth, hasUndoneDescendants } from "./utils/taskTree" import { getCurrentWindow } from "@tauri-apps/api/window"; import { LogicalSize } from "@tauri-apps/api/dpi"; import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; import { openUrl } from "@tauri-apps/plugin-opener"; import { CloseConfirmModal } from "./components/CloseConfirmModal"; import { checkForUpdate, UpdateInfo } from "./utils/updater"; @@ -209,9 +210,22 @@ function AppInner() { } }).then((f) => { unlistenUpdate = f; }); + // ローカルAPI/MCP が data.json を直接書き換えたときの再読み込み + let unlistenExternal: (() => void) | undefined; + listen("external-data-changed", async () => { + const raw = await invoke<{ tasks?: unknown[]; tags?: unknown[] } | null>("load_data"); + if (raw) { + useStore.setState({ + tasks: (raw.tasks ?? []) as never, + tags: (raw.tags ?? []) as never, + }); + } + }).then((f) => { unlistenExternal = f; }); + return () => { unlistenSettings?.(); unlistenUpdate?.(); + unlistenExternal?.(); }; }, []); From 6ffc982b538798d5d7ba86092a6048914ffa1be4 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 04:52:02 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20Google=E3=82=AB=E3=83=AC=E3=83=B3?= =?UTF-8?q?=E3=83=80=E3=83=BC/Google=20Tasks=E3=81=B8=E3=81=AE=E6=9B=B8?= =?UTF-8?q?=E3=81=8D=E8=BE=BC=E3=81=BF=E9=80=A3=E6=90=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PKCE + ループバックのOAuthで接続し、期日付きタスクを終日イベントとして、 タスク本体をGoogle Tasksに作成・更新する一方向push。 トークンは app_data_dir に 0600 で保存し、data.json には混ぜない。 セットアップ手順は docs/google-sync.md。 docs/ は .gitignore 対象のため -f で追加している。 --- docs/google-sync.md | 113 ++++ src-tauri/Cargo.lock | 524 +++++++++++++++++- src-tauri/Cargo.toml | 4 + src-tauri/src/api.rs | 4 + src-tauri/src/google.rs | 755 ++++++++++++++++++++++++++ src-tauri/src/lib.rs | 14 +- src/components/GoogleSyncSettings.tsx | 321 +++++++++++ src/components/SettingsModal.tsx | 5 + 8 files changed, 1728 insertions(+), 12 deletions(-) create mode 100644 docs/google-sync.md create mode 100644 src-tauri/src/google.rs create mode 100644 src/components/GoogleSyncSettings.tsx diff --git a/docs/google-sync.md b/docs/google-sync.md new file mode 100644 index 0000000..e07195f --- /dev/null +++ b/docs/google-sync.md @@ -0,0 +1,113 @@ +# Google カレンダー / Google ToDo 連携セットアップ + +FastTask のタスクを **Google へ書き出す(push)だけ** の一方向連携です。 +Google 側で作ったり編集したりした予定・ToDo が FastTask に取り込まれることはありません。 + +アプリにはクライアントシークレットを埋め込んでいないため、**各自の Google Cloud プロジェクトで +OAuth クライアントIDを発行**して設定画面に入力する必要があります。以下はその手順です。 + +--- + +## 1. Google Cloud プロジェクトを作る + +1. を開く +2. 画面上部のプロジェクト選択 → 「新しいプロジェクト」 +3. プロジェクト名は何でもよい(例: `FastTask`)→ 作成 + +## 2. API を有効化する + +左メニュー「API とサービス」→「ライブラリ」から、次の2つを検索して **有効にする**。 + +- **Google Calendar API** +- **Google Tasks API** + +## 3. OAuth 同意画面を設定する + +左メニュー「API とサービス」→「OAuth 同意画面」。 + +1. User Type は **外部(External)** を選択(Google Workspace 組織内だけで使うなら「内部」でも可) +2. アプリ名(例: `FastTask`)、ユーザーサポートメール、デベロッパー連絡先メールを入力 +3. **スコープ**: ここでは何も追加しなくて構いません(アプリ側が要求します)。 + FastTask が要求するスコープは最小限の3つだけです。 + - `.../auth/calendar.events` … カレンダーの予定の作成・更新 + - `.../auth/tasks` … ToDo の作成・更新 + - `.../auth/userinfo.email` … 設定画面に「接続中のアカウント」を表示するためだけに使用 +4. **テストユーザー**: 「公開ステータス」が *テスト* のままで問題ありません。 + 「テストユーザー」に **自分の Google アカウントのメールアドレスを必ず追加**してください。 + ここに登録していないアカウントでは認証が `access_denied` で失敗します。 + +> テストモードの refresh token は **7日で失効**します。失効したら設定画面で「接続を解除」→ +> 「Googleに接続」をやり直してください。恒久的に使いたい場合はアプリを「本番」に公開します +> (自分だけが使う機密スコープなら審査なしで公開できますが、警告画面は出ます)。 + +## 4. OAuth クライアントID を発行する + +左メニュー「API とサービス」→「認証情報」→「認証情報を作成」→「OAuth クライアント ID」。 + +- **アプリケーションの種類: 「デスクトップ アプリ」を選んでください。** +- 名前は任意(例: `FastTask desktop`) + +> **リダイレクトURIの入力欄は出てきません。これで正しいです。** +> 「デスクトップ アプリ」タイプは `http://127.0.0.1:<任意のポート>` へのリダイレクトが +> 自動的に許可される仕様で、FastTask は起動のたびに OS から空きポートを借りて使います +> ([Google: Loopback IP address flow](https://developers.google.com/identity/protocols/oauth2/native-app))。 +> もし種類を「ウェブ アプリケーション」で作ってしまうと、ポートが毎回変わるため動きません。作り直してください。 + +発行されると **クライアントID** と **クライアントシークレット** が表示されます。両方控えます。 +(デスクトップアプリのシークレットは Google の仕様上「機密ではない」扱いですが、FastTask は +念のためローカルに `600` パーミッションで保存します。) + +## 5. FastTask に入力して接続する + +1. FastTask の 設定(⌘,)→ **Google連携** +2. クライアントID / クライアントシークレットを貼り付けて「保存」 +3. 「Googleに接続」を押す → 既定のブラウザが開く +4. アカウントを選び、権限を許可する + - 「このアプリは確認されていません」と出たら「詳細」→「(アプリ名)に移動」を押す(テストモードのため) +5. ブラウザに「連携が完了しました」と出たらタブを閉じる。設定画面に接続中のアカウントが表示されます。 + +## 6. 書き出し設定 + +| 項目 | 内容 | +| --- | --- | +| ToDoに書き出す | タスクを Google ToDo に作成。FastTask で完了にすると Google 側も完了になります | +| カレンダーに書き出す | **期日のあるタスクだけ**を、その日の終日イベントとして作成します | +| 自動書き出し | タスクの追加・更新時に自動で push(既定OFF)。失敗してもアプリの操作は止まりません | +| カレンダーID | 既定 `primary`(メインのカレンダー)。別カレンダーに入れるなら Google カレンダーの設定からIDをコピー | +| ToDoリスト | 既定 `@default`。「リスト取得」を押すとリスト名から選べます | +| 今すぐ同期 | 全タスクを手動で push | + +--- + +## 仕様・制限 + +- **一方向だけです。** Google 側の変更は FastTask に戻ってきません。 +- **FastTask でタスクを削除しても、Google 側は削除されません**(対応表から外れるだけ)。 + Google 側で手動で消してください。 +- 期日を外すと、そのタスクのカレンダー予定は削除されます。 +- 前回 push した内容(タイトル・状態・期日・URL)と変わっていないタスクは API を呼びません。 +- 自動書き出しは保存のたびに走りますが、前回の push が実行中なら今回はスキップします + (最新の状態は次の保存で反映されます)。 + +## 保存される場所 + +アプリデータフォルダ(macOS は `~/Library/Application Support/com.fasttask.desktop/`)。 + +| ファイル | 中身 | +| --- | --- | +| `google.json` | クライアントID・シークレット・リフレッシュトークン・各種設定(**0600**) | +| `google_map.json` | FastTask のタスクID ↔ Google 側ID の対応表 | + +タスク本体(`data.json`)に認証情報は混ざりません。 +「接続を解除」を押すと Google 側でトークンを失効させ、手元の認証情報と対応表を消します。 + +## うまくいかないとき + +| 症状 | 原因と対処 | +| --- | --- | +| `access_denied` | OAuth同意画面の「テストユーザー」に自分のアカウントが入っていない | +| `refresh_token が返りませんでした` | 同じクライアントで過去に許可済み。設定で「接続を解除」してからやり直す | +| `invalid_client` | クライアントID/シークレットの貼り間違い、または種類が「デスクトップ アプリ」でない | +| 数日経つと同期エラー | テストモードの7日失効。再接続するか、アプリを本番公開する | +| `Tasks リストが見つかりません` | ToDoリストIDが違う。「リスト取得」で選び直す | +| 認証がタイムアウトしました | ブラウザでの許可を5分以内に完了する必要があります | diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dd6e387..34e156f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,9 +6,12 @@ version = 4 name = "FastTask" version = "1.2.1" dependencies = [ + "base64 0.22.1", "chrono", + "reqwest", "serde", "serde_json", + "sha2", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", @@ -17,6 +20,7 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-opener", "tiny_http", + "url", "uuid", ] @@ -269,6 +273,29 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.21.7" @@ -479,6 +506,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -521,6 +550,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -550,6 +590,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -579,6 +628,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -602,7 +661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -615,7 +674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -628,6 +687,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -952,6 +1020,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1140,6 +1217,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -1350,8 +1433,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1373,10 +1458,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -1676,6 +1764,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1694,9 +1798,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1964,6 +2070,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1992,6 +2128,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.99" @@ -2128,6 +2274,12 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + [[package]] name = "mac-notification-sys" version = "0.6.15" @@ -2543,6 +2695,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2907,6 +3065,63 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.2", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -2949,6 +3164,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2987,6 +3213,21 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3070,21 +3311,30 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3096,6 +3346,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3124,12 +3388,93 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3139,6 +3484,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3196,6 +3550,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -3320,6 +3697,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -3390,7 +3779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3401,7 +3790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3427,6 +3816,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3545,6 +3950,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3597,6 +4008,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3618,7 +4050,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.11.1", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -3628,7 +4060,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3684,7 +4116,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3901,7 +4333,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3924,7 +4356,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -4161,6 +4593,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4491,6 +4933,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4797,6 +5245,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.4" @@ -4853,6 +5311,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4995,7 +5462,7 @@ dependencies = [ "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -5075,6 +5542,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -5093,6 +5571,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -5102,6 +5589,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5521,7 +6017,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5788,6 +6284,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ebee205..694f142 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,3 +24,7 @@ serde_json = "1" tiny_http = "0.12" uuid = { version = "1", features = ["v4"] } chrono = "0.4" +reqwest = { version = "0.13", default-features = false, features = ["json", "form", "rustls", "charset", "system-proxy"] } +url = "2" +sha2 = "0.10" +base64 = "0.22" diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index ca790b0..45513b8 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -169,6 +169,10 @@ fn write_data(ctx: &Ctx, data: &Value) -> Result<(), (u16, String)> { fs::write(&tmp, s).map_err(|e| (500, format!("write error: {e}")))?; fs::rename(&tmp, data_path(ctx)).map_err(|e| (500, format!("rename error: {e}")))?; let _ = ctx.app.emit("external-data-changed", ()); + // HTTP API 経由の変更も自動pushの対象にする(設定OFFなら何もしない) + if let Some(tasks) = data.get("tasks").and_then(Value::as_array) { + crate::google::auto_push(ctx.app.clone(), tasks.clone()); + } Ok(()) } diff --git a/src-tauri/src/google.rs b/src-tauri/src/google.rs new file mode 100644 index 0000000..d1cca00 --- /dev/null +++ b/src-tauri/src/google.rs @@ -0,0 +1,755 @@ +//! Google カレンダー / Google Tasks への一方向 push 連携。 +//! +//! - OAuth 2.0 デスクトップ向けループバックフロー + PKCE (S256) +//! - クライアントID/シークレットはユーザーがGCPで発行して設定画面から入力する。アプリには埋め込まない。 +//! - 認証情報は app_data_dir/google.json に 0600 で保存し、data.json とは混ぜない。 +//! - FastTask id ↔ Google 側 id の対応は app_data_dir/google_map.json に保持。 + +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use base64::Engine; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use tauri::Manager; +use tauri_plugin_opener::OpenerExt; + +const AUTH_ENDPOINT: &str = "https://accounts.google.com/o/oauth2/v2/auth"; +const TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token"; +const REVOKE_ENDPOINT: &str = "https://oauth2.googleapis.com/revoke"; +const USERINFO_ENDPOINT: &str = "https://www.googleapis.com/oauth2/v3/userinfo"; +const CALENDAR_API: &str = "https://www.googleapis.com/calendar/v3/calendars"; +const TASKS_API: &str = "https://tasks.googleapis.com/tasks/v1"; + +/// calendar.events と tasks が本体。userinfo.email は設定画面の「接続中アカウント」表示にだけ使う。 +const SCOPES: &str = "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/userinfo.email"; + +const AUTH_TIMEOUT: Duration = Duration::from_secs(300); + +// ---------------------------------------------------------------- config + +#[derive(Serialize, Deserialize, Clone)] +#[serde(default)] +pub struct GoogleConfig { + pub client_id: String, + pub client_secret: String, + pub refresh_token: String, + pub email: String, + pub calendar_id: String, + pub tasklist_id: String, + pub auto_push: bool, + /// カレンダーへの終日イベント作成を行うか + pub push_calendar: bool, + /// Google Tasks への作成を行うか + pub push_tasks: bool, +} + +impl Default for GoogleConfig { + fn default() -> Self { + Self { + client_id: String::new(), + client_secret: String::new(), + refresh_token: String::new(), + email: String::new(), + calendar_id: "primary".into(), + tasklist_id: "@default".into(), + auto_push: false, + push_calendar: true, + push_tasks: true, + } + } +} + +/// フロントに返す安全なビュー。secret / refresh_token は決して含めない。 +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicConfig { + connected: bool, + has_secret: bool, + client_id: String, + email: String, + calendar_id: String, + tasklist_id: String, + auto_push: bool, + push_calendar: bool, + push_tasks: bool, +} + +impl From<&GoogleConfig> for PublicConfig { + fn from(c: &GoogleConfig) -> Self { + Self { + connected: !c.refresh_token.is_empty(), + has_secret: !c.client_secret.is_empty(), + client_id: c.client_id.clone(), + email: c.email.clone(), + calendar_id: c.calendar_id.clone(), + tasklist_id: c.tasklist_id.clone(), + auto_push: c.auto_push, + push_calendar: c.push_calendar, + push_tasks: c.push_tasks, + } + } +} + +fn dir(app: &tauri::AppHandle) -> Result { + let d = app + .path() + .app_data_dir() + .map_err(|e| format!("app_data_dir error: {e}"))?; + fs::create_dir_all(&d).map_err(|e| format!("mkdir error: {e}"))?; + Ok(d) +} + +pub fn load_config(app: &tauri::AppHandle) -> Result { + let path = dir(app)?.join("google.json"); + if !path.exists() { + return Ok(GoogleConfig::default()); + } + let s = fs::read_to_string(&path).map_err(|e| format!("read error: {e}"))?; + serde_json::from_str(&s).map_err(|e| format!("parse error: {e}")) +} + +/// 認証情報を含むので 0600 で書く。tmp に書いてから権限を付けて rename。 +fn save_config(app: &tauri::AppHandle, cfg: &GoogleConfig) -> Result<(), String> { + let d = dir(app)?; + let tmp = d.join("google.json.tmp"); + let s = serde_json::to_string_pretty(cfg).map_err(|e| format!("serialize error: {e}"))?; + fs::write(&tmp, s).map_err(|e| format!("write error: {e}"))?; + restrict(&tmp)?; + fs::rename(&tmp, d.join("google.json")).map_err(|e| format!("rename error: {e}")) +} + +#[cfg(unix)] +fn restrict(path: &PathBuf) -> Result<(), String> { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) + .map_err(|e| format!("chmod error: {e}")) +} + +// ponytail: Windows では ACL を触らず読み取り専用属性も付けない(ユーザープロファイル配下の +// 既定ACLに委ねる)。厳密にやるなら windows-acl 等で所有者のみに絞る。 +#[cfg(not(unix))] +fn restrict(_path: &PathBuf) -> Result<(), String> { + Ok(()) +} + +// ---------------------------------------------------------------- id map + +type IdMap = HashMap; + +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(default)] +struct MapEntry { + event_id: String, + gtask_id: String, + /// 前回 push した内容のハッシュ。一致すればAPIを呼ばない。 + hash: String, +} + +fn load_map(app: &tauri::AppHandle) -> IdMap { + let Ok(d) = dir(app) else { return IdMap::new() }; + fs::read_to_string(d.join("google_map.json")) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() +} + +fn save_map(app: &tauri::AppHandle, map: &IdMap) -> Result<(), String> { + let d = dir(app)?; + let tmp = d.join("google_map.json.tmp"); + let s = serde_json::to_string_pretty(map).map_err(|e| format!("serialize error: {e}"))?; + fs::write(&tmp, s).map_err(|e| format!("write error: {e}"))?; + fs::rename(&tmp, d.join("google_map.json")).map_err(|e| format!("rename error: {e}")) +} + +// ---------------------------------------------------------------- oauth + +fn b64url(bytes: &[u8]) -> String { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) +} + +fn random_token() -> String { + format!( + "{}{}", + uuid::Uuid::new_v4().simple(), + uuid::Uuid::new_v4().simple() + ) +} + +/// ループバック受け口を立て、ブラウザで同意画面を開き、code を受け取って token と交換する。 +pub async fn connect(app: tauri::AppHandle) -> Result { + let mut cfg = load_config(&app)?; + if cfg.client_id.trim().is_empty() { + return Err("client_id が未設定です".into()); + } + + let verifier = format!("{}{}", random_token(), random_token()); // 128文字、PKCE の許容文字のみ + let challenge = b64url(&Sha256::digest(verifier.as_bytes())); + let state = random_token(); + + // ワンショットのループバックサーバ。ポートはOS任せ(0)。 + let server = tiny_http::Server::http(("127.0.0.1", 0)) + .map_err(|e| format!("loopback bind error: {e}"))?; + let port = server + .server_addr() + .to_ip() + .ok_or("loopback addr error")? + .port(); + let redirect_uri = format!("http://127.0.0.1:{port}"); + + let mut url = url::Url::parse(AUTH_ENDPOINT).map_err(|e| e.to_string())?; + url.query_pairs_mut() + .append_pair("client_id", &cfg.client_id) + .append_pair("redirect_uri", &redirect_uri) + .append_pair("response_type", "code") + .append_pair("scope", SCOPES) + .append_pair("code_challenge", &challenge) + .append_pair("code_challenge_method", "S256") + .append_pair("state", &state) + .append_pair("access_type", "offline") + .append_pair("prompt", "consent"); + + app.opener() + .open_url(url.as_str(), None::<&str>) + .map_err(|e| format!("ブラウザを開けませんでした: {e}"))?; + + // 受信はブロッキングなので blocking スレッドへ逃がす + let expected_state = state.clone(); + let code = tauri::async_runtime::spawn_blocking(move || wait_for_code(server, &expected_state)) + .await + .map_err(|e| format!("join error: {e}"))??; + + let token: Value = http() + .post(TOKEN_ENDPOINT) + .form(&[ + ("client_id", cfg.client_id.as_str()), + ("client_secret", cfg.client_secret.as_str()), + ("code", code.as_str()), + ("code_verifier", verifier.as_str()), + ("grant_type", "authorization_code"), + ("redirect_uri", redirect_uri.as_str()), + ]) + .send() + .await + .map_err(|_| "トークン取得に失敗しました(ネットワーク)".to_string())? + .json() + .await + .map_err(|_| "トークン応答の解析に失敗しました".to_string())?; + + if let Some(err) = token.get("error").and_then(Value::as_str) { + // ponytail: error_description は client_id 等を含みうるのでそのまま出さない + return Err(format!("トークン取得エラー: {err}")); + } + let refresh = token + .get("refresh_token") + .and_then(Value::as_str) + .ok_or("refresh_token が返りませんでした(GCPの同意画面設定を確認してください)")?; + let access = token + .get("access_token") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + + cfg.refresh_token = refresh.to_string(); + cfg.email = fetch_email(&access).await.unwrap_or_default(); + save_config(&app, &cfg)?; + cache_access(&access, token.get("expires_in").and_then(Value::as_u64).unwrap_or(3000)); + Ok((&cfg).into()) +} + +fn wait_for_code(server: tiny_http::Server, expected_state: &str) -> Result { + let deadline = Instant::now() + AUTH_TIMEOUT; + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err("認証がタイムアウトしました".into()); + } + let Some(req) = server + .recv_timeout(remaining) + .map_err(|e| format!("loopback recv error: {e}"))? + else { + continue; + }; + // favicon 等の無関係なリクエストは無視して待ち続ける + let full = format!("http://127.0.0.1{}", req.url()); + let Ok(parsed) = url::Url::parse(&full) else { + let _ = req.respond(html(400, "不正なリクエストです")); + continue; + }; + let q: HashMap<_, _> = parsed.query_pairs().into_owned().collect(); + + if let Some(err) = q.get("error") { + let _ = req.respond(html(400, "認証がキャンセルされました。")); + return Err(format!("認証エラー: {err}")); + } + let (Some(code), Some(state)) = (q.get("code"), q.get("state")) else { + let _ = req.respond(html(404, "このURLは認証用ではありません。")); + continue; + }; + // CSRF 対策: state が一致しない応答は捨てる + if state != expected_state { + let _ = req.respond(html(400, "state が一致しません。もう一度お試しください。")); + return Err("state 不一致(CSRFの可能性)".into()); + } + let _ = req.respond(html(200, "連携が完了しました。このタブを閉じてください。")); + return Ok(code.clone()); + } +} + +fn html(status: u16, msg: &str) -> tiny_http::Response>> { + let body = format!( + "FastTask\ + \ +

{msg}

" + ); + tiny_http::Response::from_string(body) + .with_status_code(status) + .with_header( + tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) + .unwrap(), + ) +} + +async fn fetch_email(access: &str) -> Option { + let v: Value = http() + .get(USERINFO_ENDPOINT) + .bearer_auth(access) + .send() + .await + .ok()? + .json() + .await + .ok()?; + v.get("email").and_then(Value::as_str).map(str::to_string) +} + +pub async fn disconnect(app: tauri::AppHandle) -> Result { + let mut cfg = load_config(&app)?; + if !cfg.refresh_token.is_empty() { + // 失敗しても手元を消すのが優先(結果は無視) + let _ = http() + .post(REVOKE_ENDPOINT) + .form(&[("token", cfg.refresh_token.as_str())]) + .send() + .await; + } + cfg.refresh_token.clear(); + cfg.email.clear(); + save_config(&app, &cfg)?; + *ACCESS.lock().unwrap() = None; + // 対応表も捨てる(次の接続先が別アカウントなら無意味なため) + let _ = save_map(&app, &IdMap::new()); + Ok((&cfg).into()) +} + +// ---------------------------------------------------------------- access token + +static ACCESS: Mutex> = Mutex::new(None); + +fn cache_access(token: &str, expires_in: u64) { + let ttl = Duration::from_secs(expires_in.saturating_sub(60).max(30)); + *ACCESS.lock().unwrap() = Some((token.to_string(), Instant::now() + ttl)); +} + +fn http() -> reqwest::Client { + reqwest::Client::new() +} + +async fn access_token(cfg: &GoogleConfig) -> Result { + if let Some((tok, exp)) = ACCESS.lock().unwrap().as_ref() { + if Instant::now() < *exp { + return Ok(tok.clone()); + } + } + if cfg.refresh_token.is_empty() { + return Err("Googleに接続していません".into()); + } + let v: Value = http() + .post(TOKEN_ENDPOINT) + .form(&[ + ("client_id", cfg.client_id.as_str()), + ("client_secret", cfg.client_secret.as_str()), + ("refresh_token", cfg.refresh_token.as_str()), + ("grant_type", "refresh_token"), + ]) + .send() + .await + .map_err(|_| "トークン更新に失敗しました(ネットワーク)".to_string())? + .json() + .await + .map_err(|_| "トークン応答の解析に失敗しました".to_string())?; + if let Some(err) = v.get("error").and_then(Value::as_str) { + return Err(format!("トークン更新エラー: {err}(再接続が必要かもしれません)")); + } + let tok = v + .get("access_token") + .and_then(Value::as_str) + .ok_or("access_token が返りませんでした")?; + cache_access(tok, v.get("expires_in").and_then(Value::as_u64).unwrap_or(3000)); + Ok(tok.to_string()) +} + +// ---------------------------------------------------------------- push + +#[derive(Serialize, Default)] +pub struct PushResult { + pub pushed: usize, + pub skipped: usize, + pub errors: Vec, +} + +fn s(t: &Value, k: &str) -> String { + t.get(k).and_then(Value::as_str).unwrap_or_default().to_string() +} + +fn hash_of(t: &Value) -> String { + let mut h = Sha256::new(); + for k in ["title", "status", "dueDate", "url"] { + h.update(s(t, k).as_bytes()); + h.update(b"\x1f"); + } + b64url(&h.finalize()) +} + +/// data.json の tasks 配列を Google に push する。 +pub async fn push(app: tauri::AppHandle, tasks: Vec) -> Result { + let cfg = load_config(&app)?; + let token = access_token(&cfg).await?; + let mut map = load_map(&app); + let mut res = PushResult::default(); + + // ponytail: タスクを逐次処理している(Google APIのレート制限を踏みにくい代わりに遅い)。 + // 件数が増えて体感が悪ければ join_all で並列化し、429 時に Retry-After を見る実装にする。 + for task in &tasks { + let id = s(task, "id"); + if id.is_empty() { + continue; + } + let h = hash_of(task); + let entry = map.entry(id.clone()).or_default(); + if entry.hash == h { + res.skipped += 1; + continue; + } + let mut ok = true; + if cfg.push_tasks { + if let Err(e) = sync_gtask(&cfg, &token, task, entry).await { + res.errors.push(e); + ok = false; + } + } + if cfg.push_calendar { + if let Err(e) = sync_event(&cfg, &token, task, entry).await { + res.errors.push(e); + ok = false; + } + } + if ok { + entry.hash = h; + res.pushed += 1; + } + } + + // ponytail: FastTask で削除されたタスクの Google 側は消さずに対応表だけ掃除する。 + // 削除まで同期するなら、ここで残った entry の event_id/gtask_id を DELETE すればよい。 + let alive: std::collections::HashSet = tasks.iter().map(|t| s(t, "id")).collect(); + map.retain(|k, _| alive.contains(k)); + + save_map(&app, &map)?; + Ok(res) +} + +/// 404/410 は「Google側で消された」扱いにして、呼び出し元が作り直せるようにする。 +async fn send_json( + req: reqwest::RequestBuilder, + token: &str, + body: Option<&Value>, +) -> Result, String> { + let req = req.bearer_auth(token); + let req = match body { + Some(b) => req.json(b), + None => req, + }; + let resp = req.send().await.map_err(|_| "Google API 通信失敗".to_string())?; + let status = resp.status(); + if status == reqwest::StatusCode::NOT_FOUND || status == reqwest::StatusCode::GONE { + return Ok(None); + } + if !status.is_success() { + let detail = resp + .json::() + .await + .ok() + .and_then(|v| { + v.pointer("/error/message") + .and_then(Value::as_str) + .map(str::to_string) + }) + .unwrap_or_else(|| status.to_string()); + return Err(format!("Google API {status}: {detail}")); + } + resp.json().await.map(Some).map_err(|e| format!("応答解析失敗: {e}")) +} + +async fn sync_gtask( + cfg: &GoogleConfig, + token: &str, + task: &Value, + entry: &mut MapEntry, +) -> Result<(), String> { + let done = s(task, "status") == "done"; + let mut body = json!({ + "title": s(task, "title"), + "status": if done { "completed" } else { "needsAction" }, + }); + if !done { + // needsAction に戻す時は completed を明示的に消す必要がある + body["completed"] = Value::Null; + } + let url = s(task, "url"); + if !url.is_empty() { + body["notes"] = json!(url); + } + let due = s(task, "dueDate"); + body["due"] = if due.is_empty() { + Value::Null + } else { + json!(format!("{due}T00:00:00.000Z")) + }; + + let list = urlencoding(&cfg.tasklist_id); + if !entry.gtask_id.is_empty() { + let url = format!("{TASKS_API}/lists/{list}/tasks/{}", urlencoding(&entry.gtask_id)); + if send_json(http().patch(&url), token, Some(&body)).await?.is_some() { + return Ok(()); + } + entry.gtask_id.clear(); // Google側で消えていたので作り直す + } + let url = format!("{TASKS_API}/lists/{list}/tasks"); + let created = send_json(http().post(&url), token, Some(&body)) + .await? + .ok_or("Tasks リストが見つかりません")?; + entry.gtask_id = s(&created, "id"); + Ok(()) +} + +async fn sync_event( + cfg: &GoogleConfig, + token: &str, + task: &Value, + entry: &mut MapEntry, +) -> Result<(), String> { + let due = s(task, "dueDate"); + let cal = urlencoding(&cfg.calendar_id); + + // 期日が無い / 外されたタスクはカレンダーに置かない + if due.is_empty() { + if !entry.event_id.is_empty() { + let url = format!("{CALENDAR_API}/{cal}/events/{}", urlencoding(&entry.event_id)); + let _ = send_json(http().delete(&url), token, None).await; + entry.event_id.clear(); + } + return Ok(()); + } + + let end = next_day(&due)?; + let mut body = json!({ + "summary": s(task, "title"), + "start": { "date": due }, + "end": { "date": end }, + }); + let url_field = s(task, "url"); + if !url_field.is_empty() { + body["description"] = json!(url_field); + } + + if !entry.event_id.is_empty() { + let url = format!("{CALENDAR_API}/{cal}/events/{}", urlencoding(&entry.event_id)); + if send_json(http().patch(&url), token, Some(&body)).await?.is_some() { + return Ok(()); + } + entry.event_id.clear(); + } + let url = format!("{CALENDAR_API}/{cal}/events"); + let created = send_json(http().post(&url), token, Some(&body)) + .await? + .ok_or("カレンダーが見つかりません")?; + entry.event_id = s(&created, "id"); + Ok(()) +} + +/// 終日イベントの end.date は排他なので翌日を渡す +fn next_day(iso: &str) -> Result { + let d = chrono::NaiveDate::parse_from_str(iso, "%Y-%m-%d") + .map_err(|_| format!("dueDate の形式が不正です: {iso}"))?; + Ok((d + chrono::Duration::days(1)).format("%Y-%m-%d").to_string()) +} + +fn urlencoding(s: &str) -> String { + url::form_urlencoded::byte_serialize(s.as_bytes()).collect() +} + +pub async fn list_tasklists(app: tauri::AppHandle) -> Result { + let cfg = load_config(&app)?; + let token = access_token(&cfg).await?; + let url = format!("{TASKS_API}/users/@me/lists"); + send_json(http().get(&url), &token, None) + .await? + .ok_or("Tasks リストを取得できませんでした".into()) +} + +// ---------------------------------------------------------------- commands + +#[tauri::command] +pub async fn google_get_config(app: tauri::AppHandle) -> Result { + Ok((&load_config(&app)?).into()) +} + +/// 空文字の client_secret は「変更なし」として扱い、保存済みの値を消さない。 +#[tauri::command] +pub async fn google_set_config( + app: tauri::AppHandle, + patch: Value, +) -> Result { + let mut cfg = load_config(&app)?; + let txt = |k: &str| patch.get(k).and_then(Value::as_str).map(str::trim).map(str::to_string); + if let Some(v) = txt("clientId") { + cfg.client_id = v; + } + if let Some(v) = txt("clientSecret") { + if !v.is_empty() { + cfg.client_secret = v; + } + } + if let Some(v) = txt("calendarId") { + cfg.calendar_id = if v.is_empty() { "primary".into() } else { v }; + } + if let Some(v) = txt("tasklistId") { + cfg.tasklist_id = if v.is_empty() { "@default".into() } else { v }; + } + for (key, field) in [ + ("autoPush", 0usize), + ("pushCalendar", 1), + ("pushTasks", 2), + ] { + if let Some(b) = patch.get(key).and_then(Value::as_bool) { + match field { + 0 => cfg.auto_push = b, + 1 => cfg.push_calendar = b, + _ => cfg.push_tasks = b, + } + } + } + save_config(&app, &cfg)?; + Ok((&cfg).into()) +} + +#[tauri::command] +pub async fn google_connect(app: tauri::AppHandle) -> Result { + connect(app).await +} + +#[tauri::command] +pub async fn google_disconnect(app: tauri::AppHandle) -> Result { + disconnect(app).await +} + +#[tauri::command] +pub async fn google_list_tasklists(app: tauri::AppHandle) -> Result { + list_tasklists(app).await +} + +/// 設定画面の「今すぐ同期」。data.json を読み直して全件 push する。 +#[tauri::command] +pub async fn google_push_now(app: tauri::AppHandle) -> Result { + let tasks = read_tasks(&app)?; + push(app, tasks).await +} + +fn read_tasks(app: &tauri::AppHandle) -> Result, String> { + let path = dir(app)?.join("data.json"); + if !path.exists() { + return Ok(vec![]); + } + let s = fs::read_to_string(&path).map_err(|e| format!("read error: {e}"))?; + let v: Value = serde_json::from_str(&s).map_err(|e| format!("parse error: {e}"))?; + Ok(v.get("tasks") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default()) +} + +// ---------------------------------------------------------------- auto push + +static PUSH_IN_FLIGHT: Mutex = Mutex::new(false); + +/// save_data から呼ばれる fire-and-forget の自動push。 +/// 失敗してもアプリ操作をブロックしない(ログに出すだけ)。 +pub fn auto_push(app: tauri::AppHandle, tasks: Vec) { + let Ok(cfg) = load_config(&app) else { return }; + if !cfg.auto_push || cfg.refresh_token.is_empty() { + return; + } + { + // ponytail: 実行中なら今回の保存分はスキップする(最新状態は次の保存で追いつく)。 + // 取りこぼしを無くすなら「実行中に来たら1回だけ再実行する」フラグを足す。 + let mut flag = PUSH_IN_FLIGHT.lock().unwrap(); + if *flag { + return; + } + *flag = true; + } + tauri::async_runtime::spawn(async move { + match push(app, tasks).await { + Ok(r) if !r.errors.is_empty() => { + eprintln!("google auto push: {} 件失敗: {:?}", r.errors.len(), r.errors) + } + Err(e) => eprintln!("google auto push: {e}"), + _ => {} + } + *PUSH_IN_FLIGHT.lock().unwrap() = false; + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check() { + // 終日イベントの end は翌日(排他) + assert_eq!(next_day("2026-11-10").unwrap(), "2026-11-11"); + assert_eq!(next_day("2026-12-31").unwrap(), "2027-01-01"); + assert!(next_day("11/10").is_err()); + + // 内容が変われば hash が変わる = 差分検出が効く + let a = json!({"title": "a", "status": "todo"}); + let b = json!({"title": "a", "status": "done"}); + assert_eq!(hash_of(&a), hash_of(&json!({"title": "a", "status": "todo"}))); + assert_ne!(hash_of(&a), hash_of(&b)); + + // PublicConfig に秘密が漏れていないこと + let cfg = GoogleConfig { + client_secret: "secret".into(), + refresh_token: "refresh".into(), + ..Default::default() + }; + let json = serde_json::to_string(&PublicConfig::from(&cfg)).unwrap(); + assert!(!json.contains("secret") || !json.contains("refresh")); + assert!(!json.contains("\"refresh\"")); + assert!(!json.contains("\"secret\"")); + assert!(json.contains("\"connected\":true")); + assert!(json.contains("\"hasSecret\":true")); + + // PKCE challenge は RFC7636 の既知ベクタと一致する + let v = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + assert_eq!( + b64url(&Sha256::digest(v.as_bytes())), + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + ); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 613c8ac..a509a67 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod api; +mod google; use std::fs; use tauri::{Emitter, Manager}; @@ -59,6 +60,11 @@ async fn save_data(app: tauri::AppHandle, data: serde_json::Value) -> Result<(), let s = serde_json::to_string_pretty(&data).map_err(|e| format!("serialize error: {e}"))?; fs::write(&tmp, s).map_err(|e| format!("write error: {e}"))?; fs::rename(&tmp, &path).map_err(|e| format!("rename error: {e}"))?; + + // Google への自動push(設定ONかつ接続済みのときだけ)。失敗しても保存は成功扱い。 + if let Some(tasks) = data.get("tasks").and_then(|v| v.as_array()) { + google::auto_push(app.clone(), tasks.clone()); + } Ok(()) } @@ -118,7 +124,13 @@ pub fn run() { load_data, save_data, register_focus_shortcut, - unregister_focus_shortcut + unregister_focus_shortcut, + google::google_get_config, + google::google_set_config, + google::google_connect, + google::google_disconnect, + google::google_list_tasklists, + google::google_push_now ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/GoogleSyncSettings.tsx b/src/components/GoogleSyncSettings.tsx new file mode 100644 index 0000000..a8e15ba --- /dev/null +++ b/src/components/GoogleSyncSettings.tsx @@ -0,0 +1,321 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { Language } from "../types"; + +/** Rust 側 PublicConfig と対。secret / refresh_token は一切含まれない。 */ +interface GoogleConfig { + connected: boolean; + hasSecret: boolean; + clientId: string; + email: string; + calendarId: string; + tasklistId: string; + autoPush: boolean; + pushCalendar: boolean; + pushTasks: boolean; +} + +interface PushResult { + pushed: number; + skipped: number; + errors: string[]; +} + +interface TaskList { + id: string; + title: string; +} + +// SettingsModal と同じく、この画面のテキストはコンポーネント内に持つ +const T = { + ja: { + heading: "Google連携", + desc: "FastTask のタスクを Google カレンダー / Google ToDo に書き出します(書き込みのみ・取り込みはしません)", + clientId: "クライアントID", + clientSecret: "クライアントシークレット", + secretSaved: "保存済み(変更する場合のみ入力)", + setupHint: "GCPで発行した OAuth クライアント(デスクトップアプリ)の値を入力してください。手順は docs/google-sync.md", + connect: "Googleに接続", + connecting: "ブラウザで認証中...", + disconnect: "接続を解除", + connectedAs: "接続中", + notConnected: "未接続", + calendarId: "カレンダーID", + tasklistId: "ToDoリスト", + fetchLists: "リスト取得", + autoPush: "自動書き出し", + autoPushDesc: "タスクの追加・更新時に自動でGoogleへ書き出す", + pushCalendar: "カレンダーに書き出す", + pushCalendarDesc: "期日のあるタスクを終日イベントとして作成", + pushTasks: "ToDoに書き出す", + pushTasksDesc: "タスクを Google ToDo に作成(完了状態も反映)", + syncNow: "今すぐ同期", + syncing: "同期中...", + saved: "保存しました", + result: (r: PushResult) => `書き出し ${r.pushed} 件 / 変更なし ${r.skipped} 件`, + needClientId: "先にクライアントIDを保存してください", + save: "保存", + }, + en: { + heading: "Google sync", + desc: "Pushes FastTask tasks to Google Calendar / Google Tasks (write only, never imports)", + clientId: "Client ID", + clientSecret: "Client secret", + secretSaved: "Saved (enter only to change)", + setupHint: "Enter the OAuth client (Desktop app) values issued in GCP. See docs/google-sync.md", + connect: "Connect to Google", + connecting: "Authorizing in browser...", + disconnect: "Disconnect", + connectedAs: "Connected", + notConnected: "Not connected", + calendarId: "Calendar ID", + tasklistId: "Task list", + fetchLists: "Fetch lists", + autoPush: "Auto push", + autoPushDesc: "Push to Google automatically when a task is added or updated", + pushCalendar: "Push to Calendar", + pushCalendarDesc: "Create an all-day event for tasks with a due date", + pushTasks: "Push to Tasks", + pushTasksDesc: "Create tasks in Google Tasks (completion is mirrored)", + syncNow: "Sync now", + syncing: "Syncing...", + saved: "Saved", + result: (r: PushResult) => `${r.pushed} pushed / ${r.skipped} unchanged`, + needClientId: "Save a client ID first", + save: "Save", + }, +} as const; + +function Row({ label, desc, children }: { label: string; desc?: string; children: React.ReactNode }) { + return ( +
+
+
{label}
+ {desc &&
{desc}
} +
+ {children} +
+ ); +} + +function MiniToggle({ value, onChange, disabled }: { value: boolean; onChange: (v: boolean) => void; disabled?: boolean }) { + return ( + + ); +} + +const inputCls = + "px-2 py-1.5 text-[12px] rounded-md border border-black/10 dark:border-white/10 bg-transparent outline-none focus:border-accent w-full"; +const btnCls = + "px-3 py-1.5 text-[12px] rounded-md bg-black/8 hover:bg-black/15 dark:bg-white/10 dark:hover:bg-white/20 disabled:opacity-40 shrink-0"; + +export function GoogleSyncSettings({ lang }: { lang: Language }) { + const tx = T[lang] ?? T.ja; + const [cfg, setCfg] = useState(null); + const [clientId, setClientId] = useState(""); + const [clientSecret, setClientSecret] = useState(""); + const [calendarId, setCalendarId] = useState("primary"); + const [tasklistId, setTasklistId] = useState("@default"); + const [lists, setLists] = useState(null); + const [busy, setBusy] = useState<"" | "connect" | "sync" | "lists">(""); + const [msg, setMsg] = useState(""); + const [err, setErr] = useState(""); + + function apply(c: GoogleConfig) { + setCfg(c); + setClientId(c.clientId); + setCalendarId(c.calendarId); + setTasklistId(c.tasklistId); + setClientSecret(""); + } + + useEffect(() => { + invoke("google_get_config").then(apply).catch((e) => setErr(String(e))); + }, []); + + async function patch(p: Record) { + setErr(""); + try { + apply(await invoke("google_set_config", { patch: p })); + setMsg(tx.saved); + } catch (e) { + setErr(String(e)); + } + } + + async function run(kind: "connect" | "sync" | "lists", fn: () => Promise) { + setBusy(kind); + setErr(""); + setMsg(""); + try { + await fn(); + } catch (e) { + setErr(String(e)); + } finally { + setBusy(""); + } + } + + if (!cfg) return null; + + return ( +
+
+
{tx.heading}
+
{tx.desc}
+
+ +
+
{tx.clientId}
+
+ setClientId(e.target.value)} + /> + +
+
{tx.clientSecret}
+ setClientSecret(e.target.value)} + /> +
{tx.setupHint}
+
+ +
+
+ {cfg.connected ? ( + + {tx.connectedAs}: {cfg.email || "Google"} + + ) : ( + {tx.notConnected} + )} +
+ {cfg.connected ? ( + + ) : ( + + )} +
+ + {cfg.connected && ( +
+ + patch({ pushTasks: v })} /> + + + patch({ pushCalendar: v })} /> + + + patch({ autoPush: v })} /> + + +
+
{tx.calendarId}
+
+ setCalendarId(e.target.value)} + onBlur={() => calendarId !== cfg.calendarId && patch({ calendarId })} + /> +
+
{tx.tasklistId}
+
+ {lists ? ( + + ) : ( + setTasklistId(e.target.value)} + onBlur={() => tasklistId !== cfg.tasklistId && patch({ tasklistId })} + /> + )} + +
+
+ + +
+ )} + + {msg &&
{msg}
} + {err &&
{err}
} +
+ ); +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index f8a268d..a45dd80 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { useStore } from "../store"; import { TagManager } from "./TagManager"; +import { GoogleSyncSettings } from "./GoogleSyncSettings"; import { t, TKey } from "../i18n"; import { Language, Theme } from "../types"; @@ -287,6 +288,10 @@ export function SettingsModal({ open, onClose }: Props) { +
+ +
+ {settings.tagsEnabled && (
From d51787fd961147191dc9b08fea7e9f63d7e77d70 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 04:52:19 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=E3=82=BF=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=BE=8C=E3=81=AB=E3=82=A2=E3=82=A4=E3=83=86?= =?UTF-8?q?=E3=83=A0=E3=81=B8=E3=83=95=E3=82=A9=E3=83=BC=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=82=92=E7=A7=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 作成直後は入力欄を blur し、描画を待ってから生成されたアイテムに DOMフォーカスを移す。アイテム側に data-task-id と tabIndex を付与。 --- src/components/TaskInput.tsx | 13 +++++++++++-- src/components/TaskItem.tsx | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/TaskInput.tsx b/src/components/TaskInput.tsx index a9ba528..9cccf53 100644 --- a/src/components/TaskInput.tsx +++ b/src/components/TaskInput.tsx @@ -114,8 +114,17 @@ export function TaskInput() { if (id) lastId = id; }); setValue(""); - if (lastId) setSelectedTaskId(lastId); - requestAnimationFrame(() => inputRef.current?.focus()); + // 作成直後は生成されたアイテムを選択し、入力欄からフォーカスを外す + if (lastId) { + setSelectedTaskId(lastId); + inputRef.current?.blur(); + // アイテムは次のレンダー後に現れるので描画を待ってからDOMフォーカスを移す + requestAnimationFrame(() => { + document.querySelector(`[data-task-id="${lastId}"]`)?.focus(); + }); + } else { + requestAnimationFrame(() => inputRef.current?.focus()); + } } function submit() { diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index ac71586..d818eda 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -156,8 +156,10 @@ export function TaskItem({ task, draggable = true }: Props) { }} style={{ ...style, paddingLeft: depth > 0 ? `${depth * 20 + 8}px` : undefined }} data-project={task.projectName || undefined} + data-task-id={task.id} + tabIndex={-1} onClick={() => setSelectedTaskId(task.id)} - className={`relative group task-item ${task.projectName ? "" : "task-item-plain"} px-2 py-1.5 flex items-center gap-2 transition-all rounded-md + className={`relative group task-item focus:outline-none ${task.projectName ? "" : "task-item-plain"} px-2 py-1.5 flex items-center gap-2 transition-all rounded-md border-l-[3px] ${vibratingTaskId === task.id ? "task-shake" : ""} ${isCompleting ? "task-sweep-left" : ""} From fb394c3ad3cd3eb84850530e8a2170ab23006e38 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 04:52:20 +0900 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20=E6=9C=9F=E9=99=90=E8=B6=85?= =?UTF-8?q?=E9=81=8E=E3=81=AE=E8=B5=A4=E3=82=A4=E3=83=B3=E3=82=B8=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=92=E3=83=9B=E3=83=90=E3=83=BC=E3=81=AA?= =?UTF-8?q?=E3=81=97=E3=81=A7=E3=82=82=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 期限超過タスクのチェックボックスが opacity-0 group-hover のままで、 マウスを乗せないと赤が見えなかった。isOverdue を表示条件に加える。 --- src/components/TaskItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index d818eda..f16f1e3 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -196,7 +196,7 @@ export function TaskItem({ task, draggable = true }: Props) {
+
From fbfacd1345b86985b26ae80980f079b9010c15f6 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 05:33:25 +0900 Subject: [PATCH 09/12] =?UTF-8?q?docs:=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E3=83=91=E3=82=B9=E5=88=A5?= =?UTF-8?q?=E3=81=AB=E5=87=BA=E5=8A=9B=E3=81=97=E4=B8=80=E8=A6=A7=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/.md -> docs//index.html に出力し、公開URLを /FastTask// にする。ルートは各mdのH1と冒頭段落から 自動生成する一覧ページ。ドキュメントはmdを足すだけで増やせる。 --- docs/google-sync/index.html | 220 ++++++++++++++++++++++++++ docs/index.html | 183 ++------------------- scripts/build-docs.sh | 68 +++++--- src/components/GoogleSyncSettings.tsx | 2 +- 4 files changed, 279 insertions(+), 194 deletions(-) create mode 100644 docs/google-sync/index.html diff --git a/docs/google-sync/index.html b/docs/google-sync/index.html new file mode 100644 index 0000000..1ea7ce4 --- /dev/null +++ b/docs/google-sync/index.html @@ -0,0 +1,220 @@ + + + + + +Google カレンダー / Google ToDo 連携セットアップ — FastTask + + + + +
+

Google カレンダー / Google ToDo 連携セットアップ

+

FastTask のタスクを Google へ書き出す(push)だけ の一方向連携です。
+Google 側で作ったり編集したりした予定・ToDo が FastTask に取り込まれることはありません。

+

アプリにはクライアントシークレットを埋め込んでいないため、各自の Google Cloud プロジェクトで
+OAuth クライアントIDを発行
して設定画面に入力する必要があります。以下はその手順です。

+
+

1. Google Cloud プロジェクトを作る

+
    +
  1. https://console.cloud.google.com/ を開く
  2. +
  3. 画面上部のプロジェクト選択 → 「新しいプロジェクト」
  4. +
  5. プロジェクト名は何でもよい(例: FastTask)→ 作成
  6. +
+

2. API を有効化する

+

左メニュー「API とサービス」→「ライブラリ」から、次の2つを検索して 有効にする。

+
    +
  • Google Calendar API
  • +
  • Google Tasks API
  • +
+

3. OAuth 同意画面を設定する

+

左メニュー「API とサービス」→「OAuth 同意画面」。

+
    +
  1. User Type は 外部(External) を選択(Google Workspace 組織内だけで使うなら「内部」でも可)
  2. +
  3. アプリ名(例: FastTask)、ユーザーサポートメール、デベロッパー連絡先メールを入力
  4. +
  5. スコープ: ここでは何も追加しなくて構いません(アプリ側が要求します)。
    +FastTask が要求するスコープは最小限の3つだけです。 +
      +
    • .../auth/calendar.events … カレンダーの予定の作成・更新
    • +
    • .../auth/tasks … ToDo の作成・更新
    • +
    • .../auth/userinfo.email … 設定画面に「接続中のアカウント」を表示するためだけに使用
    • +
    +
  6. +
  7. テストユーザー: 「公開ステータス」が テスト のままで問題ありません。
    +「テストユーザー」に 自分の Google アカウントのメールアドレスを必ず追加してください。
    +ここに登録していないアカウントでは認証が access_denied で失敗します。
  8. +
+
+

テストモードの refresh token は 7日で失効します。失効したら設定画面で「接続を解除」→
+「Googleに接続」をやり直してください。恒久的に使いたい場合はアプリを「本番」に公開します
+(自分だけが使う機密スコープなら審査なしで公開できますが、警告画面は出ます)。

+
+

4. OAuth クライアントID を発行する

+

左メニュー「API とサービス」→「認証情報」→「認証情報を作成」→「OAuth クライアント ID」。

+
    +
  • アプリケーションの種類: 「デスクトップ アプリ」を選んでください。
  • +
  • 名前は任意(例: FastTask desktop)
  • +
+
+

リダイレクトURIの入力欄は出てきません。これで正しいです。
+「デスクトップ アプリ」タイプは http://127.0.0.1:<任意のポート> へのリダイレクトが
+自動的に許可される仕様で、FastTask は起動のたびに OS から空きポートを借りて使います
+(Google: Loopback IP address flow)。
+もし種類を「ウェブ アプリケーション」で作ってしまうと、ポートが毎回変わるため動きません。作り直してください。

+
+

発行されると クライアントID と クライアントシークレット が表示されます。両方控えます。
+(デスクトップアプリのシークレットは Google の仕様上「機密ではない」扱いですが、FastTask は
+念のためローカルに 600 パーミッションで保存します。)

+

5. FastTask に入力して接続する

+
    +
  1. FastTask の 設定(⌘,)→ Google連携
  2. +
  3. クライアントID / クライアントシークレットを貼り付けて「保存」
  4. +
  5. 「Googleに接続」を押す → 既定のブラウザが開く
  6. +
  7. アカウントを選び、権限を許可する +
      +
    • 「このアプリは確認されていません」と出たら「詳細」→「(アプリ名)に移動」を押す(テストモードのため)
    • +
    +
  8. +
  9. ブラウザに「連携が完了しました」と出たらタブを閉じる。設定画面に接続中のアカウントが表示されます。
  10. +
+

6. 書き出し設定

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
項目内容
ToDoに書き出すタスクを Google ToDo に作成。FastTask で完了にすると Google 側も完了になります
カレンダーに書き出す期日のあるタスクだけを、その日の終日イベントとして作成します
自動書き出しタスクの追加・更新時に自動で push(既定OFF)。失敗してもアプリの操作は止まりません
カレンダーID既定 primary(メインのカレンダー)。別カレンダーに入れるなら Google カレンダーの設定からIDをコピー
ToDoリスト既定 @default。「リスト取得」を押すとリスト名から選べます
今すぐ同期全タスクを手動で push
+
+

仕様・制限

+
    +
  • 一方向だけです。 Google 側の変更は FastTask に戻ってきません。
  • +
  • FastTask でタスクを削除しても、Google 側は削除されません(対応表から外れるだけ)。
    +Google 側で手動で消してください。
  • +
  • 期日を外すと、そのタスクのカレンダー予定は削除されます。
  • +
  • 前回 push した内容(タイトル・状態・期日・URL)と変わっていないタスクは API を呼びません。
  • +
  • 自動書き出しは保存のたびに走りますが、前回の push が実行中なら今回はスキップします
    +(最新の状態は次の保存で反映されます)。
  • +
+

保存される場所

+

アプリデータフォルダ(macOS は ~/Library/Application Support/com.fasttask.desktop/)。

+ + + + + + + + + + + + + + + + + +
ファイル中身
google.jsonクライアントID・シークレット・リフレッシュトークン・各種設定(0600)
google_map.jsonFastTask のタスクID ↔ Google 側ID の対応表
+

タスク本体(data.json)に認証情報は混ざりません。
+「接続を解除」を押すと Google 側でトークンを失効させ、手元の認証情報と対応表を消します。

+

うまくいかないとき

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
症状原因と対処
access_deniedOAuth同意画面の「テストユーザー」に自分のアカウントが入っていない
refresh_token が返りませんでした同じクライアントで過去に許可済み。設定で「接続を解除」してからやり直す
invalid_clientクライアントID/シークレットの貼り間違い、または種類が「デスクトップ アプリ」でない
数日経つと同期エラーテストモードの7日失効。再接続するか、アプリを本番公開する
Tasks リストが見つかりませんToDoリストIDが違う。「リスト取得」で選び直す
認証がタイムアウトしましたブラウザでの許可を5分以内に完了する必要があります
+
+ + + diff --git a/docs/index.html b/docs/index.html index 5ee6f34..30ee420 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ -Google連携セットアップ — FastTask +ドキュメント — FastTask +
-

Google カレンダー / Google ToDo 連携セットアップ

-

FastTask のタスクを Google へ書き出す(push)だけ の一方向連携です。
-Google 側で作ったり編集したりした予定・ToDo が FastTask に取り込まれることはありません。

-

アプリにはクライアントシークレットを埋め込んでいないため、各自の Google Cloud プロジェクトで
-OAuth クライアントIDを発行
して設定画面に入力する必要があります。以下はその手順です。

-
-

1. Google Cloud プロジェクトを作る

-
    -
  1. https://console.cloud.google.com/ を開く
  2. -
  3. 画面上部のプロジェクト選択 → 「新しいプロジェクト」
  4. -
  5. プロジェクト名は何でもよい(例: FastTask)→ 作成
  6. -
-

2. API を有効化する

-

左メニュー「API とサービス」→「ライブラリ」から、次の2つを検索して 有効にする。

-
    -
  • Google Calendar API
  • -
  • Google Tasks API
  • -
-

3. OAuth 同意画面を設定する

-

左メニュー「API とサービス」→「OAuth 同意画面」。

-
    -
  1. User Type は 外部(External) を選択(Google Workspace 組織内だけで使うなら「内部」でも可)
  2. -
  3. アプリ名(例: FastTask)、ユーザーサポートメール、デベロッパー連絡先メールを入力
  4. -
  5. スコープ: ここでは何も追加しなくて構いません(アプリ側が要求します)。
    -FastTask が要求するスコープは最小限の3つだけです。 -
      -
    • .../auth/calendar.events … カレンダーの予定の作成・更新
    • -
    • .../auth/tasks … ToDo の作成・更新
    • -
    • .../auth/userinfo.email … 設定画面に「接続中のアカウント」を表示するためだけに使用
    • -
    -
  6. -
  7. テストユーザー: 「公開ステータス」が テスト のままで問題ありません。
    -「テストユーザー」に 自分の Google アカウントのメールアドレスを必ず追加してください。
    -ここに登録していないアカウントでは認証が access_denied で失敗します。
  8. -
-
-

テストモードの refresh token は 7日で失効します。失効したら設定画面で「接続を解除」→
-「Googleに接続」をやり直してください。恒久的に使いたい場合はアプリを「本番」に公開します
-(自分だけが使う機密スコープなら審査なしで公開できますが、警告画面は出ます)。

-
-

4. OAuth クライアントID を発行する

-

左メニュー「API とサービス」→「認証情報」→「認証情報を作成」→「OAuth クライアント ID」。

-
    -
  • アプリケーションの種類: 「デスクトップ アプリ」を選んでください。
  • -
  • 名前は任意(例: FastTask desktop)
  • -
-
-

リダイレクトURIの入力欄は出てきません。これで正しいです。
-「デスクトップ アプリ」タイプは http://127.0.0.1:<任意のポート> へのリダイレクトが
-自動的に許可される仕様で、FastTask は起動のたびに OS から空きポートを借りて使います
-(Google: Loopback IP address flow)。
-もし種類を「ウェブ アプリケーション」で作ってしまうと、ポートが毎回変わるため動きません。作り直してください。

-
-

発行されると クライアントID と クライアントシークレット が表示されます。両方控えます。
-(デスクトップアプリのシークレットは Google の仕様上「機密ではない」扱いですが、FastTask は
-念のためローカルに 600 パーミッションで保存します。)

-

5. FastTask に入力して接続する

-
    -
  1. FastTask の 設定(⌘,)→ Google連携
  2. -
  3. クライアントID / クライアントシークレットを貼り付けて「保存」
  4. -
  5. 「Googleに接続」を押す → 既定のブラウザが開く
  6. -
  7. アカウントを選び、権限を許可する -
      -
    • 「このアプリは確認されていません」と出たら「詳細」→「(アプリ名)に移動」を押す(テストモードのため)
    • -
    -
  8. -
  9. ブラウザに「連携が完了しました」と出たらタブを閉じる。設定画面に接続中のアカウントが表示されます。
  10. -
-

6. 書き出し設定

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
項目内容
ToDoに書き出すタスクを Google ToDo に作成。FastTask で完了にすると Google 側も完了になります
カレンダーに書き出す期日のあるタスクだけを、その日の終日イベントとして作成します
自動書き出しタスクの追加・更新時に自動で push(既定OFF)。失敗してもアプリの操作は止まりません
カレンダーID既定 primary(メインのカレンダー)。別カレンダーに入れるなら Google カレンダーの設定からIDをコピー
ToDoリスト既定 @default。「リスト取得」を押すとリスト名から選べます
今すぐ同期全タスクを手動で push
-
-

仕様・制限

-
    -
  • 一方向だけです。 Google 側の変更は FastTask に戻ってきません。
  • -
  • FastTask でタスクを削除しても、Google 側は削除されません(対応表から外れるだけ)。
    -Google 側で手動で消してください。
  • -
  • 期日を外すと、そのタスクのカレンダー予定は削除されます。
  • -
  • 前回 push した内容(タイトル・状態・期日・URL)と変わっていないタスクは API を呼びません。
  • -
  • 自動書き出しは保存のたびに走りますが、前回の push が実行中なら今回はスキップします
    -(最新の状態は次の保存で反映されます)。
  • -
-

保存される場所

-

アプリデータフォルダ(macOS は ~/Library/Application Support/com.fasttask.desktop/)。

- - - - - - - - - - - - - - - - - -
ファイル中身
google.jsonクライアントID・シークレット・リフレッシュトークン・各種設定(0600)
google_map.jsonFastTask のタスクID ↔ Google 側ID の対応表
-

タスク本体(data.json)に認証情報は混ざりません。
-「接続を解除」を押すと Google 側でトークンを失効させ、手元の認証情報と対応表を消します。

-

うまくいかないとき

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
症状原因と対処
access_deniedOAuth同意画面の「テストユーザー」に自分のアカウントが入っていない
refresh_token が返りませんでした同じクライアントで過去に許可済み。設定で「接続を解除」してからやり直す
invalid_clientクライアントID/シークレットの貼り間違い、または種類が「デスクトップ アプリ」でない
数日経つと同期エラーテストモードの7日失効。再接続するか、アプリを本番公開する
Tasks リストが見つかりませんToDoリストIDが違う。「リスト取得」で選び直す
認証がタイムアウトしましたブラウザでの許可を5分以内に完了する必要があります
+

FastTask ドキュメント

diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index ed00cae..550082d 100644 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -2,25 +2,19 @@ # docs/*.md を GitHub Pages 用の HTML にビルドする。 # GitHub と同じ GFM レンダラ(gh api /markdown)を使うので npm 依存はゼロ。 # 使い方: bash scripts/build-docs.sh (gh のログインが必要) +# +# docs/foo.md -> docs/foo/index.html (公開URL: /FastTask/foo/) +# 一覧ページ -> docs/index.html (各mdのH1から自動生成) set -euo pipefail cd "$(dirname "$0")/.." command -v gh >/dev/null || { echo "gh コマンドが必要です" >&2; exit 1; } -# docs/google-sync.md → docs/index.html(Pages のトップをセットアップ手順にする) -render() { - local md="$1" out="$2" title="$3" - local body - body=$(gh api --method POST /markdown -f mode=gfm -f text="$(cat "$md")") +SITE_NAME="FastTask" +REPO_URL="https://github.com/riiimparm/FastTask" - cat > "$out" < - - - - -${title} - +${4:+}
-${body} +$3
- + HTML - echo "built: $out" } -render docs/google-sync.md docs/index.html "Google連携セットアップ — FastTask" +toc="" +for md in docs/*.md; do + slug=$(basename "$md" .md) + title=$(sed -n 's/^# //p' "$md" | head -1) + title=${title:-$slug} + # H1 直後の最初の段落を一覧ページの説明に使う + desc=$(awk 'NR>1 && NF && $0 !~ /^#/ { gsub(/\*\*/,""); print; exit }' "$md") + + mkdir -p "docs/$slug" + body=$(gh api --method POST /markdown -f mode=gfm -f text="$(cat "$md")") + page "docs/$slug/index.html" "${title} — ${SITE_NAME}" "$body" "← ドキュメント一覧" + echo "built: docs/$slug/index.html" + + toc="${toc}
  • ${title}

    ${desc}

  • " +done + +page docs/index.html "ドキュメント — ${SITE_NAME}" \ + "

    ${SITE_NAME} ドキュメント

      ${toc}
    " "" +echo "built: docs/index.html" # Pages で Jekyll 処理を走らせない(ここでビルド済みのため) touch docs/.nojekyll diff --git a/src/components/GoogleSyncSettings.tsx b/src/components/GoogleSyncSettings.tsx index a8ded57..22bbd6f 100644 --- a/src/components/GoogleSyncSettings.tsx +++ b/src/components/GoogleSyncSettings.tsx @@ -29,7 +29,7 @@ interface TaskList { // SettingsModal と同じく、この画面のテキストはコンポーネント内に持つ /** セットアップ手順は GitHub Pages で配信(docs/ を scripts/build-docs.sh でビルド) */ -const SETUP_GUIDE_URL = "https://riiimparm.github.io/FastTask/"; +const SETUP_GUIDE_URL = "https://riiimparm.github.io/FastTask/google-sync/"; const T = { ja: { From 6d61550ad30e7869bc071e2ee25659de8fd68c5e Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 05:38:30 +0900 Subject: [PATCH 10/12] =?UTF-8?q?docs:=20=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=83=93=E3=83=AB=E3=83=89=E3=82=92?= =?UTF-8?q?Zensical=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88CI?= =?UTF-8?q?=E3=81=A7=E5=85=AC=E9=96=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自前の build-docs.sh とコミット済みHTMLを削除し、zensical.toml と docs/index.md を追加。出力パスは従来どおり /google-sync/。 docs/ か zensical.toml の変更をトリガにCIでビルドし、mainへの pushのみ GitHub Pages にデプロイする(PRはビルド確認だけ)。 Pages の公開元は GitHub Actions に変更済み。 --- .github/workflows/docs.yml | 53 +++++++++ .gitignore | 3 + docs/.nojekyll | 0 docs/google-sync/index.html | 220 ------------------------------------ docs/index.html | 50 -------- docs/index.md | 12 ++ scripts/build-docs.sh | 99 ---------------- zensical.toml | 55 +++++++++ 8 files changed, 123 insertions(+), 369 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/.nojekyll delete mode 100644 docs/google-sync/index.html delete mode 100644 docs/index.html create mode 100644 docs/index.md delete mode 100644 scripts/build-docs.sh create mode 100644 zensical.toml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..cd414e4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: Documentation + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'zensical.toml' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'zensical.toml' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.x' + # 0.0.x のため固定。更新時はローカルで zensical build を通してから上げる + - run: pip install zensical==0.0.74 + - run: zensical build --clean --strict + - uses: actions/upload-pages-artifact@v5 + with: + path: site + + # 公開は main への push のみ(PRではビルド確認だけ) + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/.gitignore b/.gitignore index b0129f2..85f29df 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? .claude/worktrees/ CLAUDE.md + +# Zensical のビルド出力 +site/ diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/google-sync/index.html b/docs/google-sync/index.html deleted file mode 100644 index 1ea7ce4..0000000 --- a/docs/google-sync/index.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - -Google カレンダー / Google ToDo 連携セットアップ — FastTask - - - - -
    -

    Google カレンダー / Google ToDo 連携セットアップ

    -

    FastTask のタスクを Google へ書き出す(push)だけ の一方向連携です。
    -Google 側で作ったり編集したりした予定・ToDo が FastTask に取り込まれることはありません。

    -

    アプリにはクライアントシークレットを埋め込んでいないため、各自の Google Cloud プロジェクトで
    -OAuth クライアントIDを発行
    して設定画面に入力する必要があります。以下はその手順です。

    -
    -

    1. Google Cloud プロジェクトを作る

    -
      -
    1. https://console.cloud.google.com/ を開く
    2. -
    3. 画面上部のプロジェクト選択 → 「新しいプロジェクト」
    4. -
    5. プロジェクト名は何でもよい(例: FastTask)→ 作成
    6. -
    -

    2. API を有効化する

    -

    左メニュー「API とサービス」→「ライブラリ」から、次の2つを検索して 有効にする。

    -
      -
    • Google Calendar API
    • -
    • Google Tasks API
    • -
    -

    3. OAuth 同意画面を設定する

    -

    左メニュー「API とサービス」→「OAuth 同意画面」。

    -
      -
    1. User Type は 外部(External) を選択(Google Workspace 組織内だけで使うなら「内部」でも可)
    2. -
    3. アプリ名(例: FastTask)、ユーザーサポートメール、デベロッパー連絡先メールを入力
    4. -
    5. スコープ: ここでは何も追加しなくて構いません(アプリ側が要求します)。
      -FastTask が要求するスコープは最小限の3つだけです。 -
        -
      • .../auth/calendar.events … カレンダーの予定の作成・更新
      • -
      • .../auth/tasks … ToDo の作成・更新
      • -
      • .../auth/userinfo.email … 設定画面に「接続中のアカウント」を表示するためだけに使用
      • -
      -
    6. -
    7. テストユーザー: 「公開ステータス」が テスト のままで問題ありません。
      -「テストユーザー」に 自分の Google アカウントのメールアドレスを必ず追加してください。
      -ここに登録していないアカウントでは認証が access_denied で失敗します。
    8. -
    -
    -

    テストモードの refresh token は 7日で失効します。失効したら設定画面で「接続を解除」→
    -「Googleに接続」をやり直してください。恒久的に使いたい場合はアプリを「本番」に公開します
    -(自分だけが使う機密スコープなら審査なしで公開できますが、警告画面は出ます)。

    -
    -

    4. OAuth クライアントID を発行する

    -

    左メニュー「API とサービス」→「認証情報」→「認証情報を作成」→「OAuth クライアント ID」。

    -
      -
    • アプリケーションの種類: 「デスクトップ アプリ」を選んでください。
    • -
    • 名前は任意(例: FastTask desktop)
    • -
    -
    -

    リダイレクトURIの入力欄は出てきません。これで正しいです。
    -「デスクトップ アプリ」タイプは http://127.0.0.1:<任意のポート> へのリダイレクトが
    -自動的に許可される仕様で、FastTask は起動のたびに OS から空きポートを借りて使います
    -(Google: Loopback IP address flow)。
    -もし種類を「ウェブ アプリケーション」で作ってしまうと、ポートが毎回変わるため動きません。作り直してください。

    -
    -

    発行されると クライアントID と クライアントシークレット が表示されます。両方控えます。
    -(デスクトップアプリのシークレットは Google の仕様上「機密ではない」扱いですが、FastTask は
    -念のためローカルに 600 パーミッションで保存します。)

    -

    5. FastTask に入力して接続する

    -
      -
    1. FastTask の 設定(⌘,)→ Google連携
    2. -
    3. クライアントID / クライアントシークレットを貼り付けて「保存」
    4. -
    5. 「Googleに接続」を押す → 既定のブラウザが開く
    6. -
    7. アカウントを選び、権限を許可する -
        -
      • 「このアプリは確認されていません」と出たら「詳細」→「(アプリ名)に移動」を押す(テストモードのため)
      • -
      -
    8. -
    9. ブラウザに「連携が完了しました」と出たらタブを閉じる。設定画面に接続中のアカウントが表示されます。
    10. -
    -

    6. 書き出し設定

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    項目内容
    ToDoに書き出すタスクを Google ToDo に作成。FastTask で完了にすると Google 側も完了になります
    カレンダーに書き出す期日のあるタスクだけを、その日の終日イベントとして作成します
    自動書き出しタスクの追加・更新時に自動で push(既定OFF)。失敗してもアプリの操作は止まりません
    カレンダーID既定 primary(メインのカレンダー)。別カレンダーに入れるなら Google カレンダーの設定からIDをコピー
    ToDoリスト既定 @default。「リスト取得」を押すとリスト名から選べます
    今すぐ同期全タスクを手動で push
    -
    -

    仕様・制限

    -
      -
    • 一方向だけです。 Google 側の変更は FastTask に戻ってきません。
    • -
    • FastTask でタスクを削除しても、Google 側は削除されません(対応表から外れるだけ)。
      -Google 側で手動で消してください。
    • -
    • 期日を外すと、そのタスクのカレンダー予定は削除されます。
    • -
    • 前回 push した内容(タイトル・状態・期日・URL)と変わっていないタスクは API を呼びません。
    • -
    • 自動書き出しは保存のたびに走りますが、前回の push が実行中なら今回はスキップします
      -(最新の状態は次の保存で反映されます)。
    • -
    -

    保存される場所

    -

    アプリデータフォルダ(macOS は ~/Library/Application Support/com.fasttask.desktop/)。

    - - - - - - - - - - - - - - - - - -
    ファイル中身
    google.jsonクライアントID・シークレット・リフレッシュトークン・各種設定(0600)
    google_map.jsonFastTask のタスクID ↔ Google 側ID の対応表
    -

    タスク本体(data.json)に認証情報は混ざりません。
    -「接続を解除」を押すと Google 側でトークンを失効させ、手元の認証情報と対応表を消します。

    -

    うまくいかないとき

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    症状原因と対処
    access_deniedOAuth同意画面の「テストユーザー」に自分のアカウントが入っていない
    refresh_token が返りませんでした同じクライアントで過去に許可済み。設定で「接続を解除」してからやり直す
    invalid_clientクライアントID/シークレットの貼り間違い、または種類が「デスクトップ アプリ」でない
    数日経つと同期エラーテストモードの7日失効。再接続するか、アプリを本番公開する
    Tasks リストが見つかりませんToDoリストIDが違う。「リスト取得」で選び直す
    認証がタイムアウトしましたブラウザでの許可を5分以内に完了する必要があります
    -
    - - - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 30ee420..0000000 --- a/docs/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - -ドキュメント — FastTask - - - - -
    -

    FastTask ドキュメント

    -
    - - - diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7b6c4c3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# FastTask Docs + +FastTask は macOS 向けの軽量タスク管理アプリです。ここにはアプリの設定や外部連携のセットアップ手順を置いています。 + +## ドキュメント + +- [Google連携セットアップ](google-sync.md) — FastTask のタスクを Google カレンダー / Google ToDo へ書き出すための OAuth クライアント設定手順 + +## リンク + +- [リポジトリ](https://github.com/riiimparm/FastTask) +- [リリース](https://github.com/riiimparm/FastTask/releases) diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh deleted file mode 100644 index 550082d..0000000 --- a/scripts/build-docs.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# docs/*.md を GitHub Pages 用の HTML にビルドする。 -# GitHub と同じ GFM レンダラ(gh api /markdown)を使うので npm 依存はゼロ。 -# 使い方: bash scripts/build-docs.sh (gh のログインが必要) -# -# docs/foo.md -> docs/foo/index.html (公開URL: /FastTask/foo/) -# 一覧ページ -> docs/index.html (各mdのH1から自動生成) -set -euo pipefail - -cd "$(dirname "$0")/.." -command -v gh >/dev/null || { echo "gh コマンドが必要です" >&2; exit 1; } - -SITE_NAME="FastTask" -REPO_URL="https://github.com/riiimparm/FastTask" - -style() { - cat <<'CSS' - :root { color-scheme: light dark; --bg:#fff; --fg:#1b1b1f; --sub:#5c5f6b; --line:#e4e4e8; --code:#f4f4f6; --link:#2f6feb; } - @media (prefers-color-scheme: dark) { - :root { --bg:#16161a; --fg:#e8e8ec; --sub:#9b9ba6; --line:#2b2b33; --code:#1f1f26; --link:#7aa6ff; } - } - * { box-sizing: border-box; } - body { - margin: 0; background: var(--bg); color: var(--fg); - font-family: -apple-system, BlinkMacSystemFont, "Hiragino Sans", "Noto Sans JP", "Segoe UI", sans-serif; - line-height: 1.85; font-size: 16px; - } - main { max-width: 720px; margin: 0 auto; padding: 40px 16px 96px; } - nav { max-width: 720px; margin: 0 auto; padding: 24px 16px 0; font-size: 0.85rem; color: var(--sub); } - h1 { font-size: 1.7rem; letter-spacing: -0.01em; margin: 0 0 1.5rem; } - h2 { font-size: 1.2rem; margin: 2.75rem 0 0.75rem; padding-top: 1.25rem; border-top: 1px solid var(--line); } - h2:first-of-type, hr + h2 { border-top: none; padding-top: 0; } - h3 { font-size: 1rem; margin: 1.75rem 0 0.5rem; } - a { color: var(--link); text-decoration: none; } - a:hover { text-decoration: underline; } - ul, ol { padding-left: 1.4em; } - li + li { margin-top: 0.3em; } - code { background: var(--code); padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.88em; } - pre { background: var(--code); padding: 1rem; border-radius: 8px; overflow-x: auto; } - pre code { background: none; padding: 0; } - blockquote { margin: 1.25rem 0; padding: 0.1rem 1rem; border-left: 3px solid var(--line); color: var(--sub); } - table { border-collapse: collapse; width: 100%; font-size: 0.93em; } - th, td { border: 1px solid var(--line); padding: 0.5rem 0.7rem; text-align: left; } - hr { border: none; border-top: 1px solid var(--line); margin: 2.5rem 0; } - img { max-width: 100%; } - .toc { list-style: none; padding: 0; } - .toc li { border-top: 1px solid var(--line); padding: 1rem 0; margin: 0; } - .toc a { font-size: 1.05rem; font-weight: 600; } - .toc p { margin: 0.25rem 0 0; color: var(--sub); font-size: 0.9rem; } - footer { max-width: 720px; margin: 0 auto; padding: 0 16px 64px; color: var(--sub); font-size: 0.85rem; } -CSS -} - -# $1=出力パス $2= $3=本文HTML $4=上部ナビHTML(空可) -page() { - cat > "$1" <<HTML -<!doctype html> -<html lang="ja"> -<head> -<meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1"> -<title>$2 - - - -${4:+} -
    -$3 -
    - - - -HTML -} - -toc="" -for md in docs/*.md; do - slug=$(basename "$md" .md) - title=$(sed -n 's/^# //p' "$md" | head -1) - title=${title:-$slug} - # H1 直後の最初の段落を一覧ページの説明に使う - desc=$(awk 'NR>1 && NF && $0 !~ /^#/ { gsub(/\*\*/,""); print; exit }' "$md") - - mkdir -p "docs/$slug" - body=$(gh api --method POST /markdown -f mode=gfm -f text="$(cat "$md")") - page "docs/$slug/index.html" "${title} — ${SITE_NAME}" "$body" "← ドキュメント一覧" - echo "built: docs/$slug/index.html" - - toc="${toc}
  • ${title}

    ${desc}

  • " -done - -page docs/index.html "ドキュメント — ${SITE_NAME}" \ - "

    ${SITE_NAME} ドキュメント

      ${toc}
    " "" -echo "built: docs/index.html" - -# Pages で Jekyll 処理を走らせない(ここでビルド済みのため) -touch docs/.nojekyll diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..0fd8179 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,55 @@ +[project] +site_url = "https://riiimparm.github.io/FastTask/" +site_name = "FastTask Docs" +site_description = "FastTask のドキュメント" + +repo_url = "https://github.com/riiimparm/FastTask" +repo_name = "riiimparm/FastTask" +edit_uri = "edit/main/docs/" + +nav = [ + { "FastTask Docs" = "index.md" }, + { "Google連携セットアップ" = "google-sync.md" }, +] + +[project.theme] +language = "ja" +features = [ + "content.code.copy", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.follow", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "ライトモードに切り替え" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "ダークモードに切り替え" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "システム設定に従う" + +[project.markdown_extensions] +admonition = {} +attr_list = {} +md_in_html = {} +toc.permalink = true +pymdownx.details = {} +pymdownx.highlight.anchor_linenums = true +pymdownx.inlinehilite = {} +pymdownx.magiclink = {} +pymdownx.superfences = {} +pymdownx.tasklist.custom_checkbox = true From 008aed8f0982529c346c045cbd9971f33e191512 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 05:56:32 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=E5=AE=8C=E4=BA=86=E9=9F=B3?= =?UTF-8?q?=E3=82=92=E6=8A=9C=E3=81=91=E3=81=AE=E3=81=82=E3=82=8B=E9=9F=B3?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97=E3=82=BF=E3=82=A4=E3=83=97?= =?UTF-8?q?=E9=9F=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完了音は C6-E6-G6 のアルペジオにバンドパスを上へ掃引する ノイズを重ね、スカッと抜ける質感にした。 キー入力ごとに鳴る type 音を追加(12ms・ごく小音量・毎回ピッチを わずかに散らす)。タイプは速度に追従するようスロットルを18msにし、 修飾キー単独とショートカット操作では鳴らさない。 --- .python-version | 1 + main.py | 6 + pyproject.toml | 12 ++ src/components/TaskInput.tsx | 4 + src/components/TaskItem.tsx | 3 + src/i18n.ts | 4 +- src/utils/sound.test.ts | 11 +- src/utils/sound.ts | 67 ++++++- uv.lock | 347 +++++++++++++++++++++++++++++++++++ 9 files changed, 446 insertions(+), 9 deletions(-) create mode 100644 .python-version create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/main.py b/main.py new file mode 100644 index 0000000..ca9ef76 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from fasttask!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dd19aaa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "fasttask" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[dependency-groups] +dev = [ + "zensical>=0.0.74", +] diff --git a/src/components/TaskInput.tsx b/src/components/TaskInput.tsx index 9cccf53..ca5f13b 100644 --- a/src/components/TaskInput.tsx +++ b/src/components/TaskInput.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useStore } from "../store"; +import { playKeystroke } from "../utils/sound"; import { useUiContext } from "../context/UiContext"; import { parseProjectFromInput } from "../utils/project"; import { parseDateFromText, formatDateShort, dateToIso, isWithinDueWindow } from "../utils/parseDate"; @@ -150,8 +151,11 @@ export function TaskInput() { finalizeSubmit(parts, iso, firstProject); } + const soundEnabled = useStore((s) => s.settings.soundEnabled); + function onKeyDown(e: React.KeyboardEvent) { if (e.nativeEvent.isComposing || e.keyCode === 229) return; + if (soundEnabled) playKeystroke(e); if (e.key === "Escape" && searchMode) { e.preventDefault(); diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index f16f1e3..c74281c 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -3,6 +3,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useStore } from "../store"; +import { playKeystroke } from "../utils/sound"; import { useUiContext } from "../context/UiContext"; import { Tag, Task } from "../types"; import { Popover } from "./Popover"; @@ -32,6 +33,7 @@ export function TaskItem({ task, draggable = true }: Props) { const urlEnabled = settings.urlEnabled ?? true; const depth = useMemo(() => getDepth(task, allTasks), [task, allTasks]); + const soundEnabled = useStore((s) => s.settings.soundEnabled); const isLocked = useMemo(() => hasUndoneDescendants(task.id, allTasks), [task.id, allTasks]); const { selectedTaskId, focusedTaskId, isReorderMode, bulkSelected, setSelectedTaskId, vibratingTaskId, completingTaskIds } = useUiContext(); @@ -233,6 +235,7 @@ export function TaskItem({ task, draggable = true }: Props) { onBlur={commitEdit} onKeyDown={(e) => { if (e.nativeEvent.isComposing || e.keyCode === 229) return; + if (soundEnabled) playKeystroke(e); if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { setEditValue(task.title); diff --git a/src/i18n.ts b/src/i18n.ts index a0f99e7..02de76a 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -42,7 +42,7 @@ const dict = { setting_tagsEnabled: "タグ機能", setting_tagsEnabled_desc: "タグの作成・タスクへの付与を有効にする", setting_soundEnabled: "操作音", - setting_soundEnabled_desc: "タスクの作成・完了・削除時に短い効果音を鳴らす", + setting_soundEnabled_desc: "タスクの作成・完了・削除とキー入力に短い効果音を鳴らす", tagFilter: "タグ絞込", theme: "テーマ", themeLight: "ライト", @@ -128,7 +128,7 @@ const dict = { setting_tagsEnabled: "Tags feature", setting_tagsEnabled_desc: "Enable tag creation and assignment", setting_soundEnabled: "Action sounds", - setting_soundEnabled_desc: "Play a short sound when creating, completing or deleting a task", + setting_soundEnabled_desc: "Play a short sound on keystrokes and when creating, completing or deleting a task", tagFilter: "Tag filter", theme: "Theme", themeLight: "Light", diff --git a/src/utils/sound.test.ts b/src/utils/sound.test.ts index 209c5be..55ae965 100644 --- a/src/utils/sound.test.ts +++ b/src/utils/sound.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { shouldPlay } from "./sound.ts"; +import { shouldPlay, throttleFor } from "./sound.ts"; test("shouldPlay: 同種音は60ms未満で抑制、別種と経過後は鳴る", () => { const last: Record = {}; @@ -10,3 +10,12 @@ test("shouldPlay: 同種音は60ms未満で抑制、別種と経過後は鳴る" assert.equal(shouldPlay("create", 1060, last), true); // 60ms経過で再生 assert.equal(shouldPlay("nav", 1030, last), true); // 別種はスロットルされない }); + +test("throttleFor: タイプ音だけ短い間隔で鳴る", () => { + assert.equal(throttleFor("type"), 18); + assert.equal(throttleFor("complete"), 60); + + const last: Record = { type: 1000 }; + assert.equal(shouldPlay("type", 1020, last), true); // 速いタイピングでも追従 + assert.equal(shouldPlay("type", 1010, last), false); // 極端な連打は抑制 +}); diff --git a/src/utils/sound.ts b/src/utils/sound.ts index d059bb5..77fae9e 100644 --- a/src/utils/sound.ts +++ b/src/utils/sound.ts @@ -1,17 +1,24 @@ // 操作音。音源ファイルは使わず Web Audio でシンセ生成(ライセンス不要・バイナリ増ゼロ・依存ゼロ)。 // 呼び出し側(store)が settings.soundEnabled を見て呼ぶ。無効時はここに来ないので AudioContext も作られない。 -export type SoundKind = "create" | "complete" | "delete" | "nav"; +export type SoundKind = "create" | "complete" | "delete" | "nav" | "type"; /** 同種音の最小間隔(ms)。連打で音が重なって割れるのを防ぐ */ const THROTTLE_MS = 60; +/** タイプ音だけは速い連打が前提なので短く */ +const THROTTLE_BY_KIND: Partial> = { type: 18 }; + +export function throttleFor(kind: SoundKind): number { + return THROTTLE_BY_KIND[kind] ?? THROTTLE_MS; +} + /** 直近再生時刻を見て再生可否を判定する純粋関数(テスト用に分離) */ export function shouldPlay( kind: SoundKind, now: number, last: Record, - intervalMs: number = THROTTLE_MS, + intervalMs: number = throttleFor(kind), ): boolean { const prev = last[kind]; return prev === undefined || now - prev >= intervalMs; @@ -23,10 +30,11 @@ type Note = { freq: number; at: number; dur: number; gain: number; type: Oscilla const VOICES: Record = { // 作成: 軽いブリップ(A5, 三角波 50ms) create: [{ freq: 880, at: 0, dur: 0.05, gain: 0.09, type: "triangle" }], - // 完了: 上行2音(E5 -> B5)で達成感を出す + // 完了: 明るい三和音アルペジオ(C6 -> E6 -> G6)。下の NOISE と重ねて「スカッ」と抜ける音に complete: [ - { freq: 659.25, at: 0, dur: 0.07, gain: 0.08, type: "sine" }, - { freq: 987.77, at: 0.065, dur: 0.11, gain: 0.08, type: "sine" }, + { freq: 1046.5, at: 0, dur: 0.14, gain: 0.07, type: "sine" }, + { freq: 1318.51, at: 0.045, dur: 0.15, gain: 0.065, type: "sine" }, + { freq: 1567.98, at: 0.085, dur: 0.22, gain: 0.06, type: "sine" }, ], // 削除: 下行2音(A4 -> D4) delete: [ @@ -35,8 +43,27 @@ const VOICES: Record = { ], // 移動: ごく短く静かなクリック(A6 25ms) nav: [{ freq: 1174.66, at: 0, dur: 0.025, gain: 0.035, type: "sine" }], + // タイプ: 1打ごとに鳴るのでごく短く・ごく小さく。周波数は play() で僅かに揺らす + type: [{ freq: 1500, at: 0, dur: 0.012, gain: 0.02, type: "triangle" }], }; +/** ノイズ成分(完了音の「スカッ」という抜け)。バンドパスを上に掃引する */ +type Swish = { at: number; dur: number; gain: number; from: number; to: number }; +const NOISE: Partial> = { + complete: { at: 0, dur: 0.18, gain: 0.05, from: 1200, to: 6000 }, +}; + +let noiseBuf: AudioBuffer | null = null; +function getNoiseBuffer(audio: AudioContext): AudioBuffer { + if (noiseBuf && noiseBuf.sampleRate === audio.sampleRate) return noiseBuf; + const len = Math.floor(audio.sampleRate * 0.3); + const buf = audio.createBuffer(1, len, audio.sampleRate); + const data = buf.getChannelData(0); + for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1; + noiseBuf = buf; + return buf; +} + let ctx: AudioContext | null = null; const lastPlayed: Record = {}; @@ -63,7 +90,9 @@ export function play(kind: SoundKind, volume = 1): void { const osc = audio.createOscillator(); const gain = audio.createGain(); osc.type = n.type; - osc.frequency.setValueAtTime(n.freq, t0 + n.at); + // タイプ音は機械的に聞こえないよう毎回わずかにピッチを散らす + const freq = kind === "type" ? n.freq * (0.96 + Math.random() * 0.08) : n.freq; + osc.frequency.setValueAtTime(freq, t0 + n.at); // 指数減衰エンベロープ(1ms でピーク → 減衰、クリックノイズを避ける) const peak = Math.max(0.0001, n.gain * volume); gain.gain.setValueAtTime(0.0001, t0 + n.at); @@ -73,4 +102,30 @@ export function play(kind: SoundKind, volume = 1): void { osc.start(t0 + n.at); osc.stop(t0 + n.at + n.dur + 0.01); } + + const sw = NOISE[kind]; + if (sw) { + const src = audio.createBufferSource(); + src.buffer = getNoiseBuffer(audio); + const bp = audio.createBiquadFilter(); + bp.type = "bandpass"; + bp.Q.value = 0.9; + bp.frequency.setValueAtTime(sw.from, t0 + sw.at); + bp.frequency.exponentialRampToValueAtTime(sw.to, t0 + sw.at + sw.dur); + const g = audio.createGain(); + const peak = Math.max(0.0001, sw.gain * volume); + g.gain.setValueAtTime(0.0001, t0 + sw.at); + g.gain.exponentialRampToValueAtTime(peak, t0 + sw.at + 0.012); + g.gain.exponentialRampToValueAtTime(0.0001, t0 + sw.at + sw.dur); + src.connect(bp).connect(g).connect(audio.destination); + src.start(t0 + sw.at); + src.stop(t0 + sw.at + sw.dur + 0.01); + } +} + +/** キー入力1打ぶんのタイプ音。修飾キー単独とショートカット操作では鳴らさない */ +export function playKeystroke(e: { key: string; metaKey: boolean; ctrlKey: boolean; altKey: boolean }): void { + if (e.metaKey || e.ctrlKey || e.altKey) return; + if (e.key.length !== 1 && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "Enter") return; + play("type"); } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ef2a09d --- /dev/null +++ b/uv.lock @@ -0,0 +1,347 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "deepmerge" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/e9/5e99e56e03f9461858e686fdaf82e2516671bc1bd46f8b7ad7b9a8375f6f/deepmerge-4.0.0.tar.gz", hash = "sha256:948565f2bc9cdb41259a4a23d26638e5840c9e084a343ab279ffd3edcfdf74d3", size = 22792, upload-time = "2026-10-25T19:25:28.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/89/362146b2ec7d0d503f824ac8496c88f0408ee0d39203b02cc10feac08146/deepmerge-4.0.0-py3-none-any.whl", hash = "sha256:a36dd5810fc3b90bd3e0b681158221361d6a31a6109411b27b211b371fed6d5b", size = 15429, upload-time = "2026-10-25T19:25:28.012Z" }, +] + +[[package]] +name = "fasttask" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "zensical" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "zensical", specifier = ">=0.0.74" }] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/32/0b4d0297064244f04af4331b65de16237cc3cce07474171ee14222a28981/markdown-3.11.2.tar.gz", hash = "sha256:87bf5d90f2d2882e2c02fcda182c2800456b985bff38913867c49706481514b0", size = 491445, upload-time = "2026-10-19T19:41:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/66/bcab3b677b71ad286b4e607e3427499537c76e13ad2e22584ba7a7d9882c/markdown-3.11.2-py3-none-any.whl", hash = "sha256:f80390747a2a0ff2ba54da074ce4f862f72a6685f608cd2637b646a6d83993f5", size = 114266, upload-time = "2026-10-19T19:41:41.244Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "tomli" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.74" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pathspec" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, + { name = "zensical-ui" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/41/147eed1f072917de5d91e2aa0c2204edf87e5e9c7cd34c314177969d5281/zensical-0.0.74.tar.gz", hash = "sha256:f2ddd17daead93fdbcaa42b5bdc367f9a4808bad282fe706763cb52dba29d96b", size = 514731, upload-time = "2026-10-30T22:09:31.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/a3/4031992e04a323f09b3fb89531fdce6c13f83650533a39809ddab9b4a4ef/zensical-0.0.74-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:44f5afe36d1fda5a80a2e9b24cecd25e2d6e6f1f05845299284108d7be40a48b", size = 8621183, upload-time = "2026-10-30T22:09:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/6d56d6886637927204180ddf8480483f9ba4f838cf23f871c02845e8b1ef/zensical-0.0.74-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:912a6a5eb834b283e31ca89341026ccb0bba260351f9123139dbb476a3daf505", size = 8217741, upload-time = "2026-10-30T22:09:12.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/deedbf55d985629af31cdbd126a83d60c89440d32d94fd86c9ab7b13a215/zensical-0.0.74-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7722e1c2505925a477719b79fdc25defe53ce032bab1d769bd4861b130cf149c", size = 8661389, upload-time = "2026-10-30T22:09:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/48/77a87b588801a6945a274415bffd0ffe7d7781424bec449d54422780784f/zensical-0.0.74-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9da81ad4bb42bdd92a52d9e42623eaa25c936fa0df0d26e507dcd282e8bf85b2", size = 9151632, upload-time = "2026-10-30T22:09:17.611Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/c8ffff0de58e6f8810b1d7609442efa2919058c8d1035d266db4b2aad7d9/zensical-0.0.74-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4df8506f0c13226436c6955203b3332e5334b579a1daeeb3c9e7749c726e5327", size = 8830888, upload-time = "2026-10-30T22:09:20.736Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c6/3eff7d34d94fe1978637d60094db4c407e9f3779a41f37fa678a6d8e7356/zensical-0.0.74-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a6fad6241a6fb04012ca87bf31e11f7cc3b3248a84fe4bba3e3b86a3506cb09", size = 9422666, upload-time = "2026-10-30T22:09:23.882Z" }, + { url = "https://files.pythonhosted.org/packages/cf/91/dc6a6783b62d82ad872beebb14339d0e9b8372dd8b74bd5c4b388d8dab1d/zensical-0.0.74-cp311-abi3-win_amd64.whl", hash = "sha256:491ee04190f8fc0b731a40d99062f979a508b522265c5042cbd79eb7a59e250d", size = 9356286, upload-time = "2026-10-30T22:09:26.574Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/fa31d226655699c974137499a98dd5a188882f0c9460ba584107e072ad2d/zensical-0.0.74-cp311-abi3-win_arm64.whl", hash = "sha256:a43b3801fb6e90f774d32b4e485a81cdf028a5765d0d5398d28cf406be888731", size = 8750302, upload-time = "2026-10-30T22:09:29.718Z" }, +] + +[[package]] +name = "zensical-ui" +version = "0.0.41" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/a7/77f1d045900f37f703bef9f64af8c8ca3e9419ca6dbfbc7885e33ca8f2a1/zensical_ui-0.0.41.tar.gz", hash = "sha256:2268e3533a2e186eb7b67dcc8ccdbfc5994fe114cda9382a505f306256df3d88", size = 4063232, upload-time = "2026-10-30T19:06:00.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/84/bb9f8022f2a4fc57c53e135d24318c880205038da644432d443598e752d1/zensical_ui-0.0.41-py3-none-any.whl", hash = "sha256:f0cd5b4351c130257df65b2100fc7116e72c777b1fbc596fee64a9f5de4ccdad", size = 9961148, upload-time = "2026-10-30T19:05:58.612Z" }, +] From 5af87fd96e39619798ba6d9a669bc9a74e096d5b Mon Sep 17 00:00:00 2001 From: riiimparm Date: Tue, 3 Nov 2026 06:00:53 +0900 Subject: [PATCH 12/12] =?UTF-8?q?chore:=20uv=20=E3=81=AE=E9=9B=9B=E5=BD=A2?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zensical はCIで pip install、ローカルでは uvx 実行なので リポジトリに uv プロジェクトは持たない。 --- .python-version | 1 - main.py | 6 - pyproject.toml | 12 -- uv.lock | 347 ------------------------------------------------ 4 files changed, 366 deletions(-) delete mode 100644 .python-version delete mode 100644 main.py delete mode 100644 pyproject.toml delete mode 100644 uv.lock diff --git a/.python-version b/.python-version deleted file mode 100644 index e4fba21..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/main.py b/main.py deleted file mode 100644 index ca9ef76..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from fasttask!") - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index dd19aaa..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -[project] -name = "fasttask" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [] - -[dependency-groups] -dev = [ - "zensical>=0.0.74", -] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index ef2a09d..0000000 --- a/uv.lock +++ /dev/null @@ -1,347 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "click" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, -] - -[[package]] -name = "deepmerge" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/e9/5e99e56e03f9461858e686fdaf82e2516671bc1bd46f8b7ad7b9a8375f6f/deepmerge-4.0.0.tar.gz", hash = "sha256:948565f2bc9cdb41259a4a23d26638e5840c9e084a343ab279ffd3edcfdf74d3", size = 22792, upload-time = "2026-10-25T19:25:28.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/89/362146b2ec7d0d503f824ac8496c88f0408ee0d39203b02cc10feac08146/deepmerge-4.0.0-py3-none-any.whl", hash = "sha256:a36dd5810fc3b90bd3e0b681158221361d6a31a6109411b27b211b371fed6d5b", size = 15429, upload-time = "2026-10-25T19:25:28.012Z" }, -] - -[[package]] -name = "fasttask" -version = "0.1.0" -source = { virtual = "." } - -[package.dev-dependencies] -dev = [ - { name = "zensical" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [{ name = "zensical", specifier = ">=0.0.74" }] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markdown" -version = "3.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/32/0b4d0297064244f04af4331b65de16237cc3cce07474171ee14222a28981/markdown-3.11.2.tar.gz", hash = "sha256:87bf5d90f2d2882e2c02fcda182c2800456b985bff38913867c49706481514b0", size = 491445, upload-time = "2026-10-19T19:41:42.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/66/bcab3b677b71ad286b4e607e3427499537c76e13ad2e22584ba7a7d9882c/markdown-3.11.2-py3-none-any.whl", hash = "sha256:f80390747a2a0ff2ba54da074ce4f862f72a6685f608cd2637b646a6d83993f5", size = 114266, upload-time = "2026-10-19T19:41:41.244Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, - { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, - { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, - { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, - { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, - { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, - { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, - { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, - { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, - { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, - { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, - { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, - { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, - { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, - { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, - { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, - { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, - { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, - { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, - { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, - { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, - { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, - { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, - { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, - { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, - { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, - { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, - { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, - { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, - { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, - { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, - { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, - { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, - { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, - { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, - { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, - { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, - { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, - { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, - { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, - { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, - { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, - { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, - { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, - { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, - { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, - { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, - { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, - { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, - { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, - { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, - { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, -] - -[[package]] -name = "pygments" -version = "2.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "tomli" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, - { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, - { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, - { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, - { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, - { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, - { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, - { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, - { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, - { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, - { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, - { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, - { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, - { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, - { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, - { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, - { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, - { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, - { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, - { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, - { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, - { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, - { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, - { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, - { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, - { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, - { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, - { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, -] - -[[package]] -name = "zensical" -version = "0.0.74" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "deepmerge" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "pathspec" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "pyyaml" }, - { name = "tomli" }, - { name = "zensical-ui" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/41/147eed1f072917de5d91e2aa0c2204edf87e5e9c7cd34c314177969d5281/zensical-0.0.74.tar.gz", hash = "sha256:f2ddd17daead93fdbcaa42b5bdc367f9a4808bad282fe706763cb52dba29d96b", size = 514731, upload-time = "2026-10-30T22:09:31.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/a3/4031992e04a323f09b3fb89531fdce6c13f83650533a39809ddab9b4a4ef/zensical-0.0.74-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:44f5afe36d1fda5a80a2e9b24cecd25e2d6e6f1f05845299284108d7be40a48b", size = 8621183, upload-time = "2026-10-30T22:09:09.263Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0a/6d56d6886637927204180ddf8480483f9ba4f838cf23f871c02845e8b1ef/zensical-0.0.74-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:912a6a5eb834b283e31ca89341026ccb0bba260351f9123139dbb476a3daf505", size = 8217741, upload-time = "2026-10-30T22:09:12.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/deedbf55d985629af31cdbd126a83d60c89440d32d94fd86c9ab7b13a215/zensical-0.0.74-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7722e1c2505925a477719b79fdc25defe53ce032bab1d769bd4861b130cf149c", size = 8661389, upload-time = "2026-10-30T22:09:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/48/77a87b588801a6945a274415bffd0ffe7d7781424bec449d54422780784f/zensical-0.0.74-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9da81ad4bb42bdd92a52d9e42623eaa25c936fa0df0d26e507dcd282e8bf85b2", size = 9151632, upload-time = "2026-10-30T22:09:17.611Z" }, - { url = "https://files.pythonhosted.org/packages/20/80/c8ffff0de58e6f8810b1d7609442efa2919058c8d1035d266db4b2aad7d9/zensical-0.0.74-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4df8506f0c13226436c6955203b3332e5334b579a1daeeb3c9e7749c726e5327", size = 8830888, upload-time = "2026-10-30T22:09:20.736Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c6/3eff7d34d94fe1978637d60094db4c407e9f3779a41f37fa678a6d8e7356/zensical-0.0.74-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a6fad6241a6fb04012ca87bf31e11f7cc3b3248a84fe4bba3e3b86a3506cb09", size = 9422666, upload-time = "2026-10-30T22:09:23.882Z" }, - { url = "https://files.pythonhosted.org/packages/cf/91/dc6a6783b62d82ad872beebb14339d0e9b8372dd8b74bd5c4b388d8dab1d/zensical-0.0.74-cp311-abi3-win_amd64.whl", hash = "sha256:491ee04190f8fc0b731a40d99062f979a508b522265c5042cbd79eb7a59e250d", size = 9356286, upload-time = "2026-10-30T22:09:26.574Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f2/fa31d226655699c974137499a98dd5a188882f0c9460ba584107e072ad2d/zensical-0.0.74-cp311-abi3-win_arm64.whl", hash = "sha256:a43b3801fb6e90f774d32b4e485a81cdf028a5765d0d5398d28cf406be888731", size = 8750302, upload-time = "2026-10-30T22:09:29.718Z" }, -] - -[[package]] -name = "zensical-ui" -version = "0.0.41" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/a7/77f1d045900f37f703bef9f64af8c8ca3e9419ca6dbfbc7885e33ca8f2a1/zensical_ui-0.0.41.tar.gz", hash = "sha256:2268e3533a2e186eb7b67dcc8ccdbfc5994fe114cda9382a505f306256df3d88", size = 4063232, upload-time = "2026-10-30T19:06:00.915Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/84/bb9f8022f2a4fc57c53e135d24318c880205038da644432d443598e752d1/zensical_ui-0.0.41-py3-none-any.whl", hash = "sha256:f0cd5b4351c130257df65b2100fc7116e72c777b1fbc596fee64a9f5de4ccdad", size = 9961148, upload-time = "2026-10-30T19:05:58.612Z" }, -]