prefect-client 2.18.2__py3-none-any.whl → 2.19.0__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 (41) hide show
  1. prefect/__init__.py +1 -15
  2. prefect/_internal/concurrency/cancellation.py +2 -0
  3. prefect/_internal/schemas/validators.py +10 -0
  4. prefect/_vendor/starlette/testclient.py +1 -1
  5. prefect/blocks/notifications.py +6 -6
  6. prefect/client/base.py +244 -1
  7. prefect/client/cloud.py +4 -2
  8. prefect/client/orchestration.py +515 -106
  9. prefect/client/schemas/actions.py +58 -8
  10. prefect/client/schemas/objects.py +15 -1
  11. prefect/client/schemas/responses.py +19 -0
  12. prefect/client/schemas/schedules.py +1 -1
  13. prefect/client/utilities.py +2 -2
  14. prefect/concurrency/asyncio.py +34 -4
  15. prefect/concurrency/sync.py +40 -6
  16. prefect/context.py +2 -2
  17. prefect/engine.py +17 -1
  18. prefect/events/clients.py +2 -2
  19. prefect/flows.py +91 -17
  20. prefect/infrastructure/process.py +0 -17
  21. prefect/logging/formatters.py +1 -4
  22. prefect/new_flow_engine.py +166 -161
  23. prefect/new_task_engine.py +137 -202
  24. prefect/runner/__init__.py +1 -1
  25. prefect/runner/runner.py +2 -107
  26. prefect/settings.py +11 -0
  27. prefect/tasks.py +76 -57
  28. prefect/types/__init__.py +27 -5
  29. prefect/utilities/annotations.py +1 -8
  30. prefect/utilities/asyncutils.py +4 -0
  31. prefect/utilities/engine.py +106 -1
  32. prefect/utilities/schema_tools/__init__.py +6 -1
  33. prefect/utilities/schema_tools/validation.py +25 -8
  34. prefect/utilities/timeout.py +34 -0
  35. prefect/workers/base.py +7 -3
  36. prefect/workers/process.py +0 -17
  37. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/METADATA +1 -1
  38. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/RECORD +41 -40
  39. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/LICENSE +0 -0
  40. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/WHEEL +0 -0
  41. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/top_level.txt +0 -0
prefect/__init__.py CHANGED
@@ -25,25 +25,12 @@ __ui_static_path__ = __module_path__ / "server" / "ui"
25
25
 
26
26
  del _version, pathlib
27
27
 
28
- if sys.version_info < (3, 8):
29
- warnings.warn(
30
- (
31
- "Prefect dropped support for Python 3.7 when it reached end-of-life"
32
- " . To use new versions of Prefect, you will need"
33
- " to upgrade to Python 3.8+. See https://devguide.python.org/versions/ for "
34
- " more details."
35
- ),
36
- FutureWarning,
37
- stacklevel=2,
38
- )
39
-
40
28
 
41
29
  # Import user-facing API
42
- from prefect.runner import Runner, serve
43
30
  from prefect.deployments import deploy
44
31
  from prefect.states import State
45
32
  from prefect.logging import get_run_logger
46
- from prefect.flows import flow, Flow
33
+ from prefect.flows import flow, Flow, serve
47
34
  from prefect.tasks import task, Task
48
35
  from prefect.context import tags
49
36
  from prefect.manifests import Manifest
@@ -159,7 +146,6 @@ __all__ = [
159
146
  "task",
160
147
  "Task",
161
148
  "unmapped",
162
- "Runner",
163
149
  "serve",
164
150
  "deploy",
165
151
  "pause_flow_run",
@@ -581,6 +581,8 @@ def _send_exception_to_thread(thread: threading.Thread, exc_type: Type[BaseExcep
581
581
 
582
582
  This will not interrupt long-running system calls like `sleep` or `wait`.
583
583
  """
584
+ if not thread.ident:
585
+ raise ValueError("Thread is not started.")
584
586
  ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
585
587
  ctypes.c_long(thread.ident), ctypes.py_object(exc_type)
586
588
  )
@@ -112,6 +112,10 @@ def validate_values_conform_to_schema(
112
112
  """
113
113
  Validate that the provided values conform to the provided json schema.
114
114
 
115
+ TODO: This schema validation is outdated. The latest version is
116
+ prefect.utilities.schema_tools.validate, which handles fixes to Pydantic v1
117
+ schemas for null values and tuples.
118
+
115
119
  Args:
116
120
  values: The values to validate.
117
121
  schema: The schema to validate against.
@@ -322,6 +326,12 @@ def set_deployment_schedules(values: dict) -> dict:
322
326
  return values
323
327
 
324
328
 
329
+ def validate_schedule_max_scheduled_runs(v: Optional[int], limit: int) -> Optional[int]:
330
+ if v is not None and v > limit:
331
+ raise ValueError(f"`max_scheduled_runs` must be less than or equal to {limit}.")
332
+ return v
333
+
334
+
325
335
  def remove_old_deployment_fields(values: dict) -> dict:
326
336
  # 2.7.7 removed worker_pool_queue_id in lieu of worker_pool_name and
327
337
  # worker_pool_queue_name. Those fields were later renamed to work_pool_name
@@ -461,7 +461,7 @@ class TestClient(httpx.Client):
461
461
  ] = httpx._client.USE_CLIENT_DEFAULT,
462
462
  extensions: typing.Optional[typing.Dict[str, typing.Any]] = None,
463
463
  ) -> httpx.Response:
464
- url = self.base_url.join(url)
464
+ url = self._merge_url(url)
465
465
  redirect = self._choose_redirect_arg(follow_redirects, allow_redirects)
466
466
  return super().request(
467
467
  method,
@@ -235,7 +235,7 @@ class PagerDutyWebHook(AbstractAppriseNotificationBlock):
235
235
  )
236
236
 
237
237
  def block_initialization(self) -> None:
238
- from apprise.plugins.NotifyPagerDuty import NotifyPagerDuty
238
+ from apprise.plugins.pagerduty import NotifyPagerDuty
239
239
 
240
240
  url = SecretStr(
241
241
  NotifyPagerDuty(
@@ -303,7 +303,7 @@ class TwilioSMS(AbstractAppriseNotificationBlock):
303
303
  )
304
304
 
305
305
  def block_initialization(self) -> None:
306
- from apprise.plugins.NotifyTwilio import NotifyTwilio
306
+ from apprise.plugins.twilio import NotifyTwilio
307
307
 
308
308
  url = SecretStr(
309
309
  NotifyTwilio(
@@ -401,7 +401,7 @@ class OpsgenieWebhook(AbstractAppriseNotificationBlock):
401
401
  )
402
402
 
403
403
  def block_initialization(self) -> None:
404
- from apprise.plugins.NotifyOpsgenie import NotifyOpsgenie
404
+ from apprise.plugins.opsgenie import NotifyOpsgenie
405
405
 
406
406
  targets = []
407
407
  if self.target_user:
@@ -489,7 +489,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock):
489
489
  )
490
490
 
491
491
  def block_initialization(self) -> None:
492
- from apprise.plugins.NotifyMattermost import NotifyMattermost
492
+ from apprise.plugins.mattermost import NotifyMattermost
493
493
 
494
494
  url = SecretStr(
495
495
  NotifyMattermost(
@@ -582,7 +582,7 @@ class DiscordWebhook(AbstractAppriseNotificationBlock):
582
582
  )
583
583
 
584
584
  def block_initialization(self) -> None:
585
- from apprise.plugins.NotifyDiscord import NotifyDiscord
585
+ from apprise.plugins.discord import NotifyDiscord
586
586
 
587
587
  url = SecretStr(
588
588
  NotifyDiscord(
@@ -773,7 +773,7 @@ class SendgridEmail(AbstractAppriseNotificationBlock):
773
773
  )
774
774
 
775
775
  def block_initialization(self) -> None:
776
- from apprise.plugins.NotifySendGrid import NotifySendGrid
776
+ from apprise.plugins.sendgrid import NotifySendGrid
777
777
 
778
778
  url = SecretStr(
779
779
  NotifySendGrid(
prefect/client/base.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import copy
2
2
  import sys
3
3
  import threading
4
+ import time
4
5
  import uuid
5
6
  from collections import defaultdict
6
7
  from contextlib import asynccontextmanager
@@ -25,6 +26,7 @@ import httpx
25
26
  from asgi_lifespan import LifespanManager
26
27
  from httpx import HTTPStatusError, Request, Response
27
28
  from prefect._vendor.starlette import status
29
+ from prefect._vendor.starlette.testclient import TestClient
28
30
  from typing_extensions import Self
29
31
 
30
32
  import prefect
@@ -182,7 +184,7 @@ class PrefectResponse(httpx.Response):
182
184
  return new_response
183
185
 
184
186
 
185
- class PrefectHttpxClient(httpx.AsyncClient):
187
+ class PrefectHttpxAsyncClient(httpx.AsyncClient):
186
188
  """
187
189
  A Prefect wrapper for the async httpx client with support for retry-after headers
188
190
  for the provided status codes (typically 429, 502 and 503).
@@ -394,3 +396,244 @@ class PrefectHttpxClient(httpx.AsyncClient):
394
396
 
395
397
  request.headers["Prefect-Csrf-Token"] = self.csrf_token
396
398
  request.headers["Prefect-Csrf-Client"] = str(self.csrf_client_id)
399
+
400
+
401
+ class PrefectHttpxSyncClient(httpx.Client):
402
+ """
403
+ A Prefect wrapper for the async httpx client with support for retry-after headers
404
+ for the provided status codes (typically 429, 502 and 503).
405
+
406
+ Additionally, this client will always call `raise_for_status` on responses.
407
+
408
+ For more details on rate limit headers, see:
409
+ [Configuring Cloudflare Rate Limiting](https://support.cloudflare.com/hc/en-us/articles/115001635128-Configuring-Rate-Limiting-from-UI)
410
+ """
411
+
412
+ def __init__(
413
+ self,
414
+ *args,
415
+ enable_csrf_support: bool = False,
416
+ raise_on_all_errors: bool = True,
417
+ **kwargs,
418
+ ):
419
+ self.enable_csrf_support: bool = enable_csrf_support
420
+ self.csrf_token: Optional[str] = None
421
+ self.csrf_token_expiration: Optional[datetime] = None
422
+ self.csrf_client_id: uuid.UUID = uuid.uuid4()
423
+ self.raise_on_all_errors: bool = raise_on_all_errors
424
+
425
+ super().__init__(*args, **kwargs)
426
+
427
+ user_agent = (
428
+ f"prefect/{prefect.__version__} (API {constants.SERVER_API_VERSION})"
429
+ )
430
+ self.headers["User-Agent"] = user_agent
431
+
432
+ def _send_with_retry(
433
+ self,
434
+ request: Request,
435
+ send: Callable[[Request], Response],
436
+ send_args: Tuple,
437
+ send_kwargs: Dict,
438
+ retry_codes: Set[int] = set(),
439
+ retry_exceptions: Tuple[Exception, ...] = tuple(),
440
+ ):
441
+ """
442
+ Send a request and retry it if it fails.
443
+
444
+ Sends the provided request and retries it up to PREFECT_CLIENT_MAX_RETRIES times
445
+ if the request either raises an exception listed in `retry_exceptions` or
446
+ receives a response with a status code listed in `retry_codes`.
447
+
448
+ Retries will be delayed based on either the retry header (preferred) or
449
+ exponential backoff if a retry header is not provided.
450
+ """
451
+ try_count = 0
452
+ response = None
453
+
454
+ is_change_request = request.method.lower() in {"post", "put", "patch", "delete"}
455
+
456
+ if self.enable_csrf_support and is_change_request:
457
+ self._add_csrf_headers(request=request)
458
+
459
+ while try_count <= PREFECT_CLIENT_MAX_RETRIES.value():
460
+ try_count += 1
461
+ retry_seconds = None
462
+ exc_info = None
463
+
464
+ try:
465
+ response = send(request, *send_args, **send_kwargs)
466
+ except retry_exceptions: # type: ignore
467
+ if try_count > PREFECT_CLIENT_MAX_RETRIES.value():
468
+ raise
469
+ # Otherwise, we will ignore this error but capture the info for logging
470
+ exc_info = sys.exc_info()
471
+ else:
472
+ # We got a response; check if it's a CSRF error, otherwise
473
+ # return immediately if it is not retryable
474
+ if (
475
+ response.status_code == status.HTTP_403_FORBIDDEN
476
+ and "Invalid CSRF token" in response.text
477
+ ):
478
+ # We got a CSRF error, clear the token and try again
479
+ self.csrf_token = None
480
+ self._add_csrf_headers(request)
481
+ elif response.status_code not in retry_codes:
482
+ return response
483
+
484
+ if "Retry-After" in response.headers:
485
+ retry_seconds = float(response.headers["Retry-After"])
486
+
487
+ # Use an exponential back-off if not set in a header
488
+ if retry_seconds is None:
489
+ retry_seconds = 2**try_count
490
+
491
+ # Add jitter
492
+ jitter_factor = PREFECT_CLIENT_RETRY_JITTER_FACTOR.value()
493
+ if retry_seconds > 0 and jitter_factor > 0:
494
+ if response is not None and "Retry-After" in response.headers:
495
+ # Always wait for _at least_ retry seconds if requested by the API
496
+ retry_seconds = bounded_poisson_interval(
497
+ retry_seconds, retry_seconds * (1 + jitter_factor)
498
+ )
499
+ else:
500
+ # Otherwise, use a symmetrical jitter
501
+ retry_seconds = clamped_poisson_interval(
502
+ retry_seconds, jitter_factor
503
+ )
504
+
505
+ logger.debug(
506
+ (
507
+ "Encountered retryable exception during request. "
508
+ if exc_info
509
+ else (
510
+ "Received response with retryable status code"
511
+ f" {response.status_code}. "
512
+ )
513
+ )
514
+ + f"Another attempt will be made in {retry_seconds}s. "
515
+ "This is attempt"
516
+ f" {try_count}/{PREFECT_CLIENT_MAX_RETRIES.value() + 1}.",
517
+ exc_info=exc_info,
518
+ )
519
+ time.sleep(retry_seconds)
520
+
521
+ assert (
522
+ response is not None
523
+ ), "Retry handling ended without response or exception"
524
+
525
+ # We ran out of retries, return the failed response
526
+ return response
527
+
528
+ def send(self, request: Request, *args, **kwargs) -> Response:
529
+ """
530
+ Send a request with automatic retry behavior for the following status codes:
531
+
532
+ - 403 Forbidden, if the request failed due to CSRF protection
533
+ - 408 Request Timeout
534
+ - 429 CloudFlare-style rate limiting
535
+ - 502 Bad Gateway
536
+ - 503 Service unavailable
537
+ - Any additional status codes provided in `PREFECT_CLIENT_RETRY_EXTRA_CODES`
538
+ """
539
+
540
+ super_send = super().send
541
+ response = self._send_with_retry(
542
+ request=request,
543
+ send=super_send,
544
+ send_args=args,
545
+ send_kwargs=kwargs,
546
+ retry_codes={
547
+ status.HTTP_429_TOO_MANY_REQUESTS,
548
+ status.HTTP_503_SERVICE_UNAVAILABLE,
549
+ status.HTTP_502_BAD_GATEWAY,
550
+ status.HTTP_408_REQUEST_TIMEOUT,
551
+ *PREFECT_CLIENT_RETRY_EXTRA_CODES.value(),
552
+ },
553
+ retry_exceptions=(
554
+ httpx.ReadTimeout,
555
+ httpx.PoolTimeout,
556
+ httpx.ConnectTimeout,
557
+ # `ConnectionResetError` when reading socket raises as a `ReadError`
558
+ httpx.ReadError,
559
+ # Sockets can be closed during writes resulting in a `WriteError`
560
+ httpx.WriteError,
561
+ # Uvicorn bug, see https://github.com/PrefectHQ/prefect/issues/7512
562
+ httpx.RemoteProtocolError,
563
+ # HTTP2 bug, see https://github.com/PrefectHQ/prefect/issues/7442
564
+ httpx.LocalProtocolError,
565
+ ),
566
+ )
567
+
568
+ # Convert to a Prefect response to add nicer errors messages
569
+ response = PrefectResponse.from_httpx_response(response)
570
+
571
+ if self.raise_on_all_errors:
572
+ response.raise_for_status()
573
+
574
+ return response
575
+
576
+ def _add_csrf_headers(self, request: Request):
577
+ now = datetime.now(timezone.utc)
578
+
579
+ if not self.enable_csrf_support:
580
+ return
581
+
582
+ if not self.csrf_token or (
583
+ self.csrf_token_expiration and now > self.csrf_token_expiration
584
+ ):
585
+ token_request = self.build_request(
586
+ "GET", f"/csrf-token?client={self.csrf_client_id}"
587
+ )
588
+
589
+ try:
590
+ token_response = self.send(token_request)
591
+ except PrefectHTTPStatusError as exc:
592
+ old_server = exc.response.status_code == status.HTTP_404_NOT_FOUND
593
+ unconfigured_server = (
594
+ exc.response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
595
+ and "CSRF protection is disabled." in exc.response.text
596
+ )
597
+
598
+ if old_server or unconfigured_server:
599
+ # The token endpoint is either unavailable, suggesting an
600
+ # older server, or CSRF protection is disabled. In either
601
+ # case we should disable CSRF support.
602
+ self.enable_csrf_support = False
603
+ return
604
+
605
+ raise
606
+
607
+ token: CsrfToken = CsrfToken.parse_obj(token_response.json())
608
+ self.csrf_token = token.token
609
+ self.csrf_token_expiration = token.expiration
610
+
611
+ request.headers["Prefect-Csrf-Token"] = self.csrf_token
612
+ request.headers["Prefect-Csrf-Client"] = str(self.csrf_client_id)
613
+
614
+
615
+ class PrefectHttpxSyncEphemeralClient(TestClient, PrefectHttpxSyncClient):
616
+ """
617
+ This client is a synchronous httpx client that can be used to talk directly
618
+ to an ASGI app, such as an ephemeral Prefect API.
619
+
620
+ It is a subclass of both Starlette's `TestClient` and Prefect's
621
+ `PrefectHttpxSyncClient`, so it combines the synchronous testing
622
+ capabilities of `TestClient` with the Prefect-specific behaviors of
623
+ `PrefectHttpxSyncClient`.
624
+ """
625
+
626
+ def __init__(
627
+ self,
628
+ *args,
629
+ # override TestClient default
630
+ raise_server_exceptions=False,
631
+ **kwargs,
632
+ ):
633
+ super().__init__(
634
+ *args,
635
+ raise_server_exceptions=raise_server_exceptions,
636
+ **kwargs,
637
+ )
638
+
639
+ pass
prefect/client/cloud.py CHANGED
@@ -15,7 +15,7 @@ from prefect._vendor.starlette import status
15
15
 
16
16
  import prefect.context
17
17
  import prefect.settings
18
- from prefect.client.base import PrefectHttpxClient
18
+ from prefect.client.base import PrefectHttpxAsyncClient
19
19
  from prefect.client.schemas import Workspace
20
20
  from prefect.exceptions import PrefectException
21
21
  from prefect.settings import (
@@ -72,7 +72,9 @@ class CloudClient:
72
72
  httpx_settings.setdefault("base_url", host)
73
73
  if not PREFECT_UNIT_TEST_MODE.value():
74
74
  httpx_settings.setdefault("follow_redirects", True)
75
- self._client = PrefectHttpxClient(**httpx_settings, enable_csrf_support=False)
75
+ self._client = PrefectHttpxAsyncClient(
76
+ **httpx_settings, enable_csrf_support=False
77
+ )
76
78
 
77
79
  async def api_healthcheck(self):
78
80
  """