Skip to content

Commit 1e3d5a9

Browse files
Add roboflow_core/current_time@v1 Workflow block (#2410)
* Expose NumberInRange operator in workflow builder UI The (Number) in range operator was already implemented in the evaluation engine and BinaryStatement union but was not included in the introspection export, making it invisible to the workflow builder UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Implement NumberInRange operator in query language backend Adds the NumberInRange BinaryOperator class and its evaluation lambda so the operator exposed in the workflow builder UI has a working backend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add roboflow_core/current_time@v1 Workflow block New formatter block that outputs the current date/time for a user-selected timezone. Inputs a curated IANA timezone (literal or selector, default UTC) and returns a timezone-aware timestamp plus iso_string, date, and time strings. Uses stdlib zoneinfo (backports.zoneinfo for py<3.9) and adds tzdata so the timezone database is available on slim/Windows runtimes. Curated dropdown options expose friendly UTC-offset labels via values_metadata. Registered in loader.py. Includes unit tests and a full execution-engine integration test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Remove zoneinfo backport dependency --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b54110a commit 1e3d5a9

9 files changed

Lines changed: 428 additions & 3 deletions

File tree

inference/core/workflows/core_steps/common/query_language/entities/operations.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,21 @@ class NumberLowerEqual(BinaryOperator):
765765
type: Literal["(Number) <="]
766766

767767

768+
class NumberInRange(BinaryOperator):
769+
model_config = ConfigDict(
770+
json_schema_extra={
771+
"description": "Checks if first value (number) is within the inclusive range given as second value (list of two numbers: [min, max])",
772+
"operands_number": 2,
773+
"operands_kinds": [
774+
[INTEGER_KIND, FLOAT_KIND, FLOAT_ZERO_TO_ONE_KIND],
775+
[LIST_OF_VALUES_KIND],
776+
],
777+
"output_kind": [BOOLEAN_KIND],
778+
},
779+
)
780+
type: Literal["(Number) in range"]
781+
782+
768783
class StringStartsWith(BinaryOperator):
769784
model_config = ConfigDict(
770785
json_schema_extra={
@@ -994,6 +1009,7 @@ class BinaryStatement(BaseModel):
9941009
StringContains,
9951010
StringEndsWith,
9961011
StringStartsWith,
1012+
NumberInRange,
9971013
NumberLowerEqual,
9981014
NumberLower,
9991015
NumberGreaterEqual,

inference/core/workflows/core_steps/common/query_language/evaluation_engine/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"(Number) >=": lambda a, b: a >= b,
3535
"(Number) <": lambda a, b: a < b,
3636
"(Number) <=": lambda a, b: a <= b,
37+
"(Number) in range": lambda a, b: b[0] <= a <= b[1],
3738
"(String) startsWith": lambda a, b: a.startswith(b),
3839
"(String) endsWith": lambda a, b: a.endswith(b),
3940
"(String) contains": lambda a, b: b in a,

inference/core/workflows/core_steps/common/query_language/introspection/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
NotEquals,
1717
NumberGreater,
1818
NumberGreaterEqual,
19+
NumberInRange,
1920
NumberLower,
2021
NumberLowerEqual,
2122
OperationsChain,
@@ -104,6 +105,7 @@ def prepare_operators_descriptions() -> List[OperatorDescription]:
104105
NumberLower,
105106
NumberGreaterEqual,
106107
NumberGreater,
108+
NumberInRange,
107109
NotEquals,
108110
Equals,
109111
Exists,

inference/core/workflows/core_steps/formatters/current_time/__init__.py

Whitespace-only changes.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from datetime import datetime
2+
from typing import Dict, List, Literal, Optional, Type, Union
3+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
4+
5+
from pydantic import ConfigDict, Field
6+
7+
from inference.core.workflows.execution_engine.entities.base import OutputDefinition
8+
from inference.core.workflows.execution_engine.entities.types import (
9+
STRING_KIND,
10+
TIMESTAMP_KIND,
11+
Selector,
12+
)
13+
from inference.core.workflows.prototypes.block import (
14+
BlockResult,
15+
WorkflowBlock,
16+
WorkflowBlockManifest,
17+
)
18+
19+
LONG_DESCRIPTION = """
20+
Output the current date and time for a given timezone.
21+
22+
Provide one of the curated timezone options (for example `America/New_York`,
23+
`Europe/Berlin`, or `UTC`) and the block returns the current moment in that
24+
timezone. The block produces a `timestamp` (a timezone-aware `datetime` object you
25+
can pass to other blocks), along with ready-to-use `iso_string`, `date`, and
26+
`time` strings.
27+
28+
The timezone may be a literal value typed into the block, or a reference to a workflow
29+
input or another step's output.
30+
"""
31+
32+
SHORT_DESCRIPTION = "Output the current date and time for a given timezone."
33+
TIMEZONE_OPTIONS = (
34+
("Etc/GMT+12", "UTC-12 International Date Line West"),
35+
("Pacific/Pago_Pago", "UTC-11 Samoa Time (SST)"),
36+
("Pacific/Honolulu", "UTC-10 Hawaii-Aleutian Time (HST/HAST)"),
37+
("Pacific/Marquesas", "UTC-9:30 Marquesas Time (MART)"),
38+
("America/Anchorage", "UTC-9/-8 Alaska Time (AKST/AKDT)"),
39+
("Pacific/Gambier", "UTC-9 Gambier Time (GAMT)"),
40+
("America/Los_Angeles", "UTC-8/-7 Pacific Time (PST/PDT)"),
41+
("America/Denver", "UTC-7/-6 Mountain Time (MST/MDT)"),
42+
("America/Phoenix", "UTC-7 Mountain Standard Time (MST)"),
43+
("America/Chicago", "UTC-6/-5 Central Time (CST/CDT)"),
44+
("America/Mexico_City", "UTC-6 Mexico / Central America Time (CST)"),
45+
("America/New_York", "UTC-5/-4 Eastern Time (EST/EDT)"),
46+
("America/Bogota", "UTC-5 Colombia / Peru Time (COT/PET)"),
47+
("America/Halifax", "UTC-4/-3 Atlantic Time (AST/ADT)"),
48+
("America/Puerto_Rico", "UTC-4 Atlantic Standard Time (AST)"),
49+
("America/St_Johns", "UTC-3:30/-2:30 Newfoundland Time (NST/NDT)"),
50+
("America/Sao_Paulo", "UTC-3 Brasilia Time (BRT)"),
51+
("Atlantic/South_Georgia", "UTC-2 Mid-Atlantic Time (GST)"),
52+
("Atlantic/Azores", "UTC-1/+0 Azores Time (AZOT/AZOST)"),
53+
("Atlantic/Cape_Verde", "UTC-1 Cape Verde Time (CVT)"),
54+
("UTC", "UTC+0 Greenwich Mean Time (GMT/WET)"),
55+
("Europe/London", "UTC+0/+1 UK / Western European Time (GMT/BST/WET/WEST)"),
56+
("Europe/Berlin", "UTC+1/+2 Central European Time (CET/CEST)"),
57+
("Africa/Lagos", "UTC+1 West Africa Time (WAT)"),
58+
("Europe/Kyiv", "UTC+2/+3 Eastern European Time (EET/EEST)"),
59+
("Africa/Cairo", "UTC+2/+3 Egypt Time (EET/EEST)"),
60+
("Africa/Johannesburg", "UTC+2 South Africa Time (SAST)"),
61+
("Europe/Moscow", "UTC+3 Moscow Time (MSK)"),
62+
("Europe/Istanbul", "UTC+3 Turkey Time (TRT)"),
63+
("Africa/Nairobi", "UTC+3 East Africa Time (EAT)"),
64+
("Asia/Tehran", "UTC+3:30 Iran Time (IRST)"),
65+
("Asia/Dubai", "UTC+4 Gulf Time (GST)"),
66+
("Asia/Kabul", "UTC+4:30 Afghanistan Time (AFT)"),
67+
("Asia/Karachi", "UTC+5 Pakistan Time (PKT)"),
68+
("Asia/Kolkata", "UTC+5:30 India Time (IST)"),
69+
("Asia/Kathmandu", "UTC+5:45 Nepal Time (NPT)"),
70+
("Asia/Dhaka", "UTC+6 Bangladesh Time (BST)"),
71+
("Asia/Yangon", "UTC+6:30 Myanmar Time (MMT)"),
72+
("Asia/Bangkok", "UTC+7 Indochina Time (ICT)"),
73+
("Asia/Shanghai", "UTC+8 China / Western Australia Time (CST/AWST/PHT)"),
74+
("Australia/Eucla", "UTC+8:45 Central Western Australia Time (ACWST)"),
75+
("Asia/Tokyo", "UTC+9 Japan / Korea Time (JST/KST)"),
76+
("Australia/Darwin", "UTC+9:30 Australian Central Standard Time (ACST)"),
77+
("Australia/Adelaide", "UTC+9:30/+10:30 Australian Central Time (ACST/ACDT)"),
78+
("Australia/Sydney", "UTC+10/+11 Australian Eastern Time (AEST/AEDT)"),
79+
("Pacific/Port_Moresby", "UTC+10 Papua New Guinea Time (PGT)"),
80+
("Australia/Lord_Howe", "UTC+10:30/+11 Lord Howe Time (LHST/LHDT)"),
81+
("Pacific/Guadalcanal", "UTC+11 Solomon Islands Time (SBT)"),
82+
("Pacific/Norfolk", "UTC+11/+12 Norfolk Island Time (NFT/NFDT)"),
83+
("Pacific/Fiji", "UTC+12 Fiji Time (FJT)"),
84+
("Pacific/Auckland", "UTC+12/+13 New Zealand Time (NZST/NZDT)"),
85+
("Pacific/Chatham", "UTC+12:45/+13:45 Chatham Time (CHAST/CHADT)"),
86+
("Pacific/Tongatapu", "UTC+13 Tonga Time (TOT)"),
87+
("Pacific/Apia", "UTC+13 Samoa Time (WSST)"),
88+
("Pacific/Kiritimati", "UTC+14 Line Islands Time (LINT)"),
89+
)
90+
ALLOWED_TIMEZONES = tuple(timezone for timezone, _ in TIMEZONE_OPTIONS)
91+
ALLOWED_TIMEZONE_SET = frozenset(ALLOWED_TIMEZONES)
92+
TIMEZONE_METADATA: Dict[str, Dict[str, str]] = {
93+
timezone: {"name": label} for timezone, label in TIMEZONE_OPTIONS
94+
}
95+
96+
97+
class BlockManifest(WorkflowBlockManifest):
98+
model_config = ConfigDict(
99+
json_schema_extra={
100+
"name": "Current Time",
101+
"version": "v1",
102+
"short_description": SHORT_DESCRIPTION,
103+
"long_description": LONG_DESCRIPTION,
104+
"license": "Apache-2.0",
105+
"block_type": "formatter",
106+
"ui_manifest": {
107+
"section": "advanced",
108+
"icon": "far fa-clock",
109+
"blockPriority": 10,
110+
},
111+
}
112+
)
113+
type: Literal["roboflow_core/current_time@v1"]
114+
timezone: Union[Literal[ALLOWED_TIMEZONES], Selector(kind=[STRING_KIND])] = Field( # type: ignore
115+
default="UTC",
116+
description="Curated IANA timezone name to report the current time in.",
117+
examples=["UTC", "America/New_York", "Europe/Berlin", "$inputs.timezone"],
118+
json_schema_extra={"values_metadata": TIMEZONE_METADATA},
119+
)
120+
121+
@classmethod
122+
def describe_outputs(cls) -> List[OutputDefinition]:
123+
return [
124+
OutputDefinition(name="timestamp", kind=[TIMESTAMP_KIND]),
125+
OutputDefinition(name="iso_string", kind=[STRING_KIND]),
126+
OutputDefinition(name="date", kind=[STRING_KIND]),
127+
OutputDefinition(name="time", kind=[STRING_KIND]),
128+
]
129+
130+
@classmethod
131+
def get_execution_engine_compatibility(cls) -> Optional[str]:
132+
return ">=1.3.0,<2.0.0"
133+
134+
135+
class CurrentTimeBlockV1(WorkflowBlock):
136+
137+
@classmethod
138+
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
139+
return BlockManifest
140+
141+
def run(self, timezone: str = "UTC") -> BlockResult:
142+
if timezone not in ALLOWED_TIMEZONE_SET:
143+
raise ValueError(
144+
f"`roboflow_core/current_time@v1` received unsupported timezone '{timezone}'. "
145+
"Provide one of the curated timezone options shown in the block dropdown."
146+
)
147+
try:
148+
now = datetime.now(ZoneInfo(timezone))
149+
except ZoneInfoNotFoundError as error:
150+
raise ValueError(
151+
f"`roboflow_core/current_time@v1` received unknown timezone '{timezone}'. "
152+
"Provide one of the curated timezone options shown in the block dropdown."
153+
) from error
154+
return {
155+
"timestamp": now,
156+
"iso_string": now.isoformat(),
157+
"date": now.strftime("%Y-%m-%d"),
158+
"time": now.strftime("%H:%M:%S"),
159+
}

inference/core/workflows/core_steps/loader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@
153153
RateLimiterBlockV1,
154154
)
155155
from inference.core.workflows.core_steps.formatters.csv.v1 import CSVFormatterBlockV1
156+
from inference.core.workflows.core_steps.formatters.current_time.v1 import (
157+
CurrentTimeBlockV1,
158+
)
156159
from inference.core.workflows.core_steps.formatters.expression.v1 import (
157160
ExpressionBlockV1,
158161
)
@@ -815,6 +818,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]:
815818
DimensionCollapseBlockV1,
816819
DetectionsListRollUpBlockV1,
817820
FirstNonEmptyOrDefaultBlockV1,
821+
CurrentTimeBlockV1,
818822
AnthropicClaudeBlockV1,
819823
AnthropicClaudeBlockV2,
820824
AnthropicClaudeBlockV3,

requirements/_requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ APScheduler>=3.10.1,<4.0.0
44
asyncua~=1.1.5
55
cachetools<6.0.0
66
cython~=3.0.0
7-
python-dotenv~=1.2.2
8-
fastapi>=0.133.0,<0.140 # 0.133+ dropped the starlette<1.0 cap; be careful with upper pin - fastapi might remove support for on_event
9-
starlette>=1.0.1 # CVE-2026-48710 (BadHost) — Host-header path-injection fix
7+
python-dotenv~=1.0.0
8+
tzdata>=2024.1
9+
fastapi>=0.100,<0.116 # be careful with upper pin - fastapi might remove support for on_event
1010
numpy>=2.0.0,<2.4.0
1111
opencv-python>=4.8.1.78,<=4.10.0.84
1212
opencv-contrib-python>=4.8.1.78,<=4.10.0.84 # Note: opencv-python considers this as a bad practice, but since our dependencies rely on both we pin both here
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import datetime
2+
3+
from inference.core.env import WORKFLOWS_MAX_CONCURRENT_STEPS
4+
from inference.core.managers.base import ModelManager
5+
from inference.core.workflows.core_steps.common.entities import StepExecutionMode
6+
from inference.core.workflows.execution_engine.core import ExecutionEngine
7+
8+
WORKFLOW_WITH_CURRENT_TIME = {
9+
"version": "1.0",
10+
"inputs": [
11+
{
12+
"type": "WorkflowParameter",
13+
"name": "timezone",
14+
"default_value": "UTC",
15+
},
16+
],
17+
"steps": [
18+
{
19+
"type": "roboflow_core/current_time@v1",
20+
"name": "now",
21+
"timezone": "$inputs.timezone",
22+
},
23+
],
24+
"outputs": [
25+
{"type": "JsonField", "name": "timestamp", "selector": "$steps.now.timestamp"},
26+
{"type": "JsonField", "name": "iso_string", "selector": "$steps.now.iso_string"},
27+
{"type": "JsonField", "name": "date", "selector": "$steps.now.date"},
28+
{"type": "JsonField", "name": "time", "selector": "$steps.now.time"},
29+
],
30+
}
31+
32+
33+
def test_current_time_workflow(model_manager: ModelManager) -> None:
34+
# given
35+
execution_engine = ExecutionEngine.init(
36+
workflow_definition=WORKFLOW_WITH_CURRENT_TIME,
37+
init_parameters={
38+
"workflows_core.model_manager": model_manager,
39+
"workflows_core.step_execution_mode": StepExecutionMode.LOCAL,
40+
},
41+
max_concurrent_steps=WORKFLOWS_MAX_CONCURRENT_STEPS,
42+
)
43+
44+
# when
45+
result = execution_engine.run(
46+
runtime_parameters={"timezone": "America/New_York"}
47+
)
48+
49+
# then
50+
assert len(result) == 1, "Single image/parameter batch expected"
51+
row = result[0]
52+
assert set(row.keys()) == {"timestamp", "iso_string", "date", "time"}
53+
assert isinstance(row["timestamp"], datetime.datetime)
54+
assert row["timestamp"].tzinfo is not None, "Timestamp must be timezone-aware"
55+
assert str(row["timestamp"].tzinfo) == "America/New_York"
56+
# derived strings agree with the datetime object
57+
assert row["iso_string"] == row["timestamp"].isoformat()
58+
assert row["iso_string"].startswith(row["date"])
59+
assert row["date"] == row["timestamp"].strftime("%Y-%m-%d")
60+
assert row["time"] == row["timestamp"].strftime("%H:%M:%S")

0 commit comments

Comments
 (0)