diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py index 0e56684081a73..077ddbe550552 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/pools.py @@ -20,7 +20,7 @@ from collections.abc import Callable, Iterable from typing import Annotated -from pydantic import BeforeValidator, Field +from pydantic import BeforeValidator, Field, PositiveInt from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel @@ -38,7 +38,7 @@ class BasePool(BaseModel): """Base serializer for Pool.""" pool: str = Field(serialization_alias="name") - slots: int + slots: PositiveInt description: str | None = Field(default=None) include_deferred: bool @@ -66,7 +66,7 @@ class PoolPatchBody(StrictBaseModel): """Pool serializer for patch bodies.""" name: str | None = Field(default=None, alias="pool") - slots: int | None = None + slots: PositiveInt | None = None description: str | None = None include_deferred: bool | None = None team_name: str | None = Field(max_length=50, default=None) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 9993e05efc688..03a62cd64b286 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -12041,6 +12041,7 @@ components: title: Name slots: type: integer + exclusiveMinimum: 0.0 title: Slots description: anyOf: @@ -12090,6 +12091,7 @@ components: slots: anyOf: - type: integer + exclusiveMinimum: 0.0 - type: 'null' title: Slots description: @@ -12119,6 +12121,7 @@ components: title: Name slots: type: integer + exclusiveMinimum: 0.0 title: Slots description: anyOf: diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 660742644e619..54465d540f09e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -4597,6 +4597,7 @@ export const $PoolBody = { }, slots: { type: 'integer', + exclusiveMinimum: 0, title: 'Slots' }, description: { @@ -4671,7 +4672,8 @@ export const $PoolPatchBody = { slots: { anyOf: [ { - type: 'integer' + type: 'integer', + exclusiveMinimum: 0 }, { type: 'null' @@ -4728,6 +4730,7 @@ export const $PoolResponse = { }, slots: { type: 'integer', + exclusiveMinimum: 0, title: 'Slots' }, description: { diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py index 6d8d924ae2b78..bb34660f0e02f 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py @@ -285,6 +285,24 @@ class TestPatchPool(TestPoolsEndpoint): ], }, ), + # Negative slot number + ( + POOL1_NAME, + {}, + {"slots": -10}, + 422, + { + "detail": [ + { + "ctx": {"gt": 0}, + "input": -10, + "loc": ["body", "slots"], + "msg": "Input should be greater than 0", + "type": "greater_than", + }, + ], + }, + ), # Partial body on default_pool ( Pool.DEFAULT_POOL_NAME, @@ -364,7 +382,8 @@ def test_should_respond_200( if response.status_code == 422: for error in body["detail"]: # pydantic version can vary in tests (lower constraints), we do not assert the url. - del error["url"] + if "url" in error: + del error["url"] assert body == expected_response if response.status_code == 200: diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index ed5c0e4784383..4a653867a6c91 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -633,12 +633,16 @@ class PoolBody(BaseModel): extra="forbid", ) name: Annotated[str, Field(max_length=256, title="Name")] - slots: Annotated[int, Field(title="Slots")] + slots: Annotated[int, Field(gt=0, title="Slots")] description: Annotated[str | None, Field(title="Description")] = None include_deferred: Annotated[bool | None, Field(title="Include Deferred")] = False team_name: Annotated[TeamName | None, Field(title="Team Name")] = None +class Slots(RootModel[int]): + root: Annotated[int, Field(gt=0, title="Slots")] + + class PoolPatchBody(BaseModel): """ Pool serializer for patch bodies. @@ -648,7 +652,7 @@ class PoolPatchBody(BaseModel): extra="forbid", ) pool: Annotated[str | None, Field(title="Pool")] = None - slots: Annotated[int | None, Field(title="Slots")] = None + slots: Annotated[Slots | None, Field(title="Slots")] = None description: Annotated[str | None, Field(title="Description")] = None include_deferred: Annotated[bool | None, Field(title="Include Deferred")] = None team_name: Annotated[TeamName | None, Field(title="Team Name")] = None @@ -660,7 +664,7 @@ class PoolResponse(BaseModel): """ name: Annotated[str, Field(title="Name")] - slots: Annotated[int, Field(title="Slots")] + slots: Annotated[int, Field(gt=0, title="Slots")] description: Annotated[str | None, Field(title="Description")] = None include_deferred: Annotated[bool, Field(title="Include Deferred")] occupied_slots: Annotated[int, Field(title="Occupied Slots")]