Skip to content

Commit e22eaf7

Browse files
coderfromthenorth93coderfromthenorth93
andauthored
Send additional metadata to registry service during node creation (#279)
* Parse the supporting metadata and send to backend while publishing a node * Add/update tests * fix ruff formatting * fix bug --------- Co-authored-by: coderfromthenorth93 <[email protected]>
1 parent 2e36f33 commit e22eaf7

File tree

4 files changed

+311
-2
lines changed

4 files changed

+311
-2
lines changed

comfy_cli/registry/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,18 @@ def publish_node_version(self, node_config: PyProjectConfig, token) -> PublishNo
5454
"name": node_config.tool_comfy.display_name,
5555
"license": license_json,
5656
"repository": node_config.project.urls.repository,
57+
"supported_os": node_config.project.supported_os,
58+
"supported_accelerators": node_config.project.supported_accelerators,
59+
"supported_comfyui_version": node_config.project.supported_comfyui_version,
60+
"supported_comfyui_frontend_version": node_config.project.supported_comfyui_frontend_version,
5761
},
5862
"node_version": {
5963
"version": node_config.project.version,
6064
"dependencies": node_config.project.dependencies,
65+
"supported_os": node_config.project.supported_os,
66+
"supported_accelerators": node_config.project.supported_accelerators,
67+
"supported_comfyui_version": node_config.project.supported_comfyui_version,
68+
"supported_comfyui_frontend_version": node_config.project.supported_comfyui_frontend_version,
6169
},
6270
}
6371
print(request_body)

comfy_cli/registry/config_parser.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import subprocess
34
from typing import Optional
45

@@ -87,6 +88,76 @@ def sanitize_node_name(name: str) -> str:
8788
return name
8889

8990

91+
def validate_and_extract_os_classifiers(classifiers: list) -> list:
92+
os_classifiers = [c for c in classifiers if c.startswith("Operating System :: ")]
93+
if not os_classifiers:
94+
return []
95+
96+
os_values = [c[len("Operating System :: ") :] for c in os_classifiers]
97+
valid_os_prefixes = {"Microsoft", "POSIX", "MacOS", "OS Independent"}
98+
99+
for os_value in os_values:
100+
if not any(os_value.startswith(prefix) for prefix in valid_os_prefixes):
101+
typer.echo(
102+
'Warning: Invalid Operating System classifier found. Operating System classifiers must start with one of: "Microsoft", "POSIX", "MacOS", "OS Independent". '
103+
'Examples: "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: OS Independent". '
104+
"No OS information will be populated."
105+
)
106+
return []
107+
108+
return os_values
109+
110+
111+
def validate_and_extract_accelerator_classifiers(classifiers: list) -> list:
112+
accelerator_classifiers = [c for c in classifiers if c.startswith("Environment ::")]
113+
if not accelerator_classifiers:
114+
return []
115+
116+
accelerator_values = [c[len("Environment :: ") :] for c in accelerator_classifiers]
117+
118+
valid_accelerators = {
119+
"GPU :: NVIDIA CUDA",
120+
"GPU :: AMD ROCm",
121+
"GPU :: Intel Arc",
122+
"NPU :: Huawei Ascend",
123+
"GPU :: Apple Metal",
124+
}
125+
126+
for accelerator_value in accelerator_values:
127+
if accelerator_value not in valid_accelerators:
128+
typer.echo(
129+
"Warning: Invalid Environment classifier found. Environment classifiers must be one of: "
130+
'"Environment :: GPU :: NVIDIA CUDA", "Environment :: GPU :: AMD ROCm", "Environment :: GPU :: Intel Arc", '
131+
'"Environment :: NPU :: Huawei Ascend", "Environment :: GPU :: Apple Metal". '
132+
"No accelerator information will be populated."
133+
)
134+
return []
135+
136+
return accelerator_values
137+
138+
139+
def validate_version(version: str, field_name: str) -> str:
140+
if not version:
141+
return version
142+
143+
version_pattern = r"^(?:(==|>=|<=|!=|~=|>|<|<>|=)\s*)?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)?$"
144+
145+
version_parts = [part.strip() for part in version.split(",")]
146+
for part in version_parts:
147+
if not re.match(version_pattern, part):
148+
typer.echo(
149+
f'Warning: Invalid {field_name} format: "{version}". '
150+
f"Each version part must follow the pattern: [operator][version] where operator is optional (==, >=, <=, !=, ~=, >, <, <>, =) "
151+
f"and version is in format major.minor.patch[-suffix]. "
152+
f"Multiple versions can be comma-separated. "
153+
f'Examples: ">=1.0.0", "==2.1.0-beta", "1.5.2", ">=1.0.0,<2.0.0". '
154+
f"No {field_name} will be populated."
155+
)
156+
return ""
157+
158+
return version
159+
160+
90161
def initialize_project_config():
91162
create_comfynode_config()
92163

@@ -157,6 +228,28 @@ def extract_node_configuration(
157228
urls_data = project_data.get("urls", {})
158229
comfy_data = data.get("tool", {}).get("comfy", {})
159230

231+
dependencies = project_data.get("dependencies", [])
232+
supported_comfyui_frontend_version = ""
233+
for dep in dependencies:
234+
if isinstance(dep, str) and dep.startswith("comfyui-frontend-package"):
235+
supported_comfyui_frontend_version = dep.removeprefix("comfyui-frontend-package")
236+
break
237+
238+
# Remove the ComfyUI-frontend dependency from the dependencies list
239+
dependencies = [
240+
dep for dep in dependencies if not (isinstance(dep, str) and dep.startswith("comfyui-frontend-package"))
241+
]
242+
243+
supported_comfyui_version = data.get("tool", {}).get("comfy", {}).get("requires-comfyui", "")
244+
245+
classifiers = project_data.get("classifiers", [])
246+
supported_os = validate_and_extract_os_classifiers(classifiers)
247+
supported_accelerators = validate_and_extract_accelerator_classifiers(classifiers)
248+
supported_comfyui_version = validate_version(supported_comfyui_version, "requires-comfyui")
249+
supported_comfyui_frontend_version = validate_version(
250+
supported_comfyui_frontend_version, "comfyui-frontend-package"
251+
)
252+
160253
license_data = project_data.get("license", {})
161254
if isinstance(license_data, str):
162255
license = License(text=license_data)
@@ -182,14 +275,18 @@ def extract_node_configuration(
182275
description=project_data.get("description", ""),
183276
version=project_data.get("version", ""),
184277
requires_python=project_data.get("requires-python", ""),
185-
dependencies=project_data.get("dependencies", []),
278+
dependencies=dependencies,
186279
license=license,
187280
urls=URLs(
188281
homepage=urls_data.get("Homepage", ""),
189282
documentation=urls_data.get("Documentation", ""),
190283
repository=urls_data.get("Repository", ""),
191284
issues=urls_data.get("Issues", ""),
192285
),
286+
supported_os=supported_os,
287+
supported_accelerators=supported_accelerators,
288+
supported_comfyui_version=supported_comfyui_version,
289+
supported_comfyui_frontend_version=supported_comfyui_frontend_version,
193290
)
194291

195292
comfy = ComfyConfig(

comfy_cli/registry/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ class ProjectConfig:
6969
dependencies: List[str] = field(default_factory=list)
7070
license: License = field(default_factory=License)
7171
urls: URLs = field(default_factory=URLs)
72+
supported_os: List[str] = field(default_factory=list)
73+
supported_accelerators: List[str] = field(default_factory=list)
74+
supported_comfyui_version: str = ""
75+
supported_comfyui_frontend_version: str = ""
7276

7377

7478
@dataclass

tests/comfy_cli/registry/test_config_parser.py

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import pytest
44

5-
from comfy_cli.registry.config_parser import extract_node_configuration
5+
from comfy_cli.registry.config_parser import (
6+
extract_node_configuration,
7+
validate_and_extract_accelerator_classifiers,
8+
validate_and_extract_os_classifiers,
9+
validate_version,
10+
)
611
from comfy_cli.registry.types import (
712
License,
813
Model,
@@ -127,3 +132,198 @@ def test_extract_license_incorrect_format():
127132
assert result is not None, "Expected PyProjectConfig, got None"
128133
assert isinstance(result, PyProjectConfig)
129134
assert result.project.license == License(text="MIT")
135+
136+
137+
def test_extract_node_configuration_with_os_classifiers():
138+
mock_data = {
139+
"project": {
140+
"classifiers": [
141+
"Operating System :: OS Independent",
142+
"Operating System :: Microsoft :: Windows",
143+
"Programming Language :: Python :: 3",
144+
"Topic :: Software Development",
145+
]
146+
}
147+
}
148+
with (
149+
patch("os.path.isfile", return_value=True),
150+
patch("builtins.open", mock_open()),
151+
patch("tomlkit.load", return_value=mock_data),
152+
):
153+
result = extract_node_configuration("fake_path.toml")
154+
155+
assert result is not None
156+
assert len(result.project.supported_os) == 2
157+
assert "OS Independent" in result.project.supported_os
158+
assert "Microsoft :: Windows" in result.project.supported_os
159+
160+
161+
def test_extract_node_configuration_with_accelerator_classifiers():
162+
mock_data = {
163+
"project": {
164+
"classifiers": [
165+
"Environment :: GPU :: NVIDIA CUDA",
166+
"Environment :: GPU :: AMD ROCm",
167+
"Environment :: GPU :: Intel Arc",
168+
"Environment :: NPU :: Huawei Ascend",
169+
"Environment :: GPU :: Apple Metal",
170+
"Programming Language :: Python :: 3",
171+
"Topic :: Software Development",
172+
]
173+
}
174+
}
175+
with (
176+
patch("os.path.isfile", return_value=True),
177+
patch("builtins.open", mock_open()),
178+
patch("tomlkit.load", return_value=mock_data),
179+
):
180+
result = extract_node_configuration("fake_path.toml")
181+
182+
assert result is not None
183+
assert len(result.project.supported_accelerators) == 5
184+
assert "GPU :: NVIDIA CUDA" in result.project.supported_accelerators
185+
assert "GPU :: AMD ROCm" in result.project.supported_accelerators
186+
assert "GPU :: Intel Arc" in result.project.supported_accelerators
187+
assert "NPU :: Huawei Ascend" in result.project.supported_accelerators
188+
assert "GPU :: Apple Metal" in result.project.supported_accelerators
189+
190+
191+
def test_extract_node_configuration_with_comfyui_version():
192+
mock_data = {"project": {"dependencies": ["packge1>=2.0.0", "comfyui-frontend-package>=1.2.3", "package2>=1.0.0"]}}
193+
with (
194+
patch("os.path.isfile", return_value=True),
195+
patch("builtins.open", mock_open()),
196+
patch("tomlkit.load", return_value=mock_data),
197+
):
198+
result = extract_node_configuration("fake_path.toml")
199+
200+
assert result is not None
201+
assert result.project.supported_comfyui_frontend_version == ">=1.2.3"
202+
assert len(result.project.dependencies) == 2
203+
assert "comfyui-frontend-package>=1.2.3" not in result.project.dependencies
204+
assert "packge1>=2.0.0" in result.project.dependencies
205+
assert "package2>=1.0.0" in result.project.dependencies
206+
207+
208+
def test_extract_node_configuration_with_requires_comfyui():
209+
mock_data = {"project": {}, "tool": {"comfy": {"requires-comfyui": "2.0.0"}}}
210+
with (
211+
patch("os.path.isfile", return_value=True),
212+
patch("builtins.open", mock_open()),
213+
patch("tomlkit.load", return_value=mock_data),
214+
):
215+
result = extract_node_configuration("fake_path.toml")
216+
217+
assert result is not None
218+
assert result.project.supported_comfyui_version == "2.0.0"
219+
220+
221+
def test_validate_and_extract_os_classifiers_valid():
222+
"""Test OS validation with valid classifiers."""
223+
classifiers = [
224+
"Operating System :: Microsoft :: Windows",
225+
"Operating System :: POSIX :: Linux",
226+
"Operating System :: MacOS",
227+
"Operating System :: OS Independent",
228+
"Programming Language :: Python :: 3",
229+
]
230+
result = validate_and_extract_os_classifiers(classifiers)
231+
expected = ["Microsoft :: Windows", "POSIX :: Linux", "MacOS", "OS Independent"]
232+
assert result == expected
233+
234+
235+
@patch("typer.echo")
236+
def test_validate_and_extract_os_classifiers_invalid(mock_echo):
237+
"""Test OS validation with invalid classifiers."""
238+
classifiers = [
239+
"Operating System :: Microsoft :: Windows",
240+
"Operating System :: Linux", # Invalid - should be "POSIX :: Linux"
241+
"Programming Language :: Python :: 3",
242+
]
243+
result = validate_and_extract_os_classifiers(classifiers)
244+
assert result == []
245+
mock_echo.assert_called_once()
246+
assert "Invalid Operating System classifier found" in mock_echo.call_args[0][0]
247+
248+
249+
def test_validate_and_extract_accelerator_classifiers_valid():
250+
"""Test accelerator validation with valid classifiers."""
251+
classifiers = [
252+
"Environment :: GPU :: NVIDIA CUDA",
253+
"Environment :: GPU :: AMD ROCm",
254+
"Environment :: GPU :: Intel Arc",
255+
"Environment :: NPU :: Huawei Ascend",
256+
"Environment :: GPU :: Apple Metal",
257+
"Programming Language :: Python :: 3",
258+
]
259+
result = validate_and_extract_accelerator_classifiers(classifiers)
260+
expected = [
261+
"GPU :: NVIDIA CUDA",
262+
"GPU :: AMD ROCm",
263+
"GPU :: Intel Arc",
264+
"NPU :: Huawei Ascend",
265+
"GPU :: Apple Metal",
266+
]
267+
assert result == expected
268+
269+
270+
@patch("typer.echo")
271+
def test_validate_and_extract_accelerator_classifiers_invalid(mock_echo):
272+
"""Test accelerator validation with invalid classifiers."""
273+
classifiers = [
274+
"Environment :: GPU :: NVIDIA CUDA",
275+
"Environment :: GPU :: Invalid GPU", # Invalid
276+
"Programming Language :: Python :: 3",
277+
]
278+
result = validate_and_extract_accelerator_classifiers(classifiers)
279+
assert result == []
280+
mock_echo.assert_called_once()
281+
assert "Invalid Environment classifier found" in mock_echo.call_args[0][0]
282+
283+
284+
def test_validate_version_valid():
285+
"""Test version validation with valid versions."""
286+
valid_versions = [
287+
"1.1.1",
288+
">=1.0.0",
289+
"==2.1.0-beta",
290+
"1.5.2",
291+
"~=3.0.0",
292+
"!=1.2.3",
293+
">2.0.0",
294+
"<3.0.0",
295+
"<=4.0.0",
296+
"<>1.0.0",
297+
"=1.0.0",
298+
"1.0.0-alpha1",
299+
">=1.0.0,<2.0.0",
300+
"==1.2.3,!=1.2.4",
301+
">=1.0.0,<=2.0.0,!=1.5.0",
302+
"1.0.0,2.0.0",
303+
">1.0.0,<2.0.0,!=1.5.0-beta",
304+
]
305+
306+
for version in valid_versions:
307+
result = validate_version(version, "test_field")
308+
assert result == version, f"Version {version} should be valid"
309+
310+
311+
@patch("typer.echo")
312+
def test_validate_version_invalid(mock_echo):
313+
"""Test version validation with invalid versions."""
314+
invalid_versions = [
315+
"1.0", # Missing patch version
316+
">=abc", # Invalid version format
317+
"invalid-version", # Completely invalid
318+
"1.0.0.0", # Too many version parts
319+
">>1.0.0", # Invalid operator
320+
">=1.0.0,invalid",
321+
"1.0,2.0.0",
322+
">=1.0.0,>=abc",
323+
]
324+
325+
for version in invalid_versions:
326+
result = validate_version(version, "test_field")
327+
assert result == "", f"Version {version} should be invalid"
328+
329+
assert mock_echo.call_count == len(invalid_versions)

0 commit comments

Comments
 (0)