prefect-client 3.2.1__py3-none-any.whl → 3.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/__init__.py +15 -8
- prefect/_build_info.py +5 -0
- prefect/_internal/schemas/bases.py +4 -7
- prefect/_internal/schemas/validators.py +5 -6
- prefect/_result_records.py +6 -1
- prefect/client/orchestration/__init__.py +18 -6
- prefect/client/schemas/schedules.py +2 -2
- prefect/concurrency/asyncio.py +4 -3
- prefect/concurrency/sync.py +3 -3
- prefect/concurrency/v1/asyncio.py +3 -3
- prefect/concurrency/v1/sync.py +3 -3
- prefect/deployments/flow_runs.py +2 -2
- prefect/docker/docker_image.py +2 -3
- prefect/engine.py +1 -1
- prefect/events/clients.py +4 -3
- prefect/events/related.py +3 -5
- prefect/flows.py +11 -5
- prefect/locking/filesystem.py +8 -8
- prefect/logging/handlers.py +7 -11
- prefect/main.py +0 -2
- prefect/runtime/flow_run.py +10 -17
- prefect/server/api/__init__.py +34 -0
- prefect/server/api/admin.py +85 -0
- prefect/server/api/artifacts.py +224 -0
- prefect/server/api/automations.py +239 -0
- prefect/server/api/block_capabilities.py +25 -0
- prefect/server/api/block_documents.py +164 -0
- prefect/server/api/block_schemas.py +153 -0
- prefect/server/api/block_types.py +211 -0
- prefect/server/api/clients.py +246 -0
- prefect/server/api/collections.py +75 -0
- prefect/server/api/concurrency_limits.py +286 -0
- prefect/server/api/concurrency_limits_v2.py +269 -0
- prefect/server/api/csrf_token.py +38 -0
- prefect/server/api/dependencies.py +196 -0
- prefect/server/api/deployments.py +941 -0
- prefect/server/api/events.py +300 -0
- prefect/server/api/flow_run_notification_policies.py +120 -0
- prefect/server/api/flow_run_states.py +52 -0
- prefect/server/api/flow_runs.py +867 -0
- prefect/server/api/flows.py +210 -0
- prefect/server/api/logs.py +43 -0
- prefect/server/api/middleware.py +73 -0
- prefect/server/api/root.py +35 -0
- prefect/server/api/run_history.py +170 -0
- prefect/server/api/saved_searches.py +99 -0
- prefect/server/api/server.py +891 -0
- prefect/server/api/task_run_states.py +52 -0
- prefect/server/api/task_runs.py +342 -0
- prefect/server/api/task_workers.py +31 -0
- prefect/server/api/templates.py +35 -0
- prefect/server/api/ui/__init__.py +3 -0
- prefect/server/api/ui/flow_runs.py +128 -0
- prefect/server/api/ui/flows.py +173 -0
- prefect/server/api/ui/schemas.py +63 -0
- prefect/server/api/ui/task_runs.py +175 -0
- prefect/server/api/validation.py +382 -0
- prefect/server/api/variables.py +181 -0
- prefect/server/api/work_queues.py +230 -0
- prefect/server/api/workers.py +656 -0
- prefect/settings/sources.py +18 -5
- prefect/states.py +3 -3
- prefect/task_engine.py +3 -3
- prefect/types/_datetime.py +82 -3
- prefect/utilities/dockerutils.py +2 -2
- prefect/workers/base.py +5 -5
- {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/METADATA +10 -15
- {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/RECORD +70 -32
- {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/WHEEL +1 -2
- prefect/_version.py +0 -21
- prefect_client-3.2.1.dist-info/top_level.txt +0 -1
- {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
2
|
+
from uuid import UUID
|
3
|
+
|
4
|
+
from fastapi import Body, Depends, HTTPException, Path, status
|
5
|
+
|
6
|
+
import prefect.server.models as models
|
7
|
+
import prefect.server.schemas as schemas
|
8
|
+
from prefect.server.api.dependencies import LimitBody
|
9
|
+
from prefect.server.database import PrefectDBInterface, provide_database_interface
|
10
|
+
from prefect.server.schemas import actions
|
11
|
+
from prefect.server.utilities.schemas import PrefectBaseModel
|
12
|
+
from prefect.server.utilities.server import PrefectRouter
|
13
|
+
|
14
|
+
router: PrefectRouter = PrefectRouter(
|
15
|
+
prefix="/v2/concurrency_limits", tags=["Concurrency Limits V2"]
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
@router.post("/", status_code=status.HTTP_201_CREATED)
|
20
|
+
async def create_concurrency_limit_v2(
|
21
|
+
concurrency_limit: actions.ConcurrencyLimitV2Create,
|
22
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
23
|
+
) -> schemas.core.ConcurrencyLimitV2:
|
24
|
+
async with db.session_context(begin_transaction=True) as session:
|
25
|
+
model = await models.concurrency_limits_v2.create_concurrency_limit(
|
26
|
+
session=session, concurrency_limit=concurrency_limit
|
27
|
+
)
|
28
|
+
|
29
|
+
return schemas.core.ConcurrencyLimitV2.model_validate(model)
|
30
|
+
|
31
|
+
|
32
|
+
@router.get("/{id_or_name}")
|
33
|
+
async def read_concurrency_limit_v2(
|
34
|
+
id_or_name: Union[UUID, str] = Path(
|
35
|
+
..., description="The ID or name of the concurrency limit", alias="id_or_name"
|
36
|
+
),
|
37
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
38
|
+
) -> schemas.responses.GlobalConcurrencyLimitResponse:
|
39
|
+
if isinstance(id_or_name, str): # TODO: this seems like it shouldn't be necessary
|
40
|
+
try:
|
41
|
+
id_or_name = UUID(id_or_name)
|
42
|
+
except ValueError:
|
43
|
+
pass
|
44
|
+
async with db.session_context() as session:
|
45
|
+
if isinstance(id_or_name, UUID):
|
46
|
+
model = await models.concurrency_limits_v2.read_concurrency_limit(
|
47
|
+
session, concurrency_limit_id=id_or_name
|
48
|
+
)
|
49
|
+
else:
|
50
|
+
model = await models.concurrency_limits_v2.read_concurrency_limit(
|
51
|
+
session, name=id_or_name
|
52
|
+
)
|
53
|
+
|
54
|
+
if not model:
|
55
|
+
raise HTTPException(
|
56
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Concurrency Limit not found"
|
57
|
+
)
|
58
|
+
|
59
|
+
return schemas.responses.GlobalConcurrencyLimitResponse.model_validate(model)
|
60
|
+
|
61
|
+
|
62
|
+
@router.post("/filter")
|
63
|
+
async def read_all_concurrency_limits_v2(
|
64
|
+
limit: int = LimitBody(),
|
65
|
+
offset: int = Body(0, ge=0),
|
66
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
67
|
+
) -> List[schemas.responses.GlobalConcurrencyLimitResponse]:
|
68
|
+
async with db.session_context() as session:
|
69
|
+
concurrency_limits = (
|
70
|
+
await models.concurrency_limits_v2.read_all_concurrency_limits(
|
71
|
+
session=session,
|
72
|
+
limit=limit,
|
73
|
+
offset=offset,
|
74
|
+
)
|
75
|
+
)
|
76
|
+
|
77
|
+
return [
|
78
|
+
schemas.responses.GlobalConcurrencyLimitResponse.model_validate(limit)
|
79
|
+
for limit in concurrency_limits
|
80
|
+
]
|
81
|
+
|
82
|
+
|
83
|
+
@router.patch("/{id_or_name}", status_code=status.HTTP_204_NO_CONTENT)
|
84
|
+
async def update_concurrency_limit_v2(
|
85
|
+
concurrency_limit: actions.ConcurrencyLimitV2Update,
|
86
|
+
id_or_name: Union[UUID, str] = Path(
|
87
|
+
..., description="The ID or name of the concurrency limit", alias="id_or_name"
|
88
|
+
),
|
89
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
90
|
+
) -> None:
|
91
|
+
if isinstance(id_or_name, str): # TODO: this seems like it shouldn't be necessary
|
92
|
+
try:
|
93
|
+
id_or_name = UUID(id_or_name)
|
94
|
+
except ValueError:
|
95
|
+
pass
|
96
|
+
async with db.session_context(begin_transaction=True) as session:
|
97
|
+
if isinstance(id_or_name, UUID):
|
98
|
+
updated = await models.concurrency_limits_v2.update_concurrency_limit(
|
99
|
+
session,
|
100
|
+
concurrency_limit_id=id_or_name,
|
101
|
+
concurrency_limit=concurrency_limit,
|
102
|
+
)
|
103
|
+
else:
|
104
|
+
updated = await models.concurrency_limits_v2.update_concurrency_limit(
|
105
|
+
session, name=id_or_name, concurrency_limit=concurrency_limit
|
106
|
+
)
|
107
|
+
|
108
|
+
if not updated:
|
109
|
+
raise HTTPException(
|
110
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Concurrency Limit not found"
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
@router.delete("/{id_or_name}", status_code=status.HTTP_204_NO_CONTENT)
|
115
|
+
async def delete_concurrency_limit_v2(
|
116
|
+
id_or_name: Union[UUID, str] = Path(
|
117
|
+
..., description="The ID or name of the concurrency limit", alias="id_or_name"
|
118
|
+
),
|
119
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
120
|
+
) -> None:
|
121
|
+
if isinstance(id_or_name, str): # TODO: this seems like it shouldn't be necessary
|
122
|
+
try:
|
123
|
+
id_or_name = UUID(id_or_name)
|
124
|
+
except ValueError:
|
125
|
+
pass
|
126
|
+
async with db.session_context(begin_transaction=True) as session:
|
127
|
+
deleted = False
|
128
|
+
if isinstance(id_or_name, UUID):
|
129
|
+
deleted = await models.concurrency_limits_v2.delete_concurrency_limit(
|
130
|
+
session, concurrency_limit_id=id_or_name
|
131
|
+
)
|
132
|
+
else:
|
133
|
+
deleted = await models.concurrency_limits_v2.delete_concurrency_limit(
|
134
|
+
session, name=id_or_name
|
135
|
+
)
|
136
|
+
|
137
|
+
if not deleted:
|
138
|
+
raise HTTPException(
|
139
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Concurrency Limit not found"
|
140
|
+
)
|
141
|
+
|
142
|
+
|
143
|
+
class MinimalConcurrencyLimitResponse(PrefectBaseModel):
|
144
|
+
id: UUID
|
145
|
+
name: str
|
146
|
+
limit: int
|
147
|
+
|
148
|
+
|
149
|
+
@router.post("/increment", status_code=status.HTTP_200_OK)
|
150
|
+
async def bulk_increment_active_slots(
|
151
|
+
slots: int = Body(..., gt=0),
|
152
|
+
names: List[str] = Body(..., min_items=1),
|
153
|
+
mode: Literal["concurrency", "rate_limit"] = Body("concurrency"),
|
154
|
+
create_if_missing: Optional[bool] = Body(None),
|
155
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
156
|
+
) -> List[MinimalConcurrencyLimitResponse]:
|
157
|
+
async with db.session_context(begin_transaction=True) as session:
|
158
|
+
limits = [
|
159
|
+
schemas.core.ConcurrencyLimitV2.model_validate(limit)
|
160
|
+
for limit in (
|
161
|
+
await models.concurrency_limits_v2.bulk_read_or_create_concurrency_limits(
|
162
|
+
session=session, names=names, create_if_missing=create_if_missing
|
163
|
+
)
|
164
|
+
)
|
165
|
+
]
|
166
|
+
|
167
|
+
active_limits = [limit for limit in limits if bool(limit.active)]
|
168
|
+
|
169
|
+
if any(limit.limit < slots for limit in active_limits):
|
170
|
+
raise HTTPException(
|
171
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
172
|
+
detail="Slots requested is greater than the limit",
|
173
|
+
)
|
174
|
+
|
175
|
+
non_decaying = [
|
176
|
+
str(limit.name)
|
177
|
+
for limit in active_limits
|
178
|
+
if limit.slot_decay_per_second == 0.0
|
179
|
+
]
|
180
|
+
|
181
|
+
if mode == "rate_limit" and non_decaying:
|
182
|
+
raise HTTPException(
|
183
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
184
|
+
detail=(
|
185
|
+
"Only concurrency limits with slot decay can be used for "
|
186
|
+
"rate limiting. The following limits do not have a decay "
|
187
|
+
f"configured: {','.join(non_decaying)!r}"
|
188
|
+
),
|
189
|
+
)
|
190
|
+
acquired = await models.concurrency_limits_v2.bulk_increment_active_slots(
|
191
|
+
session=session,
|
192
|
+
concurrency_limit_ids=[limit.id for limit in active_limits],
|
193
|
+
slots=slots,
|
194
|
+
)
|
195
|
+
|
196
|
+
if not acquired:
|
197
|
+
await session.rollback()
|
198
|
+
|
199
|
+
if acquired:
|
200
|
+
return [
|
201
|
+
MinimalConcurrencyLimitResponse(
|
202
|
+
id=limit.id, name=str(limit.name), limit=limit.limit
|
203
|
+
)
|
204
|
+
for limit in limits
|
205
|
+
]
|
206
|
+
else:
|
207
|
+
async with db.session_context(begin_transaction=True) as session:
|
208
|
+
await models.concurrency_limits_v2.bulk_update_denied_slots(
|
209
|
+
session=session,
|
210
|
+
concurrency_limit_ids=[limit.id for limit in active_limits],
|
211
|
+
slots=slots,
|
212
|
+
)
|
213
|
+
|
214
|
+
def num_blocking_slots(limit: schemas.core.ConcurrencyLimitV2) -> float:
|
215
|
+
if limit.slot_decay_per_second > 0.0:
|
216
|
+
return slots + limit.denied_slots
|
217
|
+
else:
|
218
|
+
return (slots + limit.denied_slots) / limit.limit
|
219
|
+
|
220
|
+
blocking_limit = max((limit for limit in active_limits), key=num_blocking_slots)
|
221
|
+
blocking_slots = num_blocking_slots(blocking_limit)
|
222
|
+
|
223
|
+
wait_time_per_slot = (
|
224
|
+
blocking_limit.avg_slot_occupancy_seconds
|
225
|
+
if blocking_limit.slot_decay_per_second == 0.0
|
226
|
+
else (1.0 / blocking_limit.slot_decay_per_second)
|
227
|
+
)
|
228
|
+
|
229
|
+
retry_after = wait_time_per_slot * blocking_slots
|
230
|
+
|
231
|
+
raise HTTPException(
|
232
|
+
status_code=status.HTTP_423_LOCKED,
|
233
|
+
headers={
|
234
|
+
"Retry-After": str(retry_after),
|
235
|
+
},
|
236
|
+
)
|
237
|
+
|
238
|
+
|
239
|
+
@router.post("/decrement", status_code=status.HTTP_200_OK)
|
240
|
+
async def bulk_decrement_active_slots(
|
241
|
+
slots: int = Body(..., gt=0),
|
242
|
+
names: List[str] = Body(..., min_items=1),
|
243
|
+
occupancy_seconds: Optional[float] = Body(None, gt=0.0),
|
244
|
+
create_if_missing: bool = Body(True),
|
245
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
246
|
+
) -> List[MinimalConcurrencyLimitResponse]:
|
247
|
+
async with db.session_context(begin_transaction=True) as session:
|
248
|
+
limits = (
|
249
|
+
await models.concurrency_limits_v2.bulk_read_or_create_concurrency_limits(
|
250
|
+
session=session, names=names, create_if_missing=create_if_missing
|
251
|
+
)
|
252
|
+
)
|
253
|
+
|
254
|
+
if not limits:
|
255
|
+
return []
|
256
|
+
|
257
|
+
await models.concurrency_limits_v2.bulk_decrement_active_slots(
|
258
|
+
session=session,
|
259
|
+
concurrency_limit_ids=[limit.id for limit in limits if bool(limit.active)],
|
260
|
+
slots=slots,
|
261
|
+
occupancy_seconds=occupancy_seconds,
|
262
|
+
)
|
263
|
+
|
264
|
+
return [
|
265
|
+
MinimalConcurrencyLimitResponse(
|
266
|
+
id=limit.id, name=str(limit.name), limit=limit.limit
|
267
|
+
)
|
268
|
+
for limit in limits
|
269
|
+
]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
from fastapi import Depends, Query, status
|
4
|
+
from starlette.exceptions import HTTPException
|
5
|
+
|
6
|
+
from prefect.logging import get_logger
|
7
|
+
from prefect.server import models, schemas
|
8
|
+
from prefect.server.database import PrefectDBInterface, provide_database_interface
|
9
|
+
from prefect.server.utilities.server import PrefectRouter
|
10
|
+
from prefect.settings import PREFECT_SERVER_CSRF_PROTECTION_ENABLED
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
import logging
|
14
|
+
|
15
|
+
logger: "logging.Logger" = get_logger("server.api")
|
16
|
+
|
17
|
+
router: PrefectRouter = PrefectRouter(prefix="/csrf-token")
|
18
|
+
|
19
|
+
|
20
|
+
@router.get("")
|
21
|
+
async def create_csrf_token(
|
22
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
23
|
+
client: str = Query(..., description="The client to create a CSRF token for"),
|
24
|
+
) -> schemas.core.CsrfToken:
|
25
|
+
"""Create or update a CSRF token for a client"""
|
26
|
+
if PREFECT_SERVER_CSRF_PROTECTION_ENABLED.value() is False:
|
27
|
+
raise HTTPException(
|
28
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
29
|
+
detail="CSRF protection is disabled.",
|
30
|
+
)
|
31
|
+
|
32
|
+
async with db.session_context(begin_transaction=True) as session:
|
33
|
+
token = await models.csrf_token.create_or_update_csrf_token(
|
34
|
+
session=session, client=client
|
35
|
+
)
|
36
|
+
await models.csrf_token.delete_expired_tokens(session=session)
|
37
|
+
|
38
|
+
return token
|
@@ -0,0 +1,196 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for injecting FastAPI dependencies.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import re
|
9
|
+
from base64 import b64decode
|
10
|
+
from typing import Annotated, Any, Optional
|
11
|
+
from uuid import UUID
|
12
|
+
|
13
|
+
from fastapi import Body, Depends, Header, HTTPException, status
|
14
|
+
from packaging.version import Version
|
15
|
+
from starlette.requests import Request
|
16
|
+
|
17
|
+
from prefect.server import schemas
|
18
|
+
from prefect.settings import PREFECT_API_DEFAULT_LIMIT
|
19
|
+
|
20
|
+
|
21
|
+
def provide_request_api_version(
|
22
|
+
x_prefect_api_version: str = Header(None),
|
23
|
+
) -> Version | None:
|
24
|
+
if not x_prefect_api_version:
|
25
|
+
return
|
26
|
+
|
27
|
+
# parse version
|
28
|
+
try:
|
29
|
+
_, _, _ = [int(v) for v in x_prefect_api_version.split(".")]
|
30
|
+
except ValueError:
|
31
|
+
raise HTTPException(
|
32
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
33
|
+
detail=(
|
34
|
+
"Invalid X-PREFECT-API-VERSION header format.Expected header in format"
|
35
|
+
f" 'x.y.z' but received {x_prefect_api_version}"
|
36
|
+
),
|
37
|
+
)
|
38
|
+
return Version(x_prefect_api_version)
|
39
|
+
|
40
|
+
|
41
|
+
class EnforceMinimumAPIVersion:
|
42
|
+
"""
|
43
|
+
FastAPI Dependency used to check compatibility between the version of the api
|
44
|
+
and a given request.
|
45
|
+
|
46
|
+
Looks for the header 'X-PREFECT-API-VERSION' in the request and compares it
|
47
|
+
to the api's version. Rejects requests that are lower than the minimum version.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, minimum_api_version: str, logger: logging.Logger):
|
51
|
+
self.minimum_api_version = minimum_api_version
|
52
|
+
versions = [int(v) for v in minimum_api_version.split(".")]
|
53
|
+
self.api_major: int = versions[0]
|
54
|
+
self.api_minor: int = versions[1]
|
55
|
+
self.api_patch: int = versions[2]
|
56
|
+
self.logger = logger
|
57
|
+
|
58
|
+
async def __call__(
|
59
|
+
self,
|
60
|
+
x_prefect_api_version: str = Header(None),
|
61
|
+
) -> None:
|
62
|
+
request_version = x_prefect_api_version
|
63
|
+
|
64
|
+
# if no version header, assume latest and continue
|
65
|
+
if not request_version:
|
66
|
+
return
|
67
|
+
|
68
|
+
# parse version
|
69
|
+
try:
|
70
|
+
major, minor, patch = [int(v) for v in request_version.split(".")]
|
71
|
+
except ValueError:
|
72
|
+
await self._notify_of_invalid_value(request_version)
|
73
|
+
raise HTTPException(
|
74
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
75
|
+
detail=(
|
76
|
+
"Invalid X-PREFECT-API-VERSION header format."
|
77
|
+
f"Expected header in format 'x.y.z' but received {request_version}"
|
78
|
+
),
|
79
|
+
)
|
80
|
+
|
81
|
+
if (major, minor, patch) < (self.api_major, self.api_minor, self.api_patch):
|
82
|
+
await self._notify_of_outdated_version(request_version)
|
83
|
+
raise HTTPException(
|
84
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
85
|
+
detail=(
|
86
|
+
f"The request specified API version {request_version} but this "
|
87
|
+
f"server requires version {self.minimum_api_version} or higher."
|
88
|
+
),
|
89
|
+
)
|
90
|
+
|
91
|
+
async def _notify_of_invalid_value(self, request_version: str):
|
92
|
+
self.logger.error(
|
93
|
+
f"Invalid X-PREFECT-API-VERSION header format: '{request_version}'"
|
94
|
+
)
|
95
|
+
|
96
|
+
async def _notify_of_outdated_version(self, request_version: str):
|
97
|
+
self.logger.error(
|
98
|
+
f"X-PREFECT-API-VERSION header specifies version '{request_version}' "
|
99
|
+
f"but minimum allowed version is '{self.minimum_api_version}'"
|
100
|
+
)
|
101
|
+
|
102
|
+
|
103
|
+
def LimitBody() -> Any:
|
104
|
+
"""
|
105
|
+
A `fastapi.Depends` factory for pulling a `limit: int` parameter from the
|
106
|
+
request body while determining the default from the current settings.
|
107
|
+
"""
|
108
|
+
|
109
|
+
def get_limit(
|
110
|
+
limit: int = Body(
|
111
|
+
None,
|
112
|
+
description="Defaults to PREFECT_API_DEFAULT_LIMIT if not provided.",
|
113
|
+
),
|
114
|
+
):
|
115
|
+
default_limit = PREFECT_API_DEFAULT_LIMIT.value()
|
116
|
+
limit = limit if limit is not None else default_limit
|
117
|
+
if not limit >= 0:
|
118
|
+
raise HTTPException(
|
119
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
120
|
+
detail="Invalid limit: must be greater than or equal to 0.",
|
121
|
+
)
|
122
|
+
if limit > default_limit:
|
123
|
+
raise HTTPException(
|
124
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
125
|
+
detail=f"Invalid limit: must be less than or equal to {default_limit}.",
|
126
|
+
)
|
127
|
+
return limit
|
128
|
+
|
129
|
+
return Depends(get_limit)
|
130
|
+
|
131
|
+
|
132
|
+
def get_created_by(
|
133
|
+
prefect_automation_id: Optional[UUID] = Header(None, include_in_schema=False),
|
134
|
+
prefect_automation_name: Optional[str] = Header(None, include_in_schema=False),
|
135
|
+
) -> Optional[schemas.core.CreatedBy]:
|
136
|
+
"""A dependency that returns the provenance information to use when creating objects
|
137
|
+
during this API call."""
|
138
|
+
if prefect_automation_id and prefect_automation_name:
|
139
|
+
try:
|
140
|
+
display_value = b64decode(prefect_automation_name.encode()).decode()
|
141
|
+
except Exception:
|
142
|
+
display_value = None
|
143
|
+
|
144
|
+
if display_value:
|
145
|
+
return schemas.core.CreatedBy(
|
146
|
+
id=prefect_automation_id,
|
147
|
+
type="AUTOMATION",
|
148
|
+
display_value=display_value,
|
149
|
+
)
|
150
|
+
|
151
|
+
return None
|
152
|
+
|
153
|
+
|
154
|
+
def get_updated_by(
|
155
|
+
prefect_automation_id: Optional[UUID] = Header(None, include_in_schema=False),
|
156
|
+
prefect_automation_name: Optional[str] = Header(None, include_in_schema=False),
|
157
|
+
) -> Optional[schemas.core.UpdatedBy]:
|
158
|
+
"""A dependency that returns the provenance information to use when updating objects
|
159
|
+
during this API call."""
|
160
|
+
if prefect_automation_id and prefect_automation_name:
|
161
|
+
return schemas.core.UpdatedBy(
|
162
|
+
id=prefect_automation_id,
|
163
|
+
type="AUTOMATION",
|
164
|
+
display_value=prefect_automation_name,
|
165
|
+
)
|
166
|
+
|
167
|
+
return None
|
168
|
+
|
169
|
+
|
170
|
+
def is_ephemeral_request(request: Request) -> bool:
|
171
|
+
"""
|
172
|
+
A dependency that returns whether the request is to an ephemeral server.
|
173
|
+
"""
|
174
|
+
return "ephemeral-prefect" in str(request.base_url)
|
175
|
+
|
176
|
+
|
177
|
+
PREFECT_CLIENT_USER_AGENT_PATTERN = re.compile(
|
178
|
+
r"^prefect/(\d+\.\d+\.\d+(?:[a-z.+0-9]+)?) \(API \S+\)$"
|
179
|
+
)
|
180
|
+
|
181
|
+
|
182
|
+
def get_prefect_client_version(
|
183
|
+
user_agent: Annotated[Optional[str], Header(include_in_schema=False)] = None,
|
184
|
+
) -> Optional[str]:
|
185
|
+
"""
|
186
|
+
Attempts to parse out the Prefect client version from the User-Agent header.
|
187
|
+
|
188
|
+
The Prefect client sets the User-Agent header like so:
|
189
|
+
f"prefect/{prefect.__version__} (API {constants.SERVER_API_VERSION})"
|
190
|
+
"""
|
191
|
+
if not user_agent:
|
192
|
+
return None
|
193
|
+
|
194
|
+
if client_version := PREFECT_CLIENT_USER_AGENT_PATTERN.match(user_agent):
|
195
|
+
return client_version.group(1)
|
196
|
+
return None
|