prefect-client 3.0.0rc13__py3-none-any.whl → 3.0.0rc15__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 (36) hide show
  1. prefect/_internal/compatibility/deprecated.py +0 -53
  2. prefect/blocks/core.py +132 -4
  3. prefect/blocks/notifications.py +26 -3
  4. prefect/client/base.py +30 -24
  5. prefect/client/orchestration.py +121 -47
  6. prefect/client/utilities.py +4 -4
  7. prefect/concurrency/asyncio.py +48 -7
  8. prefect/concurrency/context.py +24 -0
  9. prefect/concurrency/services.py +24 -8
  10. prefect/concurrency/sync.py +30 -3
  11. prefect/context.py +85 -24
  12. prefect/events/clients.py +93 -60
  13. prefect/events/utilities.py +0 -2
  14. prefect/events/worker.py +9 -2
  15. prefect/flow_engine.py +6 -3
  16. prefect/flows.py +176 -12
  17. prefect/futures.py +84 -7
  18. prefect/profiles.toml +16 -2
  19. prefect/runner/runner.py +6 -1
  20. prefect/runner/storage.py +4 -0
  21. prefect/settings.py +108 -14
  22. prefect/task_engine.py +901 -285
  23. prefect/task_runs.py +24 -1
  24. prefect/task_worker.py +7 -1
  25. prefect/tasks.py +9 -5
  26. prefect/utilities/asyncutils.py +0 -6
  27. prefect/utilities/callables.py +5 -3
  28. prefect/utilities/engine.py +3 -0
  29. prefect/utilities/importtools.py +138 -58
  30. prefect/utilities/schema_tools/validation.py +30 -0
  31. prefect/utilities/services.py +32 -0
  32. {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/METADATA +39 -39
  33. {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/RECORD +36 -35
  34. {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/WHEEL +1 -1
  35. {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/LICENSE +0 -0
  36. {prefect_client-3.0.0rc13.dist-info → prefect_client-3.0.0rc15.dist-info}/top_level.txt +0 -0
prefect/task_runs.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  import atexit
3
3
  import threading
4
4
  import uuid
5
- from typing import Dict, Optional
5
+ from typing import Callable, Dict, Optional
6
6
 
7
7
  import anyio
8
8
  from cachetools import TTLCache
@@ -74,6 +74,7 @@ class TaskRunWaiter:
74
74
  maxsize=10000, ttl=600
75
75
  )
76
76
  self._completion_events: Dict[uuid.UUID, asyncio.Event] = {}
77
+ self._completion_callbacks: Dict[uuid.UUID, Callable] = {}
77
78
  self._loop: Optional[asyncio.AbstractEventLoop] = None
78
79
  self._observed_completed_task_runs_lock = threading.Lock()
79
80
  self._completion_events_lock = threading.Lock()
@@ -135,6 +136,8 @@ class TaskRunWaiter:
135
136
  # so the waiter can wake up the waiting coroutine
136
137
  if task_run_id in self._completion_events:
137
138
  self._completion_events[task_run_id].set()
139
+ if task_run_id in self._completion_callbacks:
140
+ self._completion_callbacks[task_run_id]()
138
141
  except Exception as exc:
139
142
  self.logger.error(f"Error processing event: {exc}")
140
143
 
@@ -195,6 +198,26 @@ class TaskRunWaiter:
195
198
  # Remove the event from the cache after it has been waited on
196
199
  instance._completion_events.pop(task_run_id, None)
197
200
 
201
+ @classmethod
202
+ def add_done_callback(cls, task_run_id: uuid.UUID, callback):
203
+ """
204
+ Add a callback to be called when a task run finishes.
205
+
206
+ Args:
207
+ task_run_id: The ID of the task run to wait for.
208
+ callback: The callback to call when the task run finishes.
209
+ """
210
+ instance = cls.instance()
211
+ with instance._observed_completed_task_runs_lock:
212
+ if task_run_id in instance._observed_completed_task_runs:
213
+ callback()
214
+ return
215
+
216
+ with instance._completion_events_lock:
217
+ # Cache the event for the task run ID so the consumer can set it
218
+ # when the event is received
219
+ instance._completion_callbacks[task_run_id] = callback
220
+
198
221
  @classmethod
199
222
  def instance(cls):
200
223
  """
prefect/task_worker.py CHANGED
@@ -38,6 +38,7 @@ from prefect.utilities.annotations import NotSet
38
38
  from prefect.utilities.asyncutils import asyncnullcontext, sync_compatible
39
39
  from prefect.utilities.engine import emit_task_run_state_change_event, propose_state
40
40
  from prefect.utilities.processutils import _register_signal
41
+ from prefect.utilities.services import start_client_metrics_server
41
42
  from prefect.utilities.urls import url_for
42
43
 
43
44
  logger = get_logger("task_worker")
@@ -158,6 +159,8 @@ class TaskWorker:
158
159
  """
159
160
  _register_signal(signal.SIGTERM, self.handle_sigterm)
160
161
 
162
+ start_client_metrics_server()
163
+
161
164
  async with asyncnullcontext() if self.started else self:
162
165
  logger.info("Starting task worker...")
163
166
  try:
@@ -290,12 +293,15 @@ class TaskWorker:
290
293
  await self._client._client.delete(f"/task_runs/{task_run.id}")
291
294
  return
292
295
 
296
+ initial_state = task_run.state
297
+
293
298
  if PREFECT_EXPERIMENTAL_ENABLE_CLIENT_SIDE_TASK_ORCHESTRATION:
294
299
  new_state = Pending()
295
300
  new_state.state_details.deferred = True
296
301
  new_state.state_details.task_run_id = task_run.id
297
302
  new_state.state_details.flow_run_id = task_run.flow_run_id
298
303
  state = new_state
304
+ task_run.state = state
299
305
  else:
300
306
  try:
301
307
  new_state = Pending()
@@ -327,7 +333,7 @@ class TaskWorker:
327
333
 
328
334
  emit_task_run_state_change_event(
329
335
  task_run=task_run,
330
- initial_state=task_run.state,
336
+ initial_state=initial_state,
331
337
  validated_state=state,
332
338
  )
333
339
 
prefect/tasks.py CHANGED
@@ -33,9 +33,6 @@ from uuid import UUID, uuid4
33
33
  from typing_extensions import Literal, ParamSpec
34
34
 
35
35
  import prefect.states
36
- from prefect._internal.compatibility.deprecated import (
37
- deprecated_async_method,
38
- )
39
36
  from prefect.cache_policies import DEFAULT, NONE, CachePolicy
40
37
  from prefect.client.orchestration import get_client
41
38
  from prefect.client.schemas import TaskRun
@@ -1038,7 +1035,6 @@ class Task(Generic[P, R]):
1038
1035
  ) -> State[T]:
1039
1036
  ...
1040
1037
 
1041
- @deprecated_async_method
1042
1038
  def submit(
1043
1039
  self,
1044
1040
  *args: Any,
@@ -1203,7 +1199,6 @@ class Task(Generic[P, R]):
1203
1199
  ) -> PrefectFutureList[State[T]]:
1204
1200
  ...
1205
1201
 
1206
- @deprecated_async_method
1207
1202
  def map(
1208
1203
  self,
1209
1204
  *args: Any,
@@ -1455,6 +1450,15 @@ class Task(Generic[P, R]):
1455
1450
  )
1456
1451
  ) # type: ignore
1457
1452
 
1453
+ from prefect.utilities.engine import emit_task_run_state_change_event
1454
+
1455
+ # emit a `SCHEDULED` event for the task run
1456
+ emit_task_run_state_change_event(
1457
+ task_run=task_run,
1458
+ initial_state=None,
1459
+ validated_state=task_run.state,
1460
+ )
1461
+
1458
1462
  if task_run_url := url_for(task_run):
1459
1463
  logger.info(
1460
1464
  f"Created task run {task_run.name!r}. View it in the UI at {task_run_url!r}"
@@ -30,7 +30,6 @@ import anyio.abc
30
30
  import anyio.from_thread
31
31
  import anyio.to_thread
32
32
  import sniffio
33
- import wrapt
34
33
  from typing_extensions import Literal, ParamSpec, TypeGuard
35
34
 
36
35
  from prefect._internal.concurrency.api import _cast_to_call, from_sync
@@ -210,11 +209,6 @@ def run_coro_as_sync(
210
209
  Returns:
211
210
  The result of the coroutine if wait_for_result is True, otherwise None.
212
211
  """
213
- if not asyncio.iscoroutine(coroutine):
214
- if isinstance(coroutine, wrapt.ObjectProxy):
215
- return coroutine.__wrapped__
216
- else:
217
- raise TypeError("`coroutine` must be a coroutine object")
218
212
 
219
213
  async def coroutine_wrapper() -> Union[R, None]:
220
214
  """
@@ -364,17 +364,19 @@ def parameter_schema_from_entrypoint(entrypoint: str) -> ParameterSchema:
364
364
  Returns:
365
365
  ParameterSchema: The parameter schema for the function.
366
366
  """
367
+ filepath = None
367
368
  if ":" in entrypoint:
368
369
  # split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
369
370
  path, func_name = entrypoint.rsplit(":", maxsplit=1)
370
371
  source_code = Path(path).read_text()
372
+ filepath = path
371
373
  else:
372
374
  path, func_name = entrypoint.rsplit(".", maxsplit=1)
373
375
  spec = importlib.util.find_spec(path)
374
376
  if not spec or not spec.origin:
375
377
  raise ValueError(f"Could not find module {path!r}")
376
378
  source_code = Path(spec.origin).read_text()
377
- signature = _generate_signature_from_source(source_code, func_name)
379
+ signature = _generate_signature_from_source(source_code, func_name, filepath)
378
380
  docstring = _get_docstring_from_source(source_code, func_name)
379
381
  return generate_parameter_schema(signature, parameter_docstrings(docstring))
380
382
 
@@ -444,7 +446,7 @@ def raise_for_reserved_arguments(fn: Callable, reserved_arguments: Iterable[str]
444
446
 
445
447
 
446
448
  def _generate_signature_from_source(
447
- source_code: str, func_name: str
449
+ source_code: str, func_name: str, filepath: Optional[str] = None
448
450
  ) -> inspect.Signature:
449
451
  """
450
452
  Extract the signature of a function from its source code.
@@ -460,7 +462,7 @@ def _generate_signature_from_source(
460
462
  """
461
463
  # Load the namespace from the source code. Missing imports and exceptions while
462
464
  # loading local class definitions are ignored.
463
- namespace = safe_load_namespace(source_code)
465
+ namespace = safe_load_namespace(source_code, filepath=filepath)
464
466
  # Parse the source code into an AST
465
467
  parsed_code = ast.parse(source_code)
466
468
 
@@ -783,6 +783,9 @@ def emit_task_run_state_change_event(
783
783
  "state_type",
784
784
  "state_name",
785
785
  "state",
786
+ # server materialized fields
787
+ "estimated_start_time_delta",
788
+ "estimated_run_time",
786
789
  },
787
790
  ),
788
791
  },
@@ -361,79 +361,159 @@ class AliasedModuleLoader(Loader):
361
361
  sys.modules[self.alias] = root_module
362
362
 
363
363
 
364
- def safe_load_namespace(source_code: str):
364
+ def safe_load_namespace(
365
+ source_code: str, filepath: Optional[str] = None
366
+ ) -> Dict[str, Any]:
365
367
  """
366
- Safely load a namespace from source code.
368
+ Safely load a namespace from source code, optionally handling relative imports.
367
369
 
368
- This function will attempt to import all modules and classes defined in the source
369
- code. If an import fails, the error is caught and the import is skipped. This function
370
- will also attempt to compile and evaluate class and function definitions locally.
370
+ If a `filepath` is provided, `sys.path` is modified to support relative imports.
371
+ Changes to `sys.path` are reverted after completion, but this function is not thread safe
372
+ and use of it in threaded contexts may result in undesirable behavior.
371
373
 
372
374
  Args:
373
375
  source_code: The source code to load
376
+ filepath: Optional file path of the source code. If provided, enables relative imports.
374
377
 
375
378
  Returns:
376
- The namespace loaded from the source code. Can be used when evaluating source
377
- code.
379
+ The namespace loaded from the source code.
378
380
  """
379
381
  parsed_code = ast.parse(source_code)
380
382
 
381
- namespace = {"__name__": "prefect_safe_namespace_loader"}
383
+ namespace: Dict[str, Any] = {"__name__": "prefect_safe_namespace_loader"}
382
384
 
383
- # Remove the body of the if __name__ == "__main__": block from the AST to prevent
384
- # execution of guarded code
385
- new_body = []
386
- for node in parsed_code.body:
387
- if _is_main_block(node):
388
- continue
389
- new_body.append(node)
385
+ # Remove the body of the if __name__ == "__main__": block
386
+ new_body = [node for node in parsed_code.body if not _is_main_block(node)]
390
387
  parsed_code.body = new_body
391
388
 
392
- # Walk through the AST and find all import statements
393
- for node in ast.walk(parsed_code):
394
- if isinstance(node, ast.Import):
395
- for alias in node.names:
396
- module_name = alias.name
397
- as_name = alias.asname if alias.asname else module_name
398
- try:
399
- # Attempt to import the module
400
- namespace[as_name] = importlib.import_module(module_name)
401
- logger.debug("Successfully imported %s", module_name)
402
- except ImportError as e:
403
- logger.debug(f"Failed to import {module_name}: {e}")
404
- elif isinstance(node, ast.ImportFrom):
405
- module_name = node.module
406
- if module_name is None:
407
- continue
408
- try:
409
- module = importlib.import_module(module_name)
389
+ temp_module = None
390
+ original_sys_path = None
391
+
392
+ if filepath:
393
+ # Setup for relative imports
394
+ file_dir = os.path.dirname(os.path.abspath(filepath))
395
+ package_name = os.path.basename(file_dir)
396
+ parent_dir = os.path.dirname(file_dir)
397
+
398
+ # Save original sys.path and modify it
399
+ original_sys_path = sys.path.copy()
400
+ sys.path.insert(0, parent_dir)
401
+
402
+ # Create a temporary module for import context
403
+ temp_module = ModuleType(package_name)
404
+ temp_module.__file__ = filepath
405
+ temp_module.__package__ = package_name
406
+
407
+ # Create a spec for the module
408
+ temp_module.__spec__ = ModuleSpec(package_name, None)
409
+ temp_module.__spec__.loader = None
410
+ temp_module.__spec__.submodule_search_locations = [file_dir]
411
+
412
+ try:
413
+ for node in parsed_code.body:
414
+ if isinstance(node, ast.Import):
410
415
  for alias in node.names:
411
- name = alias.name
412
- asname = alias.asname if alias.asname else name
416
+ module_name = alias.name
417
+ as_name = alias.asname or module_name
413
418
  try:
414
- # Get the specific attribute from the module
415
- attribute = getattr(module, name)
416
- namespace[asname] = attribute
417
- except AttributeError as e:
418
- logger.debug(
419
- "Failed to retrieve %s from %s: %s", name, module_name, e
420
- )
421
- except ImportError as e:
422
- logger.debug("Failed to import from %s: %s", node.module, e)
423
-
424
- # Handle local definitions
425
- for node in parsed_code.body:
426
- if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.Assign)):
427
- try:
428
- # Compile and execute each class and function definition and assignment
429
- code = compile(
430
- ast.Module(body=[node], type_ignores=[]),
431
- filename="<ast>",
432
- mode="exec",
433
- )
434
- exec(code, namespace)
435
- except Exception as e:
436
- logger.debug("Failed to compile: %s", e)
419
+ namespace[as_name] = importlib.import_module(module_name)
420
+ logger.debug("Successfully imported %s", module_name)
421
+ except ImportError as e:
422
+ logger.debug(f"Failed to import {module_name}: {e}")
423
+ elif isinstance(node, ast.ImportFrom):
424
+ module_name = node.module or ""
425
+ if filepath:
426
+ try:
427
+ if node.level > 0:
428
+ # For relative imports, use the parent package to inform the import
429
+ package_parts = temp_module.__package__.split(".")
430
+ if len(package_parts) < node.level:
431
+ raise ImportError(
432
+ "Attempted relative import beyond top-level package"
433
+ )
434
+ parent_package = ".".join(
435
+ package_parts[: (1 - node.level)]
436
+ if node.level > 1
437
+ else package_parts
438
+ )
439
+ module = importlib.import_module(
440
+ f".{module_name}" if module_name else "",
441
+ package=parent_package,
442
+ )
443
+ else:
444
+ # Absolute imports are handled as normal
445
+ module = importlib.import_module(module_name)
446
+
447
+ for alias in node.names:
448
+ name = alias.name
449
+ asname = alias.asname or name
450
+ if name == "*":
451
+ # Handle 'from module import *'
452
+ module_dict = {
453
+ k: v
454
+ for k, v in module.__dict__.items()
455
+ if not k.startswith("_")
456
+ }
457
+ namespace.update(module_dict)
458
+ else:
459
+ try:
460
+ attribute = getattr(module, name)
461
+ namespace[asname] = attribute
462
+ except AttributeError as e:
463
+ logger.debug(
464
+ "Failed to retrieve %s from %s: %s",
465
+ name,
466
+ module_name,
467
+ e,
468
+ )
469
+ except ImportError as e:
470
+ logger.debug("Failed to import from %s: %s", module_name, e)
471
+ else:
472
+ # Handle as absolute import when no filepath is provided
473
+ try:
474
+ module = importlib.import_module(module_name)
475
+ for alias in node.names:
476
+ name = alias.name
477
+ asname = alias.asname or name
478
+ if name == "*":
479
+ # Handle 'from module import *'
480
+ module_dict = {
481
+ k: v
482
+ for k, v in module.__dict__.items()
483
+ if not k.startswith("_")
484
+ }
485
+ namespace.update(module_dict)
486
+ else:
487
+ try:
488
+ attribute = getattr(module, name)
489
+ namespace[asname] = attribute
490
+ except AttributeError as e:
491
+ logger.debug(
492
+ "Failed to retrieve %s from %s: %s",
493
+ name,
494
+ module_name,
495
+ e,
496
+ )
497
+ except ImportError as e:
498
+ logger.debug("Failed to import from %s: %s", module_name, e)
499
+ # Handle local definitions
500
+ for node in parsed_code.body:
501
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.Assign)):
502
+ try:
503
+ code = compile(
504
+ ast.Module(body=[node], type_ignores=[]),
505
+ filename="<ast>",
506
+ mode="exec",
507
+ )
508
+ exec(code, namespace)
509
+ except Exception as e:
510
+ logger.debug("Failed to compile: %s", e)
511
+
512
+ finally:
513
+ # Restore original sys.path if it was modified
514
+ if original_sys_path:
515
+ sys.path[:] = original_sys_path
516
+
437
517
  return namespace
438
518
 
439
519
 
@@ -253,5 +253,35 @@ def preprocess_schema(
253
253
  process_properties(
254
254
  definition["properties"], required_fields, allow_none_with_default
255
255
  )
256
+ # Allow block types to be referenced by their id
257
+ if "block_type_slug" in definition:
258
+ schema["definitions"][definition["title"]] = {
259
+ "oneOf": [
260
+ definition,
261
+ {
262
+ "type": "object",
263
+ "properties": {
264
+ "$ref": {
265
+ "oneOf": [
266
+ {
267
+ "type": "string",
268
+ "format": "uuid",
269
+ },
270
+ {
271
+ "type": "object",
272
+ "additionalProperties": {
273
+ "type": "string",
274
+ },
275
+ "minProperties": 1,
276
+ },
277
+ ]
278
+ }
279
+ },
280
+ "required": [
281
+ "$ref",
282
+ ],
283
+ },
284
+ ]
285
+ }
256
286
 
257
287
  return schema
@@ -1,13 +1,16 @@
1
1
  import sys
2
+ import threading
2
3
  from collections import deque
3
4
  from traceback import format_exception
4
5
  from types import TracebackType
5
6
  from typing import Callable, Coroutine, Deque, Optional, Tuple
7
+ from wsgiref.simple_server import WSGIServer
6
8
 
7
9
  import anyio
8
10
  import httpx
9
11
 
10
12
  from prefect.logging.loggers import get_logger
13
+ from prefect.settings import PREFECT_CLIENT_ENABLE_METRICS, PREFECT_CLIENT_METRICS_PORT
11
14
  from prefect.utilities.collections import distinct
12
15
  from prefect.utilities.math import clamped_poisson_interval
13
16
 
@@ -150,3 +153,32 @@ async def critical_service_loop(
150
153
  sleep = interval * 2**backoff_count
151
154
 
152
155
  await anyio.sleep(sleep)
156
+
157
+
158
+ _metrics_server: Optional[Tuple[WSGIServer, threading.Thread]] = None
159
+
160
+
161
+ def start_client_metrics_server():
162
+ """Start the process-wide Prometheus metrics server for client metrics (if enabled
163
+ with `PREFECT_CLIENT_ENABLE_METRICS`) on the port `PREFECT_CLIENT_METRICS_PORT`."""
164
+ if not PREFECT_CLIENT_ENABLE_METRICS:
165
+ return
166
+
167
+ global _metrics_server
168
+ if _metrics_server:
169
+ return
170
+
171
+ from prometheus_client import start_http_server
172
+
173
+ _metrics_server = start_http_server(port=PREFECT_CLIENT_METRICS_PORT.value())
174
+
175
+
176
+ def stop_client_metrics_server():
177
+ """Start the process-wide Prometheus metrics server for client metrics, if it has
178
+ previously been started"""
179
+ global _metrics_server
180
+ if _metrics_server:
181
+ server, thread = _metrics_server
182
+ server.shutdown()
183
+ thread.join()
184
+ _metrics_server = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 3.0.0rc13
3
+ Version: 3.0.0rc15
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
@@ -24,46 +24,46 @@ Classifier: Topic :: Software Development :: Libraries
24
24
  Requires-Python: >=3.9
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: anyio <5.0.0,>=4.4.0
28
- Requires-Dist: asgi-lifespan <3.0,>=1.0
29
- Requires-Dist: cachetools <6.0,>=5.3
30
- Requires-Dist: cloudpickle <4.0,>=2.0
31
- Requires-Dist: coolname <3.0.0,>=1.0.4
32
- Requires-Dist: croniter <4.0.0,>=1.0.12
33
- Requires-Dist: exceptiongroup >=1.0.0
34
- Requires-Dist: fastapi <1.0.0,>=0.111.0
35
- Requires-Dist: fsspec >=2022.5.0
36
- Requires-Dist: graphviz >=0.20.1
37
- Requires-Dist: griffe <0.48.0,>=0.20.0
38
- Requires-Dist: httpcore <2.0.0,>=1.0.5
39
- Requires-Dist: httpx[http2] !=0.23.2,>=0.23
40
- Requires-Dist: importlib-resources <6.2.0,>=6.1.3
41
- Requires-Dist: jsonpatch <2.0,>=1.32
42
- Requires-Dist: jsonschema <5.0.0,>=4.0.0
43
- Requires-Dist: orjson <4.0,>=3.7
44
- Requires-Dist: packaging <24.3,>=21.3
45
- Requires-Dist: pathspec >=0.8.0
46
- Requires-Dist: pendulum <4,>=3.0.0
47
- Requires-Dist: pydantic <3.0.0,>=2.7
48
- Requires-Dist: pydantic-core <3.0.0,>=2.12.0
49
- Requires-Dist: pydantic-extra-types <3.0.0,>=2.8.2
27
+ Requires-Dist: anyio<5.0.0,>=4.4.0
28
+ Requires-Dist: asgi-lifespan<3.0,>=1.0
29
+ Requires-Dist: cachetools<6.0,>=5.3
30
+ Requires-Dist: cloudpickle<4.0,>=2.0
31
+ Requires-Dist: coolname<3.0.0,>=1.0.4
32
+ Requires-Dist: croniter<4.0.0,>=1.0.12
33
+ Requires-Dist: exceptiongroup>=1.0.0
34
+ Requires-Dist: fastapi<1.0.0,>=0.111.0
35
+ Requires-Dist: fsspec>=2022.5.0
36
+ Requires-Dist: graphviz>=0.20.1
37
+ Requires-Dist: griffe<0.48.0,>=0.20.0
38
+ Requires-Dist: httpcore<2.0.0,>=1.0.5
39
+ Requires-Dist: httpx[http2]!=0.23.2,>=0.23
40
+ Requires-Dist: importlib-resources<6.2.0,>=6.1.3
41
+ Requires-Dist: jsonpatch<2.0,>=1.32
42
+ Requires-Dist: jsonschema<5.0.0,>=4.0.0
43
+ Requires-Dist: orjson<4.0,>=3.7
44
+ Requires-Dist: packaging<24.3,>=21.3
45
+ Requires-Dist: pathspec>=0.8.0
46
+ Requires-Dist: pendulum<4,>=3.0.0
47
+ Requires-Dist: prometheus-client>=0.20.0
48
+ Requires-Dist: pydantic<3.0.0,>=2.7
49
+ Requires-Dist: pydantic-core<3.0.0,>=2.12.0
50
+ Requires-Dist: pydantic-extra-types<3.0.0,>=2.8.2
50
51
  Requires-Dist: pydantic-settings
51
- Requires-Dist: python-dateutil <3.0.0,>=2.8.2
52
- Requires-Dist: python-slugify <9.0,>=5.0
53
- Requires-Dist: pyyaml <7.0.0,>=5.4.1
54
- Requires-Dist: rfc3339-validator <0.2.0,>=0.1.4
55
- Requires-Dist: rich <14.0,>=11.0
56
- Requires-Dist: ruamel.yaml >=0.17.0
57
- Requires-Dist: sniffio <2.0.0,>=1.3.0
58
- Requires-Dist: toml >=0.10.0
59
- Requires-Dist: typing-extensions <5.0.0,>=4.5.0
60
- Requires-Dist: ujson <6.0.0,>=5.8.0
61
- Requires-Dist: uvicorn !=0.29.0,>=0.14.0
62
- Requires-Dist: websockets <13.0,>=10.4
63
- Requires-Dist: wrapt >=1.16.0
64
- Requires-Dist: importlib-metadata >=4.4 ; python_version < "3.10"
52
+ Requires-Dist: python-dateutil<3.0.0,>=2.8.2
53
+ Requires-Dist: python-slugify<9.0,>=5.0
54
+ Requires-Dist: pyyaml<7.0.0,>=5.4.1
55
+ Requires-Dist: rfc3339-validator<0.2.0,>=0.1.4
56
+ Requires-Dist: rich<14.0,>=11.0
57
+ Requires-Dist: ruamel.yaml>=0.17.0
58
+ Requires-Dist: sniffio<2.0.0,>=1.3.0
59
+ Requires-Dist: toml>=0.10.0
60
+ Requires-Dist: typing-extensions<5.0.0,>=4.5.0
61
+ Requires-Dist: ujson<6.0.0,>=5.8.0
62
+ Requires-Dist: uvicorn!=0.29.0,>=0.14.0
63
+ Requires-Dist: websockets<13.0,>=10.4
64
+ Requires-Dist: importlib-metadata>=4.4; python_version < "3.10"
65
65
  Provides-Extra: notifications
66
- Requires-Dist: apprise <2.0.0,>=1.1.0 ; extra == 'notifications'
66
+ Requires-Dist: apprise<2.0.0,>=1.1.0; extra == "notifications"
67
67
 
68
68
  <p align="center"><img src="https://github.com/PrefectHQ/prefect/assets/3407835/c654cbc6-63e8-4ada-a92a-efd2f8f24b85" width=1000></p>
69
69