comfygit-studio 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/.gitignore +1 -0
  2. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/PKG-INFO +4 -4
  3. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/runtime.py +164 -1
  4. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/state.py +2 -1
  5. comfygit_studio-0.7.0/comfygit_studio/static/assets/index-ry5Db1EL.js +17 -0
  6. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/index.html +1 -1
  7. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/pyproject.toml +3 -3
  8. comfygit_studio-0.5.0/comfygit_studio/static/assets/index-BlnDFmNd.js +0 -17
  9. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/README.md +0 -0
  10. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/__init__.py +0 -0
  11. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/api_schema.py +0 -0
  12. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/embedded.py +0 -0
  13. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/executor.py +0 -0
  14. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/openapi/studio-contract-api.v1.json +0 -0
  15. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  16. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  17. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  18. {comfygit_studio-0.5.0 → comfygit_studio-0.7.0}/comfygit_studio/static/assets/index-BDmIh9tA.css +0 -0
@@ -67,6 +67,7 @@ temp/
67
67
  tmp/
68
68
 
69
69
  # Local configuration
70
+ .akatz-labs/
70
71
  local_config.json
71
72
  dev/cross-platform-test.local.toml
72
73
 
@@ -1,13 +1,13 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: comfygit-studio
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: ComfyGit Studio runtime - contract API and Studio serving adapters
5
5
  Project-URL: Documentation, https://docs.comfygit.org/
6
6
  Project-URL: Repository, https://github.com/comfygit-ai/comfygit
7
7
  Project-URL: Issues, https://github.com/comfygit-ai/comfygit/issues
8
8
  Requires-Python: >=3.10
9
- Requires-Dist: aiohttp>=3.13.4
10
- Requires-Dist: comfygit-core==0.5.0
9
+ Requires-Dist: aiohttp>=3.14.3
10
+ Requires-Dist: comfygit-core==0.7.0
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  # ComfyGit Studio Runtime
@@ -172,6 +172,16 @@ class WorkerCallbackUpload:
172
172
  body: bytes
173
173
 
174
174
 
175
+ @dataclass(frozen=True)
176
+ class WorkerArtifactUploadTarget:
177
+ upload_id: str
178
+ output_name: str
179
+ artifact_index: int
180
+ transport: dict[str, Any]
181
+ storage_ref: dict[str, Any]
182
+ expected_content_type: str | None = None
183
+
184
+
175
185
  class ServeState:
176
186
  """Shared state for request handlers."""
177
187
 
@@ -833,6 +843,7 @@ async def proxy_run_create_handler(request: web.Request) -> web.Response:
833
843
  poll_interval_seconds = float(payload.get("poll_interval_seconds", 1))
834
844
  cache_token = str(payload.get("cache_token") or uuid.uuid4().hex[:10])
835
845
  callback = _proxy_callback_target(payload.get("callback"))
846
+ artifact_upload_targets = _proxy_artifact_upload_targets(payload)
836
847
 
837
848
  async def record_submitted(prompt_id: str) -> None:
838
849
  state.proxy_runs[prompt_id] = ProxyRuntimeRun(
@@ -862,6 +873,7 @@ async def proxy_run_create_handler(request: web.Request) -> web.Response:
862
873
  timeout_seconds=timeout_seconds,
863
874
  poll_interval_seconds=poll_interval_seconds,
864
875
  callback=callback,
876
+ artifact_upload_targets=artifact_upload_targets,
865
877
  )
866
878
  )
867
879
  _track_proxy_task(state, task)
@@ -1752,6 +1764,39 @@ def _proxy_callback_target(value: Any) -> ProxyCallbackTarget | None:
1752
1764
  )
1753
1765
 
1754
1766
 
1767
+ def _proxy_artifact_upload_targets(payload: Mapping[str, Any]) -> tuple[WorkerArtifactUploadTarget, ...]:
1768
+ raw_targets: Any = payload.get("artifact_upload_targets")
1769
+ delivery = payload.get("artifact_delivery")
1770
+ if not isinstance(raw_targets, list) and isinstance(delivery, Mapping):
1771
+ if str(delivery.get("mode") or "") == "direct_upload":
1772
+ raw_targets = delivery.get("targets")
1773
+ if not isinstance(raw_targets, list):
1774
+ return ()
1775
+
1776
+ targets: list[WorkerArtifactUploadTarget] = []
1777
+ for raw_target in raw_targets:
1778
+ if not isinstance(raw_target, Mapping):
1779
+ continue
1780
+ upload_id = str(raw_target.get("upload_id") or "").strip()
1781
+ output_name = str(raw_target.get("output_name") or "").strip()
1782
+ transport = raw_target.get("transport")
1783
+ if not upload_id or not output_name or not isinstance(transport, Mapping):
1784
+ continue
1785
+ storage_ref = raw_target.get("storage_ref")
1786
+ expected_content_type = raw_target.get("expected_content_type")
1787
+ targets.append(
1788
+ WorkerArtifactUploadTarget(
1789
+ upload_id=upload_id,
1790
+ output_name=output_name,
1791
+ artifact_index=max(0, _optional_int(raw_target.get("artifact_index")) or 0),
1792
+ transport={str(key): value for key, value in transport.items()},
1793
+ storage_ref={str(key): value for key, value in storage_ref.items()} if isinstance(storage_ref, Mapping) else {},
1794
+ expected_content_type=str(expected_content_type) if expected_content_type else None,
1795
+ )
1796
+ )
1797
+ return tuple(targets)
1798
+
1799
+
1755
1800
  async def _complete_proxy_runtime_run(
1756
1801
  state: ServeState,
1757
1802
  prompt_id: str,
@@ -1760,6 +1805,7 @@ async def _complete_proxy_runtime_run(
1760
1805
  timeout_seconds: float,
1761
1806
  poll_interval_seconds: float,
1762
1807
  callback: ProxyCallbackTarget | None = None,
1808
+ artifact_upload_targets: tuple[WorkerArtifactUploadTarget, ...] = (),
1763
1809
  ) -> None:
1764
1810
  record = state.proxy_runs.get(prompt_id)
1765
1811
  if record is None:
@@ -1821,7 +1867,11 @@ async def _complete_proxy_runtime_run(
1821
1867
  }
1822
1868
  if callback is not None:
1823
1869
  try:
1824
- callback_outputs, uploads = await _worker_callback_outputs_and_uploads(state, execution.outputs)
1870
+ callback_outputs, uploads = await _worker_callback_outputs_and_uploads(
1871
+ state,
1872
+ execution.outputs,
1873
+ artifact_upload_targets=artifact_upload_targets,
1874
+ )
1825
1875
  await _post_worker_completion_callback(
1826
1876
  callback,
1827
1877
  {
@@ -1907,6 +1957,8 @@ def _worker_callback_headers(callback: ProxyCallbackTarget) -> dict[str, str]:
1907
1957
  async def _worker_callback_outputs_and_uploads(
1908
1958
  state: ServeState,
1909
1959
  outputs: list[dict[str, Any]],
1960
+ *,
1961
+ artifact_upload_targets: tuple[WorkerArtifactUploadTarget, ...] = (),
1910
1962
  ) -> tuple[list[dict[str, Any]], list[WorkerCallbackUpload]]:
1911
1963
  output_payloads = [dict(output) for output in outputs]
1912
1964
  uploads: list[WorkerCallbackUpload] = []
@@ -1935,6 +1987,27 @@ async def _worker_callback_outputs_and_uploads(
1935
1987
  filename,
1936
1988
  fallback=f"{field_name}{_extension_for_content_type(response.content_type)}",
1937
1989
  )
1990
+ upload_target = _worker_artifact_upload_target(
1991
+ artifact_upload_targets,
1992
+ output_name=str(output.get("name") or f"output_{output_index + 1}"),
1993
+ artifact_index=artifact_index,
1994
+ )
1995
+ if upload_target is not None:
1996
+ await _upload_worker_artifact_to_target(
1997
+ upload_target,
1998
+ filename=safe_filename,
1999
+ content_type=response.content_type,
2000
+ body=response.body,
2001
+ )
2002
+ artifact["upload_id"] = upload_target.upload_id
2003
+ artifact["storage_ref"] = dict(upload_target.storage_ref)
2004
+ artifact["filename"] = safe_filename
2005
+ artifact["content_type"] = response.content_type
2006
+ artifact["kind"] = output_kind(output_type, safe_filename)
2007
+ artifact["size_bytes"] = len(response.body)
2008
+ artifact["sha256"] = hashlib.sha256(response.body).hexdigest()
2009
+ artifact.pop("upload_field", None)
2010
+ continue
1938
2011
  artifact["upload_field"] = field_name
1939
2012
  artifact["content_type"] = response.content_type
1940
2013
  artifact["kind"] = output_kind(output_type, safe_filename)
@@ -1949,6 +2022,96 @@ async def _worker_callback_outputs_and_uploads(
1949
2022
  return output_payloads, uploads
1950
2023
 
1951
2024
 
2025
+ def _worker_artifact_upload_target(
2026
+ targets: tuple[WorkerArtifactUploadTarget, ...],
2027
+ *,
2028
+ output_name: str,
2029
+ artifact_index: int,
2030
+ ) -> WorkerArtifactUploadTarget | None:
2031
+ for target in targets:
2032
+ if target.output_name == output_name and target.artifact_index == artifact_index:
2033
+ return target
2034
+ return None
2035
+
2036
+
2037
+ async def _upload_worker_artifact_to_target(
2038
+ target: WorkerArtifactUploadTarget,
2039
+ *,
2040
+ filename: str,
2041
+ content_type: str,
2042
+ body: bytes,
2043
+ ) -> None:
2044
+ kind = str(target.transport.get("kind") or target.transport.get("transport_kind") or "").lower()
2045
+ if kind == "local_file_put":
2046
+ path_value = target.transport.get("path") or target.storage_ref.get("path")
2047
+ if not path_value:
2048
+ raise ValueError(f"Artifact upload target {target.upload_id} is missing local path.")
2049
+ path = Path(str(path_value))
2050
+ path.parent.mkdir(parents=True, exist_ok=True)
2051
+ temp_path = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
2052
+ temp_path.write_bytes(body)
2053
+ temp_path.replace(path)
2054
+ return
2055
+
2056
+ if kind == "supabase_signed_upload":
2057
+ upload_url = str(target.transport.get("url") or "").strip()
2058
+ if not upload_url:
2059
+ raise ValueError(f"Artifact upload target {target.upload_id} is missing upload url.")
2060
+ headers = _worker_artifact_upload_headers(target.transport)
2061
+ # Supabase signed upload URLs accept multipart form PUT with a single
2062
+ # file part; sending raw bytes yields a successful HTTP request that
2063
+ # stores the wrong object shape.
2064
+ form = aiohttp.FormData()
2065
+ form.add_field("file", body, filename=filename, content_type=content_type)
2066
+ async with aiohttp.ClientSession() as session:
2067
+ async with session.put(
2068
+ upload_url,
2069
+ data=form,
2070
+ headers=headers,
2071
+ timeout=aiohttp.ClientTimeout(total=300),
2072
+ ) as response:
2073
+ response.raise_for_status()
2074
+ return
2075
+
2076
+ if kind in {"http_signed_upload", "http_put", "signed_put"}:
2077
+ upload_url = str(target.transport.get("url") or "").strip()
2078
+ if not upload_url:
2079
+ raise ValueError(f"Artifact upload target {target.upload_id} is missing upload url.")
2080
+ method = str(target.transport.get("method") or "PUT").upper()
2081
+ headers = _worker_artifact_upload_headers(target.transport)
2082
+ headers.setdefault("content-type", content_type)
2083
+ async with aiohttp.ClientSession() as session:
2084
+ async with session.request(
2085
+ method,
2086
+ upload_url,
2087
+ data=body,
2088
+ headers=headers,
2089
+ timeout=aiohttp.ClientTimeout(total=300),
2090
+ ) as response:
2091
+ response.raise_for_status()
2092
+ return
2093
+
2094
+ raise ValueError(f"Unsupported artifact upload target transport: {kind or 'unknown'}.")
2095
+
2096
+
2097
+ def _worker_artifact_upload_headers(transport: Mapping[str, Any]) -> dict[str, str]:
2098
+ raw_headers = transport.get("headers")
2099
+ if not isinstance(raw_headers, Mapping):
2100
+ return {}
2101
+ return {
2102
+ str(key): str(value)
2103
+ for key, value in raw_headers.items()
2104
+ if value is not None and str(key).lower() != "content-type"
2105
+ }
2106
+
2107
+
2108
+ def _optional_int(value: Any) -> int | None:
2109
+ try:
2110
+ return int(value)
2111
+ except (TypeError, ValueError):
2112
+ return None
2113
+
2114
+
1952
2115
  def _record_proxy_run_error(record: ProxyRuntimeRun, payload: dict[str, Any]) -> None:
1953
2116
  record.status = "error"
1954
2117
  record.error = str(payload.get("message") or payload.get("error") or "Proxy run failed.")
@@ -12,12 +12,13 @@ import binascii
12
12
  import json
13
13
  import sqlite3
14
14
  from dataclasses import dataclass
15
- from datetime import UTC, datetime
15
+ from datetime import datetime, timezone
16
16
  from pathlib import Path
17
17
  from typing import Any
18
18
 
19
19
  SERVE_STATE_SCHEMA_VERSION = 2
20
20
  GALLERY_CURSOR_VERSION = 1
21
+ UTC = timezone.utc
21
22
 
22
23
 
23
24
  def utc_now() -> str: