+23
-3
.gitignore
+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
+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
+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
bun.lockb
This is a binary file and will not be displayed.
-5
justfile
-5
justfile
+20
package.json
+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
-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
+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
+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
+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
+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
+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
+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
src/pds_message_poc/__init__.py
···
1
-
"""pds-message-poc: PDS-to-PDS message passing using docket"""
-491
src/pds_message_poc/app.py
-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()
+10
src/routes/+layout.svelte
+10
src/routes/+layout.svelte
+351
src/routes/+page.svelte
+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
+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
-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
-
]