prefect-client 3.1.0__py3-none-any.whl → 3.1.1__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.
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import inspect
3
+ from functools import wraps
4
+ from typing import Any, Callable, Coroutine, Protocol, TypeVar, Union
5
+
6
+ from typing_extensions import ParamSpec
7
+
8
+ R = TypeVar("R")
9
+ P = ParamSpec("P")
10
+
11
+
12
+ class AsyncDispatchable(Protocol[P, R]):
13
+ """Protocol for functions decorated with async_dispatch."""
14
+
15
+ def __call__(
16
+ self, *args: P.args, **kwargs: P.kwargs
17
+ ) -> Union[R, Coroutine[Any, Any, R]]:
18
+ ...
19
+
20
+ aio: Callable[P, Coroutine[Any, Any, R]]
21
+ sync: Callable[P, R]
22
+
23
+
24
+ def is_in_async_context() -> bool:
25
+ """Check if we're in an async context."""
26
+ try:
27
+ # First check if we're in a coroutine
28
+ if asyncio.current_task() is not None:
29
+ return True
30
+
31
+ # Check if we have a loop and it's running
32
+ loop = asyncio.get_event_loop()
33
+ return loop.is_running()
34
+ except RuntimeError:
35
+ return False
36
+
37
+
38
+ def async_dispatch(
39
+ async_impl: Callable[P, Coroutine[Any, Any, R]],
40
+ ) -> Callable[[Callable[P, R]], AsyncDispatchable[P, R]]:
41
+ """
42
+ Decorator that adds async compatibility to a sync function.
43
+
44
+ The decorated function will:
45
+ - Return a coroutine when in an async context (detected via running event loop)
46
+ - Run synchronously when in a sync context
47
+ - Provide .aio for explicit async access
48
+ - Provide .sync for explicit sync access
49
+
50
+ Args:
51
+ async_impl: The async implementation to dispatch to when async execution
52
+ is needed
53
+ """
54
+ if not inspect.iscoroutinefunction(async_impl):
55
+ raise TypeError(
56
+ "async_impl must be an async function to dispatch in async contexts"
57
+ )
58
+
59
+ def decorator(sync_fn: Callable[P, R]) -> AsyncDispatchable[P, R]:
60
+ @wraps(sync_fn)
61
+ def wrapper(
62
+ *args: P.args, **kwargs: P.kwargs
63
+ ) -> Union[R, Coroutine[Any, Any, R]]:
64
+ if is_in_async_context():
65
+ return async_impl(*args, **kwargs)
66
+ return sync_fn(*args, **kwargs)
67
+
68
+ # Attach both async and sync implementations directly
69
+ wrapper.aio = async_impl
70
+ wrapper.sync = sync_fn
71
+ return wrapper # type: ignore
72
+
73
+ return decorator
prefect/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-10-31T12:43:46-0700",
11
+ "date": "2024-11-08T12:38:16-0800",
12
12
  "dirty": true,
13
13
  "error": null,
14
- "full-revisionid": "a83ba39b095e5945140ab6313e39dbc56056afe1",
15
- "version": "3.1.0"
14
+ "full-revisionid": "6b50a2b9f9d4ebf59703c55e1156c6f79151f1c3",
15
+ "version": "3.1.1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -99,6 +99,7 @@ from prefect.client.schemas.objects import (
99
99
  TaskRunResult,
100
100
  Variable,
101
101
  Worker,
102
+ WorkerMetadata,
102
103
  WorkPool,
103
104
  WorkQueue,
104
105
  WorkQueueStatusDetail,
@@ -2596,6 +2597,7 @@ class PrefectClient:
2596
2597
  worker_name: str,
2597
2598
  heartbeat_interval_seconds: Optional[float] = None,
2598
2599
  get_worker_id: bool = False,
2600
+ worker_metadata: Optional[WorkerMetadata] = None,
2599
2601
  ) -> Optional[UUID]:
2600
2602
  """
2601
2603
  Sends a worker heartbeat for a given work pool.
@@ -2604,20 +2606,20 @@ class PrefectClient:
2604
2606
  work_pool_name: The name of the work pool to heartbeat against.
2605
2607
  worker_name: The name of the worker sending the heartbeat.
2606
2608
  return_id: Whether to return the worker ID. Note: will return `None` if the connected server does not support returning worker IDs, even if `return_id` is `True`.
2609
+ worker_metadata: Metadata about the worker to send to the server.
2607
2610
  """
2608
-
2611
+ params = {
2612
+ "name": worker_name,
2613
+ "heartbeat_interval_seconds": heartbeat_interval_seconds,
2614
+ }
2615
+ if worker_metadata:
2616
+ params["worker_metadata"] = worker_metadata.model_dump(mode="json")
2609
2617
  if get_worker_id:
2610
- return_dict = {"return_id": get_worker_id}
2611
- else:
2612
- return_dict = {}
2618
+ params["return_id"] = get_worker_id
2613
2619
 
2614
2620
  resp = await self._client.post(
2615
2621
  f"/work_pools/{work_pool_name}/workers/heartbeat",
2616
- json={
2617
- "name": worker_name,
2618
- "heartbeat_interval_seconds": heartbeat_interval_seconds,
2619
- }
2620
- | return_dict,
2622
+ json=params,
2621
2623
  )
2622
2624
 
2623
2625
  if (
@@ -1689,3 +1689,24 @@ class CsrfToken(ObjectBaseModel):
1689
1689
 
1690
1690
 
1691
1691
  __getattr__ = getattr_migration(__name__)
1692
+
1693
+
1694
+ class Integration(PrefectBaseModel):
1695
+ """A representation of an installed Prefect integration."""
1696
+
1697
+ name: str = Field(description="The name of the Prefect integration.")
1698
+ version: str = Field(description="The version of the Prefect integration.")
1699
+
1700
+
1701
+ class WorkerMetadata(PrefectBaseModel):
1702
+ """
1703
+ Worker metadata.
1704
+
1705
+ We depend on the structure of `integrations`, but otherwise, worker classes
1706
+ should support flexible metadata.
1707
+ """
1708
+
1709
+ integrations: List[Integration] = Field(
1710
+ default=..., description="Prefect integrations installed in the worker."
1711
+ )
1712
+ model_config = ConfigDict(extra="allow")
@@ -32,6 +32,7 @@ from prefect.settings import (
32
32
  PREFECT_LOGGING_TO_API_BATCH_SIZE,
33
33
  PREFECT_LOGGING_TO_API_MAX_LOG_SIZE,
34
34
  PREFECT_LOGGING_TO_API_WHEN_MISSING_FLOW,
35
+ get_current_settings,
35
36
  )
36
37
 
37
38
 
@@ -180,6 +181,7 @@ class APILogHandler(logging.Handler):
180
181
  """
181
182
  flow_run_id = getattr(record, "flow_run_id", None)
182
183
  task_run_id = getattr(record, "task_run_id", None)
184
+ worker_id = getattr(record, "worker_id", None)
183
185
 
184
186
  if not flow_run_id:
185
187
  try:
@@ -215,6 +217,7 @@ class APILogHandler(logging.Handler):
215
217
  log = LogCreate(
216
218
  flow_run_id=flow_run_id if is_uuid_like else None,
217
219
  task_run_id=task_run_id,
220
+ worker_id=worker_id,
218
221
  name=record.name,
219
222
  level=record.levelno,
220
223
  timestamp=pendulum.from_timestamp(
@@ -236,6 +239,44 @@ class APILogHandler(logging.Handler):
236
239
  return len(json.dumps(log).encode())
237
240
 
238
241
 
242
+ class WorkerAPILogHandler(APILogHandler):
243
+ def emit(self, record: logging.LogRecord):
244
+ if get_current_settings().experiments.worker_logging_to_api_enabled:
245
+ super().emit(record)
246
+ else:
247
+ return
248
+
249
+ def prepare(self, record: logging.LogRecord) -> Dict[str, Any]:
250
+ """
251
+ Convert a `logging.LogRecord` to the API `LogCreate` schema and serialize.
252
+
253
+ This will add in the worker id to the log.
254
+
255
+ Logs exceeding the maximum size will be dropped.
256
+ """
257
+
258
+ worker_id = getattr(record, "worker_id", None)
259
+
260
+ log = LogCreate(
261
+ worker_id=worker_id,
262
+ name=record.name,
263
+ level=record.levelno,
264
+ timestamp=pendulum.from_timestamp(
265
+ getattr(record, "created", None) or time.time()
266
+ ),
267
+ message=self.format(record),
268
+ ).model_dump(mode="json")
269
+
270
+ log_size = log["__payload_size__"] = self._get_payload_size(log)
271
+ if log_size > PREFECT_LOGGING_TO_API_MAX_LOG_SIZE.value():
272
+ raise ValueError(
273
+ f"Log of size {log_size} is greater than the max size of "
274
+ f"{PREFECT_LOGGING_TO_API_MAX_LOG_SIZE.value()}"
275
+ )
276
+
277
+ return log
278
+
279
+
239
280
  class PrefectConsoleHandler(logging.StreamHandler):
240
281
  def __init__(
241
282
  self,
@@ -12,6 +12,7 @@ from typing_extensions import Self
12
12
  import prefect
13
13
  from prefect.exceptions import MissingContextError
14
14
  from prefect.logging.filters import ObfuscateApiKeyFilter
15
+ from prefect.telemetry.logging import add_telemetry_log_handler
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from prefect.client.schemas import FlowRun as ClientFlowRun
@@ -19,6 +20,7 @@ if TYPE_CHECKING:
19
20
  from prefect.context import RunContext
20
21
  from prefect.flows import Flow
21
22
  from prefect.tasks import Task
23
+ from prefect.workers.base import BaseWorker
22
24
 
23
25
 
24
26
  class PrefectLogAdapter(logging.LoggerAdapter):
@@ -75,6 +77,8 @@ def get_logger(name: Optional[str] = None) -> logging.Logger:
75
77
  obfuscate_api_key_filter = ObfuscateApiKeyFilter()
76
78
  logger.addFilter(obfuscate_api_key_filter)
77
79
 
80
+ add_telemetry_log_handler(logger=logger)
81
+
78
82
  return logger
79
83
 
80
84
 
@@ -136,6 +140,12 @@ def get_run_logger(
136
140
  else:
137
141
  raise MissingContextError("There is no active flow or task run context.")
138
142
 
143
+ if isinstance(logger, logging.LoggerAdapter):
144
+ assert isinstance(logger.logger, logging.Logger)
145
+ add_telemetry_log_handler(logger.logger)
146
+ else:
147
+ add_telemetry_log_handler(logger)
148
+
139
149
  return logger
140
150
 
141
151
 
@@ -205,6 +215,29 @@ def task_run_logger(
205
215
  )
206
216
 
207
217
 
218
+ def get_worker_logger(worker: "BaseWorker", name: Optional[str] = None):
219
+ """
220
+ Create a worker logger with the worker's metadata attached.
221
+
222
+ If the worker has a backend_id, it will be attached to the log records.
223
+ If the worker does not have a backend_id a basic logger will be returned.
224
+ If the worker does not have a backend_id attribute, a basic logger will be returned.
225
+ """
226
+
227
+ worker_log_name = name or f"workers.{worker.__class__.type}.{worker.name.lower()}"
228
+
229
+ worker_id = getattr(worker, "backend_id", None)
230
+ if worker_id:
231
+ return PrefectLogAdapter(
232
+ get_logger(worker_log_name),
233
+ extra={
234
+ "worker_id": str(worker.backend_id),
235
+ },
236
+ )
237
+ else:
238
+ return get_logger(worker_log_name)
239
+
240
+
208
241
  @contextmanager
209
242
  def disable_logger(name: str):
210
243
  """
@@ -69,6 +69,10 @@ handlers:
69
69
  class: logging.StreamHandler
70
70
  formatter: debug
71
71
 
72
+ worker_api:
73
+ level: 0
74
+ class: prefect.logging.handlers.WorkerAPILogHandler
75
+
72
76
  loggers:
73
77
  prefect:
74
78
  level: "${PREFECT_LOGGING_LEVEL}"
@@ -86,6 +90,10 @@ loggers:
86
90
  level: NOTSET
87
91
  handlers: [api]
88
92
 
93
+ prefect.workers:
94
+ level: NOTSET
95
+ handlers: [worker_api]
96
+
89
97
  prefect.server:
90
98
  level: "${PREFECT_SERVER_LOGGING_LEVEL}"
91
99
 
prefect/main.py CHANGED
@@ -57,6 +57,11 @@ prefect.logging.get_logger("profiles").debug(
57
57
  f"Using profile {prefect.context.get_settings_context().profile.name!r}"
58
58
  )
59
59
 
60
+ # Configure telemetry
61
+ import prefect.telemetry.bootstrap
62
+
63
+ prefect.telemetry.bootstrap.setup_telemetry()
64
+
60
65
 
61
66
  from prefect._internal.compatibility.deprecated import (
62
67
  inject_renamed_module_alias_finder,
@@ -25,7 +25,7 @@ class APISettings(PrefectBaseSettings):
25
25
  )
26
26
  tls_insecure_skip_verify: bool = Field(
27
27
  default=False,
28
- description="If `True`, disables SSL checking to allow insecure requests. This is recommended only during development, e.g. when using self-signed certificates.",
28
+ description="If `True`, disables SSL checking to allow insecure requests. Setting to False is recommended only during development. For example, when using self-signed certificates.",
29
29
  )
30
30
  ssl_cert_file: Optional[str] = Field(
31
31
  default=os.environ.get("SSL_CERT_FILE"),
@@ -22,3 +22,8 @@ class ExperimentsSettings(PrefectBaseSettings):
22
22
  default=False,
23
23
  description="Enables the logging of worker logs to Prefect Cloud.",
24
24
  )
25
+
26
+ telemetry_enabled: bool = Field(
27
+ default=False,
28
+ description="Enables sending telemetry to Prefect Cloud.",
29
+ )
@@ -1,11 +1,18 @@
1
+ from functools import partial
1
2
  from pathlib import Path
2
3
  from typing import Annotated, Literal, Optional, Union
3
4
 
4
- from pydantic import AfterValidator, AliasChoices, AliasPath, Field, model_validator
5
+ from pydantic import (
6
+ AliasChoices,
7
+ AliasPath,
8
+ BeforeValidator,
9
+ Field,
10
+ model_validator,
11
+ )
5
12
  from typing_extensions import Self
6
13
 
7
14
  from prefect.settings.base import PrefectBaseSettings, _build_settings_config
8
- from prefect.types import LogLevel
15
+ from prefect.types import LogLevel, validate_set_T_from_delim_string
9
16
 
10
17
 
11
18
  def max_log_size_smaller_than_batch_size(values):
@@ -97,7 +104,7 @@ class LoggingSettings(PrefectBaseSettings):
97
104
 
98
105
  extra_loggers: Annotated[
99
106
  Union[str, list[str], None],
100
- AfterValidator(lambda v: [n.strip() for n in v.split(",")] if v else []),
107
+ BeforeValidator(partial(validate_set_T_from_delim_string, type_=str)),
101
108
  ] = Field(
102
109
  default=None,
103
110
  description="Additional loggers to attach to Prefect logging at runtime.",
@@ -72,7 +72,7 @@ class Settings(PrefectBaseSettings):
72
72
 
73
73
  client: ClientSettings = Field(
74
74
  default_factory=ClientSettings,
75
- description="Settings for for controlling API client behavior",
75
+ description="Settings for controlling API client behavior",
76
76
  )
77
77
 
78
78
  cloud: CloudSettings = Field(
@@ -128,7 +128,7 @@ class ServerEventsSettings(PrefectBaseSettings):
128
128
 
129
129
  messaging_cache: str = Field(
130
130
  default="prefect.server.utilities.messaging.memory",
131
- description="Which cache implementation to use for the events system. Should point to a module that exports a Cache class.",
131
+ description="Which cache implementation to use for the events system. Should point to a module that exports a Cache class.",
132
132
  validation_alias=AliasChoices(
133
133
  AliasPath("messaging_cache"),
134
134
  "prefect_server_events_messaging_cache",
@@ -4,6 +4,7 @@ import warnings
4
4
  from pathlib import Path
5
5
  from typing import Any, Dict, List, Optional, Tuple, Type
6
6
 
7
+ import dotenv
7
8
  import toml
8
9
  from pydantic import AliasChoices
9
10
  from pydantic.fields import FieldInfo
@@ -15,6 +16,7 @@ from pydantic_settings import (
15
16
  from pydantic_settings.sources import ConfigFileSourceMixin
16
17
 
17
18
  from prefect.settings.constants import DEFAULT_PREFECT_HOME, DEFAULT_PROFILES_PATH
19
+ from prefect.utilities.collections import get_from_dict
18
20
 
19
21
 
20
22
  class EnvFilterSettingsSource(EnvSettingsSource):
@@ -230,6 +232,25 @@ def _get_profiles_path() -> Path:
230
232
  return DEFAULT_PROFILES_PATH
231
233
  if env_path := os.getenv("PREFECT_PROFILES_PATH"):
232
234
  return Path(env_path)
235
+ if dotenv_path := dotenv.dotenv_values(".env").get("PREFECT_PROFILES_PATH"):
236
+ return Path(dotenv_path)
237
+ if toml_path := _get_profiles_path_from_toml("prefect.toml", ["profiles_path"]):
238
+ return Path(toml_path)
239
+ if pyproject_path := _get_profiles_path_from_toml(
240
+ "pyproject.toml", ["tool", "prefect", "profiles_path"]
241
+ ):
242
+ return Path(pyproject_path)
233
243
  if not (DEFAULT_PREFECT_HOME / "profiles.toml").exists():
234
244
  return DEFAULT_PROFILES_PATH
235
245
  return DEFAULT_PREFECT_HOME / "profiles.toml"
246
+
247
+
248
+ def _get_profiles_path_from_toml(path: str, keys: List[str]) -> Optional[str]:
249
+ """Helper to get the profiles path from a toml file."""
250
+
251
+ try:
252
+ toml_data = toml.load(path)
253
+ except FileNotFoundError:
254
+ return None
255
+
256
+ return get_from_dict(toml_data, keys)
File without changes
@@ -0,0 +1,32 @@
1
+ from typing import TYPE_CHECKING, Union
2
+
3
+ import prefect.settings
4
+ from prefect.client.base import ServerType, determine_server_type
5
+
6
+ if TYPE_CHECKING:
7
+ from opentelemetry.sdk._logs import LoggerProvider
8
+ from opentelemetry.sdk.metrics import MeterProvider
9
+ from opentelemetry.sdk.trace import TracerProvider
10
+
11
+
12
+ def setup_telemetry() -> (
13
+ Union[
14
+ tuple["TracerProvider", "MeterProvider", "LoggerProvider"],
15
+ tuple[None, None, None],
16
+ ]
17
+ ):
18
+ settings = prefect.settings.get_current_settings()
19
+ if not settings.experiments.telemetry_enabled:
20
+ return None, None, None
21
+
22
+ server_type = determine_server_type()
23
+ if server_type != ServerType.CLOUD:
24
+ return None, None, None
25
+
26
+ assert settings.api.key
27
+ assert settings.api.url
28
+
29
+ # This import is here to defer importing of the `opentelemetry` packages.
30
+ from .instrumentation import setup_exporters
31
+
32
+ return setup_exporters(settings.api.url, settings.api.key.get_secret_value())
@@ -0,0 +1,125 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from typing import TYPE_CHECKING
5
+ from urllib.parse import urljoin
6
+ from uuid import UUID
7
+
8
+ from opentelemetry import metrics, trace
9
+ from opentelemetry._logs import set_logger_provider
10
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
11
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
12
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
13
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
14
+ from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor
15
+ from opentelemetry.sdk.metrics import MeterProvider
16
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
17
+ from opentelemetry.sdk.resources import Resource
18
+ from opentelemetry.sdk.trace import TracerProvider
19
+
20
+ from .logging import set_log_handler
21
+ from .processors import InFlightSpanProcessor
22
+
23
+ if TYPE_CHECKING:
24
+ from opentelemetry.sdk._logs import LoggerProvider
25
+
26
+ UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
27
+
28
+ ACCOUNTS_PREFIX = "accounts/"
29
+ ACCOUNT_ID_REGEX = f"{ACCOUNTS_PREFIX}{UUID_REGEX}"
30
+
31
+ WORKSPACES_PREFIX = "workspaces/"
32
+ WORKSPACE_ID_REGEX = f"{WORKSPACES_PREFIX}{UUID_REGEX}"
33
+
34
+
35
+ def extract_account_and_workspace_id(url: str) -> tuple[UUID, UUID]:
36
+ account_id, workspace_id = None, None
37
+
38
+ if res := re.search(ACCOUNT_ID_REGEX, url):
39
+ account_id = UUID(res.group().removeprefix(ACCOUNTS_PREFIX))
40
+
41
+ if res := re.search(WORKSPACE_ID_REGEX, url):
42
+ workspace_id = UUID(res.group().removeprefix(WORKSPACES_PREFIX))
43
+
44
+ if account_id and workspace_id:
45
+ return account_id, workspace_id
46
+
47
+ raise ValueError(
48
+ f"Could not extract account and workspace id from API url: {url!r}"
49
+ )
50
+
51
+
52
+ def _url_join(base_url: str, path: str) -> str:
53
+ return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
54
+
55
+
56
+ def setup_exporters(
57
+ api_url: str, api_key: str
58
+ ) -> tuple[TracerProvider, MeterProvider, "LoggerProvider"]:
59
+ account_id, workspace_id = extract_account_and_workspace_id(api_url)
60
+ telemetry_url = _url_join(api_url, "telemetry/")
61
+
62
+ headers = {
63
+ "Authorization": f"Bearer {api_key}",
64
+ }
65
+
66
+ resource = Resource.create(
67
+ {
68
+ "service.name": "prefect",
69
+ "service.instance.id": os.uname().nodename,
70
+ "prefect.account": str(account_id),
71
+ "prefect.workspace": str(workspace_id),
72
+ }
73
+ )
74
+
75
+ trace_provider = _setup_trace_provider(resource, headers, telemetry_url)
76
+ meter_provider = _setup_meter_provider(resource, headers, telemetry_url)
77
+ logger_provider = _setup_logger_provider(resource, headers, telemetry_url)
78
+
79
+ return trace_provider, meter_provider, logger_provider
80
+
81
+
82
+ def _setup_trace_provider(
83
+ resource: Resource, headers: dict[str, str], telemetry_url: str
84
+ ) -> TracerProvider:
85
+ trace_provider = TracerProvider(resource=resource)
86
+ otlp_span_exporter = OTLPSpanExporter(
87
+ endpoint=_url_join(telemetry_url, "v1/traces"),
88
+ headers=headers,
89
+ )
90
+ trace_provider.add_span_processor(InFlightSpanProcessor(otlp_span_exporter))
91
+ trace.set_tracer_provider(trace_provider)
92
+
93
+ return trace_provider
94
+
95
+
96
+ def _setup_meter_provider(
97
+ resource: Resource, headers: dict[str, str], telemetry_url: str
98
+ ) -> MeterProvider:
99
+ metric_reader = PeriodicExportingMetricReader(
100
+ OTLPMetricExporter(
101
+ endpoint=_url_join(telemetry_url, "v1/metrics"),
102
+ headers=headers,
103
+ )
104
+ )
105
+ meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
106
+ metrics.set_meter_provider(meter_provider)
107
+
108
+ return meter_provider
109
+
110
+
111
+ def _setup_logger_provider(
112
+ resource: Resource, headers: dict[str, str], telemetry_url: str
113
+ ) -> LoggerProvider:
114
+ logger_provider = LoggerProvider(resource=resource)
115
+ otlp_exporter = OTLPLogExporter(
116
+ endpoint=_url_join(telemetry_url, "v1/logs"),
117
+ headers=headers,
118
+ )
119
+ logger_provider.add_log_record_processor(SimpleLogRecordProcessor(otlp_exporter))
120
+ set_logger_provider(logger_provider)
121
+ log_handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
122
+
123
+ set_log_handler(log_handler)
124
+
125
+ return logger_provider
@@ -0,0 +1,26 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from opentelemetry.sdk._logs import LoggingHandler
6
+
7
+ _log_handler: Optional["LoggingHandler"] = None
8
+
9
+
10
+ def set_log_handler(log_handler: Optional["LoggingHandler"]) -> None:
11
+ """Set the OTLP log handler."""
12
+ global _log_handler
13
+ _log_handler = log_handler
14
+
15
+
16
+ def get_log_handler() -> Optional["LoggingHandler"]:
17
+ """Get the OTLP log handler."""
18
+ global _log_handler
19
+ return _log_handler
20
+
21
+
22
+ def add_telemetry_log_handler(logger: logging.Logger) -> None:
23
+ """Add the OTLP log handler to the given logger if the log handler has
24
+ been configured."""
25
+ if log_handler := get_log_handler():
26
+ logger.addHandler(log_handler)
@@ -0,0 +1,57 @@
1
+ import time
2
+ from threading import Event, Lock, Thread
3
+ from typing import Optional
4
+
5
+ from opentelemetry.context import Context
6
+ from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
7
+ from opentelemetry.sdk.trace.export import SpanExporter
8
+
9
+
10
+ class InFlightSpanProcessor(SpanProcessor):
11
+ def __init__(self, span_exporter: SpanExporter):
12
+ self.span_exporter = span_exporter
13
+ self._in_flight = {}
14
+ self._lock = Lock()
15
+ self._stop_event = Event()
16
+ self._export_thread = Thread(target=self._export_periodically, daemon=True)
17
+ self._export_thread.start()
18
+
19
+ def _export_periodically(self) -> None:
20
+ while not self._stop_event.is_set():
21
+ time.sleep(1)
22
+ with self._lock:
23
+ to_export = [
24
+ self._readable_span(span) for span in self._in_flight.values()
25
+ ]
26
+ if to_export:
27
+ self.span_exporter.export(to_export)
28
+
29
+ def _readable_span(self, span: Span) -> ReadableSpan:
30
+ readable = span._readable_span()
31
+ readable._end_time = time.time_ns()
32
+ readable._attributes = {
33
+ **(readable._attributes or {}),
34
+ "prefect.in-flight": True,
35
+ }
36
+ return readable
37
+
38
+ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
39
+ if not span.context or not span.context.trace_flags.sampled:
40
+ return
41
+ with self._lock:
42
+ self._in_flight[span.context.span_id] = span
43
+
44
+ def on_end(self, span: ReadableSpan) -> None:
45
+ if not span.context or not span.context.trace_flags.sampled:
46
+ return
47
+ with self._lock:
48
+ del self._in_flight[span.context.span_id]
49
+ self.span_exporter.export((span,))
50
+
51
+ def shutdown(self) -> None:
52
+ self._stop_event.set()
53
+ self._export_thread.join()
54
+ self.span_exporter.shutdown()
55
+
56
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
57
+ return True
prefect/types/__init__.py CHANGED
@@ -7,7 +7,6 @@ import pydantic
7
7
  from pydantic import (
8
8
  BeforeValidator,
9
9
  Field,
10
- SecretStr,
11
10
  StrictBool,
12
11
  StrictFloat,
13
12
  StrictInt,
@@ -110,7 +109,7 @@ def validate_set_T_from_delim_string(
110
109
  T_adapter = TypeAdapter(type_)
111
110
  delim = delim or ","
112
111
  if isinstance(value, str):
113
- return {T_adapter.validate_strings(s) for s in value.split(delim)}
112
+ return {T_adapter.validate_strings(s.strip()) for s in value.split(delim)}
114
113
  errors = []
115
114
  try:
116
115
  return {T_adapter.validate_python(value)}
prefect/workers/base.py CHANGED
@@ -8,7 +8,9 @@ from uuid import UUID, uuid4
8
8
 
9
9
  import anyio
10
10
  import anyio.abc
11
+ import httpx
11
12
  import pendulum
13
+ from importlib_metadata import distributions
12
14
  from pydantic import BaseModel, Field, PrivateAttr, field_validator
13
15
  from pydantic.json_schema import GenerateJsonSchema
14
16
  from typing_extensions import Literal
@@ -18,7 +20,12 @@ from prefect._internal.schemas.validators import return_v_or_none
18
20
  from prefect.client.base import ServerType
19
21
  from prefect.client.orchestration import PrefectClient, get_client
20
22
  from prefect.client.schemas.actions import WorkPoolCreate, WorkPoolUpdate
21
- from prefect.client.schemas.objects import StateType, WorkPool
23
+ from prefect.client.schemas.objects import (
24
+ Integration,
25
+ StateType,
26
+ WorkerMetadata,
27
+ WorkPool,
28
+ )
22
29
  from prefect.client.utilities import inject_client
23
30
  from prefect.events import Event, RelatedResource, emit_event
24
31
  from prefect.events.related import object_as_related_resource, tags_as_related_resources
@@ -26,7 +33,11 @@ from prefect.exceptions import (
26
33
  Abort,
27
34
  ObjectNotFound,
28
35
  )
29
- from prefect.logging.loggers import PrefectLogAdapter, flow_run_logger, get_logger
36
+ from prefect.logging.loggers import (
37
+ PrefectLogAdapter,
38
+ flow_run_logger,
39
+ get_worker_logger,
40
+ )
30
41
  from prefect.plugins import load_prefect_collections
31
42
  from prefect.settings import (
32
43
  PREFECT_API_URL,
@@ -50,6 +61,7 @@ from prefect.utilities.templating import (
50
61
  resolve_block_document_references,
51
62
  resolve_variables,
52
63
  )
64
+ from prefect.utilities.urls import url_for
53
65
 
54
66
  if TYPE_CHECKING:
55
67
  from prefect.client.schemas.objects import Flow, FlowRun
@@ -407,7 +419,8 @@ class BaseWorker(abc.ABC):
407
419
  raise ValueError("Worker name cannot contain '/' or '%'")
408
420
  self.name = name or f"{self.__class__.__name__} {uuid4()}"
409
421
  self._started_event: Optional[Event] = None
410
- self._logger = get_logger(f"worker.{self.__class__.type}.{self.name.lower()}")
422
+ self.backend_id: Optional[UUID] = None
423
+ self._logger = get_worker_logger(self)
411
424
 
412
425
  self.is_setup = False
413
426
  self._create_pool_if_not_found = create_pool_if_not_found
@@ -422,7 +435,6 @@ class BaseWorker(abc.ABC):
422
435
  heartbeat_interval_seconds or PREFECT_WORKER_HEARTBEAT_SECONDS.value()
423
436
  )
424
437
 
425
- self.backend_id: Optional[UUID] = None
426
438
  self._work_pool: Optional[WorkPool] = None
427
439
  self._exit_stack: AsyncExitStack = AsyncExitStack()
428
440
  self._runs_task_group: Optional[anyio.abc.TaskGroup] = None
@@ -433,6 +445,7 @@ class BaseWorker(abc.ABC):
433
445
  self._submitting_flow_run_ids = set()
434
446
  self._cancelling_flow_run_ids = set()
435
447
  self._scheduled_task_scopes = set()
448
+ self._worker_metadata_sent = False
436
449
 
437
450
  @classmethod
438
451
  def get_documentation_url(cls) -> str:
@@ -490,15 +503,19 @@ class BaseWorker(abc.ABC):
490
503
  return slugify(self.name)
491
504
 
492
505
  def get_flow_run_logger(self, flow_run: "FlowRun") -> PrefectLogAdapter:
506
+ extra = {
507
+ "worker_name": self.name,
508
+ "work_pool_name": (
509
+ self._work_pool_name if self._work_pool else "<unknown>"
510
+ ),
511
+ "work_pool_id": str(getattr(self._work_pool, "id", "unknown")),
512
+ }
513
+ if self.backend_id:
514
+ extra["worker_id"] = str(self.backend_id)
515
+
493
516
  return flow_run_logger(flow_run=flow_run).getChild(
494
517
  "worker",
495
- extra={
496
- "worker_name": self.name,
497
- "work_pool_name": (
498
- self._work_pool_name if self._work_pool else "<unknown>"
499
- ),
500
- "work_pool_id": str(getattr(self._work_pool, "id", "unknown")),
501
- },
518
+ extra=extra,
502
519
  )
503
520
 
504
521
  async def start(
@@ -712,54 +729,97 @@ class BaseWorker(abc.ABC):
712
729
 
713
730
  self._work_pool = work_pool
714
731
 
715
- async def _send_worker_heartbeat(
716
- self, get_worker_id: bool = False
717
- ) -> Optional[UUID]:
732
+ async def _worker_metadata(self) -> Optional[WorkerMetadata]:
718
733
  """
719
- Sends a heartbeat to the API.
720
-
721
- If `get_worker_id` is True, the worker ID will be retrieved from the API.
734
+ Returns metadata about installed Prefect collections for the worker.
722
735
  """
723
- if self._work_pool:
724
- return await self._client.send_worker_heartbeat(
725
- work_pool_name=self._work_pool_name,
726
- worker_name=self.name,
727
- heartbeat_interval_seconds=self.heartbeat_interval_seconds,
728
- get_worker_id=get_worker_id,
729
- )
736
+ installed_integrations = load_prefect_collections().keys()
730
737
 
731
- async def sync_with_backend(self):
738
+ integration_versions = [
739
+ Integration(name=dist.metadata["Name"], version=dist.version)
740
+ for dist in distributions()
741
+ # PyPI packages often use dashes, but Python package names use underscores
742
+ # because they must be valid identifiers.
743
+ if dist.metadata.get("Name").replace("-", "_") in installed_integrations
744
+ ]
745
+
746
+ if integration_versions:
747
+ return WorkerMetadata(integrations=integration_versions)
748
+ return None
749
+
750
+ async def _send_worker_heartbeat(self) -> Optional[UUID]:
732
751
  """
733
- Updates the worker's local information about it's current work pool and
734
- queues. Sends a worker heartbeat to the API.
752
+ Sends a heartbeat to the API.
735
753
  """
736
- await self._update_local_work_pool_info()
737
- # Only do this logic if we've enabled the experiment, are connected to cloud and we don't have an ID.
754
+ if not self._client:
755
+ self._logger.warning("Client has not been initialized; skipping heartbeat.")
756
+ return None
757
+ if not self._work_pool:
758
+ self._logger.debug("Worker has no work pool; skipping heartbeat.")
759
+ return None
760
+
761
+ should_get_worker_id = self._should_get_worker_id()
762
+
763
+ params = {
764
+ "work_pool_name": self._work_pool_name,
765
+ "worker_name": self.name,
766
+ "heartbeat_interval_seconds": self.heartbeat_interval_seconds,
767
+ "get_worker_id": should_get_worker_id,
768
+ }
738
769
  if (
739
- get_current_settings().experiments.worker_logging_to_api_enabled
740
- and (
741
- self._client.server_type == ServerType.CLOUD
742
- or get_current_settings().testing.test_mode
743
- )
744
- and self.backend_id is None
770
+ self._client.server_type == ServerType.CLOUD
771
+ and not self._worker_metadata_sent
745
772
  ):
746
- get_worker_id = True
747
- else:
748
- get_worker_id = False
773
+ worker_metadata = await self._worker_metadata()
774
+ if worker_metadata:
775
+ params["worker_metadata"] = worker_metadata
776
+ self._worker_metadata_sent = True
749
777
 
750
- remote_id = await self._send_worker_heartbeat(get_worker_id=get_worker_id)
778
+ worker_id = None
779
+ try:
780
+ worker_id = await self._client.send_worker_heartbeat(**params)
781
+ except httpx.HTTPStatusError as e:
782
+ if e.response.status_code == 422 and should_get_worker_id:
783
+ self._logger.warning(
784
+ "Failed to retrieve worker ID from the Prefect API server."
785
+ )
786
+ params["get_worker_id"] = False
787
+ worker_id = await self._client.send_worker_heartbeat(**params)
788
+ else:
789
+ raise e
751
790
 
752
- if get_worker_id and remote_id is None:
791
+ if should_get_worker_id and worker_id is None:
753
792
  self._logger.warning(
754
793
  "Failed to retrieve worker ID from the Prefect API server."
755
794
  )
756
- else:
795
+
796
+ return worker_id
797
+
798
+ async def sync_with_backend(self):
799
+ """
800
+ Updates the worker's local information about it's current work pool and
801
+ queues. Sends a worker heartbeat to the API.
802
+ """
803
+ await self._update_local_work_pool_info()
804
+
805
+ remote_id = await self._send_worker_heartbeat()
806
+ if remote_id:
757
807
  self.backend_id = remote_id
808
+ self._logger = get_worker_logger(self)
758
809
 
759
810
  self._logger.debug(
760
811
  f"Worker synchronized with the Prefect API server. Remote ID: {self.backend_id}"
761
812
  )
762
813
 
814
+ def _should_get_worker_id(self):
815
+ """Determines if the worker should request an ID from the API server."""
816
+ return (
817
+ get_current_settings().experiments.worker_logging_to_api_enabled
818
+ and self._client
819
+ and self._client.server_type == ServerType.CLOUD
820
+ and self.backend_id is None
821
+ )
822
+
763
823
  async def _get_scheduled_flow_runs(
764
824
  self,
765
825
  ) -> List["WorkerFlowRunResponse"]:
@@ -813,6 +873,17 @@ class BaseWorker(abc.ABC):
813
873
  run_logger.info(
814
874
  f"Worker '{self.name}' submitting flow run '{flow_run.id}'"
815
875
  )
876
+ if (
877
+ get_current_settings().experiments.worker_logging_to_api_enabled
878
+ and self.backend_id
879
+ ):
880
+ worker_path = f"worker/{self.backend_id}"
881
+ base_url = url_for("work-pool", self._work_pool.id)
882
+
883
+ run_logger.info(
884
+ f"Running on worker id: {self.backend_id}. See worker logs here: {base_url}/{worker_path}"
885
+ )
886
+
816
887
  self._submitting_flow_run_ids.add(flow_run.id)
817
888
  self._runs_task_group.start_soon(
818
889
  self._submit_run,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
@@ -1,6 +1,6 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/__init__.py,sha256=UZPTpdap8ECK1zLoggfeOtZGkKcf6um1-lMb-nn4o5I,3450
3
- prefect/_version.py,sha256=Hzj0kOJRJpRTvV4Al2Mi2t5EvQK_0Q2ixDr6TVvMbNM,496
3
+ prefect/_version.py,sha256=l2zyL-F66iaGpmwpxB6wRww__gYWl19cPDqMKnOCZ2w,496
4
4
  prefect/agent.py,sha256=BOVVY5z-vUIQ2u8LwMTXDaNys2fjOZSS5YGDwJmTQjI,230
5
5
  prefect/artifacts.py,sha256=dsxFWmdg2r9zbHM3KgKOR5YbJ29_dXUYF9kipJpbxkE,13009
6
6
  prefect/automations.py,sha256=NlQ62GPJzy-gnWQqX7c6CQJKw7p60WLGDAFcy82vtg4,5613
@@ -13,7 +13,7 @@ prefect/flow_engine.py,sha256=BuxfnKDqYcJvmVfPlAv8yMSwkDM25SBeJVk_MGQnm8k,30076
13
13
  prefect/flow_runs.py,sha256=EaXRIQTOnwnA0fO7_EjwafFRmS57K_CRy0Xsz3JDIhc,16070
14
14
  prefect/flows.py,sha256=PUEPpQx5pjXMVfnrPbIeAvWUdEdeaNPO4DXj7tgxGcA,90185
15
15
  prefect/futures.py,sha256=DlZvdccKtwQKuDUFrZ4zcINeO9C1chLiNOwjE5gTgCk,16356
16
- prefect/main.py,sha256=IdtnJR5-IwP8EZsfhMFKj92ylMhNyau9X_eMcTP2ZjM,2336
16
+ prefect/main.py,sha256=TPktwZJ8a6l9U5A01a6x6wEZ7DqD4z7OWz0WadAPS6I,2441
17
17
  prefect/plugins.py,sha256=HY7Z7OJlltqzsUiPMEL1Y_hQbHw0CeZKayWiK-k8DP4,2435
18
18
  prefect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  prefect/results.py,sha256=D8pjiiK0btWuTSQ4txw4glB4VFxvdrqef6z51-JgU5c,47670
@@ -32,6 +32,7 @@ prefect/_internal/integrations.py,sha256=U4cZMDbnilzZSKaMxvzZcSL27a1tzRMjDoTfr2u
32
32
  prefect/_internal/pytz.py,sha256=WWl9x16rKFWequGmcOGs_ljpCDPf2LDHMyZp_4D8e6c,13748
33
33
  prefect/_internal/retries.py,sha256=xtgj6oPSvYQLbyk451LR6swcRQvRVWEzCxY6GMK7qA4,2284
34
34
  prefect/_internal/compatibility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ prefect/_internal/compatibility/async_dispatch.py,sha256=QpfHgjdmLW4PXp-sBXIqUlGL0bJnXhtLBGXi7PWR8Bo,2174
35
36
  prefect/_internal/compatibility/deprecated.py,sha256=PVME2C3Oe4_8tKIGufx1W4EpGkz5IQY8gFohPVOjNcM,7533
36
37
  prefect/_internal/compatibility/migration.py,sha256=oifIj6mKwVUfrUO13ODLKOCwLON4YqaTXzhBFh8AK6E,6797
37
38
  prefect/_internal/concurrency/__init__.py,sha256=YlTwU9ryjPNwbJa45adLJY00t_DGCh1QrdtY9WdVFfw,2140
@@ -69,13 +70,13 @@ prefect/client/base.py,sha256=2K8UiWzorZNNM4c8c-OiGeZ5i5ViUfZ_Q31oPobbOO0,24956
69
70
  prefect/client/cloud.py,sha256=P8GctOSRqrukU_j9iXDuOW9lIiscRFP6WLxASDcmn_o,6207
70
71
  prefect/client/collections.py,sha256=u-96saqu0RALAazRI0YaZCJahnuafMppY21KN6ggx80,1059
71
72
  prefect/client/constants.py,sha256=Z_GG8KF70vbbXxpJuqW5pLnwzujTVeHbcYYRikNmGH0,29
72
- prefect/client/orchestration.py,sha256=iyZRzoqwQSzxjT4uvP9O9O-mH4o1cmtTvgG4MCFCH2g,151297
73
+ prefect/client/orchestration.py,sha256=p3vblcSgrle4p-_Rl99GDz1sNzH-2pltxYUZHbFwUeM,151496
73
74
  prefect/client/subscriptions.py,sha256=oqF2MJsgN3psJg-MePfvwMtEWjromfP9StWF00xc1eg,3403
74
75
  prefect/client/utilities.py,sha256=89fmza0cRMOayxgXRdO51TKb11TczJ0ByOZmcZVrt44,3286
75
76
  prefect/client/schemas/__init__.py,sha256=KlyqFV-hMulMkNstBn_0ijoHoIwJZaBj6B1r07UmgvE,607
76
77
  prefect/client/schemas/actions.py,sha256=m2HJr9oAoK9vqigCHSwMGwvKPTjGNEtP0xdXgforIT0,28629
77
78
  prefect/client/schemas/filters.py,sha256=ynzD3mdvHHkI51pJ_NWgeDv8Awr7-2QtpUq7fTInEYM,36316
78
- prefect/client/schemas/objects.py,sha256=A57O9l8KlcwIY1iEAqVM-G-SA00eHB5DqKdpibijz5w,55995
79
+ prefect/client/schemas/objects.py,sha256=H3yeFyyV3LGRe6IA8Yj-1RxpVgSfqVhodOQ6YhuIcXI,56629
79
80
  prefect/client/schemas/responses.py,sha256=tV06W8npA8oCjV9d0ZNvjro4QcbHxayb8PC4LmanXjo,15467
80
81
  prefect/client/schemas/schedules.py,sha256=8rpqjOYtknu2-1n5_WD4cOplgu93P3mCyX86B22LfL4,13070
81
82
  prefect/client/schemas/sorting.py,sha256=L-2Mx-igZPtsUoRUguTcG3nIEstMEMPD97NwPM2Ox5s,2579
@@ -137,10 +138,10 @@ prefect/logging/__init__.py,sha256=zx9f5_dWrR4DbcTOFBpNGOPoCZ1QcPFudr7zxb2XRpA,1
137
138
  prefect/logging/configuration.py,sha256=5kOYdd4Hi6t-wJhxEncYS9bUAMEMTRKy_i22pepjDrw,3343
138
139
  prefect/logging/filters.py,sha256=9keHLN4-cpnsWcii1qU0RITNi9-m7pOhkJ_t0MtCM4k,1117
139
140
  prefect/logging/formatters.py,sha256=3nBWgawvD48slT0zgkKeus1gIyf0OjmDKdLwMEe5mPU,3923
140
- prefect/logging/handlers.py,sha256=EvXFVM9AnSZSk0QA75KnQ0Pso95QQvs3x9rMC5PA46U,10417
141
+ prefect/logging/handlers.py,sha256=86uVPKimc6sBqpig6-nMSSbgXFkftvlKiRgZ7EtMqsw,11776
141
142
  prefect/logging/highlighters.py,sha256=pH6TZmMaMdAkMLJB-U9NPWZIj3GUfczYCSFzCbBCq2s,1776
142
- prefect/logging/loggers.py,sha256=9fN5iTXQwBAHwKfE9iBo-90f2SyGs9Z3OKmkLPG91VY,10995
143
- prefect/logging/logging.yml,sha256=IRxiQ_D0aWHfRBpVfSxKgbPCq9Sw4bfJJZExl8ZyssM,3023
143
+ prefect/logging/loggers.py,sha256=BKYjyP8N2aJPdHwpqNU5Od4uu0pKztsDQ7RPRNb4oGo,12146
144
+ prefect/logging/logging.yml,sha256=-HoWPYLsKMt5bKRgjIqk8S7tQlAaQ93pGkwIkYtMVMo,3172
144
145
  prefect/records/__init__.py,sha256=rJJhaJBa0AWu63fJhtB-yHBi64qL6p4svo7F0qvm2sc,30
145
146
  prefect/records/base.py,sha256=Ne-7pRGNfmk0a_Vm3t5zRrj26KgGr_L2_XfLID0XzIY,8035
146
147
  prefect/records/filesystem.py,sha256=X-h7r5deiHH5IaaDk4ugOCmR5ZKnJeU2cLgp0AkMt0E,7316
@@ -165,19 +166,19 @@ prefect/settings/context.py,sha256=yKxnaDJHX8e2jmAVtw1RF9o7X4V3AOcz61sVeQyPX2c,2
165
166
  prefect/settings/legacy.py,sha256=GcaB1mkahc4HbjnUvr4_3qNNxleke08dYnOXCUt8w3k,5617
166
167
  prefect/settings/profiles.py,sha256=VZdzOV-KSuAkCxtdhBmSG9i84-K2QLSx6g2-vIUkfig,12169
167
168
  prefect/settings/profiles.toml,sha256=kTvqDNMzjH3fsm5OEI-NKY4dMmipor5EvQXRB6rPEjY,522
168
- prefect/settings/sources.py,sha256=CfgJlHStFSP0Kiq_bFPY0BmXqNzaivrId-3lYZ1wuJI,8604
169
+ prefect/settings/sources.py,sha256=ixt4dTP766s2nr_IjPKS81ZneAaiWzw93zjrWCnkKp8,9345
169
170
  prefect/settings/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
- prefect/settings/models/api.py,sha256=1MQhokm0L64ZAjG3LbgArtY_uiJX_hK6SKb1dC9aCHU,1491
171
+ prefect/settings/models/api.py,sha256=DuoBHYeMX9MlgjT6JNLWPHSj_iySo98a0uNuTVSk1r8,1511
171
172
  prefect/settings/models/cli.py,sha256=mHB-BHBVO9qfQcr9uHbBmU6MMDLlLUUDxjyaRz7v1ls,958
172
173
  prefect/settings/models/client.py,sha256=Ua18wKXWrNi0Ktb6cd42kdSOQYhFnObwTHTc3QDYg2k,2985
173
174
  prefect/settings/models/cloud.py,sha256=7-WtW0rGPMOuG1O9Lwv_H_keEldXPo9YGHmoGwCXpeg,1767
174
175
  prefect/settings/models/deployments.py,sha256=ui4xqT7N-6B_gMnGnkJ8Ys0hzdfNTOtlcwzAQo2LlQI,1235
175
- prefect/settings/models/experiments.py,sha256=AlSj4ItrVDGLvTTXJVtDmzlYEiCBjrGKNFs7VGqH1fo,728
176
+ prefect/settings/models/experiments.py,sha256=CXezPxuyV4pTxvQ9xroUmpEudXbDgUv-Y56A16307Wc,862
176
177
  prefect/settings/models/flows.py,sha256=IrW3cFQJnu-31cXvSihCl6naRhmZc2eFqSgqdnNLUzg,1056
177
178
  prefect/settings/models/internal.py,sha256=mJ7h3d_WHw-0FBVhIbDHPjYI041YtuReD1bZO98G1OU,612
178
- prefect/settings/models/logging.py,sha256=gzTjdiuh2tMdtorW86rZIp4n66jJLh3oYh5uRLi8DD4,4473
179
+ prefect/settings/models/logging.py,sha256=YxD8fOcWOROUrq6dH6EIadJwYL7LlLSUGWlHVLn-l3Y,4560
179
180
  prefect/settings/models/results.py,sha256=lU-3oVlgxGradSB1EYU-VZogK-rzE5LDw_wOcKhJp4M,1397
180
- prefect/settings/models/root.py,sha256=MkprdKVpWb0M2rR08nvUtfkSPGDIp_a3cz6cUKKmIWU,16381
181
+ prefect/settings/models/root.py,sha256=7PC_Oiz7SYM3Ea3rqn_XnozvdivNq3rKc2dEcjTA5L4,16377
181
182
  prefect/settings/models/runner.py,sha256=TOYDnmYmv6Ec5Ma6Vii-rPUTeMGszDluecLwy65Gnm4,1644
182
183
  prefect/settings/models/tasks.py,sha256=qXLWJIVZT4L1GQcaCD_NUJktpB5WYOxthOq_PDLdj20,3309
183
184
  prefect/settings/models/testing.py,sha256=r6KCYi8nvflCEwRFqS2T5J9baMHtVJfW0Q8EWARu1PQ,1733
@@ -187,13 +188,18 @@ prefect/settings/models/server/api.py,sha256=EKpt31hlX9dwrrsW-QztuJCHfUXqeWSz5xA
187
188
  prefect/settings/models/server/database.py,sha256=0eerMb05A9wD9_C4SefTzVvmba3rW18K2eteL2IcXLg,7201
188
189
  prefect/settings/models/server/deployments.py,sha256=_GcxGOsMMrCzGEnOwC2i6JQW77h2tbyAdBJzAigHDas,778
189
190
  prefect/settings/models/server/ephemeral.py,sha256=WxSpF-z9iDKAEjvcqrA5aggLEPRbl_ERocoLxPlu424,923
190
- prefect/settings/models/server/events.py,sha256=RRpwduwrV4jysp-AfxKhLnM0JEYCG4gxT7ls15yfJJg,5148
191
+ prefect/settings/models/server/events.py,sha256=UJ-cQdTvkYOxU5WCTx3EebTv01E_gDaA-3ni8jwyjN8,5147
191
192
  prefect/settings/models/server/flow_run_graph.py,sha256=MCKKV91-I6AOAGJ9ieTvHIMqH5JEri7cfuDbadEFu4w,1026
192
193
  prefect/settings/models/server/root.py,sha256=uhryCvrk6oGAu8S3GGZNc3IaxXyd_dFnsKfi1q8En_U,5128
193
194
  prefect/settings/models/server/services.py,sha256=xDQyRUh18mU_cTgp1Q_BwKpdeZHKLSh2o_YNNXvzsoo,16748
194
195
  prefect/settings/models/server/tasks.py,sha256=YzfRTcmiYnD_INezR_29rvO0M_YbAqT1by7kiK2KP60,2814
195
196
  prefect/settings/models/server/ui.py,sha256=6cTSC2RQsS4c2HpB1Se6qRms4RMEKSsaI40T2CTkobg,1780
196
- prefect/types/__init__.py,sha256=A714iHFE9makA5LiAKd0Y5wfdb7m13gBwvrpQzdLVgs,3647
197
+ prefect/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
198
+ prefect/telemetry/bootstrap.py,sha256=oOdEjIHDQHANviyP2ZKpz9B4yBq832f9y1PKwAQ5-ro,987
199
+ prefect/telemetry/instrumentation.py,sha256=S7IRYqKXaixAt-W5oxDO4CqkyXc3dy4ATgBmuindJIM,4241
200
+ prefect/telemetry/logging.py,sha256=yn5D4D2GGRrAv0y8wlHPN7PZDmQucGjQT_YauK9M9Yo,727
201
+ prefect/telemetry/processors.py,sha256=5vxjk9m-0FxqGDmVySY_u4zceZ9mwmTPiSFuCDOyGWA,2006
202
+ prefect/types/__init__.py,sha256=oabzKSvuT7tKBX6OiBev40wUtN1JstJT2L7ocpEUZ14,3640
197
203
  prefect/types/entrypoint.py,sha256=2FF03-wLPgtnqR_bKJDB2BsXXINPdu8ptY9ZYEZnXg8,328
198
204
  prefect/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
205
  prefect/utilities/annotations.py,sha256=Ocj2s5zhnGr8uXUBnOli-OrybXVJdu4-uZvCRpKpV_Q,2820
@@ -224,14 +230,14 @@ prefect/utilities/schema_tools/__init__.py,sha256=KsFsTEHQqgp89TkDpjggkgBBywoHQP
224
230
  prefect/utilities/schema_tools/hydration.py,sha256=k12qVCdLLrK-mNo1hPCdhxM5f_N14Nj0vJdtiWYWffk,8858
225
231
  prefect/utilities/schema_tools/validation.py,sha256=2GCjxwApTFwzey40ul9OkcAXrU3r-kWK__9ucMo0qbk,9744
226
232
  prefect/workers/__init__.py,sha256=8dP8SLZbWYyC_l9DRTQSE3dEbDgns5DZDhxkp_NfsbQ,35
227
- prefect/workers/base.py,sha256=jAea-LuUYjLJ9--Y4oD82lubIgAakK33Pa3bvcotASs,45381
233
+ prefect/workers/base.py,sha256=ChdMddrKvpmk08m-UQXiP2R4xCIfsiej-0zD2jt9llc,47795
228
234
  prefect/workers/block.py,sha256=BOVVY5z-vUIQ2u8LwMTXDaNys2fjOZSS5YGDwJmTQjI,230
229
235
  prefect/workers/cloud.py,sha256=BOVVY5z-vUIQ2u8LwMTXDaNys2fjOZSS5YGDwJmTQjI,230
230
236
  prefect/workers/process.py,sha256=tcJ3fbiraLCfpVGpv8dOHwMSfVzeD_kyguUOvPuIz6I,19796
231
237
  prefect/workers/server.py,sha256=lgh2FfSuaNU7b6HPxSFm8JtKvAvHsZGkiOo4y4tW1Cw,2022
232
238
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
233
- prefect_client-3.1.0.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
234
- prefect_client-3.1.0.dist-info/METADATA,sha256=c3Er1Cd_i5J7vmVAyc8IL9iE3Dwm34D5Y3q3kGNGcUI,7338
235
- prefect_client-3.1.0.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
236
- prefect_client-3.1.0.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
237
- prefect_client-3.1.0.dist-info/RECORD,,
239
+ prefect_client-3.1.1.dist-info/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
240
+ prefect_client-3.1.1.dist-info/METADATA,sha256=OlGsslMyT1YoImmLFDnko6XXPcvfqJV__WBbd6KClG4,7338
241
+ prefect_client-3.1.1.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
242
+ prefect_client-3.1.1.dist-info/top_level.txt,sha256=MJZYJgFdbRc2woQCeB4vM6T33tr01TmkEhRcns6H_H4,8
243
+ prefect_client-3.1.1.dist-info/RECORD,,