Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Enable ruff B008 (function-call-in-default-argument) and fix violations by shahar1 · Pull Request #66979 · apache/airflow · GitHub
Skip to content
Merged
18 changes: 10 additions & 8 deletions airflow-core/src/airflow/api_fastapi/common/parameters.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@

T = TypeVar("T")

_FALLBACK_PAGE_LIMIT: int = conf.getint("api", "fallback_page_limit")


class BaseParam(OrmClause[T], ABC):
"""Base class for path or query parameters with ORM transformation."""
Expand DownExpand Up@@ -106,7 +108,7 @@ def to_orm(self, select: Select) -> Select:
return select.limit(self.value)

@classmethod
def depends(cls, limit: NonNegativeInt = conf.getint("api", "fallback_page_limit")) -> LimitFilter:
def depends(cls, limit: NonNegativeInt = _FALLBACK_PAGE_LIMIT) -> LimitFilter:
return cls().set_value(min(limit, conf.getint("api", "maximum_page_limit")))


Expand DownExpand Up@@ -607,13 +609,13 @@ def dynamic_depends(self, default: str | Sequence[str] | None = None) -> Callabl
else:
default_list = list(default)

def inner(
order_by: list[str] = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
),
) -> SortParam:
_order_by_query = Query(
default=default_list,
description=f"Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. "
f"Supported attributes: `{', '.join(all_attrs) if all_attrs else self.get_primary_key_string()}`",
)

def inner(order_by: list[str] = _order_by_query) -> SortParam:
return self.set_value(order_by)

return inner
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,9 @@ def _use(mapping: dict):
@pytest.fixture
def as_user(override_deps):
@contextmanager
def _as(u=types.SimpleNamespace(id=1, username="tester")):
def _as(u=None):
if u is None:
u = types.SimpleNamespace(id=1, username="tester")
with override_deps({get_user_dep: lambda: u}):
yield u

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ def create_ray_cluster(
self,
project_id: str,
location: str,
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
python_version: str = "3.10",
ray_version: str = "2.33",
network: str | None = None,
Expand DownExpand Up@@ -115,7 +115,7 @@ def create_ray_cluster(
"""
aiplatform.init(project=project_id, location=location, credentials=self.get_credentials())
cluster_path = vertex_ray.create_ray_cluster(
head_node_type=head_node_type,
head_node_type=head_node_type or resources.Resources(),
python_version=python_version,
ray_version=ray_version,
network=network,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,7 +140,7 @@ def __init__(
self,
python_version: str,
ray_version: Literal["2.9.3", "2.33", "2.42"],
head_node_type: resources.Resources = resources.Resources(),
head_node_type: resources.Resources | None = None,
network: str | None = None,
service_account: str | None = None,
cluster_name: str | None = None,
Expand All@@ -155,7 +155,7 @@ def __init__(
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.head_node_type = head_node_type
self.head_node_type = head_node_type or resources.Resources()
self.python_version = python_version
self.ray_version = ray_version
self.network = network
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ def __init__(
replace=False,
gzip=False,
google_impersonation_chain: str | Sequence[str] | None = None,
deferrable=conf.getboolean("operators", "default_deferrable", fallback=False),
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
Comment thread
jscheffl marked this conversation as resolved.
poll_interval: int = 10,
return_gcs_uris: bool = False,
**kwargs,
Expand Down
Comment thread
jscheffl marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,6 +94,23 @@ def test_create_ray_cluster(self, mock_aiplatform_init, mock_create_ray_cluster)
labels=None,
)

@mock.patch(RAY_STRING.format("vertex_ray.create_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
def test_create_ray_cluster_default_head_node_type(
self, mock_aiplatform_init, mock_create_ray_cluster
) -> None:
self.hook.create_ray_cluster(
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
head_node_type=None,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
cluster_name=TEST_CLUSTER_NAME,
)
mock_aiplatform_init.assert_called_once()
call_kwargs = mock_create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)

@mock.patch(RAY_STRING.format("vertex_ray.delete_ray_cluster"))
@mock.patch(RAY_STRING.format("aiplatform.init"))
@mock.patch(RAY_STRING.format("PersistentResourceServiceClient.persistent_resource_path"))
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from unittest import mock

import pytest

pytest.importorskip("google.cloud.aiplatform.vertex_ray.util.resources")
from google.cloud.aiplatform.vertex_ray.util.resources import Resources

from airflow.providers.google.cloud.operators.vertex_ai.ray import CreateRayClusterOperator

TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_LOCATION = "us-central1"
TEST_PROJECT_ID = "test-project-id"
TEST_PYTHON_VERSION = "3.10"
TEST_RAY_VERSION = "2.33"
TEST_CLUSTER_NAME = "test-cluster-name"

VERTEX_AI_RAY_OP_PATH = "airflow.providers.google.cloud.operators.vertex_ai.ray.{}"


class TestCreateRayClusterOperator:
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_with_explicit_head_node_type(self, mock_hook_cls):
explicit_head = Resources()
op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
head_node_type=explicit_head,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert op.head_node_type is explicit_head

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_create_ray_cluster_default_head_node_type_is_fresh_resources(self, mock_hook_cls):
op1 = CreateRayClusterOperator(
task_id="test-task-1",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
op2 = CreateRayClusterOperator(
task_id="test-task-2",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)
assert isinstance(op1.head_node_type, Resources)
assert isinstance(op2.head_node_type, Resources)
assert op1.head_node_type is not op2.head_node_type

@mock.patch(VERTEX_AI_RAY_OP_PATH.format("VertexAIRayClusterLink"))
@mock.patch(VERTEX_AI_RAY_OP_PATH.format("RayHook"))
def test_execute_without_head_node_type_passes_default_resources(self, mock_hook_cls, mock_link):
mock_hook = mock_hook_cls.return_value
mock_hook.create_ray_cluster.return_value = (
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/persistentResources/{TEST_CLUSTER_NAME}"
)
mock_hook.extract_cluster_id.return_value = TEST_CLUSTER_NAME

op = CreateRayClusterOperator(
task_id="test-task",
project_id=TEST_PROJECT_ID,
location=TEST_LOCATION,
python_version=TEST_PYTHON_VERSION,
ray_version=TEST_RAY_VERSION,
gcp_conn_id=TEST_GCP_CONN_ID,
)

ti_mock = mock.MagicMock()
context = {"ti": ti_mock, "task": mock.MagicMock()}
op.execute(context=context)

call_kwargs = mock_hook.create_ray_cluster.call_args.kwargs
assert isinstance(call_kwargs["head_node_type"], Resources)
4 changes: 2 additions & 2 deletions providers/openlineage/tests/system/openlineage/operator.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -197,15 +197,15 @@ def __init__(
self,
event_templates: dict[str, dict] | None = None,
file_path: str | None = None,
env: Environment = setup_jinja(),
env: Environment | None = None,
allow_duplicate_events_regex: str | None = None,
clear_variables: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.event_templates = event_templates
self.file_path = file_path
self.env = env
self.env = env or setup_jinja()
self.allow_duplicate_events_regex = allow_duplicate_events_regex
self.clear_variables = clear_variables
if self.event_templates and self.file_path:
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -655,6 +655,7 @@ extend-select = [
"B004", # Checks for use of hasattr(x, "__call__") and replaces it with callable(x)
"B006", # Checks for uses of mutable objects as function argument defaults.
"B007", # Checks for unused variables in the loop
"B008", # Do not perform function call in argument defaults (use extend-immutable-calls for FastAPI DI)
Comment thread
shahar1 marked this conversation as resolved.
"B012", # Checks for `break`, `continue`, and `return` statements in `finally` blocks
"B017", # Checks for pytest.raises context managers that catch Exception or BaseException.
"B019", # Use of functools.lru_cache or functools.cache on methods can lead to memory leaks
Expand DownExpand Up@@ -703,6 +704,18 @@ unfixable = [
"PT022",
]

[tool.ruff.lint.flake8-bugbear]
Comment thread
jscheffl marked this conversation as resolved.
# FastAPI dependency injection uses function calls in argument defaults intentionally.
# SHA256 is a stateless algorithm descriptor (cryptography library).
extend-immutable-calls = [
"fastapi.Body",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Security",
"cryptography.hazmat.primitives.hashes.SHA256",
]

[tool.ruff.format]
docstring-code-format = true

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,16 +45,16 @@ class SafeDogStatsdLogger:
def __init__(
self,
dogstatsd_client: DogStatsd,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
metrics_tags: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.dogstatsd = dogstatsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.metrics_tags = metrics_tags
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,13 +175,13 @@ def __init__(
self,
otel_provider,
prefix: str = DEFAULT_METRIC_NAME_PREFIX,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
):
self.otel: Callable = otel_provider
self.prefix: str = prefix
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.meter = otel_provider.get_meter(__name__)
self.metrics_map = MetricsMap(self.meter)
self.stat_name_handler = stat_name_handler
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,16 +67,16 @@ class SafeStatsdLogger:
def __init__(
self,
statsd_client: StatsClient,
metrics_validator: ListValidator = PatternAllowListValidator(),
metrics_validator: ListValidator | None = None,
influxdb_tags_enabled: bool = False,
metric_tags_validator: ListValidator = PatternAllowListValidator(),
metric_tags_validator: ListValidator | None = None,
stat_name_handler: Callable[[str], str] | None = None,
statsd_influxdb_enabled: bool = False,
) -> None:
self.statsd = statsd_client
self.metrics_validator = metrics_validator
self.metrics_validator = metrics_validator or PatternAllowListValidator()
self.influxdb_tags_enabled = influxdb_tags_enabled
self.metric_tags_validator = metric_tags_validator
self.metric_tags_validator = metric_tags_validator or PatternAllowListValidator()
self.stat_name_handler = stat_name_handler
self.statsd_influxdb_enabled = statsd_influxdb_enabled

Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/bases/sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ def __init__(
self,
*,
poke_interval: timedelta | float = 60,
timeout: timedelta | float = conf.getfloat("sensors", "default_timeout"),
timeout: timedelta | float | None = None,
soft_fail: bool = False,
mode: str = "poke",
exponential_backoff: bool = False,
Expand All@@ -128,6 +128,8 @@ def __init__(
super().__init__(**kwargs)
self.poke_interval = self._coerce_poke_interval(poke_interval).total_seconds()
self.soft_fail = soft_fail
if timeout is None:
timeout = conf.getfloat("sensors", "default_timeout")
Comment thread
shahar1 marked this conversation as resolved.
self.timeout: int | float = self._coerce_timeout(timeout).total_seconds()
self.mode = mode
self.exponential_backoff = exponential_backoff
Expand Down
16 changes: 8 additions & 8 deletions task-sdk/src/airflow/sdk/definitions/operator_resources.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,15 +125,15 @@ class Resources:

def __init__(
self,
cpus=conf.getint("operators", "default_cpus"),
ram=conf.getint("operators", "default_ram"),
disk=conf.getint("operators", "default_disk"),
gpus=conf.getint("operators", "default_gpus"),
cpus=None,
ram=None,
disk=None,
gpus=None,
):
self.cpus = CpuResource(cpus)
self.ram = RamResource(ram)
self.disk = DiskResource(disk)
self.gpus = GpuResource(gpus)
self.cpus = CpuResource(cpus if cpus is not None else conf.getint("operators", "default_cpus"))
self.ram = RamResource(ram if ram is not None else conf.getint("operators", "default_ram"))
self.disk = DiskResource(disk if disk is not None else conf.getint("operators", "default_disk"))
self.gpus = GpuResource(gpus if gpus is not None else conf.getint("operators", "default_gpus"))
Comment thread
shahar1 marked this conversation as resolved.

def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
Expand Down
14 changes: 14 additions & 0 deletions task-sdk/tests/task_sdk/bases/test_sensor.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,8 @@
from airflow.sdk.execution_time.comms import RescheduleTask, TaskRescheduleStartDate
from airflow.sdk.timezone import datetime

from tests_common.test_utils.config import conf_vars

if TYPE_CHECKING:
from airflow.sdk.definitions.context import Context

Expand DownExpand Up@@ -358,6 +360,18 @@ def test_sensor_with_invalid_timeout(self):
task_id="test_sensor_task_3", return_value=None, poke_interval=10, timeout=positive_timeout
)

def test_sensor_timeout_default_read_from_conf_at_instantiation(self):
"""When ``timeout`` is not supplied, it should be read from ``sensors.default_timeout``
at instantiation time (not at module import time).
"""
with conf_vars({("sensors", "default_timeout"): "12345"}):
sensor = DummySensor(task_id="test_sensor_default_timeout", return_value=None, poke_interval=10)
assert sensor.timeout == 12345

with conf_vars({("sensors", "default_timeout"): "67"}):
sensor = DummySensor(task_id="test_sensor_default_timeout_2", return_value=None, poke_interval=10)
assert sensor.timeout == 67

def test_sensor_with_exponential_backoff_off(self):
sensor = DummySensor(
task_id=SENSOR_OP, return_value=None, poke_interval=5, timeout=60, exponential_backoff=False
Expand Down
Loading
Loading