Make signature artifact timestamps deterministic
This commit is contained in:
@@ -2,7 +2,9 @@ import json
|
|||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from tools.generate_popular_node_signatures import (
|
from tools.generate_popular_node_signatures import (
|
||||||
extract_repo_signatures,
|
extract_repo_signatures,
|
||||||
@@ -12,10 +14,6 @@ from tools.generate_popular_node_signatures import (
|
|||||||
|
|
||||||
|
|
||||||
class StaticExtractionTests(unittest.TestCase):
|
class StaticExtractionTests(unittest.TestCase):
|
||||||
def _normalise_generated_at(self, text):
|
|
||||||
parsed = json.loads(text)
|
|
||||||
return text.replace(parsed["generated_at"], "<generated-at>")
|
|
||||||
|
|
||||||
def _extract_source(self, source, pack_id="sample-pack"):
|
def _extract_source(self, source, pack_id="sample-pack"):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
Path(tmp, "__init__.py").write_text(textwrap.dedent(source), encoding="utf-8")
|
Path(tmp, "__init__.py").write_text(textwrap.dedent(source), encoding="utf-8")
|
||||||
@@ -4766,108 +4764,136 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
self.assertEqual("no_static_nodes", result["pack"]["status"])
|
self.assertEqual("no_static_nodes", result["pack"]["status"])
|
||||||
|
|
||||||
def test_write_artifact_is_deterministic(self):
|
def test_write_artifact_is_deterministic(self):
|
||||||
|
class FakeDateTime:
|
||||||
|
values = iter(
|
||||||
|
(
|
||||||
|
datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||||
|
datetime(2026, 1, 2, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz=None):
|
||||||
|
value = next(cls.values)
|
||||||
|
return value if tz is None else value.astimezone(tz)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
out_one = Path(tmp, "one.json")
|
out_one = Path(tmp, "one.json")
|
||||||
out_two = Path(tmp, "two.json")
|
out_two = Path(tmp, "two.json")
|
||||||
write_artifact(
|
with mock.patch("tools.generate_popular_node_signatures.datetime", FakeDateTime):
|
||||||
out_one,
|
write_artifact(
|
||||||
sources={
|
out_one,
|
||||||
"manager_url": "https://example.invalid/manager.json",
|
sources={
|
||||||
"limit": 1,
|
"manager_url": "https://example.invalid/manager.json",
|
||||||
"registry": {"z": "last", "a": "first"},
|
"limit": 1,
|
||||||
},
|
"registry": {"z": "last", "a": "first"},
|
||||||
packs={
|
|
||||||
"b-pack": {
|
|
||||||
"id": "b-pack",
|
|
||||||
"title": "B Pack",
|
|
||||||
"status": "ok",
|
|
||||||
"metadata": {"z": 2, "a": 1},
|
|
||||||
},
|
},
|
||||||
"a-pack": {
|
packs={
|
||||||
"id": "a-pack",
|
"b-pack": {
|
||||||
"title": "A Pack",
|
"id": "b-pack",
|
||||||
"status": "ok",
|
"title": "B Pack",
|
||||||
"metadata": {"z": 4, "a": 3},
|
"status": "ok",
|
||||||
|
"metadata": {"z": 2, "a": 1},
|
||||||
|
},
|
||||||
|
"a-pack": {
|
||||||
|
"id": "a-pack",
|
||||||
|
"title": "A Pack",
|
||||||
|
"status": "ok",
|
||||||
|
"metadata": {"z": 4, "a": 3},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
nodes={
|
||||||
nodes={
|
"BNode": {
|
||||||
"BNode": {
|
"type": "BNode",
|
||||||
"type": "BNode",
|
"display": "B Node",
|
||||||
"display": "B Node",
|
"pack": "b-pack",
|
||||||
"pack": "b-pack",
|
"repository": "https://github.com/example/b-pack",
|
||||||
"repository": "https://github.com/example/b-pack",
|
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
|
||||||
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
|
"required": [],
|
||||||
"required": [],
|
"outputs": ["IMAGE"],
|
||||||
"outputs": ["IMAGE"],
|
"output_names": ["image"],
|
||||||
"output_names": ["image"],
|
"confidence": "static_exact",
|
||||||
"confidence": "static_exact",
|
},
|
||||||
|
"ANode": {
|
||||||
|
"type": "ANode",
|
||||||
|
"display": "A Node",
|
||||||
|
"pack": "a-pack",
|
||||||
|
"repository": "https://github.com/example/a-pack",
|
||||||
|
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
|
||||||
|
"required": [],
|
||||||
|
"outputs": ["IMAGE"],
|
||||||
|
"output_names": ["image"],
|
||||||
|
"confidence": "static_exact",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"ANode": {
|
)
|
||||||
"type": "ANode",
|
write_artifact(
|
||||||
"display": "A Node",
|
out_two,
|
||||||
"pack": "a-pack",
|
sources={
|
||||||
"repository": "https://github.com/example/a-pack",
|
"registry": {"a": "first", "z": "last"},
|
||||||
"inputs": {"zeta": "FLOAT", "alpha": "IMAGE"},
|
"limit": 1,
|
||||||
"required": [],
|
"manager_url": "https://example.invalid/manager.json",
|
||||||
"outputs": ["IMAGE"],
|
|
||||||
"output_names": ["image"],
|
|
||||||
"confidence": "static_exact",
|
|
||||||
},
|
},
|
||||||
},
|
packs={
|
||||||
)
|
"a-pack": {
|
||||||
write_artifact(
|
"metadata": {"a": 3, "z": 4},
|
||||||
out_two,
|
"status": "ok",
|
||||||
sources={
|
"title": "A Pack",
|
||||||
"registry": {"a": "first", "z": "last"},
|
"id": "a-pack",
|
||||||
"limit": 1,
|
},
|
||||||
"manager_url": "https://example.invalid/manager.json",
|
"b-pack": {
|
||||||
},
|
"metadata": {"a": 1, "z": 2},
|
||||||
packs={
|
"status": "ok",
|
||||||
"a-pack": {
|
"title": "B Pack",
|
||||||
"metadata": {"a": 3, "z": 4},
|
"id": "b-pack",
|
||||||
"status": "ok",
|
},
|
||||||
"title": "A Pack",
|
|
||||||
"id": "a-pack",
|
|
||||||
},
|
},
|
||||||
"b-pack": {
|
nodes={
|
||||||
"metadata": {"a": 1, "z": 2},
|
"ANode": {
|
||||||
"status": "ok",
|
"confidence": "static_exact",
|
||||||
"title": "B Pack",
|
"output_names": ["image"],
|
||||||
"id": "b-pack",
|
"outputs": ["IMAGE"],
|
||||||
|
"required": [],
|
||||||
|
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
|
||||||
|
"repository": "https://github.com/example/a-pack",
|
||||||
|
"pack": "a-pack",
|
||||||
|
"display": "A Node",
|
||||||
|
"type": "ANode",
|
||||||
|
},
|
||||||
|
"BNode": {
|
||||||
|
"confidence": "static_exact",
|
||||||
|
"output_names": ["image"],
|
||||||
|
"outputs": ["IMAGE"],
|
||||||
|
"required": [],
|
||||||
|
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
|
||||||
|
"repository": "https://github.com/example/b-pack",
|
||||||
|
"pack": "b-pack",
|
||||||
|
"display": "B Node",
|
||||||
|
"type": "BNode",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
nodes={
|
|
||||||
"ANode": {
|
|
||||||
"confidence": "static_exact",
|
|
||||||
"output_names": ["image"],
|
|
||||||
"outputs": ["IMAGE"],
|
|
||||||
"required": [],
|
|
||||||
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
|
|
||||||
"repository": "https://github.com/example/a-pack",
|
|
||||||
"pack": "a-pack",
|
|
||||||
"display": "A Node",
|
|
||||||
"type": "ANode",
|
|
||||||
},
|
|
||||||
"BNode": {
|
|
||||||
"confidence": "static_exact",
|
|
||||||
"output_names": ["image"],
|
|
||||||
"outputs": ["IMAGE"],
|
|
||||||
"required": [],
|
|
||||||
"inputs": {"alpha": "IMAGE", "zeta": "FLOAT"},
|
|
||||||
"repository": "https://github.com/example/b-pack",
|
|
||||||
"pack": "b-pack",
|
|
||||||
"display": "B Node",
|
|
||||||
"type": "BNode",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
text_one = out_one.read_text(encoding="utf-8")
|
text_one = out_one.read_text(encoding="utf-8")
|
||||||
text_two = out_two.read_text(encoding="utf-8")
|
text_two = out_two.read_text(encoding="utf-8")
|
||||||
parsed = json.loads(text_one)
|
parsed = json.loads(text_one)
|
||||||
|
|
||||||
self.assertEqual(["a-pack", "b-pack"], list(parsed["packs"]))
|
self.assertEqual(["a-pack", "b-pack"], list(parsed["packs"]))
|
||||||
self.assertEqual(["ANode", "BNode"], list(parsed["nodes"]))
|
self.assertEqual(["ANode", "BNode"], list(parsed["nodes"]))
|
||||||
self.assertEqual(self._normalise_generated_at(text_one), self._normalise_generated_at(text_two))
|
self.assertEqual(text_one, text_two)
|
||||||
|
|
||||||
|
def test_write_artifact_uses_explicit_generated_at(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
out = Path(tmp, "popular_node_signatures.json")
|
||||||
|
write_artifact(
|
||||||
|
out,
|
||||||
|
sources={"manager_url": "https://example.invalid/manager.json", "limit": 1},
|
||||||
|
packs={},
|
||||||
|
nodes={},
|
||||||
|
generated_at="2026-07-02T00:00:00Z",
|
||||||
|
)
|
||||||
|
parsed = json.loads(out.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertEqual("2026-07-02T00:00:00Z", parsed["generated_at"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
|||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 1
|
||||||
MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json"
|
MANAGER_LIST_URL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json"
|
||||||
REGISTRY_NODES_URL = "https://api.comfy.org/nodes"
|
REGISTRY_NODES_URL = "https://api.comfy.org/nodes"
|
||||||
|
DEFAULT_GENERATED_AT = "1970-01-01T00:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedStaticExpression(Exception):
|
class UnsupportedStaticExpression(Exception):
|
||||||
@@ -2410,10 +2411,20 @@ def _sorted_json_value(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def write_artifact(path, sources, packs, nodes):
|
def _format_generated_at(generated_at):
|
||||||
|
if isinstance(generated_at, datetime):
|
||||||
|
if generated_at.tzinfo is None:
|
||||||
|
generated_at = generated_at.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
generated_at = generated_at.astimezone(timezone.utc)
|
||||||
|
return generated_at.replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
return str(generated_at)
|
||||||
|
|
||||||
|
|
||||||
|
def write_artifact(path, sources, packs, nodes, *, generated_at=DEFAULT_GENERATED_AT):
|
||||||
payload = {
|
payload = {
|
||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
"generated_at": _format_generated_at(generated_at),
|
||||||
"sources": _sorted_json_value(sources),
|
"sources": _sorted_json_value(sources),
|
||||||
"packs": _sorted_json_value(packs),
|
"packs": _sorted_json_value(packs),
|
||||||
"nodes": _sorted_json_value(nodes),
|
"nodes": _sorted_json_value(nodes),
|
||||||
|
|||||||
Reference in New Issue
Block a user