Skip to content

Commit f0365da

Browse files
authored
feat: accept str|enum region/runtime, omit when unset for org default (#57)
* feat: accept str|enum region/runtime, omit when unset for org default Widen the region/runtime parameters on WherobotsRunOperator, WherobotsSqlOperator, WherobotsSqlHook, and WherobotsRestAPIHook.create_run to `Optional[Union[str, Region/Runtime]]`. Strings are passed to the API untouched (BYOC regions work without a release); enums are normalized to their value. When region/runtime are omitted they are dropped from the request (region from the /runs query string, runtime from the run payload, and both from the SQL connect() call) so the API applies the organization's configured default. `warn_for_default_region` no longer injects DEFAULT_REGION — it normalizes and returns None when unset. Adds tests for create_run (region omitted / string passthrough) and warn_for_default_region. Note: the SQL path's "omit -> org default" behavior also requires the wherobots-python-dbapi release that makes connect() region/runtime optional; bump the dependency constraint to that version at release time. The run-operator path depends only on the studio-backend API change. * fix: don't normalize region to str in warn_for_default_region; pin runtime in prod smoke tests CI integration tests (which hit prod with the released dbapi) failed: - test_sql crashed with "'str' object has no attribute 'value'": warn_for_default_region returned a normalized string, and the released enum-only connect() calls region.value on it. Normalization belongs at the request boundary (create_run / connect), so warn now returns the value unchanged (Optional[Union[str, Region]]); the enum keeps working with the current dbapi and the new dbapi normalizes internally. - test_run got a 422 ("runtime field required") because the operator now omits runtime by default and the (not-yet-deployed) prod API still requires it. The prod smoke tests are meant to verify a run submits successfully, not the org-default mechanism (covered by unit tests), so they now pin runtime=Runtime.TINY for deterministic, transition-robust behavior. * fix: handle TIMED_OUT run status from the API The prod API returns a TIMED_OUT run status that the provider's RunStatus enum didn't include, so polling a timed-out run raised a Pydantic ValidationError (surfaced by the timeout smoke test once it reaches the real run path). Add TIMED_OUT to RunStatus, treat it as a timeout in Run.is_timeout, and raise the "failed due to timeout" RuntimeError for it in WherobotsRunOperator.execute. Adds a TIMED_OUT case to the execute-state unit test.
1 parent 788d061 commit f0365da

12 files changed

Lines changed: 125 additions & 34 deletions

File tree

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@ operator = WherobotsRunOperator(
7676

7777
The arguments for the `WherobotsRunOperator` constructor:
7878

79-
* `region: Region`: The Wherobots region where runs are hosted.
80-
The values available can be found in `wherobots.db.region.Region`.
79+
* `region: str | Region`: The Wherobots region where runs are hosted. Accepts a
80+
`Region` enum value (see `wherobots.db.region.Region`) or a raw string (a BYOC
81+
region is passed through as-is). Optional — when omitted, your organization's
82+
configured default region is used. Only set it to override that default.
8183
* `name: str`: The name of the run. If not specified, a default name will be
8284
generated.
83-
* `runtime: Runtime`: The runtime dictates the size and amount of resources
84-
powering the run. The default value is `Runtime.TINY`; see available values
85-
[here](https://github.com/wherobots/wherobots-python-dbapi/blob/main/wherobots/db/runtime.py).
85+
* `runtime: str | Runtime`: The runtime dictates the size and amount of resources
86+
powering the run. Accepts a `Runtime` enum value (see available values
87+
[here](https://github.com/wherobots/wherobots-python-dbapi/blob/main/wherobots/db/runtime.py))
88+
or a raw string. Optional — when omitted, your organization's configured
89+
default runtime is used.
8690
* `version: str`: The WherobotsDB version to use. Defaults to `latest`.
8791
* `poll_logs: bool`: If `True`, the operator will poll and `Logger.info()` the run logs
8892
until the run finishes. If `False`, the operator will not poll the logs, only track

airflow_providers_wherobots/hooks/rest_api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import platform
66
from functools import cached_property
7-
from typing import Any, Optional, Dict
7+
from typing import Any, Optional, Dict, Union
88

99
import requests
1010
from importlib import metadata
@@ -112,12 +112,18 @@ def get_run(self, run_id: str) -> Run:
112112
resp_json = self._api_call("GET", f"/runs/{run_id}").json()
113113
return Run.model_validate(resp_json)
114114

115-
def create_run(self, payload: Dict[str, Any], region: Region) -> Run:
115+
def create_run(
116+
self, payload: Dict[str, Any], region: Optional[Union[str, Region]] = None
117+
) -> Run:
118+
# Normalize enum -> value, pass strings through, and omit region
119+
# entirely when unset so the API applies the org default.
120+
region_value = region.value if isinstance(region, Region) else region
121+
params = {"region": region_value} if region_value else {}
116122
resp_json = self._api_call(
117123
"POST",
118124
"/runs",
119125
payload=payload,
120-
params={"region": region.value},
126+
params=params,
121127
).json()
122128
return Run.model_validate(resp_json)
123129

airflow_providers_wherobots/hooks/sql.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
Hook for Wherobots' Spatial SQL API interface.
33
"""
44

5-
from typing import Optional
5+
from typing import Optional, Union
66

77
from airflow.providers.common.sql.hooks.sql import DbApiHook
88
from wherobots.db import Connection as WDBConnection, connect
99
from wherobots.db.constants import (
1010
DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS,
1111
DEFAULT_READ_TIMEOUT_SECONDS,
12-
DEFAULT_RUNTIME,
1312
DEFAULT_SESSION_TYPE,
1413
)
1514
from wherobots.db.region import Region
@@ -24,9 +23,9 @@ class WherobotsSqlHook(DbApiHook): # type: ignore[misc]
2423

2524
def __init__( # type: ignore[no-untyped-def]
2625
self,
27-
region: Optional[Region] = None,
26+
region: Optional[Union[str, Region]] = None,
2827
wherobots_conn_id: str = DEFAULT_CONN_ID,
29-
runtime: Runtime = DEFAULT_RUNTIME,
28+
runtime: Optional[Union[str, Runtime]] = None,
3029
version: Optional[str] = None,
3130
session_wait_timeout: int = DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS,
3231
read_timeout: int = DEFAULT_READ_TIMEOUT_SECONDS,
@@ -49,7 +48,7 @@ def __init__( # type: ignore[no-untyped-def]
4948

5049
def _create_or_get_sql_session(
5150
self,
52-
runtime: Runtime = DEFAULT_RUNTIME,
51+
runtime: Optional[Union[str, Runtime]] = None,
5352
) -> WDBConnection:
5453
return connect(
5554
host=self._conn.host,
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import logging
2-
from typing import Optional
2+
from typing import Optional, Union
33

44
from wherobots.db import Region
5-
from wherobots.db.constants import DEFAULT_REGION
65

76
logger = logging.getLogger(__name__)
87

98

10-
def warn_for_default_region(region: Optional[Region]) -> Region:
9+
def warn_for_default_region(
10+
region: Optional[Union[str, Region]],
11+
) -> Optional[Union[str, Region]]:
12+
"""Resolve the region argument for a Wherobots operator.
13+
14+
The value is returned unchanged — a ``Region`` enum or a raw string (e.g. a
15+
BYOC region). Normalization (enum -> value) happens at the request boundary
16+
(``WherobotsRestAPIHook.create_run`` / ``wherobots.db.connect``), so the enum
17+
keeps working with older ``wherobots-python-dbapi`` releases. When no region
18+
is provided, returns ``None`` so the API applies the organization's
19+
configured default region — only set ``region`` to override that default.
20+
"""
1121
if not region:
12-
logger.warning(""""Parameter region was not specified, it will be required by Wherobots API in the near future, please specify it in the operator.
13-
If you don't know your Wherobots Compute Region, please contact Wherobots support.""")
14-
logger.warning(f"Using default region: {DEFAULT_REGION.value}")
15-
region = DEFAULT_REGION
22+
logger.info(
23+
"No region specified; the Wherobots API will use your organization's "
24+
"configured default region. Pass `region` to override it."
25+
)
26+
return None
1627
return region

airflow_providers_wherobots/operators/run.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66
from enum import auto
77
from time import sleep
8-
from typing import Optional, Sequence, Any, Dict
8+
from typing import Optional, Sequence, Any, Dict, Union
99

1010
from airflow.models import BaseOperator
1111
from strenum import StrEnum
@@ -20,7 +20,6 @@
2020
)
2121

2222
from wherobots.db import Runtime, Region
23-
from wherobots.db.constants import DEFAULT_RUNTIME
2423

2524

2625
class XComKey(StrEnum):
@@ -39,9 +38,9 @@ class WherobotsRunOperator(BaseOperator):
3938

4039
def __init__(
4140
self,
42-
region: Optional[Region] = None,
41+
region: Optional[Union[str, Region]] = None,
4342
name: Optional[str] = None,
44-
runtime: Runtime = DEFAULT_RUNTIME,
43+
runtime: Optional[Union[str, Runtime]] = None,
4544
version: Optional[str] = None,
4645
run_python: Optional[Dict[str, Any]] = None,
4746
run_jar: Optional[Dict[str, Any]] = None,
@@ -56,10 +55,14 @@ def __init__(
5655
super().__init__(**kwargs)
5756
# If the user specifies the name, we will use it and rely on the server to validate the name
5857
self.run_payload: Dict[str, Any] = {
59-
"runtime": runtime.value,
6058
"name": name or self.default_run_name,
6159
"timeoutSeconds": timeout_seconds,
6260
}
61+
# Only include runtime when set; otherwise the API uses the org default.
62+
if runtime is not None:
63+
self.run_payload["runtime"] = (
64+
runtime.value if isinstance(runtime, Runtime) else runtime
65+
)
6366
self.region = region
6467
if version is not None:
6568
self.run_payload["version"] = version
@@ -189,10 +192,13 @@ def execute(self, context) -> Any:
189192
run = self._wait_run_simple(rest_api_hook, run)
190193
# loop end, means run is in terminal state
191194
self._log_run_status(run)
195+
# The API may report a timeout either as an explicit TIMED_OUT
196+
# status or as FAILED with a timeout event.
197+
if run.status == RunStatus.TIMED_OUT or (
198+
run.status == RunStatus.FAILED and run.is_timeout
199+
):
200+
raise RuntimeError(f"Run {run.ext_id} failed due to timeout")
192201
if run.status == RunStatus.FAILED:
193-
# check events, see if the run is timeout
194-
if run.is_timeout:
195-
raise RuntimeError(f"Run {run.ext_id} failed due to timeout")
196202
raise RuntimeError(f"Run {run.ext_id} failed, please check the logs")
197203
if run.status == RunStatus.CANCELLED:
198204
raise RuntimeError(f"Run {run.ext_id} was cancelled by user")

airflow_providers_wherobots/operators/sql.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44

55
from __future__ import annotations
66

7-
from typing import Sequence, Optional
7+
from typing import Sequence, Optional, Union
88

99
from airflow.providers.common.sql.hooks.sql import DbApiHook
1010
from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator
1111
from wherobots.db.constants import (
1212
DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS,
1313
DEFAULT_READ_TIMEOUT_SECONDS,
14-
DEFAULT_RUNTIME,
1514
DEFAULT_SESSION_TYPE,
1615
)
1716
from wherobots.db import Cursor as WDbCursor
@@ -43,9 +42,9 @@ class WherobotsSqlOperator(SQLExecuteQueryOperator): # type: ignore[misc]
4342
def __init__( # type: ignore[no-untyped-def]
4443
self,
4544
*,
46-
region: Optional[Region] = None,
45+
region: Optional[Union[str, Region]] = None,
4746
wherobots_conn_id: str = DEFAULT_CONN_ID,
48-
runtime: Runtime = DEFAULT_RUNTIME,
47+
runtime: Optional[Union[str, Runtime]] = None,
4948
version: Optional[str] = None,
5049
session_wait_timeout: int = DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS,
5150
read_timeout: int = DEFAULT_READ_TIMEOUT_SECONDS,

airflow_providers_wherobots/wherobots/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class RunStatus(StrEnum):
1919
FAILED = auto()
2020
COMPLETED = auto()
2121
CANCELLED = auto()
22+
TIMED_OUT = auto()
2223

2324
def is_active(self) -> bool:
2425
return self in [self.PENDING, self.RUNNING]
@@ -49,6 +50,8 @@ class Run(WherobotsModel):
4950

5051
@property
5152
def is_timeout(self) -> bool:
53+
if self.status == RunStatus.TIMED_OUT:
54+
return True
5255
if not self.kube_app or not self.kube_app.events:
5356
return False
5457
return any(

tests/integration_tests/operators/test_run.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from airflow import DAG
1010
from airflow.models import Connection
1111
from airflow.utils.state import TaskInstanceState
12-
from wherobots.db import Region
12+
from wherobots.db import Region, Runtime
1313

1414
from airflow_providers_wherobots.operators.run import WherobotsRunOperator
1515
from tests.unit_tests.operators.test_run import build_ti
@@ -25,6 +25,9 @@
2525
def test_prod_run_success(prod_conn: Connection, dag: DAG) -> None:
2626
operator = WherobotsRunOperator(
2727
region=Region.AWS_US_WEST_2,
28+
# Pin runtime so this prod smoke test is deterministic; the
29+
# omit-runtime -> org-default behavior is covered by unit tests.
30+
runtime=Runtime.TINY,
2831
wherobots_conn_id=prod_conn.conn_id,
2932
task_id="test_run_smoke",
3033
name="airflow_operator_test_run_{{ ts_nodash }}",
@@ -44,6 +47,7 @@ def test_prod_run_success(prod_conn: Connection, dag: DAG) -> None:
4447
def test_prod_run_timeout(prod_conn: Connection, dag: DAG) -> None:
4548
operator = WherobotsRunOperator(
4649
region=Region.AWS_US_WEST_2,
50+
runtime=Runtime.TINY,
4751
wherobots_conn_id=prod_conn.conn_id,
4852
task_id="test_run_smoke",
4953
name="airflow_operator_test_run_{{ ts_nodash }}",

tests/integration_tests/operators/test_sql.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from airflow import DAG
1010
from airflow.models import Connection
11-
from wherobots.db import Region
11+
from wherobots.db import Region, Runtime
1212

1313
from airflow_providers_wherobots.operators.sql import WherobotsSqlOperator
1414

@@ -23,6 +23,9 @@
2323
def test_prod_run_success(prod_conn: Connection, dag: DAG) -> None:
2424
operator = WherobotsSqlOperator(
2525
region=Region.AWS_US_WEST_2,
26+
# Pin runtime so this prod smoke test is deterministic; the
27+
# omit-runtime -> org-default behavior is covered by unit tests.
28+
runtime=Runtime.TINY,
2629
task_id=TEST_TASK_ID,
2730
sql="SELECT pickup_datetime FROM wherobots_pro_data.nyc_taxi.yellow_2009_2010 LIMIT 10",
2831
wherobots_conn_id=prod_conn.conn_id,

tests/unit_tests/hooks/test_rest_api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@ def test_create_run(self, test_default_conn) -> None:
129129
with WherobotsRestAPIHook() as hook:
130130
hook.create_run(payload=create_payload, region=Region.AWS_US_WEST_2)
131131

132+
@responses.activate
133+
def test_create_run_omits_region_when_none(self, test_default_conn) -> None:
134+
"""When region is None, no region query param is sent (API uses org default)."""
135+
test_run: Run = helpers.run_factory.build()
136+
url = f"https://{test_default_conn.host}/runs"
137+
create_payload = {"name": test_run.name, "timeoutSeconds": 5000}
138+
responses.add(
139+
responses.POST,
140+
url,
141+
json=test_run.model_dump(mode="json"),
142+
match=[matchers.query_string_matcher("")],
143+
status=HTTPStatus.OK,
144+
)
145+
with WherobotsRestAPIHook() as hook:
146+
hook.create_run(payload=create_payload, region=None)
147+
148+
@responses.activate
149+
def test_create_run_passes_string_region(self, test_default_conn) -> None:
150+
"""A raw region string (e.g. a BYOC region) is passed through as-is."""
151+
test_run: Run = helpers.run_factory.build()
152+
url = f"https://{test_default_conn.host}/runs"
153+
create_payload = {"name": test_run.name, "timeoutSeconds": 5000}
154+
responses.add(
155+
responses.POST,
156+
url,
157+
json=test_run.model_dump(mode="json"),
158+
match=[matchers.query_param_matcher({"region": "byoc-acme-us-east-1"})],
159+
status=HTTPStatus.OK,
160+
)
161+
with WherobotsRestAPIHook() as hook:
162+
hook.create_run(payload=create_payload, region="byoc-acme-us-east-1")
163+
132164
@responses.activate
133165
def test_get_run_logs(self, test_default_conn) -> None:
134166
"""

0 commit comments

Comments
 (0)