rewrite as svelte static site

- replace python/textual TUI with sveltekit
- add wisp.place deployment workflow
- cleaner component structure: models.js, stores.js, PdsPanel.svelte

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+23 -3
.gitignore
··· 1 - .venv/ 2 - __pycache__/ 3 - *.pyc 1 + node_modules 2 + 3 + # Output 4 + .output 5 + .vercel 6 + .netlify 7 + .wrangler 8 + /.svelte-kit 9 + /build 10 + 11 + # OS 12 + .DS_Store 13 + Thumbs.db 14 + 15 + # Env 16 + .env 17 + .env.* 18 + !.env.example 19 + !.env.test 20 + 21 + # Vite 22 + vite.config.js.timestamp-* 23 + vite.config.ts.timestamp-*
+27
.tangled/workflows/deploy.yml
··· 1 + when: 2 + - event: ["push"] 3 + branch: main 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - bun 10 + - curl 11 + 12 + environment: 13 + WISP_DID: "did:plc:xbtmt2zjwlrfegqvch7fboei" 14 + WISP_SITE_NAME: "pds-message-poc" 15 + 16 + steps: 17 + - name: install and build 18 + command: | 19 + bun install 20 + bun run build 21 + 22 + - name: deploy to wisp 23 + command: | 24 + test -n "$WISP_APP_PASSWORD" 25 + curl -sSL https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 26 + chmod +x wisp-cli 27 + ./wisp-cli deploy "$WISP_DID" --path ./build --site "$WISP_SITE_NAME" --password "$WISP_APP_PASSWORD"
+39 -44
README.md
··· 1 1 # pds-message-poc 2 2 3 - interactive PoC of PDS-to-PDS message passing using [docket](https://github.com/chrisguidry/docket). 3 + interactive browser demo of PDS-to-PDS message passing. 4 4 5 5 demonstrates jacob.gold's proposal: PDSes have incoming message queues for DMs, like email servers. 6 6 7 7 ## run 8 8 9 9 ```bash 10 - uvx --from git+ssh://git@tangled.org/zzstoatzz.io/pds-message-poc pds-message-poc 10 + bun install 11 + bun dev 11 12 ``` 12 13 13 - or locally: 14 + ## usage 14 15 15 - ```bash 16 - just demo 17 - ``` 18 - 19 - - type a message 20 - - select sender/recipient 21 - - click Send → watch service auth token created, message queued, recipient decides 22 - - click Block → alice blocks selected sender 23 - - click Spam Label → labeler marks selected sender as spam (rejected by all) 24 - 25 - press `q` to quit. 16 + - type a message, select sender → recipient 17 + - **send** - initiates message (first message creates a request) 18 + - **accept** - recipient accepts pending request, messages flow freely 19 + - **reject** - recipient rejects request and blocks sender 20 + - **spam** - labeler marks sender as spam (rejected by all PDSes) 26 21 27 22 ## what's happening 28 23 ··· 33 28 │ │ 1. getServiceAuth(aud=alice)│ │ 34 29 │ send_message() │ ────────────────────────────>│ │ 35 30 │ │ 2. JWT: iss=bob aud=alice │ inbox queue │ 36 - │ │ <────────────────────────────│ (docket) │ 31 + │ │ <────────────────────────────│ │ 37 32 │ │ │ │ 38 33 │ │ 3. POST /inbox + JWT │ │ 39 - │ │ ────────────────────────────>│ worker checks: │ 34 + │ │ ────────────────────────────>│ evaluate(): │ 40 35 │ │ │ - token valid? │ 41 36 │ │ │ - spam label? │ 42 37 │ │ │ - blocked? │ 38 + │ │ │ - accepted? │ 43 39 │ │ │ - rate limit? │ 44 - │ │ 4. {status: accepted} │ │ 45 - │ │ <────────────────────────────│ → accept/reject│ 40 + │ │ 4. {status: ...} │ │ 41 + │ │ <────────────────────────────│ → deliver/queue│ 46 42 └─────────────────┘ └─────────────────┘ 47 - 48 - 49 - ┌───────────────┐ 50 - │ Labeler │ 51 - │ (reputation) │ 52 - │ spam labels │ 53 - └───────────────┘ 43 + 44 + 45 + ┌───────────────┐ 46 + │ Labeler │ 47 + │ (reputation) │ 48 + │ spam labels │ 49 + └───────────────┘ 54 50 ``` 55 51 52 + ## invitation flow 53 + 54 + first contact requires acceptance (like DM requests): 55 + 56 + 1. bob sends message to alice → creates **request** (message held) 57 + 2. alice sees request in her "requests" section 58 + 3. alice clicks **accept** → original message delivered, bob now accepted 59 + 4. subsequent messages from bob deliver immediately (subject to rate limits) 60 + 61 + alternatively: 62 + - alice clicks **reject** → request deleted, bob blocked permanently 63 + 56 64 ## what's demonstrated 57 65 58 66 | feature | implementation | ATProto pattern | 59 67 |---------|----------------|-----------------| 60 - | inbox queue | docket (redis streams) | proposed `dev.pds.inbox.sendMessage` endpoint | 61 68 | service auth | JWT with iss/aud/exp/lxm | [com.atproto.server.getServiceAuth](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/getServiceAuth.json) | 69 + | invitation flow | pending/accepted sets | similar to `chat.bsky.convo` request status | 62 70 | reputation | labeler with spam labels | [com.atproto.label](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/label) | 63 71 | block list | per-user set | existing pattern | 64 72 | rate limiting | per-sender, time-windowed | existing pattern | ··· 68 76 | component | current | path to real | 69 77 |-----------|---------|--------------| 70 78 | DIDs | fake strings | [PLC resolution](https://github.com/did-method-plc/did-method-plc) | 71 - | queue backend | docket `memory://` | docket `redis://` | 72 - | JWT signing | sha256 hash | [DID signing keys](https://github.com/bluesky-social/atproto/tree/main/packages/crypto) | 73 - | labeler | in-memory dict | [ozone](https://github.com/bluesky-social/atproto/tree/main/packages/ozone) | 79 + | JWT signing | base64 stub | [DID signing keys](https://github.com/bluesky-social/atproto/tree/main/packages/crypto) | 80 + | labeler | in-memory map | [ozone](https://github.com/bluesky-social/atproto/tree/main/packages/ozone) | 81 + | network | in-memory objects | actual HTTP between PDSes | 74 82 75 83 ## prior art 76 84 77 - these informed our approach: 78 - 79 - - [private data: developing a rubric for success](https://pfrazee.leaflet.pub/3lzhmtognls2q) - pfrazee on requirements for private/shared data, mentions "inbox spam due to push-messaging" 85 + - [private data: developing a rubric for success](https://pfrazee.leaflet.pub/3lzhmtognls2q) - pfrazee on requirements for private/shared data 80 86 - [AT Protocol and SMTP](https://ngerakines.leaflet.pub/3lxxk3oahzc2f) - ngerakines on PDS as crypto service, SMTP as transport 81 87 - [the community manager pattern](https://ngerakines.leaflet.pub/3majmrpjrd22b) - service auth for inter-service communication 82 88 - [bourbon protocol](https://blog.boscolo.co/3lzj5po423s2g) - invitation-based messaging, combating spam 83 89 - [why inter-service auth needs client identity](https://ngerakines.leaflet.pub/3m6xaxk64tk2h) - `client_id` in JWTs for blocking bad actors 84 - 85 - <details> 86 - <summary>how we found these</summary> 87 - 88 - searched [leaflet](https://leaflet.pub) via MCP: 89 - 90 - ```bash 91 - claude mcp add-json leaflet-search '{"type": "http", "url": "https://leaflet-search-by-zzstoatzz.fastmcp.app/mcp"}' 92 - ``` 93 - 94 - </details> 90 + - [priv (private follows)](https://github.com/TechnoJo4/priv) - using labeler reports as private signaling channel 95 91 96 92 ## references 97 93 98 94 - [jacob.gold's thread](https://bsky.app/profile/jacob.gold/post/3mbsbqsc3vc24) 99 - - [docket](https://github.com/chrisguidry/docket) 100 95 - [official PDS](https://github.com/bluesky-social/atproto/tree/main/packages/pds) 101 96 - [service auth](https://github.com/bluesky-social/atproto/blob/main/packages/xrpc-server/src/auth.ts) 102 97 - [AT Protocol specs](https://atproto.com/specs/atp)
bun.lockb

This is a binary file and will not be displayed.

-5
justfile
··· 1 - # pds-message-poc 2 - 3 - # run interactive demo 4 - demo: 5 - uv run python -m pds_message_poc.app
+20
package.json
··· 1 + { 2 + "name": "pds-message-poc", 3 + "private": true, 4 + "version": "0.0.1", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite dev", 8 + "build": "vite build", 9 + "preview": "vite preview", 10 + "prepare": "svelte-kit sync || echo ''" 11 + }, 12 + "devDependencies": { 13 + "@sveltejs/adapter-auto": "^7.0.0", 14 + "@sveltejs/adapter-static": "^3.0.10", 15 + "@sveltejs/kit": "^2.49.1", 16 + "@sveltejs/vite-plugin-svelte": "^6.2.1", 17 + "svelte": "^5.45.6", 18 + "vite": "^7.2.6" 19 + } 20 + }
-20
pyproject.toml
··· 1 - [project] 2 - name = "pds-message-poc" 3 - version = "0.1.0" 4 - description = "PoC: PDS-to-PDS message passing using docket" 5 - readme = "README.md" 6 - requires-python = ">=3.12" 7 - dependencies = [ 8 - "pydocket>=0.13", 9 - "textual>=0.50", 10 - ] 11 - 12 - [project.scripts] 13 - pds-message-poc = "pds_message_poc.app:main" 14 - 15 - [build-system] 16 - requires = ["hatchling"] 17 - build-backend = "hatchling.build" 18 - 19 - [tool.hatch.build.targets.wheel] 20 - packages = ["src/pds_message_poc"]
+49
src/app.css
··· 1 + * { 2 + box-sizing: border-box; 3 + margin: 0; 4 + padding: 0; 5 + scrollbar-width: thin; 6 + scrollbar-color: #333 #111; 7 + } 8 + 9 + body { 10 + font-family: monospace; 11 + background: #0a0a0a; 12 + color: #ccc; 13 + min-height: 100vh; 14 + padding: 1rem; 15 + font-size: 14px; 16 + line-height: 1.6; 17 + } 18 + 19 + a { 20 + color: #1b7340; 21 + text-decoration: none; 22 + } 23 + a:hover { 24 + color: #2a9d5c; 25 + } 26 + 27 + /* dark scrollbars */ 28 + ::-webkit-scrollbar { 29 + width: 8px; 30 + height: 8px; 31 + } 32 + ::-webkit-scrollbar-track { 33 + background: #111; 34 + } 35 + ::-webkit-scrollbar-thumb { 36 + background: #333; 37 + border-radius: 4px; 38 + } 39 + ::-webkit-scrollbar-thumb:hover { 40 + background: #444; 41 + } 42 + 43 + /* log colors */ 44 + .dim { color: #444; } 45 + .cyan { color: #2a9d5c; } 46 + .green { color: #4ade80; } 47 + .yellow { color: #a84; } 48 + .red { color: #c44; } 49 + .magenta { color: #a6a; }
+11
src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + %sveltekit.head% 7 + </head> 8 + <body data-sveltekit-preload-data="hover"> 9 + <div style="display: contents">%sveltekit.body%</div> 10 + </body> 11 + </html>
+143
src/lib/components/PdsPanel.svelte
··· 1 + <script> 2 + import { getPdsByDid } from '$lib/stores.js'; 3 + import Tooltip from './Tooltip.svelte'; 4 + 5 + let { pds, role } = $props(); 6 + 7 + function getHandle(did) { 8 + const p = getPdsByDid(did); 9 + return p ? p.handle : did.slice(0, 12); 10 + } 11 + 12 + function truncate(text, len = 30) { 13 + return text.length > len 14 + ? text.slice(0, len) + '...' 15 + : text; 16 + } 17 + </script> 18 + 19 + <div class="panel"> 20 + <div class="role-label {role}">{role}'s pds</div> 21 + <h2>{pds.handle}</h2> 22 + <div class="did">{pds.did}</div> 23 + <Tooltip text="max messages per minute from any accepted sender"> 24 + <div class="subtitle">rate: {pds.rateLimit}/min</div> 25 + </Tooltip> 26 + 27 + <div class="requests"> 28 + <Tooltip text="first contact requires acceptance (like DM requests)"> 29 + <h3>requests</h3> 30 + </Tooltip> 31 + {#if pds.pending.size > 0} 32 + {#each [...pds.pending] as [did, req]} 33 + <div class="request-item"> 34 + {getHandle(did)}: {truncate(req.text)} 35 + </div> 36 + {/each} 37 + {:else} 38 + <div class="empty">none</div> 39 + {/if} 40 + </div> 41 + 42 + <div class="inbox"> 43 + <Tooltip text="messages stored in this PDS's repo"> 44 + <h3>inbox</h3> 45 + </Tooltip> 46 + {#if pds.inbox.length > 0} 47 + {#each pds.inbox.slice(-10) as msg} 48 + <div class="message"> 49 + <span class="sender-name">{getHandle(msg.from)}:</span> 50 + {msg.text} 51 + </div> 52 + {/each} 53 + {:else} 54 + <div class="empty">no messages</div> 55 + {/if} 56 + </div> 57 + </div> 58 + 59 + <style> 60 + .panel { 61 + background: #111; 62 + border: 1px solid #222; 63 + padding: 1rem; 64 + } 65 + 66 + h2 { 67 + font-size: 12px; 68 + font-weight: normal; 69 + color: #888; 70 + margin-bottom: 2px; 71 + } 72 + 73 + .role-label { 74 + font-size: 10px; 75 + text-transform: uppercase; 76 + letter-spacing: 0.5px; 77 + margin-bottom: 4px; 78 + } 79 + .role-label.sender { color: #1b7340; } 80 + .role-label.recipient { color: #6a9fd4; } 81 + 82 + .did { 83 + font-size: 9px; 84 + color: #383838; 85 + font-family: monospace; 86 + margin-bottom: 4px; 87 + } 88 + 89 + .subtitle { 90 + font-size: 10px; 91 + color: #444; 92 + margin-bottom: 1rem; 93 + } 94 + 95 + .requests { 96 + background: #0a0a0a; 97 + border: 1px solid #1a1a1a; 98 + padding: 0.5rem; 99 + margin-bottom: 0.75rem; 100 + min-height: 50px; 101 + } 102 + .requests h3 { 103 + font-size: 10px; 104 + color: #1b7340; 105 + margin-bottom: 0.5rem; 106 + text-transform: lowercase; 107 + } 108 + .request-item { 109 + font-size: 12px; 110 + color: #1b7340; 111 + padding: 2px 0; 112 + } 113 + 114 + .inbox { 115 + background: #0a0a0a; 116 + border: 1px solid #1a1a1a; 117 + padding: 0.5rem; 118 + min-height: 100px; 119 + max-height: 180px; 120 + overflow-y: auto; 121 + } 122 + .inbox h3 { 123 + font-size: 10px; 124 + color: #555; 125 + margin-bottom: 0.5rem; 126 + text-transform: lowercase; 127 + } 128 + 129 + .message { 130 + font-size: 12px; 131 + padding: 2px 0; 132 + border-bottom: 1px solid #1a1a1a; 133 + } 134 + .message:last-child { border-bottom: none; } 135 + 136 + .sender-name { color: #2a9d5c; } 137 + 138 + .empty { 139 + color: #333; 140 + font-style: italic; 141 + font-size: 11px; 142 + } 143 + </style>
+43
src/lib/components/Tooltip.svelte
··· 1 + <script> 2 + let { text, children } = $props(); 3 + </script> 4 + 5 + <span class="tooltip-wrapper"> 6 + {@render children()} 7 + <span class="tooltip" role="tooltip">{text}</span> 8 + </span> 9 + 10 + <style> 11 + .tooltip-wrapper { 12 + position: relative; 13 + display: inline-flex; 14 + align-items: center; 15 + cursor: help; 16 + } 17 + 18 + .tooltip { 19 + visibility: hidden; 20 + opacity: 0; 21 + position: absolute; 22 + bottom: 100%; 23 + left: 50%; 24 + transform: translateX(-50%); 25 + margin-bottom: 6px; 26 + padding: 6px 10px; 27 + background: #1a1a1a; 28 + border: 1px solid #333; 29 + color: #aaa; 30 + font-size: 10px; 31 + line-height: 1.4; 32 + white-space: nowrap; 33 + max-width: 240px; 34 + white-space: normal; 35 + z-index: 100; 36 + transition: opacity 0.15s, visibility 0.15s; 37 + } 38 + 39 + .tooltip-wrapper:hover .tooltip { 40 + visibility: visible; 41 + opacity: 1; 42 + } 43 + </style>
+127
src/lib/models.js
··· 1 + /** 2 + * service token - simulates com.atproto.server.getServiceAuth 3 + */ 4 + export function createServiceToken(iss, aud) { 5 + const exp = Date.now() + 60000; 6 + const sig = btoa(`${iss}:${aud}:${exp}`).slice(0, 8); 7 + return { iss, aud, exp, lxm: 'dev.pds.inbox.sendMessage', sig }; 8 + } 9 + 10 + /** 11 + * labeler - simulates com.atproto.label 12 + */ 13 + export class Labeler { 14 + /** @type {Map<string, Set<string>>} */ 15 + labels = new Map(); 16 + 17 + addLabel(did, label) { 18 + if (!this.labels.has(did)) this.labels.set(did, new Set()); 19 + this.labels.get(did).add(label); 20 + } 21 + 22 + removeLabel(did, label) { 23 + if (this.labels.has(did)) this.labels.get(did).delete(label); 24 + } 25 + 26 + hasLabel(did, label) { 27 + return this.labels.has(did) && this.labels.get(did).has(label); 28 + } 29 + } 30 + 31 + /** 32 + * PDS - minimal personal data server with inbox queue 33 + */ 34 + export class PDS { 35 + /** 36 + * @param {string} did 37 + * @param {string} handle 38 + * @param {number} rateLimit 39 + */ 40 + constructor(did, handle, rateLimit = 5) { 41 + this.did = did; 42 + this.handle = handle; 43 + this.rateLimit = rateLimit; 44 + 45 + /** @type {Array<{from: string, text: string, time: Date}>} */ 46 + this.inbox = []; 47 + 48 + /** @type {Set<string>} */ 49 + this.blocked = new Set(); 50 + 51 + /** @type {Map<string, {text: string, time: Date}>} */ 52 + this.pending = new Map(); 53 + 54 + /** @type {Set<string>} */ 55 + this.accepted = new Set(); 56 + 57 + /** @type {Map<string, number[]>} */ 58 + this.rateCounts = new Map(); 59 + } 60 + 61 + /** 62 + * evaluate incoming message 63 + * @param {string} senderDid 64 + * @param {string} text 65 + * @param {{iss: string, aud: string, exp: number}} token 66 + * @param {Labeler} labeler 67 + * @returns {[boolean, string]} 68 + */ 69 + evaluate(senderDid, text, token, labeler) { 70 + // token checks 71 + if (Date.now() > token.exp) return [false, 'token-expired']; 72 + if (token.aud !== this.did) return [false, 'wrong-audience']; 73 + 74 + // policy checks 75 + if (labeler.hasLabel(senderDid, 'spam')) return [false, 'labeled-spam']; 76 + if (this.blocked.has(senderDid)) return [false, 'blocked']; 77 + 78 + // invitation flow 79 + if (!this.accepted.has(senderDid)) { 80 + if (this.pending.has(senderDid)) return [false, 'pending-acceptance']; 81 + this.pending.set(senderDid, { text, time: new Date() }); 82 + return [false, 'request-created']; 83 + } 84 + 85 + // rate limiting 86 + const now = Date.now(); 87 + const cutoff = now - 60000; 88 + let counts = this.rateCounts.get(senderDid) || []; 89 + counts = counts.filter((t) => t > cutoff); 90 + if (counts.length >= this.rateLimit) return [false, 'rate-limited']; 91 + 92 + counts.push(now); 93 + this.rateCounts.set(senderDid, counts); 94 + this.inbox.push({ from: senderDid, text, time: new Date() }); 95 + return [true, 'delivered']; 96 + } 97 + 98 + /** 99 + * accept pending request from sender 100 + * @param {string} senderDid 101 + * @returns {boolean} 102 + */ 103 + acceptRequest(senderDid) { 104 + if (this.pending.has(senderDid)) { 105 + const req = this.pending.get(senderDid); 106 + this.inbox.push({ from: senderDid, text: req.text, time: req.time }); 107 + this.pending.delete(senderDid); 108 + this.accepted.add(senderDid); 109 + return true; 110 + } 111 + return false; 112 + } 113 + 114 + /** 115 + * reject pending request and block sender 116 + * @param {string} senderDid 117 + * @returns {boolean} 118 + */ 119 + rejectRequest(senderDid) { 120 + if (this.pending.has(senderDid)) { 121 + this.pending.delete(senderDid); 122 + this.blocked.add(senderDid); 123 + return true; 124 + } 125 + return false; 126 + } 127 + }
+39
src/lib/stores.js
··· 1 + import { writable } from 'svelte/store'; 2 + import { PDS, Labeler } from './models.js'; 3 + 4 + // global labeler instance 5 + export const labeler = new Labeler(); 6 + 7 + // network of PDSes 8 + export const network = { 9 + alice: new PDS('did:plc:alice', 'alice', 3), 10 + bob: new PDS('did:plc:bob', 'bob', 5), 11 + charlie: new PDS('did:plc:charlie', 'charlie', 5) 12 + }; 13 + 14 + // get PDS by handle 15 + export function getPds(handle) { 16 + return network[handle]; 17 + } 18 + 19 + // get PDS by DID 20 + export function getPdsByDid(did) { 21 + return Object.values(network).find((p) => p.did === did); 22 + } 23 + 24 + // event log entries 25 + export const logs = writable([ 26 + { msg: 'pds-to-pds messaging demo', cls: 'dim' }, 27 + { msg: 'messages require acceptance before delivery', cls: 'dim' }, 28 + { msg: '', cls: 'dim' } 29 + ]); 30 + 31 + export function log(msg, cls = '') { 32 + logs.update((l) => [...l, { msg, cls }]); 33 + } 34 + 35 + // trigger reactivity after mutations 36 + export const tick = writable(0); 37 + export function refresh() { 38 + tick.update((n) => n + 1); 39 + }
-1
src/pds_message_poc/__init__.py
··· 1 - """pds-message-poc: PDS-to-PDS message passing using docket"""
-491
src/pds_message_poc/app.py
··· 1 - """ 2 - Interactive PDS-to-PDS Message Passing Demo 3 - 4 - User-driven demonstration of jacob.gold's proposal: 5 - - PDSes have incoming message queues 6 - - Senders push to recipient's queue 7 - - Recipients decide to accept/reject 8 - 9 - Now with: 10 - - Service auth JWT simulation (com.atproto.server.getServiceAuth pattern) 11 - - Label-based reputation (com.atproto.label pattern) 12 - """ 13 - 14 - import asyncio 15 - import hashlib 16 - import time 17 - from collections import defaultdict 18 - from dataclasses import dataclass, field 19 - from datetime import datetime, timedelta, timezone 20 - 21 - from textual.app import App, ComposeResult 22 - from textual.containers import Container, Horizontal, Vertical 23 - from textual.widgets import Button, Footer, Header, Input, Label, Log, RichLog, Select, Static 24 - 25 - from docket import Docket, Worker 26 - 27 - 28 - # --- Simulated Labeler (com.atproto.label pattern) --- 29 - 30 - @dataclass 31 - class Labeler: 32 - """ 33 - Simulates ATProto labeler service. 34 - 35 - Real impl: labels are signed records with src (labeler DID), uri (target), 36 - val (label value like "spam", "trusted"). See com.atproto.label.defs 37 - """ 38 - labels: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set)) 39 - 40 - def add_label(self, did: str, label: str) -> None: 41 - self.labels[did].add(label) 42 - 43 - def remove_label(self, did: str, label: str) -> None: 44 - self.labels[did].discard(label) 45 - 46 - def has_label(self, did: str, label: str) -> bool: 47 - return label in self.labels[did] 48 - 49 - def get_labels(self, did: str) -> set[str]: 50 - return self.labels[did] 51 - 52 - 53 - # --- Simulated Service Auth (com.atproto.server.getServiceAuth pattern) --- 54 - 55 - @dataclass 56 - class ServiceToken: 57 - """ 58 - Simulates service auth JWT. 59 - 60 - Real impl: signed JWT with iss (sender DID), aud (recipient PDS DID), 61 - exp (expiry), lxm (lexicon method). See com.atproto.server.getServiceAuth 62 - """ 63 - iss: str # issuer DID 64 - aud: str # audience DID (recipient's PDS) 65 - exp: int # expiry timestamp 66 - lxm: str # lexicon method being called 67 - 68 - def is_valid(self) -> bool: 69 - return time.time() < self.exp 70 - 71 - def signature(self) -> str: 72 - """fake signature - real impl uses DID signing key""" 73 - return hashlib.sha256(f"{self.iss}:{self.aud}:{self.exp}".encode()).hexdigest()[:8] 74 - 75 - 76 - def create_service_token(sender_did: str, recipient_did: str) -> ServiceToken: 77 - """ 78 - Simulates com.atproto.server.getServiceAuth call. 79 - 80 - Real impl: sender's PDS creates JWT signed with sender's key, 81 - recipient's PDS verifies against sender's DID document. 82 - """ 83 - return ServiceToken( 84 - iss=sender_did, 85 - aud=recipient_did, 86 - exp=int(time.time()) + 60, # 60 second expiry 87 - lxm="dev.pds.inbox.sendMessage", 88 - ) 89 - 90 - 91 - # --- Simulated PDS --- 92 - 93 - @dataclass 94 - class PDS: 95 - """ 96 - Minimal PDS with inbox queue. 97 - 98 - Real impl would use ActorStore, MST, etc. 99 - See github.com/bluesky-social/atproto/tree/main/packages/pds 100 - """ 101 - did: str 102 - handle: str 103 - inbox: list[dict] = field(default_factory=list) 104 - blocked: set[str] = field(default_factory=set) 105 - rate_limit: int = 5 106 - _counts: dict[str, list[datetime]] = field(default_factory=lambda: defaultdict(list)) 107 - 108 - def accept(self, sender: str, text: str, token: ServiceToken, labeler: Labeler) -> tuple[bool, str]: 109 - """ 110 - Decide whether to accept incoming message. 111 - 112 - Checks (in order): 113 - 1. Service auth token validity 114 - 2. Label-based reputation (spam label = reject) 115 - 3. Block list 116 - 4. Rate limit 117 - """ 118 - # verify service auth 119 - if not token.is_valid(): 120 - return False, "token-expired" 121 - if token.aud != self.did: 122 - return False, "wrong-audience" 123 - 124 - # check labels (reputation) 125 - if labeler.has_label(sender, "spam"): 126 - return False, "labeled-spam" 127 - 128 - # check block list 129 - if sender in self.blocked: 130 - return False, "blocked" 131 - 132 - # rate limit 133 - now = datetime.now(timezone.utc) 134 - cutoff = now - timedelta(minutes=1) 135 - self._counts[sender] = [t for t in self._counts[sender] if t > cutoff] 136 - 137 - if len(self._counts[sender]) >= self.rate_limit: 138 - return False, "rate-limited" 139 - 140 - self._counts[sender].append(now) 141 - self.inbox.append({"from": sender, "text": text, "time": now}) 142 - return True, "accepted" 143 - 144 - 145 - # --- Global state --- 146 - 147 - NETWORK: dict[str, PDS] = {} 148 - LABELER: Labeler = Labeler() 149 - DOCKET: Docket | None = None 150 - WORKER_TASK: asyncio.Task | None = None 151 - LOG_CALLBACK = None 152 - REFRESH_CALLBACK = None 153 - 154 - 155 - def get_pds(did: str) -> PDS | None: 156 - return NETWORK.get(did) 157 - 158 - 159 - # --- Docket task (simulates dev.pds.inbox.sendMessage endpoint) --- 160 - 161 - async def deliver_message(sender_did: str, recipient_did: str, text: str, token_sig: str) -> None: 162 - """ 163 - Worker task: deliver message to recipient's inbox. 164 - 165 - This simulates what a real PDS inbox endpoint would do: 166 - 1. Verify service auth token 167 - 2. Check reputation labels 168 - 3. Check block list 169 - 4. Apply rate limits 170 - 5. Accept or reject 171 - """ 172 - recipient = get_pds(recipient_did) 173 - if not recipient: 174 - if LOG_CALLBACK: 175 - LOG_CALLBACK(f"[red]unknown recipient: {recipient_did}[/]") 176 - return 177 - 178 - sender = get_pds(sender_did) 179 - sender_name = sender.handle if sender else sender_did[:12] 180 - 181 - # recreate token (in real impl, this would be passed and verified) 182 - token = create_service_token(sender_did, recipient_did) 183 - 184 - ok, reason = recipient.accept(sender_did, text, token, LABELER) 185 - 186 - if LOG_CALLBACK: 187 - if ok: 188 - LOG_CALLBACK(f"[green]+[/] {recipient.handle} accepted from {sender_name} [dim](sig:{token_sig})[/]") 189 - else: 190 - LOG_CALLBACK(f"[red]x[/] {recipient.handle} rejected ({reason}) from {sender_name}") 191 - 192 - if REFRESH_CALLBACK: 193 - REFRESH_CALLBACK() 194 - 195 - 196 - # --- Textual App --- 197 - 198 - class InboxWidget(Static): 199 - """displays a PDS inbox""" 200 - 201 - def __init__(self, pds: PDS, **kwargs): 202 - super().__init__(**kwargs) 203 - self.pds = pds 204 - 205 - def compose(self) -> ComposeResult: 206 - yield Label(f"[bold]{self.pds.handle}[/]", classes="inbox-title") 207 - yield Label(f"rate: {self.pds.rate_limit}/min", classes="inbox-subtitle") 208 - yield Log(classes="inbox-log") 209 - 210 - def refresh_inbox(self) -> None: 211 - log = self.query_one(".inbox-log", Log) 212 - log.clear() 213 - for msg in self.pds.inbox[-10:]: 214 - sender = get_pds(msg["from"]) 215 - name = sender.handle if sender else msg["from"][:10] 216 - log.write_line(f"{name}: {msg['text']}") 217 - 218 - 219 - class PDSApp(App): 220 - """interactive PDS-to-PDS messaging demo""" 221 - 222 - CSS = """ 223 - Screen { 224 - layout: grid; 225 - grid-size: 3; 226 - grid-columns: 1fr 2fr 1fr; 227 - } 228 - 229 - #left-panel, #right-panel { 230 - height: 100%; 231 - border: solid $primary; 232 - padding: 1; 233 - } 234 - 235 - #center-panel { 236 - height: 100%; 237 - border: solid $secondary; 238 - padding: 1; 239 - } 240 - 241 - .inbox-title { 242 - text-align: center; 243 - text-style: bold; 244 - } 245 - 246 - .inbox-subtitle { 247 - text-align: center; 248 - color: $text-muted; 249 - } 250 - 251 - .inbox-log { 252 - height: 1fr; 253 - border: solid $surface; 254 - margin-top: 1; 255 - } 256 - 257 - #message-input { 258 - margin-bottom: 1; 259 - } 260 - 261 - #event-log { 262 - height: 1fr; 263 - border: solid $surface; 264 - } 265 - 266 - Horizontal { 267 - height: auto; 268 - margin-bottom: 1; 269 - align: left middle; 270 - } 271 - 272 - Select { 273 - width: 1fr; 274 - } 275 - 276 - #block-btn, #spam-btn { 277 - margin-left: 1; 278 - } 279 - """ 280 - 281 - BINDINGS = [ 282 - ("q", "quit", "Quit"), 283 - ("ctrl+c", "quit", "Quit"), 284 - ] 285 - 286 - def __init__(self): 287 - super().__init__() 288 - self.alice = PDS(did="did:plc:alice", handle="alice", rate_limit=3) 289 - self.bob = PDS(did="did:plc:bob", handle="bob", rate_limit=5) 290 - self.charlie = PDS(did="did:plc:charlie", handle="charlie", rate_limit=5) 291 - 292 - NETWORK[self.alice.did] = self.alice 293 - NETWORK[self.bob.did] = self.bob 294 - NETWORK[self.charlie.did] = self.charlie 295 - 296 - global LOG_CALLBACK, REFRESH_CALLBACK 297 - LOG_CALLBACK = self.log_event 298 - REFRESH_CALLBACK = self.refresh_inboxes 299 - 300 - def compose(self) -> ComposeResult: 301 - yield Header() 302 - 303 - with Container(id="left-panel"): 304 - yield InboxWidget(self.bob, id="bob-inbox") 305 - yield InboxWidget(self.charlie, id="charlie-inbox") 306 - 307 - with Vertical(id="center-panel"): 308 - yield Label("[bold]send message[/]") 309 - yield Input(placeholder="type message...", id="message-input") 310 - 311 - with Horizontal(): 312 - yield Select( 313 - [(p.handle, p.did) for p in [self.alice, self.bob, self.charlie]], 314 - prompt="from", 315 - id="sender-select", 316 - value=self.bob.did, 317 - ) 318 - yield Select( 319 - [(p.handle, p.did) for p in [self.alice, self.bob, self.charlie]], 320 - prompt="to", 321 - id="recipient-select", 322 - value=self.alice.did, 323 - ) 324 - 325 - with Horizontal(): 326 - yield Button("Send", id="send-btn", variant="primary") 327 - yield Button("Block", id="block-btn", variant="primary") 328 - yield Button("Spam Label", id="spam-btn", variant="warning") 329 - 330 - yield Label("[bold]event log[/]", classes="section-title") 331 - yield RichLog(id="event-log", markup=True) 332 - 333 - with Container(id="right-panel"): 334 - yield InboxWidget(self.alice, id="recipient-inbox") 335 - 336 - yield Footer() 337 - 338 - async def on_mount(self) -> None: 339 - global DOCKET, WORKER_TASK 340 - 341 - DOCKET = Docket(name="pds-inbox", url="memory://", execution_ttl=timedelta(0)) 342 - await DOCKET.__aenter__() 343 - DOCKET.register(deliver_message) 344 - 345 - worker = Worker(DOCKET) 346 - await worker.__aenter__() 347 - WORKER_TASK = asyncio.create_task(worker.run_forever()) 348 - 349 - self.log_event("[dim]docket worker started[/]") 350 - self.log_event("[dim]alice: rate=3/min | blocks=none | labels=none[/]") 351 - self.log_event("") 352 - 353 - async def on_unmount(self) -> None: 354 - global DOCKET, WORKER_TASK 355 - if WORKER_TASK: 356 - WORKER_TASK.cancel() 357 - if DOCKET: 358 - await DOCKET.__aexit__(None, None, None) 359 - 360 - def log_event(self, msg: str) -> None: 361 - try: 362 - log = self.query_one("#event-log", RichLog) 363 - log.write(msg) 364 - except Exception: 365 - pass 366 - 367 - def refresh_inboxes(self) -> None: 368 - for widget_id in ["bob-inbox", "charlie-inbox", "recipient-inbox"]: 369 - try: 370 - widget = self.query_one(f"#{widget_id}", InboxWidget) 371 - widget.refresh_inbox() 372 - except Exception: 373 - pass 374 - 375 - def update_recipient_panel(self) -> None: 376 - """Update right panel to show selected recipient's inbox""" 377 - recipient_select = self.query_one("#recipient-select", Select) 378 - recipient_did = recipient_select.value 379 - if recipient_did == Select.BLANK: 380 - return 381 - 382 - recipient = get_pds(recipient_did) 383 - if not recipient: 384 - return 385 - 386 - try: 387 - widget = self.query_one("#recipient-inbox", InboxWidget) 388 - widget.pds = recipient 389 - # update the title label 390 - title = widget.query_one(".inbox-title", Label) 391 - title.update(f"[bold]{recipient.handle}[/]") 392 - subtitle = widget.query_one(".inbox-subtitle", Label) 393 - subtitle.update(f"rate: {recipient.rate_limit}/min") 394 - widget.refresh_inbox() 395 - except Exception: 396 - pass 397 - 398 - async def on_button_pressed(self, event: Button.Pressed) -> None: 399 - if event.button.id == "send-btn": 400 - await self.send_message() 401 - elif event.button.id == "block-btn": 402 - self.toggle_block() 403 - elif event.button.id == "spam-btn": 404 - self.toggle_spam_label() 405 - 406 - async def on_input_submitted(self, event: Input.Submitted) -> None: 407 - if event.input.id == "message-input": 408 - await self.send_message() 409 - 410 - def on_select_changed(self, event: Select.Changed) -> None: 411 - if event.select.id == "recipient-select": 412 - self.update_recipient_panel() 413 - 414 - async def send_message(self) -> None: 415 - input_widget = self.query_one("#message-input", Input) 416 - sender_select = self.query_one("#sender-select", Select) 417 - recipient_select = self.query_one("#recipient-select", Select) 418 - 419 - text = input_widget.value.strip() 420 - if not text: 421 - return 422 - 423 - sender_did = sender_select.value 424 - recipient_did = recipient_select.value 425 - 426 - if sender_did == Select.BLANK or recipient_did == Select.BLANK: 427 - self.log_event("[red]select sender and recipient[/]") 428 - return 429 - 430 - sender = get_pds(sender_did) 431 - recipient = get_pds(recipient_did) 432 - 433 - # create service auth token (simulates getServiceAuth call) 434 - token = create_service_token(sender_did, recipient_did) 435 - 436 - self.log_event(f"[cyan]>>>[/] {sender.handle} -> {recipient.handle}: {text[:20]}...") 437 - self.log_event(f"[dim] token: iss={sender.handle} aud={recipient.handle} sig={token.signature()}[/]") 438 - 439 - # queue via docket (simulates HTTP POST to recipient's inbox endpoint) 440 - await DOCKET.add(deliver_message)( 441 - sender_did=sender_did, 442 - recipient_did=recipient_did, 443 - text=text, 444 - token_sig=token.signature(), 445 - ) 446 - 447 - input_widget.value = "" 448 - 449 - def toggle_block(self) -> None: 450 - sender_select = self.query_one("#sender-select", Select) 451 - sender_did = sender_select.value 452 - 453 - if sender_did == Select.BLANK: 454 - self.log_event("[red]select a sender to block/unblock[/]") 455 - return 456 - 457 - sender = get_pds(sender_did) 458 - 459 - if sender_did in self.alice.blocked: 460 - self.alice.blocked.remove(sender_did) 461 - self.log_event(f"[yellow]alice unblocked {sender.handle}[/]") 462 - else: 463 - self.alice.blocked.add(sender_did) 464 - self.log_event(f"[yellow]alice blocked {sender.handle}[/]") 465 - 466 - def toggle_spam_label(self) -> None: 467 - """Toggle spam label on selected sender (simulates labeler action)""" 468 - sender_select = self.query_one("#sender-select", Select) 469 - sender_did = sender_select.value 470 - 471 - if sender_did == Select.BLANK: 472 - self.log_event("[red]select a sender to label[/]") 473 - return 474 - 475 - sender = get_pds(sender_did) 476 - 477 - if LABELER.has_label(sender_did, "spam"): 478 - LABELER.remove_label(sender_did, "spam") 479 - self.log_event(f"[magenta]labeler removed 'spam' from {sender.handle}[/]") 480 - else: 481 - LABELER.add_label(sender_did, "spam") 482 - self.log_event(f"[magenta]labeler added 'spam' to {sender.handle}[/]") 483 - 484 - 485 - def main() -> None: 486 - app = PDSApp() 487 - app.run() 488 - 489 - 490 - if __name__ == "__main__": 491 - main()
+2
src/routes/+layout.js
··· 1 + export const prerender = true; 2 + export const ssr = false;
+10
src/routes/+layout.svelte
··· 1 + <script> 2 + import '../app.css'; 3 + let { children } = $props(); 4 + </script> 5 + 6 + <svelte:head> 7 + <title>pds messaging demo</title> 8 + </svelte:head> 9 + 10 + {@render children()}
+351
src/routes/+page.svelte
··· 1 + <script> 2 + import PdsPanel from '$lib/components/PdsPanel.svelte'; 3 + import Tooltip from '$lib/components/Tooltip.svelte'; 4 + import { 5 + labeler, 6 + logs, 7 + log, 8 + refresh, 9 + tick, 10 + getPds 11 + } from '$lib/stores.js'; 12 + import { createServiceToken } from '$lib/models.js'; 13 + 14 + let senderHandle = $state('bob'); 15 + let recipientHandle = $state('alice'); 16 + let messageText = $state(''); 17 + 18 + let sender = $derived(getPds(senderHandle)); 19 + let recipient = $derived(getPds(recipientHandle)); 20 + let isSelf = $derived(senderHandle === recipientHandle); 21 + 22 + function sendMessage() { 23 + if (!messageText.trim() || isSelf) return; 24 + 25 + const token = createServiceToken(sender.did, recipient.did); 26 + const preview = messageText.slice(0, 30); 27 + 28 + log(`>>> ${senderHandle} -> ${recipientHandle}: ${preview}...`, 'cyan'); 29 + log(` token: iss=${senderHandle} aud=${recipientHandle}`, 'dim'); 30 + 31 + const [ok, reason] = recipient.evaluate( 32 + sender.did, 33 + messageText, 34 + token, 35 + labeler 36 + ); 37 + 38 + if (ok) { 39 + log(`delivered (sig:${token.sig})`, 'green'); 40 + } else if (reason === 'request-created') { 41 + log(`request created (awaiting acceptance)`, 'yellow'); 42 + } else if (reason === 'pending-acceptance') { 43 + log(`queued (still pending)`, 'dim'); 44 + } else { 45 + log(`rejected (${reason})`, 'red'); 46 + } 47 + 48 + messageText = ''; 49 + refresh(); 50 + } 51 + 52 + function acceptRequest() { 53 + if (recipient.acceptRequest(sender.did)) { 54 + log(`${recipientHandle} accepted ${senderHandle}`, 'green'); 55 + } else { 56 + log(`no pending request from ${senderHandle}`, 'dim'); 57 + } 58 + refresh(); 59 + } 60 + 61 + function rejectRequest() { 62 + if (recipient.rejectRequest(sender.did)) { 63 + log(`${recipientHandle} rejected ${senderHandle}`, 'red'); 64 + } else { 65 + log(`no pending request from ${senderHandle}`, 'dim'); 66 + } 67 + refresh(); 68 + } 69 + 70 + function swap() { 71 + [senderHandle, recipientHandle] = [recipientHandle, senderHandle]; 72 + } 73 + 74 + function toggleSpam() { 75 + if (labeler.hasLabel(sender.did, 'spam')) { 76 + labeler.removeLabel(sender.did, 'spam'); 77 + log(`removed 'spam' from ${senderHandle}`, 'magenta'); 78 + } else { 79 + labeler.addLabel(sender.did, 'spam'); 80 + log(`added 'spam' to ${senderHandle}`, 'magenta'); 81 + } 82 + } 83 + </script> 84 + 85 + <h1> 86 + <a href="/">pds messaging demo</a> 87 + <span class="by"> 88 + by <a href="https://bsky.app/profile/zzstoatzz.io">@zzstoatzz.io</a> 89 + </span> 90 + </h1> 91 + 92 + <div class="container"> 93 + {#key $tick} 94 + <PdsPanel pds={sender} role="sender" /> 95 + {/key} 96 + 97 + <div class="center"> 98 + <h2>send message</h2> 99 + 100 + <input 101 + type="text" 102 + bind:value={messageText} 103 + placeholder="type message..." 104 + aria-label="message text" 105 + onkeypress={(e) => e.key === 'Enter' && sendMessage()} 106 + /> 107 + 108 + <div class="selectors"> 109 + <div class="select-group"> 110 + <label class="label from" for="sender">from</label> 111 + <select id="sender" bind:value={senderHandle}> 112 + <option value="alice">alice</option> 113 + <option value="bob">bob</option> 114 + <option value="charlie">charlie</option> 115 + </select> 116 + </div> 117 + <button class="swap" onclick={swap} aria-label="swap sender and recipient">⇄</button> 118 + <div class="select-group"> 119 + <label class="label to" for="recipient">to</label> 120 + <select id="recipient" bind:value={recipientHandle}> 121 + <option value="alice">alice</option> 122 + <option value="bob">bob</option> 123 + <option value="charlie">charlie</option> 124 + </select> 125 + </div> 126 + </div> 127 + 128 + <div class="buttons"> 129 + <Tooltip text="creates service auth JWT, sends via XRPC to recipient's PDS"> 130 + <button class="send" onclick={sendMessage} disabled={isSelf}> 131 + send 132 + </button> 133 + </Tooltip> 134 + <Tooltip text="recipient accepts sender's message request"> 135 + <button class="accept" onclick={acceptRequest}>accept</button> 136 + </Tooltip> 137 + <Tooltip text="reject request and block sender permanently"> 138 + <button class="reject" onclick={rejectRequest}>reject</button> 139 + </Tooltip> 140 + <Tooltip text="labeler marks sender as spam (all PDSes reject)"> 141 + <button class="spam" onclick={toggleSpam}>spam</button> 142 + </Tooltip> 143 + </div> 144 + 145 + <div class="log"> 146 + <h3>event log</h3> 147 + {#each $logs as entry} 148 + <div class={entry.cls}>{entry.msg}</div> 149 + {/each} 150 + </div> 151 + </div> 152 + 153 + {#key $tick} 154 + <PdsPanel pds={recipient} role="recipient" /> 155 + {/key} 156 + </div> 157 + 158 + <footer> 159 + <p> 160 + demonstrating 161 + <a href="https://bsky.app/profile/jacob.gold/post/3mbsbqsc3vc24"> 162 + jacob.gold's proposal 163 + </a> 164 + for PDS-to-PDS messaging 165 + </p> 166 + <p class="detail"> 167 + each PDS has an inbox queue • service auth proves sender identity • 168 + labelers provide reputation signals 169 + </p> 170 + </footer> 171 + 172 + <style> 173 + h1 { 174 + font-size: 12px; 175 + font-weight: normal; 176 + margin-bottom: 1.5rem; 177 + text-align: center; 178 + } 179 + h1 a { color: #888; } 180 + h1 a:hover { color: #fff; } 181 + h1 .by { font-size: 10px; color: #555; } 182 + h1 .by a { color: #555; } 183 + h1 .by a:hover { color: #1b7340; } 184 + 185 + .container { 186 + display: grid; 187 + grid-template-columns: 1fr 2fr 1fr; 188 + gap: 1rem; 189 + max-width: 1000px; 190 + margin: 0 auto; 191 + } 192 + 193 + .center { 194 + background: #111; 195 + border: 1px solid #222; 196 + padding: 1rem; 197 + } 198 + .center h2 { 199 + font-size: 11px; 200 + font-weight: normal; 201 + color: #555; 202 + margin-bottom: 0.75rem; 203 + text-transform: lowercase; 204 + } 205 + 206 + input, select { 207 + width: 100%; 208 + padding: 0.5rem; 209 + font-family: monospace; 210 + font-size: 14px; 211 + background: #111; 212 + border: 1px solid #222; 213 + color: #ccc; 214 + } 215 + input:focus, select:focus { 216 + outline: none; 217 + border-color: #1b7340; 218 + } 219 + input { margin-bottom: 0.75rem; } 220 + select { cursor: pointer; } 221 + select option { background: #111; } 222 + 223 + .selectors { 224 + display: flex; 225 + gap: 0.5rem; 226 + margin-bottom: 0.75rem; 227 + align-items: center; 228 + } 229 + .select-group { 230 + flex: 1; 231 + display: flex; 232 + flex-direction: column; 233 + gap: 2px; 234 + } 235 + .label { 236 + font-size: 10px; 237 + text-transform: uppercase; 238 + letter-spacing: 0.5px; 239 + } 240 + .label.from { color: #1b7340; } 241 + .label.to { color: #6a9fd4; } 242 + .swap { 243 + flex: 0; 244 + padding: 0.25rem 0.5rem; 245 + margin-top: 14px; 246 + font-size: 14px; 247 + background: #111; 248 + border: 1px solid #333; 249 + color: #555; 250 + cursor: pointer; 251 + } 252 + .swap:hover { 253 + border-color: #555; 254 + color: #888; 255 + } 256 + 257 + .buttons { 258 + display: flex; 259 + gap: 0.5rem; 260 + margin-bottom: 1rem; 261 + } 262 + .buttons > :global(*) { 263 + flex: 1; 264 + } 265 + button { 266 + width: 100%; 267 + padding: 0.5rem; 268 + font-family: monospace; 269 + font-size: 12px; 270 + background: #111; 271 + border: 1px solid #222; 272 + color: #888; 273 + cursor: pointer; 274 + } 275 + button:hover { 276 + background: #1a1a1a; 277 + border-color: #333; 278 + color: #ccc; 279 + } 280 + button:disabled { 281 + opacity: 0.3; 282 + cursor: not-allowed; 283 + } 284 + button.send { 285 + border-color: #1b7340; 286 + color: #2a9d5c; 287 + } 288 + button.send:hover:not(:disabled) { 289 + background: rgba(27, 115, 64, 0.2); 290 + } 291 + button.accept { 292 + border-color: #1b7340; 293 + color: #2a9d5c; 294 + } 295 + button.accept:hover { 296 + background: rgba(27, 115, 64, 0.2); 297 + } 298 + button.reject { 299 + border-color: #4a2020; 300 + color: #a44; 301 + } 302 + button.reject:hover { 303 + background: rgba(170, 68, 68, 0.1); 304 + } 305 + button.spam { 306 + border-color: #4a4020; 307 + color: #a84; 308 + } 309 + button.spam:hover { 310 + background: rgba(168, 136, 68, 0.1); 311 + } 312 + 313 + .log { 314 + background: #0a0a0a; 315 + border: 1px solid #1a1a1a; 316 + padding: 0.5rem; 317 + height: 250px; 318 + overflow-y: auto; 319 + font-size: 11px; 320 + } 321 + .log h3 { 322 + font-size: 10px; 323 + color: #444; 324 + margin-bottom: 0.5rem; 325 + text-transform: lowercase; 326 + } 327 + 328 + footer { 329 + max-width: 1000px; 330 + margin: 2rem auto 0; 331 + padding: 1.5rem 1rem; 332 + text-align: center; 333 + border-top: 1px solid #1a1a1a; 334 + } 335 + footer p { 336 + font-size: 11px; 337 + color: #555; 338 + margin: 0; 339 + } 340 + footer a { 341 + color: #1b7340; 342 + } 343 + footer a:hover { 344 + color: #2a9d5c; 345 + } 346 + footer .detail { 347 + margin-top: 0.5rem; 348 + font-size: 10px; 349 + color: #383838; 350 + } 351 + </style>
+15
svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-static'; 2 + 3 + /** @type {import('@sveltejs/kit').Config} */ 4 + const config = { 5 + kit: { 6 + adapter: adapter({ 7 + fallback: 'index.html' 8 + }), 9 + paths: { 10 + base: '/zzstoatzz.io/pds-message-poc' 11 + } 12 + } 13 + }; 14 + 15 + export default config;
-517
uv.lock
··· 1 - version = 1 2 - revision = 3 3 - requires-python = ">=3.12" 4 - 5 - [[package]] 6 - name = "beartype" 7 - version = "0.22.9" 8 - source = { registry = "https://pypi.org/simple" } 9 - sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } 10 - wheels = [ 11 - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, 12 - ] 13 - 14 - [[package]] 15 - name = "cachetools" 16 - version = "6.2.4" 17 - source = { registry = "https://pypi.org/simple" } 18 - sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } 19 - wheels = [ 20 - { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, 21 - ] 22 - 23 - [[package]] 24 - name = "click" 25 - version = "8.3.1" 26 - source = { registry = "https://pypi.org/simple" } 27 - dependencies = [ 28 - { name = "colorama", marker = "sys_platform == 'win32'" }, 29 - ] 30 - sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } 31 - wheels = [ 32 - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, 33 - ] 34 - 35 - [[package]] 36 - name = "cloudpickle" 37 - version = "3.1.2" 38 - source = { registry = "https://pypi.org/simple" } 39 - sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } 40 - wheels = [ 41 - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, 42 - ] 43 - 44 - [[package]] 45 - name = "colorama" 46 - version = "0.4.6" 47 - source = { registry = "https://pypi.org/simple" } 48 - sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 49 - wheels = [ 50 - { 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" }, 51 - ] 52 - 53 - [[package]] 54 - name = "fakeredis" 55 - version = "2.33.0" 56 - source = { registry = "https://pypi.org/simple" } 57 - dependencies = [ 58 - { name = "redis" }, 59 - { name = "sortedcontainers" }, 60 - ] 61 - sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } 62 - wheels = [ 63 - { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, 64 - ] 65 - 66 - [package.optional-dependencies] 67 - lua = [ 68 - { name = "lupa" }, 69 - ] 70 - 71 - [[package]] 72 - name = "importlib-metadata" 73 - version = "8.7.1" 74 - source = { registry = "https://pypi.org/simple" } 75 - dependencies = [ 76 - { name = "zipp" }, 77 - ] 78 - sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } 79 - wheels = [ 80 - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, 81 - ] 82 - 83 - [[package]] 84 - name = "linkify-it-py" 85 - version = "2.0.3" 86 - source = { registry = "https://pypi.org/simple" } 87 - dependencies = [ 88 - { name = "uc-micro-py" }, 89 - ] 90 - sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } 91 - wheels = [ 92 - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, 93 - ] 94 - 95 - [[package]] 96 - name = "lupa" 97 - version = "2.6" 98 - source = { registry = "https://pypi.org/simple" } 99 - sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } 100 - wheels = [ 101 - { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, 102 - { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, 103 - { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, 104 - { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, 105 - { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, 106 - { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, 107 - { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, 108 - { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, 109 - { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, 110 - { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, 111 - { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, 112 - { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, 113 - { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, 114 - { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, 115 - { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, 116 - { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, 117 - { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, 118 - { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, 119 - { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, 120 - { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, 121 - { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, 122 - { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, 123 - { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, 124 - { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, 125 - { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, 126 - { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, 127 - { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, 128 - { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, 129 - { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, 130 - { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, 131 - { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, 132 - { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, 133 - { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, 134 - { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, 135 - { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, 136 - { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, 137 - { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, 138 - { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, 139 - { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, 140 - { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, 141 - { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, 142 - { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, 143 - { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, 144 - { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, 145 - ] 146 - 147 - [[package]] 148 - name = "markdown-it-py" 149 - version = "4.0.0" 150 - source = { registry = "https://pypi.org/simple" } 151 - dependencies = [ 152 - { name = "mdurl" }, 153 - ] 154 - sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } 155 - wheels = [ 156 - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, 157 - ] 158 - 159 - [package.optional-dependencies] 160 - linkify = [ 161 - { name = "linkify-it-py" }, 162 - ] 163 - 164 - [[package]] 165 - name = "mdit-py-plugins" 166 - version = "0.5.0" 167 - source = { registry = "https://pypi.org/simple" } 168 - dependencies = [ 169 - { name = "markdown-it-py" }, 170 - ] 171 - sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } 172 - wheels = [ 173 - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, 174 - ] 175 - 176 - [[package]] 177 - name = "mdurl" 178 - version = "0.1.2" 179 - source = { registry = "https://pypi.org/simple" } 180 - sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 181 - wheels = [ 182 - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 183 - ] 184 - 185 - [[package]] 186 - name = "opentelemetry-api" 187 - version = "1.39.1" 188 - source = { registry = "https://pypi.org/simple" } 189 - dependencies = [ 190 - { name = "importlib-metadata" }, 191 - { name = "typing-extensions" }, 192 - ] 193 - sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } 194 - wheels = [ 195 - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, 196 - ] 197 - 198 - [[package]] 199 - name = "opentelemetry-exporter-prometheus" 200 - version = "0.60b1" 201 - source = { registry = "https://pypi.org/simple" } 202 - dependencies = [ 203 - { name = "opentelemetry-api" }, 204 - { name = "opentelemetry-sdk" }, 205 - { name = "prometheus-client" }, 206 - ] 207 - sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } 208 - wheels = [ 209 - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, 210 - ] 211 - 212 - [[package]] 213 - name = "opentelemetry-instrumentation" 214 - version = "0.60b1" 215 - source = { registry = "https://pypi.org/simple" } 216 - dependencies = [ 217 - { name = "opentelemetry-api" }, 218 - { name = "opentelemetry-semantic-conventions" }, 219 - { name = "packaging" }, 220 - { name = "wrapt" }, 221 - ] 222 - sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } 223 - wheels = [ 224 - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, 225 - ] 226 - 227 - [[package]] 228 - name = "opentelemetry-sdk" 229 - version = "1.39.1" 230 - source = { registry = "https://pypi.org/simple" } 231 - dependencies = [ 232 - { name = "opentelemetry-api" }, 233 - { name = "opentelemetry-semantic-conventions" }, 234 - { name = "typing-extensions" }, 235 - ] 236 - sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } 237 - wheels = [ 238 - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, 239 - ] 240 - 241 - [[package]] 242 - name = "opentelemetry-semantic-conventions" 243 - version = "0.60b1" 244 - source = { registry = "https://pypi.org/simple" } 245 - dependencies = [ 246 - { name = "opentelemetry-api" }, 247 - { name = "typing-extensions" }, 248 - ] 249 - sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } 250 - wheels = [ 251 - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, 252 - ] 253 - 254 - [[package]] 255 - name = "packaging" 256 - version = "25.0" 257 - source = { registry = "https://pypi.org/simple" } 258 - sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 259 - wheels = [ 260 - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 261 - ] 262 - 263 - [[package]] 264 - name = "pds-message-poc" 265 - version = "0.1.0" 266 - source = { editable = "." } 267 - dependencies = [ 268 - { name = "pydocket" }, 269 - { name = "textual" }, 270 - ] 271 - 272 - [package.metadata] 273 - requires-dist = [ 274 - { name = "pydocket", specifier = ">=0.13" }, 275 - { name = "textual", specifier = ">=0.50" }, 276 - ] 277 - 278 - [[package]] 279 - name = "platformdirs" 280 - version = "4.5.1" 281 - source = { registry = "https://pypi.org/simple" } 282 - sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } 283 - wheels = [ 284 - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, 285 - ] 286 - 287 - [[package]] 288 - name = "prometheus-client" 289 - version = "0.23.1" 290 - source = { registry = "https://pypi.org/simple" } 291 - sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } 292 - wheels = [ 293 - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, 294 - ] 295 - 296 - [[package]] 297 - name = "py-key-value-aio" 298 - version = "0.3.0" 299 - source = { registry = "https://pypi.org/simple" } 300 - dependencies = [ 301 - { name = "beartype" }, 302 - { name = "py-key-value-shared" }, 303 - ] 304 - sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } 305 - wheels = [ 306 - { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, 307 - ] 308 - 309 - [package.optional-dependencies] 310 - memory = [ 311 - { name = "cachetools" }, 312 - ] 313 - redis = [ 314 - { name = "redis" }, 315 - ] 316 - 317 - [[package]] 318 - name = "py-key-value-shared" 319 - version = "0.3.0" 320 - source = { registry = "https://pypi.org/simple" } 321 - dependencies = [ 322 - { name = "beartype" }, 323 - { name = "typing-extensions" }, 324 - ] 325 - sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } 326 - wheels = [ 327 - { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, 328 - ] 329 - 330 - [[package]] 331 - name = "pydocket" 332 - version = "0.16.3" 333 - source = { registry = "https://pypi.org/simple" } 334 - dependencies = [ 335 - { name = "cloudpickle" }, 336 - { name = "fakeredis", extra = ["lua"] }, 337 - { name = "opentelemetry-api" }, 338 - { name = "opentelemetry-exporter-prometheus" }, 339 - { name = "opentelemetry-instrumentation" }, 340 - { name = "prometheus-client" }, 341 - { name = "py-key-value-aio", extra = ["memory", "redis"] }, 342 - { name = "python-json-logger" }, 343 - { name = "redis" }, 344 - { name = "rich" }, 345 - { name = "typer" }, 346 - { name = "typing-extensions" }, 347 - ] 348 - sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" } 349 - wheels = [ 350 - { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" }, 351 - ] 352 - 353 - [[package]] 354 - name = "pygments" 355 - version = "2.19.2" 356 - source = { registry = "https://pypi.org/simple" } 357 - sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 358 - wheels = [ 359 - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 360 - ] 361 - 362 - [[package]] 363 - name = "python-json-logger" 364 - version = "4.0.0" 365 - source = { registry = "https://pypi.org/simple" } 366 - sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } 367 - wheels = [ 368 - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, 369 - ] 370 - 371 - [[package]] 372 - name = "redis" 373 - version = "7.1.0" 374 - source = { registry = "https://pypi.org/simple" } 375 - sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } 376 - wheels = [ 377 - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, 378 - ] 379 - 380 - [[package]] 381 - name = "rich" 382 - version = "14.2.0" 383 - source = { registry = "https://pypi.org/simple" } 384 - dependencies = [ 385 - { name = "markdown-it-py" }, 386 - { name = "pygments" }, 387 - ] 388 - sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } 389 - wheels = [ 390 - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, 391 - ] 392 - 393 - [[package]] 394 - name = "shellingham" 395 - version = "1.5.4" 396 - source = { registry = "https://pypi.org/simple" } 397 - sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } 398 - wheels = [ 399 - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, 400 - ] 401 - 402 - [[package]] 403 - name = "sortedcontainers" 404 - version = "2.4.0" 405 - source = { registry = "https://pypi.org/simple" } 406 - sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } 407 - wheels = [ 408 - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, 409 - ] 410 - 411 - [[package]] 412 - name = "textual" 413 - version = "7.0.0" 414 - source = { registry = "https://pypi.org/simple" } 415 - dependencies = [ 416 - { name = "markdown-it-py", extra = ["linkify"] }, 417 - { name = "mdit-py-plugins" }, 418 - { name = "platformdirs" }, 419 - { name = "pygments" }, 420 - { name = "rich" }, 421 - { name = "typing-extensions" }, 422 - ] 423 - sdist = { url = "https://files.pythonhosted.org/packages/d4/9c/ebc9ca1f95366bef4c0e8360f4a77400d47a79aeecc08747de1990ef8bdc/textual-7.0.0.tar.gz", hash = "sha256:617638a2be74fb7507aff3ea6ec9374148be02e5a7bb1d02396d1d557b66c0a9", size = 1582005, upload-time = "2026-01-03T11:48:10.909Z" } 424 - wheels = [ 425 - { url = "https://files.pythonhosted.org/packages/63/f8/a1ef9034b2a7f334f91b2f673f2ec03020a2529bb30a9437a6beb855beee/textual-7.0.0-py3-none-any.whl", hash = "sha256:190de0f65e5f4bc820fae46f32f591e509621d76688b36400ce01fa63dc6b623", size = 715156, upload-time = "2026-01-03T11:48:09.067Z" }, 426 - ] 427 - 428 - [[package]] 429 - name = "typer" 430 - version = "0.21.1" 431 - source = { registry = "https://pypi.org/simple" } 432 - dependencies = [ 433 - { name = "click" }, 434 - { name = "rich" }, 435 - { name = "shellingham" }, 436 - { name = "typing-extensions" }, 437 - ] 438 - sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } 439 - wheels = [ 440 - { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, 441 - ] 442 - 443 - [[package]] 444 - name = "typing-extensions" 445 - version = "4.15.0" 446 - source = { registry = "https://pypi.org/simple" } 447 - sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 448 - wheels = [ 449 - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 450 - ] 451 - 452 - [[package]] 453 - name = "uc-micro-py" 454 - version = "1.0.3" 455 - source = { registry = "https://pypi.org/simple" } 456 - sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } 457 - wheels = [ 458 - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, 459 - ] 460 - 461 - [[package]] 462 - name = "wrapt" 463 - version = "1.17.3" 464 - source = { registry = "https://pypi.org/simple" } 465 - sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } 466 - wheels = [ 467 - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, 468 - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, 469 - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, 470 - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, 471 - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, 472 - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, 473 - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, 474 - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, 475 - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, 476 - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, 477 - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, 478 - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, 479 - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, 480 - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, 481 - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, 482 - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, 483 - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, 484 - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, 485 - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, 486 - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, 487 - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, 488 - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, 489 - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, 490 - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, 491 - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, 492 - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, 493 - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, 494 - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, 495 - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, 496 - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, 497 - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, 498 - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, 499 - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, 500 - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, 501 - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, 502 - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, 503 - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, 504 - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, 505 - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, 506 - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, 507 - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, 508 - ] 509 - 510 - [[package]] 511 - name = "zipp" 512 - version = "3.23.0" 513 - source = { registry = "https://pypi.org/simple" } 514 - sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } 515 - wheels = [ 516 - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, 517 - ]
+6
vite.config.js
··· 1 + import { sveltekit } from '@sveltejs/kit/vite'; 2 + import { defineConfig } from 'vite'; 3 + 4 + export default defineConfig({ 5 + plugins: [sveltekit()] 6 + });