209 Commits

Author SHA1 Message Date
e308a07dab fix(backend_server.py): call _initialize_handlers() on init 2026-03-03 20:38:55 +01:00
dafe418916 feat(backend_server.py): add warning if received an unknown message (no handler) 2026-03-03 20:18:24 +01:00
c64a258243 build(uv.lock): update judas_protocol to 0.8.0 2026-03-03 19:34:32 +01:00
ead2224066 fix(backend_server.py): do not disconnect a client if Exception raised on msg handling 2026-03-03 18:56:34 +01:00
ee381414a9 chore(backend_server.py): remove redundant HELLO msg handling 2026-03-03 18:55:45 +01:00
0ed478a88e feat(backend_server.py): implement message handling 2026-03-03 18:55:24 +01:00
6446fe883c fix(backend_server.py): check if client to disconnect has an open socket 2026-03-03 18:54:45 +01:00
ec58a5257a chore(handler/__init__.py): add module init 2026-03-03 18:53:05 +01:00
c952413d91 feat(hello_handler.py): add HELLO message handler 2026-03-03 18:52:39 +01:00
882c8780e1 feat(base_handler.py): add BaseHandler class for message handling 2026-03-03 18:52:13 +01:00
d3f68d3baf chore(backend/__init__.py): add Client and ClientStatus to __all__ 2026-03-03 17:46:51 +01:00
62acc4b181 style(client.py): correct property typing 2026-03-03 17:46:06 +01:00
faecc38261 feat(client_status.py): move ClientStatus enum to own module 2026-03-03 17:45:01 +01:00
3eb681e233 refactor(backend_server.py): move loading known clients to its own method 2026-03-03 17:43:19 +01:00
bda10a6248 chore(cache/): add cache/ directory 2026-03-03 17:41:48 +01:00
fa2da207a9 refactor(backend_server.py): refactor calls to Message class constructors after protocol changes 2026-03-01 20:17:05 +01:00
f41a7774ec build(uv.lock): update judas_protocol to 0.7.0 2026-03-01 20:14:03 +01:00
github-actions[bot]
2a8ac307d5 chore(release): 0.5.0 2026-02-28 22:49:49 +00:00
3911afff26 Merge pull request 'chore(release): 0.5.0' (#13) from release/0.5.0 into main
Reviewed-on: #13
2026-02-28 22:48:50 +00:00
7f904fdcd5 build(uv.lock): add pyyaml to depedencies 2026-02-28 23:44:58 +01:00
956da024c3 feat(style.css): make client list elements flexboxes 2026-02-28 23:44:43 +01:00
f11b442ece chore(style.css): style no connection message correctly 2026-02-28 23:44:03 +01:00
f54d974745 style(style.css): remove commented-out code 2026-02-28 23:43:37 +01:00
54eec657a5 feat(style.css): use monospaced font only for heading 2026-02-28 23:43:15 +01:00
1900bf46cc feat(client_details.html): add basic client details 2026-02-28 23:42:12 +01:00
c5771dc371 feat(panel.html): use client_details.html after clicking on client on sidebar 2026-02-28 23:41:30 +01:00
de9240e6e0 feat(backend_server.py): set client status to STALE if offline for >24h 2026-02-28 23:40:03 +01:00
b1656cdfa9 feat(client.py): redo ClientStatuses adding PENDING and STALE 2026-02-28 23:39:07 +01:00
563de5aa19 feat(backend_server.py): add known_clients.yaml file for storing clients between server restarts 2026-02-28 23:30:20 +01:00
72a74334e7 build(pyproject.toml): add pyyaml to depedencies 2026-02-28 21:31:57 +01:00
5ca5d9fcf4 chore(.vscode/launch.json): set cwd to workspace folder 2026-02-28 21:31:37 +01:00
f01eaecba3 build(uv.lock): update judas_protocol to 0.6.0 2026-02-28 20:58:09 +01:00
a5e1ba88ae refactor: adapt for client.id 2026-02-28 20:57:59 +01:00
1e38be5ec5 refactor(backend_server.py): adapt client's id after refactor 2026-02-28 20:48:17 +01:00
b265feba7a refactor(client.py): rename Client.mac_id -> Client.id 2026-02-28 20:47:37 +01:00
6971548589 feat(web_server.py): use client_details blueprint 2026-02-28 20:44:14 +01:00
31c51574f7 feat(client_details.py): add client_details route 2026-02-28 20:43:57 +01:00
b652db930f feat(style.css): style no-connection-icon and client list 2026-02-28 20:42:29 +01:00
1dfddd2fc7 feat(panel.html): add a side panel client list 2026-02-28 20:42:05 +01:00
d20ff9be6e feat(panel.html): add a no connection icon to header 2026-02-28 20:41:35 +01:00
2bbe118de6 feat: rename details.html -> client_details.html 2026-02-28 20:40:57 +01:00
840d9ce3c1 refactor(login.html): move elements from main to #content 2026-02-28 20:40:16 +01:00
9971981f66 refactor(index.html): move elements from main to #content 2026-02-28 20:40:05 +01:00
29b4f3a2ff feat(css/style.css): add .button:active color 2026-02-28 20:39:29 +01:00
69bf4f1358 feat(css/style.css): add #content styling, make main's flex column to fit aside 2026-02-28 20:39:06 +01:00
0580a6be53 chore(css/style.css): remove transition from button hover bg color 2026-02-28 20:37:29 +01:00
563dc62624 feat(css/style.css): include flaticon icons 2026-02-28 20:36:29 +01:00
bb229dc724 feat(css/style.css): make UI more compact 2026-02-28 20:36:10 +01:00
5510e9dd08 refactor(backend_server.py): rewrite _handle_connection() to minimize indents 2026-02-28 20:35:09 +01:00
3077a98d6f feat(backend_server.py): add send_close() method 2026-02-28 20:34:16 +01:00
1e02da1851 refactor(backend_server.py): rename _send_ack() -> send_ack() 2026-02-28 19:07:11 +01:00
97221bc1b7 build(uv.lock): update depedencies for Python 3.14 2026-02-28 17:52:29 +01:00
github-actions[bot]
5dd208a100 chore(release): 0.4.0 2025-11-30 17:39:03 +00:00
18b7e10631 Merge pull request 'chore(release): 0.4.0' (#12) from release/0.4.0 into main
Reviewed-on: #12
2025-11-30 17:37:18 +00:00
07106f1816 Merge pull request 'refactor: improve backend server' (#11) from refactor/improve-backend-server into develop
Reviewed-on: #11
2025-11-30 17:34:14 +00:00
115deaab4b fix(backend_server.py): use unused packet_size argument to _receive_inbound() 2025-11-30 18:32:48 +01:00
8d2b8f9519 refactor(backend_server.py): change log levels 2025-11-30 18:30:59 +01:00
f365139e9f style(backend_server.py): ignore type error in _handle_connection() 2025-11-30 18:12:24 +01:00
cc6b650f5c fix(backend_server.py): import Any from typing 2025-11-30 18:11:41 +01:00
faf1f4eeee refactor(backend_server.py): remove unused argument in BackendServer._send_outbound() 2025-11-30 18:10:37 +01:00
61a607c20e refactor: wip 2025-11-21 18:39:44 +01:00
fe7d78c1c8 build(uv.lock): update judas_protocol to 0.5.0 2025-11-19 21:46:31 +01:00
cee30251dd build(uv.lock): update judas_protocol to 0.4.3 2025-10-05 13:43:41 +02:00
721ab87e71 build(uv.lock): update judas_protocol to 0.3.0 2025-09-23 23:31:27 +02:00
1211ca2029 build(uv.lock): update judas_protocol to 0.2.0 2025-09-22 21:59:06 +02:00
5024862165 fix(backend_server.py): add 1ms sleep to prevent 100% CPU usage in _loop() 2025-09-22 21:58:59 +02:00
6f5fa33a12 feat(backend_server.py): handle all other client Exceptions in _handle_connection() 2025-09-22 21:57:43 +02:00
f0eeeb79a1 fix(backend_server.py): handle unregister exceptions 2025-09-22 21:56:40 +02:00
github-actions[bot]
6ef3f58d73 chore(release): 0.3.0 2025-09-19 13:29:28 +00:00
8f97bea0e9 Merge pull request 'release/0.3.0' (#10) from release/0.3.0 into main
Reviewed-on: #10
2025-09-19 13:28:10 +00:00
a2d1669179 Merge pull request 'feat: add backend server' (#9) from feat/add-backend-server into develop
Reviewed-on: #9
2025-09-19 13:20:40 +00:00
66db11c40b build: add ruff lint depedency 2025-09-19 14:57:38 +02:00
d96fec0222 build(web_server.py): order imports 2025-09-19 14:56:45 +02:00
6bbb152ae1 style(panel.py): order imports 2025-09-19 14:56:06 +02:00
b14541ea83 style(index.py): order imports 2025-09-19 14:55:56 +02:00
7d36e6e431 style(auth.py): order imports 2025-09-19 14:55:44 +02:00
98c49b1d0d style(api.py): order imports 2025-09-19 14:55:08 +02:00
377a846c59 style(client.py): order imports 2025-09-19 14:54:54 +02:00
a27b5b1275 style(backend_server.py): order imports 2025-09-19 14:54:43 +02:00
19aa80a08e style(__main__.py): order imports 2025-09-19 14:54:24 +02:00
a1b28faa9f build: add isort as lint depedency 2025-09-19 14:53:47 +02:00
df0b19d943 feat(routes/api.py): support mulitple clients correctly 2025-09-02 19:16:35 +02:00
b68c755c45 fix(backend_server.py): send ack on connect 2025-09-02 19:13:26 +02:00
0d074adc0d refactor(backend_server.py): use _send_ack() in _handle_connection() 2025-08-29 15:21:57 +02:00
f2b4811145 feat(backend_server.py): add BackendServer._send_ack() helper method 2025-08-29 15:20:02 +02:00
2922123a70 fix(backend/__init__.py): fix backend_server import 2025-08-29 15:17:58 +02:00
d5985cf594 feat(__main__.py): display ascii judas splash screen on launch 2025-08-29 00:38:59 +02:00
b9fd84d08f feat(gaga.py): add Lady Gaga's Born This Way album art ASCII 2025-08-29 00:38:23 +02:00
f7f6f19808 style(__main__.py): organize imports 2025-08-29 00:36:03 +02:00
ac7799213c refactor(__main__.py): provide host and port args to BackendServer explicitly 2025-08-29 00:35:22 +02:00
54512d8393 chore(routes/api.py): adapt emit_polled_data() for Client class 2025-08-29 00:33:44 +02:00
c0ad91b22f feat(backend_server.py): re-add the get_client_data() method 2025-08-29 00:31:43 +02:00
b9a7c2bdaf docs(backend_server.py): add TODO in _handle_connection() 2025-08-29 00:30:34 +02:00
ac66ce1999 chore(backend_server.py): set Client.last_seen on message receive 2025-08-29 00:29:56 +02:00
2ecd32decc feat(client.py): add Client class 2025-08-29 00:26:49 +02:00
1c96390f3c feat(backend_server.py): add error handling for HELLO message handler in _accept_connection() 2025-08-29 00:23:48 +02:00
c0cacfad2d fix(backend_server.py): fix server waiting for closed socket to dissapear by reusing it in __init__() 2025-08-29 00:20:02 +02:00
cafef5ed93 style(backend_server.py): remove unused imports 2025-08-29 00:18:18 +02:00
b5670e5d2c refactor(server.py): rename server.py to backend_server.py for clarity 2025-08-29 00:15:40 +02:00
639e1f73a0 feat(server.py): add socket-based communication 2025-08-28 20:04:32 +02:00
4e16a70174 build(uv.lock): add depedency on judas_protocol 2025-08-28 19:57:24 +02:00
4aa2ca426c build(pyproject.toml): add depedency on judas_protocol 2025-08-28 19:57:10 +02:00
github-actions[bot]
813407057e chore(release): 0.2.0 2025-08-26 18:01:32 +00:00
89b6c1ee83 ci(release.yaml): update with latest pufereq/python-template release.yaml 2025-08-26 20:00:23 +02:00
bf5cc97c11 Merge pull request 'chore(release): 0.2.0' (#8) from release/0.2.0 into main
Reviewed-on: #8
2025-08-26 17:58:45 +00:00
6f21e53677 Merge pull request 'feat: add placeholder backend' (#7) from feat/add-placeholder-backend into develop
Reviewed-on: #7
2025-08-26 17:56:00 +00:00
9f74246157 feat(web_server.py): use socketio 2025-08-26 19:42:03 +02:00
84ce50963b feat(routes/api.py): add API route 2025-08-26 19:41:45 +02:00
3bcffaafab style(style.css): format w/ prettier 2025-08-26 19:38:31 +02:00
e6cc1d901e refactor(panel.html): rewrite panel.html template 2025-08-26 19:37:14 +02:00
9e71270e41 refactor(panel.py): remove unnecesarry args to render_template 2025-08-26 19:36:43 +02:00
1fa355089d fix(panel.py): fix panel route to be at /panel instead of /panel/ 2025-08-26 19:34:55 +02:00
860ce5c836 chore(panel.py): remove unnecesarry imports 2025-08-26 19:34:14 +02:00
055ab10ab7 style(__main__.py): add typing 2025-08-26 19:33:29 +02:00
dbb464e41c feat(__main__.py): call BackendServer.run() 2025-08-26 19:32:53 +02:00
11400520c9 refactor(server.py): fix method naming and add logging 2025-08-26 19:30:50 +02:00
b8f5fdc794 feat(server.py): create a thread for _poll_loop 2025-08-26 19:10:24 +02:00
d8405fa363 fix(server.py): fix placeholder naming to be consistent 2025-08-26 19:09:41 +02:00
c628498637 chore(__main__.py): change log format 2025-08-26 19:08:46 +02:00
786c43ae06 build(uv.lock): add flask-socketio depedency 2025-08-26 19:04:58 +02:00
309efe074b build(pyproject.toml): add flask-socketio depedency 2025-08-26 19:04:43 +02:00
9949179997 style(login.html): format 2025-08-25 22:18:36 +02:00
6a072724f2 style(index.html): format 2025-08-25 22:18:23 +02:00
b79f3f3387 style(details.html): format with prettier 2025-08-25 22:18:06 +02:00
a494b606a5 feat(web_server.py): add WEB_SERVER and BACKEND app.configs 2025-08-25 21:43:15 +02:00
90094e9c43 chore(web_server.py): remove LoginManager from self.app.config 2025-08-25 21:42:38 +02:00
4285da4fa8 feat(web_server.py): add BackendServer reference to webserver 2025-08-25 21:42:00 +02:00
14450ba70d feat(__main__.py): init BackendServer in __main__ 2025-08-25 21:40:44 +02:00
5a4b5ddd82 chore(backend/__init__.py): add BackendServer to __all__ 2025-08-25 21:40:17 +02:00
2964ac7644 feat(backend/server.py): add placeholder backend 2025-08-25 21:39:32 +02:00
1d00d5f4f5 chore(routes/auth.py): make login fail message more ambigiuous 2025-08-25 20:24:57 +02:00
38da1f476e feat(templates/login.html): autofocus password field 2025-08-25 20:22:32 +02:00
c840788907 Merge pull request 'refactor: use OOP' (#6) from refactor/use-oop into develop
Reviewed-on: #6
2025-08-25 17:56:22 +00:00
a3e9aed6f4 chore(web_server.py): add web server to initial loggin to avoid confusion 2025-08-25 19:48:17 +02:00
8ad55eed10 fix(__main__.py): fix wrong import path 2025-08-25 19:40:39 +02:00
77b6bd0732 refactor(web_server.py): rewrite to use OOP 2025-08-25 19:40:11 +02:00
a32df2fd67 chore(templates/stream.html): remove stream.html template 2025-08-25 19:39:32 +02:00
8b418a7d52 refactor(templates/panel.html): adapt urls for blueprints 2025-08-25 19:38:35 +02:00
3e1fe9fa1e refactor(templates/login.html): adapt urls for blueprints 2025-08-25 19:38:22 +02:00
bd9d95b741 refactor(templates/index.html): adapt urls for blueprints 2025-08-25 19:38:12 +02:00
4baf29eb4a refactor(templates/details.html): adapt urls for blueprints 2025-08-25 19:37:58 +02:00
d91f135d53 feat(user.py): add user management 2025-08-25 19:35:16 +02:00
c1e20e5524 feat(routes/__init__.py): add __init__.py 2025-08-25 19:32:24 +02:00
d56256f681 feat(routes/panel.py): add panel.py route blueprint 2025-08-25 19:32:05 +02:00
bbaffb344f feat(routes/auth.py): add auth.py route blueprint 2025-08-25 19:31:55 +02:00
50be48dba4 feat(routes/index.py): add index.py route blueprint 2025-08-25 19:31:42 +02:00
fc5dc23c0e refactor(style.css): use new palette 2025-08-25 19:26:41 +02:00
44df495096 feat(palette.css): add catppuccin color palette 2025-08-25 19:26:13 +02:00
8dbbea3f56 feat(__main__.py): add __main__.py 2025-08-24 22:02:59 +02:00
e575e8a15d refactor(.vscode/launch.json): use __main__.py for running web server 2025-08-24 22:02:39 +02:00
1901441948 chore(py.typed): add py.typed 2025-08-15 20:38:14 +02:00
6bed7da165 build(pyproject.toml): use basic type checking to kinda supress flask_login's nonexistent typing 2025-08-15 20:36:20 +02:00
f8f74ccd4b chore(routes/__init__.py): add __init__.py 2025-08-09 22:47:47 +02:00
5fb6920436 style(.vscode/settings.json): fix indentation 2025-08-09 22:46:45 +02:00
23a53f79a0 fix(web_server.py): fix server error when using /stream 2025-08-09 22:46:06 +02:00
89dd67b489 chore(backend/__init__.py): add __init__.py 2025-08-09 22:44:06 +02:00
c2e1984959 style(web_server.py): fix typing errors 2025-08-09 22:42:19 +02:00
dbc61ac6b7 chore(web/__init__.py): add web/__init__.py 2025-08-09 22:41:43 +02:00
github-actions[bot]
ed91c8d8cb chore(release): 0.1.1 2025-08-09 15:20:50 +00:00
451bdeb618 Merge pull request 'chore(.vscode/launch.json): use shell instead of debugpy for running flask' (#5) from release/0.1.1 into main
Reviewed-on: #5
2025-08-09 15:20:11 +00:00
8b6bf1e913 chore(.vscode/launch.json): use shell instead of debugpy for running flask 2025-08-09 17:19:27 +02:00
github-actions[bot]
00aa0698ae chore(release): 0.1.0 2025-08-09 15:08:42 +00:00
d07900d64a Merge pull request 'chore(release): 0.1.0' (#4) from release/0.1.0 into main
Reviewed-on: #4
2025-08-09 15:03:44 +00:00
ff6eefb668 Merge branch 'build/migrate-to-uv' into develop 2025-08-09 16:56:59 +02:00
816582f61a chore(.vscode/launch.json): fix formatting errors 2025-08-09 15:55:57 +02:00
8459eb836f chore(.vscode/settings.json): set type checking mode to standard 2025-06-16 19:20:01 +02:00
f3c844929e feat(web_server.py): remove redirect when logged in and trying to access / 2025-06-16 17:48:09 +02:00
c41f95497c feat(index.html): add go to panel button if logged in 2025-06-16 17:47:32 +02:00
26352ca54c refactor(index.html): make logout a button 2025-06-16 17:38:15 +02:00
ea0f5f9a83 refactor(details.html): shorten heading text 2025-06-16 17:34:05 +02:00
8243ce2253 feat(style.css): add button container class 2025-06-16 14:14:16 +02:00
9847979f43 feat(style.css): add link class 2025-06-16 14:13:55 +02:00
c0eb4e0a6d feat(style.css): style details table 2025-06-16 14:13:25 +02:00
947adda4da fix(style.css): fix text color on hover 2025-06-16 14:11:31 +02:00
6979fe17e7 refactor(style.css): replace hex with var value 2025-06-16 14:10:00 +02:00
fe82dad557 refactor(details.html): change table and layout of buttons 2025-06-16 14:04:46 +02:00
2fc702a482 chore(index.html): add class link to login element 2025-06-16 14:01:22 +02:00
8c99a28a85 chore(.vscode/launch.json): run flask on 0.0.0.0 2025-06-16 13:50:58 +02:00
7c7e762f00 refactor(panel.html): remove breaking line 2025-06-16 13:49:47 +02:00
5c4441a35f style(style.css): remove empty line 2025-06-16 12:23:26 +02:00
a525f311a3 Merge branch 'feat/improve-login-page' into develop 2025-06-16 12:17:27 +02:00
e07a379036 refactor(web_server.py): display pretty error if password incorrect 2025-06-16 12:14:02 +02:00
ec6ed477b9 feat(login.html): display error if provided 2025-06-16 12:13:11 +02:00
f9e630e49b Merge pull request 'feat: style panel' (#1) from feat/style-panel into develop
Reviewed-on: #1
2025-06-16 12:12:35 +02:00
960a7dd4bf refactor(index.html): put elements in main into divs to not screw up layout 2025-06-16 12:11:11 +02:00
6872348375 feat(panel.html): change bg color based on status 2025-06-16 12:10:39 +02:00
7c98d16cc2 feat(panel.html): add description and classes to table 2025-06-16 12:09:40 +02:00
06ea0246f5 chore(web_server.py): add more pc placeholders 2025-06-16 12:08:21 +02:00
95e40a4128 feat(style.css): add error container class 2025-06-16 12:07:52 +02:00
cde27d37e3 refactor(web_server.py): make status lowercase 2025-06-16 12:07:29 +02:00
3709a42e64 feat(style.css): style panel table 2025-06-16 12:06:42 +02:00
7b09a72d5c feat(style.css): replace colors with var values 2025-06-16 12:05:39 +02:00
1f0fcc71c9 refactor(style.css): make main a flex container 2025-06-16 12:02:35 +02:00
d998b85f9f feat(style.css): make all links pretty 2025-06-16 12:01:37 +02:00
c0f51cbaaf feat(style.css): add nord colors as variables for easy use 2025-06-16 12:01:06 +02:00
f3f521efe2 style(web_server.py): fix type in index() 2025-06-16 10:45:44 +02:00
feb7a71816 refactor(web_server.py): redirect from / to /panel if logged in 2025-06-16 10:45:08 +02:00
e15f02be05 refactor(web_server.py): render login.html instead of bare form in /login 2025-06-16 10:44:00 +02:00
5031799072 feat(login.html): add basic login page 2025-06-16 10:43:18 +02:00
26fac14802 refactor(stream.html): make logout button pretty and fix name in header 2025-06-16 10:39:45 +02:00
75694f9200 refactor(details.html): make logout button pretty and fix name in header 2025-06-16 10:38:54 +02:00
522c14793a refactor(panel.html): make logout button pretty and fix name in header 2025-06-16 10:38:23 +02:00
9a074f17d8 style(index.html): add newline at end of file 2025-06-16 10:37:13 +02:00
6ec1af2cf7 feat(index.html): add pretty typewriter effect to header on main 2025-06-16 10:36:46 +02:00
ff911bc8bc feat(index.html): add welcome text and a disclaimer 2025-06-16 10:36:14 +02:00
446e5fbc04 feat(index.html): add link to homepage to app name in header 2025-06-16 10:34:52 +02:00
04c46f1e98 refactor(style.css): add barebones stylesheet 2025-06-16 10:32:04 +02:00
2786f39b9b chore(.vscode/launch.json): add launch task for flask app 2025-06-16 10:30:16 +02:00
ad479fe0d7 build(pyproject.toml): rename start script to web 2025-06-16 10:28:56 +02:00
34 changed files with 2841 additions and 282 deletions

View File

@@ -5,35 +5,35 @@ on:
inputs: inputs:
short_description: short_description:
type: string type: string
description: 'Short description of the release' description: "Short description of the release"
required: false required: false
auto_bump: auto_bump:
type: boolean type: boolean
description: 'Auto bump version?' description: "Auto bump version?"
required: true required: true
bump_type: bump_type:
type: choice type: choice
description: 'Bump type' description: "Bump type"
required: true required: true
options: options:
- 'major' - "major"
- 'minor' - "minor"
- 'patch' - "patch"
- 'prerelease' - "prerelease"
as_pre_release: as_pre_release:
type: boolean type: boolean
description: 'As pre-release?' description: "As pre-release?"
required: true required: true
prerelease_type: prerelease_type:
type: choice type: choice
description: 'Pre-release label' description: "Pre-release label"
required: true required: true
options: options:
- 'dev' - "dev"
- 'alpha' - "alpha"
- 'beta' - "beta"
- 'rc' - "rc"
- 'post' - "post"
jobs: jobs:
bump_version: bump_version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -44,7 +44,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
token: "${{ secrets.GITHUB_TOKEN }}" token: "${{ secrets.GITHUB_TOKEN }}"
submodules: 'recursive' submodules: "recursive"
- name: 📦 install uv - name: 📦 install uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
@@ -92,9 +92,12 @@ jobs:
echo "Changes:" && cat release_body.md echo "Changes:" && cat release_body.md
uv run git-cliff > CHANGELOG.md uv run git-cliff > CHANGELOG.md
- name: 📦 update lockfile (re-sync)
run: uv sync
- name: 📤 push changes - name: 📤 push changes
run: | run: |
git add CHANGELOG.md pyproject.toml src/*/__init__.py git add CHANGELOG.md pyproject.toml src/*/__init__.py uv.lock
git commit -m "chore(release): ${{ steps.get_version.outputs.BUMPED_VERSION }}" git commit -m "chore(release): ${{ steps.get_version.outputs.BUMPED_VERSION }}"
git push origin main git push origin main
@@ -105,13 +108,14 @@ jobs:
tag_name: ${{ steps.get_version.outputs.BUMPED_VERSION }} tag_name: ${{ steps.get_version.outputs.BUMPED_VERSION }}
body_path: release_body.md body_path: release_body.md
prerelease: ${{ github.event.inputs.as_pre_release }} prerelease: ${{ github.event.inputs.as_pre_release }}
discussion_category_name: 'Announcements' discussion_category_name: "Announcements"
- name: 🧹 cleanup - name: 🧹 cleanup
run: | run: |
rm release_body.md rm release_body.md
- name: 📤 merge into develop - name: 📤 merge into develop
continue-on-error: true
run: | run: |
git checkout develop git checkout develop
git merge main git merge main

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Flask",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/judas_server/__main__.py",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"justMyCode": true
}
]
}

View File

@@ -1,3 +1,4 @@
{ {
"python.analysis.autoImportCompletions": true "python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "standard"
} }

301
CHANGELOG.md Normal file
View File

@@ -0,0 +1,301 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.5.0] - 2026-02-28
### Features
- [`956da02`](https://github.com/pufereq/template-repo/commit/956da024c3bbe88d83f3ef7bfa37e380c22a04d9) **style.css**: make client list elements flexboxes
- [`54eec65`](https://github.com/pufereq/template-repo/commit/54eec657a5a4822f5b1211cbc6843045165fe25d) **style.css**: use monospaced font only for heading
- [`1900bf4`](https://github.com/pufereq/template-repo/commit/1900bf46cc6f103f6e9d359b9fd6af9a74d4cf4d) **client_details.html**: add basic client details
- [`c5771dc`](https://github.com/pufereq/template-repo/commit/c5771dc3718b447878633b252bdd94b3e793aec9) **panel.html**: use client_details.html after clicking on client on sidebar
- [`de9240e`](https://github.com/pufereq/template-repo/commit/de9240e6e0c683643df5bb9495c46cae64b0bc68) **backend_server.py**: set client status to STALE if offline for >24h
- [`b1656cd`](https://github.com/pufereq/template-repo/commit/b1656cdfa95673bb19605d9358cb3c98a69f15d2) **client.py**: redo ClientStatuses adding PENDING and STALE
- [`563de5a`](https://github.com/pufereq/template-repo/commit/563de5aa199a64946d9f41b84c6e2803aff45714) **backend_server.py**: add known_clients.yaml file for storing clients between server restarts
- [`6971548`](https://github.com/pufereq/template-repo/commit/6971548589f098f850896ee49e833c7c1bce5efc) **web_server.py**: use client_details blueprint
- [`31c5157`](https://github.com/pufereq/template-repo/commit/31c51574f7a2b92a959ddf9ae4c8cbf336494725) **client_details.py**: add client_details route
- [`b652db9`](https://github.com/pufereq/template-repo/commit/b652db930f9a299c467b52acb22420e7ca280fb7) **style.css**: style no-connection-icon and client list
- [`1dfddd2`](https://github.com/pufereq/template-repo/commit/1dfddd2fc7f4436010ddad31f50355073c8e4abb) **panel.html**: add a side panel client list
- [`d20ff9b`](https://github.com/pufereq/template-repo/commit/d20ff9be6eeb9e283304ca6d60a4635a2f9b0ab6) **panel.html**: add a no connection icon to header
- [`2bbe118`](https://github.com/pufereq/template-repo/commit/2bbe118de6290b1e014aa9c1a6f5f9d5b134d2e0) rename `details.html` -> `client_details.html`
- [`29b4f3a`](https://github.com/pufereq/template-repo/commit/29b4f3a2ff34493d245eb0eb144dd52b0cc9b134) **css/style.css**: add `.button:active` color
- [`69bf4f1`](https://github.com/pufereq/template-repo/commit/69bf4f13585f06c561c0746966bf95762c188bd0) **css/style.css**: add #content styling, make `main`'s flex column to fit `aside`
- [`563dc62`](https://github.com/pufereq/template-repo/commit/563dc626246f379f1179179faa36d13b019b6a4e) **css/style.css**: include flaticon icons
- [`bb229dc`](https://github.com/pufereq/template-repo/commit/bb229dc724ef9cec4c147c6c3d9f67b28eb700ce) **css/style.css**: make UI more compact
- [`3077a98`](https://github.com/pufereq/template-repo/commit/3077a98d6fe7f34219c861120bc217929dc919d9) **backend_server.py**: add `send_close()` method
### Miscellaneous Tasks
- [`7f904fd`](https://github.com/pufereq/template-repo/commit/7f904fdcd50ac114cc584d7438675d5490b61bcd) **uv.lock**: add pyyaml to depedencies
- [`f11b442`](https://github.com/pufereq/template-repo/commit/f11b442eced4781ee3be0102bd8ea0614bc378c4) **style.css**: style no connection message correctly
- [`72a7433`](https://github.com/pufereq/template-repo/commit/72a74334e74f1a1a73d2ae11dd08e72a8a98b094) **pyproject.toml**: add pyyaml to depedencies
- [`5ca5d9f`](https://github.com/pufereq/template-repo/commit/5ca5d9fcf4e8095b93cac1d72877769064895189) **.vscode/launch.json**: set cwd to workspace folder
- [`0580a6b`](https://github.com/pufereq/template-repo/commit/0580a6be5390eb08a13eec0ce7c41219b1dca21a) **css/style.css**: remove transition from button hover bg color
- [`97221bc`](https://github.com/pufereq/template-repo/commit/97221bc1b71112ac54168138bf5adc10acb920b5) **uv.lock**: update depedencies for Python 3.14
### Refactor
- [`a5e1ba8`](https://github.com/pufereq/template-repo/commit/a5e1ba88aeb06d337587e9dfd0a46520ef6c8d95) adapt for client.id
- [`1e38be5`](https://github.com/pufereq/template-repo/commit/1e38be5ec5d55a30fd715a650ea7402b9fb4c546) **backend_server.py**: adapt client's `id` after refactor
- [`b265feb`](https://github.com/pufereq/template-repo/commit/b265feba7a79ecd418bf8849a68a0633ae7d4778) **client.py**: rename `Client.mac_id` -> `Client.id`
- [`840d9ce`](https://github.com/pufereq/template-repo/commit/840d9ce3c1838a3d88264fe895f933aea6c8cb4f) **login.html**: move elements from `main` to `#content`
- [`9971981`](https://github.com/pufereq/template-repo/commit/9971981f666d4099132235fe0ecfb8e679745d95) **index.html**: move elements from `main` to `#content`
- [`5510e9d`](https://github.com/pufereq/template-repo/commit/5510e9dd0874a1e3448a96eb8b8472b5599458a1) **backend_server.py**: rewrite `_handle_connection()` to minimize indents
- [`1e02da1`](https://github.com/pufereq/template-repo/commit/1e02da185111f0e05651df0bf5c1df735f7dc6ff) **backend_server.py**: rename `_send_ack()` -> `send_ack()`
### Styling
- [`f54d974`](https://github.com/pufereq/template-repo/commit/f54d974745c7a0f5d8db619d86d585709649b8ec) **style.css**: remove commented-out code
### Build
- [`f01eaec`](https://github.com/pufereq/template-repo/commit/f01eaecba346715dd0c50eb4b253656df5c89ad8) **uv.lock**: update judas_protocol to 0.6.0
## [0.4.0] - 2025-11-30
### Bug Fixes
- [`115deaa`](https://github.com/pufereq/template-repo/commit/115deaab4b0aaa9f0d3577be56e64fa3bb03f76b) **backend_server.py**: use unused `packet_size` argument to `_receive_inbound()`
- [`cc6b650`](https://github.com/pufereq/template-repo/commit/cc6b650f5cd1f3889c251fbe7b36c681be48b2a0) **backend_server.py**: import Any from typing
- [`5024862`](https://github.com/pufereq/template-repo/commit/50248621650590c17fcf96c0cec9618a2b0dd07c) **backend_server.py**: add 1ms sleep to prevent 100% CPU usage in `_loop()`
- [`f0eeeb7`](https://github.com/pufereq/template-repo/commit/f0eeeb79a188548c3f306f4bc875bacc6fd89588) **backend_server.py**: handle unregister exceptions
### Features
- [`6f5fa33`](https://github.com/pufereq/template-repo/commit/6f5fa33a122e9ebdf9df91628be67721cfc031e3) **backend_server.py**: handle all other client Exceptions in `_handle_connection()`
### Refactor
- [`8d2b8f9`](https://github.com/pufereq/template-repo/commit/8d2b8f9519bb809df45ba79ea53f12775c3097d4) **backend_server.py**: change log levels
- [`faf1f4e`](https://github.com/pufereq/template-repo/commit/faf1f4eeee341f5f8d234dc7d93ae5c86e276217) **backend_server.py**: remove unused argument in `BackendServer._send_outbound()`
- [`61a607c`](https://github.com/pufereq/template-repo/commit/61a607c20e205c18e60a9b26f6fb3c37a47d6a55) wip
### Styling
- [`f365139`](https://github.com/pufereq/template-repo/commit/f365139e9f064de309f655296b99868833ba7dce) **backend_server.py**: ignore type error in `_handle_connection()`
### Build
- [`fe7d78c`](https://github.com/pufereq/template-repo/commit/fe7d78c1c8cd1d0e13276ce82549ee97b896aff0) **uv.lock**: update judas_protocol to 0.5.0
- [`cee3025`](https://github.com/pufereq/template-repo/commit/cee30251ddb87b1f3ca84d0f3c8cf40fb37debcc) **uv.lock**: update judas_protocol to 0.4.3
- [`721ab87`](https://github.com/pufereq/template-repo/commit/721ab87e7156045703f55ecefc6ed3e15ffa36aa) **uv.lock**: update judas_protocol to 0.3.0
- [`1211ca2`](https://github.com/pufereq/template-repo/commit/1211ca20294dcc3907dbadfee4b294cf7e24a8f9) **uv.lock**: update judas_protocol to 0.2.0
## [0.3.0] - 2025-09-19
### Bug Fixes
- [`b68c755`](https://github.com/pufereq/template-repo/commit/b68c755c450c7b13f95b335fc93810847306708c) **backend_server.py**: send ack on connect
- [`2922123`](https://github.com/pufereq/template-repo/commit/2922123a7050a9438b024067ebad2901adb84fe3) **backend/__init__.py**: fix backend_server import
- [`c0cacfa`](https://github.com/pufereq/template-repo/commit/c0cacfad2d13799d3472a5d033ae15fd668401f9) **backend_server.py**: fix server waiting for closed socket to dissapear by reusing it in `__init__()`
### Documentation
- [`b9a7c2b`](https://github.com/pufereq/template-repo/commit/b9a7c2bdaf4be8dcea4c6f3196a6092f76eac183) **backend_server.py**: add TODO in `_handle_connection()`
### Features
- [`df0b19d`](https://github.com/pufereq/template-repo/commit/df0b19d94361f63d7ae3def51e3f9360242755b7) **routes/api.py**: support mulitple clients correctly
- [`f2b4811`](https://github.com/pufereq/template-repo/commit/f2b4811145a0a45b2a866aefb095f833c8cb50ae) **backend_server.py**: add `BackendServer._send_ack()` helper method
- [`d5985cf`](https://github.com/pufereq/template-repo/commit/d5985cf5945a304505546ef733631bc33d004621) **__main__.py**: display ascii judas splash screen on launch
- [`b9fd84d`](https://github.com/pufereq/template-repo/commit/b9fd84d08f24bb21e51b48b529f62bfe2671c895) **gaga.py**: add Lady Gaga's Born This Way album art ASCII
- [`c0ad91b`](https://github.com/pufereq/template-repo/commit/c0ad91b22f0e011da1021ead1c126a94aaf90f44) **backend_server.py**: re-add the `get_client_data()` method
- [`2ecd32d`](https://github.com/pufereq/template-repo/commit/2ecd32decce95e6aec74433ef3255bc4cf3c3988) **client.py**: add `Client` class
- [`1c96390`](https://github.com/pufereq/template-repo/commit/1c96390f3cade804b2667bb69bec4419703a6d56) **backend_server.py**: add error handling for HELLO message handler in `_accept_connection()`
- [`639e1f7`](https://github.com/pufereq/template-repo/commit/639e1f73a07286bce1a1ef227d216a722e3fa214) **server.py**: add socket-based communication
### Miscellaneous Tasks
- [`54512d8`](https://github.com/pufereq/template-repo/commit/54512d8393a32f59a9aaddf5a52cb18ee9986fdf) **routes/api.py**: adapt `emit_polled_data()` for `Client` class
- [`ac66ce1`](https://github.com/pufereq/template-repo/commit/ac66ce19998a4518a346b6608645486121cd2123) **backend_server.py**: set `Client.last_seen` on message receive
### Refactor
- [`0d074ad`](https://github.com/pufereq/template-repo/commit/0d074adc0d441428baac6858ff57717f3164cf6a) **backend_server.py**: use `_send_ack()` in `_handle_connection()`
- [`ac77992`](https://github.com/pufereq/template-repo/commit/ac7799213cba82dbb189919873130647d7c87a78) **__main__.py**: provide `host` and `port` args to `BackendServer` explicitly
- [`b5670e5`](https://github.com/pufereq/template-repo/commit/b5670e5d2cf08a031b99478de57aa53cba78bd5b) **server.py**: rename `server.py` to `backend_server.py` for clarity
### Styling
- [`6bbb152`](https://github.com/pufereq/template-repo/commit/6bbb152ae1b1178aa77cf54c6e0f544a0fed66ef) **panel.py**: order imports
- [`b14541e`](https://github.com/pufereq/template-repo/commit/b14541ea8302679091b519e3b6f630fabc0ce476) **index.py**: order imports
- [`7d36e6e`](https://github.com/pufereq/template-repo/commit/7d36e6e43132a687d0494f14f36d9b4dc357abaf) **auth.py**: order imports
- [`98c49b1`](https://github.com/pufereq/template-repo/commit/98c49b1d0d9bb7653a12c7558b5e6daf62a7762b) **api.py**: order imports
- [`377a846`](https://github.com/pufereq/template-repo/commit/377a846c5982be129724a84b4b77a6fb69fd03df) **client.py**: order imports
- [`a27b5b1`](https://github.com/pufereq/template-repo/commit/a27b5b1275fadfb2049fa5e5c535b6667ae37ab5) **backend_server.py**: order imports
- [`19aa80a`](https://github.com/pufereq/template-repo/commit/19aa80a08ebe65685209678b5daf061ffbf19840) **__main__.py**: order imports
- [`f7f6f19`](https://github.com/pufereq/template-repo/commit/f7f6f19808b19ae95bc361d00123a13c4afd344d) **__main__.py**: organize imports
- [`cafef5e`](https://github.com/pufereq/template-repo/commit/cafef5ed932ea5456260398b44e42d71702a386a) **backend_server.py**: remove unused imports
### Build
- [`66db11c`](https://github.com/pufereq/template-repo/commit/66db11c40b34d5315eda2a7dc19cd8ae428ae862) add ruff lint depedency
- [`d96fec0`](https://github.com/pufereq/template-repo/commit/d96fec0222bbf707bc1aa48a6d215dbfec93f159) **web_server.py**: order imports
- [`a1b28fa`](https://github.com/pufereq/template-repo/commit/a1b28faa9ff30ddf9da4e882c0bbac27f4863480) add isort as lint depedency
- [`4e16a70`](https://github.com/pufereq/template-repo/commit/4e16a70174a4492ec59a2ff356deddd758e3e6e1) **uv.lock**: add depedency on judas_protocol
- [`4aa2ca4`](https://github.com/pufereq/template-repo/commit/4aa2ca426c2ae3ac29724f70aa9a07e7eb5a723b) **pyproject.toml**: add depedency on judas_protocol
## [0.2.0] - 2025-08-26
### Bug Fixes
- [`1fa3550`](https://github.com/pufereq/template-repo/commit/1fa355089df36404fc5128620674f8a535e6d2c5) **panel.py**: fix panel route to be at `/panel` instead of `/panel/`
- [`d8405fa`](https://github.com/pufereq/template-repo/commit/d8405fa36376a29b1f5ae99f571f2adc8a6cc978) **server.py**: fix placeholder naming to be consistent
- [`8ad55ee`](https://github.com/pufereq/template-repo/commit/8ad55eed10dcc7d2fb849f3643dc805903f560a7) **__main__.py**: fix wrong import path
- [`23a53f7`](https://github.com/pufereq/template-repo/commit/23a53f79a073d4c88756a347312340f59d31b0da) **web_server.py**: fix server error when using /stream
### Features
- [`9f74246`](https://github.com/pufereq/template-repo/commit/9f74246157717216b7687b0ca5cb453fba60405e) **web_server.py**: use socketio
- [`84ce509`](https://github.com/pufereq/template-repo/commit/84ce50963b7811c45e0c7fe94a1e66d71da70060) **routes/api.py**: add API route
- [`dbb464e`](https://github.com/pufereq/template-repo/commit/dbb464e41ccac68854a30084122aaa7659272081) **__main__.py**: call `BackendServer.run()`
- [`b8f5fdc`](https://github.com/pufereq/template-repo/commit/b8f5fdc794fe831386fb74e5ecb872f9ea118ec2) **server.py**: create a thread for _poll_loop
- [`a494b60`](https://github.com/pufereq/template-repo/commit/a494b606a5bf796685b1c0757c69d63d62e5e8a2) **web_server.py**: add `WEB_SERVER` and `BACKEND` app.configs
- [`4285da4`](https://github.com/pufereq/template-repo/commit/4285da4fa8be01e29041ec31bdf451d90f3e9825) **web_server.py**: add BackendServer reference to webserver
- [`14450ba`](https://github.com/pufereq/template-repo/commit/14450ba70df1b6d7effeb66407db2a4f03846404) **__main__.py**: init BackendServer in __main__
- [`2964ac7`](https://github.com/pufereq/template-repo/commit/2964ac76442425ab0078366cbe0f4162bab87638) **backend/server.py**: add placeholder backend
- [`38da1f4`](https://github.com/pufereq/template-repo/commit/38da1f476e3509d8c57b1c0a298e21bc1536c6d4) **templates/login.html**: autofocus password field
- [`d91f135`](https://github.com/pufereq/template-repo/commit/d91f135d5361ab91c55c43303e4eb2ae8e688ea2) **user.py**: add user management
- [`c1e20e5`](https://github.com/pufereq/template-repo/commit/c1e20e55245fa199d494453c9adf59addd232b86) **routes/__init__.py**: add __init__.py
- [`d56256f`](https://github.com/pufereq/template-repo/commit/d56256f6816b6f0043791bcb288264554cd68cf0) **routes/panel.py**: add panel.py route blueprint
- [`bbaffb3`](https://github.com/pufereq/template-repo/commit/bbaffb344fe0d519d2c535dc61902514acb3db9a) **routes/auth.py**: add auth.py route blueprint
- [`50be48d`](https://github.com/pufereq/template-repo/commit/50be48dba4e895a73b09c9af32fa27268df7561a) **routes/index.py**: add index.py route blueprint
- [`44df495`](https://github.com/pufereq/template-repo/commit/44df49509691505d9ec749395bc330a23e72fd38) **palette.css**: add catppuccin color palette
- [`8dbbea3`](https://github.com/pufereq/template-repo/commit/8dbbea3f56894753edcf7acdd0961634fcac8732) **__main__.py**: add __main__.py
### Miscellaneous Tasks
- [`89b6c1e`](https://github.com/pufereq/template-repo/commit/89b6c1ee835f7bf7cc8511683a0637f893daec25) **release.yaml**: update with latest pufereq/python-template release.yaml
- [`860ce5c`](https://github.com/pufereq/template-repo/commit/860ce5c836ec459b6ae70adf5583d9f5b6f8858f) **panel.py**: remove unnecesarry imports
- [`c628498`](https://github.com/pufereq/template-repo/commit/c628498637ab2a3d3554ee1e80a9872ecfd0d5e0) **__main__.py**: change log format
- [`90094e9`](https://github.com/pufereq/template-repo/commit/90094e9c4380c9f516e1134b75f13fac775d9cd0) **web_server.py**: remove LoginManager from `self.app.config`
- [`5a4b5dd`](https://github.com/pufereq/template-repo/commit/5a4b5ddd82bce3de8198dd2955c71edf5625d131) **backend/__init__.py**: add BackendServer to __all__
- [`1d00d5f`](https://github.com/pufereq/template-repo/commit/1d00d5f4f5cecb5c7e6b65347a29b3bfdfa48ddd) **routes/auth.py**: make login fail message more ambigiuous
- [`a3e9aed`](https://github.com/pufereq/template-repo/commit/a3e9aed6f4caecf2b6c707ae5b87f395e788ccc3) **web_server.py**: add `web server` to initial loggin to avoid confusion
- [`a32df2f`](https://github.com/pufereq/template-repo/commit/a32df2fd67a038b7bf147e83cbcca96498b89d58) **templates/stream.html**: remove stream.html template
- [`1901441`](https://github.com/pufereq/template-repo/commit/190144194887865716977acc51971323ab0e0bdf) **py.typed**: add py.typed
- [`f8f74cc`](https://github.com/pufereq/template-repo/commit/f8f74ccd4b27c9dc85c58b76d2328e9e7a288403) **routes/__init__.py**: add __init__.py
- [`89dd67b`](https://github.com/pufereq/template-repo/commit/89dd67b4897cfda2af9387d0f3bd6cdaa531d85e) **backend/__init__.py**: add __init__.py
- [`dbc61ac`](https://github.com/pufereq/template-repo/commit/dbc61ac6b73e821ba3ba6987bda68d8b84089aec) **web/__init__.py**: add web/__init__.py
### Refactor
- [`e6cc1d9`](https://github.com/pufereq/template-repo/commit/e6cc1d901e2eaaef2c65e9cf02ee4c6b1fd2d425) **panel.html**: rewrite panel.html template
- [`9e71270`](https://github.com/pufereq/template-repo/commit/9e71270e41aaa4e241e82d1688a29d1a9823fed4) **panel.py**: remove unnecesarry args to `render_template`
- [`1140052`](https://github.com/pufereq/template-repo/commit/11400520c984bb6efb85f3a6ed8acfd2a5cfe177) **server.py**: fix method naming and add logging
- [`77b6bd0`](https://github.com/pufereq/template-repo/commit/77b6bd07327cfe9750b18bcc7fcdb980b127ab27) **web_server.py**: rewrite to use OOP
- [`8b418a7`](https://github.com/pufereq/template-repo/commit/8b418a7d52c0a32d967da735bc6c519cf2808804) **templates/panel.html**: adapt urls for blueprints
- [`3e1fe9f`](https://github.com/pufereq/template-repo/commit/3e1fe9fa1e53a0376b2073fd208470bfb57ea1b0) **templates/login.html**: adapt urls for blueprints
- [`bd9d95b`](https://github.com/pufereq/template-repo/commit/bd9d95b741e1bc032d0fa7c816f802ce678b5645) **templates/index.html**: adapt urls for blueprints
- [`4baf29e`](https://github.com/pufereq/template-repo/commit/4baf29eb4afba7cf2cfab4e0ab5853d0c94c6ed1) **templates/details.html**: adapt urls for blueprints
- [`fc5dc23`](https://github.com/pufereq/template-repo/commit/fc5dc23c0e222db257f7fb55d0b878db5c3a255d) **style.css**: use new palette
- [`e575e8a`](https://github.com/pufereq/template-repo/commit/e575e8a15dd36616ac0d722e13e90a2df59af8b1) **.vscode/launch.json**: use __main__.py for running web server
### Styling
- [`3bcffaa`](https://github.com/pufereq/template-repo/commit/3bcffaafab8ed9bd2f5cf0380d560ea1dc70616b) **style.css**: format w/ prettier
- [`055ab10`](https://github.com/pufereq/template-repo/commit/055ab10ab702094c8a07f8e5317098e0f95eaffa) **__main__.py**: add typing
- [`9949179`](https://github.com/pufereq/template-repo/commit/99491799970e28ec488dee0f9958ea885d5b7383) **login.html**: format
- [`6a07272`](https://github.com/pufereq/template-repo/commit/6a072724f293e52d0d9eaef5ca70b92be05c115b) **index.html**: format
- [`b79f3f3`](https://github.com/pufereq/template-repo/commit/b79f3f3387b15726a2b42fb17f2355fdb97a4a4a) **details.html**: format with prettier
- [`5fb6920`](https://github.com/pufereq/template-repo/commit/5fb69204361379dab44c426c61a170baa1364280) **.vscode/settings.json**: fix indentation
- [`c2e1984`](https://github.com/pufereq/template-repo/commit/c2e198495930f667eb8d777299dfcf2e83f88301) **web_server.py**: fix typing errors
### Build
- [`786c43a`](https://github.com/pufereq/template-repo/commit/786c43ae06f0550d589e74c94b14cd1a9b1136c6) **uv.lock**: add `flask-socketio` depedency
- [`309efe0`](https://github.com/pufereq/template-repo/commit/309efe074b24d9964dd7d2b767e85d533d526006) **pyproject.toml**: add `flask-socketio` depedency
- [`6bed7da`](https://github.com/pufereq/template-repo/commit/6bed7da165f3c361d938d52ba94b3e2e15894d76) **pyproject.toml**: use basic type checking to kinda supress flask_login's nonexistent typing
## [0.1.1] - 2025-08-09
### Miscellaneous Tasks
- [`8b6bf1e`](https://github.com/pufereq/template-repo/commit/8b6bf1e91366d76ee3203b508b895e4b75d7f845) **.vscode/launch.json**: use shell instead of debugpy for running flask
## [0.1.0] - 2025-08-09
### Bug Fixes
- [`947adda`](https://github.com/pufereq/template-repo/commit/947adda4dac96dba545b795fbb71038642c8fe9a) **style.css**: fix text color on hover
### Documentation
- [`c6e9df1`](https://github.com/pufereq/template-repo/commit/c6e9df1f075ed8b997dd9034afcf6e3e281c48a3) **CODE_OF_CONDUCT.md**: add CoC
### Features
- [`f3c8449`](https://github.com/pufereq/template-repo/commit/f3c844929e585cff746e45077f5e4a77474b2722) **web_server.py**: remove redirect when logged in and trying to access `/`
- [`c41f954`](https://github.com/pufereq/template-repo/commit/c41f95497cc84b03350579bd6dd692734b9f7f89) **index.html**: add go to panel button if logged in
- [`8243ce2`](https://github.com/pufereq/template-repo/commit/8243ce22531cd3e80a8e71b0b120f36169d4e391) **style.css**: add button container class
- [`9847979`](https://github.com/pufereq/template-repo/commit/9847979f43c779af107193128133dae4b3e94070) **style.css**: add link class
- [`c0eb4e0`](https://github.com/pufereq/template-repo/commit/c0eb4e0a6dc2bb1d31c73ded8d90d3a110d18427) **style.css**: style details table
- [`ec6ed47`](https://github.com/pufereq/template-repo/commit/ec6ed477b9053007c23597518e8d1eb6adddf1aa) **login.html**: display error if provided
- [`95e40a4`](https://github.com/pufereq/template-repo/commit/95e40a412887493477035faa7e17b3cd5a5e7e39) **style.css**: add error container class
- [`d998b85`](https://github.com/pufereq/template-repo/commit/d998b85f9f103c472dc27eb6dc95614dcdcd8e4a) **style.css**: make all links pretty
- [`c0f51cb`](https://github.com/pufereq/template-repo/commit/c0f51cbaaf56ec931b5a577effd8190eb0d0cf04) **style.css**: add nord colors as variables for easy use
- [`6872348`](https://github.com/pufereq/template-repo/commit/6872348375270906d39b7154b2571baa504575e2) **panel.html**: change bg color based on status
- [`7c98d16`](https://github.com/pufereq/template-repo/commit/7c98d16cc2391c0ab22a9727887fa66d29b19b20) **panel.html**: add description and classes to table
- [`3709a42`](https://github.com/pufereq/template-repo/commit/3709a42e6416632daa9194d0346bbd7106686660) **style.css**: style panel table
- [`7b09a72`](https://github.com/pufereq/template-repo/commit/7b09a72d5cafc783e70d6f407627a2273a6b2c33) **style.css**: replace colors with var values
- [`5031799`](https://github.com/pufereq/template-repo/commit/503179907249ab8c50ed35d7f5181f7cbc24ef53) **login.html**: add basic login page
- [`6ec1af2`](https://github.com/pufereq/template-repo/commit/6ec1af2cf773bcd8bf9b3e64d639bc839c1b2802) **index.html**: add pretty typewriter effect to header on main
- [`ff911bc`](https://github.com/pufereq/template-repo/commit/ff911bc8bc3427262af8872efb96232c1ece5595) **index.html**: add welcome text and a disclaimer
- [`446e5fb`](https://github.com/pufereq/template-repo/commit/446e5fbc04ad9f7303e2327e859dd6be992d3d5b) **index.html**: add link to homepage to app name in header
- [`368769e`](https://github.com/pufereq/template-repo/commit/368769e1a7a5c12bcf9130245fba4b3e4d9b28e5) **stream.html**: add stream page template
- [`6ba91e5`](https://github.com/pufereq/template-repo/commit/6ba91e5ba6ad4aa2993abafb84677ac8ec689217) **panel.html**: add panel page template
- [`b125a30`](https://github.com/pufereq/template-repo/commit/b125a3008f8871a8f6dbe2c3fad8ca883aea7a12) **index.html**: add index page template
- [`e4620c8`](https://github.com/pufereq/template-repo/commit/e4620c81b2cbbedeb5decb192995494562a36f09) **details.html**: add details page template
- [`1b39dce`](https://github.com/pufereq/template-repo/commit/1b39dce6986e62ea7be5424f1656772b70fcc620) **style.css**: add stylesheet
- [`9edae70`](https://github.com/pufereq/template-repo/commit/9edae70618fae15d3ba138c09427c069cddb1465) **web_server.py**: add web_server.py
### Miscellaneous Tasks
- [`b4b0e91`](https://github.com/pufereq/template-repo/commit/b4b0e9145075d30e3c06258756541ffac5de4766) **release.yaml**: add release workflow
- [`a5c1490`](https://github.com/pufereq/template-repo/commit/a5c1490aca2328999b1c675320eec95052b0242e) **.gitignore**: expand gitignore
- [`816582f`](https://github.com/pufereq/template-repo/commit/816582f61a551dbc2efdfabb7bfdba711f5743cb) **.vscode/launch.json**: fix formatting errors
- [`8459eb8`](https://github.com/pufereq/template-repo/commit/8459eb836f040305612cafbb977ad803fc7db181) **.vscode/settings.json**: set type checking mode to standard
- [`2fc702a`](https://github.com/pufereq/template-repo/commit/2fc702a482380bd56ffc6c3a0769fca06569aebd) **index.html**: add class link to login element
- [`8c99a28`](https://github.com/pufereq/template-repo/commit/8c99a28a8528e21928cc7d8535c5119d08535fb4) **.vscode/launch.json**: run flask on 0.0.0.0
- [`06ea024`](https://github.com/pufereq/template-repo/commit/06ea0246f51742060ce4051d609dbce7d716fdf8) **web_server.py**: add more pc placeholders
- [`2786f39`](https://github.com/pufereq/template-repo/commit/2786f39b9baf850a8438fb483a0346b8cd1ad1b7) **.vscode/launch.json**: add launch task for flask app
- [`1ab8bb8`](https://github.com/pufereq/template-repo/commit/1ab8bb866d6bec2787f76443b76e84a2a7865398) **.vscode/settings.json**: add auto-import completion settings
- [`324c404`](https://github.com/pufereq/template-repo/commit/324c4046f7f0c9ceab6b839be754abf8ac198130) initial commit
### Refactor
- [`40d0edd`](https://github.com/pufereq/template-repo/commit/40d0edd5071f1562a3a980ac4801124ff072e877) move `judas_server/` into `src/judas_server/`
- [`26352ca`](https://github.com/pufereq/template-repo/commit/26352ca54c763a83a6dde3ca49b5479e145c8380) **index.html**: make logout a button
- [`ea0f5f9`](https://github.com/pufereq/template-repo/commit/ea0f5f9a834eaed2f0b530fc83e2ce80f7406e3d) **details.html**: shorten heading text
- [`6979fe1`](https://github.com/pufereq/template-repo/commit/6979fe17e7f252a5168e9e53ec4fd085d488eaac) **style.css**: replace hex with var value
- [`fe82dad`](https://github.com/pufereq/template-repo/commit/fe82dad557773a3dd29871863c81dbbd634cbd1f) **details.html**: change table and layout of buttons
- [`7c7e762`](https://github.com/pufereq/template-repo/commit/7c7e762f0041f7eb28768dd934e14e0ea345d21b) **panel.html**: remove breaking line
- [`e07a379`](https://github.com/pufereq/template-repo/commit/e07a379036649903bad2f92acfd47b9e2dee2c4c) **web_server.py**: display pretty error if password incorrect
- [`960a7dd`](https://github.com/pufereq/template-repo/commit/960a7dd4bf936ad58c29b013c2b5a7673f13945e) **index.html**: put elements in main into divs to not screw up layout
- [`1f0fcc7`](https://github.com/pufereq/template-repo/commit/1f0fcc71c91d70973233adf293a3bf4a2b7b41b4) **style.css**: make main a flex container
- [`cde27d3`](https://github.com/pufereq/template-repo/commit/cde27d37e3319ccc6b370f0628c96437a42ace69) **web_server.py**: make status lowercase
- [`feb7a71`](https://github.com/pufereq/template-repo/commit/feb7a7181626cf58586c27ea198ae0ac5f7e03f3) **web_server.py**: redirect from `/` to `/panel` if logged in
- [`e15f02b`](https://github.com/pufereq/template-repo/commit/e15f02be05f4c22672ea0f61df7f57ceabd96907) **web_server.py**: render login.html instead of bare form in `/login`
- [`26fac14`](https://github.com/pufereq/template-repo/commit/26fac14802df69884cc0329ffa06aa10faffebd3) **stream.html**: make logout button pretty and fix name in header
- [`75694f9`](https://github.com/pufereq/template-repo/commit/75694f920073d283bed137989f6b56a4b23ff9f7) **details.html**: make logout button pretty and fix name in header
- [`522c147`](https://github.com/pufereq/template-repo/commit/522c14793aa26cb6e6a8e272dfa514c247c65518) **panel.html**: make logout button pretty and fix name in header
- [`04c46f1`](https://github.com/pufereq/template-repo/commit/04c46f1e982f5dd14e91972dbcb54039bc0b1286) **style.css**: add barebones stylesheet
### Styling
- [`5c4441a`](https://github.com/pufereq/template-repo/commit/5c4441a35f136c0c56120a8397de209d8067d25f) **style.css**: remove empty line
- [`f3f521e`](https://github.com/pufereq/template-repo/commit/f3f521efe241635c3995680cf579ba95798a13a1) **web_server.py**: fix type in `index()`
- [`9a074f1`](https://github.com/pufereq/template-repo/commit/9a074f17d87986d69d8f8a0b7367a436aa59be1c) **index.html**: add newline at end of file
### Build
- [`de463b8`](https://github.com/pufereq/template-repo/commit/de463b8c713c87c1d37a19976fcb7432f0021401) **pyproject.toml**: make pyproject.toml compatible with uv
- [`30d9434`](https://github.com/pufereq/template-repo/commit/30d94348d7cdba23efe76303d42e39064cd2eefb) **cliff.toml**: add git-cliff config
- [`8968d7e`](https://github.com/pufereq/template-repo/commit/8968d7ec081df328a02a0dc3d4cf7643b19accf2) **pdm.lock**: remove pdm lockfile
- [`ad479fe`](https://github.com/pufereq/template-repo/commit/ad479fe0d7ab87edf37101d1234d5ba26f22b585) **pyproject.toml**: rename `start` script to `web`
- [`04d4e97`](https://github.com/pufereq/template-repo/commit/04d4e97f626fe3ddbbed1ab6586da40a439567ab) **pyproject.toml**: add flask start script
- [`9acc788`](https://github.com/pufereq/template-repo/commit/9acc788b34181c448ed08b2903bb97d14cf00e66) **pdm.lock**: add pdm lockfile
- [`7de428c`](https://github.com/pufereq/template-repo/commit/7de428c4757fcecfa1a6a2582ce042324433a67f) **pyproject.toml**: add flask and flask-login
<!-- generated by git-cliff -->

0
cache/.gitkeep vendored Normal file
View File

View File

@@ -4,16 +4,26 @@ build-backend = "uv_build"
[project] [project]
name = "judas_server" name = "judas_server"
version = "0.1.0" version = "0.5.0"
description = "The backbone of the remote PC fleet management system." description = "The backbone of the remote PC fleet management system."
readme = "README.md" readme = "README.md"
authors = [] authors = []
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = ["flask>=3.1.1", "flask-login>=0.6.3"] dependencies = [
"flask>=3.1.1",
"flask-login>=0.6.3",
"flask-socketio>=5.5.1",
"judas-protocol",
"pyyaml>=6.0.3",
]
license = { text = "GPL-3.0+" } license = { text = "GPL-3.0+" }
[dependency-groups] [dependency-groups]
bump = ["git-cliff>=2.9.1", "python-semantic-release>=10.2.0"] bump = ["git-cliff>=2.9.1", "python-semantic-release>=10.2.0"]
lint = [
"isort>=6.0.1",
"ruff>=0.13.1",
]
test = [ test = [
"pytest>=4.2.1", "pytest>=4.2.1",
"pytest-cov>=6.2.1", "pytest-cov>=6.2.1",
@@ -21,6 +31,9 @@ test = [
"pytest-mock>=3.14.1", "pytest-mock>=3.14.1",
] ]
[tool.basedpyright]
typeCheckingMode = "basic"
[tool.ruff] [tool.ruff]
line-length = 79 line-length = 79
exclude = ["tests/*"] exclude = ["tests/*"]
@@ -78,3 +91,6 @@ allowed_tags = [
minor_tags = ["feat"] minor_tags = ["feat"]
patch_tags = ["fix", "perf"] patch_tags = ["fix", "perf"]
default_bump_level = 0 default_bump_level = 0
[tool.uv.sources]
judas-protocol = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git" }

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
import logging as lg
if __name__ == "__main__":
from judas_server.backend import BackendServer
from judas_server.gaga import LADY_GAGA
from judas_server.web.web_server import JudasWebServer
lg.basicConfig(
level=lg.DEBUG,
format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s",
)
ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA")
ladygaga_logger.info(LADY_GAGA)
backend_server: BackendServer = BackendServer(
host="0.0.0.0",
port=3692,
)
backend_server.run()
web_server: JudasWebServer = JudasWebServer(
backend=backend_server, secret_key="dildo"
)
web_server.run(
host="0.0.0.0",
port=5000,
)

View File

@@ -0,0 +1,5 @@
from .backend_server import BackendServer
from .client import Client
from .client_status import ClientStatus
__all__ = ["BackendServer", "Client", "ClientStatus"]

View File

@@ -0,0 +1,358 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
import selectors
import socket
import threading
import time
from typing import TYPE_CHECKING, Any
import yaml
from judas_protocol import Category, ControlAction, Message
from judas_server.backend.client import Client, ClientStatus
from judas_server.backend.handler.hello_handler import HelloHandler
if TYPE_CHECKING:
from typing import Callable
from judas_protocol import ActionType
class BackendServer:
def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
"""Initialize the backend server.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug("Initializing Server...")
self.selector = selectors.DefaultSelector()
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
)
self._bind_socket(host, port)
self.server_socket.listen()
self.server_socket.setblocking(False)
self.selector.register(
self.server_socket, selectors.EVENT_READ, data=None
)
self.clients: dict[str, Client] = {}
self.known_clients: dict[str, dict[str, str | float]] = (
self._load_known_clients()
)
self.message_handlers: dict[
tuple[Category, ActionType], Callable[[Client, Message], None]
] = {}
self._initialize_handlers()
self.running: bool = False
def _initialize_handlers(self) -> None:
"""Initialize message handlers."""
hello_handler = HelloHandler(self)
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
hello_handler.handle
)
def _load_known_clients(self) -> dict[str, dict[str, str | float]]:
"""Load the list of known clients from a YAML file and validate."""
known_clients: dict[str, dict[str, str | float]] = {}
try:
with open("cache/known_clients.yaml", "r") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
raise ValueError("YAML root must be a dict")
known_clients = data.get("known_clients", {}) or {}
if not isinstance(known_clients, dict):
raise ValueError("'known_clients' must be a dict")
for client_id, client_data in known_clients.items():
if not isinstance(client_data, dict):
raise ValueError(
f"Client {client_id} data must be a dict"
)
last_seen = client_data.get("last_seen", 0.0)
if not isinstance(last_seen, (float, int)):
raise ValueError(
f"Client {client_id} 'last_seen' must be a float or int"
)
self.logger.debug(f"Loaded known clients: {known_clients}")
self.logger.info(f"Loaded {len(known_clients)} known clients")
for client_id in known_clients:
client = Client(id=client_id, addr=None, socket=None)
client.status = ClientStatus.OFFLINE
client.last_seen = float(
known_clients[client_id].get("last_seen", 0.0)
)
self.clients[client_id] = client
except FileNotFoundError:
self.logger.warning(
"known_clients.yaml not found, creating empty known clients list"
)
with open("cache/known_clients.yaml", "w") as f:
yaml.safe_dump({"known_clients": {}}, f)
except Exception as e:
self.logger.error(f"Error loading known clients: {e}")
raise
return known_clients
def _save_known_clients(self) -> None:
"""Save the list of known clients to a YAML file."""
with open("cache/known_clients.yaml", "w") as f:
yaml.safe_dump({"known_clients": self.known_clients}, f)
self.logger.debug("Saved known clients")
def _bind_socket(self, host: str, port: int) -> None:
"""Bind the server socket to the specified host and port.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger.debug(f"Binding socket to {host}:{port}")
while True:
try:
self.server_socket.bind((host, port))
self.logger.debug(f"Socket bound to {host}:{port}")
break
except OSError as e:
self.logger.error(
f"Failed to bind socket to {host}:{port}, retrying...: {e}"
)
time.sleep(1)
def send_ack(self, client: Client, target_id: str) -> None:
"""Send an ACK message to a client.
Args:
client (Client): The client to send the ACK to.
target_id (str): The id of the ACK'd message.
"""
ack: bytes = Message.Control.ack(target_id=target_id).to_bytes()
self.logger.info(f"[>] Sending ACK to {client}")
client.outbound += ack
def send_close(self, client: Client) -> None:
"""Send a CLOSE message to a client.
Args:
client (Client): The client to send the CLOSE message to.
"""
close_msg: bytes = Message.Control.close().to_bytes()
self.logger.info(f"[>] Sending CLOSE to {client}")
client.outbound += close_msg
def _accept_connection(self, sock: socket.socket) -> None:
"""Accept a new client connection.
Args:
sock (socket.socket): The selected socket.
"""
conn, addr = sock.accept()
self.logger.info(f"[+] Accepted connection from {addr}")
client = Client(id=None, addr=addr, socket=conn)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
self.selector.register(conn, events, data=client)
self.logger.info(f"[+] Registered client {client}, HELLO pending...")
def _disconnect(self, client: Client) -> None:
"""Disconnect a client and clean up resources.
Args:
sock (socket.socket): The client socket to disconnect.
"""
self.logger.info(f"[-] Disconnecting {client}...")
if client.socket is None:
self.logger.warning(
f"Client {client} has no socket, nothing to disconnect."
)
return
try:
self.selector.unregister(client.socket)
except Exception as e:
self.logger.error(f"Error unregistering client {client}: {e}")
client.disconnect()
self._save_known_clients()
def _send_outbound(self, sock: socket.socket, client: Client) -> None:
"""Queue data to be sent to a client.
Args:
client (Client): The client to send data to.
"""
self.logger.debug(f"[>] Sending data to {client}: {client.outbound!r}")
sent = sock.send(client.outbound)
client.outbound = client.outbound[sent:]
def _receive_inbound(
self, sock: socket.socket, client: Client, packet_size: int = 4096
) -> None:
"""Receive data from a client socket.
Args:
sock (socket.socket): The client socket to receive data from.
client (Client): The client object.
packet_size (int): The maximum amount of data to be received at once.
Returns:
bytes: The received data.
"""
recv_data = sock.recv(packet_size)
if recv_data:
self.logger.debug(
f"[<] Received data from {client}: {recv_data!r}"
)
client.inbound += recv_data
# set last seen
client.last_seen = time.time()
else:
self._disconnect(client)
def _handle_connection(
self, key: selectors.SelectorKey, mask: int
) -> None:
"""Handle a client connection.
Args:
key (selectors.SelectorKey): The selector key for the client.
mask (int): The event mask.
"""
sock: socket.socket = key.fileobj # type: ignore
client = key.data
try:
if mask & selectors.EVENT_READ:
self._receive_inbound(sock, client)
if not client.inbound:
self._disconnect(client)
return
while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1)
self.logger.debug(
f"[<] Complete message from {client}: {line!r}"
)
try:
msg = Message.from_bytes(line)
self.logger.info(f"[.] Parsed message {msg.id}")
if client.id is None:
self.logger.debug(
f"Client {client} has no ID, expecting HELLO message..."
)
if (
msg.category != Category.CONTROL
or msg.action != ControlAction.HELLO
):
self.logger.warning(
f"First message from {client} must be HELLO, disconnecting..."
)
self._disconnect(client)
continue
handler: Callable[[Client, Message], None] | None = (
self.message_handlers.get(
(msg.category, msg.action), None
)
)
if handler is not None:
handler(client, msg)
else:
self.logger.warning(
f"No handler for message {msg.id} with category {msg.category} and action {msg.action}"
)
continue
if msg.ack_required:
self.send_ack(client, target_id=msg.id)
except Exception as e:
self.logger.error(
f"Failed to parse message from {client}: {e}"
)
return
if mask & selectors.EVENT_WRITE and client.outbound:
self._send_outbound(sock, client)
except ConnectionResetError as e:
self.logger.error(f"Connection reset by {client}, disconnect: {e}")
self._disconnect(client)
except Exception as e:
self.logger.error(f"Connection error for {client}: {e}")
self._disconnect(client)
def run(self) -> None:
"""Start the backend server."""
self.running = True
threading.Thread(
name="BackendServer thread", target=self._loop, daemon=True
).start()
def _loop(self) -> None:
"""Main server loop to handle incoming connections and data."""
self.logger.info("Server is running...")
try:
while self.running:
events = self.selector.select(timeout=1)
for key, mask in events:
if key.data is None:
self._accept_connection(key.fileobj) # type: ignore
else:
self._handle_connection(key, mask)
# update client statuses
now = time.time()
for client in self.clients.values():
if (
client.status == ClientStatus.OFFLINE
and now - client.last_seen > 60 * 60 * 24 # 24 hours
):
self.clients[client.id].status = ClientStatus.STALE
time.sleep(0.001) # prevent 100% CPU usage
except Exception as e:
self.logger.error(f"Server error: {e}")
raise e
finally:
self.selector.close()
self.server_socket.close()
self.logger.info("Server has stopped.")
def get_client_data(self, client_id: str) -> dict[str, Any] | None:
client: Client | None = self.clients.get(client_id, None)
if client is None:
self.logger.warning(f"Client {client_id} not found")
return None
return {
"id": client.id,
"addr": client.addr,
"last_seen": client.last_seen,
"status": client.status,
}

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""Client representation."""
from __future__ import annotations
import logging as lg
import socket
import time
from judas_server.backend.client_status import ClientStatus
class Client:
"""Represents a client."""
def __init__(
self,
id: str | None,
addr: tuple[str, int] | None,
socket: socket.socket | None,
) -> None:
"""Initialize the client.
Args:
id (str | None): The unique identifier for the client.
Can be None if not yet assigned.
addr (tuple[str, int]): The (IP, port) address of the client.
socket (socket.socket): The socket object for communication.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug(f"Initializing Client {addr}...")
self.id: str | None = id
self.last_seen: float = 0.0 # unix timestanp of last inbound message
self.status: ClientStatus = ClientStatus.PENDING
self.socket: socket.socket | None = socket
self.addr: tuple[str, int] | None = addr
self.inbound: bytes = b""
self.outbound: bytes = b""
def __str__(self) -> str:
if self.addr:
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
return f"Client({self.id} (not connected))"
def __repr__(self) -> str:
return f"Client({self.id}, {self.addr})"
def disconnect(self) -> None:
"""Disconnect the client and close the socket."""
self.logger.debug(f"Disconnecting Client {self}...")
if self.socket is None:
self.logger.warning(
f"Client {self} not connected, nothing to disconnect."
)
return
try:
self.socket.close()
except Exception as e:
self.logger.error(f"Error closing socket for Client {self}: {e}")
self.status = ClientStatus.OFFLINE
self.last_seen = time.time()
self.logger.info(f"Client {self} disconnected.")

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from enum import Enum
class ClientStatus(str, Enum):
"""Enumeration of client connection statuses."""
ONLINE = "online"
PENDING = "pending"
OFFLINE = "offline"
STALE = "stale"

View File

@@ -0,0 +1,4 @@
from .base_handler import BaseHandler
from .hello_handler import HelloHandler
__all__ = ["BaseHandler", "HelloHandler"]

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
from typing import TYPE_CHECKING
from judas_server.backend.client import Client
if TYPE_CHECKING:
from judas_protocol import Message
from judas_server.backend import BackendServer
class BaseHandler:
"""BaseHandler is the base class for all message handlers in the backend server.
It defines the interface for handling messages and provides common functionality for all handlers.
"""
def __init__(self, backend_server: BackendServer) -> None:
"""Initialize the BaseHandler with a reference to the backend server.
Args:
backend_server (BackendServer): The backend server instance that this handler belongs to.
"""
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.backend_server: BackendServer = backend_server
def handle(self, client: Client, message: Message) -> None:
"""Handle a message from a client.
This method must be implemented by subclasses to define the specific handling logic for different message types.
Args:
client (Client): The client that sent the message.
message (Message): The message to be handled.
"""
raise NotImplementedError("handle() must be implemented by subclasses")

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING, override
from judas_protocol import Category, ControlAction, Message
from judas_server.backend.client import ClientStatus
from judas_server.backend.handler import BaseHandler
if TYPE_CHECKING:
from judas_server.backend.backend_server import BackendServer
from judas_server.backend.client import Client
class HelloHandler(BaseHandler):
def __init__(self, backend_server: BackendServer) -> None:
super().__init__(backend_server)
@override
def handle(self, client: Client, message: Message) -> None:
if client.id is not None:
return
if (
message.category != Category.CONTROL
or message.action != ControlAction.HELLO
):
self.logger.error(
f"Expected HELLO message from {client}, got {message}, disconnecting client..."
)
self.backend_server._disconnect(client)
return
if message.payload.get("id") is None:
self.logger.error(
f"HELLO message from {client} missing 'id' field, disconnecting client..."
)
self.backend_server._disconnect(client)
return
client.id = message.payload["id"]
# check if client already connected, if so disconnect old client and register new one
if (
client.id in self.backend_server.clients
and self.backend_server.clients[client.id].status == "connected"
):
old_client: Client = self.backend_server.clients[client.id]
self.backend_server.logger.warning(
f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..."
)
self.backend_server.send_close(old_client)
return
self.backend_server.clients[client.id] = client # type: ignore
self.backend_server.known_clients[client.id] = { # type: ignore
"last_seen": client.last_seen
}
self.backend_server._save_known_clients()
client.status = ClientStatus.ONLINE
self.logger.info(f"[+] Registered new client {client}")

43
src/judas_server/gaga.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""ASCII art of Lady Gaga's Born This Way album cover."""
from judas_server import __version__
LADY_GAGA: str = """
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢔⢑⠔⡁⡂⢅⠢⠃⢆⢊⠜⡩⠳⣕⣙⡪⡪⡢⣫⢺⢔⢵⢱⢕⢮⢮⢺⡢⢕⠕⡽⣘⢜⠳⣌⢢⠀⠀⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢰⣶⣆⢰⠰⡄⢢⣶⣶⢐⠂⡑⡐⢆⢊⠔⠌⢌⢂⠢⠡⢂⠅⢍⢲⣉⡙⢜⡘⢎⣗⢵⢕⡕⡧⡳⡹⣮⣫⢺⢼⡪⣮⡳⡜⡮⡙⠤⠄⢂⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠈⠉⢽⡫⠉⡏⡏⠉⠍⡏⡉⢋⡍⣇⢕⢨⡈⡔⡠⠁⠕⡀⡈⠄⠠⡀⠍⠜⠜⡪⣚⡎⢏⢯⢞⡽⣺⡺⡮⡷⣝⣗⢽⢽⣽⢽⢶⢦⡨⠌⢔⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡠⠨⡪⡹⡸⡑⡕⡑⢄⠕⠄⠔⡈⠜⡈⡂⡂⢅⠁⢂⢅⡪⡨⠒⡜⡾⣘⡪⡣⡻⣷⡻⣯⢿⡺⣗⡿⣽⢽⡽⣞⡽⡫⢷⣆⠑⡄⢀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⡀⠄⡕⣪⢞⢜⠌⡎⢜⠔⡡⢊⢰⢌⠎⡐⢄⢎⢢⡁⣂⠥⣌⢪⢪⡱⡹⡵⣎⢮⢯⡸⢝⣾⡽⣞⣿⠺⢝⣯⣿⢯⡳⡝⣄⠊⠻⢬⡢⣑⡀⠀⠀
⠀⠀⠀⠀⠀⠀⠠⡐⣼⣪⢯⢽⠢⡃⢊⠊⠜⢬⡢⣟⢮⡣⣕⢵⣱⢵⢵⣳⢯⡷⣯⣞⣞⡮⡯⣗⡯⡪⡫⣎⢷⣻⣿⣺⡿⣼⡿⣝⢽⡷⣝⢆⠁⠀⠈⠝⣖⢦⠀⠀
⠀⠀⠀⠀⢀⠨⠰⡱⡵⣯⢿⡝⢌⢐⠀⠂⠨⡠⢹⣺⡵⣳⢕⣯⢾⣻⡯⣿⣻⣿⣽⣾⣟⣿⣯⢿⡽⣳⢵⢕⡯⡿⣾⡮⣿⡿⣮⡫⣷⡻⣟⣧⡑⡐⢀⠐⡸⣕⡇⠠
⠀⠀⡀⠐⠄⠅⢕⢝⣽⣻⣯⡇⢅⠢⣁⡊⠆⢜⣮⡺⡽⣺⢽⣞⣿⣽⣟⣯⣿⣾⢿⣾⣿⣽⣾⣿⡽⣯⢿⢽⣯⣻⡽⣿⣟⣿⡸⡹⣷⣯⣟⣯⣷⡄⡂⠀⡍⡖⡷⠀
⠐⠀⠀⠨⠀⡘⡌⡇⡗⣟⡾⡊⢔⠅⡆⡌⡪⡘⣮⣞⢿⢽⢽⣺⡿⣾⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⡽⣯⣟⡾⣜⣿⣿⢽⣺⣇⠢⡙⣽⣾⣟⣿⣟⣆⠢⡸⡵⢼⣂
⠀⠀⢀⠠⠁⢜⠌⢌⠪⢪⡫⣎⢆⢇⢧⠪⡐⡘⡮⡿⡽⠹⡙⢕⣛⢿⣻⣿⢿⣻⣽⣿⣿⢿⣿⠿⣟⣿⢵⣷⣻⣪⣾⣟⡿⣯⣷⢑⢸⢸⡽⣿⣜⡿⣟⣞⠁⣳⢽⢯
⠀⠀⠀⠠⢨⢪⢈⠄⡑⡑⣕⢇⢇⢇⢧⢣⡓⣌⠊⡁⠀⠀⡈⢄⠀⠛⡾⡽⣻⣿⣻⣻⣺⠋⠁⠁⠀⠀⢏⢺⣾⣼⡏⡾⣿⣽⣾⢯⡢⡱⡱⣻⣧⢻⣿⣿⡜⣜⢿⡧
⠀⠀⡈⡌⡌⡂⡢⢐⢈⠆⡣⢪⢮⢣⡣⢇⢯⡪⡲⣐⠐⠝⡌⣎⠾⠦⠭⡎⣗⣷⡃⢧⠥⠺⠪⢅⠝⠂⢹⣨⣷⣿⣳⢝⣽⣿⣽⣯⣗⣗⢕⢯⣿⡸⣿⣿⢿⣇⣻⣇
⢠⢧⣫⢵⢕⢗⡊⢆⠢⡊⠔⣹⢪⣳⢱⢩⢇⢯⡳⣝⢽⣳⣖⣶⢽⡯⣗⢕⣗⣟⡮⡪⣺⣻⣷⣶⢶⡿⣽⣳⣿⣿⣧⣛⣾⣿⣷⣟⣷⣯⣳⡺⣿⡥⢼⣿⣿⣷⢸⣷
⢯⢿⢽⣝⢵⢱⣱⠵⡑⡌⢎⡞⡕⡵⡱⡑⡭⡳⡹⡮⣳⢳⣟⣾⣿⣯⡟⡮⣞⣯⣿⢜⠮⣷⣿⣾⣿⣿⣿⣯⣿⡿⣿⣶⢫⣿⣿⣽⣿⣷⣗⢿⣿⡗⡒⣿⣿⣻⣿⣿
⣻⡫⣳⢕⡷⡝⡎⡮⡪⡊⢢⠱⡱⢱⢱⠱⡸⡸⣕⢝⢮⡳⣻⣿⣷⣿⢽⡺⣻⡿⣟⢷⢽⣽⣿⣷⣿⠿⣽⣾⢿⣿⣯⣿⡯⣿⣿⢿⣿⣿⣿⣟⣿⣷⡐⡼⣿⡯⣿⣿
⡒⡕⡇⡯⡪⡎⡭⡪⡒⢌⠂⢕⠸⡨⡣⢣⠱⡱⡱⡳⣹⡪⣳⣿⣿⣻⡯⣟⣶⣿⣿⣽⣟⣾⣿⣿⣿⢿⣿⢯⣻⣿⣿⣿⣿⣻⣿⣯⢿⣿⣿⣿⣿⣿⣷⢏⢿⣿⢿⣿
⡈⡖⡝⣼⢝⢜⡸⡸⡨⡠⡑⡅⢇⠎⣊⢪⢊⢎⢪⢺⢸⢜⢮⢿⣯⡯⠏⡣⡑⡥⠣⠐⡨⠘⢾⢿⣿⣟⡿⣯⣻⣿⣿⢿⣯⣿⣿⣯⢯⢿⣿⣻⣿⡷⣿⣗⢽⢽⢿⣿
⠌⣮⡾⡳⡕⢝⡔⣕⢎⣪⢪⢪⢢⢃⢎⢎⡮⡺⣨⢳⣹⢸⢸⡻⣗⡏⠌⣶⣶⣾⣿⣿⡷⡕⠸⣟⣯⣿⣿⣳⢯⣿⣿⡿⣿⢷⣻⣿⣝⢝⣽⣿⣿⣟⡽⣾⡕⠝⡯⣿
⡨⡮⡳⡩⡸⡲⣹⣵⣟⡮⡺⢸⡰⡫⣸⢜⢮⢺⡸⡸⡜⡌⢎⢎⢗⠕⡨⢒⢜⢭⢗⢕⡰⡜⢼⢿⣿⣻⡾⣽⣎⣿⣿⣿⣽⢿⣗⢿⣽⡮⡪⣾⣟⡷⡍⢿⣣⢘⢮⡿
⠹⡜⡑⡬⡲⡽⣯⣷⢟⢔⢜⡴⡵⢏⢇⣣⢗⡇⢇⠎⡎⡎⡎⡜⢬⡣⠀⣧⣷⣽⣮⣯⡦⡩⣸⡿⣽⣯⢿⠕⢜⣽⣿⣽⡾⡯⣿⡞⣞⡯⡷⡿⣽⣟⠌⣞⣿⢨⢑⣿
⠮⡱⡱⣱⢽⣽⢿⢕⣯⠢⡃⡣⢑⣔⣗⠯⢓⠨⢢⢣⠣⡓⣝⢮⡘⣞⢄⠄⣽⠚⡙⠡⡈⣢⣷⢿⣻⡽⠃⢹⢔⠼⣾⢿⣿⡹⡵⣿⢺⡪⢪⡻⡮⣗⠧⡱⣿⡄⣫⡗
⢽⢸⡹⣕⣟⡞⣕⢯⣯⡇⡂⡢⢓⠌⡢⢨⢂⢕⠕⢅⠣⡊⢎⢗⢳⢸⣝⣗⣷⣶⣶⣷⣿⣟⣿⣻⢝⡮⡅⢂⠯⡪⣯⣿⢿⣎⢺⣽⣯⣻⣜⢜⢜⢜⣱⢱⣯⣏⢷⡯
⣏⢮⡪⡞⣃⢂⢪⢽⡾⣯⢂⢊⠢⡑⢅⠇⡊⠢⡑⢅⠇⢊⠔⣕⡕⡏⣿⡽⣿⣿⣿⣿⣟⣿⢝⡮⣷⢙⢌⠢⡩⡯⣷⣻⣿⣿⣔⢵⣿⡺⣟⣜⢮⢪⢎⡮⣾⣇⢽⡣
⢾⡫⡫⡊⡖⡜⡎⣕⢏⠯⣇⠪⡪⡂⡅⢂⠂⢕⠱⡑⡁⠅⡗⢱⣝⣾⡳⣹⢻⡯⡷⢿⢻⢱⣝⣞⢝⢴⡱⠀⢈⢹⣳⢽⢾⣻⣷⢕⢽⣿⢽⣞⢽⢺⢜⢮⣻⢮⢺⡸
⣣⣣⡳⣵⢽⣺⢽⣳⢱⡩⡪⡪⣐⠹⡸⡲⡸⡨⠬⠨⢰⢰⡱⡵⢻⢺⡺⣵⢻⣜⢮⡳⣽⢵⡣⣇⠣⡵⣫⡂⠰⠨⡷⣻⢽⣻⡾⣿⡸⣿⡽⣗⡝⡧⡳⡱⣻⢯⡣⣗
⣾⣺⡽⡯⣿⣝⡽⡪⡪⡂⢊⠪⡢⢣⠨⡂⣕⣔⡬⠮⠫⠓⠉⣐⢨⠚⡙⡎⡷⣕⣗⢯⡳⣝⢕⢕⣕⢯⠃⠈⡬⡨⡏⡽⣝⢷⣻⣽⣟⡾⣻⣽⣎⢯⡲⡸⣽⢗⣇⠧
⣳⢿⢽⢭⢺⣺⡪⡇⡗⡈⠢⡨⡨⡸⡸⣚⠮⡲⠭⠳⠒⠘⠉⠀⠀⠁⠀⡀⠱⣳⢽⣹⢹⢜⢕⢝⣜⠊⡀⠐⠀⢪⢻⡜⣝⢽⢽⣾⣻⣿⣽⡷⣗⢷⠱⡕⡽⢧⡳⣝
⡽⡽⡱⢱⢽⢮⣻⢊⢮⠠⠄⡀⠈⠀⠀⠀⠀⠈⠨⠀⠀⡀⠀⠀⠀⠀⠂⠀⠈⢘⢓⠕⠕⠡⠁⠁⠂⠀⠀⠀⠀⠀⢏⢷⡸⡜⡜⣷⡯⣷⣿⣽⣯⠫⠣⡯⣣⡣⡯⣞
⡎⠣⣝⣮⣟⣯⢷⡱⡑⠄⠀⠀⠑⠂⠆⢄⠀⠀⠀⠀⠀⠀⠄⢀⠀⠀⠀⠀⠉⠀⠠⠀⠂⠡⠀⠀⠀⠀⠀⠀⠀⠀⠨⢝⢷⡱⡱⢽⣻⢽⡾⣗⣯⢝⢌⡽⣧⠾⡽⣞
⠀⠈⢚⣞⡾⡯⡳⡕⡹⣆⠀⠄⡀⠀⠀⠀⠈⠑⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡀⠂⠯⣷⢝⢎⡯⡗⣿⣻⢽⠍⠭⠚⢽⡕⣰⢝
"""
# add centered subtitle
width: int = LADY_GAGA.index("\n", 1)
subtitle: str = f"judas_server {__version__}"
padding: int = (width - len(subtitle)) // 2
LADY_GAGA += " " * padding + subtitle + "\n"

View File

View File

View File

@@ -0,0 +1,5 @@
from .auth import auth_bp
from .index import index_bp
from .panel import panel_bp
__all__ = ["auth_bp", "index_bp", "panel_bp"]

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
bp: flask.Blueprint = flask.Blueprint("api", __name__, url_prefix="/api")
@bp.route("/client/<client_id>", methods=["GET"])
@flask_login.login_required
def get_client_data(client_id: str) -> tuple[Response, int]:
"""API endpoint to get client data by ID.
Args:
client_id (str): The ID of the client.
Returns:
Response: JSON response with client data or error message.
"""
backend = flask.current_app.config["BACKEND"]
data = backend.get_client_data(client_id)
if data is None:
return flask.jsonify({"error": "Client not found"}), 404
return flask.jsonify(data), 200
@bp.route("/clients", methods=["GET"])
@flask_login.login_required
def list_clients() -> tuple[Response, int]:
"""API endpoint to list all clients.
Returns:
Response: JSON response with list of client IDs.
"""
backend = flask.current_app.config["BACKEND"]
client_ids = list(backend.clients.keys())
return flask.jsonify({"clients": client_ids}), 200
def emit_polled_data(app, socketio):
backend = app.config["BACKEND"]
def poll_loop():
import time
while True:
data = {}
for client_id in backend.clients.keys():
data[client_id] = backend.get_client_data(client_id)
socketio.emit("update_data", data)
time.sleep(1)
threading.Thread(name="Socketio", target=poll_loop, daemon=True).start()

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
from judas_server.web.user import User
if TYPE_CHECKING:
from werkzeug.wrappers import Response
auth_bp: flask.Blueprint = flask.Blueprint(
"auth", __name__, url_prefix="/auth"
)
@auth_bp.route("/login", methods=["GET", "POST"])
def login() -> Response | str:
"""Handles user login via password form."""
if flask.request.method == "POST":
password = flask.request.form.get("password", "")
if password == flask.current_app.config["PASSWORD"]:
user = User("admin")
flask_login.login_user(user)
next_page = flask.request.args.get("next")
return flask.redirect(next_page or flask.url_for("index.index"))
# return flask.redirect(flask.url_for("panel.panel"))
else:
return flask.render_template(
"login.html",
error="Invalid credentials.",
)
return flask.render_template("login.html")
@auth_bp.route("/logout")
@flask_login.login_required
def logout() -> Response:
"""Logs out the current user."""
flask_login.logout_user()
return flask.redirect(flask.url_for("index.index"))

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from judas_server.backend import BackendServer
from judas_server.backend.client import Client
bp: flask.Blueprint = flask.Blueprint(
"client_details", __name__, url_prefix="/client"
)
@bp.route("/<client_id>")
@flask_login.login_required
def client_details(client_id: str) -> str:
"""Renders the client details page for a specific client.
Args:
client_id: The ID of the client to display details for.
"""
backend: BackendServer = flask.current_app.config["BACKEND"]
client: Client | None = backend.clients.get(client_id)
if not client:
flask.abort(404, description="Client not found")
return flask.render_template("client_details.html", client=client)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
index_bp: flask.Blueprint = flask.Blueprint("index", __name__)
@index_bp.route("/")
def index() -> Response | str:
"""Renders the index page."""
return flask.render_template(
"index.html", logged=flask_login.current_user.is_authenticated
)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask
import flask_login
panel_bp: flask.Blueprint = flask.Blueprint(
"panel", __name__, url_prefix="/panel"
)
@panel_bp.route("")
@flask_login.login_required
def panel() -> str:
"""Renders the main panel page with PC details.
Returns:
Rendered HTML template with PC details.
"""
return flask.render_template("panel.html")

View File

@@ -0,0 +1,80 @@
:root {
--ctp-rosewater: #f5e0dc;
--ctp-rosewater-rgb: 245 224 220;
--ctp-rosewater-hsl: 9.600 55.556% 91.176%;
--ctp-flamingo: #f2cdcd;
--ctp-flamingo-rgb: 242 205 205;
--ctp-flamingo-hsl: 0.000 58.730% 87.647%;
--ctp-pink: #f5c2e7;
--ctp-pink-rgb: 245 194 231;
--ctp-pink-hsl: 316.471 71.831% 86.078%;
--ctp-mauve: #cba6f7;
--ctp-mauve-rgb: 203 166 247;
--ctp-mauve-hsl: 267.407 83.505% 80.980%;
--ctp-red: #f38ba8;
--ctp-red-rgb: 243 139 168;
--ctp-red-hsl: 343.269 81.250% 74.902%;
--ctp-maroon: #eba0ac;
--ctp-maroon-rgb: 235 160 172;
--ctp-maroon-hsl: 350.400 65.217% 77.451%;
--ctp-peach: #fab387;
--ctp-peach-rgb: 250 179 135;
--ctp-peach-hsl: 22.957 92.000% 75.490%;
--ctp-yellow: #f9e2af;
--ctp-yellow-rgb: 249 226 175;
--ctp-yellow-hsl: 41.351 86.047% 83.137%;
--ctp-green: #a6e3a1;
--ctp-green-rgb: 166 227 161;
--ctp-green-hsl: 115.455 54.098% 76.078%;
--ctp-teal: #94e2d5;
--ctp-teal-rgb: 148 226 213;
--ctp-teal-hsl: 170.000 57.353% 73.333%;
--ctp-sky: #89dceb;
--ctp-sky-rgb: 137 220 235;
--ctp-sky-hsl: 189.184 71.014% 72.941%;
--ctp-sapphire: #74c7ec;
--ctp-sapphire-rgb: 116 199 236;
--ctp-sapphire-hsl: 198.500 75.949% 69.020%;
--ctp-blue: #89b4fa;
--ctp-blue-rgb: 137 180 250;
--ctp-blue-hsl: 217.168 91.870% 75.882%;
--ctp-lavender: #b4befe;
--ctp-lavender-rgb: 180 190 254;
--ctp-lavender-hsl: 231.892 97.368% 85.098%;
--ctp-text: #cdd6f4;
--ctp-text-rgb: 205 214 244;
--ctp-text-hsl: 226.154 63.934% 88.039%;
--ctp-subtext1: #bac2de;
--ctp-subtext1-rgb: 186 194 222;
--ctp-subtext1-hsl: 226.667 35.294% 80.000%;
--ctp-subtext0: #a6adc8;
--ctp-subtext0-rgb: 166 173 200;
--ctp-subtext0-hsl: 227.647 23.611% 71.765%;
--ctp-overlay2: #9399b2;
--ctp-overlay2-rgb: 147 153 178;
--ctp-overlay2-hsl: 228.387 16.757% 63.725%;
--ctp-overlay1: #7f849c;
--ctp-overlay1-rgb: 127 132 156;
--ctp-overlay1-hsl: 229.655 12.775% 55.490%;
--ctp-overlay0: #6c7086;
--ctp-overlay0-rgb: 108 112 134;
--ctp-overlay0-hsl: 230.769 10.744% 47.451%;
--ctp-surface2: #585b70;
--ctp-surface2-rgb: 88 91 112;
--ctp-surface2-hsl: 232.500 12.000% 39.216%;
--ctp-surface1: #45475a;
--ctp-surface1-rgb: 69 71 90;
--ctp-surface1-hsl: 234.286 13.208% 31.176%;
--ctp-surface0: #313244;
--ctp-surface0-rgb: 49 50 68;
--ctp-surface0-hsl: 236.842 16.239% 22.941%;
--ctp-base: #1e1e2e;
--ctp-base-rgb: 30 30 46;
--ctp-base-hsl: 240.000 21.053% 14.902%;
--ctp-mantle: #181825;
--ctp-mantle-rgb: 24 24 37;
--ctp-mantle-hsl: 240.000 21.311% 11.961%;
--ctp-crust: #11111b;
--ctp-crust-rgb: 17 17 27;
--ctp-crust-hsl: 240.000 22.727% 8.627%;
}

View File

@@ -1,20 +1,263 @@
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-rounded/css/uicons-regular-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-straight/css/uicons-regular-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-rounded/css/uicons-solid-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-straight/css/uicons-solid-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-bold-rounded/css/uicons-bold-rounded.css");
@import "palette.css";
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
}
body {
background-color: var(--ctp-base);
font-family: sans-serif;
color: var(--ctp-text);
}
.fi {
vertical-align: middle;
}
input {
font-family: inherit;
color: #eceff4;
background-color: var(--ctp-crust);
border: none;
border-radius: 0.25rem;
padding: 0.25rem;
}
a {
color: var(--ctp-blue);
text-decoration: none;
}
#wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
} }
header { header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: #2e3440; background-color: var(--ctp-mantle);
color: #eceff4; padding: 0.5rem 1rem;
padding: 1rem; }
header h2 {
font-family: monospace, sans-serif;
} }
main { main {
padding: 2rem; display: flex;
background-color: #eceff4; flex-grow: 1;
color: #2e3440; }
aside {
width: 20rem;
background-color: var(--ctp-base);
border-right: 2px solid var(--ctp-mantle);
}
#content {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 1rem;
}
header a {
text-decoration: none;
/* color: var(--nord-fg0); */
}
.button {
display: inline-block;
padding: 0.25rem 0.5rem;
background-color: var(--ctp-lavender);
color: var(--ctp-base);
border: none;
border-radius: 0.25rem;
text-decoration: none;
}
.button:hover {
background-color: var(--ctp-mauve);
cursor: pointer;
}
.button:active {
background-color: var(--ctp-sapphire);
}
.center {
text-align: center;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1rem;
padding: 1rem;
background-color: var(--ctp-surface0);
color: var(--ctp-red);
border: 6px solid var(--ctp-red);
border-radius: 24px;
}
.center-table {
margin: 0 auto;
width: 100%;
}
.select-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface0);
}
.select-table thead {
position: sticky;
top: -1px;
z-index: 2;
}
.select-table th,
.select-table td {
padding: 0.5rem;
text-align: center;
border: 1px solid var(--ctp-surface0);
border-collapse: collapse;
}
.select-table th {
background-color: var(--ctp-surface1);
color: var(--nord-fg0);
}
.select-table a {
display: block;
text-decoration: none;
transition: 0.1s ease-in-out;
}
.select-table tr {
transition: 0.1s ease-in-out;
}
.select-table tr:hover {
background-color: var(--ctp-surface0);
}
.select-table tr:hover a {
color: var(--ctp-base);
}
.red-bg {
background-color: var(--ctp-red);
}
.yellow-bg {
background-color: var(--ctp-yellow);
}
.green-bg {
background-color: var(--ctp-green);
}
.button-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.link {
transition: 0.3s ease-in-out;
}
.link:hover {
color: var(--ctp-blue);
}
.details-table tr td:first-child {
font-weight: 900;
background-color: var(--ctp-surface0);
white-space: nowrap;
width: 1%;
}
.details-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface1);
}
.details-table th,
.details-table td {
padding: 0.5rem;
text-align: left;
border: 1px solid var(--ctp-text);
border-collapse: collapse;
}
#notify {
display: none;
position: fixed;
top: 3rem;
right: 50%;
transform: translateX(50%);
background-color: var(--ctp-surface1);
padding: 1rem 2rem;
border: 4px solid var(--ctp-text);
border-radius: 1rem;
opacity: 0.8;
}
#no-connection-message {
color: var(--ctp-red);
}
#no-connection-message i {
padding-right: 0.5rem;
font-size: 1.5rem;
vertical-align: middle;
color: var(--ctp-red);
}
ul#client-list {
list-style: none;
display: flex;
flex-direction: column;
}
ul#client-list li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.5rem;
cursor: default;
}
ul#client-list li:hover {
background-color: var(--ctp-surface0);
}
ul#client-list li i {
line-height: 0;
}
ul#client-list li a {
display: block;
width: 100%;
color: var(--ctp-text);
text-decoration: none;
cursor: default;
} }

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Details for client {{ client.id }}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
</head>
<body>
<h1>Details for client {{ client.id }}</h1>
<p><strong>ID:</strong> {{ client.id }}</p>
<p><strong>IP Address:</strong> {{ client.addr[0] }}</p>
<p><strong>Port:</strong> {{ client.addr[1] }}</p>
<p>
<strong>Last Seen:</strong>
<span id="last-seen">{{ client.last_seen }}</span>
</p>
</body>
<script>
function updateLastSeen() {
const lastSeenElement = document.getElementById("last-seen");
const lastSeenTimestamp = parseInt(lastSeenElement.textContent);
const lastSeenDate = new Date(lastSeenTimestamp * 1000);
lastSeenElement.textContent = lastSeenDate.toLocaleString();
}
updateLastSeen();
</script>
</html>

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas panel - details</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('panel') }}">judas panel</a></h1>
<p>Welcome, {{ username }}! <a href="{{ url_for('logout') }}">Logout</a></p>
</header>
<main>
<h2>Details for PC ID: {{ pc.id }}</h2>
<h3><a href="{{ url_for('stream', pc_id=pc.id) }}">View Stream</a></h3>
<table border="1">
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Status</td>
<td>{{ pc.status }}</td>
</tr>
<tr>
<td>Last Seen</td>
<td>{{ pc.last_seen }}</td>
</tr>
</tbody>
</table>
<p><a href="{{ url_for('panel') }}">Back to Panel</a></p>
</main>
</div>
</body>
</html>

View File

@@ -1,21 +1,56 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas</title> <title>judas</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<header> <header>
<h1>judas</h1> <h2><a href="{{ url_for('index.index') }}">judas</a></h2>
{% if logged %} {% if logged %}
<p>Welcome, {{ username }}! <a href="{{ url_for('logout') }}">Logout</a></p> <p><a class="button" href="{{ url_for('auth.logout') }}">Logout</a></p>
{% else %} {% else %}
<p><a href="{{ url_for('login') }}">Login</a></p> <p><a class="button" href="{{ url_for('auth.login') }}">Login</a></p>
{% endif %} {% endif %}
</header> </header>
</div> <main class="center">
<div id="content">
<div>
<p>Welcome to</p>
<h2 id="typing-text" style="font-size: 3rem;">judas</h2>
<p>a remote PC fleet management system</p>
</div>
<p style="color: var(--ctp-red);"><strong>Notice:</strong> Please use this system responsibly and in accordance with all applicable laws and organizational policies.</p>
{% if logged %}
<p><a class="button" href="{{ url_for('panel.panel') }}">Go to panel</a></p>
{% else %}
<p>Please <a href="{{ url_for('auth.login')}}" class="link">log in</a> to manage your remote PCs.</p>
{% endif %}
</div>
</main>
</div>
<script>
var i = 0;
var txt = document.getElementById("typing-text").innerHTML;
var minSpeed = 50;
var maxSpeed = 200;
document.getElementById("typing-text").innerHTML = "";
function typeWriter() {
if (i < txt.length) {
document.getElementById("typing-text").innerHTML += txt.charAt(i);
i++;
var randomDelay = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
setTimeout(typeWriter, randomDelay);
}
}
typeWriter();
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas - login page</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="wrapper">
<header>
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
</header>
<main>
<div id="content">
<h1>Login</h1>
<form method="post" class="center">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required autofocus>
<br>
<br>
<input type="submit" value="Login" class="button">
{% if error %}
<div class="error-container">
<h1>Login failure</h1>
<p>{{ error }}</p>
</div>
{% endif %}
</form>
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,41 +1,203 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>judas panel</title> <title>judas panel</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link
</head> rel="stylesheet"
<body> href="{{ url_for('static', filename='css/style.css') }}"
/>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
$(document).ready(function () {
const socket = io();
const showNotify = (message) => {
$("#notify").stop().fadeIn();
$("#notify").text(message);
setTimeout(() => {
$("#notify").fadeOut();
}, 3000);
};
const loadClientDetails = (clientId) => {
fetch(`/client/${clientId}`)
.then((response) => response.text())
.then((html) => {
$("#content").html(html);
})
.catch((error) => {
console.error("Error fetching client details:", error);
});
};
// load client_details for the client specified in the URL hash
const hash = window.location.hash;
if (hash) {
const clientId = hash.substring(1);
loadClientDetails(clientId);
}
$("#notify").hide();
socket.on("connect", () => {
console.log("Connected to server");
$("#no-connection-message").hide();
showNotify("Connected to server");
});
socket.on("disconnect", () => {
console.log("Disconnected from server");
$("#no-connection-message").show();
showNotify("Disconnected from server");
});
socket.on("update_data", (data) => {
const clientList = $("#client-list");
const existingItems = {};
// Index current <li> by clientId
clientList.children("li").each(function () {
const a = $(this).find("a");
if (a.length) {
const clientId = a.attr("href").substring(1);
existingItems[clientId] = $(this);
}
});
// Track which clientIds are still present
const seen = new Set();
Object.entries(data).forEach(([clientId, client]) => {
seen.add(clientId);
// Build icon
const iconElement = document.createElement("i");
switch (client.status) {
case "online":
iconElement.className = "fi fi-sr-play";
iconElement.style.color = "var(--ctp-green)";
break;
case "offline":
iconElement.className = "fi fi-sr-stop";
iconElement.style.color = "var(--ctp-red)";
break;
case "pending":
iconElement.className = "fi fi-sr-pending";
iconElement.style.color = "var(--ctp-yellow)";
break;
case "stale":
iconElement.className = "fi fi-sr-skull";
iconElement.style.color = "var(--ctp-orange)";
break;
default:
iconElement.className = "fi fi-rr-question";
iconElement.style.color = "var(--ctp-gray)";
}
// Time since last seen
const lastSeen = new Date(client.last_seen * 1000);
const now = new Date();
const timeSinceLastSeen = Math.floor((now - lastSeen) / 1000);
const days = Math.floor(timeSinceLastSeen / 86400);
const hours = Math.floor((timeSinceLastSeen % 86400) / 3600);
const minutes = Math.floor((timeSinceLastSeen % 3600) / 60);
const seconds = timeSinceLastSeen % 60;
let timeSinceLastSeenText = "";
if (days > 0) timeSinceLastSeenText += `${days}d `;
if (hours > 0) timeSinceLastSeenText += `${hours}h `;
if (minutes > 0) timeSinceLastSeenText += `${minutes}m `;
if (seconds > 0) timeSinceLastSeenText += `${seconds}s`;
timeSinceLastSeenText = timeSinceLastSeenText.trim() || "0s";
let statusText = `${client.id} (${timeSinceLastSeenText})`;
// check if <li> exists
if (existingItems[clientId]) {
// update if needed
const li = existingItems[clientId];
const a = li.find("a");
if (a.text() !== statusText) {
a.text(statusText);
}
li.attr(
"title",
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
);
// update icon
const icon = li.find("i")[0];
if (icon) {
icon.className = iconElement.className;
icon.style.color = iconElement.style.color;
}
} else {
// add new <li>
const li = $("<li></li>");
li.append(iconElement);
const a = $("<a></a>")
.text(statusText)
.attr("href", `#${clientId}`);
li.attr(
"title",
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
);
li.append(a);
clientList.append(li);
}
});
// Remove <li> for clients no longer present
Object.keys(existingItems).forEach((clientId) => {
if (!seen.has(clientId)) {
existingItems[clientId].remove();
}
});
// Re-bind click handlers
$("#client-list li > a")
.off("click")
.on("click", function (e) {
const href = $(this).attr("href");
if (href.startsWith("#")) {
const clientId = href.substring(1);
loadClientDetails(clientId);
$("#client-list li > a").removeClass("active");
$(this).addClass("active");
}
});
if (window.location.hash) {
const clientId = window.location.hash.substring(1);
loadClientDetails(clientId);
}
});
});
</script>
</head>
<body>
<div id="wrapper"> <div id="wrapper">
<header> <header>
<h1><a href="{{ url_for('panel') }}">judas panel</a></h1> <h2><a href="{{ url_for('index.index') }}">judas</a></h2>
<p>Welcome, {{ username }}! <a href="{{ url_for('logout') }}">Logout</a></p> <p id="no-connection-message">
</header> <i class="fi fi-rr-link-slash"></i>
<main> <span> No connection to server </span>
<table border="1"> </p>
<thead> <a class="button" href="{{ url_for('auth.logout') }}">Logout</a>
<tr> </header>
<th>PC ID</th> <div id="notify"></div>
<th>Status</th> <main>
<th>Last Seen</th> <aside>
</tr> <ul id="client-list"></ul>
</thead> </aside>
<tbody> <div id="content"></div>
{% for pc in pcs.values() %} </main>
<tr>
<td><a href="{{ url_for('details', pc_id=pc.id) }}">{{ pc.id }}</a></td>
<td>{{ pc.status if pc.status else 'Unknown' }}</td>
<td>{{ pc.last_seen if pc.last_seen else 'Never' }}</td>
</tr>
{% else %}
<tr>
<td colspan="3">No PCs found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas panel - stream</title>
</head>
<body>
<div id="wrapper">
<header>
<h1><a href="{{ url_for('panel') }}">judas panel</a></h1>
<p>Welcome, {{ username }}! <a href="{{ url_for('logout') }}">Logout</a></p>
</header>
</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask_login
class User(flask_login.UserMixin):
"""Represents a user for authentication purposes."""
def __init__(self, id: str) -> None:
super().__init__()
self.id = id
def get_id(self) -> str:
"""Return the unique identifier for the user."""
return self.id
def __str__(self) -> str:
return f"User(id={self.id})"
def __repr__(self) -> str:
return f"User(id={self.id})"
def load_user(user_id):
if user_id == "admin":
return User("admin")
return None

View File

@@ -1,152 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Final from __future__ import annotations
import flask import logging as lg
import flask_login from typing import TYPE_CHECKING
PASSWORD: Final[str] = "123" from flask import Flask
from flask_login import LoginManager
from flask_socketio import SocketIO
app = flask.Flask(__name__) from judas_server.web.user import load_user
app.secret_key = "dildo"
login_manager = flask_login.LoginManager() if TYPE_CHECKING:
login_manager.init_app(app) from judas_server.backend import BackendServer
PC_DETAILS = { class JudasWebServer:
"PC1": { def __init__(self, backend: BackendServer, secret_key: str) -> None:
"id": "PC1", self.logger: lg.Logger = lg.getLogger(
"status": "Online", f"{__name__}.{self.__class__.__name__}"
"last_seen": "2023-10-01 12:00:00",
},
"PC2": {
"id": "PC2",
"status": "Offline",
"last_seen": "2023-10-01 11:00:00",
},
}
class User(flask_login.UserMixin):
"""Represents a user for authentication purposes."""
def __init__(self, id: str):
super().__init__()
self.id = id
@login_manager.user_loader
def load_user(user_id: str) -> User | None:
"""Loads a user by user_id.
Args:
user_id: The ID of the user.
Returns:
The User object if found, else None.
"""
if user_id == "admin":
return User("admin")
return None
@app.route("/")
def index() -> str:
"""Renders the index page with a link to the login page."""
if flask_login.current_user.is_authenticated:
return flask.render_template(
"index.html", logged=True, username=flask_login.current_user.id
) )
else: self.logger.debug("Initializing JudasWebServer...")
return flask.render_template(
"index.html", logged=False, login_url=flask.url_for("login") self.backend: BackendServer = backend
self.app: Flask = Flask(
__name__, static_folder="static", template_folder="templates"
)
self.app.secret_key = secret_key
self.app.config["WEB_SERVER"] = self
self.app.config["BACKEND"] = self.backend
# hard-code password
self.app.config["PASSWORD"] = "123"
# extensions
self.login_manager: LoginManager = LoginManager()
self.socketio: SocketIO = SocketIO(self.app, cors_allowed_origins="*")
self.configure_extensions()
self.init_routes()
def configure_extensions(self) -> None:
self.logger.debug("Configuring extensions...")
self.login_manager.init_app(self.app)
self.login_manager.user_loader(load_user)
self.login_manager.login_view = "auth.login"
# TODO: add login page
def init_routes(self) -> None:
self.logger.debug("Initializing routes...")
from judas_server.web.routes import (
api,
auth_bp,
index_bp,
panel_bp,
client_details,
) )
self.app.register_blueprint(index_bp)
self.app.register_blueprint(auth_bp)
self.app.register_blueprint(panel_bp)
self.app.register_blueprint(api.bp)
self.app.register_blueprint(client_details.bp)
api.emit_polled_data(self.app, self.socketio)
@app.route("/login", methods=["GET", "POST"]) def run(self, host: str = "0.0.0.0", port: int = 5000) -> None:
def login() -> str: self.logger.info(f"Starting web server on {host}:{port}...")
"""Handles user login via password form.""" self.socketio.run(app=self.app, host=host, port=port)
if flask.request.method == "POST": self.logger.info("Server stopped.")
password = flask.request.form.get("password", "")
if password == PASSWORD:
user = User("admin")
flask_login.login_user(user)
return flask.redirect(flask.url_for("panel"))
else:
return "Invalid password", 401
return """
<form method="post">
Password: <input type="password" name="password">
<input type="submit" value="Login">
</form>
"""
@app.route("/logout")
@flask_login.login_required
def logout() -> str:
"""Logs out the current user."""
flask_login.logout_user()
return flask.redirect(flask.url_for("index"))
@app.route("/panel")
@flask_login.login_required
def panel() -> str:
"""Renders the main panel page with PC details.
Returns:
Rendered HTML template with PC details.
"""
return flask.render_template(
"panel.html",
username=flask_login.current_user.id,
pcs=PC_DETAILS,
)
@app.route("/details/<pc_id>")
@flask_login.login_required
def details(pc_id: str) -> str:
"""Renders the details page for a specific PC.
Args:
pc_id: The ID of the PC to display details for.
Returns:
Rendered HTML template with PC details.
"""
return flask.render_template(
"details.html",
username=flask_login.current_user.id,
pc=PC_DETAILS[pc_id],
)
@app.route("/stream/<pc_id>")
@flask_login.login_required
def stream(pc_id: str) -> str:
"""Renders the stream page for a specific PC.
Args:
pc_id: The ID of the PC to stream from.
Returns:
Rendered HTML template for streaming.
"""
return flask.render_template(
logged=True,
username=flask_login.current_user.id,
pc=PC_DETAILS[pc_id],
)
@app.route("/stream_feed/<pc_id>")
@flask_login.login_required
def stream_feed(pc_id: str) -> flask.Response:
return flask.Response(mimetype="multipart/x-mixed-replace; boundary=frame")
if __name__ == "__main__":
app.run(debug=True)

938
uv.lock generated Normal file
View File

@@ -0,0 +1,938 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "bidict"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "certifi"
version = "2026.2.25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
]
[[package]]
name = "click-option-group"
version = "0.5.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[[package]]
name = "deprecated"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
name = "dotty-dict"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" },
]
[[package]]
name = "flask"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
name = "flask-login"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" },
]
[[package]]
name = "flask-socketio"
version = "5.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "flask" },
{ name = "jinja2" },
{ name = "python-socketio" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/61/3287c8a8fe4c3c59f2573d71aea7d334a113383ed3e6eb96e290dc80115f/flask_socketio-5.6.1.tar.gz", hash = "sha256:fe5bd995c3ed4da9a98f335d0d830fa1a19d84a64789f6265642a671fdacaeac", size = 37857, upload-time = "2026-02-21T13:07:52.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/98/2a46f4a3117c17fd36e07ad8b085054451e96723baaeea245682156ba546/flask_socketio-5.6.1-py3-none-any.whl", hash = "sha256:51a3f71b28b4476c650829607e3a993e076034db6c3cc31f718f0a4b45939d42", size = 18683, upload-time = "2026-02-21T13:07:51.442Z" },
]
[[package]]
name = "git-cliff"
version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/cf/dff8cd706d2e30e264cb3b9880235607188fb3ad596bfe6282147165bdcd/git_cliff-2.12.0.tar.gz", hash = "sha256:57b96b1f61167f85395353d6f47a89944b4882c03880312d53c09dacecb7ff86", size = 102106, upload-time = "2026-01-20T17:46:12.602Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/a5/dc5f800f6a6dc175faa0787653119754dbbe81a9db1274e041443690287b/git_cliff-2.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e9ee9aa29e9435211712fdab4b5ec9fb432c4bc9d244e39351b2be57aeba7999", size = 6879200, upload-time = "2026-01-20T17:45:55.964Z" },
{ url = "https://files.pythonhosted.org/packages/d7/b6/0e251bd49700e767c47d8d524a690ad713a3aed4318074278438042b8f25/git_cliff-2.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e18512138db5ef57302155b1163c0a2cf43c3d79071a5e083883b65bb990218c", size = 6456349, upload-time = "2026-01-20T17:45:58.202Z" },
{ url = "https://files.pythonhosted.org/packages/5e/63/4e8780f60ad28e8c26ae2b2b365daff9ffa84cb441a5d5bf62c42a75e75a/git_cliff-2.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d24c3e334fdf309c59802ea1a9cd3828e92c8c7cacdd619bcabdc638e00e2ade", size = 6916209, upload-time = "2026-01-20T17:45:59.931Z" },
{ url = "https://files.pythonhosted.org/packages/71/83/0bfab93065e10bcbe97e6136ccf6c1e8552715ef61c11eb678c397ff5fb0/git_cliff-2.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1aa25b05a0315d0f58fc2ac21503538ca749fc3dd7476ee5d6bdf380d9f26ab", size = 7305605, upload-time = "2026-01-20T17:46:01.991Z" },
{ url = "https://files.pythonhosted.org/packages/30/eb/78f624e387c1d9084ca7bcec3a8f28fda9fbbfbeb18c71465a727ee677b5/git_cliff-2.12.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:91eafd2f3ecf226b9a9c2a6c54d96df6042479927b48a97fcf46b728e8744bf1", size = 6927694, upload-time = "2026-01-20T17:46:03.798Z" },
{ url = "https://files.pythonhosted.org/packages/49/3f/735ddcb426c9f77498a039e9398162345c59f29c7990fbf22a530a15fb97/git_cliff-2.12.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:26c9771a50a039252c67803f4c7f187f2ce9c5eea336b8cef890e94483af7a9d", size = 7118983, upload-time = "2026-01-20T17:46:05.535Z" },
{ url = "https://files.pythonhosted.org/packages/f0/97/68a5bd8063904fc43df7811e713483ccd831a877751283c6514dfb5b079e/git_cliff-2.12.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:168f48b82f81ab8e1625d42adb739471623e25bd0a7e25b8c70490bad9e90e2b", size = 7541855, upload-time = "2026-01-20T17:46:07.348Z" },
{ url = "https://files.pythonhosted.org/packages/f7/00/2ed0bf7d71340c20906c1317db50cd6c14bdf0c90fa68a62885c9daf40a9/git_cliff-2.12.0-py3-none-win32.whl", hash = "sha256:4bc609a748c1c3493fe3e00a48305d343255ddff80e564fbf8eb954aac387784", size = 6354818, upload-time = "2026-01-20T17:46:09.117Z" },
{ url = "https://files.pythonhosted.org/packages/c0/fd/679d54e4ed37fdbadb58080219af8f35b5f659dd25e47ab1951b6349d1d0/git_cliff-2.12.0-py3-none-win_amd64.whl", hash = "sha256:c992b5756298251ecdd4db8abe087e90d00327f9eaf0c2470a44dbff64377d07", size = 7303564, upload-time = "2026-01-20T17:46:11.154Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "smmap" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
]
[[package]]
name = "gitpython"
version = "3.1.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "importlib-resources"
version = "6.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "isort"
version = "8.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/e3/e72b0b3a85f24cf5fc2cd8e92b996592798f896024c5cdf3709232e6e377/isort-8.0.0.tar.gz", hash = "sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c", size = 769482, upload-time = "2026-02-19T16:31:59.716Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/ea/cf3aad99dd12c026e2d6835d559efb6fc50ccfd5b46d42d5fec2608b116a/isort-8.0.0-py3-none-any.whl", hash = "sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9", size = 89715, upload-time = "2026-02-19T16:31:57.745Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "judas-protocol"
version = "0.8.0"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" }
[[package]]
name = "judas-server"
version = "0.5.0"
source = { editable = "." }
dependencies = [
{ name = "flask" },
{ name = "flask-login" },
{ name = "flask-socketio" },
{ name = "judas-protocol" },
{ name = "pyyaml" },
]
[package.dev-dependencies]
bump = [
{ name = "git-cliff" },
{ name = "python-semantic-release" },
]
lint = [
{ name = "isort" },
{ name = "ruff" },
]
test = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-github-actions-annotate-failures" },
{ name = "pytest-mock" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.1" },
{ name = "flask-login", specifier = ">=0.6.3" },
{ name = "flask-socketio", specifier = ">=5.5.1" },
{ name = "judas-protocol", git = "https://gitea.pufereq.pl/judas/judas_protocol.git" },
{ name = "pyyaml", specifier = ">=6.0.3" },
]
[package.metadata.requires-dev]
bump = [
{ name = "git-cliff", specifier = ">=2.9.1" },
{ name = "python-semantic-release", specifier = ">=10.2.0" },
]
lint = [
{ name = "isort", specifier = ">=6.0.1" },
{ name = "ruff", specifier = ">=0.13.1" },
]
test = [
{ name = "pytest", specifier = ">=4.2.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
{ name = "pytest-github-actions-annotate-failures", specifier = ">=0.3.0" },
{ name = "pytest-mock", specifier = ">=3.14.1" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
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" }
wheels = [
{ 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" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
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" }
wheels = [
{ 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" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
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" }
wheels = [
{ 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" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "pytest-github-actions-annotate-failures"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/d4/c54ee6a871eee4a7468e3a8c0dead28e634c0bc2110c694309dcb7563a66/pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7", size = 11248, upload-time = "2025-01-17T22:39:32.722Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/73/7b0b15cb8605ee967b34aa1d949737ab664f94e6b0f1534e8339d9e64ab2/pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf", size = 6030, upload-time = "2025-01-17T22:39:31.701Z" },
]
[[package]]
name = "pytest-mock"
version = "3.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]]
name = "python-engineio"
version = "4.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "simple-websocket" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" },
]
[[package]]
name = "python-gitlab"
version = "6.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/bd/b30f1d3b303cb5d3c72e2d57a847d699e8573cbdfd67ece5f1795e49da1c/python_gitlab-6.5.0.tar.gz", hash = "sha256:97553652d94b02de343e9ca92782239aa2b5f6594c5482331a9490d9d5e8737d", size = 400591, upload-time = "2025-10-17T21:40:02.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" },
]
[[package]]
name = "python-semantic-release"
version = "10.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "click-option-group" },
{ name = "deprecated" },
{ name = "dotty-dict" },
{ name = "gitpython" },
{ name = "importlib-resources" },
{ name = "jinja2" },
{ name = "pydantic" },
{ name = "python-gitlab" },
{ name = "requests" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "tomlkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/3a/7332b822825ed0e902c6e950e0d1e90e8f666fd12eb27855d1c8b6677eff/python_semantic_release-10.5.3.tar.gz", hash = "sha256:de4da78635fa666e5774caaca2be32063cae72431eb75e2ac23b9f2dfd190785", size = 618034, upload-time = "2025-12-14T22:37:29.782Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/01/ada29a1215df601bded0a2efd3b6d53864a0a9e0a9ea52aeaebe14fd03fd/python_semantic_release-10.5.3-py3-none-any.whl", hash = "sha256:1be0e07c36fa1f1ec9da4f438c1f6bbd7bc10eb0d6ac0089b0643103708c2823", size = 152716, upload-time = "2025-12-14T22:37:28.089Z" },
]
[[package]]
name = "python-socketio"
version = "5.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bidict" },
{ name = "python-engineio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
[[package]]
name = "rich"
version = "14.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
]
[[package]]
name = "ruff"
version = "0.15.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3c/3b/20d9a0bc954d51b63f20cf710cf506bfe675d1e6138139342dd5ccc90326/ruff-0.15.3.tar.gz", hash = "sha256:78757853320d8ddb9da24e614ef69a37bcbcfd477e5a6435681188d4bce4eaa1", size = 4569031, upload-time = "2026-02-26T15:39:38.015Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/00/c544ab1d70f86dc50a2f2a8e1262e5af5025897ccd820415f559f9f2f63f/ruff-0.15.3-py3-none-linux_armv6l.whl", hash = "sha256:f7df0fd6f889a8d8de2ddb48a9eb55150954400f2157ea15b21a2f49ecaaf988", size = 10444066, upload-time = "2026-02-26T15:39:47.708Z" },
{ url = "https://files.pythonhosted.org/packages/fb/15/9dee3f4e891261adbd690f8c6f075418a7cd76e845601b00a0da2ae2ad6e/ruff-0.15.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0198b5445197d443c3bbf2cc358f4bd477fb3951e3c7f2babc13e9bb490614a8", size = 10853125, upload-time = "2026-02-26T15:40:18.943Z" },
{ url = "https://files.pythonhosted.org/packages/88/ba/fc5aeda852c89faf821d36c951df866117342e88439e1b1e1e762a07b7fd/ruff-0.15.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:adf95b5be57b25fbbbc07cd68d37414bee8729e807ad0217219558027186967e", size = 10180833, upload-time = "2026-02-26T15:40:13.282Z" },
{ url = "https://files.pythonhosted.org/packages/06/87/e2f80a39164476fac4d45752a0d4721d6645f40b7f851e48add12af9947e/ruff-0.15.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b56dbd9cd86489ccbad96bb58fa4c958342b5510fdeb60ea13d9d3566bd845c", size = 10536806, upload-time = "2026-02-26T15:40:24.129Z" },
{ url = "https://files.pythonhosted.org/packages/fd/89/2e5bf0ed30ea3778460ea4d8cc6cb4d88ba96d9732d2c0cc33349cd65196/ruff-0.15.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6f263ce511871955d8c5401b62c7e863988ea4d0527aa0a3b1b7ecff4d4abc4", size = 10276093, upload-time = "2026-02-26T15:39:44.654Z" },
{ url = "https://files.pythonhosted.org/packages/82/cb/318206d778c7f42917ca7b0f9436cf27652d1731fe434d3c9990c4a611fa/ruff-0.15.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90fa1bed82ffede5768232b9bd23212c547ab7cd74c752007ecade1d895ee1a", size = 11051593, upload-time = "2026-02-26T15:39:35.157Z" },
{ url = "https://files.pythonhosted.org/packages/58/8f/65ee4c1b88e49dd4c0a3fc43e81832536c7942f0c702b6f3d25db0f95d6c/ruff-0.15.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e9d53760b7061ddbe5ea9e25381332c607fc14c40bde78f8a25392a93a68d74", size = 11885820, upload-time = "2026-02-26T15:39:59.504Z" },
{ url = "https://files.pythonhosted.org/packages/db/04/d4261f6729ad9a356bc6e3223ba297acf3b66118cef4795b4a8953b255ff/ruff-0.15.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec90e3b78c56c4acca4264d371dd48e29215ecb673cc2fa3c4b799b72050e491", size = 11340583, upload-time = "2026-02-26T15:39:50.781Z" },
{ url = "https://files.pythonhosted.org/packages/24/84/490f38b2bc104e0fdc9496c2a66a48fb2d24a01de46ba0c60c4f6c4d4590/ruff-0.15.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ce448fd395f822e34c8f6f7dfcd84b6726340082950858f92c4daa6baf8915", size = 11160701, upload-time = "2026-02-26T15:40:02.447Z" },
{ url = "https://files.pythonhosted.org/packages/ad/25/eae9cb7b6c28b425ed8cbe797da89c78146071102181ba74c4cdfd06bbeb/ruff-0.15.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14f7d763962d385f75b9b3b57fcc5661c56c20d8b1ddc9f5c881b5fa0ba499fa", size = 11111482, upload-time = "2026-02-26T15:39:56.462Z" },
{ url = "https://files.pythonhosted.org/packages/95/18/16d0b5ef143cb9e52724f18cbccb4b3c5cd4d4e2debbd95e2be3aeb64c9e/ruff-0.15.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b57084e3a3d65418d376c7023711c37cce023cd2fb038a76ba15ee21f3c2c2ee", size = 10497151, upload-time = "2026-02-26T15:40:10.64Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b4/1829314241ddba07c54a742ab387da343fe56a0267a6b6498f3e2ae99821/ruff-0.15.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d567523ff7dcf3112b0f71231d18c3506dd06943359476ee64dea0f9c8f63976", size = 10281955, upload-time = "2026-02-26T15:40:16.033Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/80a4ec4bd3cf58ca9b49dccf2bd232b520db14184912fb7e0eb6f3ecc484/ruff-0.15.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4223088d255bf31a50b6640445b39f668164d64c23e5fa403edfb1e0b11122e5", size = 10766613, upload-time = "2026-02-26T15:40:21.55Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/fe016b862295dc57499997e7f2edc58119469b210f4f03ccb763fa65f130/ruff-0.15.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:32399ddae088970b2db6efd8d3f49981375cb828075359b6c088ed1fe63d64e1", size = 11262113, upload-time = "2026-02-26T15:39:41.5Z" },
{ url = "https://files.pythonhosted.org/packages/42/b1/77dcd05940388d9ba3de03ac4b8b598826d57935728071e1be9f2ef5b714/ruff-0.15.3-py3-none-win32.whl", hash = "sha256:1f1eb95ff614351e3a89a862b6d94e6c42c170e61916e1f20facd6c38477f5f3", size = 10509423, upload-time = "2026-02-26T15:40:05.217Z" },
{ url = "https://files.pythonhosted.org/packages/29/d5/76aab0fabbd54e8c77d02fcff2494906ba85b539d22aa9b7124f7100f008/ruff-0.15.3-py3-none-win_amd64.whl", hash = "sha256:2b22dffe5f5e1e537097aa5208684f069e495f980379c4491b1cfb198a444d0c", size = 11637739, upload-time = "2026-02-26T15:39:53.951Z" },
{ url = "https://files.pythonhosted.org/packages/f2/61/9b4e3682dfd26054321e1b2fdb67a51361dd6ec2fb63f2b50d711f8832ae/ruff-0.15.3-py3-none-win_arm64.whl", hash = "sha256:82443c14d694d4cbd9e598ede27ef5d6f08389ccad91c933be775ea2f4e66f76", size = 10957794, upload-time = "2026-02-26T15:40:08.045Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
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" }
wheels = [
{ 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" },
]
[[package]]
name = "simple-websocket"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" },
]
[[package]]
name = "smmap"
version = "5.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
]
[[package]]
name = "tomlkit"
version = "0.13.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
]
[[package]]
name = "wrapt"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f7/37/ae31f40bec90de2f88d9597d0b5281e23ffe85b893a47ca5d9c05c63a4f6/wrapt-2.1.1.tar.gz", hash = "sha256:5fdcb09bf6db023d88f312bd0767594b414655d58090fc1c46b3414415f67fac", size = 81329, upload-time = "2026-02-03T02:12:13.786Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/ca/3cf290212855b19af9fcc41b725b5620b32f470d6aad970c2593500817eb/wrapt-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9646e17fa7c3e2e7a87e696c7de66512c2b4f789a8db95c613588985a2e139", size = 61150, upload-time = "2026-02-03T02:12:50.575Z" },
{ url = "https://files.pythonhosted.org/packages/9d/33/5b8f89a82a9859ce82da4870c799ad11ce15648b6e1c820fec3e23f4a19f/wrapt-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:428cfc801925454395aa468ba7ddb3ed63dc0d881df7b81626cdd433b4e2b11b", size = 61743, upload-time = "2026-02-03T02:11:55.733Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2f/60c51304fbdf47ce992d9eefa61fbd2c0e64feee60aaa439baf42ea6f40b/wrapt-2.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5797f65e4d58065a49088c3b32af5410751cd485e83ba89e5a45e2aa8905af98", size = 121341, upload-time = "2026-02-03T02:11:20.461Z" },
{ url = "https://files.pythonhosted.org/packages/ad/03/ce5256e66dd94e521ad5e753c78185c01b6eddbed3147be541f4d38c0cb7/wrapt-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a2db44a71202c5ae4bb5f27c6d3afbc5b23053f2e7e78aa29704541b5dad789", size = 122947, upload-time = "2026-02-03T02:11:33.596Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ae/50ca8854b81b946a11a36fcd6ead32336e6db2c14b6e4a8b092b80741178/wrapt-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d5350c3590af09c1703dd60ec78a7370c0186e11eaafb9dda025a30eee6492d", size = 121370, upload-time = "2026-02-03T02:11:09.886Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d9/d6a7c654e0043319b4cc137a4caaf7aa16b46b51ee8df98d1060254705b7/wrapt-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d9b076411bed964e752c01b49fd224cc385f3a96f520c797d38412d70d08359", size = 120465, upload-time = "2026-02-03T02:11:37.592Z" },
{ url = "https://files.pythonhosted.org/packages/55/90/65be41e40845d951f714b5a77e84f377a3787b1e8eee6555a680da6d0db5/wrapt-2.1.1-cp313-cp313-win32.whl", hash = "sha256:0bb7207130ce6486727baa85373503bf3334cc28016f6928a0fa7e19d7ecdc06", size = 58090, upload-time = "2026-02-03T02:12:53.342Z" },
{ url = "https://files.pythonhosted.org/packages/5f/66/6a09e0294c4fc8c26028a03a15191721c9271672467cc33e6617ee0d91d2/wrapt-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:cbfee35c711046b15147b0ae7db9b976f01c9520e6636d992cd9e69e5e2b03b1", size = 60341, upload-time = "2026-02-03T02:12:36.384Z" },
{ url = "https://files.pythonhosted.org/packages/7a/f0/20ceb8b701e9a71555c87a5ddecbed76ec16742cf1e4b87bbaf26735f998/wrapt-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7d2756061022aebbf57ba14af9c16e8044e055c22d38de7bf40d92b565ecd2b0", size = 58731, upload-time = "2026-02-03T02:12:01.328Z" },
{ url = "https://files.pythonhosted.org/packages/80/b4/fe95beb8946700b3db371f6ce25115217e7075ca063663b8cca2888ba55c/wrapt-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4814a3e58bc6971e46baa910ecee69699110a2bf06c201e24277c65115a20c20", size = 62969, upload-time = "2026-02-03T02:11:51.245Z" },
{ url = "https://files.pythonhosted.org/packages/b8/89/477b0bdc784e3299edf69c279697372b8bd4c31d9c6966eae405442899df/wrapt-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:106c5123232ab9b9f4903692e1fa0bdc231510098f04c13c3081f8ad71c3d612", size = 63606, upload-time = "2026-02-03T02:12:02.64Z" },
{ url = "https://files.pythonhosted.org/packages/ed/55/9d0c1269ab76de87715b3b905df54dd25d55bbffd0b98696893eb613469f/wrapt-2.1.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1a40b83ff2535e6e56f190aff123821eea89a24c589f7af33413b9c19eb2c738", size = 152536, upload-time = "2026-02-03T02:11:24.492Z" },
{ url = "https://files.pythonhosted.org/packages/44/18/2004766030462f79ad86efaa62000b5e39b1ff001dcce86650e1625f40ae/wrapt-2.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:789cea26e740d71cf1882e3a42bb29052bc4ada15770c90072cb47bf73fb3dbf", size = 158697, upload-time = "2026-02-03T02:12:32.214Z" },
{ url = "https://files.pythonhosted.org/packages/e1/bb/0a880fa0f35e94ee843df4ee4dd52a699c9263f36881311cfb412c09c3e5/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ba49c14222d5e5c0ee394495a8655e991dc06cbca5398153aefa5ac08cd6ccd7", size = 155563, upload-time = "2026-02-03T02:11:49.737Z" },
{ url = "https://files.pythonhosted.org/packages/42/ff/cd1b7c4846c8678fac359a6eb975dc7ab5bd606030adb22acc8b4a9f53f1/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ac8cda531fe55be838a17c62c806824472bb962b3afa47ecbd59b27b78496f4e", size = 150161, upload-time = "2026-02-03T02:12:33.613Z" },
{ url = "https://files.pythonhosted.org/packages/38/ec/67c90a7082f452964b4621e4890e9a490f1add23cdeb7483cc1706743291/wrapt-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:b8af75fe20d381dd5bcc9db2e86a86d7fcfbf615383a7147b85da97c1182225b", size = 59783, upload-time = "2026-02-03T02:11:39.863Z" },
{ url = "https://files.pythonhosted.org/packages/ec/08/466afe4855847d8febdfa2c57c87e991fc5820afbdef01a273683dfd15a0/wrapt-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:45c5631c9b6c792b78be2d7352129f776dd72c605be2c3a4e9be346be8376d83", size = 63082, upload-time = "2026-02-03T02:12:09.075Z" },
{ url = "https://files.pythonhosted.org/packages/9a/62/60b629463c28b15b1eeadb3a0691e17568622b12aa5bfa7ebe9b514bfbeb/wrapt-2.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:da815b9263947ac98d088b6414ac83507809a1d385e4632d9489867228d6d81c", size = 60251, upload-time = "2026-02-03T02:11:21.794Z" },
{ url = "https://files.pythonhosted.org/packages/95/a0/1c2396e272f91efe6b16a6a8bce7ad53856c8f9ae4f34ceaa711d63ec9e1/wrapt-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aa1765054245bb01a37f615503290d4e207e3fd59226e78341afb587e9c1236", size = 61311, upload-time = "2026-02-03T02:12:44.41Z" },
{ url = "https://files.pythonhosted.org/packages/b0/9a/d2faba7e61072a7507b5722db63562fdb22f5a24e237d460d18755627f15/wrapt-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:feff14b63a6d86c1eee33a57f77573649f2550935981625be7ff3cb7342efe05", size = 61805, upload-time = "2026-02-03T02:11:59.905Z" },
{ url = "https://files.pythonhosted.org/packages/db/56/073989deb4b5d7d6e7ea424476a4ae4bda02140f2dbeaafb14ba4864dd60/wrapt-2.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81fc5f22d5fcfdbabde96bb3f5379b9f4476d05c6d524d7259dc5dfb501d3281", size = 120308, upload-time = "2026-02-03T02:12:04.46Z" },
{ url = "https://files.pythonhosted.org/packages/d1/b6/84f37261295e38167a29eb82affaf1dc15948dc416925fe2091beee8e4ac/wrapt-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:951b228ecf66def855d22e006ab9a1fc12535111ae7db2ec576c728f8ddb39e8", size = 122688, upload-time = "2026-02-03T02:11:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/ea/80/32db2eec6671f80c65b7ff175be61bc73d7f5223f6910b0c921bbc4bd11c/wrapt-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ddf582a95641b9a8c8bd643e83f34ecbbfe1b68bc3850093605e469ab680ae3", size = 121115, upload-time = "2026-02-03T02:12:39.068Z" },
{ url = "https://files.pythonhosted.org/packages/49/ef/dcd00383df0cd696614127902153bf067971a5aabcd3c9dcb2d8ef354b2a/wrapt-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc5c500966bf48913f795f1984704e6d452ba2414207b15e1f8c339a059d5b16", size = 119484, upload-time = "2026-02-03T02:11:48.419Z" },
{ url = "https://files.pythonhosted.org/packages/76/29/0630280cdd2bd8f86f35cb6854abee1c9d6d1a28a0c6b6417cd15d378325/wrapt-2.1.1-cp314-cp314-win32.whl", hash = "sha256:4aa4baadb1f94b71151b8e44a0c044f6af37396c3b8bcd474b78b49e2130a23b", size = 58514, upload-time = "2026-02-03T02:11:58.616Z" },
{ url = "https://files.pythonhosted.org/packages/db/19/5bed84f9089ed2065f6aeda5dfc4f043743f642bc871454b261c3d7d322b/wrapt-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:860e9d3fd81816a9f4e40812f28be4439ab01f260603c749d14be3c0a1170d19", size = 60763, upload-time = "2026-02-03T02:12:24.553Z" },
{ url = "https://files.pythonhosted.org/packages/e4/cb/b967f2f9669e4249b4fe82e630d2a01bc6b9e362b9b12ed91bbe23ae8df4/wrapt-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3c59e103017a2c1ea0ddf589cbefd63f91081d7ce9d491d69ff2512bb1157e23", size = 59051, upload-time = "2026-02-03T02:11:29.602Z" },
{ url = "https://files.pythonhosted.org/packages/eb/19/6fed62be29f97eb8a56aff236c3f960a4b4a86e8379dc7046a8005901a97/wrapt-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9fa7c7e1bee9278fc4f5dd8275bc8d25493281a8ec6c61959e37cc46acf02007", size = 63059, upload-time = "2026-02-03T02:12:06.368Z" },
{ url = "https://files.pythonhosted.org/packages/0a/1c/b757fd0adb53d91547ed8fad76ba14a5932d83dde4c994846a2804596378/wrapt-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39c35e12e8215628984248bd9c8897ce0a474be2a773db207eb93414219d8469", size = 63618, upload-time = "2026-02-03T02:12:23.197Z" },
{ url = "https://files.pythonhosted.org/packages/10/fe/e5ae17b1480957c7988d991b93df9f2425fc51f128cf88144d6a18d0eb12/wrapt-2.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:94ded4540cac9125eaa8ddf5f651a7ec0da6f5b9f248fe0347b597098f8ec14c", size = 152544, upload-time = "2026-02-03T02:11:43.915Z" },
{ url = "https://files.pythonhosted.org/packages/3e/cc/99aed210c6b547b8a6e4cb9d1425e4466727158a6aeb833aa7997e9e08dd/wrapt-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0af328373f97ed9bdfea24549ac1b944096a5a71b30e41c9b8b53ab3eec04a", size = 158700, upload-time = "2026-02-03T02:12:30.684Z" },
{ url = "https://files.pythonhosted.org/packages/81/0e/d442f745f4957944d5f8ad38bc3a96620bfff3562533b87e486e979f3d99/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4ad839b55f0bf235f8e337ce060572d7a06592592f600f3a3029168e838469d3", size = 155561, upload-time = "2026-02-03T02:11:28.164Z" },
{ url = "https://files.pythonhosted.org/packages/51/ac/9891816280e0018c48f8dfd61b136af7b0dcb4a088895db2531acde5631b/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d89c49356e5e2a50fa86b40e0510082abcd0530f926cbd71cf25bee6b9d82d7", size = 150188, upload-time = "2026-02-03T02:11:57.053Z" },
{ url = "https://files.pythonhosted.org/packages/24/98/e2f273b6d70d41f98d0739aa9a269d0b633684a5fb17b9229709375748d4/wrapt-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:f4c7dd22cf7f36aafe772f3d88656559205c3af1b7900adfccb70edeb0d2abc4", size = 60425, upload-time = "2026-02-03T02:11:35.007Z" },
{ url = "https://files.pythonhosted.org/packages/1e/06/b500bfc38a4f82d89f34a13069e748c82c5430d365d9e6b75afb3ab74457/wrapt-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f76bc12c583ab01e73ba0ea585465a41e48d968f6d1311b4daec4f8654e356e3", size = 63855, upload-time = "2026-02-03T02:12:15.47Z" },
{ url = "https://files.pythonhosted.org/packages/d9/cc/5f6193c32166faee1d2a613f278608e6f3b95b96589d020f0088459c46c9/wrapt-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7ea74fc0bec172f1ae5f3505b6655c541786a5cabe4bbc0d9723a56ac32eb9b9", size = 60443, upload-time = "2026-02-03T02:11:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/c4/da/5a086bf4c22a41995312db104ec2ffeee2cf6accca9faaee5315c790377d/wrapt-2.1.1-py3-none-any.whl", hash = "sha256:3b0f4629eb954394a3d7c7a1c8cca25f0b07cefe6aa8545e862e9778152de5b7", size = 43886, upload-time = "2026-02-03T02:11:45.048Z" },
]
[[package]]
name = "wsproto"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
]