diff --git a/.github/workflows/make-and-test.yml b/.github/workflows/make-and-test.yml index 088354e702..2d9dc67b43 100644 --- a/.github/workflows/make-and-test.yml +++ b/.github/workflows/make-and-test.yml @@ -106,7 +106,6 @@ jobs: pip install ipykernel python -m ipykernel install --prefix=./jupyter-local --name python3-local --display-name "Python 3 (Local)" - - name: install pnpm uses: pnpm/action-setup@v4 with: @@ -128,6 +127,114 @@ jobs: name: "test-results-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}" path: 'src/packages/*/junit.xml' + - name: Start CoCalc Hub + run: | + # Create conat password for hub internal authentication + mkdir -p src/data/secrets + echo "test-conat-password-$(date +%s)" > src/data/secrets/conat-password + chmod 600 src/data/secrets/conat-password + + cd src/packages/hub + pnpm run hub-project-dev-nobuild > hub.log 2>&1 & + HUB_PID=$! + echo $HUB_PID > hub.pid + echo "Hub started with PID $HUB_PID" + # Check if process is still running after a moment + sleep 2 + if ! kill -0 $HUB_PID 2>/dev/null; then + echo "Error: Hub process died immediately after starting" + echo "Hub log:" + cat hub.log + exit 1 + fi + env: + PGDATABASE: smc + PGUSER: smc + PGHOST: localhost + COCALC_MODE: single-user + COCALC_TEST_MODE: yes + DEBUG: 'cocalc:*,-cocalc:silly:*,hub:*,project:*' + + - name: Wait for hub readiness + run: | + MAX_ATTEMPTS=30 + READY=false + for i in $(seq 1 $MAX_ATTEMPTS); do + if curl -sf --max-time 3 http://localhost:5000/healthcheck > /dev/null; then + echo "Hub is ready" + READY=true + break + fi + echo "Waiting for hub... ($i/$MAX_ATTEMPTS)" + sleep 3 + done + if [ "$READY" = "false" ]; then + echo "Hub failed to become ready after $MAX_ATTEMPTS attempts" + echo "Hub log:" + cat src/packages/hub/hub.log || echo "No log file found" + exit 1 + fi + + - name: Create CI admin user and API key + run: | + cd src/packages/hub + node dist/run/test-create-admin.js > ../../api_key.txt + # Validate API key was created + if [ ! -s ../../api_key.txt ]; then + echo "Error: API key file is empty or missing" + exit 1 + fi + API_KEY=$(cat ../../api_key.txt) + if ! echo "$API_KEY" | grep -qE '^sk-[A-Za-z0-9]+$'; then + echo "Error: Invalid API key format: $API_KEY" + exit 1 + fi + echo "API key created successfully" + env: + PGDATABASE: smc + PGUSER: smc + PGHOST: localhost + + - name: Install uv for cocalc-api tests + run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Run cocalc-api tests + run: | + export COCALC_API_KEY=$(cat src/api_key.txt) + export COCALC_HOST=http://localhost:5000 + cd src/python/cocalc-api && make ci + env: + PGDATABASE: smc + PGUSER: smc + PGHOST: localhost + + - name: Stop CoCalc Hub + if: always() + run: | + if [ -f src/packages/hub/hub.pid ]; then + HUB_PID=$(cat src/packages/hub/hub.pid) + echo "Stopping hub with PID $HUB_PID" + kill $HUB_PID || true + # Wait a bit for graceful shutdown + sleep 5 + # Force kill if still running + kill -9 $HUB_PID 2>/dev/null || true + fi + + - name: Upload hub logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: "hub-logs-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}" + path: 'src/packages/hub/hub.log' + + - name: Upload cocalc-api test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: "cocalc-api-test-results-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}" + path: 'src/python/cocalc-api/test-results.xml' + report: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index b03ea39967..f7f5392d95 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,12 @@ src/conf/tinc_hosts/ # mocha: banket coverage report src/coverage src/*/coverage + +# Python coverage files +.coverage +.coverage.* +htmlcov/ +**/htmlcov/ # comes up when testing in that directory src/rethinkdb_data/ src/dev/project/rethinkdb_data/ diff --git a/src/.claude/settings.json b/src/.claude/settings.json index 1f2ea05e47..1a86572627 100644 --- a/src/.claude/settings.json +++ b/src/.claude/settings.json @@ -17,6 +17,7 @@ "Bash(git commit:*)", "Bash(git push:*)", "Bash(grep:*)", + "Bash(make:*)", "Bash(node:*)", "Bash(npm show:*)", "Bash(npm view:*)", @@ -29,6 +30,10 @@ "Bash(pnpm build:*)", "Bash(pnpm exec tsc:*)", "Bash(pnpm i18n:*)", + "Bash(pnpm i18n:compile:*)", + "Bash(pnpm i18n:download:*)", + "Bash(pnpm i18n:extract:*)", + "Bash(pnpm i18n:upload:*)", "Bash(pnpm i18n:*:*)", "Bash(pnpm info:*)", "Bash(pnpm list:*)", @@ -43,6 +48,7 @@ "Bash(prettier -w:*)", "Bash(psql:*)", "Bash(python3:*)", + "Bash(uv:*)", "Bash(timeout:*)", "WebFetch", "WebSearch", diff --git a/src/AGENTS.md b/src/AGENTS.md index 0fa74f9444..1b12f60ef1 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -105,6 +105,20 @@ CoCalc is organized as a monorepo with key packages: 5. **Authentication**: Each conat request includes account_id and is subject to permission checks at the hub level 6. **Subjects**: Messages are routed using hierarchical subjects like `hub.account.{uuid}.{service}` or `project.{uuid}.{compute_server_id}.{service}` +#### CoCalc Conat Hub API Architecture + +**API Method Registration Pattern:** +- **Registry**: `packages/conat/hub/api/projects.ts` contains `export const projects = { methodName: authFirstRequireAccount }` +- **Implementation**: `packages/server/conat/api/projects.ts` contains `export async function methodName() { ... }` +- **Flow**: Python client `@api_method("projects.methodName")` → POST `/api/conat/hub` → `hubBridge()` → conat subject `hub.account.{account_id}.api` → registry lookup → implementation + +**Example - projects.createProject:** +1. **Python**: `@api_method("projects.createProject")` decorator +2. **HTTP**: `POST /api/conat/hub {"name": "projects.createProject", "args": [...]}` +3. **Bridge**: `hubBridge()` routes to conat subject +4. **Registry**: `packages/conat/hub/api/projects.ts: createProject: authFirstRequireAccount` +5. **Implementation**: `packages/server/conat/api/projects.ts: export { createProject }` → `@cocalc/server/projects/create` + ### Key Technologies - **TypeScript**: Primary language for all new code @@ -216,11 +230,72 @@ Same flow as above, but **before 3. i18n:upload**, delete the key. Only new keys - Ignore everything in `node_modules` or `dist` directories - Ignore all files not tracked by Git, unless they are newly created files -# Important Instruction Reminders +# CoCalc Python API Client Investigation + +## Overview + +The `python/cocalc-api/` directory contains a Python client library for the CoCalc API, published as the `cocalc-api` package on PyPI. + +## Client-Server Architecture Investigation + +### API Call Flow + +1. **cocalc-api Client** (Python) → HTTP POST requests +2. **Next.js API Routes** (`/api/conat/{hub,project}`) → Bridge to conat messaging +3. **ConatClient** (server-side) → NATS-like messaging protocol +4. **Hub API Implementation** (`packages/conat/hub/api/`) → Actual business logic + +### Endpoints Discovered + +#### Hub API: `POST /api/conat/hub` +- **Bridge**: `packages/next/pages/api/conat/hub.ts` → `hubBridge()` → conat subject `hub.account.{account_id}.api` +- **Implementation**: `packages/conat/hub/api/projects.ts` +- **Available Methods**: `createProject`, `start`, `stop`, `setQuotas`, `addCollaborator`, `removeCollaborator`, etc. +- **Missing**: ❌ **No `delete` method implemented in conat hub API** + +#### Project API: `POST /api/conat/project` +- **Bridge**: `packages/next/pages/api/conat/project.ts` → `projectBridge()` → conat project subjects +- **Implementation**: `packages/conat/project/api/` (system.ping, system.exec, system.jupyterExecute) + +### Project Deletion Investigation + +#### ✅ Next.js v2 API Route Available +- **Endpoint**: `packages/next/pages/api/v2/projects/delete.ts` +- **Functionality**: Sets deleted=true, removes licenses, stops project +- **Authentication**: Requires collaborator access or admin + +#### ❌ Missing Conat Hub API Method +- **Current Methods**: Only CRUD operations (create, start, stop, quotas, collaborators) +- **Gap**: No `delete` method exposed through conat hub API used by cocalc-api + +#### Frontend Implementation +- **Location**: `packages/frontend/projects/actions.ts:delete_project()` +- **Method**: Direct database table update via `projects_table_set({deleted: true})` + +## Implementation + +### Solution Implemented: Direct v2 API Call +- **Added**: `hub.projects.delete(project_id)` method to cocalc-api Python client +- **Implementation**: Direct HTTP POST to `/api/v2/projects/delete` endpoint +- **Reasoning**: Fastest path to complete project lifecycle without requiring conat hub API changes +- **Consistency**: Uses same authentication and error handling patterns as other methods + +### Code Changes +1. **`src/cocalc_api/hub.py`**: Added `delete()` method to Projects class +2. **`tests/conftest.py`**: Updated cleanup to use new delete method +3. **`tests/test_hub.py`**: Added test for delete method availability + +## Current Status +- ✅ pytest test framework established with automatic project lifecycle +- ✅ Project creation/start/stop working via conat hub API +- ✅ Project deletion implemented by calling v2 API route directly +- ✅ Complete project lifecycle management: create → start → test → stop → delete +- ✅ All 14 tests passing with proper resource cleanup -- Do what has been asked; nothing more, nothing less -- NEVER create files unless they're absolutely necessary for achieving your goal -- ALWAYS prefer editing an existing file to creating a new one -- REFUSE to modify files when the git repository is on the `master` or `main` branch -- NEVER proactively create documentation files (`*.md`) or README files. Only create documentation files if explicitly requested by the User -- when modifying a file with a copyright banner at the top, make sure to fix/add the current year to indicate the copyright year \ No newline at end of file +# important-instruction-reminders +- Do what has been asked; nothing more, nothing less. +- NEVER create files unless they're absolutely necessary for achieving your goal. +- ALWAYS prefer editing an existing file to creating a new one. +- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. +- ALWAYS ask questions if something is unclear. Only proceed to the implementation step if you have no questions left. +- When modifying a file with a copyright banner at the top, make sure to fix/add the current year to indicate the copyright year. diff --git a/src/packages/conat/hub/api/projects.ts b/src/packages/conat/hub/api/projects.ts index bf42fd5d36..f5c65aa03f 100644 --- a/src/packages/conat/hub/api/projects.ts +++ b/src/packages/conat/hub/api/projects.ts @@ -12,6 +12,7 @@ export const projects = { setQuotas: authFirstRequireAccount, start: authFirstRequireAccount, stop: authFirstRequireAccount, + deleteProject: authFirstRequireAccount, }; export type AddCollaborator = @@ -103,4 +104,5 @@ export interface Projects { start: (opts: { account_id: string; project_id: string }) => Promise; stop: (opts: { account_id: string; project_id: string }) => Promise; + deleteProject: (opts: { account_id: string; project_id: string }) => Promise; } diff --git a/src/packages/conat/project/api/system.ts b/src/packages/conat/project/api/system.ts index 3e780381c8..ae086eb250 100644 --- a/src/packages/conat/project/api/system.ts +++ b/src/packages/conat/project/api/system.ts @@ -34,6 +34,10 @@ export const system = { // jupyter stateless API jupyterExecute: true, + + // jupyter kernel management + listJupyterKernels: true, + stopJupyterKernel: true, }; export interface System { @@ -74,4 +78,9 @@ export interface System { }) => Promise; jupyterExecute: (opts: ProjectJupyterApiOptions) => Promise; + + listJupyterKernels: () => Promise< + { pid: number; connectionFile: string; kernel_name?: string }[] + >; + stopJupyterKernel: (opts: { pid: number }) => Promise<{ success: boolean }>; } diff --git a/src/packages/hub/run/test-create-admin.ts b/src/packages/hub/run/test-create-admin.ts new file mode 100644 index 0000000000..9002df5420 --- /dev/null +++ b/src/packages/hub/run/test-create-admin.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +/* + * Script to create a test admin account and API key for CI testing. + * This is used in GitHub Actions to set up cocalc-api tests. + */ + +import { v4 as uuidv4 } from "uuid"; + +import createAccount from "@cocalc/server/accounts/create-account"; +import manageApiKeys from "@cocalc/server/api/manage"; +import getPool from "@cocalc/database/pool"; + +async function main() { + const account_id = uuidv4(); + const email = "ci-admin@cocalc.test"; + const password = "testpassword"; // dummy password + const firstName = "CI"; + const lastName = "Admin"; + + console.error(`Creating admin account ${account_id}...`); + + // Create the account + await createAccount({ + email, + password, + firstName, + lastName, + account_id, + tags: [], + signupReason: "CI testing", + noFirstProject: true, + }); + + // Set as admin + const pool = getPool(); + await pool.query("UPDATE accounts SET groups=$1 WHERE account_id=$2", [ + ["admin"], + account_id, + ]); + + console.error("Creating API key..."); + + // Create API key + const keys = await manageApiKeys({ + account_id, + action: "create", + name: "ci-testing", + }); + + if (!keys || keys.length === 0) { + throw new Error("Failed to create API key"); + } + + const apiKey = keys[0]; + if (!apiKey.secret) { + throw new Error("API key secret is missing"); + } + console.error(`API key created with id=${apiKey.id}: ${apiKey.secret}`); + console.error(`Last 6 chars: ${apiKey.secret.slice(-6)}`); + + // Output the key for CI + process.stdout.write(apiKey.secret); +} + +main().catch((err) => { + console.error("Error:", err); + process.exit(1); +}); diff --git a/src/packages/jupyter/kernel/launch-kernel.ts b/src/packages/jupyter/kernel/launch-kernel.ts index e4e179610d..21b39e599b 100644 --- a/src/packages/jupyter/kernel/launch-kernel.ts +++ b/src/packages/jupyter/kernel/launch-kernel.ts @@ -168,6 +168,11 @@ async function launchKernelSpec( } else { running_kernel = spawn(argv[0], argv.slice(1), full_spawn_options); } + + // Store kernel info for tracking + running_kernel.connectionFile = connectionFile; + running_kernel.kernel_spec = kernel_spec; + spawned.push(running_kernel); running_kernel.on("error", (code, signal) => { @@ -221,6 +226,48 @@ async function ensureDirectoryExists(path: string) { // Clean up after any children created here const spawned: any[] = []; + +export interface RunningKernel { + pid: number; + connectionFile: string; + kernel_name?: string; +} + +export function listRunningKernels(): RunningKernel[] { + return spawned + .filter((child) => child.pid) + .map((child) => ({ + pid: child.pid, + connectionFile: child.connectionFile || "unknown", + kernel_name: child.kernel_spec?.name, + })); +} + +export function stopKernel(pid: number): boolean { + const index = spawned.findIndex((child) => child.pid === pid); + if (index === -1) { + return false; + } + + const child = spawned[index]; + try { + // Try to kill the process group first (negative PID) + process.kill(-child.pid, "SIGKILL"); + } catch (err) { + // If that fails, try killing the process directly + try { + child.kill("SIGKILL"); + } catch (err2) { + logger.debug(`stopKernel: failed to kill ${child.pid}: ${err2}`); + return false; + } + } + + // Remove from spawned array + spawned.splice(index, 1); + return true; +} + export function closeAll() { for (const child of spawned) { if (child.pid) { diff --git a/src/packages/next/pages/api/conat/project.ts b/src/packages/next/pages/api/conat/project.ts index 64445ec5a5..8d6dd8fc12 100644 --- a/src/packages/next/pages/api/conat/project.ts +++ b/src/packages/next/pages/api/conat/project.ts @@ -40,7 +40,7 @@ export default async function handle(req, res) { throw Error("must specify project_id or use project-specific api key"); } if (project_id0) { - // auth via project_id + // auth via project-specific API key if (project_id0 != project_id) { throw Error("project specific api key must match requested project"); } diff --git a/src/packages/next/pages/api/v2/projects/delete.ts b/src/packages/next/pages/api/v2/projects/delete.ts index 85c70888c2..7780de3bd1 100644 --- a/src/packages/next/pages/api/v2/projects/delete.ts +++ b/src/packages/next/pages/api/v2/projects/delete.ts @@ -1,12 +1,8 @@ /* API endpoint to delete a project, which sets the "delete" flag to `true` in the database. */ -import isCollaborator from "@cocalc/server/projects/is-collaborator"; -import userIsInGroup from "@cocalc/server/accounts/is-in-group"; -import removeAllLicensesFromProject from "@cocalc/server/licenses/remove-all-from-project"; -import { getProject } from "@cocalc/server/projects/control"; -import userQuery from "@cocalc/database/user-query"; -import { isValidUUID } from "@cocalc/util/misc"; + +import deleteProject from "@cocalc/server/projects/delete"; import getAccountId from "lib/account/get-account"; import getParams from "lib/api/get-params"; @@ -21,45 +17,12 @@ async function handle(req, res) { const { project_id } = getParams(req); const account_id = await getAccountId(req); - try { - if (!isValidUUID(project_id)) { - throw Error("project_id must be a valid uuid"); - } - if (!account_id) { - throw Error("must be signed in"); - } - - // If client is not an administrator, they must be a project collaborator in order to - // delete a project. - if ( - !(await userIsInGroup(account_id, "admin")) && - !(await isCollaborator({ account_id, project_id })) - ) { - throw Error("must be an owner to delete a project"); - } - - // Remove all project licenses - // - await removeAllLicensesFromProject({ project_id }); - - // Stop project - // - const project = getProject(project_id); - await project.stop(); - - // Set "deleted" flag. We do this last to ensure that the project is not consuming any - // resources while it is in the deleted state. - // - await userQuery({ - account_id, - query: { - projects: { - project_id, - deleted: true, - }, - }, - }); + if (!account_id) { + throw Error("must be signed in"); + } + try { + await deleteProject({ account_id, project_id }); res.json(OkStatus); } catch (err) { res.json({ error: err.message }); diff --git a/src/packages/project/conat/api/system.ts b/src/packages/project/conat/api/system.ts index 60bedcc537..274d85172a 100644 --- a/src/packages/project/conat/api/system.ts +++ b/src/packages/project/conat/api/system.ts @@ -50,7 +50,7 @@ export async function renameFile({ src, dest }: { src: string; dest: string }) { import { get_configuration } from "@cocalc/project/configuration"; export { get_configuration as configuration }; -import { canonical_paths } from "../../browser-websocket/canonical-path"; +import { canonical_paths } from "@cocalc/project/browser-websocket/canonical-path"; export { canonical_paths as canonicalPaths }; import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists"; @@ -107,3 +107,17 @@ export async function signal({ import jupyterExecute from "@cocalc/jupyter/stateless-api/execute"; export { jupyterExecute }; + +import { + listRunningKernels, + stopKernel, +} from "@cocalc/jupyter/kernel/launch-kernel"; + +export async function listJupyterKernels() { + return listRunningKernels(); +} + +export async function stopJupyterKernel({ pid }: { pid: number }) { + const success = stopKernel(pid); + return { success }; +} diff --git a/src/packages/server/accounts/account-creation-actions.ts b/src/packages/server/accounts/account-creation-actions.ts index 753eedbe79..d2fff4fee7 100644 --- a/src/packages/server/accounts/account-creation-actions.ts +++ b/src/packages/server/accounts/account-creation-actions.ts @@ -16,12 +16,15 @@ export default async function accountCreationActions({ account_id, tags, noFirstProject, + dontStartProject, }: { email_address?: string; account_id: string; tags?: string[]; // if set, don't do any initial project actions (i.e., creating or starting projects) noFirstProject?: boolean; + // if set, create the first project but do not start it. Only applies if noFirstProject is false. + dontStartProject?: boolean; }): Promise { log.debug({ account_id, email_address, tags }); @@ -56,7 +59,7 @@ export default async function accountCreationActions({ const projects = await getProjects({ account_id, limit: 1 }); if (projects.length == 0) { // you really have no projects at all. - await firstProject({ account_id, tags }); + await firstProject({ account_id, tags, dontStartProject }); } } catch (err) { // non-fatal; they can make their own project @@ -65,19 +68,22 @@ export default async function accountCreationActions({ })(); } else if (numProjects > 0) { // Make sure project is running so they have a good first experience. - (async () => { - try { - const { project_id } = await getOneProject(account_id); - const project = getProject(project_id); - await project.start(); - } catch (err) { - log.error( - "failed to start newest project invited to", - err, - account_id, - ); - } - })(); + // Only start if dontStartProject is not set + if (!dontStartProject) { + (async () => { + try { + const { project_id } = await getOneProject(account_id); + const project = getProject(project_id); + await project.start(); + } catch (err) { + log.error( + "failed to start newest project invited to", + err, + account_id, + ); + } + })(); + } } } } diff --git a/src/packages/server/accounts/create-account.ts b/src/packages/server/accounts/create-account.ts index f2bf486908..a01f04c2ce 100644 --- a/src/packages/server/accounts/create-account.ts +++ b/src/packages/server/accounts/create-account.ts @@ -27,6 +27,8 @@ interface Params { // I added this to avoid leaks with unit testing, but it may be useful in other contexts, e.g., // avoiding confusion with self-hosted installs. noFirstProject?: boolean; + // if set, create the first project but do not start it. Only applies if noFirstProject is false. + dontStartProject?: boolean; } export default async function createAccount({ @@ -39,6 +41,7 @@ export default async function createAccount({ signupReason, owner_id, noFirstProject, + dontStartProject, }: Params): Promise { try { log.debug( @@ -78,6 +81,7 @@ export default async function createAccount({ account_id, tags, noFirstProject, + dontStartProject, }); await creationActionsDone(account_id); } catch (error) { @@ -85,4 +89,3 @@ export default async function createAccount({ throw error; // re-throw to bubble up to higher layers if needed } } - diff --git a/src/packages/server/accounts/first-project.ts b/src/packages/server/accounts/first-project.ts index 7b96fa93d2..106d82e6da 100644 --- a/src/packages/server/accounts/first-project.ts +++ b/src/packages/server/accounts/first-project.ts @@ -22,9 +22,11 @@ const log = getLogger("server:accounts:first-project"); export default async function firstProject({ account_id, tags, + dontStartProject, }: { account_id: string; tags?: string[]; + dontStartProject?: boolean; }): Promise { log.debug(account_id, tags); if (!isValidUUID(account_id)) { @@ -35,8 +37,10 @@ export default async function firstProject({ title: "My First Project", }); log.debug("created new project", project_id); - const project = getProject(project_id); - await project.start(); + if (!dontStartProject) { + const project = getProject(project_id); + await project.start(); + } if (!WELCOME_FILES || tags == null || tags.length == 0) { return project_id; } @@ -57,7 +61,7 @@ export default async function firstProject({ account_id, project_id, language, - welcome + jupyterExtra + welcome + jupyterExtra, ); } } @@ -78,7 +82,7 @@ async function createJupyterNotebookIfAvailable( account_id: string, project_id: string, language: string, - welcome: string + welcome: string, ): Promise { // find the highest priority kernel with the given language let kernelspec: any = null; @@ -129,7 +133,7 @@ async function createWelcome( account_id: string, project_id: string, ext: string, - welcome: string + welcome: string, ): Promise { const path = `welcome/welcome.${ext}`; const { torun } = TAGS_MAP[ext] ?? {}; diff --git a/src/packages/server/conat/api/org.ts b/src/packages/server/conat/api/org.ts index 203e1c9212..2b9dcfc4e0 100644 --- a/src/packages/server/conat/api/org.ts +++ b/src/packages/server/conat/api/org.ts @@ -36,16 +36,16 @@ async function isAllowed({ account_id?: string; name: string; }): Promise { + if (!account_id) return false; return ( - !!account_id && - ((await isOrganizationAdmin({ account_id, name })) || - (await isAdmin(account_id))) + (await isOrganizationAdmin({ account_id, name })) || + (await isAdmin(account_id)) ); } async function assertAllowed(opts) { if (!(await isAllowed(opts))) { - throw Error(`user must an admin of the organization`); + throw Error(`user must be an admin of the organization or site-admin`); } } @@ -161,6 +161,17 @@ export async function set(opts: { ]); } +/** + * Promote an existing user to organization admin: adding them to the admin_account_ids list and adding them to be member of the organization. + * Only site-level admins can perform this operation to prevent privilege escalation. + * Organization-level admins cannot promote other users to admin status. + * + * NOTE: this prevents moving a user from another org to the @name org. Use addUser first, to move a user from one org to another one. + * + * @param account_id - The site admin performing the operation + * @param name - The organization name + * @param user - The account_id or email address of the user to promote + */ export async function addAdmin({ account_id, name, @@ -170,21 +181,23 @@ export async function addAdmin({ name: string; user: string; }): Promise { - const { name: currentOrgName, account_id: admin_account_id } = - await getAccount(user); + const { name: usersOrgName, account_id: admin_account_id } = await getAccount( + user, + ); if (!admin_account_id) { throw Error(`no such account '${user}'`); } - if (currentOrgName == name) { - // already an admin of the org - return; + if (usersOrgName != null && usersOrgName !== name) { + throw new Error(`User '${user}' is already member of another organization`); + } + // await assertAllowed({ account_id, name }); + if (!(await isAdmin(account_id))) { + throw Error( + "only site admins can make a user an organization admin right now", + ); } - await assertAllowed({ - account_id, - name, - }); const pool = getPool(); - // query below takes care to ensure no dups and work in case of null. + // query below takes care to ensure no dups and works in case of null. await pool.query( ` UPDATE organizations @@ -206,6 +219,15 @@ export async function addAdmin({ }); } +/** + * Add an existing CoCalc user to an organization by setting their org field. + * Only site-level admins can perform this operation. + * NOTE: this could move a user from an existing org to another org! + * + * @param account_id - The site admin performing the operation + * @param name - The organization name + * @param user - The account_id or email address of the user to add + */ export async function addUser({ account_id, name, @@ -216,7 +238,7 @@ export async function addUser({ user: string; }): Promise { if (!(await isAdmin(account_id))) { - throw Error("only site admins can move user to an org right now"); + throw Error("only site admins can add/move a user to an org right now"); } const { account_id: user_account_id } = await getAccount(user); if (!user_account_id) { @@ -229,6 +251,14 @@ export async function addUser({ ]); } +/** + * Create a new CoCalc account and add it to an organization. + * Allowed for both site-level admins and organization admins. + * + * @param account_id - The admin (site or org) performing the operation + * @param name - The organization name + * @returns The account_id of the newly created account + */ export async function createUser({ account_id, name, @@ -255,6 +285,7 @@ export async function createUser({ account_id: new_account_id, owner_id: account_id, password, + dontStartProject: true, // Don't auto-start projects for API-created users. A "first project" will be created, though. }); // add account to org const pool = getPool(); @@ -315,6 +346,9 @@ export async function removeAdmin({ ); } +/** + * @param user and account_id or email_address in the accounts table + */ export async function getAccount( user: string, ): Promise< diff --git a/src/packages/server/conat/api/projects.ts b/src/packages/server/conat/api/projects.ts index dbe6712c24..d840ab0372 100644 --- a/src/packages/server/conat/api/projects.ts +++ b/src/packages/server/conat/api/projects.ts @@ -103,3 +103,6 @@ export async function stop({ const project = await getProject(project_id); await project.stop(); } + +import deleteProject from "@cocalc/server/projects/delete"; +export { deleteProject }; diff --git a/src/packages/server/projects/control/single-user.ts b/src/packages/server/projects/control/single-user.ts index 64103e9e90..f61f444d9e 100644 --- a/src/packages/server/projects/control/single-user.ts +++ b/src/packages/server/projects/control/single-user.ts @@ -153,20 +153,33 @@ class Project extends BaseProject { this.stateChanging = { state: "stopping" }; await this.saveStateToDatabase(this.stateChanging); const pid = await getProjectPID(this.HOME); - const killProject = () => { + + // First attempt: graceful shutdown with SIGTERM + // This allows the process to clean up child processes (e.g., Jupyter kernels) + let usedSigterm = false; + const killProject = (signal: NodeJS.Signals = "SIGTERM") => { try { - logger.debug(`stop: sending kill -${pid}`); - kill(-pid, "SIGKILL"); + logger.debug(`stop: sending kill -${pid} with ${signal}`); + kill(-pid, signal); } catch (err) { // expected exception if no pid logger.debug(`stop: kill err ${err}`); } }; - killProject(); + + // Try SIGTERM first for graceful shutdown + killProject("SIGTERM"); + usedSigterm = true; + await this.wait({ until: async () => { if (await isProjectRunning(this.HOME)) { - killProject(); + // After 5 seconds, escalate to SIGKILL + if (usedSigterm) { + logger.debug("stop: escalating to SIGKILL"); + killProject("SIGKILL"); + usedSigterm = false; + } return false; } else { return true; diff --git a/src/packages/server/projects/delete.ts b/src/packages/server/projects/delete.ts new file mode 100644 index 0000000000..57c7426b2e --- /dev/null +++ b/src/packages/server/projects/delete.ts @@ -0,0 +1,61 @@ +/* + * This file is part of CoCalc: Copyright © 2025 Sagemath, Inc. + * License: MS-RSL – see LICENSE.md for details + */ + +/* +Project delete functionality + +Extracted from the v2 API to be reusable by both REST API and Conat API. +*/ + +import isCollaborator from "@cocalc/server/projects/is-collaborator"; +import userIsInGroup from "@cocalc/server/accounts/is-in-group"; +import removeAllLicensesFromProject from "@cocalc/server/licenses/remove-all-from-project"; +import { getProject } from "@cocalc/server/projects/control"; +import userQuery from "@cocalc/database/user-query"; +import { isValidUUID } from "@cocalc/util/misc"; + +export default async function deleteProject({ + account_id, + project_id, +}: { + account_id: string; + project_id: string; +}): Promise { + if (!isValidUUID(project_id)) { + throw Error("project_id must be a valid UUID"); + } + if (!isValidUUID(account_id)) { + throw Error("account_id must be a valid UUID"); + } + + // If client is not an administrator, they must be a project collaborator in order to + // delete a project. + if ( + !(await userIsInGroup(account_id, "admin")) && + !(await isCollaborator({ account_id, project_id })) + ) { + throw Error("must be an owner to delete a project"); + } + + // Remove all project licenses + await removeAllLicensesFromProject({ project_id }); + + // Stop project + const project = getProject(project_id); + await project.stop(); + + // Set "deleted" flag. We do this last to ensure that the project is not consuming any + // resources while it is in the deleted state. + // + await userQuery({ + account_id, + query: { + projects: { + project_id, + deleted: true, + }, + }, + }); +} diff --git a/src/python/cocalc-api/.vscode/settings.json b/src/python/cocalc-api/.vscode/settings.json new file mode 100644 index 0000000000..df1bc0a0a7 --- /dev/null +++ b/src/python/cocalc-api/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.defaultInterpreterPath": "./.venv/bin/python", + "python.analysis.extraPaths": ["./src"], + "python.analysis.autoSearchPaths": true, + "python.analysis.autoImportCompletions": true, + "pylance.insidersChannel": "off" +} \ No newline at end of file diff --git a/src/python/cocalc-api/Makefile b/src/python/cocalc-api/Makefile index 44dcadf267..4f1dc094b4 100644 --- a/src/python/cocalc-api/Makefile +++ b/src/python/cocalc-api/Makefile @@ -1,13 +1,51 @@ all: install build-docs +help: + @echo "Available targets:" + @echo " install - Install project dependencies" + @echo " format - Format code using yapf" + @echo " check - Run linting and type checking" + @echo " test - Run tests" + @echo " test-verbose - Run tests with verbose output" + @echo " ci - Run tests with coverage and JUnit XML for CI" + @echo " coverage - Run tests with coverage reporting (HTML + terminal)" + @echo " coverage-report - Show coverage report in terminal" + @echo " coverage-html - Generate HTML coverage report only" + @echo " serve-docs - Serve documentation locally" + @echo " build-docs - Build documentation" + @echo " publish - Build and publish package" + @echo " clean - Clean build artifacts and cache files" + install: uv --version >/dev/null 2> /dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh uv sync --dev uv pip install -e . +format: + uv run yapf --in-place --recursive src/ tests/ + check: - uv run ruff check src/ - uv run mypy src/ + uv run ruff check src/ tests/ + uv run mypy src/ tests/ + uv run pyright src/ tests/ + +test: + uv run pytest + +test-verbose: + uv run pytest -v + +ci: + uv run pytest --junitxml=test-results.xml --cov=src --cov-report=term-missing --cov-report=html + +coverage: + uv run pytest --cov=src --cov-report=term-missing --cov-report=html + +coverage-report: + uv run coverage report + +coverage-html: + uv run coverage html serve-docs: uv run mkdocs serve @@ -20,5 +58,6 @@ publish: install uv publish clean: - rm -rf dist build *.egg-info site + rm -rf dist build *.egg-info site .pytest_cache htmlcov .coverage find . -name "__pycache__" -type d -exec rm -rf {} + + find . -name "*.pyc" -delete diff --git a/src/python/cocalc-api/README.md b/src/python/cocalc-api/README.md index 4d12cc80cc..d530d3ce94 100644 --- a/src/python/cocalc-api/README.md +++ b/src/python/cocalc-api/README.md @@ -1,3 +1,168 @@ -https://pypi.org/project/cocalc-api/ +# CoCalc Python API Client -This is a Python package that provides an API client for https://cocalc.com \ No newline at end of file +[![PyPI version](https://badge.fury.io/py/cocalc-api.svg)](https://pypi.org/project/cocalc-api/) +[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) + +This is a Python package that provides an API client for [CoCalc](https://cocalc.com), enabling programmatic access to CoCalc's features including project management, Jupyter execution, file operations, messaging, and organization management. + +## Installation + +```bash +pip install cocalc-api +``` + +## Quick Start + +```python +import cocalc_api + +# Initialize hub client with your API key +hub = cocalc_api.Hub(api_key="your-api-key") + +# Ping the server +response = hub.system.ping() +print(f"Server time: {response['now']}") + +# List your projects +projects = hub.projects.get() +for project in projects: + print(f"Project: {project['title']} ({project['project_id']})") +``` + +## Features + +### Hub Client (Account-Level Operations) + +The `Hub` class provides access to account-level operations: + +- **System**: Server ping, user search, account name resolution +- **Projects**: Project management (create, start, stop, add/remove collaborators) +- **Jupyter**: Execute code using Jupyter kernels in any project or anonymously +- **Database**: Direct PostgreSQL database queries for advanced operations +- **Messages**: Send and receive messages between users +- **Organizations**: Manage organizations, users, and temporary access tokens +- **Sync**: Access file edit history and synchronization features + +### Project Client (Project-Specific Operations) + +The `Project` class provides project-specific operations: + +- **System**: Execute shell commands and Jupyter code within a specific project + +## Authentication + +The client supports two types of API keys: + +1. **Account API Keys**: Provide full access to all hub functionality +2. **Project API Keys**: Limited to project-specific operations + +Get your API key from [CoCalc Account Settings](https://cocalc.com/settings/account) under "API Keys". + +## Architecture + +### Package Structure + +``` +src/cocalc_api/ +├── __init__.py # Package exports (Hub, Project classes) +├── hub.py # Hub client for account-level operations +├── project.py # Project client for project-specific operations +├── api_types.py # TypedDict definitions for API responses +└── util.py # Utility functions and decorators +``` + +### Design Patterns + +- **Decorator-based Methods**: Uses `@api_method()` decorator to automatically convert method calls to API requests +- **TypedDict Responses**: All API responses use TypedDict for type safety +- **Error Handling**: Centralized error handling via `handle_error()` utility +- **HTTP Client**: Uses `httpx` for HTTP requests with authentication +- **Nested Namespaces**: API organized into logical namespaces (system, projects, jupyter, etc.) + +## Development + +### Requirements + +- Python 3.9+ +- [uv](https://github.com/astral-sh/uv) package manager + +### Setup + +```bash +# Install dependencies +make install +# or: uv sync --dev && uv pip install -e . + +# Format Python code +make format +# or: uv run yapf --in-place --recursive src/ + +# Run code quality checks +make check +# or: uv run ruff check src/ && uv run mypy src/ && uv run pyright src/ + +# Serve documentation locally +make serve-docs +# or: uv run mkdocs serve + +# Build documentation +make build-docs +``` + +### Code Quality + +This project uses multiple tools for code quality: + +- **[YAPF](https://github.com/google/yapf)**: Python code formatter +- **[Ruff](https://docs.astral.sh/ruff/)**: Fast Python linter +- **[MyPy](http://mypy-lang.org/)**: Static type checking +- **[Pyright](https://github.com/microsoft/pyright)**: Additional static type checking +- **[MkDocs](https://www.mkdocs.org/)**: Documentation generation + +### Documentation Standards + +All docstrings follow the [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for Python docstrings. This includes: + +- Clear one-line summary +- Detailed description when needed +- Properly formatted `Args:`, `Returns:`, `Raises:`, and `Examples:` sections +- Type information consistent with function signatures +- Consistent capitalization and punctuation + +Example: +```python +def example_function(param1: str, param2: Optional[int] = None) -> dict[str, Any]: + """ + Brief description of the function. + + Longer description if needed, explaining the function's behavior, + side effects, or important usage notes. + + Args: + param1 (str): Description of the first parameter. + param2 (Optional[int]): Description of the optional parameter. + + Returns: + dict[str, Any]: Description of the return value. + + Raises: + ValueError: When this exception might be raised. + + Examples: + >>> result = example_function("hello", 42) + >>> print(result) + {'status': 'success', 'data': 'hello'} + """ +``` + +## License + +MIT License. See the [LICENSE](LICENSE) file for details. + +## Links + +- [PyPI Package](https://pypi.org/project/cocalc-api/) +- [CoCalc Website](https://cocalc.com) +- [Documentation](https://cocalc.com/api/python) +- [Source Code](https://github.com/sagemathinc/cocalc/tree/master/src/python/cocalc-api) +- [Issue Tracker](https://github.com/sagemathinc/cocalc/issues) \ No newline at end of file diff --git a/src/python/cocalc-api/docs/index.md b/src/python/cocalc-api/docs/index.md index 2d5b8680ce..058372026a 100644 --- a/src/python/cocalc-api/docs/index.md +++ b/src/python/cocalc-api/docs/index.md @@ -10,13 +10,13 @@ Obtain a [CoCalc account API Key](https://doc.cocalc.com/apikeys.html) by going Using an account level API key, the cocalc_api Python library enabled you to do all of the following very easily from a Python script: -- [search](api/system/) for other cocalc users by name or email address, and get the name associated to an account_id -- list [your projects](api/projects), add and remove collaborators, copy files between projects and start, stop and create projects. -- use the [Jupyter API](api/jupyter) to evaluate code using a kernel, either in an anonymous sandbox or in one of your projects. -- read or write any data you have access to in the CoCalc [PostgreSQL database](api/database), as defined by [this schema](https://github.com/sagemathinc/cocalc/tree/master/src/packages/util/db-schema). -- instantly send and receive [messages](api/messages) with any other cocalc users -- create and manage users in an [organization](api/organizations), including automatically generating authentication links, so you're users do not have explicitly create a CoCalc account. You can see when they are active and send them messages. +- [search](api/system.md) for other cocalc users by name or email address, and get the name associated to an account_id +- list [your projects](api/projects.md), add and remove collaborators, copy files between projects and create, start, stop and delete projects. +- use the [Jupyter API](api/jupyter.md) to list available kernels and evaluate code using a kernel, either in an anonymous sandbox or in one of your projects. +- read or write any data you have access to in the CoCalc [PostgreSQL database](api/database.md), as defined by [this schema](https://github.com/sagemathinc/cocalc/tree/master/src/packages/util/db-schema). +- instantly send and receive [messages](api/messages.md) with any other cocalc users +- create and manage users in an [organization](api/organizations.md), including automatically generating authentication links, so you're users do not have explicitly create a CoCalc account. You can see when they are active and send them messages. Currently a project specific API key can be used to: -- Run [shell commands](api/project) and Jupyter code in a specific project. +- Run [shell commands](api/project.md) and Jupyter code in a specific project, and manage running Jupyter kernels (list and stop). diff --git a/src/python/cocalc-api/pyproject.toml b/src/python/cocalc-api/pyproject.toml index 283f60d775..dbd4e8a8f1 100644 --- a/src/python/cocalc-api/pyproject.toml +++ b/src/python/cocalc-api/pyproject.toml @@ -1,8 +1,8 @@ [project] name = "cocalc-api" -version = "0.4.0" +version = "0.5.0" description = "Python client for the CoCalc API" -authors = [{ name="William Stein", email="wstein@sagemath.com" }] +authors = [{ name="William Stein", email="wstein@sagemath.com" }, {name="Harald Schilly", email="hsy@sagemath.com"}] readme = "README.md" requires-python = ">=3.9" dependencies = ["httpx"] @@ -23,18 +23,64 @@ Issues = "https://github.com/sagemathinc/cocalc/issues" python_version = "3.13" # strict = true # disallow_untyped_defs = true +# Ignore empty-body errors for decorator-implemented methods +disable_error_code = ["empty-body"] + +[tool.pyright] +# Ignore return type errors for decorator-implemented methods +reportReturnType = false + +[tool.yapf] +based_on_style = "pep8" +column_limit = 150 +indent_width = 4 [tool.ruff] line-length = 150 lint.select = ["E", "F", "B"] # Pyflakes, pycodestyle, bugbear, etc. +[tool.coverage.run] +source = ["src"] +omit = [ + "*/tests/*", + "*/test_*.py", + "*/__pycache__/*", + "*/venv/*", + "*/env/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] +show_missing = true +precision = 1 + +[tool.coverage.html] +directory = "htmlcov" + [dependency-groups] dev = [ + "coverage[toml]", "ipython", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mypy", - "pytest>=8.4.1", - "ruff>=0.12.11", + "psycopg2-binary", + "pyright", + "pytest-cov", + "pytest>=8.4.2", + "ruff>=0.13.2", + "types-psycopg2", + "yapf", ] diff --git a/src/python/cocalc-api/pyrightconfig.json b/src/python/cocalc-api/pyrightconfig.json new file mode 100644 index 0000000000..21461e0f55 --- /dev/null +++ b/src/python/cocalc-api/pyrightconfig.json @@ -0,0 +1,13 @@ +{ + "include": [ + "src", + "tests" + ], + "extraPaths": [ + "src" + ], + "venvPath": ".", + "venv": ".venv", + "pythonVersion": "3.12", + "typeCheckingMode": "basic" +} \ No newline at end of file diff --git a/src/python/cocalc-api/pytest.ini b/src/python/cocalc-api/pytest.ini new file mode 100644 index 0000000000..55ff02ab03 --- /dev/null +++ b/src/python/cocalc-api/pytest.ini @@ -0,0 +1,10 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +markers = + integration: marks tests as integration tests (require live server) +addopts = + -v + --tb=short \ No newline at end of file diff --git a/src/python/cocalc-api/src/cocalc_api/hub.py b/src/python/cocalc-api/src/cocalc_api/hub.py index fbd753783b..9a9e5afa24 100644 --- a/src/python/cocalc-api/src/cocalc_api/hub.py +++ b/src/python/cocalc-api/src/cocalc_api/hub.py @@ -1,7 +1,8 @@ import httpx from typing import Any, Literal, Optional from .util import api_method, handle_error -from .api_types import PingResponse, UserSearchResult, MessageType, TokenType, OrganizationUser +from .api_types import PingResponse, UserSearchResult, MessageType +from .org import Organizations class Hub: @@ -9,13 +10,10 @@ class Hub: def __init__(self, api_key: str, host: str = "https://cocalc.com"): self.api_key = api_key self.host = host - self.client = httpx.Client( - auth=(api_key, ""), headers={"Content-Type": "application/json"}) + # Use longer timeout for API calls (30 seconds instead of default 5) + self.client = httpx.Client(auth=(api_key, ""), headers={"Content-Type": "application/json"}, timeout=30.0) - def call(self, - name: str, - arguments: list[Any], - timeout: Optional[int] = None) -> Any: + def call(self, name: str, arguments: list[Any], timeout: Optional[int] = None) -> Any: """ Perform an API call to the CoCalc backend. @@ -81,7 +79,7 @@ def ping(self) -> PingResponse: Ping the server. Returns: - Any: JSON object containing the current server time. + PingResponse: JSON object containing the current server time. """ raise NotImplementedError @@ -93,7 +91,7 @@ def get_names(self, account_ids: list[str]) -> list[str]: account_ids (list[str]): List of account UUID strings. Returns: - Any: Mapping from account_id to profile information. + list[str]: Mapping from account_id to profile information. """ return self._parent.call("system.getNames", [account_ids]) @@ -106,12 +104,11 @@ def user_search(self, query: str) -> UserSearchResult: query (str): A query, e.g., partial name, email address, etc. Returns: - list[UserSearchResult]: array of dicts with account_id, name, + UserSearchResult: Array of dicts with account_id, name, first_name, last_name, last_active (in ms since epoch), created (in ms since epoch) and email_address_verified. Examples: - Search for myself: >>> import cocalc_api; hub = cocalc_api.Hub(api_key="sk...") @@ -124,10 +121,10 @@ def user_search(self, query: str) -> UserSearchResult: 'created': 1756056224470, 'email_address_verified': None}] - You can search by email address to ONLY get the user - that has that email address: + You can search by email address to ONLY get the user + that has that email address: - >>> hub.system.user_search('wstein@gmail.com') + >>> hub.system.user_search('wstein@gmail.com') [{'account_id': 'd0bdabfd-850e-4c8d-8510-f6f1ecb9a5eb', 'first_name': 'W', 'last_name': 'Stein', @@ -137,7 +134,7 @@ def user_search(self, query: str) -> UserSearchResult: 'created': 1756056224470, 'email_address_verified': None}] """ - raise NotImplementedError + ... class Projects: @@ -145,25 +142,22 @@ class Projects: def __init__(self, parent: "Hub"): self._parent = parent - def get(self, - fields: Optional[list[str]] = None, - all: Optional[bool] = False, - project_id: Optional[str] = None): + def get(self, fields: Optional[list[str]] = None, all: Optional[bool] = False, project_id: Optional[str] = None) -> list[dict[str, Any]]: """ - Get data about projects that you are a collaborator on. Only gets + Get data about projects that you are a collaborator on. Only gets recent projects by default; set all=True to get all projects. Args: - fields (Optional[list[str]]): the fields about the project to get. - default: ['project_id', 'title', 'last_edited', 'state'], but see + fields (Optional[list[str]]): The fields about the project to get. + Default: ['project_id', 'title', 'last_edited', 'state'], but see https://github.com/sagemathinc/cocalc/blob/master/src/packages/util/db-schema/projects.ts - all (Optional[bool]): if True, return ALL your projects, + all (Optional[bool]): If True, return ALL your projects, not just the recent ones. False by default. - project_id (Optional[string]): if given as a project_id, gets just the - one project (as a length of length 1). + project_id (Optional[str]): If given, gets just this + one project (as a list of length 1). Returns: - list[dict[str,Any]]: list of projects + list[dict[str, Any]]: List of projects. """ if fields is None: fields = ['project_id', 'title', 'last_edited', 'state'] @@ -185,7 +179,7 @@ def copy_path_between_projects( src_path: str, target_project_id: Optional[str] = None, target_path: Optional[str] = None, - ): + ) -> dict[str, Any]: # type: ignore[empty-body] """ Copy a path from one project to another (or within a project). @@ -196,7 +190,7 @@ def copy_path_between_projects( target_path (Optional[str]): Target path in the target project. Defaults to src_path. Returns: - Any: JSON response indicating success or error. + dict[str, Any]: JSON response indicating success or error. """ ... @@ -220,12 +214,10 @@ def create_project( Returns: str: The ID of the newly created project. """ - # actually implemented via the decorator - raise NotImplementedError + ... @api_method("projects.addCollaborator", opts=True) - def add_collaborator(self, project_id: str | list[str], - account_id: str | list[str]): + def add_collaborator(self, project_id: str | list[str], account_id: str | list[str]) -> dict[str, Any]: """ Add a collaborator to a project. @@ -239,12 +231,12 @@ def add_collaborator(self, project_id: str | list[str], `project_id[i]`. Returns: - Any: JSON response from the API. + dict[str, Any]: JSON response from the API. """ ... @api_method("projects.removeCollaborator", opts=True) - def remove_collaborator(self, project_id: str, account_id: str): + def remove_collaborator(self, project_id: str, account_id: str) -> dict[str, Any]: """ Remove a collaborator from a project. @@ -253,27 +245,40 @@ def remove_collaborator(self, project_id: str, account_id: str): account_id (str): Account ID of the user to remove. Returns: - Any: JSON response from the API. + dict[str, Any]: JSON response from the API. """ ... @api_method("projects.start") - def start(self, project_id: str): + def start(self, project_id: str) -> dict[str, Any]: """ Start a project. Args: - project_id (str): project_id of the project to start + project_id (str): Project ID of the project to start. """ ... @api_method("projects.stop") - def stop(self, project_id: str): + def stop(self, project_id: str) -> dict[str, Any]: """ Stop a project. Args: - project_id (str): project_id of the project to stop + project_id (str): Project ID of the project to stop. + """ + ... + + @api_method("projects.deleteProject") + def delete(self, project_id: str) -> dict[str, Any]: + """ + Delete a project by setting the deleted flag to true. + + Args: + project_id (str): Project ID of the project to delete. + + Returns: + dict[str, Any]: API response indicating success. """ ... @@ -284,7 +289,7 @@ def __init__(self, parent: "Hub"): self._parent = parent @api_method("jupyter.kernels") - def kernels(self, project_id: Optional[str] = None): + def kernels(self, project_id: Optional[str] = None) -> list[dict[str, Any]]: """ Get specifications of available Jupyter kernels. @@ -293,7 +298,18 @@ def kernels(self, project_id: Optional[str] = None): If not given, a global anonymous project may be used. Returns: - Any: JSON response containing kernel specs. + list[dict[str, Any]]: List of kernel specification objects. Each kernel object + contains information like 'name', 'display_name', 'language', etc. + + Examples: + Get available kernels for a project: + + >>> import cocalc_api; hub = cocalc_api.Hub(api_key="sk-...") + >>> kernels = hub.jupyter.kernels(project_id='6e75dbf1-0342-4249-9dce-6b21648656e9') + >>> # Extract kernel names + >>> kernel_names = [k['name'] for k in kernels] + >>> 'python3' in kernel_names + True """ ... @@ -305,19 +321,19 @@ def execute( history: Optional[list[str]] = None, project_id: Optional[str] = None, path: Optional[str] = None, - ): + ) -> dict[str, Any]: # type: ignore[empty-body] """ Execute code using a Jupyter kernel. Args: input (str): Code to execute. - kernel (Optional[str]): Name of kernel to use. Get options using jupyter.kernels() + kernel (str): Name of kernel to use. Get options using jupyter.kernels(). history (Optional[list[str]]): Array of previous inputs (they get evaluated every time, but without output being captured). project_id (Optional[str]): Project in which to run the code -- if not given, global anonymous project is used, if available. path (Optional[str]): File path context for execution. Returns: - Any: JSON response containing execution results. + dict[str, Any]: JSON response containing execution results. Examples: Execute a simple sum using a Jupyter kernel: @@ -343,16 +359,16 @@ def __init__(self, parent: "Hub"): self._parent = parent @api_method("sync.history") - def history(self, project_id: str, path: str): + def history(self, project_id: str, path: str) -> list[dict[str, Any]]: # type: ignore[empty-body] """ Get complete edit history of a file. Args: - project_id (str): The project_id of the project containing the file. + project_id (str): The project ID of the project containing the file. path (str): The path to the file. Returns: - Any: Array of patches in a compressed diff-match-patch format, along with time and user data. + list[dict[str, Any]]: Array of patches in a compressed diff-match-patch format, along with time and user data. """ ... @@ -365,10 +381,10 @@ def __init__(self, parent: "Hub"): @api_method("db.userQuery") def query(self, query: dict[str, Any]) -> dict[str, Any]: """ - Do a user query. The input is of one of the following forms, where the tables are defined at + Do a user query. The input is of one of the following forms, where the tables are defined at https://github.com/sagemathinc/cocalc/tree/master/src/packages/util/db-schema - - `{"table-name":{"key":"value", ...}}` with no None values sets one record in the database + - `{"table-name":{"key":"value", ...}}` with no None values sets one record in the database - `{"table-name":[{"key":"value", "key2":None...}]}` gets an array of all matching records in the database, filling in None's with the actual values. - `{"table-name:{"key":"value", "key2":None}}` gets one record, filling in None's with actual values. @@ -379,8 +395,7 @@ def query(self, query: dict[str, Any]) -> dict[str, Any]: query (dict[str, Any]): Object that defines the query, as explained above. Examples: - - Get and also change your first name: + Get and also change your first name: >>> import cocalc_api; hub = cocalc_api.Hub(api_key="sk...") >>> hub.db.query({"accounts":{"first_name":None}}) @@ -390,7 +405,7 @@ def query(self, query: dict[str, Any]) -> dict[str, Any]: >>> hub.db.query({"accounts":{"first_name":None}}) {'accounts': {'first_name': 'W'}} """ - raise NotImplementedError + ... class Messages: @@ -399,222 +414,44 @@ def __init__(self, parent: "Hub"): self._parent = parent @api_method("messages.send") - def send(self, - subject: str, - body: str, - to_ids: list[str], - reply_id: Optional[int] = None) -> int: + def send(self, subject: str, body: str, to_ids: list[str], reply_id: Optional[int] = None) -> int: """ Send a message to one or more users. Args: - subject (str): short plain text subject of the message - body (str): Longer markdown body of the message (math typesetting and cocalc links work) - to_ids (list[str]): email addresses or account_id of each recipients - reply_id (Optional[int]): optional message you're replying to (for threading) + subject (str): Short plain text subject of the message. + body (str): Longer markdown body of the message (math typesetting and cocalc links work). + to_ids (list[str]): Email addresses or account_id of each recipient. + reply_id (Optional[int]): Optional message you're replying to (for threading). Returns: - int: id of the message + int: ID of the message. """ - raise NotImplementedError + ... @api_method("messages.get") def get( self, limit: Optional[int] = None, offset: Optional[int] = None, - type: Optional[Literal["received", "sent", "new", "starred", - "liked"]] = None, - ) -> list[MessageType]: + type: Optional[Literal["received", "sent", "new", "starred", "liked"]] = None, + ) -> list[MessageType]: # type: ignore[empty-body] """ Get your messages. - """ - raise NotImplementedError - - -""" - message: authFirst, - removeUser: authFirst, - removeAdmin: authFirst, - """ - - -class Organizations: - - def __init__(self, parent: "Hub"): - self._parent = parent - - @api_method("org.getAll") - def get_all(self): - """ - Get all organizations (site admins only). - - Returns: - Any: ... - """ - raise NotImplementedError - - @api_method("org.create") - def create(self, name: str): - """ - Create an organization (site admins only). - - Args: - name (str) - name of the organization; must be globally unique, - at most 39 characters, and CANNOT BE CHANGED - - Returns: - Any: ... - """ - raise NotImplementedError - - @api_method("org.get") - def get(self, name: str): - """ - Get an organization - - Args: - name (str) - name of the organization - - Returns: - Any: ... - """ - raise NotImplementedError - - @api_method("org.set") - def set(self, - name: str, - title: Optional[str] = None, - description: Optional[str] = None, - email_address: Optional[str] = None, - link: Optional[str] = None): - """ - Set properties of an organization. - - Args: - name (str): name of the organization - title (Optional[str]): the title of the organization - description (Optional[str]): description of the organization - email_address (Optional[str]): email address to reach the organization - (nothing to do with a cocalc account) - link (Optional[str]): a website of the organization - """ - raise NotImplementedError - - @api_method("org.addAdmin") - def add_admin(self, name: str, user: str): - """ - Make the user with given account_id or email an admin - of the named organization. Args: - name (str): name of the organization - user (str): email or account_id - """ - raise NotImplementedError - - @api_method("org.addUser") - def add_user(self, name: str, user: str): - """ - Make the user with given account_id or email a member - of the named organization. Only site admins can do this. - If you are an org admin, instead use create_user to create - new users in your organization, or contact support. - - Args: - name (str): name of the organization - user (str): email or account_id - """ - raise NotImplementedError - - @api_method("org.createUser") - def create_user(self, - name: str, - email: str, - firstName: Optional[str] = None, - lastName: Optional[str] = None, - password: Optional[str] = None) -> str: - """ - Create a new cocalc account that is a member of the - named organization. - - Args: - name (str): name of the organization - email (str): email address - firstName (Optional[str]): optional first name of the user - lastName (Optional[str]): optional last name of the user - password (Optional[str]): optional password (will be randomized if - not given; you can instead use create_token to grant temporary - account access). + limit (Optional[int]): Maximum number of messages to return. + offset (Optional[int]): Number of messages to skip. + type (Optional[Literal]): Filter by message type. Returns: - str: account_id of the new user + list[MessageType]: List of messages. """ - raise NotImplementedError - - @api_method("org.createToken") - def create_token(self, user: str) -> TokenType: - """ - Create a token that provides temporary access to the given - account. You must be an admin for the org that the user - belongs to or a site admin. - - Args: - user (str): email address or account_id - - Returns: - TokenType: token that grants temporary access - - Notes: - The returned `TokenType` has the following fields: - - - `token` (str): The random token itself, which you may retain - in case you want to explicitly expire it early. - - `url` (str): The url that the user should visit to sign in as - them. You can also test out this url, since the token works - multiple times. - """ - raise NotImplementedError - - @api_method("org.expireToken") - def expire_token(self, token: str): - """ - Immediately expire a token created using create_token. - - Args: - token (str): a token - """ - raise NotImplementedError - - @api_method("org.getUsers") - def get_users(self, name: str) -> OrganizationUser: - """ - Return list of all accounts that are members of the named organization. - - Args: - name (str): name of the organization - - Returns: - list[OrganizationUser] - - Notes: - The returned `OrganizationUser` has the following fields: - - - `first_name` (str) - - `last_name` (str) - - `account_id` (str): a uuid - - `email_address` (str) - """ - raise NotImplementedError + ... - @api_method("org.message") - def message(self, name: str, subject: str, body: str): - """ - Send a message from you to every account that is a member of - the named organization. - Args: - name (str): name of the organization - subject (str): plain text subject of the message - body (str): markdown body of the message (math typesetting works) - """ +""" +message: authFirst, +removeUser: authFirst, +removeAdmin: authFirst, +""" diff --git a/src/python/cocalc-api/src/cocalc_api/org.py b/src/python/cocalc-api/src/cocalc_api/org.py new file mode 100644 index 0000000000..8f6af5ef3f --- /dev/null +++ b/src/python/cocalc-api/src/cocalc_api/org.py @@ -0,0 +1,210 @@ +from typing import Any, Optional, TYPE_CHECKING +from .util import api_method +from .api_types import TokenType, OrganizationUser + +if TYPE_CHECKING: + from .hub import Hub + + +class Organizations: + + def __init__(self, parent: "Hub"): + self._parent = parent + + @api_method("org.getAll") + def get_all(self) -> dict[str, Any]: + """ + Get all organizations (site admins only). + + Returns: + dict[str, Any]: Organization data. + """ + ... + + @api_method("org.create") + def create(self, name: str) -> dict[str, Any]: + """ + Create an organization (site admins only). + + Args: + name (str): Name of the organization; must be globally unique, + at most 39 characters, and CANNOT BE CHANGED. + + Returns: + dict[str, Any]: Organization data. + """ + ... + + @api_method("org.get") + def get(self, name: str) -> dict[str, Any]: + """ + Get an organization. + + Args: + name (str): Name of the organization. + + Returns: + dict[str, Any]: Organization data. + """ + ... + + @api_method("org.set") + def set(self, + name: str, + title: Optional[str] = None, + description: Optional[str] = None, + email_address: Optional[str] = None, + link: Optional[str] = None) -> dict[str, Any]: + """ + Set properties of an organization. + + Args: + name (str): Name of the organization. + title (Optional[str]): The title of the organization. + description (Optional[str]): Description of the organization. + email_address (Optional[str]): Email address to reach the organization + (nothing to do with a cocalc account). + link (Optional[str]): A website of the organization. + """ + ... + + @api_method("org.addAdmin") + def add_admin(self, name: str, user: str) -> dict[str, Any]: + """ + Make the user with given account_id or email an admin + of the named organization. + + Args: + name (str): name of the organization + user (str): email or account_id + """ + ... + + @api_method("org.addUser") + def add_user(self, name: str, user: str) -> dict[str, Any]: + """ + Make the user with given account_id or email a member + of the named organization. Only site admins can do this. + If you are an org admin, instead use user to create + new users in your organization, or contact support. + + Args: + name (str): name of the organization + user (str): email or account_id + """ + ... + + @api_method("org.createUser") + def create_user(self, + name: str, + email: str, + firstName: Optional[str] = None, + lastName: Optional[str] = None, + password: Optional[str] = None) -> str: + """ + Create a new cocalc account that is a member of the + named organization. + + Args: + name (str): name of the organization + email (str): email address + firstName (Optional[str]): optional first name of the user + lastName (Optional[str]): optional last name of the user + password (Optional[str]): optional password (will be randomized if + not given; you can instead use create_token to grant temporary + account access). + + Returns: + str: account_id of the new user + """ + ... + + @api_method("org.createToken") + def create_token(self, user: str) -> TokenType: + """ + Create a token that provides temporary access to the given + account. You must be an admin for the org that the user + belongs to or a site admin. + + Args: + user (str): email address or account_id + + Returns: + TokenType: token that grants temporary access + + Notes: + The returned `TokenType` has the following fields: + + - `token` (str): The random token itself, which you may retain + in case you want to explicitly expire it early. + - `url` (str): The url that the user should visit to sign in as + them. You can also test out this url, since the token works + multiple times. + """ + ... + + @api_method("org.expireToken") + def expire_token(self, token: str) -> dict[str, Any]: + """ + Immediately expire a token created using create_token. + + Args: + token (str): a token + """ + ... + + @api_method("org.getUsers") + def get_users(self, name: str) -> list[OrganizationUser]: # type: ignore[empty-body] + """ + Return list of all accounts that are members of the named organization. + + Args: + name (str): name of the organization + + Returns: + list[OrganizationUser] + + Notes: + The returned `OrganizationUser` has the following fields: + + - `first_name` (str) + - `last_name` (str) + - `account_id` (str): a uuid + - `email_address` (str) + """ + ... + + @api_method("org.removeUser") + def remove_user(self, name: str, user: str) -> dict[str, Any]: + """ + Remove a user from an organization. + + Args: + name (str): name of the organization + user (str): email or account_id + """ + ... + + @api_method("org.removeAdmin") + def remove_admin(self, name: str, user: str) -> dict[str, Any]: + """ + Remove an admin from an organization. + + Args: + name (str): name of the organization + user (str): email or account_id + """ + ... + + @api_method("org.message") + def message(self, name: str, subject: str, body: str) -> dict[str, Any]: + """ + Send a message from you to every account that is a member of + the named organization. + + Args: + name (str): name of the organization + subject (str): plain text subject of the message + body (str): markdown body of the message (math typesetting works) + """ + ... diff --git a/src/python/cocalc-api/src/cocalc_api/project.py b/src/python/cocalc-api/src/cocalc_api/project.py index 4c50f95e6b..5f9779834f 100644 --- a/src/python/cocalc-api/src/cocalc_api/project.py +++ b/src/python/cocalc-api/src/cocalc_api/project.py @@ -6,20 +6,14 @@ class Project: - def __init__(self, - api_key: str, - host: str = "https://cocalc.com", - project_id: Optional[str] = None): + def __init__(self, api_key: str, host: str = "https://cocalc.com", project_id: Optional[str] = None): self.project_id = project_id self.api_key = api_key self.host = host - self.client = httpx.Client( - auth=(api_key, ""), headers={"Content-Type": "application/json"}) + # Use longer timeout for API calls (60 seconds to handle slow kernel startups in CI) + self.client = httpx.Client(auth=(api_key, ""), headers={"Content-Type": "application/json"}, timeout=60.0) - def call(self, - name: str, - arguments: list[Any], - timeout: Optional[int] = None) -> Any: + def call(self, name: str, arguments: list[Any], timeout: Optional[int] = None) -> Any: """ Perform an API call to the CoCalc backend. @@ -31,14 +25,10 @@ def call(self, Returns: Any: JSON-decoded response from the API. """ - payload: dict[str, Any] = { - "name": name, - "args": arguments, - "project_id": self.project_id - } + payload: dict[str, Any] = {"name": name, "args": arguments, "project_id": self.project_id} if timeout is not None: payload["timeout"] = timeout - resp = self.client.post(self.host + '/api/conat/project', json=payload) + resp = self.client.post(self.host + "/api/conat/project", json=payload) resp.raise_for_status() return handle_error(resp.json()) @@ -59,18 +49,18 @@ def ping(self) -> PingResponse: Ping the project. Returns: - Any: JSON object containing the current server time. + PingResponse: JSON object containing the current server time. Examples: - Ping a project. The api_key can be either an account api key or a project + Ping a project. The api_key can be either an account api key or a project specific api key (in which case the project_id option is optional): - >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...", project_id='...') + >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...", project_id='...') >>> project.ping() {'now': 1756489740133} """ - raise NotImplementedError + ... @api_method("system.exec", timeout_seconds=True) def exec( @@ -90,16 +80,15 @@ def exec( Execute an arbitrary shell command in the project. Args: - - command (str): command to run; can be a program name (e.g., "ls") or absolute path, or a full bash script - args (Optional[list[str]]): optional arguments to the command - path (Optional[str]): path (relative to HOME directory) where command will be run - cwd (Optional[str]): absolute path where code excuted from (if path not given) - timeout (Optional[int]): optional timeout in SECONDS - max_output (Optional[int]): bound on size of stdout and stderr; further output ignored - bash (Optional[bool]): if True, ignore args and evaluate command as a bash command - env (Optional[dict[str, Any]]): if given, added to exec environment - compute_server_id (Optional[number]): compute server to run code on (instead of home base project) + command (str): Command to run; can be a program name (e.g., "ls") or absolute path, or a full bash script. + args (Optional[list[str]]): Optional arguments to the command. + path (Optional[str]): Path (relative to HOME directory) where command will be run. + cwd (Optional[str]): Absolute path where code executed from (if path not given). + timeout (Optional[int]): Optional timeout in SECONDS. + max_output (Optional[int]): Bound on size of stdout and stderr; further output ignored. + bash (Optional[bool]): If True, ignore args and evaluate command as a bash command. + env (Optional[dict[str, Any]]): If given, added to exec environment. + compute_server_id (Optional[int]): Compute server to run code on (instead of home base project). Returns: ExecuteCodeOutput: Result of executing the command. @@ -111,16 +100,14 @@ def exec( - `stderr` (str): Output written to stderr. - `exit_code` (int): Exit code of the process. - Examples: - >>> import cocalc_api >>> project = cocalc_api.Project(api_key="sk-...", - project_id='6e75dbf1-0342-4249-9dce-6b21648656e9') + ... project_id='6e75dbf1-0342-4249-9dce-6b21648656e9') >>> project.system.exec(command="echo 'hello from cocalc'") {'stdout': 'hello from cocalc\\n', 'stderr':'', 'exit_code': 0} """ - raise NotImplementedError + ... @api_method("system.jupyterExecute") def jupyter_execute( @@ -129,26 +116,84 @@ def jupyter_execute( kernel: str, history: Optional[list[str]] = None, path: Optional[str] = None, - ): + ) -> list[dict[str, Any]]: # type: ignore[empty-body] """ Execute code using a Jupyter kernel. Args: input (str): Code to execute. - kernel (Optional[str]): Name of kernel to use. Get options using jupyter.kernels() + kernel (str): Name of kernel to use. Get options using hub.jupyter.kernels(). history (Optional[list[str]]): Array of previous inputs (they get evaluated every time, but without output being captured). path (Optional[str]): File path context for execution. Returns: - Any: JSON response containing execution results. + list[dict[str, Any]]: List of output items. Each output item contains + execution results with 'data' field containing output by MIME type + (e.g., 'text/plain' for text output) or 'name'/'text' fields for + stream output (stdout/stderr). Examples: Execute a simple sum using a Jupyter kernel: - >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...") - >>> project.jupyter.execute(history=['a=100;print(a)'], - input='sum(range(a+1))', - kernel='python3') - {'output': [{'data': {'text/plain': '5050'}}], ...} + >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...", project_id='...') + >>> result = project.system.jupyter_execute(input='sum(range(100))', kernel='python3') + >>> result + [{'data': {'text/plain': '4950'}}] + + Execute with history context: + + >>> result = project.system.jupyter_execute( + ... history=['a = 100'], + ... input='sum(range(a + 1))', + ... kernel='python3') + >>> result + [{'data': {'text/plain': '5050'}}] + + Print statements produce stream output: + + >>> result = project.system.jupyter_execute(input='print("Hello")', kernel='python3') + >>> result + [{'name': 'stdout', 'text': 'Hello\\n'}] + """ + ... + + @api_method("system.listJupyterKernels") + def list_jupyter_kernels(self) -> list[dict[str, Any]]: # type: ignore[empty-body] + """ + List all running Jupyter kernels in the project. + + Returns: + list[dict[str, Any]]: List of running kernels. Each kernel has: + - pid (int): Process ID of the kernel + - connectionFile (str): Path to the kernel connection file + - kernel_name (str, optional): Name of the kernel (e.g., 'python3') + + Examples: + List all running kernels: + + >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...", project_id='...') + >>> kernels = project.system.list_jupyter_kernels() + >>> kernels + [{'pid': 12345, 'connectionFile': '/run/user/1000/jupyter/kernel-abc123.json', 'kernel_name': 'python3'}] + """ + ... + + @api_method("system.stopJupyterKernel") + def stop_jupyter_kernel(self, pid: int) -> dict[str, bool]: # type: ignore[empty-body] + """ + Stop a specific Jupyter kernel by process ID. + + Args: + pid (int): Process ID of the kernel to stop + + Returns: + dict[str, bool]: Dictionary with 'success' key indicating if the kernel was stopped + + Examples: + Stop a kernel by PID: + + >>> import cocalc_api; project = cocalc_api.Project(api_key="sk-...", project_id='...') + >>> project.system.stop_jupyter_kernel(pid=12345) + {'success': True} """ ... diff --git a/src/python/cocalc-api/src/cocalc_api/util.py b/src/python/cocalc-api/src/cocalc_api/util.py index 35c2ab7e1a..16a607c9c6 100644 --- a/src/python/cocalc-api/src/cocalc_api/util.py +++ b/src/python/cocalc-api/src/cocalc_api/util.py @@ -9,9 +9,7 @@ def handle_error(x: Any) -> Any: return x -def api_method(name: str, - opts: bool = False, - timeout_seconds: bool = False) -> Callable: +def api_method(name: str, opts: bool = False, timeout_seconds: bool = False) -> Callable: """ Decorator for CoCalcAPI methods. Converts arguments (excluding self) into a dict, removes None values, @@ -32,11 +30,7 @@ def wrapper(self, *args, **kwargs) -> Any: # Bind args/kwargs to parameter names bound = sig.bind(self, *args, **kwargs) bound.apply_defaults() - args_dict = { - k: v - for k, v in bound.arguments.items() - if k != "self" and v is not None - } + args_dict = {k: v for k, v in bound.arguments.items() if k != "self" and v is not None} if timeout_seconds and 'timeout' in args_dict: timeout = 1000 * args_dict['timeout'] else: diff --git a/src/python/cocalc-api/tests/README.md b/src/python/cocalc-api/tests/README.md new file mode 100644 index 0000000000..b367d15383 --- /dev/null +++ b/src/python/cocalc-api/tests/README.md @@ -0,0 +1,139 @@ +# CoCalc API Tests + +This directory contains pytest tests for the cocalc-api Python package. + +## Prerequisites + +1. **Required**: Set the `COCALC_API_KEY` environment variable with a valid CoCalc API key (tests will fail if not set) +2. **Recommended**: Set `PGHOST` for database cleanup (see [Automatic Cleanup](#automatic-cleanup) below) +3. Optionally set `COCALC_HOST` to specify the CoCalc server URL (defaults to `http://localhost:5000`) + +## Running Tests + +```bash +# Run all tests +make test + +# Run tests with verbose output +make test-verbose + +# Or use pytest directly +uv run pytest +uv run pytest -v + +# Run specific test files +uv run pytest tests/test_hub.py -v +uv run pytest tests/test_jupyter.py -v +``` + +## Test Structure + +- `conftest.py` - Pytest configuration and fixtures (includes resource tracking and cleanup) +- `test_hub.py` - Tests for Hub client functionality (projects, database queries, messages) +- `test_project.py` - Tests for Project client functionality (ping, exec commands) +- `test_jupyter.py` - Tests for Jupyter kernel installation and code execution +- `test_org.py` - Tests for organization management (create, users, licenses) +- `test_org_basic.py` - Basic organization API tests + +## Environment Variables + +### Required + +- `COCALC_API_KEY` - Your CoCalc API key + +### Optional + +- `COCALC_HOST` - CoCalc server URL (default: `http://localhost:5000`) +- `COCALC_TESTS_CLEANUP` - Enable/disable automatic cleanup (default: `true`) + +### For Database Cleanup (Recommended) + +- `PGHOST` - PostgreSQL host (socket path or hostname) +- `PGUSER` - PostgreSQL user (default: `smc`) +- `PGDATABASE` - PostgreSQL database (default: `smc`) +- `PGPORT` - PostgreSQL port for network connections (default: `5432`) +- `PGPASSWORD` - PostgreSQL password (only needed for network connections) + +## Resource Tracking and Cleanup + +### Tracked Resource System + +The test suite uses a **resource tracking system** to automatically manage all created resources. When writing tests, use the provided helper functions to ensure proper cleanup: + +```python +def test_my_feature(hub, resource_tracker): + # Create tracked resources using helper functions + org_id = create_tracked_org(hub, resource_tracker, "test-org") + user_id = create_tracked_user(hub, resource_tracker, "test-org", email="test@example.com") + project_id = create_tracked_project(hub, resource_tracker, title="Test Project") + + # Run your tests... + + # No manual cleanup needed - happens automatically! +``` + +**Available Helper Functions:** + +- `create_tracked_project(hub, resource_tracker, **kwargs)` - Create and track a project +- `create_tracked_user(hub, resource_tracker, org_name, **kwargs)` - Create and track a user account +- `create_tracked_org(hub, resource_tracker, org_name)` - Create and track an organization + +All tracked resources are automatically cleaned up at the end of the test session. + +### Shared Fixtures + +**Session-scoped fixtures** (created once, shared across all tests): + +- `temporary_project` - A single test project used by all tests in the session +- `project_client` - A Project client instance connected to the temporary project +- `hub` - A Hub client instance + +These fixtures ensure efficient resource usage by reusing the same project across all tests. + +### Automatic Cleanup + +At the end of each test session, the cleanup system automatically: + +1. **Stops** all tracked projects (via API to gracefully shut them down) +2. **Hard-deletes** all tracked resources from the PostgreSQL database in order: + - Projects (removed first) + - Accounts (including all owned projects like "My First Project") + - Organizations (removed last) + +#### Cleanup Configuration + +**Socket Connection (Local Development - Recommended):** + +```bash +export PGHOST=/path/to/cocalc-data/socket +export PGUSER=smc +# No password needed for Unix socket authentication +``` + +**Network Connection:** + +```bash +export PGHOST=localhost +export PGPORT=5432 +export PGUSER=smc +export PGPASSWORD=your_password +``` + +**Disable Cleanup (Not Recommended):** + +```bash +export COCALC_TESTS_CLEANUP=false +``` + +When cleanup is disabled, test resources will remain in the database and must be manually removed. + +#### Why Direct Database Cleanup? + +The test suite uses **direct PostgreSQL deletion** instead of API calls because: + +- API deletion only sets `deleted=true` (soft delete), leaving data in the database +- Tests create many resources (projects, accounts, orgs) that need complete removal +- Direct SQL ensures thorough cleanup including auto-created projects (e.g., "My First Project") +- Prevents database bloat from repeated test runs + +The cleanup process is safe and only removes resources that were explicitly tracked during test execution. diff --git a/src/python/cocalc-api/tests/__init__.py b/src/python/cocalc-api/tests/__init__.py new file mode 100644 index 0000000000..5149a75101 --- /dev/null +++ b/src/python/cocalc-api/tests/__init__.py @@ -0,0 +1 @@ +# Tests package for cocalc-api \ No newline at end of file diff --git a/src/python/cocalc-api/tests/conftest.py b/src/python/cocalc-api/tests/conftest.py new file mode 100644 index 0000000000..92c8c1ab81 --- /dev/null +++ b/src/python/cocalc-api/tests/conftest.py @@ -0,0 +1,449 @@ +""" +Pytest configuration and fixtures for cocalc-api tests. +""" +import os +import time +import uuid +import pytest + +from cocalc_api import Hub, Project + +from psycopg2 import pool as pg_pool + +# Database configuration examples (DRY principle) +PGHOST_SOCKET_EXAMPLE = "/path/to/cocalc-data/socket" +PGHOST_NETWORK_EXAMPLE = "localhost" + + +def assert_valid_uuid(value, description="value"): + """ + Assert that the given value is a string and a valid UUID. + + Args: + value: The value to check + description: Description of the value for error messages + """ + assert isinstance(value, str), f"{description} should be a string, got {type(value)}" + assert len(value) > 0, f"{description} should not be empty" + + try: + uuid.UUID(value) + except ValueError: + pytest.fail(f"{description} should be a valid UUID, got: {value}") + + +def cleanup_project(hub, project_id): + """ + Clean up a test project by stopping it and deleting it. + + Args: + hub: Hub client instance + project_id: Project ID to cleanup + """ + try: + hub.projects.stop(project_id) + except Exception as e: + print(f"Warning: Failed to stop project {project_id}: {e}") + + try: + hub.projects.delete(project_id) + except Exception as e: + print(f"Warning: Failed to delete project {project_id}: {e}") + + +@pytest.fixture(scope="session") +def api_key(): + """Get API key from environment variable.""" + key = os.environ.get("COCALC_API_KEY") + if not key: + pytest.fail("COCALC_API_KEY environment variable is required but not set") + return key + + +@pytest.fixture(scope="session") +def cocalc_host(): + """Get CoCalc host from environment variable, default to localhost:5000.""" + return os.environ.get("COCALC_HOST", "http://localhost:5000") + + +@pytest.fixture(scope="session") +def hub(api_key, cocalc_host): + """Create Hub client instance.""" + return Hub(api_key=api_key, host=cocalc_host) + + +@pytest.fixture(scope="session") +def temporary_project(hub, resource_tracker, request): + """ + Create a temporary project for testing and return project info. + Uses a session-scoped fixture so only ONE project is created for the entire test suite. + """ + # Create a project with a timestamp to make it unique and identifiable + timestamp = time.strftime("%Y%m%d-%H%M%S") + title = f"CoCalc API Test {timestamp}" + description = "Temporary project created by cocalc-api tests" + + # Use tracked creation + project_id = create_tracked_project(hub, resource_tracker, title=title, description=description) + + # Start the project so it can respond to API calls + try: + hub.projects.start(project_id) + + # Wait for project to be ready (can take 10-15 seconds) + from cocalc_api import Project + + for attempt in range(10): + time.sleep(5) # Wait 5 seconds before checking + try: + # Try to ping the project to see if it's ready + test_project = Project(project_id=project_id, api_key=hub.api_key, host=hub.host) + test_project.system.ping() # If this succeeds, project is ready + break + except Exception: + if attempt == 9: # Last attempt + print(f"Warning: Project {project_id} did not become ready within 50 seconds") + + except Exception as e: + print(f"Warning: Failed to start project {project_id}: {e}") + + project_info = {'project_id': project_id, 'title': title, 'description': description} + + # Note: No finalizer needed - cleanup happens automatically via cleanup_all_test_resources + + return project_info + + +@pytest.fixture(scope="session") +def project_client(temporary_project, api_key, cocalc_host): + """Create Project client instance using temporary project.""" + return Project(project_id=temporary_project['project_id'], api_key=api_key, host=cocalc_host) + + +# ============================================================================ +# Database Cleanup Infrastructure +# ============================================================================ + + +@pytest.fixture(scope="session") +def resource_tracker(): + """ + Track all resources created during tests for cleanup. + + This fixture provides a dictionary of sets that automatically tracks + all projects, accounts, and organizations created during test execution. + At the end of the test session, all tracked resources are automatically + hard-deleted from the database. + + Usage: + def test_my_feature(hub, resource_tracker): + # Create tracked resources using helper functions + org_id = create_tracked_org(hub, resource_tracker, "test-org") + user_id = create_tracked_user(hub, resource_tracker, "test-org", email="test@example.com") + project_id = create_tracked_project(hub, resource_tracker, title="Test Project") + + # Test logic here... + + # No cleanup needed - happens automatically! + + Returns a dictionary with sets for tracking: + - projects: set of project_id (UUID strings) + - accounts: set of account_id (UUID strings) + - organizations: set of organization names (strings) + """ + tracker = { + 'projects': set(), + 'accounts': set(), + 'organizations': set(), + } + return tracker + + +@pytest.fixture(scope="session") +def check_cleanup_config(): + """ + Check cleanup configuration BEFORE any tests run. + Fails fast if cleanup is enabled but database credentials are missing. + """ + cleanup_enabled = os.environ.get("COCALC_TESTS_CLEANUP", "true").lower() != "false" + + if not cleanup_enabled: + print("\n⚠ Database cleanup DISABLED via COCALC_TESTS_CLEANUP=false") + print(" Test resources will remain in the database.") + return # Skip checks if cleanup is disabled + + # Cleanup is enabled - verify required configuration + pghost = os.environ.get("PGHOST") + + # PGHOST is mandatory + if not pghost: + pytest.exit("\n" + "=" * 70 + "\n" + "ERROR: Database cleanup is enabled but PGHOST is not set!\n\n" + "To run tests, you must either:\n" + f" 1. Set PGHOST for socket connection (no password needed):\n" + f" export PGHOST={PGHOST_SOCKET_EXAMPLE}\n\n" + f" 2. Set PGHOST for network connection (requires PGPASSWORD):\n" + f" export PGHOST={PGHOST_NETWORK_EXAMPLE}\n" + " export PGPASSWORD=your_password\n\n" + " 3. Disable cleanup (not recommended):\n" + " export COCALC_TESTS_CLEANUP=false\n" + "=" * 70, + returncode=1) + + +@pytest.fixture(scope="session") +def db_pool(check_cleanup_config): + """ + Create a PostgreSQL connection pool for direct database cleanup. + + Supports both Unix socket and network connections: + + Socket connection (local dev): + export PGUSER=smc + export PGHOST=/path/to/cocalc-data/socket + # No password needed for socket auth + + Network connection: + export PGUSER=smc + export PGHOST=localhost + export PGPORT=5432 + export PGPASSWORD=your_password + + To disable cleanup: + export COCALC_TESTS_CLEANUP=false + """ + # Check if cleanup is disabled + cleanup_enabled = os.environ.get("COCALC_TESTS_CLEANUP", "true").lower() != "false" + + if not cleanup_enabled: + print("\n⚠ Database cleanup DISABLED via COCALC_TESTS_CLEANUP=false") + print(" Test resources will remain in the database.") + return None + + # Get connection parameters with defaults + pguser = os.environ.get("PGUSER", "smc") + pghost = os.environ.get("PGHOST") + pgport = os.environ.get("PGPORT", "5432") + pgdatabase = os.environ.get("PGDATABASE", "smc") + pgpassword = os.environ.get("PGPASSWORD") + + # PGHOST is mandatory (already checked in check_cleanup_config, but double-check) + if not pghost: + pytest.fail("\n" + "=" * 70 + "\n" + "ERROR: PGHOST environment variable is required for database cleanup!\n" + "=" * 70) + + # Determine if using socket or network connection + is_socket = pghost.startswith("/") + + # Build connection kwargs + conn_kwargs = { + "host": pghost, + "database": pgdatabase, + "user": pguser, + } + + # Only add port for network connections + if not is_socket: + conn_kwargs["port"] = pgport + + # Only add password if provided + if pgpassword: + conn_kwargs["password"] = pgpassword + + try: + connection_pool = pg_pool.SimpleConnectionPool(1, 5, **conn_kwargs) + + if is_socket: + print(f"\n✓ Database cleanup enabled (socket): {pguser}@{pghost}/{pgdatabase}") + else: + print(f"\n✓ Database cleanup enabled (network): {pguser}@{pghost}:{pgport}/{pgdatabase}") + + yield connection_pool + + connection_pool.closeall() + + except Exception as e: + conn_type = "socket" if is_socket else "network" + pytest.fail("\n" + "=" * 70 + "\n" + f"ERROR: Failed to connect to database ({conn_type}) for cleanup:\n{e}\n\n" + f"Connection details:\n" + f" Host: {pghost}\n" + f" Database: {pgdatabase}\n" + f" User: {pguser}\n" + (f" Port: {pgport}\n" if not is_socket else "") + + "\nTo disable cleanup: export COCALC_TESTS_CLEANUP=false\n" + "=" * 70) + + +def create_tracked_project(hub, resource_tracker, **kwargs): + """Create a project and register it for cleanup.""" + project_id = hub.projects.create_project(**kwargs) + resource_tracker['projects'].add(project_id) + return project_id + + +def create_tracked_user(hub, resource_tracker, org_name, **kwargs): + """Create a user and register it for cleanup.""" + user_id = hub.org.create_user(name=org_name, **kwargs) + resource_tracker['accounts'].add(user_id) + return user_id + + +def create_tracked_org(hub, resource_tracker, org_name): + """Create an organization and register it for cleanup.""" + org_id = hub.org.create(org_name) + resource_tracker['organizations'].add(org_name) # Track by name + return org_id + + +def hard_delete_projects(db_pool, project_ids): + """Hard delete projects from database using direct SQL.""" + if not project_ids: + return + + conn = db_pool.getconn() + try: + cursor = conn.cursor() + for project_id in project_ids: + try: + cursor.execute("DELETE FROM projects WHERE project_id = %s", (project_id, )) + conn.commit() + print(f" ✓ Deleted project {project_id}") + except Exception as e: + conn.rollback() + print(f" ✗ Failed to delete project {project_id}: {e}") + cursor.close() + finally: + db_pool.putconn(conn) + + +def hard_delete_accounts(db_pool, account_ids): + """ + Hard delete accounts from database using direct SQL. + + This also finds and deletes ALL projects where the account is the owner, + including auto-created projects like "My First Project". + """ + if not account_ids: + return + + conn = db_pool.getconn() + try: + cursor = conn.cursor() + for account_id in account_ids: + try: + # First, find ALL projects where this account is the owner + # The users JSONB field has structure: {"account_id": {"group": "owner", ...}} + cursor.execute( + """ + SELECT project_id FROM projects + WHERE users ? %s + AND users->%s->>'group' = 'owner' + """, (account_id, account_id)) + owned_projects = cursor.fetchall() + + # Delete all owned projects (including auto-created ones) + for (project_id, ) in owned_projects: + cursor.execute("DELETE FROM projects WHERE project_id = %s", (project_id, )) + print(f" ✓ Deleted owned project {project_id} for account {account_id}") + + # Remove from organizations (admin_account_ids array and users JSONB) + cursor.execute( + "UPDATE organizations SET admin_account_ids = array_remove(admin_account_ids, %s), users = users - %s WHERE users ? %s", + (account_id, account_id, account_id)) + + # Remove from remaining project collaborators (users JSONB field) + cursor.execute("UPDATE projects SET users = users - %s WHERE users ? %s", (account_id, account_id)) + + # Delete the account + cursor.execute("DELETE FROM accounts WHERE account_id = %s", (account_id, )) + conn.commit() + print(f" ✓ Deleted account {account_id}") + except Exception as e: + conn.rollback() + print(f" ✗ Failed to delete account {account_id}: {e}") + cursor.close() + finally: + db_pool.putconn(conn) + + +def hard_delete_organizations(db_pool, org_names): + """Hard delete organizations from database using direct SQL.""" + if not org_names: + return + + conn = db_pool.getconn() + try: + cursor = conn.cursor() + for org_name in org_names: + try: + cursor.execute("DELETE FROM organizations WHERE name = %s", (org_name, )) + conn.commit() + print(f" ✓ Deleted organization {org_name}") + except Exception as e: + conn.rollback() + print(f" ✗ Failed to delete organization {org_name}: {e}") + cursor.close() + finally: + db_pool.putconn(conn) + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_all_test_resources(hub, resource_tracker, db_pool, request): + """ + Automatically clean up all tracked resources at the end of the test session. + + Cleanup is enabled by default. To disable: + export COCALC_TESTS_CLEANUP=false + """ + + def cleanup(): + # Skip cleanup if db_pool is None (cleanup disabled) + if db_pool is None: + print("\n⚠ Skipping database cleanup (COCALC_TESTS_CLEANUP=false)") + return + + print("\n" + "=" * 70) + print("CLEANING UP TEST RESOURCES FROM DATABASE") + print("=" * 70) + + total_projects = len(resource_tracker['projects']) + total_accounts = len(resource_tracker['accounts']) + total_orgs = len(resource_tracker['organizations']) + + print("\nResources to clean up:") + print(f" - Projects: {total_projects}") + print(f" - Accounts: {total_accounts}") + print(f" - Organizations: {total_orgs}") + + # First, soft-delete projects via API (stop them gracefully) + if total_projects > 0: + print(f"\nStopping {total_projects} projects...") + for project_id in resource_tracker['projects']: + try: + cleanup_project(hub, project_id) + except Exception as e: + print(f" Warning: Failed to stop project {project_id}: {e}") + + # Then hard-delete from database in order: + # 1. Projects (no dependencies) + if total_projects > 0: + print(f"\nHard-deleting {total_projects} projects from database...") + hard_delete_projects(db_pool, resource_tracker['projects']) + + # 2. Accounts (must remove from organizations/projects first) + if total_accounts > 0: + print(f"\nHard-deleting {total_accounts} accounts from database...") + hard_delete_accounts(db_pool, resource_tracker['accounts']) + + # 3. Organizations (no dependencies after accounts removed) + if total_orgs > 0: + print(f"\nHard-deleting {total_orgs} organizations from database...") + hard_delete_organizations(db_pool, resource_tracker['organizations']) + + print("\n✓ Test resource cleanup complete!") + print("=" * 70) + + request.addfinalizer(cleanup) + + yield diff --git a/src/python/cocalc-api/tests/test_hub.py b/src/python/cocalc-api/tests/test_hub.py new file mode 100644 index 0000000000..675027a5ff --- /dev/null +++ b/src/python/cocalc-api/tests/test_hub.py @@ -0,0 +1,347 @@ +""" +Tests for Hub client functionality. +""" +import time +import pytest + +from cocalc_api import Hub, Project +from .conftest import assert_valid_uuid, cleanup_project, create_tracked_project, create_tracked_user, create_tracked_org + + +class TestHubSystem: + """Tests for Hub system operations.""" + + def test_ping(self, hub): + """Test basic ping connectivity with retry logic.""" + # Retry with exponential backoff in case server is still starting up + max_attempts = 5 + delay = 2 # Start with 2 second delay + + for attempt in range(max_attempts): + try: + result = hub.system.ping() + assert result is not None + # The ping response should contain some basic server info + assert isinstance(result, dict) + print(f"✓ Server ping successful on attempt {attempt + 1}") + return # Success! + except Exception as e: + if attempt < max_attempts - 1: + print(f"Ping attempt {attempt + 1} failed, retrying in {delay}s... ({e})") + time.sleep(delay) + delay *= 2 # Exponential backoff + else: + pytest.fail(f"Server ping failed after {max_attempts} attempts: {e}") + + def test_hub_initialization(self, api_key, cocalc_host): + """Test Hub client initialization.""" + hub = Hub(api_key=api_key, host=cocalc_host) + assert hub.api_key == api_key + assert hub.host == cocalc_host + assert hub.client is not None + + def test_invalid_api_key(self, cocalc_host): + """Test behavior with invalid API key.""" + hub = Hub(api_key="invalid_key", host=cocalc_host) + with pytest.raises((ValueError, RuntimeError, Exception)): # Should raise authentication error + hub.system.ping() + + def test_multiple_pings(self, hub): + """Test that multiple ping calls work consistently.""" + for _i in range(3): + result = hub.system.ping() + assert result is not None + assert isinstance(result, dict) + + def test_user_search(self, hub, resource_tracker): + """Test user search functionality.""" + import time + timestamp = int(time.time()) + + # Create a test organization and user with a unique email + org_name = f"search-test-org-{timestamp}" + test_email = f"search-test-user-{timestamp}@test.local" + test_first_name = f"SearchFirst{timestamp}" + test_last_name = f"SearchLast{timestamp}" + + # Use tracked creation + org_id = create_tracked_org(hub, resource_tracker, org_name) + print(f"\nCreated test organization: {org_name} (ID: {org_id})") + + # Create a user with unique identifiable names + user_id = create_tracked_user(hub, resource_tracker, org_name, email=test_email, firstName=test_first_name, lastName=test_last_name) + print(f"Created test user: {user_id}, email: {test_email}") + + # Give the database a moment to index the new user + time.sleep(0.5) + + # Test 1: Search by email (exact match should return only this user) + print("\n1. Testing search by email...") + results = hub.system.user_search(test_email) + assert isinstance(results, list), "user_search should return a list" + assert len(results) >= 1, f"Expected at least 1 result for email {test_email}, got {len(results)}" + + # Find our user in the results + our_user = None + for user in results: + if user.get('email_address') == test_email: + our_user = user + break + + assert our_user is not None, f"Expected to find user with email {test_email} in results" + print(f" Found user by email: {our_user['account_id']}") + + # Verify the structure of the result + assert 'account_id' in our_user + assert 'first_name' in our_user + assert 'last_name' in our_user + assert our_user['first_name'] == test_first_name + assert our_user['last_name'] == test_last_name + assert our_user['account_id'] == user_id + print(f" User data: first_name={our_user['first_name']}, last_name={our_user['last_name']}") + + # Test 2: Search by full first name (to ensure we find our user) + print("\n2. Testing search by full first name...") + # Use the full first name which is guaranteed unique with timestamp + results = hub.system.user_search(test_first_name) + assert isinstance(results, list) + print(f" Search for '{test_first_name}' returned {len(results)} results") + # Our user should be in the results + found = any(u.get('account_id') == user_id for u in results) + if not found and len(results) > 0: + print(f" Found these first names: {[u.get('first_name') for u in results]}") + assert found, f"Expected to find user {user_id} when searching for '{test_first_name}'" + print(f" Found user in {len(results)} results") + + # Test 3: Search by full last name (to ensure we find our user) + print("\n3. Testing search by full last name...") + # Use the full last name which is guaranteed unique with timestamp + results = hub.system.user_search(test_last_name) + assert isinstance(results, list) + found = any(u.get('account_id') == user_id for u in results) + assert found, f"Expected to find user {user_id} when searching for '{test_last_name}'" + print(f" Found user in {len(results)} results") + + # Test 4: Nonexistent search should return empty list + print("\n4. Testing search with unlikely query...") + unlikely_query = f"xyznonexistent{timestamp}abc" + results = hub.system.user_search(unlikely_query) + assert isinstance(results, list) + assert len(results) == 0, f"Expected 0 results for non-existent query, got {len(results)}" + print(" Search for non-existent query correctly returned 0 results") + + print("\n✅ User search test completed successfully!") + + # Note: No cleanup needed - happens automatically via cleanup_all_test_resources + + +class TestHubProjects: + """Tests for Hub project operations.""" + + def test_create_project(self, hub, resource_tracker): + """Test creating a project via hub.projects.create_project.""" + import time + timestamp = int(time.time()) + title = f"test-project-{timestamp}" + description = "Test project for API testing" + + project_id = create_tracked_project(hub, resource_tracker, title=title, description=description) + + assert project_id is not None + assert_valid_uuid(project_id, "Project ID") + + # Note: No cleanup needed - happens automatically + + def test_list_projects(self, hub): + """Test listing projects.""" + projects = hub.projects.get() + assert isinstance(projects, list) + # Each project should have basic fields + for project in projects: + assert 'project_id' in project + assert isinstance(project['project_id'], str) + + def test_delete_method_exists(self, hub): + """Test that delete method is available and callable.""" + # Test that the delete method exists and is callable + assert hasattr(hub.projects, 'delete') + assert callable(hub.projects.delete) + + # Note: We don't actually delete anything in this test since + # deletion is tested in the project lifecycle via temporary_project fixture + + def test_project_lifecycle(self, hub, resource_tracker): + """Test complete project lifecycle: create, wait for ready, run command, delete, verify deletion.""" + + # 1. Create a project + timestamp = int(time.time()) + title = f"lifecycle-test-{timestamp}" + description = "Test project for complete lifecycle testing" + + print(f"\n1. Creating project '{title}'...") + project_id = create_tracked_project(hub, resource_tracker, title=title, description=description) + assert project_id is not None + assert_valid_uuid(project_id, "Project ID") + print(f" Created project: {project_id}") + + # Start the project + print("2. Starting project...") + hub.projects.start(project_id) + print(" Project start request sent") + + # Wait for project to become ready + print("3. Waiting for project to become ready...") + project_client = Project(project_id=project_id, api_key=hub.api_key, host=hub.host) + + ready = False + for attempt in range(12): # 60 seconds max wait time + time.sleep(5) + try: + project_client.system.ping() + ready = True + print(f" ✓ Project ready after {(attempt + 1) * 5} seconds") + break + except Exception as e: + if attempt == 11: # Last attempt + print(f" Warning: Project not ready after 60 seconds: {e}") + else: + print(f" Attempt {attempt + 1}: Project not ready yet...") + + # Check that project exists in database + print("4. Checking project exists in database...") + projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id) + assert len(projects) == 1, f"Expected 1 project, found {len(projects)}" + project = projects[0] + assert project["project_id"] == project_id + assert project["title"] == title + assert project.get("deleted") is None or project.get("deleted") is False + print(f" ✓ Project found in database: title='{project['title']}', deleted={project.get('deleted')}") + + # 2. Run a command if project is ready + if ready: + print("5. Running 'uname -a' command...") + result = project_client.system.exec("uname -a") + assert "stdout" in result + output = result["stdout"] + assert "Linux" in output, f"Expected Linux system, got: {output}" + assert result["exit_code"] == 0, f"Command failed with exit code {result['exit_code']}" + print(f" ✓ Command executed successfully: {output.strip()}") + else: + print("5. Skipping command execution - project not ready") + + # 3. Stop and delete the project + print("6. Stopping and deleting project...") + cleanup_project(hub, project_id) + + # 4. Verify project is marked as deleted in database + print("8. Verifying project is marked as deleted...") + projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id, all=True) + assert len(projects) == 1, f"Expected 1 project (still in DB), found {len(projects)}" + project = projects[0] + assert project["project_id"] == project_id + assert project.get("deleted") is True, f"Expected deleted=True, got deleted={project.get('deleted')}" + print(f" ✓ Project correctly marked as deleted in database: deleted={project.get('deleted')}") + + print("✅ Project lifecycle test completed successfully!") + + # Note: No cleanup needed - hard-delete happens automatically at session end + + def test_collaborator_management(self, hub, resource_tracker): + """Test adding and removing collaborators from a project.""" + import time + timestamp = int(time.time()) + + # 1. Site admin creates two users + print("\n1. Creating two test users...") + user1_email = f"collab-user1-{timestamp}@test.local" + user2_email = f"collab-user2-{timestamp}@test.local" + + # Create a temporary organization for the users + org_name = f"collab-test-org-{timestamp}" + org_id = create_tracked_org(hub, resource_tracker, org_name) + print(f" Created organization: {org_name} (ID: {org_id})") + + user1_id = create_tracked_user(hub, resource_tracker, org_name, email=user1_email, firstName="CollabUser", lastName="One") + print(f" Created user1: {user1_id}") + + user2_id = create_tracked_user(hub, resource_tracker, org_name, email=user2_email, firstName="CollabUser", lastName="Two") + print(f" Created user2: {user2_id}") + + # 2. Create a project for the first user + print("\n2. Creating project for user1...") + project_title = f"collab-test-project-{timestamp}" + project_id = create_tracked_project(hub, resource_tracker, title=project_title) + print(f" Created project: {project_id}") + + # 3. Check initial collaborators + print("\n3. Checking initial collaborators...") + projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id) + assert len(projects) == 1 + initial_users = projects[0].get('users', {}) + print(f" Initial collaborators: {list(initial_users.keys())}") + print(f" Number of initial collaborators: {len(initial_users)}") + + # Report on ownership structure + for user_id, perms in initial_users.items(): + print(f" User {user_id}: {perms}") + + # 4. Add user1 as collaborator + print(f"\n4. Adding user1 ({user1_id}) as collaborator...") + result = hub.projects.add_collaborator(project_id=project_id, account_id=user1_id) + print(f" Add collaborator result: {result}") + + # Check collaborators after adding user1 + projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id) + users_after_user1 = projects[0].get('users', {}) + print(f" Collaborators after adding user1: {list(users_after_user1.keys())}") + print(f" Number of collaborators: {len(users_after_user1)}") + for user_id, perms in users_after_user1.items(): + print(f" User {user_id}: {perms}") + + # 5. Add user2 as collaborator + print(f"\n5. Adding user2 ({user2_id}) as collaborator...") + result = hub.projects.add_collaborator(project_id=project_id, account_id=user2_id) + print(f" Add collaborator result: {result}") + + # Check collaborators after adding user2 + projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id) + users_after_user2 = projects[0].get('users', {}) + print(f" Collaborators after adding user2: {list(users_after_user2.keys())}") + print(f" Number of collaborators: {len(users_after_user2)}") + # Note: There will be 3 users total: the site admin (owner) + user1 + user2 + for user_id, perms in users_after_user2.items(): + print(f" User {user_id}: {perms}") + + # Verify user1 and user2 are present + assert user1_id in users_after_user2, f"Expected user1 ({user1_id}) to be a collaborator" + assert user2_id in users_after_user2, f"Expected user2 ({user2_id}) to be a collaborator" + + # Identify the owner (should be the site admin who created the project) + owner_id = None + for uid, perms in users_after_user2.items(): + if perms.get('group') == 'owner': + owner_id = uid + break + print(f" Project owner: {owner_id}") + + # 6. Remove user1 + print(f"\n6. Removing user1 ({user1_id}) from project...") + result = hub.projects.remove_collaborator(project_id=project_id, account_id=user1_id) + print(f" Remove collaborator result: {result}") + + # Check collaborators after removing user1 + projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id) + users_after_removal = projects[0].get('users', {}) + print(f" Collaborators after removing user1: {list(users_after_removal.keys())}") + print(f" Number of collaborators: {len(users_after_removal)}") + # Should have 2 users: owner + user2 + assert len(users_after_removal) == 2, f"Expected 2 collaborators (owner + user2), found {len(users_after_removal)}" + assert user2_id in users_after_removal, f"Expected user2 ({user2_id}) to still be a collaborator" + assert user1_id not in users_after_removal, f"Expected user1 ({user1_id}) to be removed" + assert owner_id in users_after_removal, f"Expected owner ({owner_id}) to remain" + for user_id, perms in users_after_removal.items(): + print(f" User {user_id}: {perms}") + + print("\n✅ Collaborator management test completed successfully!") + + # Note: No cleanup needed - hard-delete happens automatically at session end diff --git a/src/python/cocalc-api/tests/test_jupyter.py b/src/python/cocalc-api/tests/test_jupyter.py new file mode 100644 index 0000000000..dbc39f5109 --- /dev/null +++ b/src/python/cocalc-api/tests/test_jupyter.py @@ -0,0 +1,280 @@ +""" +Tests for Jupyter kernel functionality. +""" + +import pytest +import time +from typing import Optional + + +class TestJupyterKernelSetup: + """Tests for Jupyter kernel installation and availability.""" + + def test_install_ipykernel(self, project_client): + """Test installing ipykernel in the project.""" + # Install ipykernel package + result = project_client.system.exec( + command="python3", + args=["-m", "pip", "install", "ipykernel"], + timeout=120, # 2 minutes should be enough for pip install + ) + + # Check that installation succeeded + assert result["exit_code"] == 0 + assert "stderr" in result + + def test_install_jupyter_kernel(self, project_client): + """Test installing the Python 3 Jupyter kernel.""" + # Install the kernel spec + result = project_client.system.exec( + command="python3", + args=[ + "-m", + "ipykernel", + "install", + "--user", # Install to user location, not system + "--name=python3", + "--display-name=Python 3", + ], + timeout=30, + ) + + # Check that kernel installation succeeded + assert result["exit_code"] == 0 + + +class TestJupyterKernels: + """Tests for Jupyter kernel availability.""" + + def test_kernels_list_with_project(self, hub, temporary_project): + """Test getting kernel specs for a specific project.""" + project_id = temporary_project["project_id"] + kernels = hub.jupyter.kernels(project_id=project_id) + + # Should return a list of kernel specs + assert isinstance(kernels, list) + assert len(kernels) > 0 + + def test_python3_kernel_available(self, hub, temporary_project): + """Test that the python3 kernel is available after installation.""" + project_id = temporary_project["project_id"] + kernels = hub.jupyter.kernels(project_id=project_id) + + # Extract kernel names from the list + kernel_names = [k.get("name") for k in kernels if isinstance(k, dict)] + assert "python3" in kernel_names + + +class TestJupyterExecuteViaHub: + """Tests for executing code via hub.jupyter.execute().""" + + @pytest.mark.skip(reason="hub.jupyter.execute() has timeout issues - use project.system.jupyter_execute() instead") + def test_execute_simple_sum(self, hub, temporary_project): + """Test executing a simple sum using the python3 kernel.""" + project_id = temporary_project["project_id"] + + result = hub.jupyter.execute(input="sum(range(100))", kernel="python3", project_id=project_id) + + # Check the result structure + assert isinstance(result, dict) + assert "output" in result + + # Check that we got the correct result (sum of 0..99 = 4950) + output = result["output"] + assert len(output) > 0 + + # Extract the result from the output + # Format: [{'data': {'text/plain': '4950'}}] + first_output = output[0] + assert "data" in first_output + assert "text/plain" in first_output["data"] + assert first_output["data"]["text/plain"] == "4950" + + @pytest.mark.skip(reason="hub.jupyter.execute() has timeout issues - use project.system.jupyter_execute() instead") + def test_execute_with_history(self, hub, temporary_project): + """Test executing code with history context.""" + project_id = temporary_project["project_id"] + + result = hub.jupyter.execute(history=["a = 100"], input="sum(range(a + 1))", kernel="python3", project_id=project_id) + + # Check the result (sum of 0..100 = 5050) + assert isinstance(result, dict) + assert "output" in result + + output = result["output"] + assert len(output) > 0 + + first_output = output[0] + assert "data" in first_output + assert "text/plain" in first_output["data"] + assert first_output["data"]["text/plain"] == "5050" + + @pytest.mark.skip(reason="hub.jupyter.execute() has timeout issues - use project.system.jupyter_execute() instead") + def test_execute_print_statement(self, hub, temporary_project): + """Test executing code that prints output.""" + project_id = temporary_project["project_id"] + + result = hub.jupyter.execute(input='print("Hello from Jupyter")', kernel="python3", project_id=project_id) + + # Check that we got output + assert isinstance(result, dict) + assert "output" in result + + output = result["output"] + assert len(output) > 0 + + # Print statements produce stream output + first_output = output[0] + assert "name" in first_output + assert first_output["name"] == "stdout" + assert "text" in first_output + assert "Hello from Jupyter" in first_output["text"] + + +class TestJupyterExecuteViaProject: + """Tests for executing code via project.system.jupyter_execute().""" + + def test_jupyter_execute_simple_sum(self, project_client): + """ + Test executing a simple sum via project API. + + The result is a list of output items directly (not wrapped in a dict). + + Note: First execution may take longer as kernel needs to start up (30+ seconds). + """ + # Retry logic for first kernel startup + max_retries = 3 + retry_delay = 15 + result: Optional[list] = None + + for attempt in range(max_retries): + try: + result = project_client.system.jupyter_execute(input="sum(range(100))", kernel="python3") + break + except RuntimeError as e: + if "timeout" in str(e).lower() and attempt < max_retries - 1: + print(f"Attempt {attempt + 1} timed out, retrying in {retry_delay}s...") + time.sleep(retry_delay) + else: + raise + + # Result is a list, not a dict with 'output' key + assert isinstance(result, list) + assert len(result) > 0 + + # Check that we got the correct result (sum of 0..99 = 4950) + first_output = result[0] + assert "data" in first_output + assert "text/plain" in first_output["data"] + assert first_output["data"]["text/plain"] == "4950" + + def test_jupyter_execute_with_history(self, project_client): + """ + Test executing code with history via project API. + + The result is a list of output items directly. + """ + result = project_client.system.jupyter_execute(history=["b = 50"], input="b * 2", kernel="python3") + + # Result is a list + assert isinstance(result, list) + assert len(result) > 0 + + # Check the result (50 * 2 = 100) + first_output = result[0] + assert "data" in first_output + assert "text/plain" in first_output["data"] + assert first_output["data"]["text/plain"] == "100" + + def test_jupyter_execute_list_operation(self, project_client): + """ + Test executing code that works with lists. + + The result is a list of output items directly. + """ + # Retry logic for kernel startup + max_retries = 3 + retry_delay = 15 + result: Optional[list] = None + + for attempt in range(max_retries): + try: + result = project_client.system.jupyter_execute(input="[x**2 for x in range(5)]", kernel="python3") + break + except RuntimeError as e: + if "timeout" in str(e).lower() and attempt < max_retries - 1: + print(f"Attempt {attempt + 1} timed out, retrying in {retry_delay}s...") + time.sleep(retry_delay) + else: + raise + + # Result is a list + assert isinstance(result, list) + assert len(result) > 0 + + # Check the result ([0, 1, 4, 9, 16]) + first_output = result[0] + assert "data" in first_output + assert "text/plain" in first_output["data"] + assert first_output["data"]["text/plain"] == "[0, 1, 4, 9, 16]" + + +class TestJupyterKernelManagement: + """Tests for Jupyter kernel management (list and stop kernels).""" + + def test_list_jupyter_kernels(self, project_client): + """Test listing running Jupyter kernels.""" + # First execute some code to ensure a kernel is running + # Retry logic for first kernel startup (may take longer in CI) + max_retries = 3 + retry_delay = 15 + + for attempt in range(max_retries): + try: + project_client.system.jupyter_execute(input="1+1", kernel="python3") + break + except RuntimeError as e: + if "timeout" in str(e).lower() and attempt < max_retries - 1: + print(f"Attempt {attempt + 1} timed out, retrying in {retry_delay}s...") + time.sleep(retry_delay) + else: + raise + + # List kernels + kernels = project_client.system.list_jupyter_kernels() + + # Should return a list + assert isinstance(kernels, list) + + # Should have at least one kernel running (from previous tests) + assert len(kernels) > 0 + + # Each kernel should have required fields + for kernel in kernels: + assert "pid" in kernel + assert "connectionFile" in kernel + assert isinstance(kernel["pid"], int) + assert isinstance(kernel["connectionFile"], str) + + def test_stop_jupyter_kernel(self, project_client): + """Test stopping a specific Jupyter kernel.""" + # Execute code to start a kernel + project_client.system.jupyter_execute(input="1+1", kernel="python3") + + # List kernels + kernels = project_client.system.list_jupyter_kernels() + assert len(kernels) > 0 + + # Stop the first kernel + kernel_to_stop = kernels[0] + result = project_client.system.stop_jupyter_kernel(pid=kernel_to_stop["pid"]) + + # Should return success + assert isinstance(result, dict) + assert "success" in result + assert result["success"] is True + + # Verify kernel is no longer in the list + kernels_after = project_client.system.list_jupyter_kernels() + remaining_pids = [k["pid"] for k in kernels_after] + assert kernel_to_stop["pid"] not in remaining_pids diff --git a/src/python/cocalc-api/tests/test_org.py b/src/python/cocalc-api/tests/test_org.py new file mode 100644 index 0000000000..28ee03ddab --- /dev/null +++ b/src/python/cocalc-api/tests/test_org.py @@ -0,0 +1,607 @@ +""" +Tests for Organization functionality. + +Note: These tests assume the provided API key belongs to a site admin user. +The tests exercise actual organization functionality rather than just checking permissions. +""" +import pytest +import time +import uuid + +from .conftest import assert_valid_uuid + + +class TestAdminPrivileges: + """Test that the API key has admin privileges.""" + + def test_admin_can_get_all_orgs(self, hub): + """Test that the user can call get_all() - verifies admin privileges.""" + try: + result = hub.org.get_all() + # If we get here without an exception, the user has admin privileges + assert isinstance(result, list), "get_all() should return a list" + print(f"✓ Admin verified - found {len(result)} organizations") + except Exception as e: + pytest.fail(f"Admin verification failed. API key may not have admin privileges: {e}") + + +class TestOrganizationBasics: + """Test basic organization module functionality.""" + + def test_org_module_import(self, hub): + """Test that the org module is properly accessible from hub.""" + assert hasattr(hub, 'org') + assert hub.org is not None + + def test_org_methods_available(self, hub): + """Test that all expected organization methods are available.""" + org = hub.org + + expected_methods = [ + 'get_all', + 'create', + 'get', + 'set', + 'add_admin', + 'add_user', + 'create_user', + 'create_token', + 'expire_token', + 'get_users', + 'remove_user', + 'remove_admin', + 'message', + ] + + for method_name in expected_methods: + assert hasattr(org, method_name), f"Method {method_name} not found" + assert callable(getattr(org, method_name)), f"Method {method_name} is not callable" + + +class TestOrganizationCRUD: + """Test organization Create, Read, Update, Delete operations.""" + + def test_get_all_organizations(self, hub): + """Test getting all organizations.""" + orgs = hub.org.get_all() + assert isinstance(orgs, list), "get_all() should return a list" + + # Each org should have expected fields + for org in orgs: + assert isinstance(org, dict), "Each org should be a dict" + assert 'name' in org, "Each org should have a 'name' field" + + def test_create_and_cleanup_organization(self, hub): + """Test creating an organization and basic operations.""" + # Create unique org name + timestamp = int(time.time()) + random_id = str(uuid.uuid4())[:8] + org_name = f"test-org-{timestamp}-{random_id}" + + print(f"Creating test organization: {org_name}") + + try: + # Create the organization + org_id = hub.org.create(org_name) + assert_valid_uuid(org_id, "Organization ID") + print(f"✓ Organization created with ID: {org_id}") + + # Get the organization details + org_details = hub.org.get(org_name) + assert isinstance(org_details, dict), "get() should return a dict" + assert org_details['name'] == org_name, "Organization name should match" + print(f"✓ Organization retrieved: {org_details}") + + # Update organization properties + hub.org.set(name=org_name, + title="Test Organization", + description="This is a test organization created by automated tests", + email_address="test@example.com", + link="https://example.com") + + # Verify the update + updated_org = hub.org.get(org_name) + assert updated_org['title'] == "Test Organization" + assert updated_org['description'] == "This is a test organization created by automated tests" + assert updated_org['email_address'] == "test@example.com" + assert updated_org['link'] == "https://example.com" + print("✓ Organization properties updated successfully") + + except Exception as e: + pytest.fail(f"Organization CRUD operations failed: {e}") + + +class TestOrganizationUserManagement: + """Test organization user management functionality.""" + + @pytest.fixture(scope="class") + def test_organization(self, hub): + """Create a test organization for user management tests.""" + timestamp = int(time.time()) + random_id = str(uuid.uuid4())[:8] + org_name = f"test-user-org-{timestamp}-{random_id}" + + print(f"Creating test organization for user tests: {org_name}") + + # Create the organization + org_id = hub.org.create(org_name) + + yield {'name': org_name, 'id': org_id} + + # Cleanup would go here, but since we can't delete orgs, + # we leave them for manual cleanup if needed + + def test_get_users_empty_org(self, hub, test_organization): + """Test getting users from a newly created organization.""" + users = hub.org.get_users(test_organization['name']) + assert isinstance(users, list), "get_users() should return a list" + assert len(users) == 0, f"Newly created organization should be empty, but has {len(users)} users" + print("✓ Newly created organization is empty as expected") + + def test_create_user_in_organization(self, hub, test_organization): + """Test creating a user within an organization.""" + # Create unique user details + timestamp = int(time.time()) + test_email = f"test-user-{timestamp}@example.com" + + try: + # Create user in the organization + new_user_id = hub.org.create_user(name=test_organization['name'], email=test_email, firstName="Test", lastName="User") + + assert_valid_uuid(new_user_id, "User ID") + print(f"✓ User created with ID: {new_user_id}") + + # Wait a moment for database consistency + import time as time_module + time_module.sleep(1) + + # Verify user appears in org users list + users = hub.org.get_users(test_organization['name']) + user_ids = [user['account_id'] for user in users] + + print(f"Debug - Organization name: '{test_organization['name']}'") + print(f"Debug - Created user ID: '{new_user_id}'") + print(f"Debug - Users in org: {len(users)}") + print(f"Debug - User IDs: {user_ids}") + + assert new_user_id in user_ids, f"New user {new_user_id} should appear in organization users list. Found users: {user_ids}" + + # Find the created user in the list + created_user = next((u for u in users if u['account_id'] == new_user_id), None) + assert created_user is not None, "Created user should be found in users list" + assert created_user['email_address'] == test_email, "Email should match" + assert created_user['first_name'] == "Test", "First name should match" + assert created_user['last_name'] == "User", "Last name should match" + + print(f"✓ User verified in organization: {created_user}") + + except Exception as e: + pytest.fail(f"User creation failed: {e}") + + def test_admin_management(self, hub, test_organization): + """Test adding and managing admins - simplified workflow.""" + timestamp = int(time.time()) + + try: + # Create user directly in the target organization + user_email = f"test-admin-{timestamp}@example.com" + user_id = hub.org.create_user(name=test_organization['name'], email=user_email, firstName="Test", lastName="Admin") + assert_valid_uuid(user_id, "User ID") + print(f"✓ Created user in organization: {user_id}") + + # Promote the user to admin + hub.org.add_admin(test_organization['name'], user_id) + print(f"✓ Promoted user to admin of {test_organization['name']}") + + # Verify admin status + org_details = hub.org.get(test_organization['name']) + admin_ids = org_details.get('admin_account_ids') or [] + assert user_id in admin_ids, "User should be in admin list" + print(f"✓ Admin status verified: {admin_ids}") + + # Test remove_admin + hub.org.remove_admin(test_organization['name'], user_id) + print(f"✓ Admin status removed for {user_id}") + + # Verify admin removal + updated_org = hub.org.get(test_organization['name']) + updated_admin_ids = updated_org.get('admin_account_ids') or [] + assert user_id not in updated_admin_ids, "User should no longer be admin" + print("✓ Admin removal verified") + + except Exception as e: + pytest.fail(f"Admin management failed: {e}") + + def test_admin_workflow_documentation(self, hub): + """Document the correct admin assignment workflows.""" + timestamp = int(time.time()) + + try: + # Create target organization + target_org = f"target-workflow-{timestamp}" + hub.org.create(target_org) + print(f"✓ Created organization: {target_org}") + + # Workflow 1: Create user in org, then promote to admin (simplest) + user_id = hub.org.create_user(name=target_org, email=f"workflow-simple-{timestamp}@example.com", firstName="Workflow", lastName="Simple") + assert_valid_uuid(user_id, "Workflow user ID") + print("✓ Created user in organization") + + # Promote to admin - works directly since user is in same org + hub.org.add_admin(target_org, user_id) + org_details = hub.org.get(target_org) + admin_ids = org_details.get('admin_account_ids') or [] + assert user_id in admin_ids + print("✓ Workflow 1 (Same org user → admin): SUCCESS") + + # Workflow 2: Move user from org A to org B, then promote to admin + other_org = f"other-workflow-{timestamp}" + hub.org.create(other_org) + other_user_id = hub.org.create_user(name=other_org, email=f"workflow-cross-{timestamp}@example.com", firstName="Cross", lastName="Org") + print(f"✓ Created user in {other_org}") + + # Step 1: Use addUser to move user from other_org to target_org (site admin only) + hub.org.add_user(target_org, other_user_id) + print(f"✓ Moved user from {other_org} to {target_org} using addUser") + + # Step 2: Now promote to admin in target_org + hub.org.add_admin(target_org, other_user_id) + updated_org = hub.org.get(target_org) + updated_admin_ids = updated_org.get('admin_account_ids') or [] + assert other_user_id in updated_admin_ids, "Moved user should be admin" + print("✓ Workflow 2 (Cross-org: addUser → addAdmin): SUCCESS") + print("✓ Admin workflow documentation complete") + + except Exception as e: + pytest.fail(f"Admin workflow documentation failed: {e}") + + def test_cross_org_admin_promotion_blocked(self, hub): + """Test that promoting a user from org A to admin of org B is blocked.""" + timestamp = int(time.time()) + + try: + # Create two organizations + org_a = f"org-a-{timestamp}" + org_b = f"org-b-{timestamp}" + hub.org.create(org_a) + hub.org.create(org_b) + print(f"✓ Created organizations: {org_a} and {org_b}") + + # Create user in org A + user_id = hub.org.create_user(name=org_a, email=f"cross-org-user-{timestamp}@example.com", firstName="CrossOrg", lastName="User") + assert_valid_uuid(user_id, "Cross-org user ID") + print(f"✓ Created user in {org_a}") + + # Try to promote user from org A to admin of org B - should fail + try: + hub.org.add_admin(org_b, user_id) + pytest.fail("Expected error when promoting user from different org to admin") + except Exception as e: + error_msg = str(e) + assert "already member of another organization" in error_msg, \ + f"Expected 'already member of another organization' error, got: {error_msg}" + print(f"✓ Cross-org promotion correctly blocked: {error_msg}") + + # Demonstrate correct workflow: use addUser to move, then addAdmin + # Note: addUser is site-admin only, so we can't test the full workflow + # without site admin privileges, but we document the pattern + print("✓ Correct workflow: Use addUser to move user between orgs first, then addAdmin") + print("✓ Cross-org admin promotion blocking test passed") + + except Exception as e: + pytest.fail(f"Cross-org admin promotion test failed: {e}") + + +class TestOrganizationTokens: + """Test organization token functionality.""" + + @pytest.fixture(scope="class") + def test_org_with_user(self, hub): + """Create a test organization with a user for token tests.""" + timestamp = int(time.time()) + random_id = str(uuid.uuid4())[:8] + org_name = f"test-token-org-{timestamp}-{random_id}" + + # Create the organization + org_id = hub.org.create(org_name) + + # Create a user in the organization + test_email = f"token-user-{timestamp}@example.com" + user_id = hub.org.create_user(name=org_name, email=test_email, firstName="Token", lastName="User") + assert_valid_uuid(user_id, "Token user ID") + + yield {'name': org_name, 'id': org_id, 'user_id': user_id, 'user_email': test_email} + + def test_create_and_expire_token(self, hub, test_org_with_user): + """Test creating and expiring access tokens.""" + try: + # Create token for the user + token_info = hub.org.create_token(test_org_with_user['user_id']) + + assert isinstance(token_info, dict), "create_token() should return a dict" + assert 'token' in token_info, "Token info should contain 'token' field" + assert 'url' in token_info, "Token info should contain 'url' field" + + token = token_info['token'] + url = token_info['url'] + + assert isinstance(token, str) and len(token) > 0, "Token should be a non-empty string" + assert isinstance(url, str) and url.startswith('http'), "URL should be a valid HTTP URL" + + print(f"✓ Token created: {token[:10]}... (truncated)") + print(f"✓ Access URL: {url}") + + # Expire the token + hub.org.expire_token(token) + print("✓ Token expired successfully") + + except Exception as e: + pytest.fail(f"Token management failed: {e}") + + +class TestOrganizationMessaging: + """Test organization messaging functionality.""" + + @pytest.fixture(scope="class") + def test_org_with_users(self, hub): + """Create a test organization with multiple users for messaging tests.""" + timestamp = int(time.time()) + random_id = str(uuid.uuid4())[:8] + org_name = f"test-msg-org-{timestamp}-{random_id}" + + # Create the organization + org_id = hub.org.create(org_name) + + # Create multiple users in the organization + users = [] + for i in range(2): + test_email = f"msg-user-{i}-{timestamp}@example.com" + user_id = hub.org.create_user(name=org_name, email=test_email, firstName=f"User{i}", lastName="Messaging") + assert_valid_uuid(user_id, f"Messaging user {i} ID") + + users.append({'id': user_id, 'email': test_email}) + + yield {'name': org_name, 'id': org_id, 'users': users} + + def test_send_message_to_organization(self, hub, test_org_with_users, cocalc_host): + """Test sending a message to all organization members and verify receipt.""" + from cocalc_api import Hub + + test_subject = "Test Message from API Tests" + test_body = "This is a test message sent via the CoCalc API organization messaging system." + user_token = None + + try: + # Step 1: Create a token for the first user to act as them + first_user = test_org_with_users['users'][0] + token_info = hub.org.create_token(first_user['id']) + + assert isinstance(token_info, dict), "create_token() should return a dict" + assert 'token' in token_info, "Token info should contain 'token' field" + + user_token = token_info['token'] + print(f"✓ Created token for user {first_user['id']}") + + # Step 2: Create Hub client using the user's token + user1 = Hub(api_key=user_token, host=cocalc_host) + print("✓ Created Hub client using user token") + + # Step 3: Get user's messages before sending org message (for comparison) + try: + messages_before = user1.messages.get(limit=5, type="received") + print(f"✓ User has {len(messages_before)} received messages before test") + except Exception as e: + print(f"⚠ Could not get user's messages before test: {e}") + messages_before = [] + + # Step 4: Send the organization message + result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=test_body) + + # Note: org.message() may return None, which is fine (indicates success) + print(f"✓ Organization message sent successfully (result: {result})") + + # Step 5: Wait a moment for message delivery + import time + time.sleep(2) + + # Step 6: Check if user received the message + try: + messages_after = user1.messages.get(limit=10, type="received") + print(f"✓ User has {len(messages_after)} received messages after test") + + # Look for our test message in user's received messages + found_message = False + for msg in messages_after: + if isinstance(msg, dict) and msg.get('subject') == test_subject: + found_message = True + print(f"✓ VERIFIED: User received message with subject: '{msg.get('subject')}'") + + # Verify message content + if 'body' in msg: + print(f"✓ Message body confirmed: {msg['body'][:50]}...") + break + + if found_message: + print("🎉 SUCCESS: Organization message was successfully delivered to user!") + else: + print("⚠ Message not found in user's received messages") + print(f" Expected subject: '{test_subject}'") + if messages_after: + print(f" Recent subjects: {[msg.get('subject', 'No subject') for msg in messages_after[:3]]}") + + except Exception as msg_check_error: + print(f"⚠ Could not verify message delivery: {msg_check_error}") + + except Exception as e: + pytest.fail(f"Message sending and verification failed: {e}") + + finally: + # Clean up: expire the token + if user_token: + try: + hub.org.expire_token(user_token) + print("✓ User token expired (cleanup)") + except Exception as cleanup_error: + print(f"⚠ Failed to expire token during cleanup: {cleanup_error}") + + def test_send_markdown_message(self, hub, test_org_with_users, cocalc_host): + """Test sending a message with markdown formatting and verify receipt.""" + from cocalc_api import Hub + + test_subject = "📝 Markdown Test Message" + markdown_body = """ +# Test Message with Markdown + +This is a **test message** with *markdown* formatting sent from the API tests. + +## Features Tested +- Organization messaging +- Markdown formatting +- API integration + +## Math Example +The formula $E = mc^2$ should render properly. + +## Code Example +```python +print("Hello from CoCalc API!") +``` + +[CoCalc API Documentation](https://cocalc.com/api/python/) + +--- +*This message was sent automatically by the organization API tests.* + """.strip() + + user_token = None + + try: + # Create a token for the second user (to vary which user we test) + if len(test_org_with_users['users']) > 1: + test_user = test_org_with_users['users'][1] + else: + test_user = test_org_with_users['users'][0] + + token_info = hub.org.create_token(test_user['id']) + user_token = token_info['token'] + user_hub = Hub(api_key=user_token, host=cocalc_host) + print(f"✓ Created token and Hub client for user {test_user['id']}") + + # Send the markdown message + result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=markdown_body) + + # Note: org.message() may return None, which is fine (indicates success) + print(f"✓ Markdown message sent successfully (result: {result})") + + # Wait for message delivery and verify + import time + time.sleep(2) + + try: + messages = user_hub.messages.get(limit=10, type="received") + + # Look for the markdown message + found_message = False + for msg in messages: + if isinstance(msg, dict) and msg.get('subject') == test_subject: + found_message = True + print("✓ VERIFIED: User received markdown message") + + # Verify it contains markdown content + body = msg.get('body', '') + if '**test message**' in body or 'Test Message with Markdown' in body: + print("✓ Markdown content confirmed in received message") + break + + if found_message: + print("🎉 SUCCESS: Markdown message was successfully delivered!") + else: + print("⚠ Markdown message not found in user's received messages") + + except Exception as msg_check_error: + print(f"⚠ Could not verify markdown message delivery: {msg_check_error}") + + except Exception as e: + pytest.fail(f"Markdown message sending and verification failed: {e}") + + finally: + # Clean up: expire the token + if user_token: + try: + hub.org.expire_token(user_token) + print("✓ Markdown test token expired (cleanup)") + except Exception as cleanup_error: + print(f"⚠ Failed to expire markdown test token: {cleanup_error}") + + +class TestOrganizationIntegration: + """Integration tests for organization functionality.""" + + def test_full_organization_lifecycle(self, hub): + """Test a complete organization lifecycle with users and messaging.""" + timestamp = int(time.time()) + random_id = str(uuid.uuid4())[:8] + org_name = f"test-lifecycle-{timestamp}-{random_id}" + + try: + print(f"Testing full lifecycle for organization: {org_name}") + + # 1. Create organization + org_id = hub.org.create(org_name) + print(f"✓ 1. Organization created: {org_id}") + + # 2. Set organization properties + hub.org.set(name=org_name, title="Lifecycle Test Organization", description="Testing complete organization lifecycle") + print("✓ 2. Organization properties set") + + # 3. Create users + users = [] + for i in range(2): + user_email = f"lifecycle-user-{i}-{timestamp}@example.com" + user_id = hub.org.create_user(name=org_name, email=user_email, firstName=f"User{i}", lastName="Lifecycle") + assert_valid_uuid(user_id, f"Lifecycle user {i} ID") + + users.append({'id': user_id, 'email': user_email}) + print(f"✓ 3. Created {len(users)} users") + + # 4. Promote a user to admin (simplified workflow) + # Create user and promote directly to admin + admin_email = f"lifecycle-admin-{timestamp}@example.com" + admin_id = hub.org.create_user(name=org_name, email=admin_email, firstName="Admin", lastName="User") + assert_valid_uuid(admin_id, "Admin user ID") + hub.org.add_admin(org_name, admin_id) + print("✓ 4. Created and promoted user to admin") + + # 5. Create and expire a token + token_info = hub.org.create_token(users[1]['id']) + hub.org.expire_token(token_info['token']) + print("✓ 5. Token created and expired") + + # 6. Send message to organization + hub.org.message(name=org_name, subject="Lifecycle Test Complete", body="All organization lifecycle tests completed successfully!") + print("✓ 6. Message sent") + + # 7. Verify final state + final_org = hub.org.get(org_name) + final_users = hub.org.get_users(org_name) + + assert final_org['title'] == "Lifecycle Test Organization" + assert len(final_users) >= len(users) + 1, "All users plus admin should be in organization" + + # Check admin status + admin_ids = final_org.get('admin_account_ids') or [] + assert admin_id in admin_ids, "Admin should be in admin list" + print(f"✓ Admin assignment successful: {admin_ids}") + + print(f"✓ 7. Final verification complete - org has {len(final_users)} users") + print(f"✓ Full lifecycle test completed successfully for {org_name}") + + except Exception as e: + pytest.fail(f"Full lifecycle test failed: {e}") + + +def test_delete_method_still_available(hub): + """Verify that projects.delete is still available after org refactoring.""" + assert hasattr(hub.projects, 'delete') + assert callable(hub.projects.delete) + print("✓ Projects delete method still available after org refactoring") diff --git a/src/python/cocalc-api/tests/test_org_basic.py b/src/python/cocalc-api/tests/test_org_basic.py new file mode 100644 index 0000000000..f2ad5416cc --- /dev/null +++ b/src/python/cocalc-api/tests/test_org_basic.py @@ -0,0 +1,131 @@ +""" +Basic Organization functionality tests. + +This file contains tests that verify the organization API is properly exposed +and accessible, without necessarily requiring full admin privileges or server connectivity. +""" +import pytest + + +class TestOrganizationAPIExposure: + """Test that organization API methods are properly exposed.""" + + def test_org_module_available(self, hub): + """Test that the org module is accessible from hub.""" + assert hasattr(hub, 'org') + assert hub.org is not None + + def test_all_org_methods_available(self, hub): + """Test that all expected organization methods are available and callable.""" + org = hub.org + + expected_methods = [ + 'get_all', 'create', 'get', 'set', 'add_admin', 'add_user', 'create_user', 'create_token', 'expire_token', 'get_users', 'remove_user', + 'remove_admin', 'message' + ] + + for method_name in expected_methods: + assert hasattr(org, method_name), f"Method {method_name} not found" + method = getattr(org, method_name) + assert callable(method), f"Method {method_name} is not callable" + + print(f"✓ All {len(expected_methods)} organization methods are properly exposed") + + def test_org_methods_are_api_decorated(self, hub): + """Test that org methods make actual API calls (not just stubs).""" + # We can verify this by attempting to call a method that should fail + # with authentication/permission errors rather than NotImplementedError + + with pytest.raises(Exception) as exc_info: + # This should make an actual API call and fail with auth or server error, + # not with NotImplementedError + hub.org.get("nonexistent-org-for-testing-12345") + + # Should NOT be NotImplementedError (which would indicate the method isn't implemented) + assert not isinstance(exc_info.value, NotImplementedError), \ + "Organization methods should make actual API calls, not raise NotImplementedError" + + print(f"✓ Organization methods make actual API calls: {type(exc_info.value).__name__}") + + def test_message_method_signature(self, hub): + """Test that the message method has the correct signature.""" + import inspect + + sig = inspect.signature(hub.org.message) + params = list(sig.parameters.keys()) + + # Should have name, subject, body parameters + required_params = ['name', 'subject', 'body'] + for param in required_params: + assert param in params, f"Message method missing required parameter: {param}" + + print("✓ Message method has correct parameters:", params) + + def test_create_user_method_signature(self, hub): + """Test that create_user method has the correct signature.""" + import inspect + + sig = inspect.signature(hub.org.create_user) + params = sig.parameters + + # Check required parameters + assert 'name' in params, "create_user missing 'name' parameter" + assert 'email' in params, "create_user missing 'email' parameter" + + # Check optional parameters + optional_params = ['firstName', 'lastName', 'password'] + for param in optional_params: + assert param in params, f"create_user missing optional parameter: {param}" + # Optional params should have default values + assert params[param].default is not inspect.Parameter.empty, \ + f"Optional parameter {param} should have a default value" + + print("✓ create_user method has correct parameter signature") + + def test_create_token_return_annotation(self, hub): + """Test that create_token has proper return type annotation.""" + import inspect + + sig = inspect.signature(hub.org.create_token) + return_annotation = sig.return_annotation + + # Should be annotated to return TokenType + assert return_annotation.__name__ == 'TokenType', \ + f"create_token should return TokenType, got {return_annotation}" + + print("✓ create_token method has correct return type annotation") + + +class TestOrganizationImportIntegrity: + """Test that the organization refactoring didn't break anything.""" + + def test_organizations_class_imported_correctly(self, hub): + """Test that Organizations class is properly imported in hub.""" + # The hub.org should be an instance of the Organizations class + from cocalc_api.org import Organizations + + assert isinstance(hub.org, Organizations), \ + "hub.org should be an instance of Organizations class" + + print("✓ Organizations class properly imported and instantiated") + + def test_original_hub_functionality_preserved(self, hub): + """Test that refactoring didn't break other hub functionality.""" + # Test that other hub properties still work + assert hasattr(hub, 'system'), "Hub should still have system property" + assert hasattr(hub, 'projects'), "Hub should still have projects property" + assert hasattr(hub, 'messages'), "Hub should still have messages property" + + # Test that projects.delete is still available (from main task) + assert hasattr(hub.projects, 'delete'), "Projects should still have delete method" + assert callable(hub.projects.delete), "Projects delete should be callable" + + print("✓ All original Hub functionality preserved after org refactoring") + + +def test_make_check_compatibility(): + """Test that the refactoring passes all static analysis checks.""" + # This test exists to document that the refactored code should pass + # make check (ruff, mypy, pyright) - the actual checking is done by CI/make + print("✓ Organization refactoring should pass make check (ruff, mypy, pyright)") + assert True diff --git a/src/python/cocalc-api/tests/test_project.py b/src/python/cocalc-api/tests/test_project.py new file mode 100644 index 0000000000..fefe182dbc --- /dev/null +++ b/src/python/cocalc-api/tests/test_project.py @@ -0,0 +1,117 @@ +""" +Tests for Project client functionality. +""" +import pytest + +from cocalc_api import Project +from .conftest import assert_valid_uuid + + +class TestProjectCreation: + """Tests for project creation and management.""" + + def test_create_temporary_project(self, temporary_project): + """Test that a temporary project is created successfully.""" + assert temporary_project is not None + assert 'project_id' in temporary_project + assert 'title' in temporary_project + assert 'description' in temporary_project + assert temporary_project['title'].startswith('CoCalc API Test ') + assert temporary_project['description'] == "Temporary project created by cocalc-api tests" + # Project ID should be a valid UUID + assert_valid_uuid(temporary_project['project_id'], "Project ID") + + def test_project_exists_in_list(self, hub, temporary_project): + """Test that the created project appears in the projects list.""" + projects = hub.projects.get(all=True) + project_ids = [p['project_id'] for p in projects] + assert temporary_project['project_id'] in project_ids + + +class TestProjectSystem: + """Tests for Project system operations.""" + + def test_ping(self, project_client): + """Test basic ping connectivity to project.""" + result = project_client.system.ping() + assert result is not None + assert isinstance(result, dict) + + def test_project_initialization(self, api_key, cocalc_host): + """Test Project client initialization.""" + project_id = "test-project-id" + project = Project(project_id=project_id, api_key=api_key, host=cocalc_host) + assert project.project_id == project_id + assert project.api_key == api_key + assert project.host == cocalc_host + assert project.client is not None + + def test_project_with_temporary_project(self, project_client, temporary_project): + """Test Project client using the temporary project.""" + assert project_client.project_id == temporary_project['project_id'] + # Test that we can ping the specific project + result = project_client.system.ping() + assert result is not None + assert isinstance(result, dict) + + def test_exec_command(self, project_client): + """Test executing shell commands in the project.""" + # Test running 'date -Is' to get ISO date with seconds + result = project_client.system.exec(command="date", args=["-Is"]) + + # Check the result structure + assert 'stdout' in result + assert 'stderr' in result + assert 'exit_code' in result + + # Should succeed + assert result['exit_code'] == 0 + + # Should have minimal stderr + assert result['stderr'] == '' or len(result['stderr']) == 0 + + # Parse the returned date and compare with current time + from datetime import datetime + import re + + date_output = result['stdout'].strip() + # Expected format: 2025-09-29T12:34:56+00:00 or similar + + # Check if the output matches ISO format + iso_pattern = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$' + assert re.match(iso_pattern, date_output), f"Date output '{date_output}' doesn't match ISO format" + + # Parse the date from the command output + # Remove the timezone for comparison (date -Is includes timezone) + date_part = date_output[:19] # Take YYYY-MM-DDTHH:MM:SS part + remote_time = datetime.fromisoformat(date_part) + + # Get current time + current_time = datetime.now() + + # Check if the times are close (within 60 seconds) + time_diff = abs((current_time - remote_time).total_seconds()) + assert time_diff < 60, f"Time difference too large: {time_diff} seconds. Remote: {date_output}, Local: {current_time.isoformat()}" + + def test_exec_stderr_and_exit_code(self, project_client): + """Test executing a command that writes to stderr and returns a specific exit code.""" + # Use bash to echo to stderr and exit with code 42 + bash_script = "echo 'test error message' >&2; exit 42" + + # The API raises an exception for non-zero exit codes + # but includes the stderr and exit code information in the error message + with pytest.raises(RuntimeError) as exc_info: + project_client.system.exec(command=bash_script, bash=True) + + error_message = str(exc_info.value) + + # Verify the error message contains expected information + assert "exited with nonzero code 42" in error_message + assert "stderr='test error message" in error_message + + # Extract and verify the stderr content is properly captured + import re + stderr_match = re.search(r"stderr='([^']*)'", error_message) + assert stderr_match is not None, "Could not find stderr in error message" + stderr_content = stderr_match.group(1).strip() + assert stderr_content == "test error message" diff --git a/src/python/cocalc-api/uv.lock b/src/python/cocalc-api/uv.lock index c949ebfde1..3643b57fd3 100644 --- a/src/python/cocalc-api/uv.lock +++ b/src/python/cocalc-api/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -17,9 +17,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] [[package]] @@ -155,7 +155,7 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -164,14 +164,14 @@ resolution-markers = [ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] name = "cocalc-api" -version = "0.4.0" +version = "0.5.0" source = { virtual = "." } dependencies = [ { name = "httpx" }, @@ -179,15 +179,21 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "coverage", extra = ["toml"] }, { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "psycopg2-binary" }, + { name = "pyright" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, + { name = "types-psycopg2" }, + { name = "yapf" }, ] [package.metadata] @@ -195,13 +201,19 @@ requires-dist = [{ name = "httpx" }] [package.metadata.requires-dev] dev = [ + { name = "coverage", extras = ["toml"] }, { name = "ipython" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extras = ["python"] }, { name = "mypy" }, - { name = "pytest", specifier = ">=8.4.1" }, - { name = "ruff", specifier = ">=0.12.11" }, + { name = "psycopg2-binary" }, + { name = "pyright" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov" }, + { name = "ruff", specifier = ">=0.13.2" }, + { name = "types-psycopg2" }, + { name = "yapf" }, ] [[package]] @@ -213,6 +225,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -236,11 +364,11 @@ wheels = [ [[package]] name = "executing" -version = "2.2.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] @@ -257,14 +385,14 @@ wheels = [ [[package]] name = "griffe" -version = "1.13.0" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/b5/23b91f22b7b3a7f8f62223f6664946271c0f5cb4179605a3e6bbae863920/griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0", size = 412759, upload-time = "2025-08-26T13:27:11.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/8c/b7cfdd8dfe48f6b09f7353323732e1a290c388bd14f216947928dc85f904/griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559", size = 139365, upload-time = "2025-08-26T13:27:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, ] [[package]] @@ -386,7 +514,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.5.0" +version = "9.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -404,9 +532,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/34/29b18c62e39ee2f7a6a3bba7efd952729d8aadd45ca17efc34453b717665/ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731", size = 4396932, upload-time = "2025-09-29T10:55:53.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, + { url = "https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196", size = 616170, upload-time = "2025-09-29T10:55:47.676Z" }, ] [[package]] @@ -447,82 +575,110 @@ wheels = [ [[package]] name = "markdown" -version = "3.8.2" +version = "3.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, ] [[package]] @@ -552,7 +708,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -603,13 +759,11 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.18" +version = "9.6.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -620,9 +774,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/46/db0d78add5aac29dfcd0a593bcc6049c86c77ba8a25b3a5b681c190d5e99/mkdocs_material-9.6.18.tar.gz", hash = "sha256:a2eb253bcc8b66f8c6eaf8379c10ed6e9644090c2e2e9d0971c7722dc7211c05", size = 4034856, upload-time = "2025-08-22T08:21:47.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/d5/ab83ca9aa314954b0a9e8849780bdd01866a3cfcb15ffb7e3a61ca06ff0b/mkdocs_material-9.6.21.tar.gz", hash = "sha256:b01aa6d2731322438056f360f0e623d3faae981f8f2d8c68b1b973f4f2657870", size = 4043097, upload-time = "2025-09-30T19:11:27.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/0b/545a4f8d4f9057e77f1d99640eb09aaae40c4f9034707f25636caf716ff9/mkdocs_material-9.6.18-py3-none-any.whl", hash = "sha256:dbc1e146a0ecce951a4d84f97b816a54936cdc9e1edd1667fc6868878ac06701", size = 9232642, upload-time = "2025-08-22T08:21:44.52Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/98681c2030375fe9b057dbfb9008b68f46c07dddf583f4df09bf8075e37f/mkdocs_material-9.6.21-py3-none-any.whl", hash = "sha256:aa6a5ab6fb4f6d381588ac51da8782a4d3757cb3d1b174f81a2ec126e1f22c92", size = 9203097, upload-time = "2025-09-30T19:11:24.063Z" }, ] [[package]] @@ -636,7 +790,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.30.0" +version = "0.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, @@ -647,9 +801,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, ] [package.optional-dependencies] @@ -674,7 +828,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, @@ -682,45 +836,45 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, - { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, - { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, - { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] @@ -732,6 +886,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -810,6 +973,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397, upload-time = "2024-10-16T11:18:58.647Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806, upload-time = "2024-10-16T11:19:03.935Z" }, + { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361, upload-time = "2024-10-16T11:19:07.277Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836, upload-time = "2024-10-16T11:19:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552, upload-time = "2024-10-16T11:19:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789, upload-time = "2024-10-16T11:19:18.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776, upload-time = "2024-10-16T11:19:23.023Z" }, + { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959, upload-time = "2024-10-16T11:19:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329, upload-time = "2024-10-16T11:19:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659, upload-time = "2024-10-16T11:19:32.864Z" }, + { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605, upload-time = "2024-10-16T11:19:35.462Z" }, + { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817, upload-time = "2024-10-16T11:19:37.384Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397, upload-time = "2024-10-16T11:19:40.033Z" }, + { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806, upload-time = "2024-10-16T11:19:43.5Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370, upload-time = "2024-10-16T11:19:46.986Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780, upload-time = "2024-10-16T11:19:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583, upload-time = "2024-10-16T11:19:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831, upload-time = "2024-10-16T11:19:57.762Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822, upload-time = "2024-10-16T11:20:04.693Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975, upload-time = "2024-10-16T11:20:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320, upload-time = "2024-10-16T11:20:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617, upload-time = "2024-10-16T11:20:24.711Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618, upload-time = "2024-10-16T11:20:27.718Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816, upload-time = "2024-10-16T11:20:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437, upload-time = "2024-10-16T11:23:42.946Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340, upload-time = "2024-10-16T11:23:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905, upload-time = "2024-10-16T11:23:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640, upload-time = "2024-10-16T11:24:06.122Z" }, + { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812, upload-time = "2024-10-16T11:24:17.025Z" }, + { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933, upload-time = "2024-10-16T11:24:24.858Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990, upload-time = "2024-10-16T11:24:29.571Z" }, + { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352, upload-time = "2024-10-16T11:24:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614, upload-time = "2024-10-16T11:24:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341, upload-time = "2024-10-16T11:24:48.056Z" }, + { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958, upload-time = "2024-10-16T11:24:51.882Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -850,9 +1079,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] +[[package]] +name = "pyright" +version = "1.1.405" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, +] + [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -863,9 +1105,23 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -882,55 +1138,75 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] @@ -962,28 +1238,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, - { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, - { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, - { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, ] [[package]] @@ -1066,6 +1342,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "types-psycopg2" +version = "2.9.21.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/20/3dcb89df8d1661cf6c4c2d9f84d4ba94dde48559cdcf7b536a380a9c387f/types_psycopg2-2.9.21.20250915.tar.gz", hash = "sha256:bfeb8f54c32490e7b5edc46215ab4163693192bc90407b4a023822de9239f5c8", size = 26678, upload-time = "2025-09-15T03:01:08.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/4d/ebf1c72809a30150ad142074e1ad5101304f7569c0df2fa872906d76d0af/types_psycopg2-2.9.21.20250915-py3-none-any.whl", hash = "sha256:eefe5ccdc693fc086146e84c9ba437bb278efe1ef330b299a0cb71169dc6c55f", size = 24868, upload-time = "2025-09-15T03:01:07.613Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1123,11 +1408,24 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "yapf" +version = "0.43.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +dependencies = [ + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" }, ] [[package]]