Skip to content

Commit 25e173a

Browse files
authored
AIP-81 Implement POST/Insert Multiple Connections in REST API (FastAPI) (#44531)
* Move validate key to pydantic model, leave unique check to database session for post endpoints, include bulk connection insert endpoint * Fix test naming
1 parent 1ee37a0 commit 25e173a

File tree

9 files changed

+405
-23
lines changed

9 files changed

+405
-23
lines changed

airflow/api_fastapi/core_api/datamodels/connections.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class ConnectionTestResponse(BaseModel):
7979
class ConnectionBody(BaseModel):
8080
"""Connection Serializer for requests body."""
8181

82-
connection_id: str = Field(serialization_alias="conn_id")
82+
connection_id: str = Field(serialization_alias="conn_id", max_length=200, pattern=r"^[\w.-]+$")
8383
conn_type: str
8484
description: str | None = Field(default=None)
8585
host: str | None = Field(default=None)
@@ -88,3 +88,9 @@ class ConnectionBody(BaseModel):
8888
port: int | None = Field(default=None)
8989
password: str | None = Field(default=None)
9090
extra: str | None = Field(default=None)
91+
92+
93+
class ConnectionBulkBody(BaseModel):
94+
"""Connections Serializer for requests body."""
95+
96+
connections: list[ConnectionBody]

airflow/api_fastapi/core_api/openapi/v1-generated.yaml

+58
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,50 @@ paths:
13991399
application/json:
14001400
schema:
14011401
$ref: '#/components/schemas/HTTPValidationError'
1402+
/public/connections/bulk:
1403+
post:
1404+
tags:
1405+
- Connection
1406+
summary: Post Connections
1407+
description: Create connection entry.
1408+
operationId: post_connections
1409+
requestBody:
1410+
content:
1411+
application/json:
1412+
schema:
1413+
$ref: '#/components/schemas/ConnectionBulkBody'
1414+
required: true
1415+
responses:
1416+
'201':
1417+
description: Successful Response
1418+
content:
1419+
application/json:
1420+
schema:
1421+
$ref: '#/components/schemas/ConnectionCollectionResponse'
1422+
'401':
1423+
description: Unauthorized
1424+
content:
1425+
application/json:
1426+
schema:
1427+
$ref: '#/components/schemas/HTTPExceptionResponse'
1428+
'403':
1429+
description: Forbidden
1430+
content:
1431+
application/json:
1432+
schema:
1433+
$ref: '#/components/schemas/HTTPExceptionResponse'
1434+
'409':
1435+
description: Conflict
1436+
content:
1437+
application/json:
1438+
schema:
1439+
$ref: '#/components/schemas/HTTPExceptionResponse'
1440+
'422':
1441+
description: Validation Error
1442+
content:
1443+
application/json:
1444+
schema:
1445+
$ref: '#/components/schemas/HTTPValidationError'
14021446
/public/connections/test:
14031447
post:
14041448
tags:
@@ -6207,6 +6251,8 @@ components:
62076251
properties:
62086252
connection_id:
62096253
type: string
6254+
maxLength: 200
6255+
pattern: ^[\w.-]+$
62106256
title: Connection Id
62116257
conn_type:
62126258
type: string
@@ -6252,6 +6298,18 @@ components:
62526298
- conn_type
62536299
title: ConnectionBody
62546300
description: Connection Serializer for requests body.
6301+
ConnectionBulkBody:
6302+
properties:
6303+
connections:
6304+
items:
6305+
$ref: '#/components/schemas/ConnectionBody'
6306+
type: array
6307+
title: Connections
6308+
type: object
6309+
required:
6310+
- connections
6311+
title: ConnectionBulkBody
6312+
description: Connections Serializer for requests body.
62556313
ConnectionCollectionResponse:
62566314
properties:
62576315
connections:

airflow/api_fastapi/core_api/routes/public/connections.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from __future__ import annotations
1818

1919
import os
20-
from typing import Annotated
20+
from typing import Annotated, cast
2121

2222
from fastapi import Depends, HTTPException, Query, status
2323
from sqlalchemy import select
@@ -27,6 +27,7 @@
2727
from airflow.api_fastapi.common.router import AirflowRouter
2828
from airflow.api_fastapi.core_api.datamodels.connections import (
2929
ConnectionBody,
30+
ConnectionBulkBody,
3031
ConnectionCollectionResponse,
3132
ConnectionResponse,
3233
ConnectionTestResponse,
@@ -35,7 +36,6 @@
3536
from airflow.configuration import conf
3637
from airflow.models import Connection
3738
from airflow.secrets.environment_variables import CONN_ENV_PREFIX
38-
from airflow.utils import helpers
3939
from airflow.utils.strings import get_random_string
4040

4141
connections_router = AirflowRouter(tags=["Connection"], prefix="/connections")
@@ -126,24 +126,29 @@ def post_connection(
126126
session: SessionDep,
127127
) -> ConnectionResponse:
128128
"""Create connection entry."""
129-
try:
130-
helpers.validate_key(post_body.connection_id, max_length=200)
131-
except Exception as e:
132-
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"{e}")
133-
134-
connection = session.scalar(select(Connection).filter_by(conn_id=post_body.connection_id))
135-
if connection is not None:
136-
raise HTTPException(
137-
status.HTTP_409_CONFLICT,
138-
f"Connection with connection_id: `{post_body.connection_id}` already exists",
139-
)
140-
141129
connection = Connection(**post_body.model_dump(by_alias=True))
142130
session.add(connection)
143-
144131
return connection
145132

146133

134+
@connections_router.post(
135+
"/bulk",
136+
status_code=status.HTTP_201_CREATED,
137+
responses=create_openapi_http_exception_doc([status.HTTP_409_CONFLICT]),
138+
)
139+
def post_connections(
140+
post_body: ConnectionBulkBody,
141+
session: SessionDep,
142+
) -> ConnectionCollectionResponse:
143+
"""Create connection entry."""
144+
connections = [Connection(**body.model_dump(by_alias=True)) for body in post_body.connections]
145+
session.add_all(connections)
146+
return ConnectionCollectionResponse(
147+
connections=cast(list[ConnectionResponse], connections),
148+
total_entries=len(connections),
149+
)
150+
151+
147152
@connections_router.patch(
148153
"/{connection_id}",
149154
responses=create_openapi_http_exception_doc(

airflow/ui/openapi-gen/queries/common.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,9 @@ export type BackfillServiceCreateBackfillMutationResult = Awaited<
15941594
export type ConnectionServicePostConnectionMutationResult = Awaited<
15951595
ReturnType<typeof ConnectionService.postConnection>
15961596
>;
1597+
export type ConnectionServicePostConnectionsMutationResult = Awaited<
1598+
ReturnType<typeof ConnectionService.postConnections>
1599+
>;
15971600
export type ConnectionServiceTestConnectionMutationResult = Awaited<
15981601
ReturnType<typeof ConnectionService.testConnection>
15991602
>;

airflow/ui/openapi-gen/queries/queries.ts

+40
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
BackfillPostBody,
3939
ClearTaskInstancesBody,
4040
ConnectionBody,
41+
ConnectionBulkBody,
4142
CreateAssetEventsBody,
4243
DAGPatchBody,
4344
DAGRunClearBody,
@@ -2684,6 +2685,45 @@ export const useConnectionServicePostConnection = <
26842685
}) as unknown as Promise<TData>,
26852686
...options,
26862687
});
2688+
/**
2689+
* Post Connections
2690+
* Create connection entry.
2691+
* @param data The data for the request.
2692+
* @param data.requestBody
2693+
* @returns ConnectionCollectionResponse Successful Response
2694+
* @throws ApiError
2695+
*/
2696+
export const useConnectionServicePostConnections = <
2697+
TData = Common.ConnectionServicePostConnectionsMutationResult,
2698+
TError = unknown,
2699+
TContext = unknown,
2700+
>(
2701+
options?: Omit<
2702+
UseMutationOptions<
2703+
TData,
2704+
TError,
2705+
{
2706+
requestBody: ConnectionBulkBody;
2707+
},
2708+
TContext
2709+
>,
2710+
"mutationFn"
2711+
>,
2712+
) =>
2713+
useMutation<
2714+
TData,
2715+
TError,
2716+
{
2717+
requestBody: ConnectionBulkBody;
2718+
},
2719+
TContext
2720+
>({
2721+
mutationFn: ({ requestBody }) =>
2722+
ConnectionService.postConnections({
2723+
requestBody,
2724+
}) as unknown as Promise<TData>,
2725+
...options,
2726+
});
26872727
/**
26882728
* Test Connection
26892729
* Test an API connection.

airflow/ui/openapi-gen/requests/schemas.gen.ts

+18
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ export const $ConnectionBody = {
750750
properties: {
751751
connection_id: {
752752
type: "string",
753+
maxLength: 200,
754+
pattern: "^[\\w.-]+$",
753755
title: "Connection Id",
754756
},
755757
conn_type: {
@@ -840,6 +842,22 @@ export const $ConnectionBody = {
840842
description: "Connection Serializer for requests body.",
841843
} as const;
842844

845+
export const $ConnectionBulkBody = {
846+
properties: {
847+
connections: {
848+
items: {
849+
$ref: "#/components/schemas/ConnectionBody",
850+
},
851+
type: "array",
852+
title: "Connections",
853+
},
854+
},
855+
type: "object",
856+
required: ["connections"],
857+
title: "ConnectionBulkBody",
858+
description: "Connections Serializer for requests body.",
859+
} as const;
860+
843861
export const $ConnectionCollectionResponse = {
844862
properties: {
845863
connections: {

airflow/ui/openapi-gen/requests/services.gen.ts

+27
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import type {
5858
GetConnectionsResponse,
5959
PostConnectionData,
6060
PostConnectionResponse,
61+
PostConnectionsData,
62+
PostConnectionsResponse,
6163
TestConnectionData,
6264
TestConnectionResponse,
6365
GetDagRunData,
@@ -997,6 +999,31 @@ export class ConnectionService {
997999
});
9981000
}
9991001

1002+
/**
1003+
* Post Connections
1004+
* Create connection entry.
1005+
* @param data The data for the request.
1006+
* @param data.requestBody
1007+
* @returns ConnectionCollectionResponse Successful Response
1008+
* @throws ApiError
1009+
*/
1010+
public static postConnections(
1011+
data: PostConnectionsData,
1012+
): CancelablePromise<PostConnectionsResponse> {
1013+
return __request(OpenAPI, {
1014+
method: "POST",
1015+
url: "/public/connections/bulk",
1016+
body: data.requestBody,
1017+
mediaType: "application/json",
1018+
errors: {
1019+
401: "Unauthorized",
1020+
403: "Forbidden",
1021+
409: "Conflict",
1022+
422: "Validation Error",
1023+
},
1024+
});
1025+
}
1026+
10001027
/**
10011028
* Test Connection
10021029
* Test an API connection.

airflow/ui/openapi-gen/requests/types.gen.ts

+40
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ export type ConnectionBody = {
214214
extra?: string | null;
215215
};
216216

217+
/**
218+
* Connections Serializer for requests body.
219+
*/
220+
export type ConnectionBulkBody = {
221+
connections: Array<ConnectionBody>;
222+
};
223+
217224
/**
218225
* Connection Collection serializer for responses.
219226
*/
@@ -1499,6 +1506,12 @@ export type PostConnectionData = {
14991506

15001507
export type PostConnectionResponse = ConnectionResponse;
15011508

1509+
export type PostConnectionsData = {
1510+
requestBody: ConnectionBulkBody;
1511+
};
1512+
1513+
export type PostConnectionsResponse = ConnectionCollectionResponse;
1514+
15021515
export type TestConnectionData = {
15031516
requestBody: ConnectionBody;
15041517
};
@@ -2766,6 +2779,33 @@ export type $OpenApiTs = {
27662779
};
27672780
};
27682781
};
2782+
"/public/connections/bulk": {
2783+
post: {
2784+
req: PostConnectionsData;
2785+
res: {
2786+
/**
2787+
* Successful Response
2788+
*/
2789+
201: ConnectionCollectionResponse;
2790+
/**
2791+
* Unauthorized
2792+
*/
2793+
401: HTTPExceptionResponse;
2794+
/**
2795+
* Forbidden
2796+
*/
2797+
403: HTTPExceptionResponse;
2798+
/**
2799+
* Conflict
2800+
*/
2801+
409: HTTPExceptionResponse;
2802+
/**
2803+
* Validation Error
2804+
*/
2805+
422: HTTPValidationError;
2806+
};
2807+
};
2808+
};
27692809
"/public/connections/test": {
27702810
post: {
27712811
req: TestConnectionData;

0 commit comments

Comments
 (0)