Merge pull request #25 from StableLlama/time_nodes

Time nodes
This commit is contained in:
StableLlama
2025-07-17 21:29:10 +02:00
committed by GitHub
4 changed files with 516 additions and 4 deletions
+4 -3
View File
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "basic_data_handling"
version = "0.4.7"
version = "0.5.0"
description = """Basic Python functions for manipulating data that every programmer is used to, lightweight with no additional dependencies.
Supported data types:
- ComfyUI native: BOOLEAN, FLOAT, INT, STRING, and data lists
- Python types as custom data types: DICT, LIST, SET
- Python types as custom data types: DICT, LIST, SET, DATETIME, TIMEDELTA
Feature categories:
- Boolean logic operations
@@ -21,7 +21,8 @@ Feature categories:
- Mathematical formula node in a safe implementation
- String manipulation
- File system path handling, including STRING, IMAGE and MASK load and save
- SET operations"""
- SET operations
- time and date handling"""
authors = [
{name = "StableLlama"}
]
+3 -1
View File
@@ -1,7 +1,7 @@
from . import (boolean_nodes, casting_nodes, comparison_nodes, control_flow_nodes,
data_list_nodes, dict_nodes, float_nodes, int_nodes, list_nodes,
math_nodes, math_formula_node, path_nodes, regex_nodes, set_nodes,
string_nodes)
string_nodes, time_nodes)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS)
@@ -19,6 +19,7 @@ NODE_CLASS_MAPPINGS.update(set_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(math_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(math_formula_node.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(string_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(time_nodes.NODE_CLASS_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(boolean_nodes.NODE_DISPLAY_NAME_MAPPINGS)
@@ -36,3 +37,4 @@ NODE_DISPLAY_NAME_MAPPINGS.update(set_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(math_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(math_formula_node.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(string_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(time_nodes.NODE_DISPLAY_NAME_MAPPINGS)
+312
View File
@@ -0,0 +1,312 @@
from inspect import cleandoc
import datetime
import time
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except ImportError:
# Define dummy classes if comfy is not available
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
FLOAT = "FLOAT"
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
# Add custom IO types for DATETIME and TIMEDELTA
IO.DATETIME = "DATETIME"
IO.TIMEDELTA = "TIMEDELTA"
class TimeNow(ComfyNodeABC):
"""
Returns the current time and date as a DATETIME object.
Note: Output changes for every run, providing a fresh timestamp each time.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"trigger": (IO.ANY, {"description": "Optional input to trigger execution"})
}
}
RETURN_TYPES = (IO.DATETIME,)
RETURN_NAMES = ("now",)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_now"
@classmethod
def IS_CHANGED(s, **kwargs):
# Always return a changing value to indicate the output changes every run
return time.time()
def get_now(self, trigger=None) -> tuple[datetime.datetime]:
"""
Retrieves the current system time.
The optional trigger input can be used to trigger execution.
"""
return (datetime.datetime.now(),)
class TimeToUnix(ComfyNodeABC):
"""
Converts a DATETIME object to a Unix timestamp (a float representing seconds since the epoch).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"datetime": (IO.DATETIME, {}),
}
}
RETURN_TYPES = (IO.FLOAT,)
RETURN_NAMES = ("unix_timestamp",)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "to_unix"
def to_unix(self, datetime: datetime.datetime) -> tuple[float]:
"""
Converts the given datetime object to a Unix timestamp.
"""
return (datetime.timestamp(),)
class UnixToTime(ComfyNodeABC):
"""
Converts a Unix timestamp (float or int) to a DATETIME object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"unix_timestamp": (IO.NUMBER, {"default": 0.0}),
}
}
RETURN_TYPES = (IO.DATETIME,)
RETURN_NAMES = ("datetime",)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "from_unix"
def from_unix(self, unix_timestamp: float) -> tuple[datetime.datetime]:
"""
Creates a datetime object from a Unix timestamp.
"""
return (datetime.datetime.fromtimestamp(unix_timestamp),)
class TimeFormat(ComfyNodeABC):
"""
Formats a DATETIME object into a string using a specified format code.
Common format codes: %Y (year), %m (month), %d (day), %H (hour), %M (minute), %S (second).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"datetime": (IO.DATETIME, {}),
"format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S"}),
}
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("formatted_string",)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "format_time"
def format_time(self, datetime: datetime.datetime, format_string: str) -> tuple[str]:
"""
Formats a datetime object into a string.
"""
return (datetime.strftime(format_string),)
class TimeParse(ComfyNodeABC):
"""
Parses a string containing a date and time into a DATETIME object, using a specified format code.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"time_string": (IO.STRING, {}),
"format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S"}),
}
}
RETURN_TYPES = (IO.DATETIME,)
RETURN_NAMES = ("datetime",)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "parse_time"
def parse_time(self, time_string: str, format_string: str) -> tuple[datetime.datetime]:
"""
Parses a string into a datetime object.
"""
return (datetime.datetime.strptime(time_string, format_string),)
class TimeDelta(ComfyNodeABC):
"""
Creates a TIMEDELTA object, which represents a duration and can be used for date calculations.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"days": (IO.FLOAT, {"default": 0}),
"seconds": (IO.FLOAT, {"default": 0}),
"microseconds": (IO.FLOAT, {"default": 0}),
"milliseconds": (IO.FLOAT, {"default": 0}),
"minutes": (IO.FLOAT, {"default": 0}),
"hours": (IO.FLOAT, {"default": 0}),
"weeks": (IO.FLOAT, {"default": 0}),
}
}
RETURN_TYPES = (IO.TIMEDELTA,)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_delta"
def create_delta(self, **kwargs: float) -> tuple[datetime.timedelta]:
"""
Creates a timedelta object from the given durations.
"""
return (datetime.timedelta(**kwargs),)
class TimeAddDelta(ComfyNodeABC):
"""
Adds a TIMEDELTA (duration) to a DATETIME object, resulting in a new DATETIME.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"datetime": (IO.DATETIME, {}),
"delta": (IO.TIMEDELTA, {}),
}
}
RETURN_TYPES = (IO.DATETIME,)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "add"
def add(self, datetime: datetime.datetime, delta: datetime.timedelta) -> tuple[datetime.datetime]:
"""
Adds a timedelta to a datetime object.
"""
return (datetime + delta,)
class TimeSubtractDelta(ComfyNodeABC):
"""
Subtracts a TIMEDELTA (duration) from a DATETIME object, resulting in a new DATETIME.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"datetime": (IO.DATETIME, {}),
"delta": (IO.TIMEDELTA, {}),
}
}
RETURN_TYPES = (IO.DATETIME,)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "subtract"
def subtract(self, datetime: datetime.datetime, delta: datetime.timedelta) -> tuple[datetime.datetime]:
"""
Subtracts a timedelta from a datetime object.
"""
return (datetime - delta,)
class TimeDifference(ComfyNodeABC):
"""
Calculates the difference between two DATETIME objects, returning a TIMEDELTA object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"datetime1": (IO.DATETIME, {}),
"datetime2": (IO.DATETIME, {}),
}
}
RETURN_TYPES = (IO.TIMEDELTA,)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "difference"
def difference(self, datetime1: datetime.datetime, datetime2: datetime.datetime) -> tuple[datetime.timedelta]:
"""
Calculates the duration between two datetime objects.
"""
return (datetime1 - datetime2,)
class TimeExtract(ComfyNodeABC):
"""
Extracts individual components (year, month, day, hour, etc.) from a DATETIME object.
Weekday is returned as an integer, where Monday is 0 and Sunday is 6.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "datetime": (IO.DATETIME, {}) }
}
RETURN_TYPES = (IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT)
RETURN_NAMES = ("year", "month", "day", "hour", "minute", "second", "microsecond", "weekday")
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "extract"
def extract(self, datetime: datetime.datetime) -> tuple[int, int, int, int, int, int, int, int]:
"""
Extracts all time components from a datetime object.
"""
return (datetime.year, datetime.month, datetime.day, datetime.hour,
datetime.minute, datetime.second, datetime.microsecond, datetime.weekday())
NODE_CLASS_MAPPINGS = {
"Basic data handling: TimeNow": TimeNow,
"Basic data handling: TimeToUnix": TimeToUnix,
"Basic data handling: UnixToTime": UnixToTime,
"Basic data handling: TimeFormat": TimeFormat,
"Basic data handling: TimeParse": TimeParse,
"Basic data handling: TimeDelta": TimeDelta,
"Basic data handling: TimeAddDelta": TimeAddDelta,
"Basic data handling: TimeSubtractDelta": TimeSubtractDelta,
"Basic data handling: TimeDifference": TimeDifference,
"Basic data handling: TimeExtract": TimeExtract,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: TimeNow": "Time Now",
"Basic data handling: TimeToUnix": "Time to Unix Timestamp",
"Basic data handling: UnixToTime": "Unix Timestamp to Time",
"Basic data handling: TimeFormat": "Format Time String",
"Basic data handling: TimeParse": "Parse Time String",
"Basic data handling: TimeDelta": "Create Time Delta",
"Basic data handling: TimeAddDelta": "Add Time Delta",
"Basic data handling: TimeSubtractDelta": "Subtract Time Delta",
"Basic data handling: TimeDifference": "Time Difference",
"Basic data handling: TimeExtract": "Extract Time Components",
}
+197
View File
@@ -0,0 +1,197 @@
#import pytest
from datetime import datetime, timedelta
from src.basic_data_handling.time_nodes import (
TimeNow,
TimeToUnix,
UnixToTime,
TimeFormat,
TimeParse,
TimeDelta,
TimeAddDelta,
TimeSubtractDelta,
TimeDifference,
TimeExtract,
)
def test_time_now():
node = TimeNow()
result = node.get_now()
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], datetime)
# Test with trigger parameter
result_with_trigger = node.get_now(trigger="any value")
assert isinstance(result_with_trigger, tuple)
assert len(result_with_trigger) == 1
assert isinstance(result_with_trigger[0], datetime)
def test_time_to_unix():
node = TimeToUnix()
test_datetime = datetime(2023, 1, 1, 12, 0, 0)
result = node.to_unix(test_datetime)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], float)
# 2023-01-01 12:00:00 should convert to a specific timestamp
expected_timestamp = test_datetime.timestamp()
assert result[0] == expected_timestamp
def test_unix_to_time():
node = UnixToTime()
# Test with a specific timestamp (2023-01-01 12:00:00)
test_timestamp = 1672574400.0 # This is 2023-01-01 12:00:00 in UTC
result = node.from_unix(test_timestamp)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], datetime)
# The returned datetime will be in local timezone, so hours might differ
# Just check that the date is correct and hours are reasonable
returned_dt = result[0]
assert returned_dt.year == 2023
assert returned_dt.month == 1
assert returned_dt.day == 1
# Allow for timezone differences (typically within ±12 hours)
assert 0 <= returned_dt.hour < 24
assert returned_dt.minute == 0
assert returned_dt.second == 0
def test_time_format():
node = TimeFormat()
test_datetime = datetime(2023, 1, 1, 12, 30, 45)
# Test with default format
result = node.format_time(test_datetime, "%Y-%m-%d %H:%M:%S")
assert result == ("2023-01-01 12:30:45",)
# Test with custom format
result = node.format_time(test_datetime, "%d/%m/%Y")
assert result == ("01/01/2023",)
# Test with time-only format
result = node.format_time(test_datetime, "%H:%M")
assert result == ("12:30",)
def test_time_parse():
node = TimeParse()
# Test standard format
result = node.parse_time("2023-01-01 12:30:45", "%Y-%m-%d %H:%M:%S")
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], datetime)
assert result[0] == datetime(2023, 1, 1, 12, 30, 45)
# Test custom format
result = node.parse_time("01/01/2023", "%d/%m/%Y")
assert result[0] == datetime(2023, 1, 1, 0, 0, 0)
# Test time-only format
result = node.parse_time("12:30", "%H:%M")
parsed_datetime = result[0]
assert parsed_datetime.hour == 12
assert parsed_datetime.minute == 30
def test_time_delta():
node = TimeDelta()
# Test with days only
result = node.create_delta(days=5)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], timedelta)
assert result[0].days == 5
# Test with multiple parameters
result = node.create_delta(days=1, hours=2, minutes=30)
delta = result[0]
# Convert to seconds for easier comparison
total_seconds = delta.total_seconds()
expected_seconds = 24*60*60 + 2*60*60 + 30*60 # 1 day, 2 hours, 30 minutes
assert total_seconds == expected_seconds
# Test with all parameters
result = node.create_delta(
days=1,
seconds=30,
microseconds=500,
milliseconds=100,
minutes=5,
hours=2,
weeks=1
)
delta = result[0]
# A week plus a day
assert delta.days == 8
# Convert the remainder to seconds for comparison
remainder_seconds = delta.seconds
expected_remainder = 2*60*60 + 5*60 + 30 + 0.1 # 2 hours, 5 minutes, 30 seconds, 100 milliseconds
assert abs(remainder_seconds - expected_remainder) < 1
assert delta.microseconds == 100500
def test_time_add_delta():
node = TimeAddDelta()
test_datetime = datetime(2023, 1, 1, 12, 0, 0)
test_delta = timedelta(days=1, hours=2, minutes=30)
result = node.add(test_datetime, test_delta)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], datetime)
expected_datetime = datetime(2023, 1, 2, 14, 30, 0) # 1 day, 2 hours, 30 minutes later
assert result[0] == expected_datetime
def test_time_subtract_delta():
node = TimeSubtractDelta()
test_datetime = datetime(2023, 1, 2, 14, 30, 0)
test_delta = timedelta(days=1, hours=2, minutes=30)
result = node.subtract(test_datetime, test_delta)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], datetime)
expected_datetime = datetime(2023, 1, 1, 12, 0, 0) # 1 day, 2 hours, 30 minutes earlier
assert result[0] == expected_datetime
def test_time_difference():
node = TimeDifference()
datetime1 = datetime(2023, 1, 2, 14, 30, 0)
datetime2 = datetime(2023, 1, 1, 12, 0, 0)
result = node.difference(datetime1, datetime2)
assert isinstance(result, tuple)
assert len(result) == 1
assert isinstance(result[0], timedelta)
expected_delta = timedelta(days=1, hours=2, minutes=30)
assert result[0] == expected_delta
# Test reverse order (should give negative timedelta)
result = node.difference(datetime2, datetime1)
assert result[0] == -expected_delta
def test_time_extract():
node = TimeExtract()
test_datetime = datetime(2023, 1, 2, 14, 30, 45, 123456)
result = node.extract(test_datetime)
assert isinstance(result, tuple)
assert len(result) == 8
year, month, day, hour, minute, second, microsecond, weekday = result
assert year == 2023
assert month == 1
assert day == 2
assert hour == 14
assert minute == 30
assert second == 45
assert microsecond == 123456
# January 2, 2023 was a Monday (weekday 0)
assert weekday == 0