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.
Files changed (72) hide show
  1. prefect/__init__.py +15 -8
  2. prefect/_build_info.py +5 -0
  3. prefect/_internal/schemas/bases.py +4 -7
  4. prefect/_internal/schemas/validators.py +5 -6
  5. prefect/_result_records.py +6 -1
  6. prefect/client/orchestration/__init__.py +18 -6
  7. prefect/client/schemas/schedules.py +2 -2
  8. prefect/concurrency/asyncio.py +4 -3
  9. prefect/concurrency/sync.py +3 -3
  10. prefect/concurrency/v1/asyncio.py +3 -3
  11. prefect/concurrency/v1/sync.py +3 -3
  12. prefect/deployments/flow_runs.py +2 -2
  13. prefect/docker/docker_image.py +2 -3
  14. prefect/engine.py +1 -1
  15. prefect/events/clients.py +4 -3
  16. prefect/events/related.py +3 -5
  17. prefect/flows.py +11 -5
  18. prefect/locking/filesystem.py +8 -8
  19. prefect/logging/handlers.py +7 -11
  20. prefect/main.py +0 -2
  21. prefect/runtime/flow_run.py +10 -17
  22. prefect/server/api/__init__.py +34 -0
  23. prefect/server/api/admin.py +85 -0
  24. prefect/server/api/artifacts.py +224 -0
  25. prefect/server/api/automations.py +239 -0
  26. prefect/server/api/block_capabilities.py +25 -0
  27. prefect/server/api/block_documents.py +164 -0
  28. prefect/server/api/block_schemas.py +153 -0
  29. prefect/server/api/block_types.py +211 -0
  30. prefect/server/api/clients.py +246 -0
  31. prefect/server/api/collections.py +75 -0
  32. prefect/server/api/concurrency_limits.py +286 -0
  33. prefect/server/api/concurrency_limits_v2.py +269 -0
  34. prefect/server/api/csrf_token.py +38 -0
  35. prefect/server/api/dependencies.py +196 -0
  36. prefect/server/api/deployments.py +941 -0
  37. prefect/server/api/events.py +300 -0
  38. prefect/server/api/flow_run_notification_policies.py +120 -0
  39. prefect/server/api/flow_run_states.py +52 -0
  40. prefect/server/api/flow_runs.py +867 -0
  41. prefect/server/api/flows.py +210 -0
  42. prefect/server/api/logs.py +43 -0
  43. prefect/server/api/middleware.py +73 -0
  44. prefect/server/api/root.py +35 -0
  45. prefect/server/api/run_history.py +170 -0
  46. prefect/server/api/saved_searches.py +99 -0
  47. prefect/server/api/server.py +891 -0
  48. prefect/server/api/task_run_states.py +52 -0
  49. prefect/server/api/task_runs.py +342 -0
  50. prefect/server/api/task_workers.py +31 -0
  51. prefect/server/api/templates.py +35 -0
  52. prefect/server/api/ui/__init__.py +3 -0
  53. prefect/server/api/ui/flow_runs.py +128 -0
  54. prefect/server/api/ui/flows.py +173 -0
  55. prefect/server/api/ui/schemas.py +63 -0
  56. prefect/server/api/ui/task_runs.py +175 -0
  57. prefect/server/api/validation.py +382 -0
  58. prefect/server/api/variables.py +181 -0
  59. prefect/server/api/work_queues.py +230 -0
  60. prefect/server/api/workers.py +656 -0
  61. prefect/settings/sources.py +18 -5
  62. prefect/states.py +3 -3
  63. prefect/task_engine.py +3 -3
  64. prefect/types/_datetime.py +82 -3
  65. prefect/utilities/dockerutils.py +2 -2
  66. prefect/workers/base.py +5 -5
  67. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/METADATA +10 -15
  68. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/RECORD +70 -32
  69. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/WHEEL +1 -2
  70. prefect/_version.py +0 -21
  71. prefect_client-3.2.1.dist-info/top_level.txt +0 -1
  72. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ import base64
2
+ from typing import TYPE_CHECKING, List, Optional
3
+
4
+ from fastapi import Response, WebSocket, status
5
+ from fastapi.exceptions import HTTPException
6
+ from fastapi.param_functions import Depends, Path
7
+ from fastapi.params import Body, Query
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from starlette.requests import Request
10
+ from starlette.status import WS_1002_PROTOCOL_ERROR
11
+
12
+ from prefect.logging import get_logger
13
+ from prefect.server.api.dependencies import is_ephemeral_request
14
+ from prefect.server.database import PrefectDBInterface, provide_database_interface
15
+ from prefect.server.events import messaging, stream
16
+ from prefect.server.events.counting import (
17
+ Countable,
18
+ InvalidEventCountParameters,
19
+ TimeUnit,
20
+ )
21
+ from prefect.server.events.filters import EventFilter, EventOrder
22
+ from prefect.server.events.models.automations import automations_session
23
+ from prefect.server.events.pipeline import EventsPipeline
24
+ from prefect.server.events.schemas.events import Event, EventCount, EventPage
25
+ from prefect.server.events.storage import (
26
+ INTERACTIVE_PAGE_SIZE,
27
+ InvalidTokenError,
28
+ database,
29
+ )
30
+ from prefect.server.utilities import subscriptions
31
+ from prefect.server.utilities.server import PrefectRouter
32
+ from prefect.settings import (
33
+ PREFECT_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL,
34
+ PREFECT_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ import logging
39
+
40
+ logger: "logging.Logger" = get_logger(__name__)
41
+
42
+
43
+ router: PrefectRouter = PrefectRouter(prefix="/events", tags=["Events"])
44
+
45
+
46
+ @router.post("", status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
47
+ async def create_events(
48
+ events: List[Event],
49
+ ephemeral_request: bool = Depends(is_ephemeral_request),
50
+ ) -> None:
51
+ """Record a batch of Events"""
52
+ if ephemeral_request:
53
+ await EventsPipeline().process_events(events)
54
+ else:
55
+ received_events = [event.receive() for event in events]
56
+ await messaging.publish(received_events)
57
+
58
+
59
+ @router.websocket("/in")
60
+ async def stream_events_in(websocket: WebSocket) -> None:
61
+ """Open a WebSocket to stream incoming Events"""
62
+
63
+ await websocket.accept()
64
+
65
+ try:
66
+ async with messaging.create_event_publisher() as publisher:
67
+ async for event_json in websocket.iter_text():
68
+ event = Event.model_validate_json(event_json)
69
+ await publisher.publish_event(event.receive())
70
+ except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS: # pragma: no cover
71
+ pass # it's fine if a client disconnects either normally or abnormally
72
+
73
+ return None
74
+
75
+
76
+ @router.websocket("/out")
77
+ async def stream_workspace_events_out(
78
+ websocket: WebSocket,
79
+ ) -> None:
80
+ """Open a WebSocket to stream Events"""
81
+ websocket = await subscriptions.accept_prefect_socket(
82
+ websocket,
83
+ )
84
+ if not websocket:
85
+ return
86
+
87
+ try:
88
+ # After authentication, the next message is expected to be a filter message, any
89
+ # other type of message will close the connection.
90
+ message = await websocket.receive_json()
91
+
92
+ if message["type"] != "filter":
93
+ return await websocket.close(
94
+ WS_1002_PROTOCOL_ERROR, reason="Expected 'filter' message"
95
+ )
96
+
97
+ wants_backfill = message.get("backfill", True)
98
+
99
+ try:
100
+ filter = EventFilter.model_validate(message["filter"])
101
+ except Exception as e:
102
+ return await websocket.close(
103
+ WS_1002_PROTOCOL_ERROR, reason=f"Invalid filter: {e}"
104
+ )
105
+
106
+ filter.occurred.clamp(PREFECT_EVENTS_MAXIMUM_WEBSOCKET_BACKFILL.value())
107
+ filter.order = EventOrder.ASC
108
+
109
+ # subscribe to the ongoing event stream first so we don't miss events...
110
+ async with stream.events(filter) as event_stream:
111
+ # ...then if the user wants, backfill up to the last 1k events...
112
+ if wants_backfill:
113
+ backfilled_ids = set()
114
+
115
+ async with automations_session() as session:
116
+ backfill, _, next_page = await database.query_events(
117
+ session=session,
118
+ filter=filter,
119
+ page_size=PREFECT_EVENTS_WEBSOCKET_BACKFILL_PAGE_SIZE.value(),
120
+ )
121
+
122
+ while backfill:
123
+ for event in backfill:
124
+ backfilled_ids.add(event.id)
125
+ await websocket.send_json(
126
+ {
127
+ "type": "event",
128
+ "event": event.model_dump(mode="json"),
129
+ }
130
+ )
131
+
132
+ if not next_page:
133
+ break
134
+
135
+ backfill, _, next_page = await database.query_next_page(
136
+ session=session,
137
+ page_token=next_page,
138
+ )
139
+
140
+ # ...before resuming the ongoing stream of events
141
+ async for event in event_stream:
142
+ if not event:
143
+ if await subscriptions.still_connected(websocket):
144
+ continue
145
+ break
146
+
147
+ if wants_backfill and event.id in backfilled_ids:
148
+ backfilled_ids.remove(event.id)
149
+ continue
150
+
151
+ await websocket.send_json(
152
+ {"type": "event", "event": event.model_dump(mode="json")}
153
+ )
154
+
155
+ except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS: # pragma: no cover
156
+ pass # it's fine if a client disconnects either normally or abnormally
157
+
158
+ return None
159
+
160
+
161
+ def verified_page_token(
162
+ page_token: str = Query(..., alias="page-token"),
163
+ ) -> str:
164
+ try:
165
+ page_token = base64.b64decode(page_token.encode()).decode()
166
+ except Exception:
167
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
168
+
169
+ if not page_token:
170
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
171
+
172
+ return page_token
173
+
174
+
175
+ @router.post(
176
+ "/filter",
177
+ )
178
+ async def read_events(
179
+ request: Request,
180
+ filter: Optional[EventFilter] = Body(
181
+ None,
182
+ description=(
183
+ "Additional optional filter criteria to narrow down the set of Events"
184
+ ),
185
+ ),
186
+ limit: int = Body(
187
+ INTERACTIVE_PAGE_SIZE,
188
+ ge=0,
189
+ le=INTERACTIVE_PAGE_SIZE,
190
+ embed=True,
191
+ description="The number of events to return with each page",
192
+ ),
193
+ db: PrefectDBInterface = Depends(provide_database_interface),
194
+ ) -> EventPage:
195
+ """
196
+ Queries for Events matching the given filter criteria in the given Account. Returns
197
+ the first page of results, and the URL to request the next page (if there are more
198
+ results).
199
+ """
200
+ filter = filter or EventFilter()
201
+ async with db.session_context() as session:
202
+ events, total, next_token = await database.query_events(
203
+ session=session,
204
+ filter=filter,
205
+ page_size=limit,
206
+ )
207
+
208
+ return EventPage(
209
+ events=events,
210
+ total=total,
211
+ next_page=generate_next_page_link(request, next_token),
212
+ )
213
+
214
+
215
+ @router.get(
216
+ "/filter/next",
217
+ )
218
+ async def read_account_events_page(
219
+ request: Request,
220
+ page_token: str = Depends(verified_page_token),
221
+ db: PrefectDBInterface = Depends(provide_database_interface),
222
+ ) -> EventPage:
223
+ """
224
+ Returns the next page of Events for a previous query against the given Account, and
225
+ the URL to request the next page (if there are more results).
226
+ """
227
+ async with db.session_context() as session:
228
+ try:
229
+ events, total, next_token = await database.query_next_page(
230
+ session=session, page_token=page_token
231
+ )
232
+ except InvalidTokenError:
233
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
234
+
235
+ return EventPage(
236
+ events=events,
237
+ total=total,
238
+ next_page=generate_next_page_link(request, next_token),
239
+ )
240
+
241
+
242
+ def generate_next_page_link(
243
+ request: Request,
244
+ page_token: Optional[str],
245
+ ) -> Optional[str]:
246
+ if not page_token:
247
+ return None
248
+
249
+ next_page = (
250
+ f"{request.base_url}api/events/filter/next"
251
+ f"?page-token={base64.b64encode(page_token.encode()).decode()}"
252
+ )
253
+ return next_page
254
+
255
+
256
+ @router.post(
257
+ "/count-by/{countable}",
258
+ )
259
+ async def count_account_events(
260
+ filter: EventFilter,
261
+ countable: Countable = Path(...),
262
+ time_unit: TimeUnit = Body(default=TimeUnit.day),
263
+ time_interval: float = Body(default=1.0, ge=0.01),
264
+ db: PrefectDBInterface = Depends(provide_database_interface),
265
+ ) -> List[EventCount]:
266
+ """
267
+ Returns distinct objects and the count of events associated with them. Objects
268
+ that can be counted include the day the event occurred, the type of event, or
269
+ the IDs of the resources associated with the event.
270
+ """
271
+ async with db.session_context() as session:
272
+ return await handle_event_count_request(
273
+ session=session,
274
+ filter=filter,
275
+ countable=countable,
276
+ time_unit=time_unit,
277
+ time_interval=time_interval,
278
+ )
279
+
280
+
281
+ async def handle_event_count_request(
282
+ session: AsyncSession,
283
+ filter: EventFilter,
284
+ countable: Countable,
285
+ time_unit: TimeUnit,
286
+ time_interval: float,
287
+ ) -> List[EventCount]:
288
+ try:
289
+ return await database.count_events(
290
+ session=session,
291
+ filter=filter,
292
+ countable=countable,
293
+ time_unit=time_unit,
294
+ time_interval=time_interval,
295
+ )
296
+ except InvalidEventCountParameters as exc:
297
+ raise HTTPException(
298
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
299
+ detail=exc.message,
300
+ )
@@ -0,0 +1,120 @@
1
+ """
2
+ Routes for interacting with flow run notification policy objects.
3
+ """
4
+
5
+ from typing import List
6
+ from uuid import UUID
7
+
8
+ from fastapi import Body, Depends, HTTPException, Path, status
9
+
10
+ import prefect.server.api.dependencies as dependencies
11
+ import prefect.server.models as models
12
+ import prefect.server.schemas as schemas
13
+ from prefect.server.database import PrefectDBInterface, provide_database_interface
14
+ from prefect.server.utilities.server import PrefectRouter
15
+
16
+ router: PrefectRouter = PrefectRouter(
17
+ prefix="/flow_run_notification_policies", tags=["Flow Run Notification Policies"]
18
+ )
19
+
20
+
21
+ @router.post("/", status_code=status.HTTP_201_CREATED)
22
+ async def create_flow_run_notification_policy(
23
+ flow_run_notification_policy: schemas.actions.FlowRunNotificationPolicyCreate,
24
+ db: PrefectDBInterface = Depends(provide_database_interface),
25
+ ) -> schemas.core.FlowRunNotificationPolicy:
26
+ """
27
+ Creates a new flow run notification policy.
28
+ """
29
+ async with db.session_context(begin_transaction=True) as session:
30
+ return await models.flow_run_notification_policies.create_flow_run_notification_policy(
31
+ session=session, flow_run_notification_policy=flow_run_notification_policy
32
+ )
33
+
34
+
35
+ @router.patch("/{id}", status_code=status.HTTP_204_NO_CONTENT)
36
+ async def update_flow_run_notification_policy(
37
+ flow_run_notification_policy: schemas.actions.FlowRunNotificationPolicyUpdate,
38
+ flow_run_notification_policy_id: UUID = Path(
39
+ ..., description="The flow run notification policy id", alias="id"
40
+ ),
41
+ db: PrefectDBInterface = Depends(provide_database_interface),
42
+ ) -> None:
43
+ """
44
+ Updates an existing flow run notification policy.
45
+ """
46
+ async with db.session_context(begin_transaction=True) as session:
47
+ result = await models.flow_run_notification_policies.update_flow_run_notification_policy(
48
+ session=session,
49
+ flow_run_notification_policy_id=flow_run_notification_policy_id,
50
+ flow_run_notification_policy=flow_run_notification_policy,
51
+ )
52
+ if not result:
53
+ raise HTTPException(
54
+ status_code=status.HTTP_404_NOT_FOUND,
55
+ detail=f"Flow run notification policy {id} not found",
56
+ )
57
+
58
+
59
+ @router.get("/{id}")
60
+ async def read_flow_run_notification_policy(
61
+ flow_run_notification_policy_id: UUID = Path(
62
+ ..., description="The flow run notification policy id", alias="id"
63
+ ),
64
+ db: PrefectDBInterface = Depends(provide_database_interface),
65
+ ) -> schemas.core.FlowRunNotificationPolicy:
66
+ """
67
+ Get a flow run notification policy by id.
68
+ """
69
+ async with db.session_context() as session:
70
+ flow_run_notification_policy = await models.flow_run_notification_policies.read_flow_run_notification_policy(
71
+ session=session,
72
+ flow_run_notification_policy_id=flow_run_notification_policy_id,
73
+ )
74
+ if not flow_run_notification_policy:
75
+ raise HTTPException(
76
+ status_code=status.HTTP_404_NOT_FOUND,
77
+ detail="flow run notification policy not found",
78
+ )
79
+ return flow_run_notification_policy
80
+
81
+
82
+ @router.post("/filter")
83
+ async def read_flow_run_notification_policies(
84
+ limit: int = dependencies.LimitBody(),
85
+ flow_run_notification_policy_filter: schemas.filters.FlowRunNotificationPolicyFilter = None,
86
+ offset: int = Body(0, ge=0),
87
+ db: PrefectDBInterface = Depends(provide_database_interface),
88
+ ) -> List[schemas.core.FlowRunNotificationPolicy]:
89
+ """
90
+ Query for flow run notification policies.
91
+ """
92
+ async with db.session_context() as session:
93
+ return await models.flow_run_notification_policies.read_flow_run_notification_policies(
94
+ session=session,
95
+ flow_run_notification_policy_filter=flow_run_notification_policy_filter,
96
+ offset=offset,
97
+ limit=limit,
98
+ )
99
+
100
+
101
+ @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
102
+ async def delete_flow_run_notification_policy(
103
+ flow_run_notification_policy_id: UUID = Path(
104
+ ..., description="The flow run notification policy id", alias="id"
105
+ ),
106
+ db: PrefectDBInterface = Depends(provide_database_interface),
107
+ ) -> None:
108
+ """
109
+ Delete a flow run notification policy by id.
110
+ """
111
+ async with db.session_context(begin_transaction=True) as session:
112
+ result = await models.flow_run_notification_policies.delete_flow_run_notification_policy(
113
+ session=session,
114
+ flow_run_notification_policy_id=flow_run_notification_policy_id,
115
+ )
116
+ if not result:
117
+ raise HTTPException(
118
+ status_code=status.HTTP_404_NOT_FOUND,
119
+ detail="flow run notification policy not found",
120
+ )
@@ -0,0 +1,52 @@
1
+ """
2
+ Routes for interacting with flow run state objects.
3
+ """
4
+
5
+ from typing import List
6
+ from uuid import UUID
7
+
8
+ from fastapi import Depends, HTTPException, Path, status
9
+
10
+ import prefect.server.models as models
11
+ import prefect.server.schemas as schemas
12
+ from prefect.server.database import PrefectDBInterface, provide_database_interface
13
+ from prefect.server.utilities.server import PrefectRouter
14
+
15
+ router: PrefectRouter = PrefectRouter(
16
+ prefix="/flow_run_states", tags=["Flow Run States"]
17
+ )
18
+
19
+
20
+ @router.get("/{id}")
21
+ async def read_flow_run_state(
22
+ flow_run_state_id: UUID = Path(
23
+ ..., description="The flow run state id", alias="id"
24
+ ),
25
+ db: PrefectDBInterface = Depends(provide_database_interface),
26
+ ) -> schemas.states.State:
27
+ """
28
+ Get a flow run state by id.
29
+ """
30
+ async with db.session_context() as session:
31
+ flow_run_state = await models.flow_run_states.read_flow_run_state(
32
+ session=session, flow_run_state_id=flow_run_state_id
33
+ )
34
+ if not flow_run_state:
35
+ raise HTTPException(
36
+ status.HTTP_404_NOT_FOUND, detail="Flow run state not found"
37
+ )
38
+ return flow_run_state
39
+
40
+
41
+ @router.get("/")
42
+ async def read_flow_run_states(
43
+ flow_run_id: UUID,
44
+ db: PrefectDBInterface = Depends(provide_database_interface),
45
+ ) -> List[schemas.states.State]:
46
+ """
47
+ Get states associated with a flow run.
48
+ """
49
+ async with db.session_context() as session:
50
+ return await models.flow_run_states.read_flow_run_states(
51
+ session=session, flow_run_id=flow_run_id
52
+ )