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 34cf4cb982550..cde5746b42beb 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 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 @@ -65,7 +65,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 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 45f38707a6eea..38db3d22848de 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 @@ -11433,6 +11433,7 @@ components: title: Name slots: type: integer + exclusiveMinimum: 0.0 title: Slots description: anyOf: @@ -11476,6 +11477,7 @@ components: slots: anyOf: - type: integer + exclusiveMinimum: 0.0 - type: 'null' title: Slots description: @@ -11499,6 +11501,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 ab7aa4942f781..7366d970a680a 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 @@ -4248,6 +4248,7 @@ export const $PoolBody = { }, slots: { type: 'integer', + exclusiveMinimum: 0, title: 'Slots' }, description: { @@ -4310,7 +4311,8 @@ export const $PoolPatchBody = { slots: { anyOf: [ { - type: 'integer' + type: 'integer', + exclusiveMinimum: 0 }, { type: 'null' @@ -4355,6 +4357,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 a444af70f7028..2abd7e23835dc 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 @@ -259,6 +259,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, @@ -335,7 +353,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 f546515584aeb..65364faa318bb 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -614,11 +614,15 @@ 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 +class Slots(RootModel[int]): + root: Annotated[int, Field(gt=0, title="Slots")] + + class PoolPatchBody(BaseModel): """ Pool serializer for patch bodies. @@ -628,7 +632,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 @@ -639,7 +643,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")]