From ff2319a50e4829c624ea18206772d4058005a04d Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Sun, 15 Feb 2026 23:33:18 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20Commit=20message:=20Update=20202?= =?UTF-8?q?6-02-15=2023:33:18,=203=20files,=2091=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📁 Files changed: 3 📝 Lines changed: 91 • .coverage • CLAUDE.md • uv.lock --- .coverage | Bin 159744 -> 159744 bytes CLAUDE.md | 29 +++++++++++++++++++------ uv.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/.coverage b/.coverage index 0c1aa4d7142d518e7a3ea5524ce4d1c011fd2ea4..c852765fb5420f7970b35e79311913047731a8c6 100644 GIT binary patch delta 26782 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 TOK>3!`_TW4Jv#r_Ejs@PO+Za< delta 25313 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 diff --git a/CLAUDE.md b/CLAUDE.md index 4a52540..d06b02a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,12 +134,27 @@ curl -H "X-API-Key: test" http://localhost:8000/api/db/stats Tags trigger PyPI publish: `git tag v0.1.x && git push origin v0.1.x` -## TODO: ComfyUI Integration +## ComfyUI Integration -ComfyUI is running on junkpile (port 8188) but NOT integrated with tensors. +ComfyUI GUI is accessible via tensors reverse proxy with session auth: -Needed: -- [ ] Add `/api/generate` endpoint that proxies to ComfyUI -- [ ] Workflow template management -- [ ] Queue job → poll for completion → return image -- [ ] Config: `[comfyui] url = "http://127.0.0.1:8188"` +**URL:** https://tensors-api.saiden.dev/comfy/login + +**Environment Variables:** +```bash +COMFYUI_URL=http://127.0.0.1:8188 # ComfyUI backend (localhost only) +COMFYUI_USER=admin # Login username +COMFYUI_PASS= # Login password +SESSION_SECRET= # Cookie signing secret +``` + +**Flow:** +1. User visits `/comfy/login` → dark mode login page +2. Auth with static user/pass → session cookie set +3. All `/comfy/*` requests proxied to ComfyUI (HTTP + WebSocket) +4. Full GUI works: queue, previews, node editor, etc. + +**Security:** +- ComfyUI listens on localhost only (not exposed) +- tensors handles auth via session cookies +- WebSocket connections also authenticated diff --git a/uv.lock b/uv.lock index 4814ac9..9786a61 100644 --- a/uv.lock +++ b/uv.lock @@ -662,6 +662,15 @@ 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 = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -884,8 +893,10 @@ dependencies = [ [package.optional-dependencies] server = [ { name = "fastapi" }, + { name = "python-multipart" }, { name = "scalar-fastapi" }, { name = "uvicorn" }, + { name = "websockets" }, ] [package.dev-dependencies] @@ -896,10 +907,12 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "python-multipart" }, { name = "respx" }, { name = "ruff" }, { name = "scalar-fastapi" }, { name = "uvicorn" }, + { name = "websockets" }, ] [package.metadata] @@ -907,6 +920,7 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "huggingface-hub", specifier = ">=0.25.0" }, + { name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.9" }, { name = "rich", specifier = ">=13.0.0" }, { name = "safetensors", specifier = ">=0.4.0" }, { name = "scalar-fastapi", marker = "extra == 'server'", specifier = ">=1.6" }, @@ -914,6 +928,7 @@ requires-dist = [ { name = "typer", specifier = ">=0.15.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.30" }, { name = "websocket-client", specifier = ">=1.9.0" }, + { name = "websockets", marker = "extra == 'server'", specifier = ">=12.0" }, ] provides-extras = ["server"] @@ -925,10 +940,12 @@ dev = [ { name = "pre-commit", specifier = ">=3.6" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-cov", specifier = ">=4.1" }, + { name = "python-multipart", specifier = ">=0.0.9" }, { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.9.0" }, { name = "scalar-fastapi", specifier = ">=1.6" }, { name = "uvicorn", specifier = ">=0.30" }, + { name = "websockets", specifier = ">=12.0" }, ] [[package]] @@ -1028,6 +1045,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "zstandard" version = "0.25.0"